静态链接分两步,(1)空间与地址分配,(2)符号解析与重定位。
1 空间与地址分配。空间域地址分配有两个含义,一个是输出的可执行文件的空间,一个是装载后的虚拟地址的空间。在这里我们指的是后者。在将多个目标文件(静态)链接成可执行文件的时候,链接器会将所有的代码段放在一起,会将所有的数据段放在一起。放在一起之后,其实每个符号的地址就确定了。比如说有两个目标文件。第一个目标文件中的代码段有一个符号,它的地址为X,即距离代码段的偏移为X。假设经过链接器合并之后,第一个目标文件的代码段的起始地址为A,那么在可执行文件中这个符号的地址为A+X。另外,在目标文件中,地址都是从0开始,但是在可执行文件中,地址不是从0开始。
2 符号解析与重定位。假设两个目标文件a.o,b.o,a.o里可能调用了b.o的函数或者使用了b.o中的变量,那么在a.o里,是不知道这些符号的具体地址的。但是在链接过程的“空间与地址分配”结束后,所有符号的地址就确定了,所谓的重定位就是说,要将那些还没有正确填入符号地址的地方都填上正确的符号地址。这里,就需要一种重定位表,链接器通过重定位表,才知道哪些地方需要重定位。前面说过,如果代码段里有需要重定位的地方,那么会有专门针对于代码段的重定位表(重定位表也是目标文件中的一个段,所以可能应该叫重定位段,我们这里都叫重定位表),比如.rel.text。当然,比如代码段中有多个地方需要重定位的时候,重定位表里就有很多项。其中每一项的数据结构包括两个部分:
(1)r_offset:重定位入口的偏移。这个指的是要修正的位置相对于段的偏移。
(2)r_info:这里面包括两部分,重定位入口的类型和符号。它的低八位表示重定位入口的类型(在静态链接中,一般是两种类型,R_386_32绝对寻址修正,R_386_PC32相对寻址修正),高24位表示重定位入口的符号在符号表中的下标。
那么现在就比较明显了,链接器可以从r_info的高24位知道它得符号的名字进而得到符号的地址,从r_offset中知道要修正哪里,从r_info的低八位中知道如何修正。链接器依次处理重定位表里每一项就可以确定所有的重定位信息。
下面说另外一个问题。在gcc中,支持这样一种机制:强符号与弱符号。先看代码。
1 //A.c 2 int x; 3 4 void f() 5 { 6 ++x; 7 }
//B.c #include <stdio.h> int x=100; int main() { f(); printf("%d\n",x); }
然后编译 gcc A.c B.c 得到a.out,运行./a.out最后输出了101 。这里A.c的x就是一个弱符号,B.c的x就是强符号。强符号的定义为初始化了的全局变量,弱符号为没有初始化的全局变量。在链接的时候,出现多个符号名相同的强符号的时候会链接错误,当有强符号有弱符号的时候弱符号会被覆盖。没有强符号有多个弱符号的时候最后链接器会保留占用内存最大的弱符号。所以在目标文件中,会将弱符号放在一个叫做COMMON的块(段)里,而不是放在BSS段里,因为不能确定它的大小(可能别的目标文件里的同一个弱符号占用的内存更大)。