动态链接的过程研究及PLT以及GOT的功能说明

在学习极客时间geektime 徐文浩老师的《深入理解计算机组成原理》课程中的动态链接一节时,对PLT及GOT的功能不是很理解,查看了一些资料进行了相关的学习,如果理解的有误,请大家不吝赐教。

前置知识1:静态链接 动态链接

链接器链接存储在硬盘上的目标文件代码,合并代码段的方法,是静态链接(Static Link)

动态链接Dynamic Link,链接的,不是存储在硬盘上的目标文件代码,而是加载到内存中的共享库(Shared Libraries)

注:共享库在内存中也是采用分页机制的。

在 Windows 下,这些共享库文件就是.dll 文件,也就是 Dynamic-Link Libary(DLL,动态链接库)

在 Linux 下,这些共享库文件就是.so 文件,也就是 Shared Object(一般我们也称之为动态链接库)

前置知识2:地址无关代码 地址相关代码

大部分函数库其实都可以做到地址无关,因为它们都接受特定的输入,进行确定的操作,然后给出返回结果就好了。无论是实现一个向量加法,还是实现一个打印的函数,这些代码逻辑和输入的数据在内存里面的位置并不重要。

常见的地址相关的代码,比如绝对地址代码(Absolute Code)、利用重定位表的代码等等,都是地址相关的代码。你回想一下我们之前讲过的重定位表。在程序链接的时候,我们就把函数调用后要跳转访问的地址确定下来了,这意味着,如果这个函数加载到一个不同的内存地址,跳转就会失败。

如何实现使用动态链接库共享内存

场景

共享内存动态链接库,要解决的问题:

这些机器码必须是 地址无关的 Position-Independent Code

意思就是:这段代码,无论加载在哪个内存地址,都能够正常执行

这个问题又可以分解为两个小问题:

  1. 动态链接库内部 地址无关
    解决方案:
    使用相对地址,一个相对于当前指令偏移量的内存地址

  2. 在不同的应用程序中,使用动态链接库,要做到地址无关,如下图

可以看到ab应用代码中动态链接库的虚拟内存地址与cd应用代码不同

案例

源代码:

有一个lib.h文件,lib.c文件,lib.c文件中提供了一个show_me_the_money函数

在show_me_poor.c这个文件中调用了共享库lib.so中show_me_the_money函数

机器码及汇编代码:

……

0000000000400540 show_me_the_money@plt-0x10:

400540: ff 35 12 05 20 00 push QWORD PTR [rip+0x200512] # 600a58 <GLOBAL_OFFSET_TABLE+0x8>

400546: ff 25 14 05 20 00 jmp QWORD PTR [rip+0x200514] # 600a60 <GLOBAL_OFFSET_TABLE+0x10>

40054c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]

0000000000400550 <show_me_the_money@plt>:

400550: ff 25 12 05 20 00 jmp QWORD PTR [rip+0x200512] # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>

400556: 68 00 00 00 00 push 0x0

40055b: e9 e0 ff ff ff jmp 400540 <_init+0x28>

……

0000000000400676 :

400676: 55 push rbp

400677: 48 89 e5 mov rbp,rsp

40067a: 48 83 ec 10 sub rsp,0x10

40067e: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5

400685: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]

400688: 89 c7 mov edi,eax

40068a: e8 c1 fe ff ff call 400550 <show_me_the_money@plt>

40068f: c9 leave

400690: c3 ret

400691: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]

400698: 00 00 00

40069b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]

……

解决方案

参考:聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT

show_me_poor这个进程运行起来之后,lib.so动态库也装载了,show_me_the_money函数地址也确定了,这个call指令如何重定位呢?

一个简单的方法就是将指令中的 show_me_the_money函数地址 修改为lib.so中此函数的真正地址

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

  1. 现代操作系统不允许修改代码段.text Section,只能修改数据段.data Section
  2. show_me_the_money函数在lib.so内,如果修改了代码段,那么其就无法做到系统内所有进程共享一个动态库

所以show_me_the_money函数地址,只能写回到数据段内,而不能回写到代码段上,这个回写,是指运行时修改,更专业的称谓是运行时重定位

具体是如何做的呢?(注:关于PLT及GOT的解释会在后文介绍,这里只是做过程介绍)

  1. 编译阶段show_me_poor程序是不知道show_me_the_money函数地址的,其使用重定位项来描述:
    40068a: e8 c1 fe ff ff call 400550 <show_me_the_money@plt>
    @plt关键字,代表我们需要从PLT,程序链接表Procedure Linkage Table里面找到调用的函数,而这个表的地址,就是400550这个地址

  2. 这个地址在链接时要修正,它的修正值是根据show_me_the_money函数地址(更确切的叫法应该是符号,链接器眼中只有符号,没有所谓的函数和变量)来修正,它的修正方式按相对引用方式,这个过程叫做链接时重定位,与刚才提到的运行时重定位工作原理完全一样,只是修正时机不同

除了重定位过程,其它动作是无法修改中间文件中函数体内指令的,而重定位过程也只能是修改指令中的操作数,换句话说,链接过程无法修改编译过程生成的汇编指令

  1. 由于我们的show_me_the_money是定义在动态链接库lib.so下,所以链接阶段无法做重定位,只能做运行时重定位,而运行时重定位是无法修改代码段的,所以只能将show_me_the_money重定位到数据段

  2. show_me_poor这个进程运行起来之后,lib.so动态库也装载了,show_me_the_money函数地址也确定了(实际上show_me_poor GOT表中lib.so相关的数据,就是加载lib.so动态库的时候写进去的,写在了数据段的虚拟地址中

  3. 现在的问题就是:编译阶段就已经生成好的call指令,怎么感知这个已经重定位好的数据段内容呢?

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

    链接阶段发现show_me_the_money定义在动态库时,链接器生成一段小代码,比如show_me_the_money@plt,然后show_me_the_money@plt地址取代原来的调用函数。

    因此转化为链接阶段对show_me_the_money做链接重定位,而运行时才对show_me_the_money做运行时重定位

总结来说,动态链接需要考虑两点:

  1. 需要存放外部函数地址的数据段

  2. 获取数据段存放函数地址的一小段额外代码

如果可执行文件中调用多个动态库函数,那每个函数都需要这两样东西,这样每样东西就形成一个表,每个函数使用中的一项。

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

完整的过程总结

  1. 编译时重定向 来生成 重定位项,之后进行静态链接和动态链接

  2. 由于其需要进行动态链接,那么会生成两个表(不使用动态链接就不需要GOT和PLT了)

    存放函数地址的数据表,称为全局偏移表(GOT, Global Offset Table)

    额外代码段表,内容是获取数据段存放函数地址的一小段额外代码,称为程序链接表(PLT,Procedure Link Table)

  3. show_me_poor这个进程运行起来,通过loader进行装载,lib.so动态库也装载了,在装载的时候,会把lib.so中show_me_the_money函数真实内存地址写入到show_me_poor进程控制块PCB的数据段.data Section的虚拟地址中的GOT中

  4. 当执行call show_me_the_money函数的时候,其执行下面的过程

    伪代码形式

    图形式

    图1:编译时重定向生成的重定位项

    图2:去PLT表中获取GOT虚拟地址空间

    图3:从GOT中获取lib.so中show_me_the_money函数的真实物理地址

    图4:写的是lib.so在show_me_poor虚拟内存中的地址,而这个虚拟内存中的地址对应的就是实际上lib.so的真实物理内存地址,真正执行函数

发布了442 篇原创文章 · 获赞 222 · 访问量 115万+

猜你喜欢

转载自blog.csdn.net/u013905744/article/details/104003247