ELF动态链接时GOT、PLT原理

背景

我们知道linux为了降低可执行程序体积,提高空间利用效率,经常采用动态链接方式生成动态库或者可执行程序。在动态链接时,为了避免在加载时对代码段进行重定位导致动态库代码段无法实现共享,我们采用了位置无关代码PIC技术(Position-Independent Code)。针对模块外代码,为了实现PIC技术,我们需要借助全局偏移表(GOT,Gobal Offset Table)。

全局偏移表GOT

首先写一个小的测试程序进行说明。在SendMessage.c文件中实现了一个函数SendMessage1供外部模块调用。

SendMessage.c:

 在SendMessage.h文件中进行声明。

SendMessage.h:

首先将SendMessage.c编译成动态库libSendMessage.so

gcc -o libSendMessage.so -fPIC -shared SendMessage.c

 在main函数中调用动态库libSendMessage.so中的SendMessage1函数。

main.c:

 现在编译最终的可执行程序test

gcc -o test main.c -L ./ -lSendMessage -Wl,rpath=./

测试验证test程序可正常执行:

 上面可执行程序test调用了外部动态库libSendMessage.so中的函数SendMessage1。为了实现位置无关代码PIC,需要增加GOT段。其原理简单说就是,我们test程序在生成时是不知道外部函数SendMessage1的实际地址的,但我们又想实现动态库libSendMessage.so的代码共享,因此想出了一个办法,即test中对SendMessage1函数的地址引用均通过test自身数据段中一个具体位置(即GOT)的间接引用,也即该具体位置存放了SendMessage1最终加载的实际地址。因为test中调用SendMessage1函数指令位置和GOT间相对位置固定,两者存在固定偏移,因此动态库代码段在各个程序中使用时不需要修改,可以直接共享使用,实际只需要在加载时对GOT中填充各个外部引用全局变量、函数的实际加载地址即可。GOT段又存放在Data数据段中,即各个可执行程序都有独立的备份,互不干扰(和动态库数据段处理类似)。

过程链接表PLT

上面PIC中使用GOT可以解决动态库代码段直接共享问题,但在使用中发现由于程序运行前需要加载动态库并更新所有的外部符号表位置引用(GOT),这会导致程序启动较慢。为了解决该问题,我们又引入了过程链接表PLT(Procedure Linkage Table)。

我们在引用动态库时,经常发现动态库中很多函数并未使用,如果在加载时也对这部分函数进行重定位会变得很没效率和无意义,因此我们可以考虑在程序加载时先让程序运行,待需要用到某个外部符号时再对该符号进行重定位(即延迟绑定,Lasy Binding),这将有利于程序的启动速度,提高程序的运行效率。

为了实现延迟绑定技术,我们需要过程链接表PLT。PLT其实就是一段段可执行代码,该代码会实现外部符号的引用。具体为:

  1. 第一次调用该外部函数时会通过_dl_runtime_resole函数查找该外部符号的实际加载地址,并填充对应的GOT表,顺带调用该外部函数。
  2. 第二次调用该外部函数时,即可直接通过GOT表获取到该外部函数加载地址,从而达到引用效果,不再需要查找。

PLT结构如下:(bar为外部引用函数)

PLT0:
push *(GOT + 4)
jump *(GOT + 8)
....
bar@plt:
jmp *(bar@GOT)
push n
jump PLT0

PLT在ELF中结构如下:

 从上面可知,PLT将GOT分为.got和.got.plt,其中.got用于保存外部全局变量地址引用,.got.plt保存了外部函数符号地址引用。PLT段为可读可执行属性,和代码段放在一起。.got.plt段的前三行固定,分别保存动态库段.dynamic地址、引用模块ID和外部符号查找函数_dl_runtime_resolve地址。后面分别存在各个函数实际加载地址(第一次未初始化钱存放plt中下一行地址)。

实例说明

以上面程序为例进行说明。

  • 当test程序未运行时,查看其重定位段、plt段和got段,分别如下:

重定位段:

 从重定位段,我们可以看到外部函数符号SendMessage1放在GOT表中的0x000000200fd0地址。

plt段:

 got段:(本编译服务器got段未区分.got和.got.plt)

 从.got段,我们可以看到0x000000200fd0地址处当前存放的地址为0x00000000000005f6,也即SendMessage1@plt段中jmpq   *0x2009da(%rip)下一行地址,也即程序未运行时,got中存放的是下一行代码地址,这代价较小,不影响效率。

  • 当程序运行时.got段如下:

为了便于查看指定内存内容,利用gdb对test程序进程调试。在main函数调用SendMessage1处打断点使程序暂停。

1. 利用shell ps命令查看目标test程序的PID为32187

2. 查看test进程各个段加载地址

 从上图可知,test的代码段加载地址为0x555555554000,而从上面test的ELF文件信息中可知外部符号SendMessage1函数的GOT表位置(相对偏移)为0x200fd0,两者相加即可知加载后SendMessage1函数的GOT表位置的物理地址:

0x555555554000 + 0x200fd0 = 0x5555 5575 4fd0

3. 利用x命令查看地址0x5555 5575 4fd0保存内容:

 test程序为64位,且为小端模式,则可知0x5555 5575 4fd0中保存的内容为0x00007fff f7bd161a,也即为SendMessage1函数符号最终的加载的物理地址。

4. 利用disassemble命令查看0x00007fff f7bd161a地址进行验证:

该地址即为SendMessages1函数,符合预期。 

 5. 从libSendMessage.so动态库实际加载位置角度验证符号SendMessage1函数最终的加载物理地址。

  • 首先查看SendMessage1函数在libSendMessage.so动态库中相对偏移。

从上面可知SendMessag1函数在libSendMessage.so库中的偏移为0x61a。 

  •  从上面步骤二图中可知libSendMessage.so动态库代码段加载地址(也为整个libSendMessage.so库的加载基地址)为0x7ffff7bd1000。其和SendMessage1函数在libSendMessage.so库中偏移0x61a相加即为函数SendMessage1在test中的最终加载地址:

        0x7ffff7bd1000 + 0x61a = 0x7ffff7bd161a

这和上面步骤4中推算的函数SendMessag1最终加载地址一致,即验证了推导结果的正确性。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
lab4主要是关于elf格式和链接器的内容。 elf格式是一种用于表示可执行文件、目标文件、共享库等二进制文件的标准格式。它提供了一种通用的、可移植的文件格式,使得不同体系结构的机器能够共享相同的程序和库。elf文件由多个(Segment)和节(Section)组成,其中用于表示程序运行的基本单位,而节用于存储与程序的静态结构相关的信息。 链接器(linker)是一种用于将多个目标文件合并成一个可执行文件的工具。链接器负责解析目标文件中的符号(符号表中的符号)并进行符号地址的重定位,然后生成最终的可执行文件。链接过程中还可能进行符号的强引用和弱引用解析、合并相同的节等操作。 在链接过程中,链接器主要完成以下几个步骤:首先,链接器会将所有输入的目标文件合并成一个输出文件。其次,链接器会解析符号引用,通过符号表找到对应的符号定义,并进行符号地址的重定位。对于强符号引用,链接器会将其绑定到合适的地址上;对于弱符号引用,链接器会在地址没有被绑定的情况下给符号一个默认值。最后,链接器会根据需要调整节的地址和大小,合并相同的节,以及进行其他的优化。 通过学习elf格式和链接器的相关知识,我们可以更好地理解并应用于程序的开发和调试中。理解elf格式可以帮助我们更好地理解程序的内部结构和特性,而链接器则是将多个目标文件合并成一个可执行文件的重要工具。对于理解二进制文件的内容和进行程序的调试和优化都有重要的意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值