chap-7 7.3 地址无关代码

7.3 地址无关代码
7.3.3 地址无关代码
装载时重定位是解决动态模块中有绝对地址引用的办法之一,但是它有一个很大的缺点就是指令部分
无法在多个进程之间共享,这就失去了动态链接节省内存的一大优势,我们的目标就是程序模块中
共享的指令部分在装载时不需要因为装载地址的改变而改变,因此基本想法就是把指令中需要被修改
的地方分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分在每个进程中拥有
一个副本,这种方案被称为地址无关代码(PIC, Position-Independent Code)。

我们先来分析模块中各种类型的地址引用方式。这里我们把共享对象模块中的地址引用按照是否为
跨模块分为两类:模块内部引用和模块外部引用;按照不同的引用方式又可以分为指令引用和数据引用。
这样我们可以得到下面四种组合情况:
第一种是模块内部的函数调用、跳转等
第二种是模块内部的数据访问,比如模块中定义的全局变量、静态变量
第三种是模块外部的函数调用、跳转等
第四种是模块外部的数据访问,比如其他模块中定义的全局变量

用下面这个C程序做为上面四种情况的说明:
//smaple.c
static int a;
extern int b;
extern void ext();

void bar() {
a = 1; //Type 2, Inner-Module data access
b = 2; //Type 4, Inter-Module data access
}

void foo() {
bar(); //Type 1, Inner-Module function call
ext(); //Type 3, Inter-Module function call
}

类型一:模块内部调用或者跳转
此种情况下,被调用的函数和调用这都在同一个模块内,它们之间的相对位置是确定的,所以这种
情况相对简单,模块内部的跳转、函数调用都可以是相对地址调用,或者是基于寄存器的相对调用,
所以对于这种指令是不需要重定位的。

类型二:模块内部数据访问
这种情况下,指令中不能包含数据的绝对地址,为什么?因为如果指令中包含数据的绝对地址,同时
每次模块装载进内存的位置都是不确定的,所以指令中对数据的引用所涉及到的地址也要跟着变化。
那么唯一的办法就是相对寻址,一个摸块前面一般是若干个页的代码,后面紧跟着若干页的数据,这些页
之间的相对位置是确定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是
固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据。现在体系结构中,
数据的相对寻址往往没有相对于当前指令地址(PC)的寻址方式,所以ELF用了一个很巧妙的办法来获取
当前PC的值,然后再加上一个偏移量就可以达到访问相应变量的目的。下面是最常用的一种:

chap-7 7.3 地址无关代码
***图7.3.1***

chap-7 7.3 地址无关代码
***图7.3.2***
图7.3.1是对sample.c先编译成为动态链接库,然后反汇编以后的代码。地址为563,568和56e的两条指令是
对模块内部变量a操作的指令,首先调用一个叫“__X86.get_pc_thunk..cx”的函数,这个函数的作用就是
将返回地址的值放到ecx寄存器中,即把call的下一条指令的地址放到ecx寄存器中。
我们知道当处理器执行call指令时,下一条指令的地址会被压到栈顶,而esp寄存器始终指向栈顶,那么当
“__X86.get_pc_thunk.cx”执行mov (%esp), %ecx的时候,即把call的下一条指令的地址放到ecx寄存器。

接着568和56e分别执行一条add指令和一条mov指令,变量a的地址是寄存器ecx的内容加上两个偏移量0x1a98
和0x24。而寄存器ecx的内容正是add指令的地址568。我们来核实一下这样计算变量a的地址是否正确?
根据上面的计算方法,变量ad的地址应为0x568 + 0x1a98 + 0x24 = 0x2024。变量a是全局静态变量,我们
用readelf -s sample.so查看一下变量a所处的段,如下图所示:

chap-7 7.3 地址无关代码
***图7.3.3***
从上图我们可以看出变量a所处段的下标为22,我们再用readeld -S sample.so查看一下段表,如下图所示:

chap-7 7.3 地址无关代码
***图7.3.4***
变量a在.bss段,而.bss段的起始地址为0x2020,大小为8字节。从我们上面的计算结果看出,a确实在.bss段,
并且上述代码正确的引用变量a的地址。

类型三:模块间数据访问
模块间的数据访问比模块内的稍微麻烦,因为模块间的数据访问目标地址等到装载时才能确定。前面提到了
要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,很明显,这些其他模块的全局
变量地址是跟模块装载地址有关的。ELF的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为
全局偏移表(Global Offset Table, GOT)。当代码需要引用全局变量时,可以通过GOT中相对应的项简介引用。

当指令要访问变量b时,程序会首先找到本模块内的GOT表,然后根据GOT中变量所对应的项找到变量的目标地址。
GOT表是放在数据段的,所以它可以在模块装载的时候被修改,并且每个进程都可以有独立的副本,相互不受影响。

模块在编译时可以确定模块内部变量相对于当前指令的偏移,那么也可以在编译时确定GOT表相对于当前指令
的偏移。确定GOT表的地址和得到变量a地址的方法类似,通过得到PC值然后加上一个偏移量,就可以得到GOT
表的位置,然后根据变量地址在GOT表中的偏移就可以得到变量的地址,GOT表中每个地址对应于那个变量是由
编译器确定的。

我们可以看一下图7.3.1中对变量b的操作,因为变量b属于其他模块,因此,要获取变量b的地址,要先得到GOT
表的地址,在图7.3.1中对b操作的是578和57e两条指令,变量b的地址在GOT表中的位置是由寄存器ecx得到,
然后由寄存器间接寻址的方式把2赋值给b。变量b的地址为0x568 + 0x1a98 - 0x14 = 0x1fec。我们来验证一下
变量b的地址。

可以用objdump -h sample.so查看GOT表的位置,如下图所示:

chap-7 7.3 地址无关代码
***图7.3.5***
可以看出GOT表的地址为0x1fe8,在文件中的偏移为0x1fe8。
再用objdump -R sample.so查看动态链接时的重定位项,如下图所示:

chap-7 7.3 地址无关代码
***图7.3.6***
从图中可以看出变量b的地址为0x1fec,也就是在GOT表中偏移为4。

如何区分一个Dynamic Shared Object是否为Position-Indenpendent Code,因为动态共享对象的段表中不包含
代码段重定位表,因此我们可以从这个方面来进行验证。

7.3.4 共享模块的全局变量问题
当一个模块引用了一个定义在共享对象的全部变量的时候,比如一个共享对象定义了一个全部变量global,而模块
module.c中是这么引用的:

extern int global;
int foo() {
global = 1;
}
当编译器编译module.c时,它无法根据这个上下文判断global是定义在同一个模块的其他目标文件中,还是定义在
另外一个共享对象中,即无法判断是否为跨模块间的调用。

chap-7 7.3 地址无关代码

上一篇:NFS服务器篇


下一篇:H.264:FFMpeg 实现简单的播放器