动态链接、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 0xc, %esp
80483d5: push 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 标准库和数学库这样的共享库的代码和数据的区域,这部分区域主要与链接过程相关。
为什么动态连接编译还要带上 so 文件?
假如说 Program1.c
和 Program2.c
两个文件都引用了动态链接库 Lib.so 中的文件,那么为什么在动态链接的时候还要指定上 Lib.so 呢?
因为虽然是动态链接,但是编译的时候总要告诉 Program,foobar 这个函数是动态链接吧。而 footbar 的符号信息就在 Lib.so 中,所以这里编译时还是要带上 Lib.so
。这样,当编译器找不到 Program1.c
, Program2.c
中 foo()
函数的实现时,再对比 Lib.so
中的符号表,那么就可以轻而易举地得出这个符号是需要动态链接的了。
gcc -o Program1 Program1.c ./Lib.so
gcc -o Program2 Program2.c ./Lib.so