前面分析的是从应用程序调用动态库的情况。动态库本身是怎么完成符号解析的呢?
根据Ian Lance Taylor的说法,最好是以PIC(-fpic)的方式编译shared lib,不这样也可以,但会增加dynamic linker做重定位的负担。以PIC方式编译的lib可以大量减少必要的relocation info,但调用non-static functions和访问global/static variables的时候都需要通过plt/got间接进行 (All problems in computer science can be solved by another level of indirection.)

如果libfoo.so是以PIC模式编译的,并调用了一个外部函数bar,则bar会出现在.rel.plt section中(readelf -r);而如果不是以PIC模式编译,则bar将出现在.rel.dyn section中。如果是后者的话,dynamic linker会在load libfoo.so的时候利用该section提供的偏移量信息,直接将其中引用bar的地方patch上,这样一来也就意味着指令本身被修改了(dynamic linker之后是不是应该重新将指令改为只读?),因此也就丧失了可被多个进程共享的特性。而如果是PIC模式,则会有一次间接的过程,我们现在分析的就是这一过程。与函数调用不同,我们发现全局变量不论是以哪种方式编译,它们的重定位信息都被置于.rel.dyn中,我想这是因为数据的访问不像控制转移一样可以借助几层跳转来完成,因此也无法进行lazy binding而必须在load时做完。

PIC模式与非PIC模式最大的不同就是前者不直接patch指令,而是patch GOT。所有的指令都访问GOT从而来达到position independence,与前文中动态解析库函数的idea非常类似。可是既然是地址无关的,怎么知道GOT的位置呢?关键在于每一个shared lib都带有自己的GOT,而且整个lib是作为一个整体被load到内存,因此GOT的基址与每条指令的相对偏移总是确定的。这样一来,一条指令在访问GOT的时候,只要算出自己当前的IP地址,再加上这个被静态确定下来的偏移量,就可以定位到自己要访问的symbol的GOT entry了。



计算当前IP地址的函数一般是__i686.get_pc_thunk.bx,它会附带在每个PIC module中,因此它与调用它的函数的相对偏移也是固定下来的。它非常简单:


mov (%esp),%ebx
ret


这样就把它的返回地址,也就是caller function的IP地址给放到了ebx中。有时还能见到__i686.get_pc_thunk.cx,会写入ecx,这是因为ebx是callee saved reg,而ecx是caller saved,因此如果一个函数要调用别的函数则最好使用ebx,否则最好使用其它寄存器。接下来的指令(0x00113458)使用自己的IP加上一个固定的偏移便得到了本lib的GOT地址。这一值通常会一直缓存在寄存器(ebx)中。

(gdb) disassemble
Dump of assembler code for function foo:
0x0011344c : push %ebp
0x0011344d : mov %esp,%ebp
0x0011344f : push %ebx
0x00113450 : sub $0x4,%esp
0x00113453 : call 0x113447 <__i686.get_pc_thunk.bx>
0x00113458 : add $0x1180,%ebx
0x0011345e : mov 0xfffffff0(%ebx),%eax ; a negative number because we are accessing .got from .got.plt
0x00113464 : movb $0x32,(%eax)
0x00113467 : call 0x113320
0x0011346c : lea 0xffffef08(%ebx),%eax
...


0x00113458这条指令的$0x1180是如何得来的呢?
我们运行readelf -r foo.o得到:


Relocation section '.rel.text' at offset 0x484 contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00000008 00000b02 R_386_PC32 00000000 __i686.get_pc_thunk.bx
0000000e 00000c0a R_386_GOTPC 00000000 _GLOBAL_OFFSET_TABLE_
...


说明linker应该在.text section的0xe偏移出patch上_GLOBAL_OFFSET_TABLE_的真实地址。而0xe对应的恰恰就是foo.o的.text section的那条add指令的操作数:


$ objdump -d foo.o
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 53 push %ebx
4: 83 ec 04 sub $0x4,%esp
7: e8 fc ff ff ff call 8 ; 0x8: __i686.get_pc_thunk.bx
c: 81 c3 02 00 00 00 add $0x2,%ebx ; 0xe: _GLOBAL_OFFSET_TABLE_
...


我们再看一下_GLOBAL_OFFSET_TABLE_这个symbol的值在libfoo.so中是多少


$ nm libfoo.so |grep _GLOBAL_OFFSET_TABLE_
000015d8 a _GLOBAL_OFFSET_TABLE_
$ readelf -S libfoo.so|grep 15d8
[20] .got.plt PROGBITS 000015d8 0005d8 00001c 04 WA 0 0 4



正是.got.plt的地址!因此可以看出来,所有访问got的指令都留了个空,告诉linker在决定了got的地址时(其实也就是偏移量而非绝对地址),把got相对于该指令的偏移量填进来。

注意计算了半天得到got的地址只是为了能访问全局变量,因为需要绝对地址(0x0011345e)。而调用plt中的函数只需要一个相对地址就够了,因此不需要通过ebx来间接访问(0x00113467)。