5.4.7 重定位可执行程序

可执行程序如果引用的是自身定义的函数和变量,这些符号在编译时就已经确定,不需要任何重定位。即使其他动态库中也定义了与可执行程序中相同的符号,链接器也优先使用可执行程序自身定义的函数和变量。

如果引用了动态库中的函数和全局变量,那么编译时可执行程序根本不知道这些符号最终的地址,在重定位了动态库之后,可执行程序也需要重定位这些符号。可执行程序的重定位与共享库原理基本一致,只有一点差别,我们这里简单讨论一下它们之间的差别。

(1)重定位引用的动态库中的函数

我们以hello中引用动态库libf1中的函数foo1_func为例,来看关于函数的重定位。可执行程序hello中调用foo1_func的反汇编代码如下:

5.4.7 重定位可执行程序 - 图1

可见,可执行程序也使用了延迟绑定的技术。再来看看PLT部分的代码:

5.4.7 重定位可执行程序 - 图2

5.4.7 重定位可执行程序 - 图3

与动态库不同,可执行程序的地址在编译时就已经分配好了,所以,GOT的地址在编译时就确定了,不必再如动态那样在运行时动态获取GOT表的基址。我们来看看hello的GOT表的基址:

5.4.7 重定位可执行程序 - 图4

GOT表的基址为0x0804a000,所以任何以GOT表基址为参照的偏移,直接使用这个地址即可。比如访问GOT表中的第3项,即函数_dl_runtime_resolve时,直接在此地址上加两个4字节偏移即可(因为_dl_runtime_resolve占据GOT表的第3项,所以偏移8字节):

5.4.7 重定位可执行程序 - 图5

观察hello中plt0部分,即地址0x8048486处,我们看到,指令中也确实是这么做的,jmp的目标地址在编译时就计算好了,就是*0x804a008。

除GOT表的基址固定外,可执行程序函数的重定位与动态库中函数的重定位完全一致。

(2)重定位引用的动态库中的变量

可执行程序与动态库不同,一般而言,其地址是编译时分配好的,是固定的(这里我们不考虑为了安全而使用PIE技术)。如果编译时没有传给编译器参数"-fPIC",那么对于引用的外部的全局变量,可执行程序不使用GOT表的方式寻址。换句话说,可执行程序引用的变量,在编译链接时就需要在编译链接时确定好地址,不能在加载时再进行重定位。

但是,编译时动态库都不能确定自己的变量的最终加载地址,更别提可执行程序了。那怎么办呢?于是ELF标准定义了一种新的重定位类型——R_386_COPY。对于这种重定位类型,编译器、链接器和动态链接器是这样协作的:编译时,编译器将偷偷地在可执行程序的BSS段创建了一个变量,这样就解决了编译时,变量地址不确定的问题。在程序加载时,动态链接器将动态库中的变量的初值复制到可执行程序的BSS段中来。然后,动态库(包括其他动态库)在引用这个变量时,因为可执行程序在link_map的最前面,所以解析符号都将使用可执行程序中的这个偷偷创建的变量。

下面我们结合hello引用动态库libf1中的变量foo1来具体的讨论一下。先来看一下hello的动态符号表:

5.4.7 重定位可执行程序 - 图6

5.4.7 重定位可执行程序 - 图7

虽然我们没有在可执行程序中定义变量foo1,但是根据动态符号表可见,可执行程序hello中却定义了变量foo1,其所在地址是0x0804a028,而且在第25个段中。我们来看看第25个段是什么:

5.4.7 重定位可执行程序 - 图8

可见,第25个段是.bss。也就是说,编译时,链接器为可执行程序hello定义了一个未初始化的全局变量foo1。而hello中,使用的恰恰是hello自己的foo1,而不是库libf1中的foo1。观察下面中引用的符号foo1的地址,正是hello中定义的符号foo1的地址:

5.4.7 重定位可执行程序 - 图9

链接器将hello的重定位表中foo1的重定位类型设置为R_386_COPY,当处理这个类型的重定位时,动态链接器将在加载时,将库libf1中变量foo1的值复制到hello中的foo1:

5.4.7 重定位可执行程序 - 图10

下面我们将程序运行起来,动态观察一下R_386_COPY类型的重定位过程。

5.4.7 重定位可执行程序 - 图11

理论上,动态链接器应该将库libf1中的foo1的初值10复制到hello中定义的foo1处。我们将hello中定义变量foo1所在地址实际的值打印出来:

5.4.7 重定位可执行程序 - 图12

可见,hello中的foo1已经被赋值为库libf1中的foo1的初值10了。

另外,库libf1中GOT表中保存的foo1的地址,也应该指向hello中定义的foo1的地址,而不是库libf1中的变量foo1的地址。原因是链接时,可执行程序排在链表link_map的表头,所以hello中的符号foo1当然要优先于库libf1中的foo1。我们来实际验证一下这一点,首先找到库libf1中变量foo1所在位置:

5.4.7 重定位可执行程序 - 图13

在另外一个终端中查看库libf1在进程hello的地址空间中映射的基址:

5.4.7 重定位可执行程序 - 图14

库libf1的GOT表中记录符号foo1的地址是:

5.4.7 重定位可执行程序 - 图15

我们打印一下GOT表中的值:

5.4.7 重定位可执行程序 - 图16

根据输出可见,地址0x0804a040正是hello中定义的符号foo1的地址。可见,动态库libf1中使用的foo1变量是可执行程序中创建的这个副本。显然,虽然这个副本仅仅是编译器为其偷偷分配的,但是实际已经取代了库libf1中的foo1,已经转正了。

当然,在编译可执行程序时也可以给其传递参数"-fPIC",如此,可执行程序中对外部变量的应用也将采用GOT表的方式,但是这对可执行程序没有任何意义。