PLT属于代码段,在进程加载和运行过程都不会发生改变,PLT指向GOT表的关系在编译时已完全确定,唯一能发生变化的是GOT表。
- PLT表开头的18个字(72字节)为dynamic linker保留
如果可执行文件或共享库需要N个.PLTi入口,那么紧跟着这18个字,link editor就会保留3*N个字(12*N字节),开头的2*N个字就是所有的.PLT入口,对于第i个引用符号,它的.PLT入口是(72+(i-1)*8)(1<=i<=N),剩下的N个字留给dynamic linker使用。
过程链接表虽然存在于文件当中,但它的初始化是由dynamic linker在装载可执行文件和共享库时完成的。下面是一个可能的被初始化的PLT表的内容:
Global Offset Table(GOT)
在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。
因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。
在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。
对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:
GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址(前面提到加载的共享库会形成一个链表);
GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数,后面我们会详细分析。
Procedure Linkage Table(PLT)
过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。
在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(如前所说,这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。
在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。
PLT和GOT关系图
PLT表结构有以下特点:
- PLT表中的第一项为公共表项,剩下的是每个动态库函数为一项(当然每项是由多条指令组成的,jmp *0xXXXXXXXX这条指令是所有plt的开始指令)
- 每项PLT都从对应的GOT表项中读取目标函数地址
GOT表结构有以下特点:
如果将PLT和GOT抽象起来描述,可以写成以下的伪代码
- GOT表中前3个为特殊项,分别用于保存 .dynamic段地址、本镜像的link_map数据结构地址和_dl_runtime_resolve函数地址;但在编译时,无法获取知道link_map地址和_dl_runtime_resolve函数地址,所以编译时填零地址,进程启动时由动态链接器进行填充
- 3个特殊项后面依次是每个动态库函数的GOT表项
plt[0]:
pushl got[1]
jmp *got[2]
plt[n]: // n >= 1
jmp *got[n+2] // GOT前3项为公共项,第3项开始才是函数项,plt[1]对应的GOT[3],依次类推
push (n-1)*8
jmp plt[0]
got[0] = address of .dynamic section
got[1] = address of link_map object( 编译时填充0)
got[2] = address of _dl_runtime_resolve function (编译时填充为0)
got[n+2] = plt[n] + 6 (即plt[n]代码片段的第二条指令)
进程起动后的GOT表
Linux加载进程时,通过execve系统调用进入内核态,将镜像加载到内存,然后返回用户态执行。返回用户态时,它的控制权并不是交给可执行文件,而是给动态链接器去完成一些基础的功能,比如上述的GOT[1],GOT[2]的填写就是这个阶段完成的。下图是动态链接器填完GOT[1],GOT[2]后的GOT图:
readelf -d XXX 查看动态段信息
Dynamic section at offset 0xe18 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libka.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x400540
0x000000000000000d (FINI) 0x400714
0x0000000000000019 (INIT_ARRAY) 0x600e00
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e08
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x4003f0
0x0000000000000006 (SYMTAB) 0x4002d0
0x000000000000000a (STRSZ) 181 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 72 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x4004f8
0x0000000000000007 (RELA) 0x4004e0
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4004c0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4004a6
0x0000000000000000 (NULL) 0x0
其实.dynamic段还藏着很多其它信息,都是跟动态运行相关的信息
1:main函数执行呼叫函数foo
由于foo是在外面库中定义,这时候函数的地址还不能确定,因此先Call 到绝对地址:400590 符号foo@plt,定位到PLT段
2:地址 foo@plt 指向GOT Table 的一个偏移地址,这个偏移地址在动态链接时 写入绝对地址。