1、为什么要有PLT和GOT表两个概念,因为我们程序编译链接一系列过程为可执行文件时、执行可执行文件会调用一些函数,这些函数可能在编译生成的.o文件里获得地址,也可能在libc函数库里面来获得地址,如果我们在函数编译链接的过程中就进行大量的函数地址重定位工作,会占用大量内存以及消耗大量时间,所以,我们有这样一个机制:只有可执行文件在调用某个函数时,我们才对函数真正装载的地址进行寻址,这样减少了很多内存占用和时间消耗,现在我们详细研究下这个技术:·延迟绑定(PLT)的具体细节
2、我们为了简略,将代码生成可执行文件并执行的过程挑出三个阶段:编译,链接,执行
编译:编译阶段还不知道printf在glibc中还是在其他的.o中
链接:
- 如果是.o,则直接绑定到函数的真正地址,一般为自定义函数
- 如果是libc,则无法直接绑定函数真实地址,将地址位置绑定为函数对应的plt
运行:运行时绑定函数实际地址,通过_dl_runtime_resolve来寻找libc中函数地址,并绑定
3、上面的过程乍一看一定一脸懵逼,主要在于链接的第二步,plt做了哪些事情
void printf@plt()
{
fun:
jmp printf@got // 链接器将printf@got填成下一语句lookup_printf的地址
lookup_printf:
调用重定位函数查找printf地址,并写到printf@got
goto fun;
}
这是printf的plt函数伪代码,printf@got还不知道真实地址,我们需要通过调用下面的函数来寻找libc中printf的地址,来填入got表中,这样回来继续调用的时候,调用plt函数直接调转got函数就可以跳转到函数真实地址,而不用再进行寻找
4、plt属于代码段,进程加载运行都不会改变
plt指向got再编译时已经完全确定
唯一变化的是got表
5、plt和got的抽象
got[0]: 本ELF动态段(.dynamic段)的装载地址
got[1]:本ELF的link_map数据结构描述符地址
got[2]:_dl_runtime_resolve函数的地址
plt[0]:
pushl got[1]
jmp *got[2]
plt[n]: // n >= 1
jmp *got[n+2] // GOT前3项为公共项,第3项开始才是函数项,plt[1]对应的GOT[3],依次类推
push (n-1)*8
jmp plt[0]
got[0] = address of .dynamic section
got[1] = address of link_map object( 编译时填充0)
got[2] = address of _dl_runtime_resolve function (编译时填充为0)
got[n+2] = plt[n] + 6 (即plt[n]代码片段的第二条指令)
参考文章:1、https://blog.csdn.net/linyt/article/details/51635768
2、《程序员的自我修养》 7.4节延迟绑定