动态链接
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
静态链接时,整个程序最终只有一个可执行文件,它是一个不可分割的整体;
在动态链接下,一个程序被分成了若干个文件。
那么在共享对象被装载时,如何确定他在进程虚拟地址空间中的位置呢?
地址无关代码
在早期有一种做法叫静态共享库,但是现在不用了。
静态共享库的做法是将程序的各种模块统一交给操作系统来管理,操作系统在某个特定的地址划分出一些地址块,为那些已知的模块预留足够的空间。
装载时重定位
为了使共享对象能在任意地址装载,我们的基本思路就是,在链接时,对所有绝对地址的引用不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。
在静态链接时,我提到的重定位叫链接时重定位
现在这种情况叫做装载时重定位,又叫做基址重置
动态链接模块被装载映射至虚拟空间后,指令部分是在多个进程之间共享的,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享。
地址无关代码
装载时重定位有一个很大的缺点是指令部分无法在多个进程之间共享。所以一个基本想法就是把指令中需要被修改的部分分离出来,跟数据部分放在一起。这样指令部分保持不变,而数据部分可以在每个进程中拥有一个副本,这就是地址无关代码。
我们把共享对象按是否跨模块,引用方式的区别可以分为四种情况。
一、模块内部函数调用
对于现代的系统来讲,模块内部的跳转、调用都是相对地址调用,或是基于寄存器的相对调用,所以这种指令是不需要重定位的。
二、模块内部数据访问
很明显,指令不能直接包含数据的绝对地址,那么唯一的办法就是相对寻址。
一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页的相对位置是固定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据。
三、模块间数据访问
linux下ELF的做法是在数据段里面建立一个指向这些变量的数组,也被称为全局偏移表GOT
。当代码需要引用该全局变量时,可以通过GOT
中对应的项间接引用。
注:链接器会在装载模块的时候查找每个变量所在的地址,然后填充GOT中的各个项。
四、模块间调用、跳转
这个原理和三差不多,不过是GOT
中保存的是目标函数的地址。
共享模块的全局变量问题
ELF共享库编译的时候,默认都把定义在模块内部的全局变量当作定义在其他模块的全局变量,通过GOT
实现变量的访问。
当共享模块被装载时,如果某个全局变量在可执行文件中拥有副本,
那么动态链接器就会把GOT中相应地址指向该副本,这样该变量在运行时实际就只有一个实例。
如果变量在共享模块中被初始化,那么动态链接器还需要将该初始化值复制到程序主模块中变量中的变量副本。
如果没有副本,那么GOT中响应地址就指向模块内部该变量副本。
延迟绑定
在动态链接下,程序模块之间包含了大量的函数引用,所以在程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找以及重定位。所以ELF采用一种叫做延迟绑定的做法,基本思想就是当函数第一次用到才进行绑定。
参考文献
[1] 俞甲子 石凡 潘爱明.程序员的自我修养.电子工业出版社,2009.4.