ELF静态链接
一直对ELF目标文件是怎样链接成可执行文件感到比较的疑惑,ELF文件里面的重定位段是怎样解决符号引用问题的?前几天偶然看了《深入理解计算机系统》里面讲了这个问题,看了之后对里面的实现机制终于有了一定的理解。
当有链接器链接多个可重定位的共享对象时,共享对象时怎样合并的呢?很简单,链接器将相同类型的节合并在一起,比如将所有输入文件的.text合并到输出文件的.text段,接着是.data段,.bss段等。
链接器扫描所有的输入目标文件,并且获得它们各个节的长度,属性和位置,并将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表中。链接器能够获得所有输入目标文件的节长度,并将它们合并,并计算出输出文件中各个节合并后的段长度和位置,并建立段和节之间的映射关系。也就是说,在将输入文件的各个节映射到段中后,输入文件中的各个节在链接后的虚拟地址就已经确定了,那么全局符号表中的符号地址就可以知道了。
知道了定义符号的虚拟地址,合并了各个节,是不是就大功告成了?显然没有这么简单,由于输入给链接器的文件都是可重定位的目标文件,这些目标文件中引用符号的地方存放的地址肯定不是最终的虚拟地址,因为这个时候符号地址还不确定。那么链接器在知道了符号的各个虚拟地址后怎样来修改引用符号的地址为实际的符号虚拟地址呢?这个工作是通过重定位目标文件中的重定位表来实现的。
对于每个要重定位的ELF节都有一个对应的重定位表,而一个重定位表往往就是ELF文件的一个节。比如代码节.text有要重定位的地方,那么会有一个相对应的.rel.text的节保存.text的重定位表;如果数据节.data有要被重定位的地方,也会有一个相对应的叫.rel.data的节与之对应。
每个要被重定位的地方叫做一个重定位入口(Relocation Entry),重定位入口的偏移(Offset)表示该入口在要被重定位的节中的位置。重定位表的结构很简单,它是一个Elf32_Rel结构的数组,每个数组元素对应一个重定位入口。Elf32_Rel定义如下:
typedef struct elf32_rel { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
r_offset表示重定位入口的偏移。对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于节起始的偏移;对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址。
r_info表示重定位入口的类型和符号。这个成员的高8位表示重定位入口的类型,低24位表示重定位入口的符号在符号表中的下标。
- R_386_PC32:重定位一个使用32位pc相对地址的引用。
- R_386_32:重定位使用32位绝对地址的引用。
链接器中根据重定位表来修改符号引用地址的算法是:
第一行迭代每个节,第二行迭代这个节中的每一个重定位表项,为了简单,我们假设节是一个字节数组,每个重定位表项都是Elf32_Rel类型,并假设当这个算法运行时,链接器也已经知道了每个节的运行时地址(定义为ADDR(s))和每个符号的运行时地址(定义为ADDR(r.symbol))。
根据重定位入口的类型,需要分两个情况来修改引用地址。
重定位PC相对引用
假设函数printf在main函数被调用,现在来看下它在main函数中是怎么被调用的:
1b: e8 fc ff ff ff call 7 <main+0x7>
我们可以看到call指令在节的偏移量为0x7,这条指令由1个字节的操作码0xe8和32比特的引用0xfffffffc(-4)组成。我们也可以看到对这个引用的重定位项:
r.offset = 0x7
r.symbol = printf
r.type = R_386_PC32
重定位的项告诉链接器修改32位PC相对引用在节的偏移量为0x7,现在假设链接器已经决定了:
ADDR(s) = ADDR(.text) = 0x80483b4
ADDR(r.symbol) = ADDR(printf) = 0x80483c8
使用前面的算法来计算引用的运行时地址:
refaddr = ADDR(s) + r.offset
= 0x80483b4 + 0x7
= 0x80483bb
修改引用符号地址的值,使得引用指向printf运行时的地址:
*refptr = (unsigned)(ADDR(r.symbol) + *refptr - refaddr) = (unsigned)(0x80483c8 + (-4) - 0x80483bb) = (unsigned)(0x9)
这样,printf的重定位值就为0x9。当程序运行时,由于call指令时相对pc寻址的,所以计算出的printf的实际地址为:
pc + 0x9 = 0x80483bf + 0x9 = 0x80483c8
重定位绝对地址引用
假设定义了下面一个全局变量:
int *bufp0 = &buf[0]
由于bufp0是一个初始化了的数据对象,它会被存放在.data节中。由于它被初始化为全局变量buf[0]的地址,因此bufp0需要被重定位。下面是bufp0在.data节中的反汇编代码:
00000000<bufp0> 0: 00 00 00 00
我们可以看到.data节中包含了一个简单的32位的引用,指针bufp0的值位0x0。重定位表项告诉链接器这是一个32位的绝对地址引用,相对于.data节的偏移量为0。它必须被重定位以指向buf[0]的地址。假设链接器已经决定了:
ADDR(r.symbol) = ADDR(buf) = 0x8049454
链接器将根据上面的算法更新这个引用:
*refptr = (unsigned)(ADDR(r.symbol) + *refptr) = (unsigned)0x8049454 + 0) = (unsigned)(0x8049454)