ELF 文件中的 GOT 表和 PLT 表解读

《程序员的自我修养》
ELF可以生成一种特殊的代码——与位置无关的代码(position-independent code,PIC)。用户对gcc使用-fPIC指示GNU编译系统生成PIC代码。它是实现共享库或共享可执行代码的基础.这种代码的特殊性在于它可以加载到内存地址空间的任何地址执行.这也是加载器可以很方便的在进程中动态链接共享库。
PIC的实现运用了一个事实,就是代码段中任何指令和数据段中的任何变量之间的距离都是一个与代码段和数据段的绝对存储器位置无关的常量。因此,编译器在数据段开始的地方创建了一个表.叫做全局偏移量表(global offset table.GOT)。GOT包含每个被这个目标模块引用的全局数据目标的表目。编译器还为GOT中每个表目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个表目,使得它包含正确的绝对地址。PIC代码在代码中实现通过GOT间接的引用每个全局变量,这样,代码中本来简单的数据引用就变得复杂,必须加入得到GOT适当表目内容的指令。对只读数据的引用也根据同样的道理,所以,加上 IC编译成的代码比一般的代码开销大。
如果一个elf可执行文件需要调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT(procedure linkage table,过程链接表).这两个节之间的交互可以实现延迟绑定(lazy binging),这种方法将过程地址的绑定推迟到第一次调用该函数。为了实现延迟绑定,GOT的头三条表目是特殊的:GOT[0]包含.dynamic段的地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;GOT[1]包含动态链接器的标识;GOT[2]包含动态链接器的延迟绑定代码的入口点。GOT的其他表目为本模块要引用的一个全局变量或函数的地址。PLT是一个以16字节(32位平台中)表目的数组形式出现的代码序列。其中PLT[0]是一个特殊的表目,它跳转到动态链接器中执行;每个定义在共享库中并被本模块调用的函数在PLT中都有一个表目,从PLT[1]开始.模块对函数的调用会转到相应PLT表目中执行,这些表目由三条指令构成。第一条指令是跳转到相应的GOT存储的地址值中.第二条指令把函数相应的ID压入栈中,第三条指令跳转到PLT[O]中调用动态链接器解析函数地址,并把函数真正地址存入相应的GOT表目中。被调用函数GOT相应表目中存储的最初地址为相应PLT表目中第二条指令的地址值,函数第一次被调用后.GOT表目中的值就为函数的真正地址。因此,第一次调用函数时开销比较大.但是其后的每次调用都只会花费一条指令和一个间接的存储器引用。

————————————————————————————————————————————
延迟绑定(PLT)
1 基本思想
动态链接以牺牲一部份性能为代价。PLT是另一种优化动态链接性能的方法。
● 在动态链接下,程序模块之间包含了大量的函数引用,所以在程序开始执行前,会耗费不少时间解决函数引用的符号查找以及重定位。
● 但是,在一个程序运行过程中,可能很多函数在程序执行完时都不会被用刀,比如一些错误处理函数。
● 所以ELF采用了一种叫做延迟绑定的做法。
● 基本思想:就是当函数第一次被用到时才进行绑定。如果没有用则不进行绑定,所以在开始时模块间的函数调用都没有进行绑定,而是需要用到时才绑定。
2 具体做法
-动态链接器需要某个函数来完成地址绑定工作,这个函数至少要知道这个地址绑定发生在哪个模块 哪个函数,如lookup(module,function)。
在glibc中,lookup的函数真名叫做_dl_runtime_reolve()

  • 当我们调用某个外部模块时,调用函数并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行跳转,每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的项地址叫做bar@plt,具体实现
    bar@plt:
    jmp *(bar@GOT)
    push n
    push moduleID
    jump _dl_runtime_resolve
    第一条指令是一条通过GOT间接跳转指令,bar@GOT表示GOT中保存bar()这个函数的相应项。
    但是为了实现延迟绑定,连接器在初始化阶段没有将bar()地址填入GOT,而是将“push n”的地址填入到bar@GOT中,所以第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将n压栈,接着将模块ID压栈,跳转到_dl_runtime_resolve。实际上就是lookup(module,function)的调用。
    _dl_runtime_resolve()在工作完成后将bar()真实地址填入bar@GOT中。
    一旦bar()解析完毕,再次调用bar@plt时,直接就能跳转到bar()的真实地址。
    “”

3 实际实现
PLT的真正实现要更复杂些,ELF将GOT拆分成两个表“.got”和”.got.plt”,前者用来保存全局变量引用的地址,后者用来保存函数引用的地址。
也就是说,所有对于外部函数的引用被分离出来放到了“.got.plt”中