链接、装载与库:动态链接

简介


       本篇整理动态链接相关的原理、整个链接和装载的过程。

为什么要动态链接


       1. 静态链接的方式对于内存和磁盘空间浪费严重。例如,使用静态链接的每一个C程序都将自己保留一份C静态库。

       2. 使用静态链接方式的程序一旦有任何模块的更新或微小的改动,都需要整个程序重新编译链接以及发布。


动态链接


       在静态链接的总结一文已经介绍过,这里再重复一遍。动态链接就是,对生成最终可执行文件的各目标文件不进行简单统一地链接操作,而是在等到程序运行时才进行。也就是说,将链接的过程推迟到程序运行装载时再进行

       简单来说,静态链接是在程序运行装载之前先生成一个独立完整的可执行文件;动态链接则是等到程序运行装载时才链接成一个完整的程序。在Linux中,ELF动态链接文件成为动态共享对象,即以“.so”为扩展名的文件。

       动态链接使得共用库或者目标文件在内存或磁盘中都可以只保留一份,节省了内存和磁盘空间。对于程序的更新或某个模块的微小改动,也只需修改某独立的库或目标文件。

       也因为程序在运行时才进行链接的特点,人们可以动态地选择链接各种程序模块,通过编写符合要求的动态链接文件,由此加载第三方的模块,这个就是制作程序插件的原理。

补充:关于可执行文件和程序的这两个名词间的关系?

       1. 采用静态链接方式的程序,整个程序最终只生成一个可执行文件。这里,“程序” = “可执行文件”;

       2. 采用动态链接方式的程序,整个程序除了生成一个可执行文件,还需要该程序所依赖的动态共享对象(比如libc.so等)。这里,“程序” = “可执行文件” + “各种动态共享对象(*.so文件)”。并且,“可执行文件”与“各种共享对象”都可以看作是程序的一个模块。


进程虚拟空间分布

       

       采用动态链接方式的进程虚拟空间的分布与静态链接区别:多映射了动态链接器与所依赖的共享对象

       动态共享对象的文件结构以及进程虚拟空间分布如下图:

       

       这里就有一个问题,采用动态链接方式的程序,当共享对象被装载时,如何确定它在进程虚拟空间中的地址

       对于可执行文件,因为往往是第一个被加载的文件,所以基本可以固定在自己进程虚拟空间中的起始位置。但是对于共享对象,则不能固定装载位置,因为共享对象可能存在多个进程虚拟空间中,如果它在每个进程虚拟空间中的位置都固定,一定会引起地址冲突。

       一种解决方式是,设想可以让共享对象在进程虚拟空间的任意位置被加载。并且,当共享对象在各个不进程虚拟空间中确定了加载位置只后,程序才对所有必要的符号进行重定位。这里引入了两个概念,即链接时重定位 (静态链接)、装载时重定位 (动态链接)。

       我们知道,动态共享对象同样分为指令部分和数据部分。当它被加载至虚拟空间时,由于装载时重定位,导致重定位后的指令对于各个不同的进程来说,地址是不同的。对于数据部分,因为其本身对于每个不同的进程都有自己的副本,所以没有问题;但是对于指令部分,因为地址不同的关系,所以也就不可能做到同一份指令被多个进程共享。所以,使用装载时重定位依然还存在问题。于是,人们又想出一种所谓的地址无关代码 (PIC, Position-independent Code)的方案。


地址无关代码


       在共享对象模块中,按照不同的地址引用方式可以分为四类:

       1. 模块内部的函数调用、跳转等;

       2. 模块内部的数据访问,比如自身定义的全局变量、静态变量;

       3. 模块外部的函数调用、跳转等;

       4. 模块外部的数据访问,比如访问外部定义的全局变量;

       对于模块内部的地址引用,采用相对寻址方方式解决;而模块外部的地址引用,则采用GOT(Gloabl Offset Table)表间接访问,动态链接的可执行文件体现在.got段;另外对于共享模块的全局变量,由于存在无法判断是否为模块内部还是外部的问题,所以统一将全局变量当作模块外部定义的全局变量,通过GOT实现访问。

       总之,通过地址无关性在理论上解决了共享对象指令部分的共享问题。(详细介绍请参考《程序员的自我修养》7.3节--地址无关代码)


延迟绑定


       对于以上内容做下小结:

       1. 动态链接工作在装载时完成,即程序开始执行,操作系统将控制权交由动态链接器,动态链接器寻找并装载共享对象,后续进行符号查找及重定位;

       2. 动态链接下对于模块间的调用/全局和静态数据访问都是先进行GOT定位,然后间接寻址跳转或访问;

       因为上面两部分的工作,相比静态链接,动态链接在性能上自然会“慢”一些。所以就有了一些优化动态链接性能的方法,这就有了延迟绑定。延迟绑定的基本思想:当函数第一次被用到时才进行绑定(即符号查找、重定位等),如果没用到则不进行绑定。

       所以,所谓延迟绑定技术,就是当程序开始运行时,模块间的函数调用大部分都没有进行绑定,等到需要时才由动态链接器负责绑定。


动态链接相关结构

       

       继续看上面的动态共享对象 (*.so)的ELF文件结构图,跟动态链接相关的主要有以下几个:

       .interp段:保存一个字符串,该字符串是可执行文件所需要的动态链接器的路径。在Linux中,操作系统在对可执行文件加载的时候,会寻找装载该可执行文件所需要的动态链接器,即“.interp”段指定路径的对象。

       .dynamic段:动态链接ELF中最重要的段,保存了动态链接器所需要的基本信息,比如依赖哪些共享对象、动态链接符号表的位置。动态链接重定位表的位置、共享对象初始化代码的地址等。

       .dynsym段:动态符号表,只保存与动态链接相关的符号。对于模块内符号,比如私有变量则不进行保存。

       诸如“.rel.dyn”、“.rel.plt”等段,表示动态链接重定位表,“.rel.dyn”段表示对数据引用的重定位,修正的位置位于“.got”段以及数据段;“.rel.plt”段表示对函数引用的重定位,修正的位置位于“.got.plt”段。


动态链接步骤


       动态链接基本上分为三步:第一步,动态链接器自举;第二步,装载所有需要的共享对象;第三步,重定位和初始化。


       0)动态链接器启动之前

       在操作系统将控制权交给动态链接器之前,首先是对可执行文件进行装载,这与静态链接情况基本一样:读取可执行文件头部,检查文件合法性,然后读取可执行文件的各个“Segment”的虚拟地址等,并与该进程的虚拟空间进行映射...

       之后,操作系统启动“.interp”段中指定路径的动态链接器。在Linux下,动态链接器是“/lib/ld-linux.so.2”,它实际上也是一个动态共享对象,操作系统同样通过映射的方式将其加载到进程的虚拟地址空间中。加载完毕,即将控制权交由动态链接器的入口地址(与可执行文件入口地址类似)。


       1)动态链接器自举

       动态链接器获得控制权,首先开始自身的初始化操作,所有初始化信息都保存在进程堆栈中,包括进程环境,可执行文件入口地址等,由操作系统一并传递给动态链接器。

       动态链接器因为本身是一个共享对象,但是它有其特殊性,负责程序所依赖的共享对象的链接和装载。但它本身则不依赖于任何共享对象,并且所有需要的重定位工作由它自身完成,也就是“自举”。动态链接器入口地址,即自举代码的入口地址。自举代码开始执行,首先会找到动态链接器本身的GOT。而GOT的首个入口就是“.dynamic”中的偏移地址,由此即找到动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,并开始进行重定位操作。


       2)装载共享对象

       动态链接器完成自举,继续通过“.dynamic”段的信息找到可执行文件依赖的所有共享对象,并将共享对象名字放入一个装载集合。之后,链接器就依次取共享对象名字,找到相应文件后打开并读取文件头、“.dynamic”段等信息,并将相应的代码段和数据段映射至进程空间。如果该可执行文件继续依赖其他共享对象,则继续将这些共享对象名字加入装载集合,循环往复,直至装载集合中所有对象均被装载...

       值得注意的是,当一个新的共享对象(包括动态链接器本身)被装载,它的符号表就会被合并到全局符号表中。


       3)重定位和初始化

       所有共享对象装载完毕,链接器重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT中的每个需要重定位的位置再此进行修正。此时动态链接器拥有全局符号表,所以重定位工作很容易的事...

       对于C++实现的共享对象,重定位完成后,动态链接器还会执行“.init”段中的代码;进程退出时会执行“.finit”段的代码,一般就是构造析构之类的操作。值得注意的,如果进程的可执行文件也有“.init”段等,链接器不会执行,它将交由程序初始化部分代码负责。。


       至此,链接、装载工作全部完成,动态链接器将进程控制权交给可执行文件入口,程序开始执行。。。





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值