定义: 将函数地址的解析推迟到函数调用的时候。
实现方式:通过全局偏移量表【GOT】和过程链接表【PLT】之间的交互来完成。对于每一个调用了定义在共享库中函数的目标模块,那么它就会维持一个自己的GOT和PLT。其中GOT属于数据段的一部分、PLT属于代码段的一部分。
相关数据结构
-
过程链接表【PLT】:该结构是一个数组,每一个数据成员占用16字节的大小。雁过留声,每一个在共享库中定义的函数都可以在该数组中找到身影。我们下来看一下具体的数据成员所代表的含义:
- PLT[0]: 是一个特殊的条目,它存在的目的主要是为了跳转到动态链接器中,实现对所调用函数在GOT表中的地址的修改。这个我们稍后会详细谈到。
- PLT[1]:存放的是系统的启动函数,完成对启动环境的初始化然后调用main函数并处理其返回值。
- 之后的成员存放的是用户提供的具体函数。
-
全局偏移量表【GOT】:该结构的存在形式也是一个数组,不同的是每一个条目所占用的大小是8字节,其中PLT表中的数据存放的是相对应的GOT表项的地址。最后对函数实际地址的查找也是通过PLT表的过渡最终由该表最实现的。当函数第一次调用时,GOT表项中的数据指向对应PLT表中的下一条指令。同样的,我们看一下它的成员所代表的含义:
- GOT[0]与GOT[1]:存放的是函数地址解析时动态链接器需要的有关信息。
- GOT[2]:存放的是动态链接器模块(Linux下是ld-linux.so)的入口地址
- GOT[3]:系统的启动函数
- 之后的数据成员存放的是与PLT对应的用户所提供的函数
用到的程序:
main 函数:
#include "sum.h"
int main (void) {
int ret = sum(1,2);
printf("ret:%d\n",ret);
return 0;
}
子函数sum.h:
#ifndef _SUM_H_
#define _SUM_H_
#include <stdio.h>
int sum (int a,int b);
#endif
子函数sum.c
#include "sum.h"
int sum (int a,int b) {
return a+b;
}
介绍完相关的数据结构后,我们接下来看一下具体的函数地址解析过程:
我们直接看一下可执行程序的反汇编代码:
我们可以看到sum@plt这个标志,其中@plt函数是编译器自己加的,我们可以看看里面的代码。
我们可以通过objdump -R 查看一个可执行程序的GOT地址
写在前面:
程序调用会首先进入相应的PLT条目,接着会执行图五中的三行代码,#后面所跟的十六进制数是该函数在GOT中对应的表项所在的地址,这几行代码分别完成的功能是
- 跳转:根据图四图五我们可以判断目前该函数即sum在PLT表中对应的表项为PLT[2],地址0x4020所对应的GOT表项为GOT[4],也就是该函数在GOT表中所存放的地址。此时,程序由PLT[2] -> GOT[4]。目前GOT[4]中保存的数据为对应的PLT[2]中指令的下一条:即后面紧跟的压栈指令
- 压栈:将调用函数sum的ID(0x1)压入栈中
- 跳转到PLT[0]所在的位置。
接下来我们看一下跳转到PLT[0]后编译器所做的事情。
- 我们可以看到首先将动态链接器的一个参数即GOT[1]压栈
- 跳转至GOT[2]即动态链接器的入口地址。此时动态链接器利用已经压入栈的两个参数生成该函数(sum)的实际运行地址,然后利用该地址重写GOT[4],并将程序的控制权交给所调用函数。
- 当后续目标模块中调用函数sum时,该函数还是首先会进入PLT[2]条目,然后跳转到GOT[4],不同的是,此时程序间接跳转会将控制直接转移到sum调用。至此,函数地址的解析便告一段落。