通过前文我们已经对目标文件和符号有了一个较为清晰的理解,接下来,我们将以下面两个源文件作为一个例子阐述一下静态链接的处理过程。
/* a.c */
extern int shared;
extern void swap(itn *a, int *b);
int main()
{
int a = 100;
swap(&a, &shared);
return 0;
}
/* b.c */
int shared = 1;
void swap(int *a, int *b)
{
*a ^= *b ^= *a ^= *b;
}
假设程序只有两个源文件“a.c”和“b.c”,我们通过编译得到“a.o”和“b.o”这两个目标文件。从代码可知,“b.c”定义了两个全局符号,变量shared和函数swap,而“a.c”定义了一个全局符号main,同时引用“b.c”定义的符号shared和swap。
1.空间地址分配
链接首先要解决的一个问题就是需要把各个目标文件整合在一起,这里通常使用的方法是相似段合并方法。即如下图所示,将两个目标文件中相同的段(.text段,.data段等等)合并在一起,组成一个大的段。
详细来说,在链接的时候,链接器首先会扫描所有输入目标文件,获得它们各个段的长度,属性和位置。并将输入目标文件中的符号表中所有符号定义和引用收集起来,统一放在一个全局符号表中。然后合并所有输入目标文件的段长度,把相同的段合并在一起,重新计算合并后的段的长度和位置。
如下图所示,我们可以看到a.o和b.o的.text段长度之和等于连接后的可执行程序ab的.text段的长度。
可能有同学会好奇,我们之前介绍目标文件的时候不是提到过,.bss段不是在目标文件中不占任何内存么,为何上图中,目标文件的相似段合并中还有.bss段呢?这里我们必须明确一个概念,当我们谈论到空间的时候,其实有两层含义,一层是这段内存本身在目标文件中所占据的空间,另一层含义是指这段内存在加载时,在系统的虚拟地址空间中占据的内存空间。比如.bss段,该段本身在目标文件中不占任何空间,所以目标文件中也没有关于.bss段的数据。但是在程序加载后,程序需要为.bss段分配内存空间,才能正确的执行。在静态链接过程中,我们指的空间都是计算机系统的虚拟地址空间,因为该空间和应用程序的加载是息息相关的。
2.符号解析与重定位
我们使用objdump的-d选项查看a.o文件的反汇编代码,如下所示,可以发现main函数的起始地址为0,而红圈中标出的变量shared和函数swap的地址也都是0。这是因为当a.c被编译器编译成a.o的时候,编译器并不知道shared和swap的地址,因为这两个符号定义在其他的目标文件中,因此编译器会暂时认为,这两个符号的地址是0。
链接器在链接过程中,会给合并后的.text,.data等段分配起始地址,至于起始地址是多少,这和链接器的虚拟地址分配规则相关。当合并后的.text段,.data段,.bss段都有起始空间后,每个目标文件中的符号在合并后的对应的段中的偏移都是一定的,这样也就能确定每个符号对应的地址了。
我们使用objdump查看可执行文件ab的反汇编代码,可以发现main函数,shared变量和swap函数都被正确分配了地址。
那么链接器是怎么知道哪些指令使用了这些需要重定位的符号呢?并且链接器又是怎么知道该如何调整指令呢?(比如虽然shared和swap符号的地址都需要重新计算,但是call和mov两个指令对地址的解释方式是不相同的)
我们之前在介绍目标文件的时候,曾经提到过重定位表,即那些类型为RELA的段。重定位表是和其他段一一对应的,比如当.text段中有需要重定位的地方时,那就会有一个与之对应的.rela.text重定位表。
使用objdump的-r选项查看a.o的重定位信息,显示如下,我们发现重定位表中包含三个信息,分别是重定位符号的偏移,类型和值。
- 重定位符号的偏移指的是该重定位符号相对于所在段的偏移,例如我们的目标文件a.o中.text段的0x14位置上,便是mov指令使用符号shared的地方。
- 重定位符号的值指的是经过地址分配之后的符号的地址。
- 重定位的类型包括_32和PC_32两种,表示调整指令的方法。_32是绝对地址修正法(S+A),_PC_32是相对地址修正方法(S+A-P)。其中A表示未修正前指令中填的符号值,大部分都是0。S表示重定位符号的值,P表示被修正的位置。
链接器在链接过程中会扫描每个目标文件的重定位表,对于重定位表中的符号,链接器会查找合并后的符号表,如果没找到该符号或者找到多个同名符号,链接器便会报告“符号未定义”或者“符号重复定义错误”。当在符号表中找到符号后,便根据重定位符号的偏移找到需要修正的指令为止,再根据重定位符号的类型和值修正指令。至此,符号的解析和重定位工作便完成了。