转载0005 plt表和got表5
前言
本文转载自知乎,仅用作存储用。
作者主页:红色的红
正文

- 魔鬼都藏在细节中, 不深入细节我总以为我已经懂了. 此文就是用于解决所有细节的落脚。
待解决的疑问:
- 动态库中的PLT表与主程序中的PLT表是否为同一份。
- 动态库中的GOT.PLT表与主程序中的GOT.PLT表是否为同一份。
- PLT表是代码还是数据。
- 动态库内部函数之间的调用为何不直接使用相对地址调用。
- 由于
.plt段的作用(用于延时加载)不是 这篇文章的重点. 我们都知道其延时加载的原理和基本方法.这里就不额外展开说明这一点.可以参考内容[1][2].
代码准备
- 简单对代码作一个说明:
add.so会被编译为一个动态库. 里面包含两个函数:- addtwo 完成两个数的加法.
- addthree 完成
main.c
1 |
|
动态库: add.so
源文件: add.c
1 | int addtwo(int x,int y) |
makefile
一份”能用”的makefile,主要是用于记录编译参数. 方便重复输入:
1 | so: |
编译结果分析
- 我们着重对相关的
.dynamic和相关的符号(symbol)和重定位表rel.dyn等进行详细分析。
分析: add.so
- 命令:
objdump -d add.so
1 | .add.so: file format elf32-i386 |
我们看到如下的代码:

结论:
- PLT 实际为代码段. 不是简单的数据段. 首先其内容不会变. 且内容的确为一些JMP相关的代码。
- PLT的第一项地址为: 0x0000030C (这个是基于VMA基地址为0的虚拟地址)
- 上面的ebx的值实际为
.got.plt段的基地址.(后面会证明) - 使用
.got.plt表进行间接寻址时,是从第4项开始的,即前三项进行了跳过。 .got.plt表项的大小为4字节addtwo作为一个内部和外部都会调用的函数.在PLT中有自己的表项。- 注意: 而同时也作为外部函数的addthree 并没有自己的PLT项。
验证 ebx的内容是.got.plt基地址
我 们先看一下.got的段信息。
1 | # 命令: objdump -j .got -h add.so |
我们发现.got段的大小为12字节. 偏移地址为:0x000015a8,紧接着我们看一下.got.plt的信息。
1 | # 命令 objdump -j .got.plt -h add.so |
重点: .got.plt的VMA是:0x000015b4;这个好像并不能说明什么。别急,我们看一下ebx的内容是怎么得到的,先看一下addthree中对addtwo的调用部分:
1 | 0000040c <addtwo>: |
我们对计算的部分进行一下标注:

- 与预期一致,因为这个库里面的所有的代码都是需要重定位的,没有静态可以确定位置的。
再看一下动态段的重定位表:

因此我们可以确定,当所有的调用到.plt段的时候,ebx都会提前初始化为.got.plt的段基地址。而在上面的PLT的表项中的代码中,在引用EBX进行JMP的时候,还会跟上一个表内偏移。
- 第一个PLT项是一个common的项。没有用于跳转找目标函数。
- 从第二个PLT项开始安排为跳转项。每一个PLT项的大小为:16字节。因为每个表项之间的偏移为16字节。
- PLT引用的
.got.plt段前三个entry都没有用到. 根据书中的内容我们可知:

重定位表
我们看一下重定位表的内容,按道理重定位表应该会对.got和.got.plt的内容进行重定位,因为动态链接库的装载地址是不确定的。因此.got项和.got.plt的内容都需要在装载时重定位。
先看一下非动态段的重定位表:

- 与预期一致,因为这个库里面的所有的代码都是需要重定位的。没有静态可以确定位置的。
再看一下动态段的重定位表:

解读一下:
- 同样没有函数:
addthree的符号的相关的重定位项. (并不代表addthree不需要重定位. 这是两个概念.) - 里面有三个
R_386_JUMP_SLOT类型的重定位项. 三个项的地址分别是:- 000015c0:
R_386_JUMP_SLOT - 000015c4:
R_386_JUMP_SLOT - 000015c8:
R_386_JUMP_SLOT
- 000015c0:
我们知道.got.plt的首地址是: 0x15B4。而第一个JUMP_SLOT的地址是:15B4+4*3 = 15C0 和前面分析的跳过了前三项got.plt项保持一致。
为什么没有addthree的plt项
- 这个问题其实非常重要。它将揭示
.plt表项的本质。以及结合上面的PLT项是代码还是数据结合看就可以得出结论了。
虽然我们的动态链接库同时导出了两个函数:addtwo和addthree;但是并不是两个函数都出现在了.plt中。而且只有一个被内部自己调用的addtwo函数有一个.plt表项. 这似乎隐约在说明一个问题:
.plt段是一个胶水代码段,他属于调用方,不属于被调用方。
1 |
|
我们发现:
- 在main中对
.got.plt的引用不再使用ebx寄存器进行基址相对寻址。 - 使用的第一个
.got.plt项是0x80497d4。
而我们上面说.got.plt的基地址是:080497c8,那前三项是被跳过了的即:080497c8,080497cc,080497d0,然后从0x80497d4开始,与前面所说的跳过前三项用于加载当前模块的动态依赖相关的信息有关系。
到此。所有的疑问都很清晰了,还有一点需要我们探究一下。那就是为什么main模块中对.got.plt引用不是使用的ebx进行寻址。而add.so中却是如此,其实啊,这个与两个模块的虚拟地址的确定与否有关系。
- 主模块的加载虚拟地址是确定的。它是加载到进程中的第一个模块。他的地址在编译链接时就已经确认。在加载到内存的时候直接按照
program header的信息加载到内存即可。因此:模块代码与got相关的位置是固定的,且是确定的。那在进行寻址的时候,直接使用一个固定的地址的间接引用即可。(即上面的:jmp *0x80497d0) - 而动态模块在加载的时候其加载基地址是不确定的,因此需要使用ebx去动态的获取到当前代码加载的地址。这样才能够满足动态加载的需求。
最后总结一下:plt是属于调用方的一段胶水代码。有多少个模块就可能存在多少个.plt段,而这个.plt段还是单独为这一个模块服务的。比如模块a调用了模块b。那a中就会有一个单独的.plt属于模块a用来间接寻址模块b。且这个.plt段会一个配套的.got.plt段来存储最后的真正的地址。那如果还有一个模块c也调用了模块b。那c需要一个自己的``.plt和.got.plt```来完成相同的工作。
由于有全局符号介入的机制的存在,即使是自己调用自己,那也需要在自己的模块里面生成一段.plt代码和相应的.got.plt入口来完成对自己的代码的“相对寻址”,虽然明明知道自己要调用的代码与自己的相对位置是多少。但是我们就是偏偏不能那样做。因为由于全局符号介入的关系,我们最终调用的符号自己,可能不真的我们自己。
下面我画了一个图来总结呈现下这个关系吧,以防止你和我一样被绕晕:

结尾
最后我们还是以问题结束吧:
- 动态库中的PLT表与主程序中的PLT表是否为同一份?
- 答:不是同一份
- 动态库中的GOT.PLT表与主程序中的GOT.PLT表是否为同一份?
- 答:同样不是同一份
- PLT表是代码还是数据?
- 这个是代码,不是数据。虽然<自我修养>中说是一个个的项
- 动态库内部函数之间的调用为何不直接使用相对地址调用?
- 因为有全局符号介入的存在.导致实际调用有可能不是调用自己的函数的情况。
上面并没有论证.got是否为同一个,看完上面的内容,或许你已经有了答案。
- 因为有全局符号介入的存在.导致实际调用有可能不是调用自己的函数的情况。
