Content #
假设liba.so需要调用libc.so中的bar()函数,那么当liba.so中第一次调用 bar()时,这时候就需要调用动态链接器中的某个函数来完成地址绑定工作,我们假设这个函数叫做lookup(),那么lookup()至少需要知道这个地址绑定发生在哪个模块,哪个函数?那么我们可以假设lookup的原型为lookup(module, function),这两个参数的值在我们这个例子中分别为liba.so和bar()。在 Glibc中,我们这里的lookup()函数真正的名字叫_dl_runtime_resolve()。
当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中相应的项进行间接跳转。PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转。调用函数并不直接通过GOT跳转,而是通过一个叫作PLT项的结构来进行跳转。每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的项的地址我们称之为bar@plt。让我们来看看bar@plt的实现:
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve
bar@plt的第一条指令是一条通过GOT间接跳转的指令。bar@GOT表示GOT中保存 bar()这个函数相应的项。如果链接器在初始化阶段已经初始化该项,并且将 bar()的地址填入该项,那么这个跳转指令的结果就是我们所期望的,跳转到 bar(),实现函数正确调用。
但是为了实现延迟绑定,链接器在初始化阶段并没有将bar()的地址填入到该项,而是将上面代码中第二条指令“push n”的地址填入到bar@GOT中,这个步骤不需要查找任何符号,所以代价很低。很明显,第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。
第二条指令将一个数字n压入堆栈中,这个数字是bar这个符号引用在重定位表“.rel.plt”中的下标。
接着又是一条push指令将模块的ID压入到堆栈,然后跳转到_dl_runtime_resolve。这实际上就是在实现我们前面提到的lookup(module, function)这个函数的调用:先将所需要决议符号的下标压入堆栈,再将模块ID压入堆栈,然后调用动态链接器的_dl_runtime_resolve()函数来完成符号解析和重定位工作。_dl_runtime_resolve()在进行一系列工作以后将bar()的真正地址填入到 bar@GOT中。
一旦bar()这个函数被解析完毕,当我们再次调用bar@plt时,第一条jmp指令就能够跳转到真正的bar()函数中,bar()函数返回的时候会根据堆栈里面保存的 EIP直接返回到调用者,而不会再继续执行bar@plt中第二条指令开始的那段代码,那段代码只会在符号未被解析时执行一次。
From #
程序员的自我修养