Linux下库函数动态链接过程分析-结合glibc-2.11源码

Linux下程序库函数调用的动态链接过程是很常见的,其实刚学编程时写的helloworld程序调用的printf就牵涉到动态链接,只是我们那时没有去注意罢了。
请看下面的helloworld程序反汇编代码
int main(int argc, char **argv) { 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 83 e4 f0 and $0xfffffff0,%esp 80483ea: 83 ec 10 sub $0x10,%esp printf("helloworld/n"); 80483ed: c7 04 24 c8 84 04 08 movl $0x80484c8,(%esp) 80483f4: e8 03 ff ff ff call 80482fc return 0; 80483f9: b8 00 00 00 00 mov $0x0,%eax } 80483fe: c9 leave 80483ff: c3 ret
图1
可以看到图1第9行输出时的指令为call 80482fc
再看看80482fc处是什么东东,如下
Disassembly of section .plt: 080482cc <__gmon_start__@plt-0x10>: 80482cc: ff 35 f8 9f 04 08 pushl 0x8049ff8 80482d2: ff 25 fc 9f 04 08 jmp *0x8049ffc 80482d8: 00 00 add %al,(%eax) ... 080482dc <__gmon_start__@plt>: 80482dc: ff 25 00 a0 04 08 jmp *0x804a000 80482e2: 68 00 00 00 00 push $0x0 80482e7: e9 e0 ff ff ff jmp 80482cc <_init+0x18> 080482ec <__libc_start_main@plt>: 80482ec: ff 25 04 a0 04 08 jmp *0x804a004 80482f2: 68 08 00 00 00 push $0x8 80482f7: e9 d0 ff ff ff jmp 80482cc <_init+0x18> 080482fc : 80482fc: ff 25 08 a0 04 08 jmp *0x804a008 8048302: 68 10 00 00 00 push $0x10 8048307: e9 c0 ff ff ff jmp 80482cc <_init+0x18>
图2
可见这是.plt段的一段代码。细心的我们或许会发现.plt有一个特点,除了第一项外,其他的项都是三条指令,jmp *, push, jmp,并且第三条指令跳到了.plt的第一项。我们来分析这一项:
图2第16行的间接跳转,以GOT表的某一项间接寻址(如果不知道GOT表是什么,看看elf手册就清楚了),GOT表是一个如下的数组
extern Elf32_Addr _GLOBAL_OFFSET_TABLE_[];
我们用readelf -s helloworld命令可以看出这个数组的起始地址为08049ff4,如下

35: 08049ff40 OBJECTLOCALHIDDEN22 _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 : push%ebp 0xb7edda61 : mov%esp,%ebp 0xb7edda63 : sub$0x1c,%esp 0xb7edda66 : mov%ebx,-0xc(%ebp) 0xb7edda69 : mov0x8(%ebp),%eax 0xb7edda6c : call0xb7e969ef 0xb7edda71 : add$0xe3583,%ebx 0xb7edda77 : mov%esi,-0x8(%ebp) 0xb7edda7a : mov%edi,-0x4(%ebp) 0xb7edda7d : mov%eax,(%esp) (gdb)我们现在知道了在上面的helloworld中puts使用了GOT[5]这一项,大家可能有疑问,那么GOT表到底有多少项?其他项是干嘛的? 对于第一个问题,答案是不确定,GOT项的项数是由可执行文件或者共享对象引用的外部对象确定的。 第二个问题,只有前3项有确定的用途,其他项可以用于外部函数,也可以用于外部数据。查阅elf手册可知 The table’s entry zero is reserved to hold the address of the dynamic structure, referenced with the symbol _DYNAMIC. This allows a program, such as the dynamic linker, to find its own dynamic structure without having yet processed its relocation entries. This is especially important for the dynamic linker, because it must initialize itself without relying on other programs to relocate its memory image. On the 32-bit Intel Architecture, entries one and two in the global offset table also are reserved. ‘‘Procedure Linkage Table’’ below describes them. 对于GOT[0],放的是_DYNAMIC的地址,这里仍然需要gdb启动程序查看,如下 lzs@Gentoo /home/lzs/programming/test $ readelf -s helloworld | grep OFFSET 35: 08049ff40 OBJECTLOCALHIDDEN22 _GLOBAL_OFFSET_TABLE_ lzs@Gentoo /home/lzs/programming/test $ readelf -x .got.plt helloworld Hex dump of section '.got.plt': 0x08049ff4 209f0408 00000000 00000000 0a830408............... 0x0804a004 1a830408 2a830408 3a830408....*...:... lzs@Gentoo /home/lzs/programming/test $ readelf -s helloworld | grep _DYNAMIC 38: 08049f200 OBJECTLOCALHIDDEN20 _DYNAMIC lzs@Gentoo /home/lzs/programming/test $ gdb helloworld warning: Can not parse XML syscalls information; XML support was disabled at compile time. GNU gdb (Gentoo 7.0.1 p1) 7.0.1 Copyright (C) 2009 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or laterThis is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details. This GDB was configured as "i686-pc-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /home/lzs/programming/test/helloworld...done. (gdb) b main Breakpoint 1 at 0x80483ed: file helloworld.c, line 6. (gdb) r Starting program: /home/lzs/programming/test/helloworldBreakpoint 1, main (argc=1, argv=0xbffff014) at helloworld.c:6 6 printf("helloworld/n"); (gdb) x/x 0x08049ff4 0x8049ff4 <_GLOBAL_OFFSET_TABLE_>: 0x08049f20 (gdb) x/x 0x08049f20 0x8049f20 <_DYNAMIC>: 0x00000001 (gdb)由上面的结果可以看到,GOT[0]的静态值为209f0408,并不是_DYNAMIC的地址08049f20,但是gdb启动程序看到的动态地址就是_DYNAMIC的地址了。 GOT[1]放的是struct link_map的类型变量的地址,后面详述。 GOT[2]放的是函数_dl_runtime_resolve的地址,也在后面详述。 上面我们可看到,对GOT表的访问使用了绝对地址,这是由于helloworld是一个可执行文件,对于共享对象又有不同的处理。因为共享对象加载的内存位置是运行时确定的,因此对GOT的访问必须是某种偏移的方式,请看objdump -S /lib/libc.so.6 | less -N的结果 12 /lib/libc.so.6:file format elf32-i386 345 Disassembly of section .plt: 67 00016904 : 816904:ff b3 04 00 00 00pushl0x4(%ebx) 91690a:ff a3 08 00 00 00jmp*0x8(%ebx) 1016910:00 00add%al,(%eax) 11... 1213 00016914 : 1416914:ff a3 0c 00 00 00jmp*0xc(%ebx) 151691a:68 00 00 00 00push$0x0 161691f:e9 e0 ff ff ffjmp169041718 00016924 : 1916924:ff a3 10 00 00 00jmp*0x10(%ebx) 201692a:68 08 00 00 00push$0x8 211692f:e9 d0 ff ff ffjmp169042223 00016934 : 2416934:ff a3 14 00 00 00jmp*0x14(%ebx) 251693a:68 10 00 00 00push$0x10 261693f:e9 c0 ff ff ffjmp16904可见对共享对象的GOT的访问是通过相对于寄存器%ebx的偏移访问的,%ebx放的就是GOT的起始地址,至于%ebx的值由谁设定,由于每个共享对象有独立的GOT表,很显然,访问不同的共享对象中的函数需要设置不同的%ebx的值,很自然的由库函数自身来设定,实际上也是这么做的。请看下面printf的汇编码 57305 00046e50 <_IO_printf>: 5730646e50:55push%ebp 5730746e51:89 e5mov%esp,%ebp 5730846e53:53push%ebx 5730946e54:e8 96 fb fc ffcall169ef <_Unwind_Find_FDE@plt+0x7b> 5731046e59:81 c3 9b a1 0f 00add$0xfa19b,%ebx 5731146e5f:83 ec 0csub$0xc,%esp 5731246e62:8d 45 0clea0xc(%ebp),%eax 5731346e65:89 44 24 08mov%eax,0x8(%esp) 5731446e69:8b 45 08mov0x8(%ebp),%eax 5731546e6c:89 44 24 04mov%eax,0x4(%esp) 5731646e70:8b 83 30 ff ff ffmov-0xd0(%ebx),%eax 5731746e76:8b 00mov(%eax),%eax 5731846e78:89 04 24mov%eax,(%esp) 5731946e7b:e8 e0 5a ff ffcall3c960 <_IO_vfprintf> 5732046e80:83 c4 0cadd$0xc,%esp 5732146e83:5bpop%ebx 5732246e84:5dpop%ebp 5732346e85:c3ret第57309和57310就是设置%ebx的值,先看169ef处的代码 90169ef:8b 1c 24mov(%esp),%ebx 91169f2:c3ret因此57309行的call返回后就取得了call指令的地址到%ebx,第57310在该值上加上一个偏移0xfa19b就得到本共享对象(libc.so.6)的GOT表的地址。 关于GOT[1],下面是我写的一个小程序,可以通过GOT[1]中的link_map地址打印出装载的共享库信息。 #include#include #include#include#include #include#include #include #include#include#include#include#include static void print_link_map(const int *got) { const struct link_map *link_map, *lnk_tmp; int i; for (i = 0; i < 3; i++){ printf("got[%d] = %#x/n", i, got[i]); } link_map = (const struct link_map *)got[1]; printf("Loaded images:/n"); for (lnk_tmp = link_map; lnk_tmp; lnk_tmp = lnk_tmp->l_next){ printf("/t%s: %#x/n", lnk_tmp->l_name, lnk_tmp->l_addr); } printf("/n***************/n/n"); } int main(int argc, char **argv) { int fd; int base, img_base; int i, dyna_sect_sz = 0; struct stat stat; const Elf32_Ehdr *ehdr; const Elf32_Shdr *shdr, *dyna_sect; const Elf32_Phdr *phdr; const char *strtab; const int *got = 0; if ((fd = open(argv[0], O_RDONLY)) < 0){ perror("open"); exit(errno); } if (fstat(fd, &stat) < 0){ perror("fstat"); exit(errno); } if (-1 == (base = (int)mmap(0, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0))){ perror("mmap"); exit(errno); } if (close(fd) < 0){ perror("close"); exit(errno); } ehdr = (const Elf32_Ehdr *)base; phdr = (const Elf32_Phdr *)(base + ehdr->e_phoff); /* * get the progress image's base address, for executable file, this * address is the same with the static address */ for (i = 0; i < ehdr->e_phnum; i++){ if (PT_LOAD == phdr[i].p_type){ img_base = phdr[i].p_vaddr; break; } } shdr = (const Elf32_Shdr *)(base + ehdr->e_shoff); strtab = (const char *)(base + shdr[ehdr->e_shstrndx].sh_offset); for (i = 0; i < ehdr->e_shnum; i++){ if (!strcmp(".got.plt", strtab + shdr[i].sh_name)){ got = (const int *)shdr[i].sh_addr; break; } } if (0 == got){ /*try section .got*/ for (i = 0; i < ehdr->e_shnum; i++){ if (!strcmp(".got", strtab + shdr[i].sh_name)){ got = (const int *)shdr[i].sh_addr; break; } } } assert(got); /*get .dynamic section*/ for (i = 0; i < ehdr->e_shnum; i++){ if (!strcmp(".dynamic", strtab + shdr[i].sh_name)){ dyna_sect = shdr + i; dyna_sect_sz = dyna_sect->sh_size; break; } } /*temp map file is useless now, unmap it*/ if (munmap((void *)base, stat.st_size) < 0){ perror("munmap"); exit(errno); } printf("one way to get got address(from .got.plt or .got ...)/n"); print_link_map(got); /*another way to get got address*/ for (i = 0; i < dyna_sect_sz / sizeof(Elf32_Dyn); i++){ if (DT_PLTGOT == _DYNAMIC[i].d_tag){ got = (const int *)_DYNAMIC[i].d_un.d_ptr; } } printf("another way to get got address(from .dynamic ...)/n"); print_link_map(got); return 0; }图3. 打印已装载的共享库信息小程序 编译 gcc -Wall -msse3 -mfpmath=sse -g-lm -ldl -lpthreadlink_map.c-o link_map 为了得到更多的依赖库,这里强加上了-lm -ldl -lpthread 运行结果: lzs@Gentoo /home/lzs/programming/test $ ./link_mapone way to get got address(from .got.plt or .got ...) got[0] = 0x8049f08 got[1] = 0xb772a8e0 got[2] = 0xb7720c80 Loaded images: : 0 : 0xb770e000 /lib/libm.so.6: 0xb76ca000 /lib/libdl.so.2: 0xb76c6000 /lib/libpthread.so.0: 0xb76ad000 /lib/libc.so.6: 0xb7568000 /lib/ld-linux.so.2: 0xb770d000 *************** another way to get got address(from .dynamic ...) got[0] = 0x8049f08 got[1] = 0xb772a8e0 got[2] = 0xb7720c80 Loaded images: : 0 : 0xb770e000 /lib/libm.so.6: 0xb76ca000 /lib/libdl.so.2: 0xb76c6000 /lib/libpthread.so.0: 0xb76ad000 /lib/libc.so.6: 0xb7568000 /lib/ld-linux.so.2: 0xb770d000 *************** 下面部分结合glibc-2.11源码分析动态链接的过程
方便起见,还是一helloworld程序为例。我们可以看到,主程序helloworld调用puts时使用的call指令把控制转移到plt的相应项(图1第09行->图2第15行),每个库函数相应的plt项为三条指令(jmp *, push, jmp),对于puts来说,如下:
80482fc:ff 25 08 a0 04 08jmp*0x804a008

8048302:68 10 00 00 00push$0x10
8048307:e9 c0 ff ff ffjmp80482cc <_init+0x18>

前面谈到,0x804a008地址的初始内容为8048302,也就是第二条指令push的地址,因此jmp *执行完后,控制到了push指令,第三条的jmp又转移到下面:

80482cc:ff 35 f8 9f 04 08pushl0x8049ff8
80482d2:ff 25 fc 9f 04 08jmp*0x8049ffc
80482d8:00 00add%al,(%eax)
前面说到0x8049ff8也就是&GOT[1]放到的是struct link_map *l变量,l是一个链,该链上有所有已经加载的共享对象的信息。
【Linux下库函数动态链接过程分析-结合glibc-2.11源码】0x8049ffc也就是&GOT[2]放的是函数_dl_runtime_resolve的地址,容易分析出,当控制到了_dl_runtime_resolve时,栈的情况如下

图4. 控制到_dl_runtime_resolve时栈的情况


_dl_runtime_resolve程序比较简单,如图5所示

图5. _dl_runtime_resolve函数
执行了图5中蓝色高亮的call指令后,控制转移到_dl_fixup,这里采用了寄存器传参,容易分析出_dl_fixup的第一个参数eax为GOT[1],第二个参数edx为0x10,如图5所示。

图6. 执行call _dl_fixup前栈的情况
下面控制转移到_dl_fixup函数。我们已经知道这个函数的第一个参数为struct link_map *的链,那么第二个参数是0x10是什么呢?是puts函数对应的重定位项的在重定位表的偏移。如下

lzs@Gentoo /home/lzs/programming/test $ readelf -r helloworld

Relocation section '.rel.dyn' at offset 0x2e4 contains 1 entries:
OffsetInfoTypeSym.ValueSym. Name
08049ff000000106 R_386_GLOB_DAT00000000__gmon_start__

Relocation section '.rel.plt' at offset 0x2ec contains 3 entries:
OffsetInfoTypeSym.ValueSym. Name
0804a00000000107 R_386_JUMP_SLOT00000000__gmon_start__
0804a00400000307 R_386_JUMP_SLOT00000000__libc_start_main
0804a00800000407 R_386_JUMP_SLOT00000000puts
由于重定位项每项大小为8Byte,因此puts的重定位项偏移恰好为0x10。 下面逐段分析_dl_fixup函数。 图7. _dl_fixup part I 第76行80行得到puts的符号表项,用于在相应的共享对象中查找puts函数并得到其地址信息,第81行得到puts的相应重定位地址,大家可能已经想到了,该地址就为&GOT[5] (0x804a008)。 图8. _dl_fixup part II 图9. _dl_fixup part III 图10. _dl_fixup part IV 图8-图10,解析出puts函数在内存的绝对地址,并存放在value中。 图11. _dl_fixup part V 第154行把解析出的绝对地址写入重定位地址,也就是GOT[5]中。 继续回到图5的_dl_runtime_resolve函数,第43行的ret指令直接进入puts函数并清除栈中冗余项,至此重定位过程已经完成。 上面有些细节问题(例如puts函数绝对地址的查找过程)有些复杂,很值得去研究。很显然的,上面的重定位过程只是在调用了共享对象中的函数且是第一次此调用时才进行,以后的调用通过plt中的jmp *指令直接跳转到了对应的函数。

    推荐阅读