9、链接阶段
链接是将各种代码和数据部分收集起来并合并为一个单一文件的过程,该文件最后被加载到存储器中并运行。
链接可以执行于编译时,由静态链接器完成;也可以执行于加载和运行时,由动态链接器完成。可以看出链接器在软件开发中扮演一个关键的角色,它使得分离编译成为可能。在我们开发一个大型程序的时候,通用的做法是将它分解为更小、更好管理的模块,独立地修改和编译这些模块,当改变其中的一个模块时只需简单地重新编译它,并重新链接应用而无须重新编译其他文件
链接是处在编译器、体系结构和操作系统的交叉点上,它要求理解代码生成、机器语言编程、程序实例化和虚拟存储器,刚好不落在某个通常的计算机系统领域,因而并不是一个描述很具体的议题。
链接过程
链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的、可执行的和共享的,如下:
l 可重定位目标文件:由各种不同的代码和数据节组成,其形式可以在编译时与其他可重定位目标文件合并起来,生成一个可执行目标文件
l 可执行目标文件:由各种段映像字节组成,其形式可以被直接拷贝到内存中并执行
l 共享目标文件:含特殊类型的可重定位目标文件,可以在加载或运行时被动态地加载到内存中并链接
一般来说,链接器程序ld将一系列可重定位目标文件和链接命令参数作为输入,处理目标文件的外部引用和符号重定位,最后生成一个完全链接的可以加载和运行的可执行目标文件。为了构造可执行文件,链接器主要完成两个工作:
l 符号解析:将目标文件中的每个符号引用绑定到一个唯一的定义中
l 重定位:确定每个符号的最终存储器地址,并修改对那些目标符号的引用
下面以现代Unix系统的格式:Executable and LinkableFormat-ELF文件讲解链接过程涉及到的几个重点
可重定位目标文件
编译和汇编阶段生成从地址0开始的可重定位目标文件,要记住目标文件其实是一个字节序列,一组字节块的集合,它定义了各种字节块。在Unix中利用READELF工具显示一个目标文件的完整结构,包括ELF头中编码的所有信息,各个节的信息。
执行命令readelf -ahello.o,典型的ELF可重定位目标文件结构如下:
l ELF头: 描述了有关目标文件的整体结构信息:文件类型(可重定位的,可执行的),数据表示方法(大小端表示),program header、section header等的起始地址、文件偏移大小、各段的节个数
l .text: 已编译程序的机器代码
l .rodata: 只读数据,如printf语句中的格式串
l .data: 已初始化的数据。注:局部C变量在运行时保存在栈中,既不出现在.data节中也不出现在.bss节中
l .bss: 未初始化的数据。占位符,区分初始化和未初始化变量是为了空间效率,它并不需要占据空间
l .symtab: 符号表,存放在程序中定义和引用的符号信息(函数和全局变量)
l .rel.text: 代码重定位信息,合并多目标文件时用于.text节的重定位
l .rel.data: 数据重定位信息,合并多目标文件时用于.data节的重定位(被模块引用或定义的任何全局变量)
l .debug: 只有以-g选项调用gcc才能得到这样调试符号表,用于记录定义和引用的局部变量、全局变量、类型定义等条目
l .line: 只有以-g选项调用gcc才能得到一个源程序的行号与.text节中机器指令间的映射
l .strtab: 包含.symtab和.debug节中的符号表,包含源程序代码和行号、局部符号和数据结构描述信息
符号解析和重定位
每个可重定位目标文件m均有一个符号表,它包含m所定义和引用的符号信息,根据ld的上下文,有3种不同的符号
l 由m定义并能被其他模块引用的全局符号,比如非静态的C函数,不带C static属性的全局变量
l 由其他模块定义并被模块m引用的全局符号,称为外部符号,对应于定义在其他模块中的C函数和变量
l 只被模块m定义和引用的本地符号,比如带static属性的C函数和全局变量
如下为main.c的程序代码,并运行如下命令,它将显示该目标文件main.o的符号表信息
void swap();
int buf[2]={1,2};
int main(){
swap();
return 0;
}
#gcc –c main.c
#readelf –s main.o
可以看出main.o的符号表有11个条目,Bind字段表示符号是本地的还是全局的,Ndx字段表示每个节的整数索引,每个符号都必须和目标文件中的某个节关联,1表示.text节,3表示.data节,UND表示本模块文件未定义的符号,ABS表示不该被重定位的符号,COM表示还未被分配位置的未初始化数据。最后3个条目的意思是全局符号buf是一个位于.data节偏移为0处的8个字节目标,随后的是全局符号main的定义,它表示main是一个位于.text节偏移为0处的36字节函数,最后一个条目是来自对外部符号swap的引用。如果在程序中添加一个带static属性的静态变量,将会增加一个Binding字段为LOCAL,Ndx字段为3的符号a。
所以符号解析的目的就是将每个引用与可重定位目标文件中的符号表一个确定的符号定义联系起来。对于那些目标模块的本地符号的引用,符号解析是非常明确的,但对于不是在当前模块定义而被引用的全局符号(变量和函数名),规定函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号,使用以下规则来处理:
l 规则1:不允许有多个强符号,比如两个文件都定义了main函数这是不允许的
l 规则2:如果有一个强符号和多个弱符号,选择强符号
l 规则3:如果有多个弱符号,任意选择一个
重定位
链接器程序ld完成了符号解析这一步后,就可以开始重定位了。这一步将要为每个确定定义的符号分配运行时地址。它由两步组成:
l 重定位节和符号定义。 在这一步中,链接器将所有相同类型的节合并为同一类型的新聚合节,
l 重定位节中的符号引用。在这一步中,利用可重定位目标模块中的代码和数据的可重定位条目修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址
重定位的核心在于如何利用ELF重定位条目。每个条目的长度为8字节,其结构如下所示(参考linux v0.11内核中a.out.h中的设计):
struct relocation_info {
int r_address;
unsigned intr_symbolnum:24;
unsigned int r_pcrel:1;
unsigned int r_length:2;
unsigned int r_extern:1;
unsigned int r_pad:4;
};
该结构地址字段r_address是指可重定位项从代码段或数据段开始算起的偏移值。r_symbolnum指定符号表中的符号或段,2bit的长度字段r_length表示被重定位项的长度,0到3分别表示被重定位项的宽度是1、2、4、8字节,标志位r_pcel指出被重定位是一个PC相对的项,即作为一个相对地址用于指令当中。外部标志位r_extern控制r_symbolnum的含义,指明重定位的项是段还是符号。为0,表示普通的重定位项,对应r_symbolnum表示在某个段中寻址定位,为1,表示对外部符号的引用。
具体的重定位算法不打算详细描述,但注意如果要查看重定位过程中的汇编指令,可以利用OBJDUMP工具反汇编.text节中的二进制指令。
例如:#objdump –Smain.o
可执行目标文件
经过上面链接器程序ld的符号解析和重定位工作最后生成了一个可被加载到内存中运行的可执行目标文件。
可执行文件的格式与可重定位目标文件格式类似,ELF头部描述文件的整体结构,它还包括了程序的入口点地址,即程序运行时要执行的第一条执行的地址。.init节中定义了一个小函数,程序初始化时会调用它,.text,.rodata,.data节和之前的文件类似,除了这些节被重定位到他们最终的运行时存储器地址以外。其他的符号表和调试信息只有当编译程序开启了-g选项才有,但是并不加载到存储器中。
Gcc产生汇编文件,执行如下:#gcc –o p main.c
其中p就是汇编文件。通过objdump –S p 可以查看程序入口点。
或者readelf –a p | less
ELF可执行文件的段头部表,它描述了可执行文件的连续片(文件节)被映射到对应的运行时内存地址的关系。执行命令objdump -x p | less ,ProgramHeader部分对应了文件p的段头部表
注:off- 文件偏移; vaddr/paddr- 虚拟/物理地址; algin- 段对齐; filesz- 目标文件中的段大小; memsz- 存储器中的段大小; flags- 运行时许可