在学习极客时间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
意思就是:这段代码,无论加载在哪个内存地址,都能够正常执行
这个问题又可以分解为两个小问题:
-
动态链接库内部 地址无关
解决方案:
使用相对地址,一个相对于当前指令偏移量的内存地址 -
在不同的应用程序中,使用动态链接库,要做到地址无关,如下图
可以看到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中此函数的真正地址
但这个方案面临两个问题:
- 现代操作系统不允许修改代码段.text Section,只能修改数据段.data Section
- show_me_the_money函数在lib.so内,如果修改了代码段,那么其就无法做到系统内所有进程共享一个动态库
所以show_me_the_money函数地址,只能写回到数据段内,而不能回写到代码段上,这个回写,是指运行时修改,更专业的称谓是运行时重定位
具体是如何做的呢?(注:关于PLT及GOT的解释会在后文介绍,这里只是做过程介绍)
-
编译阶段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这个地址 -
这个地址在链接时要修正,它的修正值是根据show_me_the_money函数地址(更确切的叫法应该是符号,链接器眼中只有符号,没有所谓的函数和变量)来修正,它的修正方式按相对引用方式,这个过程叫做
链接时重定位
,与刚才提到的运行时重定位
工作原理完全一样,只是修正时机不同
除了重定位过程,其它动作是无法修改中间文件中函数体内指令的,而重定位过程也只能是修改指令中的操作数,换句话说,链接过程无法修改编译过程生成的汇编指令
-
由于我们的show_me_the_money是定义在动态链接库lib.so下,所以链接阶段无法做重定位,只能做运行时重定位,而运行时重定位是无法修改代码段的,所以只能将show_me_the_money
重定位到数据段
-
show_me_poor这个进程运行起来之后,lib.so动态库也装载了,show_me_the_money函数地址也确定了(
实际上show_me_poor GOT表中lib.so相关的数据,就是加载lib.so动态库的时候写进去的,写在了数据段的虚拟地址中
) -
现在的问题就是:编译阶段就已经生成好的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做运行时重定位
总结来说,动态链接需要考虑两点:
-
需要存放外部函数地址的数据段
-
获取数据段存放函数地址的一小段额外代码
如果可执行文件中调用多个动态库函数,那每个函数都需要这两样东西,这样每样东西就形成一个表,每个函数使用中的一项。
存放函数地址的数据表,称为全局偏移表(GOT, Global Offset Table)
额外代码段表,称为程序链接表(PLT,Procedure Link Table)
完整的过程总结
-
编译时重定向 来生成 重定位项,之后进行静态链接和动态链接
-
由于其需要进行动态链接,那么会生成两个表(不使用动态链接就不需要GOT和PLT了)
存放函数地址的数据表,称为全局偏移表(GOT, Global Offset Table)
额外代码段表,内容是获取数据段存放函数地址的一小段额外代码,称为程序链接表(PLT,Procedure Link Table)
-
show_me_poor这个进程运行起来,通过loader进行装载,lib.so动态库也装载了,在装载的时候,会把lib.so中show_me_the_money函数真实内存地址写入到show_me_poor进程控制块PCB的数据段.data Section的虚拟地址中的GOT中
-
当执行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的真实物理内存地址,真正执行函数