关于动态链接与静态链接简单理解:如果我的文章引用了别人的一部分文字,在我发布文章的时候把别人的段落复制到我的文章里面就属于静态连接,而做一个超链接让你们自己去看就属于动态链接了
PLT&GOT
在介绍PLT和GOT表之前,先列举一个例子
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}
编译
gcc -Wall -g -o test.o -c test.c -m32
链接
gcc -o test test.o -m32
通过编译与链接,可以得到一个test.o与可执行文件test
通过objdump -d test.o
可以查看文件的反汇编代码
print_banner函数调用了printf函数,但是printf函数位于glibc动态库中,在编译和链接阶段,没有办法确定地址。只用在进程运行时,才可以确定地址,所以先用fc ff ff ff填充,也就是有符号数 -4 代替。
用重定位向来描述:这个地址在链接时要修正,它的修正值是根据printf地址(更确切的叫法应该是符号,链接器眼中只有符号,没有所谓的函数和变量)来修正,它的修正方式按相对引用方式。
在程序运行时,重定位,不能修改代码段的数据,只能修改数据段内的数据,所以要如何确定printf的真正地址呢?
运用的是链接时重定位,但是连接过程无法修改编译过程生成的汇编指令,而编译器也无法知道printf函数是在glibc运行库还是在其他.o中
所以链接器生成一段额外的小代码片段,通过这段代码来获取printf函数地址,并完成对它的调用
.text
...
// 调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
jmp rax // 跳过去执行printf函数
.data
...
printf函数的储存地址:
这里储存printf函数重定位后的地址
动态链接每个函数实际总结有了两点:
- 1、需要存放外部函数地址的数据段
- 2、获取数据段存放函数地址的一小段额外代码
所以就引出了GOT和PLT表
其中用来需要存放外部函数地址的数据段的数据表称为全局偏移表(GOT, Global Offset Table),用来存放那一小段额外代码的数据表称为程序链接表(PLT,Procedure Link Table)
可执行文件里面保存的是 PLT 表的地址,对应 PLT 地址指向的是 GOT 的地址,GOT 表指向的就是 glibc 中的地址
也可以说PLT表中的每一项的数据内容都是对应的GOT表中一项的地址这个是固定不变的
延迟绑定
通过上边的过程,可以发现通过真个过程比较麻烦,所以Linux引入了延迟绑定
延迟绑定(Lazy Binding)的要求:即函数第一次被用到时才进行绑定。通过延迟绑定大大加快了程序的启动速度。而 ELF 则使用了PLT(Procedure Linkage Table,过程链接表)的技术来实现延迟绑定。
可以使用类似的代码来实现
//一开始没有重定位的时候将 printf@got 填成 lookup_printf 的地址
void printf@plt()
{
address_good:
jmp *printf@got
lookup_printf:
调用重定位函数查找 printf 地址,并写到 printf@got
goto address_good;//再返回去执行address_good
}
在链接成可执行文件test时,链接器将printf@got表项的内容填写lookup_printf标签的地址。
解释一下这段代码的流程,刚开始,printf@got 是 lookup_printf 函数的地址,这个函数用来寻找 printf() 的地址,然后写入 printf@got,lookup_printf 执行完成后会返回到 address_good,这样再 jmp 的话就可以直接跳到printf 来执行了
也就是说这样的机制的话如果不知道 printf 的地址,就去找一下,知道的话就直接去 jmp 执行 printf 了
参考中说明利用下面的命令可以反编译生成汇编代码,但我的没有显示,就借用一下大佬的图片吧
objdump -d test > test.asm
将第一项plt表改为了 common@plt ,因为objdump -d输出结果会使用错误的符号名
通过gdn的查看,我么可以明显看出,地址跳转到顺序
xxx@plt -> xxx@got -> xxx@plt -> 公共@plt -> _dl_runtime_resolve
我们可以看到它push了一个参数,而这个参数就相当于函数的ID
push $0x0 //将数据压到栈上,作为将要执行的函数的参数
jmp 0x80482d0 //去到了第一个表项
补充:
ELF将GOT拆为了两个表叫做“.got”,".got.plt"。其中 .got 用来保存全局变量的引用地址,.got.plt 用来保存函数引用的地址,也就是说,所有对于外部函数的引用被分离到了 .got.plt 表中
在 i386 架构下,除了每个函数占用一个 GOT 表项外,GOT 表项还保留了3个公共表项,也即 got 的前3项,分别保存:
got [0]: 本 ELF 动态段 (.dynamic 段)的装载地址
got [1]:本 ELF 的 link_map 数据结构描述符地址
got [2]:_dl_runtime_resolve 函数的地址
动态链接器在加载完 ELF 之后,都会将这3地址写到 GOT 表的前3项
参考中所说的大佬的流程图
第一次调用:
再次调用: