简介

  1. 为什么要动态链接

    • 浪费内存和磁盘空间,例如公共库占用了每个程序和进程的空间
    • 程序开发和发布,每次更新任何一个模块,都需要更新整个程序
    • 有利于程序设计,将程序产品规模较大时,可分割为多个子系统及模块,动态链接使各个模块更加独立,耦合度更小
  2. 概念
    把程序的模块分隔,形成独立的文件,而不是静态地链接在一起,简单来说,就是不对那些组成程序的目标文件进行链接,等到程序运行时才进行链接。

动态链接过程

我们发现,动态库Lib.so也参与了Program1的链接过程,这是怎么回事呢?
当链接器将Program.o链接成可执行文件时,必须确定Program.o中引用的函数的性质:

  • 若是定义在其他静态目标模块中的函数时,则按照静态链接规则处理,对其进行重定位
  • 若是定义在动态共享对象的函数,则链接器将其标记为动态链接的符号,不对其进行重定位,留到装载时进行

链接器如何知道函数的性质呢?这就是动态库Lib.so也参与了链接过程的原因了,Lib.so中保留了完整的符号信息,将其作为Program1的链接输入文件,链接器在解析符号时就可以知道,其中的那些函数是动态符号。

地址空间分布

对于静态链接的可执行文件来说,整个进程只有一个文件要被映射,那就是可执行文件本身,但对于动态链接来说,除了可执行文件外,还有它引用的共享目标文件,那么这种情况瞎进程的地址空间分布又会怎么样呢?

在查看进程的虚拟地址空间发现,多出了一个ld-2.6.so,他是linux下的动态链接器,它和普通共享对象一样被映射到了进程虚拟地址空间,在系统运行程序之前,会先将控制权交给动态链接器,完成动态链接工作后再开始执行程序

而共享对象映射到进程空间中的地址是多少呢?其实共享对象的最终装载地址在编译时是不确定的,而是在装载时根据空闲状态分配。

地址无关代码

  1. 固定装载地址的困扰

    由上一节可知,我们需要确定共享对象的最终装载地址,我们可以手工指定各个模块的地址,例如把0x1000-0x2000分配给A,将0x2000-0x3000分配给B,但当多个模块被多个程序,每个模块不知道其他模块占用的地址,极易发生冲突,早期这种方法叫做静态共享库
    静态共享库不止有地址冲突问题,升级也有很大麻烦,为了解决这个问题,我们设想,共享对象在编译时不能假设自己在进程虚拟空间中的位置。

  2. 装载时重定位
    基本思路:在链接时,对所有绝对地址的引用不作重定位,而把这一步推迟到装载时进行,一旦模块装载地址确定,再进行重定位。
    在静态链接过程中的重定位叫做链接时重定位(Link Time Relocation),现在这种叫做装载时重定位(Load Time Relocation)

  3. 地址无关代码
    上述的装载时重定位是解决动态模块中有绝对地址引用的方法之一,但是他有个很大的缺点就是指令部分无法在多个进程之间共享,这样就失去了动态链接节省内存的优点。

    由于共享对象在每个进程装载的地址都可能不一样,那就表示在上一种方法下,你的指令部分在每个进程中都不同

    所以我们的目的就是希望程序模块中共享的指令部分在装载时不需要根据装载地址改变而改变,所以基本思想就是将指令中需要改变的部分和数据部分放在一起,这样指令就可以保持不变,而数据部分在每个进程中拥有一个副本,这种方案就叫做地址无关代码(PIC,Position-independtent Code)的技术。

    例如模块间的数据和函数访问,因为目标地址到装载时才能确认,为了实现地址无关代码,ELF的做法是在数据段里面建立一个指向这些变量或函数的指针数组,也称为全局偏移表(Global Offset Table),当代码引用这些变量或函数时,可通过GOT中相对应的项间接引用,

    链接器在装载模块时,会查找每个变量或函数的所在地址然后填充GOT的各个项。由于GOT本身在数据段,所以它可在模块装载时被修改,且每个进程有独立的副本,互不影响。

    如果可执行文件是动态链接的,那GCC默认会使用PIC来产生可执行文件的代码段。所以可以在其中看到.got段。

延迟绑定

动态链接性能比静态差,因为装载时链接导致启动慢和GOT查询导致执行慢,优化如下:

使用延迟绑定:在函数第一次调用时,才进行绑定(符号查找,重定位等),大大加快程序启动速度。

显式运行时链接

也叫运行时加载,就是让程序自己在运行时控制加载指定的模块,并且可以在不需要时卸载

  1. void *dlopen(const char *filename, int flag);
    该函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程
    filename:被加载动态库的路径,若是绝对路径(以“/”开头),则尝试直接打开该动态库,若是相对路径,则尝试以下顺序查找该库文件

     • 环境变量LD_LIBRARY_PATH
     • /etc/ld.so.cache里面指定的共享库路径
     • /lib  /usr/lib

    flag表示函数符号的解析方式
    RTLD_LAZY表示延迟绑定,当函数第一次被用到时才进行绑定
    RTLD_NOW表示当模块被加载时即完成所有函数绑定工作
    返回值:后续函数使用的句柄

  2. void *dlsym(void *handle, const char *symbol);
    通过该函数找到所需的符号
    handle即为dlopen函数返回值
    symbol为符号名
    返回值:函数和变量的地址,若查找的是常量,则返回常量的值

  3. char *dlerror(void);
    判断其余函数是否调用成功,若返回NULL,则表示调用成功,若不是,则返回错误消息

  4. int dlclose(void *handle);
    卸载模块

其余关于动态链接的详细结构体和段信息,还有链接过程的具体实现我们就不深入探究