大家肯定都知道计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类,一种是编译型语言,例如C,C++,Java,另一种是解释型语言,例如Python、Ruby、MATLAB 、JavaScript。 本文将介绍如何将高层的C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程,包括四个步骤:' f$ y- D. q* A6 s5 N
GCC 工具链介绍 通常所说的GCC是GUN Compiler Collection的简称,是Linux系统上常用的编译工具。GCC工具链软件包括GCC、Binutils、C运行库等。 GCC GCC(GNU C Compiler)是编译工具。本文所要介绍的将C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程即由编译器完成。 Binutils 一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。这一组工具是开发和调试不可缺少的工具,分别简介如下:
C运行库 C语言标准主要由两部分组成:一部分描述C的语法,另一部分描述C标准库。C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的printf函数便是一个C标准库函数,其原型定义在stdio头文件中。 C语言标准仅仅定义了C标准库函数原型,并没有提供实现。因此,C语言编译器通常需要一个C运行时库(C Run Time Libray,CRT)的支持。C运行时库又常简称为C运行库。与C语言类似,C++也定义了自己的标准,同时提供相关支持库,称为C++运行时库。 准备工作 由于GCC工具链主要是在Linux环境中进行使用,因此本文也将以Linux系统作为工作环境。为了能够演示编译的整个过程,本节先准备一个C语言编写的简单Hello程序作为示例,其源代码如下所示: #include <stdio.h> ' ~( u, D2 T. p3 i//此程序很简单,仅仅打印一个Hello World的字符串。 int main(void) {3 `7 v# y" _6 P& b- Z printf("Hello World! \n");9 q* t" Q/ m0 i, G return 0;$ x- A2 X' m+ G4 o. ]" { } 编译过程 1.预处理 预处理的过程主要包括以下过程:
// GCC的选项-E使GCC在进行完预处理后即停止5 T3 b3 \3 m% T/ }% L/ `# y hello.i文件可以作为普通文本文件打开进行查看,其代码片段如下所示: // hello.i代码片段2 c4 O/ W8 k0 K8 t" M, R4 d" {& yextern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));9 W& g+ g9 a+ _+ _ # 942 "/usr/include/stdio.h" 3 44 a) [( B+ P' {! ~$ B, k2 ? # 2 "hello.c" 2 8 b$ p1 u) T: G3 E# g7 H # 3 "hello.c"9 ?3 W& C8 v& }' b- l x6 w2 ^ int main(void) { printf("Hello World!" "\n");2 \" _* e3 Y7 {: v7 D5 j( B return 0; }/ B" s- f& R e0 F 2.编译 编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。 使用gcc进行编译的命令如下: $ gcc -S hello.i -o hello.s // 将预处理生成的hello.i文件编译生成汇编程序hello.s8 {& v3 M5 t$ ~: G+ d! P! f// GCC的选项-S使GCC在执行完编译后停止,生成汇编程序/ ?6 Z' x. o1 B h" W9 y 上述命令生成的汇编程序hello.s的代码片段如下所示,其全部为汇编代码。 // hello.s代码片段main: .LFB0: m7 \! q+ L$ o3 j! ] .cfi_startproc( d+ V- ?' |* s( k4 L1 L, C" t$ q+ M pushq %rbp9 b9 E0 ]# W6 j) C0 u .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %edi call puts movl $0, %eax, H: `0 s, o8 s* Z, n2 L popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc ) J J/ E" p' D$ J4 M- w1 \( [ 3.汇编 汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用Binutils中的汇编器as根据汇编指令和处理器指令的对照表一一翻译即可。 当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。 使用gcc进行汇编的命令如下: $ gcc -c hello.s -o hello.o // 将编译生成的hello.s文件汇编生成目标文件hello.o7 u D& S b$ x A" Z Z D2 w// GCC的选项-c使GCC在执行完汇编后停止,生成目标文件1 E5 C9 C# S: j; n //或者直接调用as进行汇编: ?+ [) p( K; z0 @8 D0 ?. ? $ as -c hello.s -o hello.o //使用Binutils中的as将hello.s文件汇编生成目标文件( e$ |9 S# W: x& ?+ w; a 注意:hello.o目标文件为ELF(Executable and Linkable Format)格式的可重定向文件。 4.链接 链接也分为静态链接和动态链接,其要点如下:
2 T7 N/ M7 C) y- l; ~4 I: f' E 由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如libtest.a和libtest.so,gcc链接时默认优先选择动态库,会链接libtest.so,如果要让gcc选择链接libtest.a则可以指定gcc选项-static,该选项会强制使用静态库进行链接。以Hello World为例:
链接器链接后生成的最终文件为ELF格式可执行文件,一个ELF可执行文件通常被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss等段。 分析ELF文件 1.ELF文件的段 ELF文件格式如下图所示,位于ELF Header和Section Header Table之间的都是段(Section)。一个典型的ELF文件包含下面几个段:
可以使用readelf -S查看其各个section的信息如下: $ readelf -S helloThere are 31 section headers, starting at offset 0x19d8:* L# ~ n2 T; [1 B2 \4 H7 r Section Headers:6 l4 o: A* }: ` [Nr] Name Type Address Offset& v; O2 g. }& Z. x/ l: M Size EntSize Flags Link Info Align( B4 ^3 r; r# j9 [3 f0 D [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0- c a" Y \3 [4 } ……1 P* T! @- i. D) U% P$ b# E [11] .init PROGBITS 00000000004003c8 000003c88 F8 X1 A6 d9 ?* |' d( G 000000000000001a 0000000000000000 AX 0 0 4 …… [14] .text PROGBITS 0000000000400430 00000430 0000000000000182 0000000000000000 AX 0 0 16 [15] .fini PROGBITS 00000000004005b4 000005b43 q2 _! S; ]! j/ s ……3 h9 Y6 ^6 B9 K# B3 U 2.反汇编ELF 由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法。 使用objdump -D对其进行反汇编如下: $ objdump -D hello…… 0000000000400526 <main>: // main标签的PC地址9 {7 v8 h0 c9 n4 T0 E //PC地址:指令编码 指令的汇编格式 400526: 55 push %rbp 400527: 48 89 e5 mov %rsp,%rbp0 q5 h6 Y- X7 V# D7 H# H# E: ` 40052a: bf c4 05 40 00 mov $0x4005c4,%edi- y7 P: \3 d% c, J/ M 40052f: e8 cc fe ff ff callq 400400 <puts@plt> 400534: b8 00 00 00 00 mov $0x0,%eax9 K, k* c* C$ \' k" o 400539: 5d pop %rbp' f9 d7 X3 L+ G4 \ 40053a: c3 retq 0 ]" ~/ I: m* k7 c 40053b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1); T Q- E/ N0 H3 z+ t ……. L! t/ H. z; R- X: F2 F 使用objdump -S将其反汇编并且将其C语言源代码混合显示出来: $ gcc -o hello -g hello.c //要加上-g选项" ]4 d- O' u3 ~7 D% m$ objdump -S hello4 r* I' B% V, H5 o …… 0000000000400526 <main>:1 C- _1 B0 h3 \0 X. a5 ^1 Z6 }( } #include <stdio.h> : Q5 o0 @* u2 S. ~7 @5 W% p7 `7 | int main(void)7 I, C# y* R: @% U- T4 ] { 400526: 55 push %rbp+ d" E% T; m( V) ]0 J3 T 400527: 48 89 e5 mov %rsp,%rbp1 D7 ~( a) e7 m7 s9 |8 t( Z printf("Hello World!" "\n"); 40052a: bf c4 05 40 00 mov $0x4005c4,%edi7 e' N, C! J7 q `/ K4 s 40052f: e8 cc fe ff ff callq 400400 <puts@plt>: m! t- U( t; l6 e. p8 s( h! H return 0; 400534: b8 00 00 00 00 mov $0x0,%eax }; F: _5 D3 { [: O9 V" X 400539: 5d pop %rbp b, ^8 e2 g; m 40053a: c3 retq 0 `& [5 L- A$ P% V5 Y h- m. f) g2 X3 h 40053b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) ……2 i; u# k8 C9 m p& Y/ d |