重定位

//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"编译成指令时,它如何访问"shared"变量?如何调用"swap"函数?
使用objump的"-d"参数可以看到"a.o"的代码段反汇编结果:

$objdump -d a.o

在这里插入图片描述
当源代码"a.c"在被编译成目标文件时,编译器并不知道"shared"和"swap"的地址,因为它们定义在其他目标文件中,所以编译器就暂时把地址0看作是"shared"的地址,我们可以看到这条"mov"指令中,关于"shared"的地址部分为"0x00000000".
另外一个是偏移量为0x26的指令的一条调用指令,它其实就表示对swap函数的调用,在没有重定位前,相对偏移量被置为0xFFFFFFFC。
编译器把这两条指令的地址暂时用地址"0x00000000"和"0xFFFFFFFC"代替者,把真正的地址计算工作留给了链接器。我们通过前面的空间与地址分配可以得知,链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地位修正。

重定位表

那么链接器是怎么知道哪些指令是要被调用的?这些指令的哪些部分要被调整?事实上在ELF文件中,有一个叫重定位表的结构专门用来保存这些与重定位相关的信息。
对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容,比如代码段".text"如有要被重定位的地方,那么会有一个相应叫".rel.text"的段保存了代码段的重定位表。每一个要被重定位的地点叫一个重定位入口,重定位入口的偏移表示该入口在要被重定位的段中的位置。

符号解析

在我们通常的观念里,之所以要链接时因为我们目标文件中用到的符号被定义在其他目标文件,所以要将它们链接起来。比如我们直接使用id
来链接"a.o",而不将"b.o"作为输入。链接器就会发现shared和swap两个符号没有被定义,没有办法完成链接工作,这也是我们平时在编写程序的时候常碰见的问题之一,就是链接时符号未定义,导致这个问题的原因有很多,最常见的一般都是链接时缺少了某个库,或者输入目标文件路径不正确或符号的声明与定义不一样,所以从普通程序员的角度看,符号的解析占据了链接的主要内容。
其实重定位也伴随着符号的解析过程,每个目标文件都可能定义一些符号,也可能引用到定义在其他目标文件的符号,重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器须要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址,这时候链接器就会去查找由所有输入目标文件的符号组成的全局符号表,找到相应的符号后进行重定位。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐