概念
在 ELF 文件中,查看可以获得它的节的名字。其中有几个带有 plt 和 got 的节。
在此处,给出各节的定义如下:
- .got:Global Offset Table,全局偏移表。这是链接器为外部符号填充的实际偏移表。
- .plt:Procedure Linkage Table,程序链接表。他有两个作用,要么在 .got.plt 中拿到链接地址跳转,要么触发链接器去寻找地址。
- .got.plt:是 .got 的一部分(但是是两个不同的节),是 got 专门为 plt 准备的节,包含了 plt 表需要的地址。(新版 gcc 可能将他叫为 .plt.got)
- .rela.plt:程序链接表的重定位表,记录所有全局函数的动态链接信息,用于在程序加载时修正 plt 表中的跳转指针,使它们指向正确的地址。
实验
接下来将使用 gdb 一步一步跟着汇编走完动态链接的过程。
准备工作
实验代码如下:
1 |
|
查看节的地址与大小:
1 | @└────> # objdump -h plt | grep -E "plt|got" |
查看需要动态链接的符号:
1 | @└────> # readelf -r plt |
反汇编查看 plt 相关函数:
1 | @└────> # objdump -d plt |
开始
- 首先断点到 puts 函数,查看调用处:
1 | @(gdb) disassemble main |
可以看到,调用处实际上是使用 call 指令走到 puts 的代码段。下面的 printf 也是如出一辙。
- 查看 puts@plt 的汇编指令
1 | @(gdb) disassemble |
可以看到,在汇编中,他首先要跳转到 0x601018 地址的位置。这个地址内容是个全局变量,实际上根据节的地址位置和大小可以判断,是处于 .got.plt 的位置内( 0x601000 ~ 0x601028)。所以可以认为,在 .got.plt 中,存在了 puts 函数的地址。
- 查看 .got.plt
1 | @(gdb) x/16x 0x601018 |
查看表中内容,发现跳转的地址是 0x4004d6,这不就是我们跳转之前的下一个地址吗!(puts@plt 的第二条指令) 同理,printf 函数也是如此(0x4004e6)。这是因为,之前没有调用过 puts 函数,第一次查找的时候,.got.plt 表中找不到函数的地址,那就先返回继续执行去调用链接器获取地址。
- 准备调用链接器
1 | 00000000004004d0 <puts@plt>: |
首先 pushq $0x0,这个是在 got.plt 中的编号,如 puts 是 0,printf 是 1。这个参数是给后续链接器使用的。然后跳到了 .plt 的位置执行(0x4004c0)。可以看到,printf@plt 函数最后也是跳到这个位置执行。
- 调用链接器
1 | 00000000004004c0 <.plt>: |
首先 push 了 0x601008 到栈中,这是 .got.plt 表中的一个地址。之后跳转到 0x601010 所存储的地址去执行相应的代码。不难看出,0x601010 也是存储在 .got.plt 表中的。查看一下存储的内容:
1 | @(gdb) x/10x 0x601010 |
可以看到,是让我们跳转到 0x00007ffff7de64a0 去执行相应的代码。那么这块代码是什么呢?
1 | @(gdb) info sharedlibrary |
可以看到,该地址是 ld-linux-x86-64.so 加载的位置。说明执行的是链接器的代码。
1 | 1: x/5i $pc |
可以看到这里代码执行的是 ld 中的 _dl_runtime_resolve_xsavec 函数是第一次函数调用时用于查找函数符号的,并且在结尾处会直接去调用找到的函数符号(本文中为 puts 函数)。
- 写回 .got.plt 表
在 puts 上打个断点,这样继续的话就是执行完 _dl_runtime_resolve_xsavec 还未执行 puts 的状态了。
1 | @(gdb) bt |
可以看到,此时,got.plt 表中的地址已经被写为 puts 函数实际的地址了(0x00007ffff7a7d8c0 在 0x00007ffff7a2cb90 ~ 0x00007ffff7b798ad 范围内,属于 /lib64/libc.so.6),这样下次调用 puts 就不用再次调用链接器了。
题外话
其实看一下 .got.plt 表的内容,会发现明明 puts 是第一个需要被链接的函数,为什么第一个却不是它呢?
1 | @(gdb) x/10x 0x601000 |
puts 地址实际上是 got[3]:0x00007ffff7a7d8c0,前面还有 3 项。其中:
- got[0]:0x0000000000600e10 自身模块 dynamic 段地址
1 | @(gdb) info symbol 0x0000000000600e10 |
-
got[1]:0x00007ffff7ffe1d0 本模块的 link_map 的地址。编译期间会初始化为 0。link_map 是一个双向链表的入口,链接进程所有加载的动态库。当链接器查找符号时,通过遍历该链表找到对应的符号。
-
got[2]:0x00007ffff7de64a0 _dl_runtime_resolve_xsavec 的地址。
1 | @(gdb) info symbol 0x00007ffff7de64a0 |
_dl_runtime_resolve 格式:
1 | //调用形式为: |
总结
虚拟地址空间内流程图:
第二次调用:








