全局偏移表(GOT)和过程链接表(PLT)
ELF 格式的共享库使用 PIC 技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC 在代码中的跳转和分支指令不使用绝对地址。PIC 在 ELF 可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表 GOT
对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址;对于本模块内的静态变量和静态函数,用 GOT 表的首地址作为一个基准,用相对于该基准的偏移量来引用,因为不论程序被加载到何种地址空间,模块内的静态变量和静态函数与 GOT 的距离是固定的,并且在链接阶段就可知晓其距离的大小。这样,PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。
对于 PIC 代码,代码段内不存在重定位项,实际的重定位项只是在数据段的 GOT 表内。共享目标文件中的重定位类型有 R_386_RELATIVE、R_386_GLOB_DAT 和 R_386_JMP_SLOT,用于在动态链接器加载映射共享库或者模块运行的时候对指针类型的静态数据、全局变量符号地址和全局函数符号地址进行重定 位。
1.2 PLT 表
过程链接表用于把位置独立的函数调用重定向到绝对位置。通过 PLT 动态链接的程序支持惰性绑定模式。每个动态链接的程序和共享库都有一个 PLT,PLT 表的每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。程序对某个函数的访问都被调整为对 PLT 入口的访问。
每个 PLT 入口项对应一个 GOT 项,执行函数实际上就是跳转到相应 GOT 项存储的地址,该 GOT 项初始值为 PLTn项中的 push 指令地址(即 jmp 的下一条指令,所以第 1 次跳转没有任何作用),待符号解析完成后存放符号的真正地址。动态链接器在装载映射共享库时在 GOT 里设置 2 个特殊值:在 GOT+4( 即 GOT[1]) 设置动态库映射信息数据结构link_map 地址;在 GOT+8(即 GOT[2])设置动态链接器符号解
析函数的地址_dl_runtime_resolve。
PLT 的第 1 个入口 PLT0 是一段访问动态链接器的特殊代码。程序对 PLT 入口的第 1 次访问都转到了 PLT0,最后跳入 GOT[2]存储的地址执行符号解析函数。待完成符号解析后,将符号的实际地址存入相应的 GOT 项,这样以后调用函数时可直接跳到实际的函数地址,不必再执行符号解析函数
操作系统运行程序时,首先将解释器程序即动态链接器ld.so 映射到一个合适的地址,然后启动 ld.so。ld.so 先完成自己的初始化工作,再从可执行文件的动态库依赖表中指定的路径名查找所需要的库,将其加载映射到内存。
Linux用一个全局的库映射信息结构 struct link_map链表来管理和控制所有动态库的加载,动态库的加载过程实际上是映射库文件到内存中,并填充库映射信息结构添加到链表中的过程。结构 struct link_map 描述共享目标文件的加载映射信息,是动态链接器在运行时内部使用的一个结构,通过它保持对已装载的库和库中符号的跟踪。
link_map 使用双向链接中间件“l_next”和“l_prev”链接进程中所有加载的共享库。当动态链接器需要去查找符号的时候,可以向前或向后遍历这个链表,通 过访问链表上的每一个库去搜索需要查找的符号。Link_map 链表的入口由每个可执行映像的全局偏移表的第 2 个入口(GOT[1])指向,查找符号时先从 GOT[1]读取 link_map 结点地址,然后沿着link-map 结点进行搜索。
动态库的加载映射过程主要分 3 步:
(1) 动态链接器调用 __mmap 函数对动态库的所有PT_LOAD 可加载段进行整体映射:
l_map_start=(ElfW(Addr))__mmap ((void *)0, maplength, prot,
MAP_COPY | MAP_FILE, fd, mapoff);
返回值 l_map_start 是实际映射的虚拟地址,和段结构成员 p_vaddr 指定的虚拟地址不一定相同,这对于位置无关代码不会产生影响。但是对于数据段和 link_map 结构中其它相关的位置描述信息还要进行修正。共享库映射的内存位置关系如图 1,l_addr 是实际映射地址和原来指定的映射地址的差值,用于其它位置信息的修正,即简单地将原来指定的虚拟地址加上 l_addr 就可以得到实际加载的虚拟地址
(2)共享文件映射完毕,动态链接器处理共享库的PT_DYNAMIC 动态段,将各项动态链接信息主要是哈希表、符号表、字符串表、重定位表、PLT 重定位项表等地址填写到 link_map 的 l_info 数组结构中。l_info 是 link_map 最重要的字段之一,几乎所有与动态链接管理相关的内容都与 l_info数组有关。动态链接器还要加载处理当前共享库的所有依赖库。
(3)由于实际的映射地址和指定的虚拟地址有可能不同,因此还要对动态库及其依赖库进行重定位。设置动态库的第1 个和第 2 个 GOT 表项:
Elf32_Addr *got =
(Elf32_Addr *) lmap->l_info[DT_PLTGOT].d_un.d_ptr;
got[1]=lmap;
got[2]=&_dl_runtime_resolve;
对动态库的所有重定位项进行重定位,在重定位项指定的偏移地址处加上修正值 l_addr。动态项 DT_REL 给出了重定位表的地址,DT_RELSZ 给出重定位表项的数目。
映射完毕后,动态链接器调用共享库(包括所有相关的依赖库)自备的初始化函数进行初始化。
程序连接表(Procedure Linkage Table)可以使被感染的文件调用外部的函数。这要比修改LD_PRELOAD环境变量实现调用的重定向优越的多,首先不牵扯到环境变量的修改
程序连接表(PLT)
在ELF文件中,全局偏移表(Global Offset Table,GOT)能够把位置无关的地址定位到绝对地址,程序连接表也有类似的作用,它能够把位置无关的函数调用定向到绝对地址。连接编辑器(link editor)不能解决程序从一个可执行文件或者共享库目标到另外一个的执行转移。结果,连接编辑器只能把包含程序转移控制的一些入口安排到程序连接表 (PLT)中。在system V体系中,程序连接表位于共享正文中,但是它们使用私有全局偏移表(private
global offset table)中的地址。动态连接器(例如:ld-2.2.2.so)会决定目标的绝对地址并且修改全局偏移表在内存中的影象。因而,动态连接器能够重定向 这些入口,而勿需破坏程序正文的位置无关性和共享特性。可执行文件和共享目标文件有各自的程序连接表。
elf的动态连接库是内存位置无关的,就是说你可以把这个库加载到内存的任何位置都没有影响。这就叫做position independent。在编译内存位置无关的动态连接库时,要给编译器加上 -fpic选项,让编译器产生的目标文件是内存位置无关的还会尽量减少对变量引用时使用绝对地址。把库编译成内存位置无关会带来一些花费,编译器会保留一 个寄存器来指向全局偏移量表(global offset table (or GOT for short)),这就会导致编译器在优化代码时少了一个寄存器可以使用,但是在最坏的情况下这种性能的减少只有3%,在其他情况下是大大小于3%的。