动态链接库中与地址无关代码(PIC)对于地址引用的处理

2019-04-15 12:42发布

动态链接库希望所有进程共享指令段而各自拥有数据段的私有副本,为了实现这个目标,就要采用与地址无关代码(PIC: Position Independent code)的技术。该实现的基本思想是:把指令中需要修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分则在每个进程拥有一个副本


与地址无关的代码,也就是需要考虑代码中会对地址进行引用的情况,共享对象(GCC中的动态链接文件)中地址引用可以分为以下几种情况:
a) 模块内函数调用、跳转等
b) 模块内数据的访问,如模块内定义的静态变量,全局变量等
c) 模块外部的函数调用、跳转等
d) 模块外部的数据的访问,比如别的模块定义的全局变量
static int a; extern int b; extern void ext(); void bar() { a = 1; //情况b b = 2; //情况d } void foo() { bar(); //情况a ext(); //情况c }
以下分别讨论:

1.模块(动态链接文件)内部的函数的调用

由于此时调用者与被调者都是位于同一个模块,所以调用者与被调者之间的相对位置是固定的,因此,对被调者的调用就可以使用相对地址来替代绝对地址,对于这种指令就是不需要重定位的。 eg: 对上面代码中的 foo() 与 bar() 函数,因为bar() 函数相对 foo()函数中的调用语句之间的距离是固定不变的,因此该语句将是与地址无关的。

2.模块内部的数据调用

与上面分析同理,由于数据定义与引用指令是位于同一个模块的,因此它们之间的相对位置是固定的。但是此时有一些区别,现代体系结构中,数据的相对寻址没有基于当前指令的寻址方式,因此 ELF 采用了一个巧妙的方法来获取当前的PC(程序计数器)的值,再在该基础上添加一个偏移,即可访问到变量 获取当前PC的方法:调用 “__i686.get_pc_thunk.cx”函数,该函数的作用是将返回地址的值放入ecx寄存器中,即把 call 的下一句指令的地址放入 ecx 寄存器中。(以上面的例子,操作是将 a = 1 该句指令的地址放入寄存器) 实现将返回地址放入寄存器的方法:处理器执行 call 指令后,下一条指令的地址会被压到栈顶,而 esp 寄存器指向栈顶,那么当“__i686.get_pc_thunk.cx”执行 "mov (%esp) ecx" 的时候,返回地址就放入了寄存器。

3.模块外部的函数的调用

此时对外部符号的引用显然是与地址有关的,按照先前说的基本思想,此时需要将与地址相关的部分放到数据段里。ELF 的做法是在数据段中建立一个指向这些函数的指针数组,也即是全局偏移表(GOT,Global Offset Tabel),当代码需要引用这些外部函数时,则可以通过GOT 中的相对应的项间接引用 动态链接器在装载模块的时候会查找每个函数所在地址,并填充GOT中的各个表项,以保证每个指针均指向正确的地址。同时由于GOT本身是放在数据段的,因此它可以在模块装载的时候被修改,并且每个进程都可有自己的副本。

4.模块外部的数据的调用

该方法与模块外部的函数访问方法相同,同样引入 GOT ,只是此时GOT 的表项中存储的是变量的地址
*如何区分一个动态共享对象是否是PIC的? PIC 的 DSO 是不会包含任何的代码段重定位表的。 readelf -d lib.so | grep TEXTREL上面的代码对PIC 的动态共享对象是不会有输出的。