整个过程
- 预处理器:将.c 文件转化成 .i 文件,使用的 gcc 命令是:gcc –E,对应于预处理命令 cpp;
- 编译器:将.c/.h 文件转换成.s 文件,使用的 gcc 命令是:gcc –S,对应于编译命令 cc –S;
- 汇编器:将.s 文件转化成 .o 文件,使用的 gcc 命令是:gcc –c,对应于汇编命令是 as;
- 链接器:将.o 文件转化成可执行程序,使用的 gcc 命令是:gcc,对应于链接命令是 ld;
- 加载器:将可执行程序加载到内存并进行执行,loader 和 ld-linux.so。
过程详解
1. 预编译
在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。
- 宏定义指令,如 #define a b 对于这种伪指令,预编译所要做的是将程序中的所有 a 用 b 替换,但作为字符串常量的 a 则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。
- 条件编译指令,如 #ifdef,#ifndef,#else,#elif,#endif 等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
- 头文件包含指令,如 #include “FileName” 或者 #include 等。该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
- 特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的 LINE 标识将被解释为当前行号(十进制数),FILE 则被解释为当前被编译的 C 源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
头文件的目的主要是为了使某些定义可以供多个不同的 C 源程序使用,这涉及到头文件的定位即搜索路径问题。头文件搜索规则如下:
- 所有 header file 的搜寻会从 - I 开始
- 然后找环境变量 C_INCLUDE_PATH,CPLUS_INCLUDE_PATH,OBJC_INCLUDE_PATH 指定的路径
- 再找默认目录(/usr/include、/usr/local/include、/usr/lib/gcc-lib/i386-linux/2.95.2/include……)
2. 编译
通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
3. 汇编
汇编器 (as) 把汇编语言代码翻译成目标机器指令(.o)。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
- 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
- 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
4. 链接
将有关的目标文件彼此相连接生成可加载、可执行的目标文件。链接器的核心工作就是符号表解析和重定位。
4.1 链接的时机
- 编译时,就是源代码被编译成机器代码时(静态链接器负责);
- 加载时,也就是程序被加载到内存时(加载器负责);
- 运行时,由应用程序来实施(动态链接器负责)。
4.2 链接的作用
- 使得分离编译成为可能;
- 动态绑定(binding): 使定义、实现、使用分离
4.3 静态库搜索路径
- gcc 先从 - L 寻找;
- 再找环境变量 LIBRARY_PATH 指定的搜索路径;
- 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的。
4.4 动态库搜索路径
- 编译目标代码时指定的动态库搜索路径 -L;
- 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径;
- 配置文件 /etc/ld.so.conf 中指定的动态库搜索路径;
- 默认的动态库搜索路径 /lib /usr/lib/ /usr/local/lib
4.5 静态链接(编译时)
链接器将函数的代码从其所在地(目标文件或静态链接库中)拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
为创建可执行文件,链接器必须要完成的主要任务:
- 符号解析:把目标文件中符号的定义和引用联系起来;
- 重定位:把符号定义和内存地址对应起来,然后修改所有对符号的引用。
重定位
让我们结合具体的 CPU 指令来了解这个过程。假设我们有个全局变量叫做 var,它在目标文件 A 中。我们在目标文件 B 里面要访问这个全局变量,比如我们在目标文件 B 里面有这么一条指令:movl $0x2a, var
这条指令就是给这个 var 变量赋值 0x2a,相当于 C 语言中的语句 var = 42。然后我们编译目标文件 B,得到这条指令机器码
由于在编译目标文件 B 的时候,编译器并不知道变量 var 的目标地址,所以编译器在没法确定地址的情况下,将这条 mov 指令的目标地址设为 0,等待链接器在将目标文件 A 和 B 链接起来的时候再将其修正。假设 A 和 B 链接后,变量 var 的地址确定下来为 0x1000,那么链接器将会把这个指令的目标地址部分修改成 0x10000。这个地址修正的过程也叫做重定位 (Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。
每个目标文件除了拥有自己的数据和二进制代码外,还提供了三个表:未解决符号表、导出符号表、地址重定向表。
-
未解决符号表提供了所有在该编译单元里引用但是定义并不是在本编译单元的符号以及其出现的地址;
- 导出符号表提供了本编译单元具有定义,并且愿意提供给其他单元使用的符号及其地址;
- 地址重定向表提供了本编译单元所有对自身地址的引用的记录;
编译器将 extern 声明的变量置入未解决符号表,而不置入导出符号表;—- 外部链接
编译器将 static 声明的全局变量不置入未解决符号表,也不置入导出符号表,因此其他单元无法使用;—- 内部链接
普通变化及其函数被置入导出符号表;
4.6 动态链接(加载、运行时)
在此种方式下,函数的定义在动态链接库或共享对象的目标文件中。在编译的链接阶段,动态链接库只提供符号表和其他少量信息用于保证所有符号引用都有定义,保证编译顺利通过。动态链接器 (ld-linux.so) 链接程序在运行过程中根据记录的共享对象的符号定义来动态加载共享库,然后完成重定位。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
各种文件
ELF Executable
ELF exectuable File | 内容 |
---|---|
文件头 | 描述文件属性, 段表, 重定位表 |
代码段 .code .text | 源代码编译后的机器指令,程序的指令 |
数据段.data | 已初始化的全局变量, 局部静态变量 |
.bss | 未初始化的全局变量, 局部静态变量 |
.symtab | 存放在程序中定义和引用的函数和全局变量的信息符号表 |
目标文件
- 可重定位 (Relocatable) 文件:由编译器和汇编器生成,可以与其他可重定位目标文件合并创建一个可执行或共享的目标文件;
- 共享 (Shared) 目标文件:一类特殊的可重定位目标文件,可以在链接 (静态共享库) 时加入目标文件或加载时或运行时 (动态共享库) 被动态的加载到内存并执行;
- 可执行 (Executable) 文件:由链接器生成,可以直接通过加载器加载到内存中充当进程执行的文件。
静态库(Archive FIle)
多个.o 文件的集合.Linux 中默认后缀是.a, 静态库的的.o 没有进行链接,只是.o 的集合。
共享目标文件
包含了代码和数据,可以在两种情况下使用
- 链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件
- 动态链接器可以将几个这种共享目录文件与可执行文件结合,作为进程映像的一部分来运行
GNU 工具
gnu 下提供了很多工具来帮助处理目标文件:
- AR : 创建静态库,插入、删除、列出和提取成员;
- STRINGS : 列出目标文件中所有可以打印的字符串;
- STRIP : 从目标文件中删除符号表信息;
- NM : 列出目标文件符号表中定义的符号;
- SIZE : 列出目标文件中节的名字和大小;
- READELF : 显示一个目标文件的完整结构,包括 ELF 头中编码的所有信息。
- OBJDUMP : 显示目标文件的所有信息,最有用的功能是反汇编.text 节中的二进制指令。
- LDD : 列出可执行文件在运行时需要的共享库。