请稍侯

静态链接与动态链接的原理与区别

09 September 2020

静态链接与动态链接的原理与区别

源文件先是被编译成一个个目标文件, 再由链接器把这些目标文件组合成一个可执行文件或库。链接的过程,其核心工作是解决模块间各种符号(变量,函数)相互引用的问题,对符号的引用本质是对其在内存中具体地址的引用,因此确定符号地址是编译,链接,加载过程中一项不可缺少的工作,这就是所谓的符号重定位。
对于可执行文件或静态库来说,符号重定位是在链接时完成的。对于动态链接库来说,因为动态库的加载是在运行时,且加载的地址不固定,因此动态库的符号重定位是推迟进行的,主要有两种方式:加载时符号重定位地址无关代码

加载时符号重定位

加载时重定位,其原理很简单,它与链接时重定位是一致的,只是把重定位的时机放到了动态库被加载到内存之后,由动态链接器来进行。 加载时重定位实际上是一个重新修改动态库中数据符号地址的过程(函数符号的地址因为延迟绑定的存在不需要在代码段中重定位)。不同的进程即使是对同一个动态库也很可能是加载到不同地址上,因此当以加载时重定位的方式来使用动态库时,该动态库就没法做到被各个进程所共享,而只能在每个进程中 copy 一份:因为符号重定位后,该动态库与在别的进程中就不同了,可见这样动态库节省内存的优势就不复存在了。

地址无关代码

因加载时重定位有两个缺点:

  1. 它不能使动态库的指令代码被共享。
  2. 程序启动加载动态库后,对动态库中的符号引用进行重定位会比较花时间,特别是动态库多且复杂的情况下。

地址无关代码的实现在模块内部与模块间又有所不同。

模块内部符号的访问:地址无关代码方案是通过对变量及函数的访问加一层跳转来实现。主要根据当前 IP(指令指针寄存器) 值来动态计算数据的绝对地址,其原理是当动态库编译好之后,库中的数据段,代码段的相对位置就已经固定了,此时对任意一条指令来说,该指令的地址与数据段的距离都是固定的,那么,只要程序在运行时获取到当前指令的地址,就可以直接加上该固定的位移,从而得到所想要访问的数据的绝对地址了。

模块间符号的访问:因为动态库运行时被加载到哪里是未知的,为了能使得代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转,ELF 的做法是在动态库的数据段中加一个表项,叫作 GOT(global offset table), GOT 表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。

延迟加载

动态库是在进程启动的时候加载进来的,加载后,动态链接器需要对其作一系列的初始化,如符号重定位(动态库内以及可执行文件内),这些工作是比较费时的,特别是对函数的重定位。 因为一个动态库里可能包含很多的全局函数,但是我们往往可能只用到了其中一小部分而已,而且在这用到的一小部分里,很可能其中有些还压根不会执行到,因此完全没必要把那些没用到的函数也过早进行重定位。所以应该等到第一次发生对该函数的调用时才进行符号绑定,也叫延迟绑定。

延迟绑定的实现步骤如下:

  1. 建立一个 GOT.PLT 表,该表用来放全局函数的实际地址,但最开始时,该里面放的不是真实的地址而是一个跳转,接下来会讲。
  2. 对每一个全局函数,链接器生成一个与之相对应的影子函数,如 fun@plt。
  3. 所有对 fun 的调用,都换成对 fun@plt 的调用,每个fun@plt 长成如下样子:
    fun@plt:
     jmp *(fun@got.plt)
     push index
     jmp _init
    

    其中第一条指令直接从 got.plt 中去拿真实的函数地址,如果已经之前已经发生过调用,got.plt 就已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 fun@plt 中的第二条指令,这就使得当执行第一次调用时,fun@plt中的第一条指令其实什么事也没做,直接继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号作为参数传给 _init(),而 _init() 这个函数则用于把 fun 进行重定位,然后把结果写入到 got.plt 相应的地方,最后直接跳过去该函数。

参考

https://www.cnblogs.com/catch/p/3857964.html