如果使用动态链接方式生成的程序模块中使用大量的函数引用,在程序执行时会花费大量的时间用于模块间函数引用的符号查找和重定位,导致程序性能下降。由于程序中可能存在部分不常用的功能模块,那么在程序开始执行时就完成所有函数的链接工作将会是一种浪费。因此,Linux 系统采用延迟绑定机制优化动态链接程序的符号重定位过程。
0x01 延迟绑定原理
延迟绑定是当函数第一次被调用的时候才进行绑定(包括符号查找、重定位等),如果函数不被调用就不进行绑定。延迟绑定机制可以大大加快程序的启动速度,特别有利于一些引用了大量函数的程序。
GOT(Global Offset Table,全局偏移表)
GOT 是数据段用于地址无关代码的 Linux ELF 文件中确定全局变量和外部函数地址的表。ELF 中有 .got 和 .plt.got 两个 GOT 表,.got 表用于全局变量的引用地址,.got.plt 用于保存函数引用的地址。PLT(Procedure Linkage Table,程序链接表)
PLT 是 Linux ELF 文件中用于延迟绑定的表。
下面介绍延迟绑定的基本原理。假设程序中调用 func 函数,该函数在 .plt 段中相应的项为 func@plt,在 .got.plt 中相应的项为 func@got,链接器在初始化时将 func@got 中的值填充为 “preapre resolver” 指令处的地址。func@plt 的伪代码如下:
1. 首次调用
第一次调用 func 函数时,首先会跳转到 PLT 执行 jmp *(func@got)
,由于该函数没被调用过,func@got 中的值不是 func 函数的地址,而是 PLT 中的 “preapre resolver” 指令的地址,所以会跳转到 “preapre resolver” 执行,接着会调用 _dl_runtime_resolve 解析 func 函数的地址,并将该函数真正的地址填充到 func@got,最后跳转到 func 函数继续执行代码。
2. 非首次调用
当再次调用 func 函数时,由于 func@got 中已填充正确的函数地址,此时执行 PLT 中的 jmp *(func@got)
即可成功跳转到 func 函数中执行。
0x02 实例调试
下面通过调试程序中 func 函数的调用过程说明延迟绑定的原理。首先函数执行 call 指令调用 func 函数时会跳转到 0x8048420(func@plt)处执行。
接着跳转到 ds[0x804a010](func@got)处,由于是第一次调用该函数,func@got 中的地址并非函数的真实地址,需要对其进行地址重定位。
0x804a010 是 func 函数的重定位偏移,即重定位表中 func 符号的重定位入口。此时 0x804a010(func@got)中的地址为 0x8048426,即 PLT 中准备进行地址解析的指令地址。
程序跳转到 0x8048426 后,又经过 2 次跳转到 ds[0x804a008] 处执行。
ds[0x804a008] 处即为用于解析 func 地址的 _dl_runtime_resolve 函数。
_dl_runtime_resolve 函数会将 func 函数的真实地址填充到 0x804a010(func@got)中,并返回到 func 函数中继续执行。
至此,使用延迟绑定的可执行文件中函数地址重定位已完成,当再次调用 func 函数时即可通过 jmp ds[0x804a010] 直接跳转到 func 函数中执行。
References:
[1]《程序员的自我修养》
[2] 通过 GDB 调试理解 GOT/PLT
[3] 手把手教你栈溢出从入门到放弃(下)