7.3. 地址无关代码

共享对象在编译时不能假设自己在进程虚拟地址空间中的位置。
为了能够使共享对象在任意地址装载,需要装载时重定位。
Linux GCC支持装载时重定位,如果只使用-shared那么输出的共享对象就使用装载时重定位。

地址无关:PIC pic PIE pie:
装载时重定位无法支持代码部分在多个进程间共享,那么我们希望代码段在装载时不需要因为装载地址的变化而变化,如果将指令中变化的部分提取出来作为数据的一部分,那么指令部分大家共享,而数据部分每个进程有自己的副本,地址无关技术。

模块间的地址引用方式:
第一种:模块内的函数调用,跳转等: 不需要重定位
第二种:模块内部的数据访问,比如模块中定义的全局变量,静态变量: 指令中不能直接包含数据的绝对地址,
任何一条指令与它访问的模块内部数据间的相对位置是固定的,ELF得到当前的PC值,然后再加上一个偏移量来达到访问响应变量的目的。
0000054c <bar>:
 54c:   55                      push   %ebp
 54d:   89 e5                   mov    %esp,%ebp
 54f:   e8 40 00 00 00          call   594 <__x86.get_pc_thunk.cx>
 554:   81 c1 20 12 00 00       add    $0x1220,%ecx
 55a:   c7 81 24 00 00 00 01    movl   $0x1,0x24(%ecx) //a = 1
 561:   00 00 00 
 564:   8b 81 ec ff ff ff       mov    -0x14(%ecx),%eax
 56a:   c7 00 02 00 00 00       movl   $0x2,(%eax)
 570:   5d                      pop    %ebp
 571:   c3                      ret    

00000594 <__x86.get_pc_thunk.cx>:
 594:   8b 0c 24                mov    (%esp),%ecx
 597:   c3                      ret    
 变量a的地址是当前PC值加上两个偏移。



第三种:模块外部的函数调用,跳转等
在数据段里建立一个指向目标函数的指针数组,当模块需要调用目标函数时,可以通过GOT中的项进行间接跳转。
先得到但前指令地址PC,然后加上一个偏移得到函数地址在GOT中的偏移,然后一个间接调用
54f:   e8 40 00 00 00          call   594 <__x86.get_pc_thunk.cx>
554:   81 c1 20 12 00 00       add    $0x1220,%ecx
55a:   c7 81 24 00 00 00 01    movl   $0x1,0x24(%ecx)
561:   00 00 00 
564:   8b 81 ec ff ff ff       mov    -0x14(%ecx),%eax
56a:   c7 00 02 00 00 00       movl   $0x2,(%eax)
第四种:模块外部的数据访问,比如其它模块定义的全局变量
模块间数据的访问地址要等到转载时才知道,那么把跟地址相关的部分放到数据段,在数据段里建立一个指向这些变量的指针数组,全局偏移表GOT,当代码需要引用这些全局变量时,通过GOT中相应的项间接引用。

各种地址引用

模块内部   相对跳转和调用        相对地址访问
模块外部   间接跳转和调用(GOT) 间接访问(GOT)
-fpic 指示GCC产生地址无关代码,产生代码相对较快,较小,但跟硬件平台相关
-fPIC 通常使用这个产生地址无关代码

如何判断一个动态文件是否是PIC,只要产看有没有任何代码重定位段,TEXTREL表示代码段重定位表地址。

地址无关技术也可应用于普通可执行程序,-fPIE或-fpie`

共享模块的全局变量:
如果当一个模块应用了一个定义在共享对象的全局变量时,由于程序主模块的代码并不是地址无关代码,它引用全局变量的方式跟普通数据访问一样,由于可执行程序在运行时不进行代码重定位,为了使链接过程正常进行,链接器会在创建可执行程序时,在.bss段创建一个变量的副本
那么全局变量定义在原先的共享对象中,而可执行文件的.bss段也有一个副本,那么就产生了冲突。
为了解决这个问题,共享库在编译时,默认都把定义在模块内部的全局变量当作定义在其它模块的全局变量,通过GOT来实现变量的访问,带嗯共享模块加载时,如果某个全局变量在可执行文件中有副本,那么动态链接器就把GOT中相应的地址指向该副本,如果变量在共享库中被初始化,那么动态链接器还需要把初始化值复制到程序主模块中的变量副本。

假设是共享对象的一部分,那么GCC在-fPIC情况下,会把变量的调用按照跨模块方式产生代码,因为编译器无法确定对全局变量的引用是跨模块的还是模块内部的,即使是模块内部的全局变量,还是会按照跨模块的引用方式,因为可能被可执行程序引用。


数据段地址无关性:
static int a;
static int *p = &a;
那么p就是一个绝对地址,会随着共享对象的装载地址变化而变化。
对于数据段,它在每个进程中都有一份独立的副本,可以在装载时重定位来解决数据段的绝对地址引用问题。对于共享对象,如果数据段有绝对地址引用,那么编译器和链接器会产生一个重定位表,R_386_RELATIVE,当动态链接器加载共享对象时,如果发现该共享对象有这样的重定位入口,那么动态链接器会对该对象重定位。

可以使用不带-fPIC的选项产生装载时重定位的代码段,
gcc -shared pic.o -o pic.so
因为是地址相关代码,所有多个进程间不能共享此代码段,造成内存的浪费,但是地址相关代码段不需要每次访问全局变量和函数时的地址计算和间接选址,因此会比地址无关代码效率高。

对于可执行文件来说,默认下,如果可执行程序是动态链接的,那么GCC选用PIC方式产生可执行文件的代码段,以便不同的进程能够共享代码段,节约内存。所以动态链接的可执行文件存在.got段。