装载与动态链接——动态链接(二)

前文速递:
静态链接——编译和链接(一)
静态链接——目标文件(二)
静态链接——静态链接(三)
装载与动态链接——装载与进程(一)

本文主要是《程序员的自我修养》一书的内容摘要和梳理,如有需要并且没有被本文涵盖的内容,建议读者自行观看原书。

前面主要写了文章来介绍书中对程序的静态链接的相关内容,接下来系列开启对动态链接的学习。

为什么要动态链接

静态链接可以使程序开发者可以独立地开发和测试自己的程序模块,大大促进了程序开发的效率,但随着程序规模的扩大,静态链接浪费内存、模块更新难等问题也逐渐暴露出来。

  • 浪费空间:静态链接库会包含很多用不到的公共库函数
  • 模块更新难:一个模块更新整个程序积极需要重新链接、发布给客户

解决上述问题的办法就是把程序的模块相互分割开,形成独立的文件,等到程序运行时再进行链接,这就是动态链接的基本思想。

如果程序A和程序B都依赖 libc.o,当运行程序A时,libc.o 会被加载进内存,当再运行程序B时,系统检测到内存中有一份 libc.o 就不会再加载一份了,会共享目标文件。

动态链接可以动态地选择各种程序模块,这个优点被后面人们用来制作各种程序的插件(plugin)。

由于程序每次被装载时都要进行重新链接,相对静态链接会有大约 %1~%5 的性能损失。

如果符号是定义在其他动态共享对象中的,链接器会把这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,而需要的动态共享对象(例如:libc.o)里面保存了完整的符号信息,动态链接器解析符号时需要这些信息。

地址无关代码

为了解决共享对象地址冲突的问题,共享对象在编译时不能假设自己在进程虚拟地址空间中的位置,与之对应的可执行文件基本可以确定自己在进程虚拟空间中的起始位置。

前面在静态链接部分提到的重定位叫做链接时重定位(Link time Relocation),而在程序装载时对程序的指令和数据中对绝对地址的引用进行重定位叫做装载时重定位(Load Time Relocation)。但是进程装载时,内存地址都是私有的,且起始地址都不相同,所以相关共享代码的指令被修改后没办法共享。

我们的诉求时共享对象指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要修改的部分分离出来,跟数据部分放在一起,这样指令部分可以保持不变,数据部分每个进程中都有一个副本,这种方案就叫地址无关代码(PIC,Position-independent Code)。

实现地址无关代码模块间的访问需要借助一种中间结构——全局偏移表(GOT,Global Offset Table),其中是一个包含模块外的变量/函数的指针数组。
在这里插入图片描述
因为有中间跳转这一步,使用地址无关代码会比装载时重定位慢。

延迟绑定

动态链接比静态链接慢的主要原因是对全局和静态的数据访问都要进行复杂的 GOT 定位,然后间接寻址/跳转;还有一个原因是程序开始执行,动态链接器都要进行一次链接工作。延迟绑定就是优化动态链接性能的方法。

在程序开始执行前,动态链接耗费不少时间将模块之间的函数引用的符号查找以及重定位,不过可能很多函数在程序执行完时都不会被用到,如果一开始就把所有函数都链接好实际是一种浪费。所有 ELF 采用一种叫延迟绑定(Lazy Binding)的做法,基本思想就是当函数第一次被用到时才进行绑定(符号查找、重定位等),这样可以加速程序的启动速度。

延迟绑定在通过 GOT 跳转的过程又增加了一层间接跳转。调用函数不直接通过 GOT 跳转,而是通过 PLT 项的结构进行跳转。每个外部函数在 PLT 中都有一个对应的项,通过先跳转到 PLT 项实现只有符号未解析时执行一次,解析完就直接 GOT 跳转。

原书中对 PLT 结构进行了详细的内容描述,感兴趣的读者可以查看原书相关内容。

动态链接步骤与实现

动态连接器自举

动态链接器用来对其他共享对象链接,但是它自己的重定位工作也是由它自己完成的,这个行为就叫做自举(Bootstrap),其入口地址就是自举代码的入口,自举程序会找到自己的 GOT,然后 GOT 第一个保存的就是 .dynamic 段的偏移地址,通过.dynamic 段,动态链接器本身就可以获得自己的重定位表和符号表,从而将自己需要重定位的符号进行重定位,自举代码中不能使用全局变量和静态变量,也不能调用函数,因为使用上述内容需要 GOT/PLT ,但是它们都还没重定位。完成自举代码以后才开始正常使用自己的全局变量和静态变量。

装载共享对象

完成自举后,动态链接器将可执行文件和链接器本身的符号表合并成一个全局符号表,然后查找可执行文件所依赖的共享对象,将其放入一个装载集合中,之后开始从集合中依次读取加载共享对象,如果当前共享对象还依赖其他共享对象,则将依赖的共享对象也加入到装载集合,以此类推,类似于图的搜索,一般使用广度优先(也可以使用深度优先)的算法加载。

当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。

重定位和初始化

上述步骤完成之后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的 GOT/PLT 中每一个需要重定位的位置进行修正,如果某个共享对象中包含 “.init” 段,动态链接器还需要执行 “.init” 段来初始化,因为共享对象的全局/静态对象的构造需要通过 “.init” 段来初始化。

ps. 可执行文件的 “.init” 段链接器不会执行,这部分是由代码初始化负责执行的。

当完成上述 3 步时,动态链接器就完成了它的工作,将进程的控制权转交给程序的入口并且开始执行。

显式运行时链接

  • dlopen():打开一个动态库,并将其加载到进程的地址空间,完成初始化过程
  • dlsym():找到加载的库中所需要的符号
  • dlclose():作用与 dlopen 相反,将已加载的模块卸载,通过计数器实现
  • dlerror():调用上述以后可以使用 dlerror 函数判断是否调用成功,成功返回 NULL,否则返回 char* 错误信息
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值