动态链接库希望所有进程共享指令段而各自拥有数据段的私有副本,为了实现这个目标,就要采用与地址无关代码(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 的动态共享对象是不会有输出的。