对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)是第一次调用某函数时进行的三步操作,是实现延时绑定机制的关键

可以理解为这种状态:

img

image-20220410004226564

最后借用一下大佬的动图:

在这里插入图片描述


后期补充:

其实具体实现比想象的还要复杂一下,下面是动态链接的过程,也是dl_runtime_resolve需要掌握的

  1. dl_runtime_resolve 需要两个参数,一个是 reloc_arg,就是函数自己的 plt 表项 push 的内容,一个是link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址
  2. 而 .dynamic 可以找到 .dynstr、.dynsym、.rel.plt 的这些东西的地址
  3. .rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,这个指针对应的里面放着 r_offset、r_info
  4. 将 r_info>>8 得到的就是 .dynsym 的下标,这个下标的内容就是 name_offset
  5. .dynstr+name_offset 得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名
  6. 在动态链接库里面找这个函数的地址,赋值给 *rel->r_offset,也就是 GOT 表就完成了一次函数的动态链接

image-20210822212347908

1
2
3
4
typedef struct {
Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址
Elf32_Word r_info; // 符号表索引
} Elf32_Rel;

.rel.plt节是用于函数重定位

.rel.dyn节是用于变量重定位

.got节保存全局变量偏移表

.got.plt节保存全局函数偏移表,对应着Elf32_Rel结构中r_offset的值

.dynsym节包含了动态链接符号表

.dynstr节包含了动态链接的字符串

.plt节是过程链接表