一.首先分析一下为什么需要重定位(暂时不考虑fpic代码,动态链接库等复杂情况)
这里举一个简单的例子,如下a.c中代码如下
int a ;
int test()
{
a = 1;
return 0;
}
编译生成目标文件a.o,再经过与带有main函数的模块连接之后生成可执行文件,则可运行了;但是在这里要考虑一个问题,当编译a=1这一赋值语句时要知道a的地址,才能编译出相应的代码.但是在链接之前,明显a的地址是不能确定的.所以编译器在编译该段代码时会针对a=1这一对a发生引用的地方记录一个重定位信息(包括其在当前section中的offset,关联的符号,重定位类型).这样在链接时,首先算出确定所有符号的地址,然后根据重定位信息找到要修改相应的位置,再根据已经确定的符号地址算出实际要填入的值,然后修改对应位置.
如果没有重定位过程,则再编译a.c到a.o的过程中,a变量的地址可能是随机指定的,如0.可是当a.o与其他模块链接后,最终a的地址可能被定义到0x100.则在生成的可执行文件中,若不对对应位置进行修改,则明显会出错.
二.哪些地方会发生重定位,各有什么特点
在代码中,甚至在数据中(例如a=&b),都有可能需要重定位.下面分析一下各种情况.
1.在代码段中,例如上面的test函数引用变量a,这里需要生成重定位记录,以便链接器链接后来修改.具体还有几种情况
1.1 对函数,变量的引用(注意这里是对函数符号的引用,如a = (int)printf)
1.1.1 对静态函数或变量的引用
1.1.2 对全局函数或变量的引用
1.2对函数的调用(体会与引用的区别,若是引用,则要重定为函数的绝对地址,若是调用则不一定,与平台有关,在x86下是e8+偏移值,所以改成的不是绝对地址)
1.2.1 对全局函数的调用
1.2.2 对静态函数的调用
2.在数据段中,分为两种情况
2.1 对静态函数或变量的引用
2.2 对全局函数或变量的引用
三.重定位的时机
其实重定位的实际不止在ld链接时,还发生在加载器(ld-linux.so加载相应模块时.所以一个是链接时,一个是加载时.
四.为什么要区别不同的重定位类型
1.为什么要区分区域,分成代码区和数据区重定位
首先,代码区重定位可以转化为数据区重定位,通过在数据区添加一个变量和函数的地址存储区域.将每个代码区重定位转移为数据区重定位(后面再具体分析),这样有什么好处呢.这里得提到动态库,大家知道动态库的好处是可以在物理内存中只有一份拷贝,然后映射到不同的进程的地址空间,可是.so文件如果存在重定位项,则加载器在加载.so文件到每个进程空间时会发生重定位(这里的重定位时机在加载时),这时便要修改.so文件的很多地方,包括代码段和数据段,这样麻烦就来了,因为存在写时复制,会为每个进程产生一个拷贝,这样.so库的优点就荡然无存了,所以这时候在数据区开出一个固定的区域,将重定位项集中到该区域,则问题便解决了.这有就是fpic代码的实现方法.