Linux下程序库函数调用的动态链接过程是很常见的,其实刚学编程时写的helloworld程序调用的printf就牵涉到动态链接,只是我们那时没有去注意罢了。
请看下面的helloworld程序反汇编代码
图1
可以看到图1第9行输出时的指令为call 80482fc <puts@plt>
再看看80482fc处是什么东东,如下
图2
可见这是.plt段的一段代码。细心的我们或许会发现.plt有一个特点,除了第一项外,其他的项都是三条指令,jmp *, push, jmp,并且第三条指令跳到了.plt的第一项。我们来分析<puts@plt>这一项:
图2第16行的间接跳转,以GOT表的某一项间接寻址(如果不知道GOT表是什么,看看elf手册就清楚了),GOT表是一个如下的数组
extern Elf32_Addr _GLOBAL_OFFSET_TABLE_[];
我们用readelf -s helloworld命令可以看出这个数组的起始地址为08049ff4,如下
35: 08049ff4 0 OBJECT LOCAL HIDDEN 22 _GLOBAL_OFFSET_TABLE_
可以计算一下,上面的间接跳转以GOT[(0x804a008 - 0x8049ff4) / 4] = GOT[5]的值间接跳转,那么这个值是什么呢,当然静态是看不到的,我们gdb启动程序并查看0x804a008地址的值
(gdb) b main
Breakpoint 1 at 0x80483ed: file helloworld.c, line 6.
(gdb) r
Starting program: /home/lzs/programming/test/helloworld
Breakpoint 1, main (argc=1, argv=0xbfffefe4) at helloworld.c:6
6 printf("helloworld/n");
(gdb) x/x 0x804a008
0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0x08048302
(gdb)
可见该值为0x08048302,大家发现了没有,这个值就是图2第17行指令的地址,也就是执行了第16行的指令后就跑到了第17行,然后到了.plt的第一项,再然后又间接跳转到了某个地方,大家觉得这很没有必要,其实是有其原因的,这就是动态链接的过程。试想如果有人把GOT[5]的值改成了puts的绝对地址后,那么第16行的间接跳转不就直接到了puts的函数体了吗。
到了这里,大家可能有很多疑问,试列举如下:
1。第18行跳到.plt的第一项,然后第4行间接跳转到哪儿去了,干了些什么事情?
答:这是动态链接的过程,本文后面要结合源码分析的,现在可以简单的说下,第4行的间接跳转到了ld-linux.so.2的_dl_runtime_resolve函数,这个函数解析出puts的绝对地址,回填到GOT[5],以达到前面所叙的效果。
2。为什么不一上来就把所有的库函数对应的GOT[]项全部填上函数的绝对地址?
答:这是Linux系统设计的哲学之一,称之为LAZY的策略,即真正要用到某个东西时,我才为它构建好必要的环境,如这里的地址回填,又如COW机制。这是有性能考虑的,程序中可能会引用很多的库函数,但有很多库函数并不会真正执行到,如if (error){perror("err"); exit(errno);}的perror函数几乎不会执行,这样为这些不会执行的函数解析出绝对地址是没有必要的,也会增加时间的开销。
在puts函数执行了一次后,由于有动态链接的过程,GOT[5]就回填上了puts的绝对地址,请看下面
(gdb) n
helloworld
8 return 0;
(gdb) x/x 0x804a008
0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0xb7edda60
(gdb) x/10i 0xb7edda60
0xb7edda60 <puts>: push %ebp
0xb7edda61 <puts+1>: mov %esp,%ebp
0xb7edda63 <puts+3>: sub $0x1c,%esp
0xb7edda66 <puts+6>: mov %ebx,-0xc(%ebp)
0xb7edda69 <puts+9>: mov 0x8(%ebp),%eax
0xb7edda6c <puts+12>: call 0xb7e969ef
0xb7edda71 <puts+17>: add $0xe3583,%ebx
0xb7edda77 <puts+23>: mov %esi,-0x8(%ebp)
0xb7edda7a <puts+26>: mov %edi,-0x4(%ebp)
0xb7edda7d <puts+29>: mov %eax,(%esp)
(gdb)
我们现在知道了在上面的helloworld中puts使用了GOT[5]这一项,大家可能有疑问,那么GOT表到底有多少项?其他项是干嘛的?
对于第一