地址无关代码(PIC, position independent code)
  从前面的介绍我们知道装载时重定位有重大的缺点:
  它不能使动态库的指令代码被共享。
  程序启动加载动态库后,符号重定位会比较花时间,特别是动态库多且复杂的情况下。
  为了克服这些缺陷,ELF 引用了一种叫作地址无关代码的实现方案,该解决方案通过对变量及函数的访问加一层跳转来实现,非常的灵活。
  1.模块内部符号的访问
  模块内部符号在这里指的是:static 类型的变量与函数,这种类型的符号比较简单,对于 static 函数来说,因为在动态库编译完后,它在模块内的相对地址已经确定了,而 x86 上函数调用只用到相对地址,因此此时根本连重定位都不需要进行,编译时能确定地址,稍微麻烦一点的是访问数据,因为访问数据需要地址,但动态库未被加载时,地址是没法得知的,怎么办呢?
  ELF 在这里使用了一个小技巧,根据当前 IP 值来动态计算数据的地址,它的原理很简单,当动态库编译好之后,库中的数据段,代码段的相对位置已经固定了,此时对任意一条指令来说,该指令的地址与数据段的距离都是固定的,那么,只要程序在运行时获取到当前指令的地址,可以直接加上该固定的位移,从而得到所想要访问的数据的地址了,下面我们用实例验证一下:
int g_share = 1;
static int g_share2 = 2;
int g_func(int a)
{
g_share += a;
return a * 2;
}
int g_func2()
{
int a = 2;
int b = g_func(3);
return a + b;
}
static int g_fun3()
{
g_share2 += 3;
return g_share2 - 1;
}
static int g_func4()
{
int a = g_fun3();
a + 2;
return a;
}
  以上代码在x86 linux 下编译,再反汇编看看得到如下结果:
  -bash-3.00$ gcc -o liba.so -fPIC -shared a.c
  -bash-3.00$ objdump -S liba.so
  //skip some of the output
  00000564 <g_fun3>:
  564: 55                    push   %ebp
  565: 89 e5                 mov    %esp,%ebp
  567: e8 00 00 00 00        call   56c <g_fun3+0x8>
  56c: 59                    pop    %ecx
  56d: 81 c1 60 11 00 00     add    $0x1160,%ecx
  573: 83 81 20 00 00 00 03  addl   $0x3,0x20(%ecx)
  57a: 8b 81 20 00 00 00     mov    0x20(%ecx),%eax
  580: 48                    dec    %eax
  581: c9                    leave
  582: c3                    ret
  //skip some of the output
  现在我们来分析验证一下,首先是地址 567 的指令有些怪,这儿不深究,简单来说,x86 下没有指令可以取当前 ip 的值,因此这儿使了个技巧通过函数调用来获取 ip 值(x86_64下不用这么麻烦),这个技巧的原理在于进行函数调用时要将返回地址压到栈上,此时通过读这个栈上的值可以获得下一条指令的地址了,在这儿我们只要知道指令 56c 执行后,%ecx 中包含了当前指令的地址,也是 0x56c,再看 56d 及 573 两条指令,得知 %ecx + 0x1160 + 0x20 = 0x16ec 是 573 指令所需要访问的地址,这个地址指向哪里了呢?
  -bash-3.00$ objdump -s liba.so
  Contents of section .data:
  16e0 e0160000 f4150000 01000000 02000000  ................
  结果是数据段里的第二个 int,也是 g_share2!
  2.模块间符号的访问
  模块间的符号访问比模块内的符号访问要麻烦很多,因为动态库运行时被加载到哪里是未知的,为了能使得代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转,ELF 的做法是在动态库的数据段中加一个表项,叫作 GOT(global offset table), GOT 表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。
  仍然使用前面的例子,我们来看看 g_func 是怎么访问 g_share 变量的。
00000504 <g_func>:
504: 55                    push   %ebp
505: 89 e5                 mov    %esp,%ebp
507: 53                    push   %ebx
508: e8 00 00 00 00        call   50d <g_func+0x9>
50d: 5b                    pop    %ebx
50e: 81 c3 bf 11 00 00     add    $0x11bf,%ebx
514: 8b 8b f0 ff ff ff     mov    0xfffffff0(%ebx),%ecx
51a: 8b 93 f0 ff ff ff     mov    0xfffffff0(%ebx),%edx
520: 8b 45 08              mov    0x8(%ebp),%eax
523: 03 02                 add    (%edx),%eax
525: 89 01                 mov    %eax,(%ecx)
527: 8b 45 08              mov    0x8(%ebp),%eax
52a: d1 e0                 shl    %eax
52c: 5b                    pop    %ebx
52d: c9                    leave
52e: c3                    ret
  上面的输出中,508 与 50d 处的指令用于获取 ip 值, 执行完 50d 后, %ebx 中放的是 0x50d, 地址 50e 用于计算 g_share 在 GOT 中的地址 0x50d + 0x11bf + 0xfffffff0 = 0x16bc, 我们检查一下该地址是不是 GOT:
  -bash-3.00$ objdump -h liba.so
  liba.so:     file format elf32-i386
  Sections:
  Idx Name          Size      VMA       LMA       File off  Algn
  //skip some of the output
  16 .got          00000010  000016bc  000016bc  000006bc  2**2
  CONTENTS, ALLOC, LOAD, DATA
  显然,0x16bc 是 GOT 表的第一项。
  事实上,ELF 文件中还包含了一个重定位段,里面记录了哪些符号需要进行重定位,我们可以通过它验证一下上面的计算是否与之匹配:
  -bash-3.00$ objdump -R liba.so
  liba.so:     file format elf32-i386
  DYNAMIC RELOCATION RECORDS
  OFFSET   TYPE              VALUE
  000016e0 R_386_RELATIVE    *ABS*
  000016e4 R_386_RELATIVE    *ABS*
  000016bc R_386_GLOB_DAT    g_share
  000016c0 R_386_GLOB_DAT    __cxa_finalize
  000016c4 R_386_GLOB_DAT    _Jv_RegisterClasses
  000016c8 R_386_GLOB_DAT    __gmon_start__
  000016d8 R_386_JUMP_SLOT   g_func
  000016dc R_386_JUMP_SLOT   __cxa_finalize
  如上输出, g_share 的地址在 0x16bc,与前面的计算完全吻合!
  致此,模块间的数据访问介绍完了,模块间的函数调用在实现原理上是一样的,也需要经过一个类似 GOT 的表格进行跳转,但在具体实现上,ELF 为了实现所谓延迟绑定而作了更精细的处理,接下来会介绍。