Keep Calm and Carry On

动态链接

1. 静态链接的局限

  1. 浪费内存空间
    如果不同的程序都要用到相同的模块,在静态链接的情况下,链接为可执行程序的时候都有一份相同的目标文件的副本,也就是无论是磁盘还是内存都会保留多个被公用的目标文件,这对于空间的浪费是很严重的。
  2. 影响程序更新
    如果某个目标文件是第三方提供的,一旦这个目标文件被修改,那么引用这个目标文件的程序就要重新获取新的目标文件,然后重新链接程序,发布程序。

2. 动态链接

基本思想:不对组成程序的目标文件进行链接,而是等到程序要运行时才链接,也就是把链接过程推迟到了程序运行。这个时候如果不同的程序使用了相同的模块,比如 p1 和 p2 都要链接目标文件 lib.so,当 p1 要运行时,系统首先加载 p1,然后发现要链接 lib.so,就接着加载 lib.so,如果还需要其他的模块,还会继续加载;这时候如果要运行 p2,系统就仅仅需要加载 p2,而不需要加载 lib.so,因为 lib.so 在内存中已经有一份副本,系统只要讲 p2 和 lib.so 链接起来即可。

在 Linux 中,ELF 动态链接文件被称为动态共享对象(Dynamic Shared Object),一般以“.so”为扩展名,而 Windows 系统中动态链接文件是 dynamical Linking Library,以“.DLL”为扩展名。共享对象的最终装载地址不是在编译时确定的,而是在装载时根据当前内存的空闲情况决定的。

3. 地址无关代码(PIC)

  1. 不能固定装载
    不能为被共享的模块预设它的加载地址。
    问题:共享对象在被装载时如何去欸的那个它在进程虚拟地址空间中的位置?

共享对象在编译时不能假设自己在进程虚拟地址空间的位置,与此不同的是,可执行文件基本可以确定自己在进程虚拟空间中的起始位置,一般会选择一个固定的空闲的地址,比如 Linux 下一般都是 0x080400000,Windows下一般都是 0x0040000。l

  1. 装载时重定位
    与静态链接的链接时重定位类似,动态链接采用的是装载时重定位,也就是在装载共享对象后,重新确定所有的绝对地址的引用,用偏移地址加上装载地址即可。这样装载时重定位有一个确定,那就是共享对象的指令部分无法被多个进程共享,因为使用了装载时重定位,对不同进程来说共享对象在进程虚拟空间地址是不一样的,因此要修改指令。

  2. 地址无关代码
    Position Independent Code 就可以解决上面装载时重定位指令无法共享的问题,方法就是将把那部分需要因装载地址改变而改变的指令分离出来,和数据部分放在一起,这样剩余的指令可以只在内存中保留一份,而数据部分(包括一些指令)则在每个进程有一份副本。对于一个程序来说,有一下四种类型的地址引用方式:
    1) 模块内部的函数调用、跳转;
    2) 模块内部的数据访问,比如模块内定义的全局变量、静态变量;
    3) 模块外部的函数调用、跳转;
    4) 模块外部的数据访问,比如定义在其他模块的全局变量;
    这四种情况要产生地址无关代码都是不同的情况:

情况1——模块内调用跳转:因为模块内部的调用、跳转可以是相对地址的调用或者基于寄存器的调用,所以这些指令不需要重定位;
情况2——模块内数据访问:ELF首先使用是一个巧妙地方法获得当前地指令地址 PC,然后加上偏移量来获得内部数据的地址,因此这些指令也不需要重定位;
情况3——模块外数据访问:ELF的做法是在数据段建立一个指向这些全局变量的指针数组,称为全局偏移表(Global Offset Table,GOT),当代码要引用该全局变量时,可以通过 GOT 中相对应的项间接引用,GOT 中每个地址对应于哪个变量时由编译器决定的,比如第一个地址对应 a,第二个地址对应 b 等。当一条指令要访问另一个模块的数据时,首先会找到自己数据段的 GOT,根据 GOT 的变量对应的项得到变量的地址,然后再去访问。每个地址都对应一个4字节的地址,链接器在装载模块时会查找每个变量所在的地址,然后填充 GOT 的表项,确保每个指向指向变量的地址都正确,每个模块都会有独立的 GOT 的副本。
情况4——模块外调用跳转:对于模块间的调用、跳转也是采用 GOT 的方法做到地址无关。

  1. 数据段地址无关性
    对于共享对象数据段来说,每个进程都有一份独立的副本,所以不用担心呗进程改变。

  2. 延迟绑定(PLT)
    动态链接相比于静态链接牺牲了一部分的性能(比如全局和静态的数据访问都要进行复杂的 GOT 定位,然后间接寻址,对于模块间的调用也是类似的,还有就是由于不是静态链接一样预先链接好要运行时链接),因此可以使用延迟绑定的方法优化动态链接的性能。
    一般来说模块之间会包含大量的函数调用,而不会包含很多的全局变量的引用,因为大量引用模块间的全局变量会增加模块的耦合性。在程序开始执行之前,花费大量的时间解决模块之间的函数引用的符号查找,填写 GOT 会降低性能,而一个基本事实就是很多函数在程序执行完成时都不一定会被用到(比如错误处理函数),因此ELF采用一种延迟绑定的方法。延迟绑定的基本思想是:当函数第一次被用到时才会进程绑定(符号查找,重定位),如果没有用到则不进行绑定。所以,程序执行时模块间的函数调用都是没有进行绑定的,当被调用了才由动态链接器进行绑定,这样可以加快程序的启动速度。
    ELF 使用 PLT(Procedure Linkage Table)的方法来实现延迟绑定。

ELF 将 GOT 拆分为两个表,.got 用来保存全局变量引用的地址,.got.plt用来保存函数引用的地址,.got.plt 的前三项为:

  • .dynamic 段的地址
  • 本模块的 ID
  • _dl_runtime_resolve()(用来完成函数的地址绑定)的地址