4.1 空间地址分配
链接过程,就是将几个目标文件加工后合并成一个输出文件。那么,对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件呢?
方法一:按序叠加
就是将各个目标文件直接依次合并,但是这样做会产生一个问题:当目标文件很多的情况下,会产生大量零散的段,因为每个段都必须要有一定的地址和空间对齐要求(比如对于x86硬件来说,段的装载地址和空间的对齐单位是页,也就是4096字节),因此这种做法非常浪费空间。
方法二:相似段合并
就是将同样的段放在一起,每个目标文件的.text段合并,接着是.data段,.bss段等。
我们都知道,.bss段在目标文件和可执行文件时并不占用文件的空间,但是装载时占用地址空间,因此链接器要将.bss段合并,并且分配虚拟空间。
这里的“空间分配”到底是什么空间?
还有文件空间和地址空间有什么不同?
“链接器为目标文件分配地址和空间”,“地址和空间”有两个含义:一时在输出的可执行文件中占用的空间,也就是文件空间。而是装载后的虚拟地址中的虚拟地址空间,也就是地址空间。
现在的链接器空间分配都采用相似段合并的方法,一般都使用一种叫两步链接的方法:
第一步:空间与地址分配。扫描所有的目标文件,获得各个段的长度和位置,收集各目标文件中的符号表中的符号定义和符号引用,放到一个全局符号表。
第二步:符号解析与定位。使用步骤一中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析和重定位、调整代码中的地址。这一步是链接的核心,尤其是重定位过程。
举例:
a.c
extern int shared;
int main()
{
int a = 100;
swap (&a, &shared);
}
b.c
int shared = 1;
void swap (int* a, int* b)
{
*a ^= *b ^= *a ^= *b;
}
如上,将a.c和b.c生成的目标文件a.o和b.o链接。
如上所示,分别是a.o b.o ab的段信息。首先,VMA表示虚拟地址,LMA表示加载地址,正常情况下这两个值应该是一样的。
我们可以看到,在加载之前,VMA的值都为0,等到链接之后,可执行文件ab中的各个段都被分配到了相应的虚拟地址。
4.2 符号解析与重定位
1、重定位
上图是a.o文件的反汇编结果,我们可以看到,因为是虚拟地址,因此main的起始地址为0x0000 0000 0000 0000开始,a.0只定义了一个函数main,这个函数占0x26个字节,共有11条指令。另外,偏移为0x20的指令,是调用swap函数的指令,e8位操作码,后面的四位是swap函数的地址。因为此时还未分配地址,所以暂用00 00 00 00 代替。
上图是可执行文件ab的反汇编结果,可以看到swap函数已经被分配了地址。(Call指令是一条近址相对位移调用指令,后面跟的是调用指令的下一条指令的偏移量,在这里,下一条指令是leave,地址为0x40010d,加上偏移量3,结果是0x400110,刚好是swap函数的地址。)
2、重定位表
ELF文件中有重定位表,用来描述如何修改相应的段里的内容。
上图是a.o文件的重定位信息,即其所有引用到外部符号的地址。每个要被重定位的地方叫一个重定位入口;重定位入口的偏移,表示该入口在要被重定位的段中的位置;RELOCATION RECORDS FOR [.text]表示这个重定位表是用于代码段的。偏移0x14和0x21分别就是代码段中mov指令和call指令的地址部分。
3、符号解析
在写程序时,我们经常会碰到链接是符号为定义的问题。如上如所示,在未链接时,shared和swap都是“UND”,表示未定义类型,这种未定义的符号都是因为该目标文件中有关于它们的重定位项,所以在链接器扫描完所有的目标文件之后,这些未定义的符号都应该能够在全局符号表中找到,否则链接器就会报符号未定义错误。
4.5 静态库链接
静态链接可以简单看成是一组目标文件的集合。
比如在C语言的运行库中,包含了很多跟系统功能相关的代码,编译完成之后有很多目标文件,比如printf.o,scanf.o, date.o, time.o…… 把这些零散的目标文件直接提供给库的使用者,很大程度上会造成文件传输管理不便,因此通常使用“ar”压缩程序将这些目标文件压缩在一起。我们也可以使用“ar”工具来查看一个库包含了哪些目标文件。
ar –t *.a