对PLT表和GOT表的浅显理解
之前做PWN题的时候了解过一点plt表和got表——plt表可跳转到got表,got表里存放着函数的真实地址
今天巧合在网上翻到一篇用gdb调试plt表和got表关系的博客,这才对它有了进一步的了解。
GOT(全局偏移表)和 PLT(过程链接表),是两个表之间的交互才使得函数实现延迟绑定,通过这种方法将过程地址的绑定推迟到第一次调用该函数。
GOT表构成
为了实现延迟绑定,GOT的头三条表目是特殊的:
GOT[0]包含.dynamic段的地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;
GOT[1]包含动态链接器的标识;
GOT[2]包含动态链接器的延迟绑定代码的入口点。
GOT的其他表目为本模块要引用的一个全局变量或函数的地址。
PLT表构成
PLT是一个以16字节(32位平台中)表目的数组形式出现的代码序列。就像GOT表,PLT表并不是每个表项都用于存放“函数地址“,其中PLT[0]是一个特殊的表目,它跳转到动态链接器中执行,换句话说,PLT[0]是一个函数,这个函数的作用是通过GOT[1]和GOT[2]来正确绑定一个函数的正式地址到GOT表中来。
实现过程
每个定义在共享库中并被本模块调用的函数在PLT中都有一个表目,从PLT[1]开始.模块对函数的调用会转到相应PLT表目中执行,这些表目由三条指令构成。
第一条指令是跳转到相应的GOT存储的地址值中.
第二条指令把函数相应的ID压入栈中,
第三条指令跳转到PLT[0]中调用动态链接器解析函数地址,并把函数真正地址存入相应的GOT表目中。
被调用函数GOT相应表目中存储的最初地址为相应PLT表目中第二条指令的地址值,函数第一次被调用后.GOT表目中的值就为函数的真正地址。
因此,第一次调用函数时开销比较大.但是其后的每次调用都只会花费一条指令和一个间接的存储器引用。
所以第一次函数调用过程如下:
1.调用函数找到plt表
2.jmp 相应的got表
3.push got表的下标//相应的标识
4.jmp plt[0]
5.plt[0]的指令转向got[2],进入动态连接器入口
//由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址,这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x]中
6.将真正的函数地址覆盖到got表中
其中2、3、4步(jmp、push、jmp)是第一次调用某函数时进行的三步操作,是实现延时绑定机制的关键
可以理解为这种状态:
最后借用一下大佬的动图:
后期补充:
其实具体实现比想象的还要复杂一下,下面是动态链接的过程,也是dl_runtime_resolve需要掌握的
- dl_runtime_resolve 需要两个参数,一个是 reloc_arg,就是函数自己的 plt 表项 push 的内容,一个是link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址
- 而 .dynamic 可以找到 .dynstr、.dynsym、.rel.plt 的这些东西的地址
- .rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,这个指针对应的里面放着 r_offset、r_info
- 将 r_info>>8 得到的就是 .dynsym 的下标,这个下标的内容就是 name_offset
- .dynstr+name_offset 得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名
- 在动态链接库里面找这个函数的地址,赋值给 *rel->r_offset,也就是 GOT 表就完成了一次函数的动态链接
1 | typedef struct { |
.rel.plt
节是用于函数重定位
.rel.dyn
节是用于变量重定位
.got
节保存全局变量偏移表
.got.plt
节保存全局函数偏移表,对应着Elf32_Rel
结构中r_offset
的值
.dynsym
节包含了动态链接符号表
.dynstr
节包含了动态链接的字符串
.plt
节是过程链接表