GOT表和PLT表

1. GOT表(Global Offset Table)

ELF(Executable and Linking Format)格式的共享库使用PIC(Position Indepent Code)技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC在代码中的跳转和分支指令不使用绝对地址。PIC在ELF可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表GOT

对于模块外部引用的全局变量和全局函数,用GOT表的表项内容作为地址来间接寻址;对于本模块内的静态变量和静态函数,用GOT表的首地址作为一个基准,用相对于该基准的偏移量来引用,因为不论程序被加载到何种地址空间,模块内的静态变量和静态函数与GOT的距离是固定的,并且在链接阶段就可知晓其距离的大小。这样,PIC使用 GOT来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。

对于PIC代码,代码段内不存在重定位项,实际的重定位项只是在数据段的GOT表内。共享目标文件中的重定位类型有R_386_RELATIVER_386_GLOB_DATR_386_JMP_SLOT,用于在动态链接器加载映射共享库或者模块运行的时候对指针类型的静态数据、全局变量符号地址和全局函数符号地址进行重定位。

2. PLT表(Procedure Linkage Table)

讲一下延时绑定。ARM相对寻址的本质其实就是寄存器间接寻址,只不过基址换成了PC而已,访问效率还是比较低的,包括程序运行之前的动态链接和重定位操作,也会对程序的及时响应和性能造成一定的影响。假设一个软件中有几百个地方使用了动态链接,如果把所有的动态库一次性全部加载到内存并一一对它们进行重定位,会耗费不少的时间。程序中存在大量的if-else分支,并不是所有的指令都能执行到,我们加载到内存的动态库可能根本就没有被调用到,这又会白白浪费内存空间。基于这个原因,可执行文件一般都采用延迟绑定:程序在运行时,并不急着把所有的动态库都加载到内存中并进行重定位。当动态库中的函数第一次被调用到时,才会把用到的动态库加载到内存中并进行重定位。这样做既节省了内存,又可以提高程序的运行速度,因此得到广泛应用。

汇编代码如下:

指令代码中每一个使用动态链接的符号<x@plt>,都被保存在PLT中。过程链接表其实就是一个跳转指令,它无法单独工作,要和GOT表相关联,协同工作。当程序中引用某个符号时,就会从过程链接表跳转到GOT表,跳到GOT表中对应的项。如当程序中第一次引用<printf@plt>符号时,会跳到GOT表的0x21010处。在0x21010处,存放的是动态链接库的地址0x10490;动态链接库加载printf()函数到内存,然后会将printf()函数在内存中的实际地址保存在0x21010处,再将控制权交给printf()函数执行。等程序第二次调用printf()函数时,再次通过PLT表跳到GOT表的0x21010处,因为此时该地址上保存的是printf()函数在内存中的实际地址,所以就可以直接跳转过去执行了。

找到main()函数中调用add的代码部分(第10624行),可以看到:调用add的指令跳到了0x104a4<add@plt>处执行。在0x104a4地址处,可以看到这里并不是add()函数实现的地方,而是一个跳转命令,跳到了GOT表中地址为0x2100c的地方。一般情况下,GOT表中的每一项存放的都是符号的真实地址,但此时因为add第一次被调用,相应的动态库还没有加载到内存中,需要调用动态链接器去加载add的动态库,所以此时大家可以看到GOT表中每一项都是相同的值:0x10490。在0x10490地址处是一个跳转指令,跳转到动态链接器去执行,动态链接器的入口地址保存在GOT表的0x21008~0x2100b处。动态链接器的主要工作就是加载动态库到内存中并进行重定位操作:把add动态库加载到内存中,然后将add的实际地址更新到GOT表中保存add地址的那一项0x2100c地址处。此时在GOT表的0x2100c处保存的不再是默认的动态链接器地址0x10490,而是add()函数加载到内存中的实际地址。等第二次再调用add()函数时,就可以根据GOT表中的实际地址直接跳过去执行了。