符号解析与重定位
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”指令的下一条指令是一条近址相对位移调用指令,他后面跟的是调用指令的下一条指令的偏移量。