Linux动态链接中的PLT表和GOT表
引言
文件在编译链接时,是不知道glibc动态库内printf的函数地址的,而只是写了一个“占位符”在汇编中,glibc动态库内printf的函数地址只有装载进内存时才能够知道
但是进程运行起来后,glibc动态库装载了,printf函数地址也确定了,那call指令是怎么重定位地址的呢?
一种简单的方法就是将指令中的“占位符”修改成printf函数的真实地址即可。但是随之又出现两个问题:
- 现代操作系统不允许修改代码段,只能修改数据段
- 如果函数是在一个动态库(.so)中,修改了代码段,那么它就无法做到系统内所有进程共享同一个动态库
所以prinf函数的地址只能回写到数据段,而不是代码段,这样的回写其实叫做运行时重定位,与之相应的还有链接时重定位
编译与链接
每个编译单元(通常是一个.cpp文件)都会经历编译和链接两个阶段
编译阶段是将.c源代码编译成汇编指令的中间文件,即.o中间文件。由于编译阶段无法知道printf函数的地址,所以call指令后会预先放一个占位符,然后用重定位项来描述:这个地址在链接时需要修正,它的修正值是根据printf函数地址(更确切应该说是符号,链接器眼中只有符号,没有函数、变量)来修正的,它的修正方式按相对引用方式。而这个过程叫做链接时重定位,与运行时重定位工作原理完全一样,只是修正时机不同
链接阶段是将一个或者多个中间文件(.o)通过链接器将它们链接成一个可执行文件,链接阶段主要完成以下事情:
- 各个中间文件之间的同名section合并
- 对代码段、数据段以及各符号进行地址分配
- 链接时重定位修正
除了重定位过程,其它动作是无法修改中间文件中函数体内指令的,而重定位过程也只能修改指令中的操作数,无法修改编译过程生成的汇编指令
编译器并不知道printf函数是在glibc动态库,而不是定义在其它.o文件中的,所以编译器只能老老实实生成调用printf函数的汇编指令,如果在其它.o中定义了printf函数,那么链接阶段,printf函数地址已经确定,可以直接重定位。但是如果printf定义在动态库内,链接阶段无法做重定位
我们前面知道了,运行时重定位是无法修改代码段的,只能将printf重定位到数据段。而编译阶段已生成好的call指令,如何找到这个已重定位好的数据段内容呢?
所以链接器会生成一段额外的小代码片段,通过这段代码支获取printf函数地址,并完成对它的调用
链接器生成额外的伪代码:
1 | .text |
链接阶段可以知道printf在哪定义,只不过定义在动态库内时并不知道它的地址。当链接阶段发现printf定义在动态库时,链接器会生成一段小代码printf_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接时重定位,而运行时才对printf做运行时重定位(其目的就是把本需要写入代码段的运行时地址,转移到数据段去写入)
PLT表与GOT表
动态链接需要考虑的因素说白了就两点:
- 需要存放外部函数的数据段
- 获取数据段存放函数地址的一小段额外代码
如果可执行文件中调用多个动态库函数,那么每个函数都需要这两样东西,这样就形成了两个表,每个函数使用中的一项
于是,存放函数地址的数据表,我们称为全局偏移表(GOT,Global Offset Table),位于数据段;而额外代码段表,称为程序链接表(PLT,Procedure Link Table),位于代码段。它们各司其职又相互配合,完成重定位这个过程。


