链接器一般采用两步链接的方法:
(1)空间与地址分配:扫描所有输入的目标文件,然后将符号表中所有的符号定义与符号引用收集起来放到全局部符号表中。获取输入目标文件段的长度,并将它们合并,计算出输出文件中各个段合并后的长度与位置,然后建立映射关系。
(2)符号解析与重定位:读取输入文件中段的数据、重定位信息,进行符号解析与重定位、调整代码中的地址。
1.空间与地址分配
(1)相似段合并
对于输入的多个目标文件,链接器一般采用“相似段合并”的方法将相同性质的段合并到一起,如下图:
(2)符号地址的确定
当合并相似段之后,链接器开始计算各个符号的虚拟地址,由于各个符号在段内的相对位置是固定的,所以链接器只需要给每一个符号加上一个偏移量,使得它们能够调整到正确的虚拟地址上。
2.符号解析与重定位
(1)重定位
如果当前目标文件中有使用外部符号,那么在当前目标文件中的外部符号的地址就需要进行重定位。重定位的大概思路如下:
在位进行空间分配之前,目标文件代码段中的起始地址以0X00000000起始,等到空间分配完成以后,各个符号才会确定自己在虚拟地址空间中的位置。所以在链接器执行工作之前,相关指令的地址是由编译器暂时用某些地址带代替着,等待链接器开始工作后,链接器就会完成地址和空间分配之后确定符号的虚拟地址,那么链接器就可以根据符号的虚拟地址对每一个需要重定位的指令进行地址修正。
(2)重定位表
重定位表用来保存与重定位相关的信息。它在ELF文件中有一个或者多个。
比如:代码段有要被重定位的地方,那马就会有一个“.rel.data”的段保存数据段的重定位表。
对于32位的Intel x86系列的处理器来说,重定位表是一个Elf32_Rel结构的数组,每一个数组元素对应一个重定位入口。
typedef struct
{
Elf32_Addr r_offset;//重定位入口的偏移
Elf32_Word r_info;//重定位入口的类型和符号
}Elf32_Rel;
(3)符号解析
重定位的过程伴随着符号解析的过程。在重定位的过程中,每一个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址,这时候链接器就会查找所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
(4)指令的修正方式
通过objdump来查看目标文件的重定位表,例如:
上面的R_386_PC32是绝对寻址修正,R_386_PC32是相对寻址修正。
①绝对寻址修正:修正后的地址为该符号的实际地址。修正方法:S+A
②相对寻址修正:修正后的地址为符号距离被修正位置的地址差。修正方法:S+A-P
其中:
A:被存在被修正位置的值
P:被修正的位置(该值可以通过r_offset计算得到)
S:符号的实际地址。
3.COMMON块
由于存在同一个符号被定义在多个文件中,所以存在的一个问题是:多个文件中同一符号的类型不一致,主要有三种情况:
①两个或两个以上强符号类型不一致:这种情况链接器会报错。
②有一个强符号,其他是弱符号:这种情况以强符号的类型为标准。如果链接过程中弱符号的大小大于强符号,链接器会报出警告。
③两个或者两个以上弱符号类型不一致:这种情况以弱符号类型中所占字节最大的哪个为标准。
注:
①有的编译器将未初始化的全局变量标记为COMMMON的原因:如果一个未初始化的全局变量不是以common块的形式存在,那么就相当于一个强符号,如果其他文件中还有同一个变量的强符号定义,链接时就会发生符号重定义。而把它标记为COMMON,就是一个弱符号,如果其他文件也定义了同一个全局变量,但是类型不一样,那么链接后以最大所占字节的类型为标准在.bss段分配空间。
②使用“ 类型+变量名+__attribute__((nocommon));”来将一个未初始化的全局变量不以common块的形式存在。
4.C++相关问题
(1)重复代码消除
重复代码存在会出现:空间浪费、地址交易出错、指令运行效率较低的问题。
解决的方法:
一种是在链接的时候将相同的段合并。在GCC中把这种类似的需要在最终链接时合并的段叫“Link Once”;
另一种是将相同段丢弃。在VISUAL C++编译器中,它把这种类型的段叫做“COMDAT”,这种段的属性字段都有IMAGE_SCN_LNK_COMDAT这个标记,在链接器看到这个标记后,他就认为该段是COMDAT类型的,在链接的时候就会将重复段丢弃。
注:
函数级别链接:让所有函数单独保存在一个段里面,当链接器需要用到某个函数时,就将它合并到输出文件中,对于那些没有用的函数则将它们丢弃。
(2)ABI
不同编译器编译出来的目标文件进行连接,需要满足的条件是ABI相同。
ABI:二进制层面的接口
API:源码级别的接口
5.静态库链接
静态库可以简单地看成一组目标文件的集合。当进行目标文件链接的时候,需要将所有需要的目标文件全部连接进来,否则就会存在外部符号未定义的问题。所以ld链接器可以自动寻找需要的符号及它们所在的目标文件,将这个目标文件从“libc.a”中“解压”出来,最终将它们连接在一起成为一个可执行文件。
注:
①collect2是ld链接器的一个包装,他会调用ld链接器来完成目标文件的链接,然后在对链接结果进行一些处理。
②一个目标文件中指包含一个函数的原因:可以减少空间的浪费,让那些没有被用到的目标函数不被链接到最终的输出文件中。
6.链接过程控制
我们使用链接器提供的默认链接规则对目标文件进行连接,但是在一些特殊要求的程序,比如:操作系统内核或一些在没有操纵系统的情况下运行的程序,由于这些特殊的环境,就会对程序的各个段地址有特殊要求。
链接器一般都提供多种控制整个连接过程的方法以用来产生用户所需的文件,如下:
①使用命令行来给链接器指定参数,比如:ld -o、-e.
②将连接指令存放在目标文件里面,编译器经常会通过这种方法向链接器传递指令。
③使用连接控制脚本
7.BFD库
BFD库的目标是希望通过一种统一的接口来处理不同的目标文件格式。利用BFD库的最大好处是将编译器和链接器本身同具体的目标文件格式隔离开来,一旦我们需要支持一种新的目标文件格式,只需要在BFD库里面添加一种格式就可以,而不需要修改编译器和链接器。