示例程序 main.c:
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}
一个源文件想变成能够在内存中运行的程序,需经历编译、链接、载入3个步骤。
编译:gcc -Wall -g -o main.o -c main.c -m32
链接:gcc -o main main.o -m32
注:
可执行文件跟.o的区别在于:可执行文件有一个可由操作系统调用的入口函数main。
1、编译
编译生成的每一个.o文件包含3部分内容:
1)代码段:存放当前源文件定义的函数;
2)数据段:存放当前源文件定义的全局变量;
3)符号表:存放当前源文件未定义却使用了的函数或全局变量(它们可能由其他.o或.so定义)。
在符号表中,未定义的函数/全局变量被标记为UND,即undefined。(可使用命令:objdump -x XXX.o 查看XXX.o的符号表和重定位表)
在编译阶段,可确定前两部分相对当前.o起始位置的偏移地址,但对于第三部分,由于编译器不清楚printf的地址,因此此处预写一个 0XFF FF FF FC(即-4),用于告诉链接器:需根据printf地址来修正该地址。
2、链接(ld)
链接包括静态链接、载入时动态链接、运行时动态链接。
2.1、静态链接(.o之间的链接或.o与静态库之间的链接)
链接器根据链接顺序,将所有的.o(对于静态库,只会合并所用到的目标文件)合并为一个可执行文件:
1)所有.o中的代码段/数据段 合并到 可执行文件的代码段/数据段。合并后,可执行文件中的函数/全局变量相对文件起始位置的偏移地址,即为最终内存地址。
2)对于符号表中的函数/全局变量,若它们是由其他.o或.a定义,则通过查找前一步的合并结果,即可确定其最终内存地址,并将0XFF FF FF FC修正为最终内存地址即可。
2.2、载入时动态链接
在2.1中,对于.so定义的函数/全局变量的最终内存地址,链接器是无法确定的,而只能在载入动态库后确定。那么,在载入动态库glibc后,应如何让call指令找到printf的地址?
最为简单的方法是,直接将0XFF FF FF FC修改为printf的真正地址即可。问题在于:
1)call指令位于代码段中,而代码段在运行时是不允许被修改;
2)即使代码段允许修改,但若print_banner定义在.so中,则修改后就无法在多个进程之间共用这个.so。因为它们共享.so的代码段,但每个进程都有一份独立的数据段。
那么,能否在.so加载后,将函数/数据的地址存入数据段,而代码直接读取数据段内的地址?
Yes!
这个存储函数/数据地址的数据段,即为GOT(Global Offset Table,全局偏移表)。表中的每一项为函数/数据的地址。
因此链接器的做法是,生成一段额外代码print_stub,并将call printf指令重定位到print_stub,而printf_stub又重定位到GOT中的函数/数据地址。
这个存储额外代码的表,称为PLT(Procedure Link Table,程序链接表)。
而载入时动态链接是指,在将可执行文件加载到内存后,且在程序开始运行前,查找链接时所指定的动态库,并将GOT中的函数/数据地址修正为其真正地址。
那么程序在运行阶段,printf的调用过程:
2.3、PLT/GOT
PLT表包含若干项,第一项为公共项,指向GOT中的第二项,即动态链接器内的_dl_runtime_resolve函数(该项在链接阶段为0X000000,载入阶段由动态链接器填充),其余每一项对应一个动态库函数。(每一项包含若干指令)
第一次调用printf:
1)调用PLT中printf对应的代码片段;
2)调用GOT中存储的printf。此处存储的地址是1)中PLT的下一条指令的地址,因此程序调回到PLT中;
3)调用PLT第一项_dl_runtime_resolve对应的代码段;
4)调用GOT中存储的_dl_runtime_resolve函数的真实地址,而_dl_runtime_resolve会去.so中查找printf的地址,并将2)中GOT存储的printf地址修改为printf的真实地址(那么下次调用无需_dl_runtime_resolve),并调用printf函数。
非第一次调用printf:
2.4 PIC(position-independent code,地址无关代码)
对于动态库来说,是指将动态库中的代码段映射到不同进程的地址空间,其实现是借助GOT/PLT表。
原理是:当某指令想访问某个函数/数据时,并不是通过绝对地址进行访问,而是访问存放函数/数据的绝对地址的GOT表。(指令与GOT的相对位置链接器已知)
2.5、运行时动态链接(延迟重定位)
运行时动态链接是指,在编译链接时不指定所依赖的动态库,而在程序中使用dlopen/dlsym时,才对GOT表项做重定位。因此相比载入时动态链接,可加快程序的启动速度。
3、静态链接 VS 动态链接
对于同时包含静态/动态链接库的若干个可执行文件:
1)磁盘空间:每个可执行文件中都有一份静态链接库的代码/数据,但仅包含动态链接库的必要信息。
2)独立运行:动态链接生成的执行文件不可独立运行;
3)性能:加载时动态链接会拖慢程序的启动速度,对于运行时动态链接,当首次调用动态库的函数时,程序会被暂停直至链接结束。
4)内存:若干个同时运行的程序,都使用了某个静态链接库/动态链接库,则内存中会有该静态链接库的多个副本,而大家共用一份动态链接库。
5)重新编译:若修改了动态库,只要未改变接口,则无需重新编译可执行文件。