接上一篇。
7.3 地址无关代码
对于现代机器来说,引入地址无关代码并不麻烦,我们展示下各种模型的地址引用方式:
1. 模块内部函数调用
2. 模块内部的数据访问,如全局变量、静态变量。
3. 模块外部的函数调用,跳转。
4. 模块外部的数据访问。
当编译器在编译 pic.c 时,它实际上并不能确定变量b和函数ext()是模块外部的还是模块内部的,因为它们有可能被定义在同一个共享对象的其他目标文件中。由于没法确定,编译器只能把它们都当作模块外部的函数和变量来处理。MSVC编译器提供了_declspec(dllimport)编译器扩展来表示一个符号是模块内部的还是模块外部的。
1. 模块内部函数调用
同一模块中的函数地址相对固定,寻址相对简单些,使用的是相对寻址,不需要重定位。
这条指令中的后4个字节是目的地址相对于当前指令的下一条指令的偏移,即0xFFFFFFE8(Little-endian)。0xFFFFFFE8是-24的补码形式,即 bar 的地址为0x804835c+(-24)=0x8048344。那么只要bar和foo的相对位置不变,这条指令是地址无关的。即无论模块被装载到哪个位置,这条指令都是有效的。这种相对地址的方式对于jmp指令也有效。
2. 模块内部数据访问
模块内部访问也是相对寻址。一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。现代的体系结构中,数据的相对寻址往往没有相对与当前指令地址(PC)的寻址方式,所以ELF用了一个很巧妙的办法来得到当前的PC值,然后再加上一个偏移量就可以达到访问相应变量的目的了。得到PC值的方法很多,我们来看看最常用的一种,也是现在ELF的共享对象里面用的一种方法:
3. 模块间数据访问
模块间数据访问相对麻烦些,因为地址要等到装载时才能确定。比如上面例子中的变量b,它被定义在其他模块中,并且该地址在装载时才能确定。我们前面提到要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,很明显,这些其他模块的全局变量的地址是跟模块装载地址有关的。ELF的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用,它的基本机制如图7-7所示。
当需要访问b时,程序会先找到GOT,然后根据GOT中的变量所对应的项找到变量的目标地址。链接器在装载模块的时候会查找每个变量所在地址,然后将各个项填入GOT,GOT。
未完待续!!!