动态链接、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;
}
经过编译与链接后,可执行文件中print_banner
函数的汇编指令是怎样的呢?我猜应该与下面的汇编类似:
080483cc <print_banner>:
80483cc: push %ebp
80483cd: mov %esp, %ebp
80483cf: sub $0x8, %esp
80483d2: sub $0xc, %esp
80483d5: push $0x80484a8
80483da: call <printf函数的地址>
80483df: add $0x10, %esp
80483e2: nop
80483e3: leave
80483e4: ret
print_banner
函数内调用了printf
函数,而printf
函数位于glibc
动态库内,所以在编译和链接阶段,链接器无法知知道进程运行起来之后printf
函数的加载地址。故上述的<printf函数的地址>
一项是无法填充的,只有进程运行后,printf
函数的地址才能确定。
在运行时如何进行重定位呢?一个简单的方法就是直接把<printf函数的地址>
修改为这个函数真正的地址即可。
这个方案有以下两个缺点:
- 现代操作系统不允许修改代码段,只能修改数据段;
- 如果
print_banner
函数是在一个动态库(.so对象)内,修改了代码段,那么它就无法做到系统内所有进程共享同一个动态库。
因此,直接更改代码段是不可行的,我们应当通过更改数据段来实现我们的目标。
运行时重定位无法修改代码段,只能将printf
重定位到数据段。那在编译阶段就已生成好的call
指令,怎么感知这个已重定位好的数据段内容呢?
答案是:链接器生成一段额外的小代码片段,通过这段代码支获取printf
函数地址,并完成对它的调用。
链接器生成额外的伪代码如下:
.text
...
// 调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
jmp rax // 跳过去执行printf函数
.data
...
printf函数的储存地址:
这里储存printf函数重定位后的地址
我们通过将<printf函数的地址>
替代为<printf_stub函数的地址>
,然后插入一段printf_stub
的代码。其中插入的代码表项我们称其为程序链接表(PLT,Procedure Link Table),位于代码段中,而存放函数地址的数据表项我们称其为全局偏移表(GOT, Global Offset Table),位于数据段中。
当确定了printf
函数真正的地址之后,如何跳转过去呢?因为在运行过程中printf
函数是一个外部函数,并没有位于我们进程当中的代码段中。答案是每一个进程都维护了一个共享库的内存映射区域,大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域,这部分区域主要与链接过程相关。