动态链接原理 --- PLT/GOT

如果调用者使用了共享库的符号,则调用者的数据段会有一个GOT,用于记录共享库符号的地址;如果共享库A作为调用者使用了共享库B的符号,则共享库A的数据段也会有一个GOT。由于编译的时候不能知道共享库的符号地址,所以调用者通过GOT获取共享库的符号地址,运行时链接只需要修改位于数据段的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;
}

经编译和链接阶段之后,test可执行文件中print_banner函数的汇编指令如下:

080483cc <print_banner>:
 80483cc:    push %ebp
 80483cd:    mov  %esp, %ebp
 80483cf:    sub  $0x8, %esp
 80483d2:    sub  $0xc, %esp
 80483d5:    push $0x80484a8  
 80483da:    call **<printf函数的地址>**
 80483df:    add $0x10, %esp
 80483e2:    nop
 80483e3:    leave
 80483e4:    ret
print_banner函数内调用了printf函数,而printf函数位于glibc动态库内,所以在编译和链接阶段,链接器无法知知道进程运行起来之后printf函数的加载地址。故上述的**<printf函数地址>** 一项是无法填充的,只有进程运运行后,printf函数的地址才能确定。

那么问题来了:进程运行起来之后,glibc动态库也装载了,printf函数地址亦已确定,上述call指令如何修改(重定位)呢?

一个简单的方法就是将指令中的**<printf函数地址>**修改printf函数的真正地址即可。

但这个方案面临两个问题:

  • 现代操作系统不允许修改代码段,只能修改数据段
  • 如果print_banner函数是在一个动态库内,修改了代码段,那么它就无法做到系统内所有进程共享同一个动态库。

 

因此,printf函数地址只能回写到数据段内,而不能回写到代码段上。

注意:刚才谈到的回写,是指运行时修改,更专业的称谓应该是运行时重定位,与之相对应的还有链接时重定位

根据前面讨论,运行时重定位是无法修改代码段的,只能将printf重定位到数据段。那在编译阶段就已生成好的call指令,怎么感知这个已重定位好的数据段内容呢?

答案是:链接器生成一段额外的小代码片段,通过这段代码获取printf函数地址,并完成对它的调用

链接器生成额外的伪代码如下:

.text
...

// 调用printf的call指令
call printf_stub
...

printf_stub:
    mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
    jmp rax // 跳过去执行printf函数

.data
...
printf函数的储存地址:
  这里储存printf函数重定位后的地址

链接阶段发现printf定义在动态库时,链接器生成一段小代码print_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接重定位,而运行时才对printf做运行时重定位。

存放函数地址的数据表,称为全局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序链接表(PLT,Procedure Link Table)。

 

got 表即: 全局偏移表 , Global Offset Table. 当动态链接库中的代码使用PIC模式编译出地址无关代码时,在调用方需要生成相应的 .got 段来存储函数的地址。相当于一个函数指针来寻址动态链接库中的函数,这样做的原因: 把原本在 .text section 中对一个动态库中函数地址的相对地址调用转换为从数据段 .got 中的函数指针进行间接调用,这样就可以使主模块中(只是这里举例的场景, 调用者不一定是主模块) 的 .text section 的代码中的函数地址不用重定位。

 

当启用延时加载特性时,我们的函数的间接调用会再加一层间接调用,也就是 .plt 段。此时,调用时: caller -> targetFun@plt -> targetFun@got.plt -> 目标函数。而 .plt 段的目的是提供一个特殊代码序列,当没有加载地址时,会在 plt 段顺序执行以触发延时加载。当加载完成后,以后都如上面的链条一样两次跳转后到达真正的函数。当有使用 .plt 段的时候,原来的 .got 不再存储函数的指针。此时会有另外一个类似的段来承担 相应的工作: .plt.got。

 

posted @ 2024-01-14 00:03  流水灯  阅读(128)  评论(0编辑  收藏  举报