延迟加载
  我们知道,动态库是在程序启动的时候加载进来的,加载后,动态链接器需要对其作一系列的初始化,如符号重定位,这些工作是比较费时的,特别是对函数的重定位,那么我们能不能把对函数的重定位延迟进行呢?这个改进是很有意义的,毕竟很多时候,一个动态库里可能包含很多的全局函数,但是我们往往可能只用到了其中一小部分而已,完全没必要把那些没用到的函数也过早进行重定位,具体来说,是应该等到第一次发生对该函数的调用时才进行符号绑定 -- 此谓之延迟绑定。
  延迟绑定的实现步骤如下:
  建立一个 GOT.PLT 表,该表用来放全局函数的实际地址,但开始时,该里面放的不是真实的地址而是一个跳转,接下来会讲。
  对每一个全局函数,链接器生成一个与之相对应的影子函数,如 fun@plt
  所有对 fun 的调用,都换成对 fun@plt 的调用, 长成如下样子:
  fun@plt:
  jmp *(fun@got.plt)
  push index
  jmp _init
  其中第一条指令直接从 got.plt 中去拿真实的函数地址,如果已经之前已经发生过调用,got.plt 已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 fun@plt 中的第二条指令,这使得当执行第一次调用时,fun@plt中的第一条指令其实什么事也没做,直接继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号作为参数传给 init(),而 init() 这个函数则用于把 fun 进行重定位,然后把结果写入到 got.plt 相应的地方,后直接跳过去该函数。
  仍然是使用前面的例子,我们看看 g_func2 是怎样调用 g_func 的:
  0000052f <g_func2>:
  52f: 55                    push   %ebp
  530: 89 e5                 mov    %esp,%ebp
  532: 53                    push   %ebx
  533: 83 ec 14              sub    $0x14,%esp
  536: e8 00 00 00 00        call   53b <g_func2+0xc>
  53b: 5b                    pop    %ebx
  53c: 81 c3 91 11 00 00     add    $0x1191,%ebx
  542: c7 45 f8 02 00 00 00  movl   $0x2,0xfffffff8(%ebp) // a = 2
  549: 83 ec 0c              sub    $0xc,%esp
  54c: 6a 03                 push   $0x3 // push argument 3 for g_func.
  54e: e8 d5 fe ff ff        call   428 <g_func@plt>
  553: 83 c4 10              add    $0x10,%esp
  556: 89 45 f4              mov    %eax,0xfffffff4(%ebp)
  559: 8b 45 f4              mov    0xfffffff4(%ebp),%eax
  55c: 03 45 f8              add    0xfffffff8(%ebp),%eax
  55f: 8b 5d fc              mov    0xfffffffc(%ebp),%ebx
  562: c9                    leave
  563: c3                    ret
  如上汇编,指令 536, 53b, 53c, 用于计算 got.plt 的具体位置,计算方式与前面对数据的访问原理是一样的,经计算此时, %ebx = 0x53b + 0x1191 = 0x16cc, 注意指令 54e, 该指令调用了函数 g_func@plt:
  00000428 <g_func@plt>:
  428: ff a3 0c 00 00 00     jmp    *0xc(%ebx)
  42e: 68 00 00 00 00        push   $0x0
  433: e9 e0 ff ff ff        jmp    418 <_init+0x18>
  注意到此时, %ebx 中放的是 got.plt 的地址,g_func@plt 的第一条指令用于获取 got.plt 中 func 的具体地址, func 放在 0xc + %ebx = 0xc + 0x16cc = 0x16d8, 这个地址里放的是什么呢?我们查一下重定位表:
  -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_func 的具体地址,那此时 0x16d8 放的是真正的地址了吗?我们再看看 got.plt:
  Contents of section .got.plt:
  16cc fc150000 00000000 00000000 2e040000  ................
  16dc 3e040000
  16d8 处的内容是: 2e040000, 小端序,换回整形是 0x000042e, 该地址是 fun@plt 的第二条指令!是不是觉得有点儿绕?你可以定下心来再看一遍,其实不绕,而是很巧妙。
  后话
  对动态链接库来说,加载时重定位与链接时重定位各有优缺点,前者使得动态库的代码段不能被多个进程间所共享,加载动态库时也比较费时,但是加载完成后,因为对符号的引用不需要进行跳转,程序运行的效率相对是较高的。而对地址无关的代码,它的缺点是动态库的体积相对较大,毕竟增加了很多表项及相关的函数,另外运行时对全局符号的引用需要通过表格进行跳转,程序执行的效率不可避免有所损失,优点嘛,是动态库加载比较快,而且代码可以在多个进程间共享,对整个系统而言,可以大大节约对内存的使用,这个好处的吸引力是非常大的,所以你可以看到,目前来说在常用的动态库使用上,PIC 相较而言是更加被推崇的,道理在此。