动态链接GOT与PLT
主要是动态链接的一些内容
断断续续磨了一个周,本来只是想学一下got表后来发现越挖越深什么都不会,大致了解一下,最后写出了这样一个网络垃圾(x
预备知识
链接就是把目标文件与一些库文件生成可执行文件的一个过程。
编译过程
要了解“链接”的概念,首先要了解C语言编译生成可执行文件的过程:(1) 预处理;(2) 编译;(3) 汇编;(4) 链接;
01 预处理
使用预处理器把源文件test.c
经过预处理生成test.i
文件,预处理用于将所有的#include
头文件以及宏定义替换成其真正的内容,其中test.i
是文本文件。
这里是一个c语言的源文件:
1 |
|
gcc处理命令为:
1 |
|
处理后的test.i文件就会变得很长很长,以下是部分截图:
02 编译
使用编译器将预处理文件test.i
编译成汇编文件test.s
,其中test.s
是文本文件。
gcc命令为:
1 |
|
处理后的test.s文件又变得短短的了
03 编译
使用汇编器将汇编文件test.s
转换成目标文件test.o
,其中test.o
是二进制文件。
汇编过程的命令为:
1 |
|
04 链接
链接过程使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。
该步骤的命令为:
1 |
|
拖进IDA就是我们比较熟悉的样子了
动态链接库
我们在写程序的时候,通常不会完全靠自己来实现所有功能,我们会调用我们所需要的系统库或者第三方库来实现我们的功能,这些库就是动态链接库。
动态链接库可以映射到不同进程的不同虚拟地址,所以属于“地址无关代码”。
🌰:
1 |
|
在这段代码中我们调用了系统库,在编译完成后可以查看文件的symbol。会发现在printf和strncpy前面都是没有定义的,这就是用于支持动态连接功能的。
两种命令可以随意挑选,但结果不太一样,还没搞明白😳
nm -g [filename]
readelf -s [filename]
通过objdump命令查看相关函数的反汇编模块 objdump -D [filename]
(在.plt部分)
可以看到,程序首先进行jupq操作跳转到相应的代码段,这个代码段就是用于给“地址无关代码”做动态地址重定位,链接器将这个函数的调用代码跳转到程序运行时的动态装载地址。
链接器
链接器(Linker)是一个程序,将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。目标文件是包括机器码和链接器可用信息的程序模块。
简单的讲,链接器的工作就是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。
链接器还要完成程序中各目标文件的地址空间的组织,涉及重定位工作。
链接器的工作步骤
- 将代码和数据模块象征性地放入内存
- 决定数据和指令标签的地址
- 修补内部和外部引用
链接器需要对动态链接库做的情
- 链接库在将目标文件链接成可执行文件的的时候,如果发现某一个变量或者函数在目标中找不到,就会按照 gcc 预定义的动态库寻找动态库中定义的变量或者函数
- 如果链接库在某一动态库中找到了该变量或者函数的定义,链接库首先会把这个动态链接库写到可执行文件的依赖库中,然后生成这个当前变量或者函数的代理symbol
- 在偏移表中生成真正的动态跳转指令,并且在库函数代理symbol中跳转到相应的偏移位置
重定位
重定位(Relocations)就是把程序的逻辑地址变换成为内存中的实际地址空间的过程。
重定位分为两步:
- 重定位节和符号引用
在这一步中,连接器将所有相同类型的节合并为同一类型的新聚节。随后链接器把运行时的内存赋值给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块的每个符号。
e.g.所有输入模块的.data节被合并为一个节,这个节成为输入的可执行目标文件的.data节。
当这一步完成,程序中的每条指令和全局变量都有唯一的运行时的内存地址了。
- 重定位节中的符号引用
在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行地址。
其中,链接器执行这一步需要依靠可重定位目标模块中成为重定位条目的数据结构。
重定位条目
当汇编器生成一个目标模块时,他并不知道数据和代码最终会被放在内存的那个位置,也不知道这个模块引用的外部定义的函数或者全局变量的位置。所以,当汇编器遇到一个对最终位置未知的目标引用,它就会生成一个重定位条目,告诉连接器将目标文件合并成可执行文件时如何修改这个引用。
代码的重定位条目放在.rel.text
中,已经初始化数据的重定位条目放在.rel.data
中。
重定位条目分为两种格式:Rel
和Rela
。每个重定位条目表示一个必须被重定位的符号引用,并指明如何计算被修改的符号引用。
重定位条目
Rel
和Rela
之间的唯一区别:Rel
中没有Addend
字段。
查看文件中的重定位信息:
1 |
|
Offset
是 Relocation Entry 结构体中的第 1 个字段,占用 8 字节,表示需要修改的符号引用的位置。对于可重定位目标文件,该字段表示需要修改的符号引用的起始位置在目标 section (
.rela.text
中的重定位条目对应的目标 section 为.text
,.rela.data
中的重定位条目对应的目标 section 为.data
,以此类推)中的偏移量(字节)。对于可执行目标文件和可共享目标文件,该字段表示需要修改的符号引用的起始位置所对应的虚拟内存地址。
Info
是 Relocation Entry 结构体中的第 2 个字段,占用 8 字节,表示符号表索引和重定位类型(符号表索引占用高 32 位,重定位类型占用低 32 位)。符号表索引表示需要修改的符号引用在
.symtab
section中的索引。这里的Sym. Value
和Sym. Name
列只是打印了所对应符号表条目中Value
和Name
列的值。重定位类型指示链接器如何修改该符号引用的值。重定位类型因不同的处理器而异。
Addend
是 Relocation Entry 结构体中的第 3 个字段,占用 8 字节,表示一个有符号常数,一些重定位类型要使用它对被修改符号引用的值做偏移调整。
静态链接和动态链接
静态链接
静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成链接到一块生成可执行程序。这里的库指的是静态链接库,Windows下以
.lib
为后缀,Linux下以.a
为后缀。
若程序使用静态链接方式,则程序所有代码都将集成到同一个二进制文件中,其优点在于无依赖关系,可以在不同运行环境的OS下运行。
但是缺点也十分明显,由于二进制文件中包含全部代码,所以所占空间较大;如果多次运行同一个程序,则OS可能会对某个库函数进行多次重复 的加载,占用了不必要的内存;若某个公用的库函数产生了更新,则需要重新编译所有使用了该库的程序,工作量较大。
优点
- 代码装载速度快,执行速度略比动态链接库快;
- 只需保证在开发者的计算机中有正确的.lib文件,在以二进制形式发布程序时不需考虑在用户的计算机上.lib文件是否存在及版本问题。
缺点
- 使用静态链接生成的可执行文件体积较大,包含一些重复相同的代码,造成内存空间的浪费。
动态链接
动态链接(Dynamic Linking),把链接这个过程推迟到了运行时再进行,在可执行文件装载时或运行时,由操作系统的装载程序加载库。这里的库指的是动态链接库,Windows下以.dll
为后缀,Linux下以.so
为后缀。
在Windows下的动态链接也可以用到.lib为后缀的文件,但这里的.lib
文件叫做导入库,是由.dll
文件生成的。
优点
生成的可执行文件较小;
适用于大规模的软件开发,使开发过程相对独立,耦合度减小,便于不同的开发者和开发组织之间进行开发和测试;
不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性
缺点
- 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息;
- 速度比静态链接慢
GOT与PLT
为了支持动态链接这一工作过程,在.elf
文件中有四个section与之相关:
.got
:全局偏移表(Global Offset Table),用于存储外部符号的绝对地址即运行时符号的真实地址,由链接器进行填充,包含动态链接的函数的地址。.plt
:过程链接表(Procedure Linkage Table),就是一小段跳转指令,存有从.got.plt
中查找外部函数地址的代码,若是第一次调用该函数,则会触发链接器解析函数地址并填充在.got.plt
相应的位置;若函数地址已经存储在.got.plt
中则直接跳转到对应地址继续执行。.got.plt
:是plt的got。它包含返回.plt去触发查找的地址,或者是一个经过查找后填充的正确符号地址。.plt.got
:不知道干啥用的……
延迟绑定
因为静态链接中的重定位工作全部在运行时完成,且当我们引用了某一个库,程序将会对其中的所有函数和全局变量进行重定位,这种情况下,链接的动态库越大链接的时间就越长,系统的启动时间就越长。
为了解决这一问题,延迟绑定的概念被提出。延迟绑定规定只有符号真正被引用时才会进行重定位,而不是在刚开始就对所有的动态符号进行重定位。
延迟绑定由plt来实现,在elf文件中,plt表和got表几乎时时刻刻伴随着(这是可以嗑的吗是可以的吗)。
在got表中,前三项内容不对应符号的引用,分别对应:
- got[0]:当前的elf文件中.synamic段的地址
- got[1]:保留
- got[2]:动态链接器的符号解析函数
其余的项被用作符号重定位,对于外部函数(即外部跳转)的 got 表项而言,在编译阶段保存的是 .plt 表的起始位置,对于数据引用的 plt 表项而言,编译阶段的值为 0。
plt 的作用是为每一次模块外部的函数调用设置一小段跳转代码。在 arm 编译器的实现中,对于每一项外部跳转,对应 plt 中三条指令。和 got 类似,plt 的前面部分也被系统”征用”了,这部分负责调用动态链接器中的符号解析函数完成动态解析工作,后续的部分才是对应具体外部跳转的指令。
在程序的编译阶段,plt 跳转指令表项和 got 表项就实现了绑定,其映射关系为:plt 第一段跳转指令对应 got 第四个表项,plt 第二段跳转指令对应 got 第五个表项,以此类推。
🌰:
对于一个c语言文件
1 |
|
对其进行编译后查看其main函数的反汇编结果
%rip是一个指针,此处相当于寄存器相对寻址
在0x40054d
调用了0x400430<puts@plt>
,当程序rip到400340时,需要执行的操作对应的汇编代码为:
陆陆续续把基础的小零碎学完啦!🥳