# ELF原理五

全局偏移量表(Global Offset Table)

位置无关的代码不能包含绝对地址(程序员只能看到虚拟地址,所以在讨论应用程序时不需要考虑其他地址)。所以引入全局偏移表来保存这些绝对地址信息,全局偏移表保存在每个进程自己的数据段中,因此可以在不影响位置独立性和程序文本共享性的情况下可以使用绝对地址。程序使用与位置无关的寻址方式来使用它的全局偏移表,并从中提取绝对值,从而将位置无关的数据和函数引用重定向到绝对位置(如何找到绝对地址?自然由动态链接器来操作)。

最初,全局偏移表保存着重定位表项所需的信息(参考前面说的 重定位一节)。当系统为一个可加载的目标文件创建了内存段后,动态链接器将处理保存的重定位表项,而其中一些重定位表项是 R_386_GLOB_DAT 类型的,这些重定位将引用全局偏移表,动态链接器将根据重定位表项确定相关的符号值,并计算它们的绝对地址,然后将全局偏移量表中对应的内存表项设置为得到的绝对地址值。虽然在链接编辑器构建一个对象文件内存信息时,绝对地址是未知的,但是动态链接器知道所有内存段的地址,因此可以计算其中使用符号的绝对地址。

如果一个程序需要直接访问一个符号的绝对地址,那么该符号将存在一个全局偏移表项来保存该地址信息。注意:因为可执行文件和共享对象有单独的全局偏移表,所以一个符号的地址可能出现在几个表中。动态链接器在将控制权(控制权值得的是CPU的控制权,让CPU从动态链接器的代码中转向执行进程代码段中的指令)交给进程映像中的任何代码之前,将处理所有的全局偏移表重定位,从而确保在执行期间引用的绝对地址是可用的。

全局偏移表数组下标为0的表项用于保存动态链接信息的地址,该地址由符号_DYNAMIC引用。这允许一个程序,比如动态链接器,在处理重定位项之前找到保存动态链接信息的结构。这对于动态链接器尤其重要,因为它必须初始化自己,而不依赖于其他程序来重新定位它的内存映像。在32位Intel架构中,全局偏移表中的表项1和表项2也是保留的。详细信息将会在过程链接(Procedure Linkage Table)表中进行描述。

操作系统可以在不同的程序中为相同的共享对象选择不同的内存段地址,它甚至可以为同一程序的不同进程选择不同的库地址。尽管如此,一旦建立了进程映像,内存段将永远不会改变地址,只要进程存在,那么它的内存段就驻留在固定的虚拟地址上。

全局偏移表的格式和解释是特定于处理器的。对于32位Intel体系结构,可以使用符号GLOBAL_OFFSET_TABLE来访问表。GLOBAL_OFFSET_TABLE_数组可以位于.got节的中间。

img

过程链接表(Procedure Linkage Table)

就像全局偏移表将位置无关地址计算重定向到绝对位置一样(通过将计算好的地址放到GOT表项中),过程链接表将位置无关的函数调用重定向到绝对位置(注意:GOT 表中包含两种信息:数据的绝对地址、函数的绝对地址,而 PLT 就是用于从代码段中引用GOT函数地址而引入的)。链接编辑器无法解析从一个可执行或共享对象到另一个对象的执行地址(如函数调用)。因此,链接编辑器将引用的函数调用转移到过程链接表中的表项。在SYSTEM V架构中,过程链接表保存在共享文本段中,但是它们内部将使用进程私有的全局偏移表中的地址(这时:数据独立、代码共享,达到了动态链接库的目的)。动态链接器确定引用的函数的绝对地址,获取到该地址后将该地址放入全局偏移表对应的表项中,因此,动态链接器可以在不影响位置独立性和程序文本共享性的情况下进行动态重定向。注意:可执行文件和共享对象文件具有单独的过程链接表。

如图所示,过程链接表中的指令将对绝对代码和位置无关代码使用不同的操作数寻址模式。动态链接器和程序配合完成重定向:通过过程链接表和全局偏移表解析符号引用,步骤如下:

  1. 当第一次创建程序的内存映像时,动态链接器将全局偏移表中的第二个和第三个表项设置为特殊值。下面的步骤将详细解释这些值。
  2. 如果过程链接表是位置无关的,那么全局偏移表的地址必须驻留在ebx寄存器中。进程映像中的每个共享对象文件都有自己的过程链接表,因此,调用函数负责在调用过程链接表项之前设置全局偏移表的基址寄存器
  3. 为了说明这一点,如图所示,假设程序调用了name1函数,它将控制转移到标签.PLT1(也即执行:.PLT1的代码)
  4. 标签.PLT1处的第一条指令跳转到 name1 的全局偏移表表项中的地址。最初,全局偏移表中 name1 的表项 保存了下面的 pushl 指令的地址,而不是 name1 的真实地址,此时控制转移到标签.PLT1处的第二条指令
  5. 因此,程序将一个重定位偏移量(offset)压入堆栈。重定位偏移量是重定位表的32位非负字节偏移量,该偏移量处的重定位表项类型为 R_386_JMP_SLOT,该表项中的偏移量为在前面的 jmp 指令中使用的全局偏移表项,重定位表项还包含一个符号表索引下标,从而告诉动态链接器正在引用什么符号,在本例中是name1
  6. 在推入重定位偏移量之后,程序然后跳转到. plt0,这是程序链接表中的第一个表项,pushl指令放置第二个全局偏移表项的值( got_plus_4 或 4(%ebx)),从而给动态链接器一个字的标识信息(这里为重定位表的地址),然后,程序跳转到第三个全局偏移表条目(got_plus_8 或 8(%ebx))中的地址,它将控制权传递给动态链接器
  7. 当动态链接器接收到控制时,它从堆栈信息中获取指定的重定位表项,找到符号的值,将name1的“真实”地址存储在它的全局偏移表项中,并将控制转移到 name1 函数
  8. 后续的过程链接表表项的执行将直接转移到 name1 函数,而不需要第二次调用动态连接器,也就是说 . plt1 处的 jmp指令将转移到 name1,而不是后面的pushl指令

imgimg

前面说过:LD_BIND_NOW 环境变量可以更改动态链接行为。如果其值为非空,则动态连接器在将控制转移到程序之前处理过程链接表项,也就是说,动态链接器在进程初始化时处理 R_386_JMP_SLOT 类型的重定位表项,否则,动态链接器会惰性地评估过程链接表项,将符号解析和重定位延迟到表项第一次执行时。

注:惰性绑定通常会提高应用程序的整体性能,因为不使用的符号不会导致额外的动态链接开销。然而,有两种情况使得惰性绑定在某些应用程序中不受欢迎。第一,对共享对象函数的初始引用要比后续调用花费更长的时间,因为动态链接器会拦截函数调用来解析符号,一些应用程序不能容忍这种不可预测性。第二,如果发生错误,动态链接器不能解析该符号,动态链接器将终止程序,在惰性绑定下,这可能在任意时刻发生,同样,一些应用程序不能容忍这种不可预测性,这时可以通过关闭惰性绑定,动态链接器将强制在进程初始化期间,在应用程序接收到控制之前发生失败。

初始化和终止函数(Initialization and Termination Functions)

在动态链接器完成构建进程映像并执行重定位之后,每个动态链接库都可能执行一些初始化代码,这些初始化函数的调用顺序没有指定,但是所有的共享对象初始化都发生在可执行文件获得控制权之前(也即在执行可执行文件的代码之前完成这些初始化函数的调用)。

类似地,动态链接库可能包含终止函数,在进程执行完成后将会执行这些终止函数,将会利用atexit(OS调用)机制执行这些终止函数,同样,动态链接器调用终止函数的顺序没有指定。

动态链接库通过 DT_INIT 和 DT_FINI 指定它们的初始化和终止函数。通常,这些函数的代码保存在 .init 和 .fini 节中。

注:虽然 atexit 机制将会运行终止函数,但并不保证在进程死亡时一定执行,特别地,如果进程调用了_exit 函数,或者进程因为接收到一个既没有捕获也没有忽略的信号而退出,那么进程将不会执行 终止函数。