转自《程序员的自我修养》
对于动态链接,如果在装载时进行重定位,由于指令部分在多个进程之间是共享的,由于装载时重定位需要修改指令,所以没有办法做到一份指令被多个进程共享。地址无关代码就是把指令中那些需要修改的部分分离出来,跟数据部分放到一起,这样指令部分就可以保持不变,数据部分可以在每个进程拥有一个副本。
共享对象模块中的地址引用可以分为四种:
1、模块内部的函数调用、跳转等。
2、模块内部发数据访问,比如模块中定义的全局变量,静态变量。
3、模块外部的函数调用、跳转等。
4、模块外部的数据访问,比如其他模块定义的全局变量。
使用gcc产生地址无关代码只需要使用-fPIC参数即可。实际上还提供了另一个类似的参数-fpic,这两个参数功能上来讲完全一样,唯一的区别是-fPIC产生的代码要大,而-fpic较小,并且较快。如何区分一个DSO(共享对象)是否为PIC:readelf -d foo.so | grep TEXTREL,如果有任何输出那么就不是PIC。因为PIC的DSO是不会包含任何代码段重定位表的。TEXTREL表示代码段重定位表地址。
地址无关代码技术还可以用在可执行文件上:地址无关可执行文件(PIE Position-Independent Executable)。使用-fPIE参数即可。
定义在共享模块内部的全局变量
当一个模块引用的一个定义在共享对象内部的全局变量时,比如module.c中这样引用:
extern int global;
int foo()
{
global = 1;
}
当编译器编译module.c时,它无法判断这个global是定义在同一个模块的其他文件还是定义在别的共享对象中,即无法判断是否跨模块。假设global是主模块中的,由于主模块不是地址无关代码,所以引用这个全局变量的方式和普通数据一样,编译器会产生这样的代码:movl $0x1, XXXXXXX, XXXXXXX就是global的地址。链接器会在创建可执行文件的时候在它的.bss段串讲一个global变量的副本。那么就会出现global定义在原来的共享对象中,而在可执行文件的.bss段中还有一个副本,这样可定不行。
ELF共享库在编译时,默认都把定义在模块内部的全局变量当做定义在其他模块的全局变量,也就是类型4.,通过GOT来实现变量访问。当装载共享模块时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器会把GOT中相对应的地址指向该副本,这样运行时就只有一个实例。如果变量在共享模块中被初始化,那么动态链接器还需要将该初始值复制到程序主模块中的变量副本。如果该变量在主模块中没有副本,那么GOT中相应地址就指向模块内部该变量副本。
假设module.c是共享对象的一部分,那么GCC编译器在-fPIC的情况下,就会对global的调用按照跨模块模式产生代码,原因很简单,编译器无法确定对global的引用是跨模块还是模块内部。