Chinaunix首页 | 论坛 | 博客
  • 博客访问: 488836
  • 博文数量: 104
  • 博客积分: 3045
  • 博客等级: 少校
  • 技术积分: 1230
  • 用 户 组: 普通用户
  • 注册时间: 2008-02-29 10:18
文章分类

全部博文(104)

文章存档

2011年(72)

2010年(1)

2009年(1)

2008年(30)

分类: C/C++

2011-05-22 17:40:01

转自:http://blog.csdn.net/lzshlzsh/archive/2010/12/09/6066628.aspx

Linux下程序库函数调用的动态链接过程是很常见的,其实刚学编程时写的helloworld程序调用的printf就牵涉到动态链接,只是我们那时没有去注意罢了。

请看下面的helloworld程序反汇编代码

  1. int main(int argc, char **argv)  
  2. {  
  3.  80483e4:       55                      push   %ebp  
  4.  80483e5:       89 e5                   mov    %esp,%ebp  
  5.  80483e7:       83 e4 f0                and    $0xfffffff0,%esp  
  6.  80483ea:       83 ec 10                sub    $0x10,%esp  
  7.         printf("helloworld\n");  
  8.  80483ed:       c7 04 24 c8 84 04 08    movl   $0x80484c8,(%esp)  
  9.  80483f4:       e8 03 ff ff ff          call   80482fc   
  10.         return 0;  
  11.  80483f9:       b8 00 00 00 00          mov    $0x0,%eax  
  12. }  
  13.  80483fe:       c9                      leave    
  14.  80483ff:       c3                      ret      
 

图1

可以看到图1第9行输出时的指令为call 80482fc

再看看80482fc处是什么东东,如下

  1. Disassembly of section .plt:  
  2. 080482cc <__gmon_start__@plt-0x10>:  
  3.  80482cc:       ff 35 f8 9f 04 08       pushl  0x8049ff8  
  4.  80482d2:       ff 25 fc 9f 04 08       jmp    *0x8049ffc  
  5.  80482d8:       00 00                   add    %al,(%eax)  
  6.         ...  
  7. 080482dc <__gmon_start__@plt>:  
  8.  80482dc:       ff 25 00 a0 04 08       jmp    *0x804a000  
  9.  80482e2:       68 00 00 00 00          push   $0x0  
  10.  80482e7:       e9 e0 ff ff ff          jmp    80482cc <_init+0x18>  
  11. 080482ec <__libc_start_main@plt>:  
  12.  80482ec:       ff 25 04 a0 04 08       jmp    *0x804a004  
  13.  80482f2:       68 08 00 00 00          push   $0x8  
  14.  80482f7:       e9 d0 ff ff ff          jmp    80482cc <_init+0x18>  
  15. 080482fc :  
  16.  80482fc:       ff 25 08 a0 04 08       jmp    *0x804a008  
  17.  8048302:       68 10 00 00 00          push   $0x10  
  18.  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: 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 : push   %ebp
0xb7edda61 : mov    %esp,%ebp
0xb7edda63 : sub    $0x1c,%esp
0xb7edda66 : mov    %ebx,-0xc(%ebp)
0xb7edda69 : mov    0x8(%ebp),%eax
0xb7edda6c : call   0xb7e969ef
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: 08049ff4     0 OBJECT  LOCAL  HIDDEN   22 _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: 08049f20     0 OBJECT  LOCAL  HIDDEN   20 _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 later
This 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/helloworld 
Breakpoint 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的结果
      1 
      2 /lib/libc.so.6:     file format elf32-i386
      3 
      4 
      5 Disassembly of section .plt:
      6 
      7 00016904 :
      8    16904:       ff b3 04 00 00 00       pushl  0x4(%ebx)
      9    1690a:       ff a3 08 00 00 00       jmp    *0x8(%ebx)
     10    16910:       00 00                   add    %al,(%eax)
     11         ...
     12 
     13 00016914 :
     14    16914:       ff a3 0c 00 00 00       jmp    *0xc(%ebx)
     15    1691a:       68 00 00 00 00          push   $0x0
     16    1691f:       e9 e0 ff ff ff          jmp    16904
     17 
     18 00016924 :
     19    16924:       ff a3 10 00 00 00       jmp    *0x10(%ebx)
     20    1692a:       68 08 00 00 00          push   $0x8
     21    1692f:       e9 d0 ff ff ff          jmp    16904
     22 
     23 00016934 :
     24    16934:       ff a3 14 00 00 00       jmp    *0x14(%ebx)
     25    1693a:       68 10 00 00 00          push   $0x10
     26    1693f:       e9 c0 ff ff ff          jmp    16904
可见对共享对象的GOT的访问是通过相对于寄存器%ebx的偏移访问的,%ebx放的就是GOT的起始地址,至于%ebx的值由谁设定,由于每个共享对象有独立的GOT表,很显然,访问不同的共享对象中的函数需要设置不同的%ebx的值,很自然的由库函数自身来设定,实际上也是这么做的。请看下面printf的汇编码
  57305 00046e50 <_IO_printf>:
  57306    46e50:       55                      push   %ebp
  57307    46e51:       89 e5                   mov    %esp,%ebp
  57308    46e53:       53                      push   %ebx
  57309    46e54:       e8 96 fb fc ff          call   169ef <_Unwind_Find_FDE@plt+0x7b>
  57310    46e59:       81 c3 9b a1 0f 00       add    $0xfa19b,%ebx
  57311    46e5f:       83 ec 0c                sub    $0xc,%esp
  57312    46e62:       8d 45 0c                lea    0xc(%ebp),%eax
  57313    46e65:       89 44 24 08             mov    %eax,0x8(%esp)
  57314    46e69:       8b 45 08                mov    0x8(%ebp),%eax
  57315    46e6c:       89 44 24 04             mov    %eax,0x4(%esp)
  57316    46e70:       8b 83 30 ff ff ff       mov    -0xd0(%ebx),%eax
  57317    46e76:       8b 00                   mov    (%eax),%eax
  57318    46e78:       89 04 24                mov    %eax,(%esp)
  57319    46e7b:       e8 e0 5a ff ff          call   3c960 <_IO_vfprintf>
  57320    46e80:       83 c4 0c                add    $0xc,%esp
  57321    46e83:       5b                      pop    %ebx
  57322    46e84:       5d                      pop    %ebp
  57323    46e85:       c3                      ret    
第57309和57310就是设置%ebx的值,先看169ef处的代码
     90    169ef:       8b 1c 24                mov    (%esp),%ebx
     91    169f2:       c3                      ret    
因此57309行的call返回后就取得了call指令的地址到%ebx,第57310在该值上加上一个偏移0xfa19b就得到本共享对象(libc.so.6)的GOT表的地址。
关于GOT[1],下面是我写的一个小程序,可以通过GOT[1]中的link_map地址打印出装载的共享库信息。
  1. #include   
  2. #include   
  3. #include   
  4. #include   
  5. #include   
  6. #include   
  7. #include   
  8. #include   
  9. #include   
  10. #include   
  11. #include   
  12. #include   
  13. #include   
  14. static void print_link_map(const int *got)  
  15. {  
  16.     const struct link_map *link_map, *lnk_tmp;  
  17.     int i;  
  18.     for (i = 0; i < 3; i++){  
  19.         printf("got[%d] = %#x\n", i, got[i]);  
  20.     }  
  21.     link_map = (const struct link_map *)got[1];  
  22.     printf("Loaded images:\n");  
  23.     for (lnk_tmp = link_map; lnk_tmp; lnk_tmp = lnk_tmp->l_next){  
  24.         printf("\t%s: %#x\n", lnk_tmp->l_name, lnk_tmp->l_addr);  
  25.     }  
  26.     printf("\n***************\n\n");  
  27. }  
  28. int main(int argc, char **argv)  
  29. {  
  30.     int fd;  
  31.     int base, img_base;  
  32.     int i, dyna_sect_sz = 0;  
  33.     struct stat stat;  
  34.     const Elf32_Ehdr *ehdr;  
  35.     const Elf32_Shdr *shdr, *dyna_sect;  
  36.     const Elf32_Phdr *phdr;  
  37.     const char *strtab;  
  38.     const int *got = 0;  
  39.     if ((fd = open(argv[0], O_RDONLY)) < 0){  
  40.         perror("open");  
  41.         exit(errno);  
  42.     }  
  43.     if (fstat(fd, &stat) < 0){  
  44.         perror("fstat");  
  45.         exit(errno);  
  46.     }  
  47.       
  48.     if (-1 == (base = (int)mmap(0, stat.st_size, PROT_READ, MAP_PRIVATE,   
  49.                     fd, 0))){  
  50.         perror("mmap");  
  51.         exit(errno);  
  52.     }  
  53.     if (close(fd) < 0){  
  54.         perror("close");  
  55.         exit(errno);  
  56.     }  
  57.     ehdr = (const Elf32_Ehdr *)base;  
  58.     phdr = (const Elf32_Phdr *)(base + ehdr->e_phoff);  
  59.     /* 
  60.      * get the progress image's base address, for executable file, this  
  61.      * address is the same with the static address 
  62.      */  
  63.     for (i = 0; i < ehdr->e_phnum; i++){  
  64.         if (PT_LOAD == phdr[i].p_type){  
  65.             img_base = phdr[i].p_vaddr;  
  66.             break;  
  67.         }  
  68.     }  
  69.     shdr = (const Elf32_Shdr *)(base + ehdr->e_shoff);  
  70.     strtab = (const char *)(base + shdr[ehdr->e_shstrndx].sh_offset);  
  71.     for (i = 0; i < ehdr->e_shnum; i++){  
  72.         if (!strcmp(".got.plt", strtab + shdr[i].sh_name)){  
  73.             got = (const int *)shdr[i].sh_addr;  
  74.             break;  
  75.         }  
  76.     }  
  77.     if (0 == got){  
  78.         /*try section .got*/  
  79.         for (i = 0; i < ehdr->e_shnum; i++){  
  80.             if (!strcmp(".got", strtab + shdr[i].sh_name)){  
  81.                 got = (const int *)shdr[i].sh_addr;  
  82.                 break;  
  83.             }  
  84.         }  
  85.     }  
  86.       
  87.     assert(got);  
  88.       
  89.     /*get .dynamic section*/  
  90.     for (i = 0; i < ehdr->e_shnum; i++){  
  91.         if (!strcmp(".dynamic", strtab + shdr[i].sh_name)){  
  92.             dyna_sect = shdr + i;  
  93.             dyna_sect_sz = dyna_sect->sh_size;  
  94.             break;  
  95.         }  
  96.     }  
  97.     /*temp map file is useless now, unmap it*/  
  98.     if (munmap((void *)base, stat.st_size) < 0){  
  99.         perror("munmap");  
  100.         exit(errno);  
  101.     }  
  102.     printf("one way to get got address(from .got.plt or .got ...)\n");  
  103.       
  104.     print_link_map(got);  
  105.     /*another way to get got address*/  
  106.     for (i = 0; i < dyna_sect_sz / sizeof(Elf32_Dyn); i++){  
  107.         if (DT_PLTGOT == _DYNAMIC[i].d_tag){  
  108.             got =  (const int *)_DYNAMIC[i].d_un.d_ptr;  
  109.         }  
  110.     }  
  111.       
  112.     printf("another way to get got address(from .dynamic ...)\n");  
  113.     print_link_map(got);  
  114.     return 0;  
  115. }  
 
图3. 打印已装载的共享库信息小程序
编译
gcc -Wall -msse3 -mfpmath=sse -g   -lm -ldl -lpthread   link_map.c   -o link_map
为了得到更多的依赖库,这里强加上了-lm -ldl -lpthread
运行结果:
lzs@Gentoo /home/lzs/programming/test $ ./link_map 
one 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 08       jmp    *0x804a008  

8048302:       68 10 00 00 00          push   $0x10  

8048307:       e9 c0 ff ff ff          jmp    80482cc <_init+0x18>

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

80482cc:       ff 35 f8 9f 04 08       pushl  0x8049ff8  

80482d2:       ff 25 fc 9f 04 08       jmp    *0x8049ffc  

80482d8:       00 00                   add    %al,(%eax)  

前面说到0x8049ff8也就是&GOT[1]放到的是struct link_map *l变量,l是一个链,该链上有所有已经加载的共享对象的信息。

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:

 Offset     Info    Type            Sym.Value  Sym. Name

08049ff0  00000106 R_386_GLOB_DAT    00000000   __gmon_start__

Relocation section '.rel.plt' at offset 0x2ec contains 3 entries:

 Offset     Info    Type            Sym.Value  Sym. Name

0804a000  00000107 R_386_JUMP_SLOT   00000000   __gmon_start__

0804a004  00000307 R_386_JUMP_SLOT   00000000   __libc_start_main

0804a008  00000407 R_386_JUMP_SLOT   00000000   puts

由于重定位项每项大小为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 *指令直接跳转到了对应的函数。
阅读(2402) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~