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

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

intmain(intargc,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

return0;

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 :

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 :

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

080482ec :

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

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

图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

6printf("helloworld\n");

(gdb) x/x 0x804a008

0x804a008 :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

8return 0;

(gdb) x/x 0x804a008

0x804a008 :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

6printf("helloworld\n");

(gdb) x/x 0x08049ff4

0x8049ff4 :0x08049f20

(gdb) x/x 0x08049f20

0x8049f20 :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 :

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

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

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地址打印出装载的共享库信息。

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

staticvoidprint_link_map(constint*got)

{

conststructlink_map *link_map, *lnk_tmp;

inti;

for(i = 0; i

printf("got[%d] = %#x\n", i, got[i]);

}

link_map = (conststructlink_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");

}

intmain(intargc,char**argv)

{

intfd;

intbase, img_base;

inti, dyna_sect_sz = 0;

structstat stat;

constElf32_Ehdr *ehdr;

constElf32_Shdr *shdr, *dyna_sect;

constElf32_Phdr *phdr;

constchar*strtab;

constint*got = 0;

if((fd = open(argv[0], O_RDONLY))

perror("open");

exit(errno);

}

if(fstat(fd, &stat)

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)

perror("close");

exit(errno);

}

ehdr = (constElf32_Ehdr *)base;

phdr = (constElf32_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 e_phnum; i++){

if(PT_LOAD == phdr[i].p_type){

img_base = phdr[i].p_vaddr;

break;

}

}

shdr = (constElf32_Shdr *)(base + ehdr->e_shoff);

strtab = (constchar*)(base + shdr[ehdr->e_shstrndx].sh_offset);

for(i = 0; i e_shnum; i++){

if(!strcmp(".got.plt", strtab + shdr[i].sh_name)){

got = (constint*)shdr[i].sh_addr;

break;

}

}

if(0 == got){

/*try section .got*/

for(i = 0; i e_shnum; i++){

if(!strcmp(".got", strtab + shdr[i].sh_name)){

got = (constint*)shdr[i].sh_addr;

break;

}

}

}

assert(got);

/*get .dynamic section*/

for(i = 0; i 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)

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 sizeof(Elf32_Dyn); i++){

if(DT_PLTGOT == _DYNAMIC[i].d_tag){

got =  (constint*)_DYNAMIC[i].d_un.d_ptr;

}

}

printf("another way to get got address(from .dynamic ...)\n");

print_link_map(got);

return0;

}

图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

前面谈到,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 *指令直接跳转到了对应的函数。

你可能感兴趣的:(linux,libc源码)