5.4.8 重定位动态链接器

在Linux中,动态链接器被实现为一个动态库的形式,而且这个动态库是自包含的(self-contained),没有引用其他库的符号,但是与普通动态库一样的道理,它在编译时也不知道自己的确切位置,所以它也难逃重定位的命运。事实上,当C库加载后,动态链接使用了C库中的内存管理相关函数替换了自身的实现。

查看一下动态链接器的重定位表就可见其需要重定位的符号:

5.4.8 重定位动态链接器 - 图1

但是,与动态库和可执行程序不同,它们有动态链接器负责为它们重定位,而动态链接器则没有这么好的命。在内核跳转到动态链接器时,它是非常残酷的,并没有给动态链接器如link_map信息。好在动态链接器不依赖其他的动态库,只需要确定自己被加载的基地址,然后找到动态链接需要的段.dynamic就可以解决问题,后续的重定位过程与动态库的过程基本完全相同。因此,动态链接器重定位自己的关键是:

❑确定自己被加载的基地址;

❑找到段.dynamic。

动态链接器被加载的地址就相当于link_map中的l_addr了。运行后,动态链接器可以获取到某个符号的地址,但是这并不足以计算出动态链接器在进程地址空间中映射的基址,只有对比,才能求出基址。因此,动态链接器还是需要编译时的链接器作一点小小的配合。在编译时,链接器定义了一个符号"_DYNAMIC":

5.4.8 重定位动态链接器 - 图2

定义这个符号的目的就是为了标识段.dynamic所在的地址,看下面动态链接器的Section Header Table:

5.4.8 重定位动态链接器 - 图3

由上可见,符号_DYNAMIC的地址正是段.dynamic的地址。在运行时,动态链接器使用如x86指令lea读取符号_DYNAMIC的运行时地址,实际就是读取运行时段.dynamic的地址。

除了定义了这个符号外,在编译时,段.dynamic的地址也被装载到了GOT表中的第1项。读者回忆一下在5.4.6节讨论GOT表时的内容。其中,第2项的linkmap和第3项的解析函数我们都已经看到其作用了,但是尚未看到第1项的意义。在重定位动态链接器时,这一项发挥了关键作用。前面我们就已经看到过,编译时定义了另外一个符号_GLOBAL_OFFSET_TABLE,目的与DYNAMIC相似,是为了标识GOT表的地址。因此,动态链接器就可以使用符号_GLOBAL_OFFSET_TABLE找到GOT表,从而取出GOT表中第1项的值。

然后,使用取得的符号_DYNAMIC,也就是段.dynamic的运行时地址,与GOT表第一项在编译时保存的段.dynamic的地址(其是相对于0的)做差,得出的就是动态链接器在进程地址空间映射的基址了。相关代码如下:

5.4.8 重定位动态链接器 - 图4

注意变量bootstrap_map,相信从名字读者已经猜出来了,相当于代表普通动态库和执行程序的link_map。而且根据这个变量的名字,我们也可以揣摩到开发者的用意是在表达这是动态链接器的自举过程。变量bootstrap_map中的关键两项读者应该非常熟悉了,l_addr是代表动态链接器自己被映射的地址,l_ld代表动态链接器的段.dynamic所在的地址。找到段.dynamic后,动态链接器调用elf_get_dynamic_info读取了这个段的信息。

我们来看看获取l_addr和l_ld这两个地址的函数:

5.4.8 重定位动态链接器 - 图5

5.4.8 重定位动态链接器 - 图6

函数elfmachine_dynamic利用在编译时定义的符号_GLOBAL_OFFSET_TABLE读取GOT表中第0项的值。

函数elf_machine_load_address计算动态链接器加载的地址。其首先取得符号_DYNAMIC的运行时地址,对于x86来说,可以使用指令lea,然后与GOT表中保存的编译时的地址做差,从而得出动态库在进程地址空间中映射的基址。

事实上,动态连接器重定位表中的那些动态内存管理的函数,如malloc、free等,最初动态链接器使用的是自己内部的实现:

5.4.8 重定位动态链接器 - 图7

但是一旦C库加载后,动态链接器将再次重定位这几个函数,使用C库中的相应实现。