7.3.4 共享模块的全局变量问题
当一个模块引用了一个定义在共享对象的全部变量的时候,比如一个共享对象定义了一个全部变量global,而模块
module.c中是这么引用的:
extern int global;
int foo() {
global = 1;
}
当编译器编译module.c时,它无法根据这个上下文判断global是定义在同一个模块的其他目标文件中,还是定义在
另外一个共享对象中,即无法判断是否为跨模块间的调用。
7.3.5 数据段地址无关性
例如有如下代码段:
static int a;
static int* p = &a;
如果共享对象里面有这样一段代码的话,那么指针p的地址就是一个绝对地址,它指向变量a,而变量a的地址会随着
共享对象的装载地址改变巍峨改变,那么如何解决这个问题呢?
对数据段来说,它在每个进程都有一个独立的副本,所以不担心被进程改变,从这点来看,我们可以选择装载时
重定位的方法来解决数据段中绝对地址引用的问题。对于共享对象而言,如果数据段中有绝对地址引用,那么编译器
和链接器就会产生一个重定位表,这个重定位表中包含了“R_386_RELATIVE”类型的重定位入口,用于解决上述问题,
当动态链接器装载共享对象时,如果发现该共享对象有这样的重定位入口,那么动态链接器就会对该共享对象进行
重定位。
实际上,也可以让代码段也使用这种装载时重定位的方法,而不使用地址无关代码。在前面的例子中,为了生成地址
无关代码,在GCC编译的时候,要加上-fPIC参数,这个参数代表产生地址无关的代码段。如果我们不使用这个参数来
产生共享对象又会怎么样呢?例如,用如下命令:
$gcc -shared sample.c -o sample.so
上面这个命令就产生了一个不使用地址无关代码而使用装载时重定位的共享对象,但是如果代码不是地址无关的,那么
它就不能被多个进程之间共享,于是就失去了节省内存的优点。装载时重定位的共享对象的运行速度比使用地址无关代码
的共享对象快,因为它省去了地址无关代码中每次访问全局数据和函数时需要做一次计算当前地址以及间接地址寻址过程。
对于可执行文件来说,如果可执行文件是动态链接的,那么GCC会使用PIC的方法来产生可执行文件的代码段部分,以便于
不同进程能够共享代码段,节省内存。所以我们看到,动态链接的可执行文件中存在“.got”这样的段。
7.4 延迟绑定
动态链接的确有很多优势,但是是以牺牲一部分性能为代价的。据统计ELF程序在静态链接下要比动态链接稍微快些,大约
为1%~5%,主要原因有以下两点:1.动态链接下对于全局变量和静态数据的访问都要进行复杂的GOT定位,然后间接寻址;对于
模块间的调用也要先定位GOT,然后再进行间接跳转。2.动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器
都要进行一次链接工作,例如动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址重定位工作。
延迟绑定实现
在动态链接下,程序模块之间包含了大量的函数引用,所以在程序开始执行以前,动态链接会耗费不少时间用于解决
模块之间的函数引用的符号查找和重定位。不过在一个函数的执行过程中,很多函数在程序执行完时都不会用到,如果
一开始就把所有函数都链接好实际上是一种浪费。所以ELF采用了一种叫做延迟绑定(Lazy Binding)的做法,基本思想就是
当函数第一次被用到时才进行绑定,如果没有用到则不绑定。
ELF使用PLT(Procedure Linkage Table)的方法来实现。假设liba.so需要调用libc.so中的bar()函数,那么当liba.so中
调用bar()函数时,这时候就需要调用动态链接器中的某个函数来完成地址绑定工作,但是这个函数需要知道一定的信息
来完成绑定过程,例如这个绑定的动作应该针对那个模块中的那个函数进行。
上一节中说道,当调用外部模块的函数时,如果按照通常的做法,应该是通过GOT表中相应的项进行跳转。PLT为了实现延迟
绑定,在这个过程中间又增加了一层间接跳转。调用函数并不直接通过GOT跳转,而是通过一个叫作PLT项的结构来进行跳转。
每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的地址为bar@plt。
ELF将GOT拆分称为两个表,叫做".got"和".got.plt",其中".got"用来保存全局变量引用的地址,".got.plt"用来保存函数引用
的地址,即对于所有外部函数的引用全部被分离出来放到了".got.plt"中,另外".got.plt"还有一个特殊的地方在于它的前三项
是有特殊意义的:
1.第一项保存的是.Dynamic段的地址
2.第二项保存的是本模块的ID
3.第三项保存的是_dl_runtime_resolve()的地址
其中第二项、第三项是由动态链接器在装载共享模块时负责将其初始化,“.got.plt”中其余每一项对应每个外部函数的引用。
我们可以用readelf -S lib.so查看该共享对象的段表,如下图所示:
***图7.4.1***