一.简介plt和got表
1.plt
PLT : 程序链接表(PLT,Procedure Link Table)
2.got
GOT : 全局偏移表(GOT, Global Offset Table)
plt以及got的缘由都来自于文件的动态链接,简言之:
- 需要存放外部函数的数据段 —— PLT
- 获取数据段存放函数地址的一小段额外代码 —— GOT
如果可执行文件中调用多个动态库函数,那每个函数都需要这两样东西,这样每样东西就形成一个表,每个函数使用中的一项。
存放函数地址的数据表,称为全局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序链接表(PLT,Procedure Link Table)。
注:更详细的解释可以查看大佬博客,这里就不做深入解释。
https://blog.csdn.net/farmwang/article/details/73556017
二.保护机制
RELRO(RELocation Read Only)
在Linux中有两种RELRO模式:”Partial RELRO” 和 “Full RELRO”。Linux中Partical RELRO默认开启
Partial RELRO:
特点:
- 该ELF文件的各个部分被重新排序。内数据段(internal data sections)(如.got,.dtors等)置于程序数据段(program’s data sections)(如.data和.bss)之前;
- 无 plt 指向的GOT是只读的;
- GOT表可写(应该是与上面有所区别的)。
Full RELRO:简言之,会导致GOT表完全不可写。
- 支持Partial模式的所有功能;
- 整个GOT表映射为只读的。
三.调用过程
分为第一次和第二次调用:
第一次调用:
在函数调用时,程序会call plt表中的地址,而plt表会跳转got表中的地址同时给plt表项的后六个字节,这个时候plt表得知got表没有自己想要jmp的地址,转身亲自去找需要跳转的地址,在这个过程还会将某个数压入栈中,这个数可以看出需要调用函数的ID,之后跳转回plt执行_dl_runtime_resolve函数(会根据函数的ID寻找对应函数),找到以后,plt表会将找到的函数地址给got表,然后plt跳转到相应的函数地址,成功执行call命令。
第二次调用(同一个函数):
plt会跳转到got表寻找函数地址,got中因为第一次调用时与函数绑定,可以直接跳转到相应的函数地址。
延迟绑定
这就是为什么在第一次调用时got表中没有函数地址。
当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。
为了提到cpude效率,在程序加载时并不会解析所有函数,而是在某个函数被调用时通过plt和got来对函数解析,然后将获得的函数地址放在got中,下一次调用就会直接使用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
objdump -d test.o
printf () 和函数是在 glibc 动态库里面的,只有当程序运行起来的时候才能确定地址,所以此时的 printf () 函数先用 fc ff ff ff 也就是有符号数的 -4 代替
程序运行时,printf的地址就会被存储到data段(data段中存储printf地址相当于printf的got表),(相当于printf的plt表)printf_sub就会跳转到data段中printf地址,最后到printf函数。
执行过程如上所示。
参考博客
https://blog.csdn.net/MrTreebook/article/details/124391848
https://zhuanlan.zhihu.com/p/130271689
https://eli0t-g.github.io/post/plt%E3%80%81got%E5%92%8C%E5%BB%B6%E8%BF%9F%E7%BB%91%E5%AE%9A/