Linux动态链接之二:优化加速之延迟绑定PLT

1. PLT延迟绑定的提出

动态链接速度损耗主要两方面:
1.对全局和静态的数据访问都要进行复杂的GOT定位,然后再间接跳转寻址;
2.动态链接的很多工作是在程序运行时完成的,动态链接器需寻找并装载目标共享对象、符号查找、地址重定位等,如果不加以优化,会出现程序启动过慢的情况。
故而需要出台一些优化动态链接策略的方法。

动态链接下,模块之间包含大量的函数引用(全局变量往往较少,过多的全局变量会导致模块间的耦合度很大,不符合软件编辑规范,参考丰田车载软件系统功能失效事件)。

动态链接会耗费不少时间解决模块之间的函数引用的符号查找及重定位(GOT表填充和更新),但是其实程序运行过程中,可能很多函数时用不到的(比如C++编程#include ,但是其实只用了一个printf()),比如一些错误处理函数或者一些用户很少用到的功能模块等,延迟绑定的核心思想是函数第一次被用到时才进行绑定,这种做法可以大大加速程序的启动速度,特别有利于一些有大量函数引用和大量模块的软件。

ELF针对延迟绑定,采用的是PLT(procedure linkage table)机制来实现,这种方法使用了很多精巧的指令序列来完成。在Linux中,动态链接在扫描可执行文件时,一旦扫描到外部函数的引用,则glibc会启用绑定函数_dl_runtime_resolve(module, function)来确定模块module中的function函数的在GOT的填充工作。(举个例子,liba.so需要调用libc.so中的bar()函数,那么当liba.so第一次调用bar()时,需要调用_dl_runtime_resolve来完成地址绑定工作,该函数显然需要知道绑定发生在哪个模块,哪个函数。

2. PLT的实现

延迟绑定PLT(Procedure Linkage Table)在GOT表基础又做了一次间接跳转。即模块内关于外部函数的地址引用,这下并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行,每个外部函数引用都对应PLT表中的一个表项,比如 bar()函数在PLT表中的表项称为 bar@plt,实现如下:

bar@plt:
jmp    *(bar@GOT)
push   n
push   moduleID
jump   _dl_runtime_resolve

可以看到bar@plt表项采用了类似于桩代码的格式。bar@plt第一指令中bar@GOT指向GOT表中函数bar()对应项地址,如果GOT表中关于bar()的地址已经被填充非空,则显然将会直接跳转到GOT表给出的bar()函数地址,实现正常的函数调用。

但所谓延迟绑定,即是指在初始未遇到bar()函数之前,GOT表中并无函数bar()的地址信息,而是将后续push n指令的地址填充到GOT中bar()表项中,这时jmp指令将会直接跳转继续执行后续的push n...等指令,该步操作很简单也不需要遍历寻址目标符号,故而代价很低,只需要在生成桩代码时将push n指令位置填入即可。而后的操作便是正常参数压栈工作,其中push n中的参数n对应的是bar()函数符号在重定位表.rel.plt中的下标,push moduleID中的moduleID则是模块ID,调用_dl_runtime_resolve函数完成具体的符号解析和重定位工作,将外部模块中bar()函数的真正地址填入GOT对应的bar@GOT表项。这样当我们下次再次回到PLT表bar@plt表项中转时,便会进入正常的函数调用过程,而不会继续执行push n及之后的代码,那段代码只会在符号未被解析时执行only一次。

上述是PLT的原理,而PLT机制的具体实现则复杂得多。ELF将GOT表分成全局变量专用表”.got”和函数引用专用子表”.got.plt”,所有外部函数的引用中转全部被集中到”.got.plt”子表中(前面提到过每个共享模块都有自己的一份GOT表,通过推出GOT表来实现代码段的地址无关性,而将地址相关性转移到GOT表上,这也是为啥GOT表示放在.data段中的原因)。关于”.got.plt”子表需要注意的是它的前三项:第一项为”.dynamic”段地址,第二项为本模块的ID,第三项保存的是_dl_runtime_resolve()地址。

Linux动态链接之二:优化加速之延迟绑定PLT_第1张图片

其中第二项和第三项由动态链接器在装载共享模块的时候负责初始化。 PLT每项规定16个字节,刚好可以放3条指令,如下:

bar@plt:
jmp     *(bar@GOT)
push    n
jmp     PLT0

其中函数PLT0是通用的的,形式如下:

PLT0:
push   *(GOT+4)
jmp   *(GOT+8)

对照图片可以看到,moduleID是方便_dl_runtime_resolve()找到目标模块的”.got.plt”函数地址子表,而n则对应要修正函数的表项下标。这样便实现了一个利用桩代码完成的PLT延迟绑定操作。

你可能感兴趣的:(Linux内核,linux,PLT,延迟绑定)