第七章
动态链接
1
为什么要动态链接
节省内存和磁盘空间
有利于程序的升级
有利于程序的可扩展和兼容性
ELF
动态链接文件被称为动态共享对象(
DSO
,
Dynamic Shared Objects
),简称共享对象。
Windows
系统中,动态链接文件被称为动态链接库(
DLL
,
Dynamical Linking library
)。
2
简单的动态链接的例子
gcc �Co Program1 Program1.c ./lib.so
Program1.c
引用到
lib.so
中的
foobar()
函数,这里
foobar
定义在共享对象中,连接器会将这个符号的引用标记为一个动态链接的符号,不对其进行重定位,留到装载时再进行。
lib.so
保存了完整的符号信息,把
./lib.so
作为连接的输入符号之一,连接器在解析符号时就知道
foobar
是定义在
lib.so
中的动态符号。
如果
foobar
是定义在其他目标文件中的函数,链接器会按照静态链接的规则进行重定位。
共享对象的最终装载地址在编译时时不确定的,而是在装载时分配。
3
地址无关代码
固定装载地址困扰
共享对象在被装载时,如何确定其在虚拟空间中的地址?
共享对象地址冲突问题:程序模块的指令和数据中可能会包含一些绝对地址的引用。不同模块目标装载地址不能一样,容易造成冲突。这就是模块装载地址固定问题。
一种解决方法是静态共享库,操作系统在某些特定的地区划分一些地址块,为那些已知的模块预留足够的空间。
另外一种方法是让共享对象在编译时不能假设自己在进程中的虚拟地址空间中的位置。这就就可以再任意地址加载。
装载时重定位(任意地址加载,方法一)
装载时重定位是首先想到的解决共享对象任意地址装载问题。在链接时,对所有绝对地址的引用都不尽兴重定位,而是把这一步推迟到装载时进行。
连接时重定位
vs
装载时重定位(基址重置)
这种方法对共享对象并不是很合适,因为这种方法需要修改指令,而指令部分被多个进程共享,没法做到一份指令被多个进程共享,因为指令被重定位后对每个进程都是不同地。
但这种方法可以修改共享对象的数据部分,因为数据部分都是进程私有的。
地址无关代码(任意地址加载,方法二)
要想实现指令部分的进程共享,解决共享对象指令中对绝对地址的重定位问题,方法是将指令中那些需要被修改的部分分离出来,放在数据部分。这种方案称为地址无关代码(
PIC, Position-Independent Code
)技术。
模块中四种类型的地址引用:
(1)
模块内部的函数调用、跳转
(2)
模块内部的数据访问,全局变量
?
、静态变量
(3)
模块外部的函数调用、跳转
(4)
模块外部的数据访问,如其他模块定义的全局变量
第一类:模块内部的函数调用或跳转
调用者和被调用者处于同一模块,相对地址固定。跳转或者函数调用可以是相对地址调用或者基于寄存器的调用,不需要重定位。(未考虑全局符号介入问题)
第二类:模块内部数据访问
一个模块前面是若干页的代码,后面是若干页的数据,这些页之间的相对位置是固定的。也就是说,指令和所需的模块数据之间的相对位置固定,只需要将当前指令位置
+
偏移量就可以访问模块数据了。(编译时用符号,链接时候根据符号值和重定位信息,完成,像静态链接一样)
ELF
有巧妙的方法获得当前
PC
值。
第三类:模块间的数据访问
在数据段里建立一个指向外部模块数据的指针数组,称为全局偏移表(
GOT, Global offset table
)。当代码需要引用全局变量时,可以通过
GOT
中相对应项间接引用。
当指令要访问外部变量,程序先找到
GOT
,根据
GOT
中变量所对应的项找到变量的目标地址。连接器在装载模块时会查找每个变量所在的地址,然后填充
GOT
中的各个项。
由于
GOT
在数据段,“编译的时候”可以确定
GOT
相对于当前指令的偏移,方法跟第二类相似,
PC+offset
。而且
GOT
中每个地址对应于哪个变量编译器是知道的。
第四类:模块间的函数调用和跳转
可以采用第三类中的方法,这是
GOT
种保存的是目标函数的地址。
共享模块的全局变量问题
对于定义在共享对象模块内部的全局变量,能不能按类型二来解决呢?
假如有一个对共享对象中全局变量
b
的访问:(无法判断是否跨模块调用)
情况一:该访问位于可执行文件中。由于程序主模块代码并不是地址无关代码,引用全局变量的方式跟普通数据访问一样:
Movl $0x1, XXXXXXXX
XXXXXXXX
是全局变量的地址
b
的地址,可执行文件并不进行装载时代码重定位,需要在链接时确定其地址,所以在
.bss
段有
b
的副本,完成连接工作。于是在共享对象中有一个
b
,在可执行文件中有一个
b
。
解决方法是编译共享库是将模块内部的全局变量当做定义在其他模块的全局变量,即类型三来处理,通过
GOT
实现变量访问,指向
.bss
段中的副本。
情况二:该访问位于共享对象内部。则对
b
的访问按照跨模块模式产生代码,因为编译器无法知道对
b
的引用时模块内部还是跨模块,即使是模块内部也会按照情况一产生跨模块代码。
数据段的地址无关性
对于共享对象数据段某些变量引用绝对地址的问题,我们可以使用装载时重定位方法来解决绝对地址引用问题,因为每个进程都会有一份独立的数据段副本。编译器和链接器会产生一个从定位表。
实际上代码段也可以通过装载时重定位方法来解决任意地址装载问题,但这样就失去了代码重用的优点。
(未完待续)