1.重定位

在完成空间与地址的分配步骤之后,链接器就进入了符号解析与重定位的步骤,这也就是静态链接的核心作用;
在分析符号解析和重定位之前,首先让我们来看看“a.o”里面是怎么使用这两个外部符号,也就是说我们在“a.c”源程序里面使用了“shared”变量和“swap”函数,那么编译器在将“a.c”编译成指令时,它如何访问“shared”变量?如何调用“swap”函数?
使用objdump的-d参数可以看到“a.o”的代码反汇编结果:
objdump -d a.o
符号解析与重定位-编程知识网

我们知道在程序的代码里面使用的都是虚拟地址,在这里也可以看到“main”的起始地址以0x00000000开始,等到空间分配完成之后,各个函数才回确定自己在虚拟地址空间中的位置;
我们可以很清楚地看见“a.o”的反汇编结果中,“a.o”共定义了函数main,这个函数占用了0x33个字节,共17条指令;最左边的那列是每条指令的偏移量,每一行代表一条指令(有些指令的长度很长,如偏移0x18的mov指令,它的二进制显示占据了两行)。我们已经用粗体标出了两个引用“shared”和“swap”的位置,对于“shared”的引用是一条“mov”指令,这条指令总共8个字节,它的作用是将“shared”的地址赋值给ESP寄存器+4的偏移地址中去,前面4个字节是指令码,,后面4个字节是“shared”的地址,我们只关心后面的4个字节部分,如图4-4:
符号解析与重定位-编程知识网

当源代码“a.c”在被编译成目标文件时,编译器并不知道“shared”和“swap”的地址,因为它们定义在其他目标文件中,所以编译器就暂时把地址0看成“shared”的地址,我们可以看到这条“mov”指令中,关于“shared”的地址部分为“0x00000000”。
另一个偏移是0x26的指令的一条调用,它其实就是表示对swap函数的调用,如4-5所示:
符号解析与重定位-编程知识网

这条指令共5个字节,前面的0xE8是操作码(intel从IA-32手册可以查阅到),这条指令是一条近址相对位移调用指令(Call near),后面的4个字节就是被调用函数的相对于调用指令的下一条指令的偏移量。在没有重定位之前,相对偏移被置为0xFFFFFFFC(小端),它是常量“-4”的补码形式。
让我们来仔细看看这条指令的含义。紧跟在这条call指令后面的那条指令为add指令,add指令的实际调用地址为0x27。我们可以看到0x27存放着并不是swap函数的地址,跟前面的“shared” 一样,“0xFFFFFFFC”只是一个临时的假地址,因为在编译的时候,编译器并不知道“swap”的真正地址。
编译器把这两条指令的地址部分暂时用地址“0x00000000”和“0xfffffffc”代替着,把真正的地址计算工作留给了链接器。我们通过前面的空间和 地址分配可以得知,链接器在完成地址和空间分配之后就已经确定了所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个须要重定位的指令进行地位修正。我们用objdump来反汇编输出程序“ab”的代码段,可以看到main函数的两个重定位入口都已经被修正到正确的位置:
符号解析与重定位-编程知识网

经过修正之后,“shared”和“swap”的地址分别是0x08049108和0x00000009。关于“shared”很好理解,因为“shared”的变量的地址的却是0x08049108。对于“swap”来说稍显晦涩。我们前面介绍过,这个“call”指令的下一条指令是一条近址相对位移调用指令,他后面跟的是调用指令的下一条指令的偏移量。

2. 重定位表

那么链接器是怎么知道哪些指令是要被调整的呢?这些指令的哪些部分要被调整?怎么调整?比如上面例子中“mov”指令和“ca”指令的调整方式就有所不同。事实上在ELF文件中,有一个叫重定位表( Relocation Table)的结构专门用来保存这些与重定位相关的信息,我们在前面介绍ELF文件结构时已经提到过了重定位表,它在ELF文件中往往是个或多个段。

对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段,所以其实重定位表也可以叫重定位段,我们在这里统一称作重定位表。比如代码段“text”如有要被重定位的地方,那么会有一个相对应叫“, rel text”的段保存了代码段的重定位表;如果代码段“data”有要被重定位的地方,就会有一个相对应叫“ rel. data"”的段保存了数据段的重定位表。我们可以使用 objdump来查看目标文件的重定位表。
符号解析与重定位-编程知识网

这个命令可以用来查看“ao”里面要重定位的地方,即“a.o”所有引用到外部符号的地址。每个要被重定位的地方叫一个重定入口( Relocation Entry,我们可以看到“a.o"里面有两个重定位入口。重定位入口的偏移(Oset)表示该入口在要被重定位的段中的位置,“ RELOCATION RECORDS FOR txt”表示这个重定位表是代码段的重定位表,所以偏移表示代码段中须要被调整的位置。对照前面的反汇编结果可以知道,这里的0xlc和0x27分别就是代码段中“mov”指令和“call”指令的地址部分.

对于32位的 Intel x86系列处理器来说,重定位表的结构也很简单,它是一个El32 Rel 结构的数组,每个数组元素对应一个重定位入口。Ef32Rel的定义如下:

typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
}

符号解析与重定位-编程知识网

3.符号的解析

在我们通常的观念里,之所以要链接是因为我们目标文件中用到的符号被定义在其他目标文件,所以要将它们链接起来。比如我们直接使用ld来链接“a.o”,而不将“b.o”作为输入。链接器就会发现 shared和swap两个符号没有被定义,没有办法完成链接工作:

符号解析与重定位-编程知识网

这也是我们平时在编写程序的时候最常碰到的问题之一,就是链接时符号未定义。导致这个问题的原因很多,最常见的一般都是链接时缺少了某个库,或者输入目标文件路径不正确或符号的声明与定义不一样。所以从普通程序员的角度看,符号的解析占据了链接过程的主要内容.。

通过前面指令重定位的介绍,我们可以更加深层次地理解为什么缺少符号的定义会导致链接错误。其实重定位过程也伴随着符号的解析过程,每个目标文件都可能定义一些符号也可能引用到定义在其他目标文件的符号。重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器须要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。

比如我们查看“a.o”的符号表:

符号解析与重定位-编程知识网

GLOBAL”类型的符号,除了“main”函数是定义在代码段之外,其他两个“ shared和“swap”都是“UND”,即“ undefined”未定义类型,这种未定义的符号都是因为该目标文件中有关于它们的重定位项。所以在链接器扫描完所有的输入目标文件之后,所有这些未定义的符号都应该能够在全局符号表中找到,否则链接器就报符号未定义错误。