开始学习新的章节——链接。我们现在要研究在gcc调用的过程中,到底有哪些过程是被我们所忽略的,在编译的过程中它们又有着什么作用呢?
编译器驱动程序
我们以下面的这段代码为例,将以此来分析整个编译的过程
1 | //file 1 |
大多数编译系统都会提供一个编译驱动程序,在用户需要时调用语言处理器,编译器,汇编器,链接器。比如我们要用GNU编译系统进行编译时,我们就需要使用gcc
编译驱动:
1 | ylin@Ylin:~/Program/test$ gcc -Og -o test main.c sum.c |
但是实际上省略了很多中间过程并没有让我们看到,我们可以通过加入-v
参数来观察这个过程:

红色框出来的分别时ccl as ld
的调用,我们之后会分析这几个程序的作用。现在我们可以将从ASCIII源码到执行文件的编译驱动的过程总结一下:

首先预处理器cpp(预处理器实际上和编译器是集成在一起的)将ASCII源文件翻译成一个ASCII码的中间文件
1 | ylin@Ylin:~/Program/test$ cpp main.c -o main.i |
然后驱动程序运行C编译器ccl(这里我们使用gcc实现),将中间文件翻译成汇编语言:
1 | ylin@Ylin:~/Program/test$ gcc -S main.i |
接着驱动程序运行汇编器as,将main.s翻译成一个可重定位目标文件main.o:
1 | ylin@Ylin:~/Program/test$ as main.s -o main.o |
然后对sum.c进行同样的操作,得到sum.o。然后驱动程序运行链接器程序ld(在gcc -v
的过程中可以看到链接过程使用的是collect2,实际上是ld的封装用法),将main.o和sum.o以及一些必要的系统目标文件编译起来,创建一个可执行的目标文件:
1 | ylin@Ylin:~/Program/test$ ld -o test sum.o main.o |
最后当我们执行编译出来的test
程序时,shell会调用一个名为加载器loader的函数,将可执行文件中的代码和数据复制到内存,然后将控制转移到这个程序的开头:
1 | ylin@Ylin:~/Program/test$ ./test |
不过实际上,这些中间过程是被什么略的,其中生成的中间文件会被存放在\tmp
下,待编译结束后被清理。
静态链接
像ld
这样的静态连接器以一组可重定位目标文件个命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。输出的可重定位目标文件由各种不同的代码和数据节section
组成,每一节都是一个连续的字节序列。指令在一个节中,初始化了的全局变量在另一个节中,而未初始化的变量又在另一个节中…
为了构建一个可重定位文件,链接器要实现一下的功能:
符号解析:
目标文件会定义和引用符号。每个符号可能对应一个全局变量、一个函数或一个静态变量(static声明的变量),符号解析则是将每个符号的引用和它的定义联系起来。
重定位:
编译器和汇编器会生成从地址0开始的代码和数据节。链接器通过把每个符号的定义和一个内存位置关联起来,从而重定位这些节,然后修改这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位信息条目的详细指令给,进行这些重定位操作
之后我们会详细的分析这几个过程。实际上我们只需要清除,目标文件实际上就是字节块的集合。这些块中,有的包含程序代码,有的包含程序数据,其他的则包含引到链接器和加载器的数据结构。链接器负责将这些块连接起来,确定被连接块的运行时的位置,并且修改代码和数据块中的各种位置。
目标文件
目标文件有三种形式:
可重定位目标文件:
主要包含二进制代码和数据,其形式可以在链接时与其他的可重定位目标文件合并起来,创建一个可执行目标文件。
可执行目标文件:
包含二进制代码和数据,其形式可以直接被复制到内存中并执行。
共享目标文件:
一种特殊类型的可重定位目标文件,可以在加载或者运行时,被动态地加载进内存并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。
从技术上来说,一个目标模块就是一个字节序列,而一个目标文件就是一个以文件形式存储在磁盘中的目标模块。本质上它们是一样的。
不同的系统有不同的目标文件格式,如:
- Unix :a.out格式
- Windows :可移植可执行格式(PE)
- Mac :Mach-O格式
- 现代Linux/Unix :可执行可链接格式(ELF)
可重定位目标文件

这是一个典型的ELF可重定位目标文件的格式。我们从ELF头开始说起,我们可以使用readelf -h
来获取一个程序的ELF头信息:
1 | ylin@Ylin:~/Program/test$ readelf -h test |
以一个16字节长的字节序列Magic开始,这个序列用于系统判断是否为ELF文件格式(字节序和位宽),ELF头的其他部分则包含了帮链接器语法分析和解释目标文件的信息。其中包括
- ELF头的大小 –> Size of this header
- 目标文件的类型 –> Type
- 机器类型 –> Machine
- 节头部表的文件偏移 –> Start of section headers
- 节头部表条目的大小和数量 –> Number of section headers + Size of section headers
不同节的位置和大小是由节头部表描述的,其中目标文件中的每个节都有一个固定的条目(entry)。夹在ELF头和节头部表中间的都是节。一个典型的ELF可重定位目标文件包含以下的节:
.text
已编译程序的机器代码
.rodata
只读数据(常量数据,运行期间不可更改)
.data
已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,不被保存在节
.bss
未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据任何实际的空间,仅仅是一个占位符。区分已初始化和未初始化是为了空间效率,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始化为0
.symtab
一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。和使用
-g
编译出的程序得到的符号表信息不同,.symtab
中不包含局部变量的条目.rel.text
重定位节。一个
.text
节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。这个时候.text
中有哪些地址/符号需要在链接或加载时被修正的内容,就记录在.rel.text
中。.rel.data
数据的重定位节。记录
.data
中哪些变量/指针需要链接器修改地址.debug
调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有在使用gcc时加上
-g
选项,才可以得到这张表.line
原始C源程序中的行号和
.text
节中机器指令之间的映射。只有使用-g
时得到。.strtab
一个字符串表,内容包括其他节中的字符串信息。它自己不包含逻辑结构,只是被其他节按索引引用,用来存放符号名,段名,动态库名等文本。因此其他节只需要存放对应字符串的偏移索引就行了,真正的字符内容放在
.strtab
.
符号和符号表
符号
每个可重定位模块m都有一个符号表,它包含m定义和引用的符号的信息。在连接器的上下文中,有三种不同的符号:
- 由模块m定义并能被其他模块引用的全局符号。全局符号对应于非静态的C函数和全局变量。
- 由其他模块定义并被模块m引用的全局符号,称之为外部符号,对应于在其他模块中定义的非静态C函数和全局变量
- 只被模块m定义和引用的局部符号。它们对应于带
static
属性的C函数和全局变量。这些符号在模块m中任何位置可见但是,不能被其他模块引用。
链接器不关心本地局部变量。同样的.symtab
中的符号表也不包含对应于本地非静态程序变量的任何符号。这些符号在运行时由栈管理。不被链接器考虑。
不过定义为带有Cstatic
属性的本地过程变量时不在栈中管理的(使用static属性可以隐藏模块内部的变量和函数声明,就像class中使用private,来保护变量和函数只能被本地模块使用)。相反,编译器在.data
或.bss
中会为每个定义分配空间,并在符号表中创建一个有唯一名字的本地连接符号。比如:
1 | int f(){ |
此时编译器会向汇编器输出两个不同名字的局部链接符号。比如,可以用x.1
表示函数f中的定义,用x.2
表示函数g中的定义。
符号表
符号表是由汇编器构造的,使用的是编译器给出的符号。ELF符号表被存放在.symtab
中。这个符号表包含着一个条目的数组,其条目结构如下:
1 | typedef struct{ |
它们的作用分别是:
- name:字符串表中的字节偏移,指向符号的字符串名字
- type:表示符号的种类(数据/函数)
- binding:表示符号的可见性(本地/全局)
- reverse:暂时无用,留作数据对齐。
- section:节头部表的索引,用于定位节
- value:指定节中的偏移,相当于一个绝对运行时的地址
- size:目标的大小(字节大小)
每个符号都会被分配到目标文件的某个节中。由section
字段表示,该字段也是一个到节头部表的索引。有三个特殊的伪节,它们在节头部表中并没有条目:
- ABS,表示符号值是绝对的,不应该被重定位
- UNDEF,表示符号未定义,就是在本模块中使用,但是在其他地方定义的符号。
- COMMON,表示未被分配位置的未初始化的数据目标 > 只有在可重定位目标文件中才有伪节,可执行目标文件中没有
对于COMMON符号,value给出的是对齐要求,size给出的是最小的大小。
这么一看COMMON和.bss
似乎差不多。现代的汇编器根据以下规则来将符号分配到COMMON和.bss
中:
1
2COMMON 未初始化的全局变量
.bss 未初始化的静态变量,以及初始化为0的全局变量和静态变量
之后我们会详细解释这个设置的原因。