Chinaunix首页 | 论坛 | 博客
  • 博客访问: 59246
  • 博文数量: 27
  • 博客积分: 2000
  • 博客等级: 大尉
  • 技术积分: 300
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-24 17:31
文章分类
文章存档

2011年(1)

2010年(8)

2009年(18)

我的朋友

分类: LINUX

2009-06-28 10:56:11

 

elf动态解析符号过程(修订版)(WSS-Articles-02001)


Author: alert7

Email: alert7@whitecell.org

Homepage:http://www.whitecell.org

Date: 2002-01-10

 

 

★★ 前言

 

本篇文章以linux为平台为例,演示elf动态解析符号的过程。不正之处,还请斧正。

 

通常,elf解析符号方式是用称为lazy MODE装载的。这种装载技术是linux平台上 默认的方式。在不同的硬件体系平台上实现这种机制也是不同的。但是i386SPARC 在大部分上是相同的。

 

动态连接器(rtld)身兼多职,提供符号的动态连接装载共享object文件解析符号引用。通常/lib/ld-linux.so,rtld文件本身可以是一个共享也可以是个可执行文件。

 

(# elf文件3种类型:可执行文件:*.exe;可共享文件(又叫共享库):*.so;可重定位文件:*.o)

 

★★ 符号表(symbol table)

每个共享库要想对其他elf文件可用,就要用到符号表(symbol table)中的symbol entry。事实上,一个symbol entry 是个symbol结构,它描述了这个symbol的名字和该symbolvalue

 

(# 符号表symbol table的重要作用之一是为了在多个elf文件之间共享符号,object A 把它的符号写在它自己的符号表里,并export出来,则object B就可以查object A的符号表,使用object A中的符号)

 

symbol name  dynamic string tableindex

symbol value 是该symbol在elf文件内的地址。当文件加载进内存时,该地址通常需要被重新定位(需要加上该object装载到内存的基地址(base load address)),从而构成该symbol在内存中的绝对地址;

 

一个符号表表项(symbol entry)有如下的格式:

typedef struct

{

elf32_Word st_name;           /* Symbol name (string tbl index) */

elf32_Addr st_value;          /* Symbol value */

elf32_Word st_size;           /* Symbol size */

unsigned char st_info;        /* Symbol type and binding */

unsigned char st_other;       /* No defined meaning, 0 */

elf32_Section st_shndx;       /* Section index */

} elf32_Sym;

 

可执行文件知道运行时刻大部分符号的地址,可执行文件内部定义的符号(局部变量)在编译+连接的时候就已经被重定位了。(外部定义的全局符号在把文件加载进内存时由动态连接器进行重定位)

 

★★ GOTglobal offset table

 

GOT是一个数组,位于elf文件的数据段(segment),在内存中要经常被修改。内容是一些对象(比如外部全局函数)的指针。动态连接器将文件加载进内存时,将对 那些在编译+连接时还没有确定下来绝对地址的符号 进行重定位地址解析,然后重新修改全局符号所对应的GOT表项。所以说GOTi386动态连接中扮演着重要的角色。

 

 

★★ PLTprocedure linkage table

PLT表位于elf文件的代码段,不可修改。每个PLT表项对应一个本文件内引用的外部函数。每个PLT项目包含了一些汇编代码片段(通常是:几句控制跳转的汇编指令)用来把控制权跳转到其他处理过程。

 

i386体系下,PLT和他的代码片段entries有如下格式:

 

PLT0:

push GOT[1] ; word of identifying information

jmp GOT[2] ; pointer to rtld function nop

... 

...

... 

PLTn:

jmp GOT[x + n] ; GOT offset of symbol address

push n ; relocation offset of symbol

jmp PLT0 ; call the rtld

 

PLTn+1:

jmp GOT[x +n +1]; GOT offset of symbol address

push n +1 ; relocation offset of symbol

jmp PLT0 ; call the rtld

 

在ELF文件执行过程中,当调用一个外部函数时,它跳转到跟该符号名字相关联的那个PLT entry 处执行。

 

(# 比如调用外部定义的全局库函数printfelf文件中记载的printf符号的地址并非它的真实地址,而是printf对应的PLT entry的地址。该PLT entry是在连接建立文件的时候由连接器计算生成的)

 

假设是跳转到PLTn处,开始执行其中包含的代码片段。执行PLTn中的第一条指令:jmp GOT[x + n] jumpGOT表里[x + n] 项目中包含的地址处执行;符号被解析之前,GOT[x + n]项目中起初存放着的是PLTn中的第二条指令:push n 的地址,于是继续执行push n,把该符号在重定位表(.rel.plt section)里所对应的重定位项目的偏移量数值n 压入到堆栈,然后执行下一条指令:jmp PLT0 把控制权传递到PLT[0]地址处。PLT[0]中包含了调用rtld的符号解析函数<_dl_runtime_resolve>的入口地址。(ELF程序加载进入内存时,<_dl_runtime_resolve>函数的地址会被提前放进程序内存映像中的GOT[2]里。)

 

接着<_dl_runtime_resolve>函数将展开堆栈,获取需要解析的符号对应的重定位表项。重定位表项、符号表和字符串表 可以获取PLTn引用的那个符号的完整信息,然后计算在进程内存映像中符号的内存绝对地址。最后,该符号在内存中的绝对地址被解析出来,并存放在PLTn所对应的GOT[x+n]中。下一次调用该符号时,与之对应的GOT entry中已经包含了该符号的正确地址。以后再调用该符号时,将直接通过GOT[x+n]传递控制权。动态连接器只是在第一次调用符号名字时进行地址解析;这种引用方式就是我们上面所说的 lazy MODE (注意:PLTnGOT[x+n]中两个n值并不相等,因为GOT[1]GOT[2]是为动态连接器rtld保留的。)

 

 

★★ 哈希表和链(hash table and chain)

 

除了符号表(symbol table),GOTglobal offset table),PLTprocedure linkage table),字符串表(string table),elf文件有时候还包含一个hash tablechain(用来使动态连接器 解析符号更加容易更加有效率)。hash tablechain 通常用来迅速判定在符号表中哪个entry可能符合 请求解析的符号名。hash table(总是伴随着chain)被作为整型数组存放。在hash表中,一半位置是留给那些buckets的,另一半是留给在chain中的元素(element)的。hash table直接反映了symbol table 的元素数目和他们的次序。

 

动态连接器结构可以保证:所有动态连接的执行 均以透明方式访问动态连接器(即应用程序调用的printf的地址,实际上是PLT表地址,应用程序并不知道PLT表项目会继续调用动态连接器里的地址解析函数)。然而,应用程序直接进行显式访问也是可以的:应用程序可以通过直接调用动态连接器(rtld)内部定义的一些函数,如:dlopen(),dlsym(),dlclose()等来完成动态连接。这些函数包含在动态连接器本身之中,为了访问这些函数,连接时需要把动态连接函数库(libdl)连接进来。该库包含了一些stub函数,连接器(ld)连接创建应用程序时,会对这些函数(dlopen,dlsym,dlclose)符号名字的引用 尝试进行解析;然而stub函数们只是简单的返回0。因为事实上这些函数(dlopen())真正的函数体并不是在libdl库里,而是包含在动态连接器文件(/lib//lib/ld-2.2.4.so)中,要想使用的话,就要加载动态连接器进内存。假如在静态连接的elf文件中调用这些函数(dlopen,dlsym,dlclose),共享的装载将会失败。

 

(# 静态连接的elf文件 文件头里没有动态连接器的相关信息,不会在自己的进程空间加载使用动态连接器,当然也不能使用其内部包含的函数)

(# 动态连接:连接器ld在连接时不能完全决定所有符号的地址,在程序运行期间使用动态连接器rtld对被调用函数符号名字进行动态解析;静态连接:连接器ld在连接时完全知道并决定所有符号的地址)

 

执行期间动态连接器必须知道的是:

hash table,

hash table元素的数目,

chain,

dynamic string table,

dynamic symbol table

 

满足了这些条件,下面算法适用任何symbol的地址计算:

1. hn = elf_hash(sym_name) % nbuckets                     # sym_name 请求解析的符号名

2. for (ndx = hash[ hn ]; ndx; ndx = chain[ ndx ]) {      # for循环遍历sym_tab的每1个表项,hash: hash table

3.     symbol = sym_tab + ndx                             # sym_tab 为动态符号表,参见后面关于sh_link的说明

4.     if (strcmp(sym_name, str_tab + symbol->st_name) == 0)

5.           return (load_addr + symbol->st_value); }     # 返回符号真正的内存地址:内存基地址+符号偏移

 

 

1行:hashhn elf_hash()的返回值,在elf规范的第4部分有定义,以hash table中元素个数取模。

2行:hn被用来做hash table的下标索引,求得hash值,找出与之匹配的符号名的chain的索引:ndx

3行:以ndx为索引,到动态符号表sym_tab中获得符号:symbol

4行:比较 获得的字符串名字(str_tab + symbol->st_name) 和 请求解析的符号名(sym_name) 是否相同。

使用这个算法,就可以简单解析任何符号了。

 

(# 4:str_tab:动态字符串表;str_tab + symbol->st_name:表示 以symbolst_name为索引到动态字符串表str_tab中查找得到symbol(符号)对应的字符串名字)

(# 5:load_addr是文件加载进内存的基地址。.exe文件里,printfsymbol->st_value = 0804833c,实际上是printf对应的PLT表项的地址。.exe文件每次加载进内存的地址是固定不变的,很多符号的内存地址 在连接时候就可以确定了,所以对于.exe文件,load_addr是不需要的,其值=0.so文件里,printfsymbol->st_value = 0,需要动态连接器进行重定位计算。.so文件每次加载进内存的地址是不固定的,因此需要Load_addr纪录其内存加载基地址。应该以.so文件进行演示可以更好的说明在内存中动态定位地址的问题。)

 

 

★★ 演示

/* test.c */

#include

int main(int argc, char *argv[])

{

printf("Hello, world\n");

return 0;

}

 

[alert7@redhat]$ gcc -o test test.c

[alert7@redhat]$ ./test

Hello, world

[alert7@redhat]$ readelf -a ./test

...

...

Relocation section '.rel.got' at offset 0x270 contains 1 entries:

  Offset    Info  Type            Symbol's Value  Symbol's Name

  0804948c  00706 R_386_GLOB_DAT        00000000  __gmon_start__          

 

Relocation section '.rel.plt' at offset 0x278 contains 4 entries:

Offset   Info  Type             Symbol's Value  Symbol's Name

0804947c 00107 R_386_JUMP_SLOT  080482d8        __register_frame_info

08049480 00207 R_386_JUMP_SLOT  080482e8        __deregister_frame_info

08049484 00307 R_386_JUMP_SLOT  080482f8        __libc_start_main

08049488 00407 R_386_JUMP_SLOT  08048308        printf

(只有R_386_JMP_SLOT的重定位类型才会出现在GOT中。)

...

...

Symbol table '.dynsym' contains 7 entries:   # .dynsym section保存着动态符号表

Num: Value  Size Type    Bind   Ot  Ndx  Name

0: 0        0    NOTYPE  LOCAL  0   UND

1: 80482d8  116  FUNC    WEAK   0   UND  __register_frame_info@GLIBC_2.0 (2)

2: 80482e8  162  FUNC    WEAK   0   UND  __deregister_frame_info@GLIBC_2.0 (2)

3: 80482f8  261  FUNC    GLOBAL 0   UND  __libc_start_main@GLIBC_2.0 (2)

4: 8048308  41   FUNC    GLOBAL 0   UND  printf@GLIBC_2.0 (2)

5: 804843c  4    OBJECT  GLOBAL 0   14   _IO_stdin_used

6: 0        0    NOTYPE  WEAK   0   UND  __gmon_start__

 

 

[alert7@redhat]$ objdump -x test

...

Dynamic Section:   # .dynamic section

NEEDED libc.so.6

INIT 0x8048298

FINI 0x804841c

HASH 0x8048128

STRTAB 0x80481c8

SYMTAB 0x8048158   # 指向 .dynsym section

STRSZ 0x70

SYMENT 0x10

DEBUG 0x0

PLTGOT 0x8049470

PLTRELSZ 0x20

PLTREL 0x11

JMPREL 0x8048278   # 指向 .rel.plt section

REL 0x8048270

RELSZ 0x8

RELENT 0x8

VERNEED 0x8048250

VERNEEDNUM 0x1

VERSYM 0x8048242

...

7 .rel.got 00000008 08048270 08048270 00000270 2**2

CONTENTS, ALLOC, LOAD, READONLY, DATA

8 .rel.plt 00000020 08048278 08048278 00000278 2**2

CONTENTS, ALLOC, LOAD, READONLY, DATA

9 .init 0000002f 08048298 08048298 00000298 2**2

CONTENTS, ALLOC, LOAD, READONLY, CODE

10 .plt 00000050 080482c8 080482c8 000002c8 2**2

CONTENTS, ALLOC, LOAD, READONLY, CODE

11 .text 000000fc 08048320 08048320 00000320 2**4

CONTENTS, ALLOC, LOAD, READONLY, CODE

12 .fini 0000001a 0804841c 0804841c 0000041c 2**2

CONTENTS, ALLOC, LOAD, READONLY, CODE

13 .rodata 00000016 08048438 08048438 00000438 2**2

CONTENTS, ALLOC, LOAD, READONLY, DATA

14 .data 0000000c 08049450 08049450 00000450 2**2

CONTENTS, ALLOC, LOAD, DATA

15 .eh_frame 00000004 0804945c 0804945c 0000045c 2**2

CONTENTS, ALLOC, LOAD, DATA

16 .ctors 00000008 08049460 08049460 00000460 2**2

CONTENTS, ALLOC, LOAD, DATA

17 .dtors 00000008 08049468 08049468 00000468 2**2

CONTENTS, ALLOC, LOAD, DATA

18 .got 00000020 08049470 08049470 00000470 2**2

CONTENTS, ALLOC, LOAD, DATA

19 .dynamic 000000a0 08049490 08049490 00000490 2**2

CONTENTS, ALLOC, LOAD, DATA

...

[alert7@redhat]$ gdb -q test

(gdb) disass main

Dump of assembler code for function main:

0x80483d0

: push %ebp

0x80483d1 : mov %esp,%ebp

0x80483d3 : push $0x8048440

0x80483d8 : call 0x8048308     # printf库函数是全局函数,在符号表里其属性为global0x8048308PLT[4]的地址,而不是printf的真实地址。

0x80483dd : add $0x4,%esp

0x80483e0 : xor %eax,%eax

0x80483e2 : jmp 0x80483e4

0x80483e4 : leave

0x80483e5 : ret

(gdb) b * 0x80483d8

Breakpoint 1 at 0x80483d8

(gdb) r

Starting program: /home/alert7/test

Breakpoint 1, 0x80483d8 in main ()

(gdb) disass 0x8048308                 // 0x8048308printf对应的PLT[4]的地址

//连接器将该地址保存在printf对应的.rel.plt重定位表项里面

0x8048308 : jmp *0x8049488

0x804830e : push $0x18

0x8048313 : jmp 0x80482c8 <_init+48>

 

(gdb) x 0x8049488              // 0x8049488GOT[6]的地址,在GOT表中printf符号对应GOT[6]

0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e   //此时,GOT[6]中存放的值是0x804830e

                                                   //PLT[4]中的push $0x18指令的地址

 

(gdb) disass 0x80482c8                      //查看PLT表的内容,0x80482c8.plt section的开始地址

PLT表的PLT[0]表项:

80482c8: ff 35 74 94 04 08 pushl 0x8049474    // pushl GOT[1] //0x8049474GOT[1]的地址

80482ce: ff 25 78 94 04 08 jmp *0x8049478     // jmp GOT[2]  //0x8049478GOT[2]的地址

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

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

PLT表的PLT[1]表项:

80482d8: ff 25 7c 94 04 08 jmp *0x804947c         // jmp GOT[3] //0x804947cGOT[3]的地址

80482de: 68 00 00 00 00 push $0x0

80482e3: e9 e0 ff ff ff jmp 80482c8 <_init+0x30>  // 0x80482c8PLT[0] 的地址

PLT表的PLT[2]表项:

80482e8: ff 25 80 94 04 08 jmp *0x8049480         // jmp GOT[4] //0x8049480GOT[4]的地址

80482ee: 68 08 00 00 00 push $0x8

80482f3: e9 d0 ff ff ff jmp 80482c8 <_init+0x30>  // 0x80482c8PLT[0] 的地址

PLT表的PLT[3]表项:

80482f8: ff 25 84 94 04 08 jmp *0x8049484         // jmp GOT[5] //0x8049484GOT[5]的地址

80482fe: 68 10 00 00 00 push $0x10

8048303: e9 c0 ff ff ff jmp 80482c8 <_init+0x30>  // 0x80482c8PLT[0] 的地址

PLT表的PLT[4]表项:

8048308: ff 25 88 94 04 08 jmp *0x8049488         // jmp GOT[6] //0x8049488GOT[6]的地址

804830e: 68 18 00 00 00 push $0x18                // $0x18 printf符号在.rel.plt对应的重定位表项的偏移量

8048313: e9 b0 ff ff ff jmp 80482c8 <_init+0x30>  // 0x80482c8PLT[0] 的地址

 

(gdb) b * 0x80482c8         //在即将跳到PLT[0]之前设置断点;

Breakpoint 2 at 0x80482c8

(gdb) c

Continuing.

Breakpoint 2, 0x80482c8 in _init ()

(gdb) x/8x 0x8049470        //查看GOT表前2项的内容,0x8049470.got section的开始地址

0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550

0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x0804830e 0x00000000

 

GOT表是一个简单数组,存放各种对象(外部全局函数)的地址。

GOT[0]= 0x08049490 是动态连接数组_dynamic[]地址(.dynamic section的起始地址)

GOT[1]= 0x40013ed0 是一个link_map类型的指针;

GOT[2]= 0x4000a960 是动态连接器的地址解析函数<_dl_runtime_resolve>的入口地址;

我们可以看到:在第1次调用printf之前,printf符号对应的GOT[6] = 0x0804830e,是PLT[4]: push $0x18指令的地址。

 

(gdb) x/50x 0x40013ed0     //继续察看GOT[1]的详细内容

0x40013ed0: 0x00000000 0x40010c27 0x08049490 0x400143e0

0x40013ee0: 0x00000000 0x40014100 0x00000000 0x08049490

0x40013ef0: 0x080494e0 0x080494d8 0x080494a8 0x080494b0

0x40013f00: 0x080494b8 0x00000000 0x00000000 0x00000000

0x40013f10: 0x080494c0 0x080494c8 0x08049498 0x080494a0

0x40013f20: 0x00000000 0x00000000 0x00000000 0x080494f8

0x40013f30: 0x08049500 0x08049508 0x080494e8 0x080494d0

0x40013f40: 0x00000000 0x080494f0 0x00000000 0x00000000

0x40013f50: 0x00000000 0x00000000 0x00000000 0x00000000

0x40013f60: 0x00000000 0x00000000 0x00000000 0x00000000

 

 

GOT[1]是一个鉴别信息,是link_map类型的一个指针;

/usr/include/link.h  link_map定义如下:

struct link_map

  {

    /* These first few members are part of the protocol with the debugger.

       This is the same format used in SVR4.  */

 

    ElfW(Addr) l_addr;          /* Base address shared object is loaded at.  */

    char *l_name;               /* Absolute file name object was found in.  */

    ElfW(Dyn) *l_ld;            /* Dynamic section of the shared object.  */

    struct link_map *l_next, *l_prev;   /* Chain of loaded objects.  */

  };

 

我们可以看到:l_ld = 0x08049490 .dynamic section 的首地址

 

 

(gdb) disass 0x4000a960       //继续察看GOT[2]的详细内容

Dump of assembler code for function _dl_runtime_resolve:

0x4000a960 <_dl_runtime_resolve>: push %eax

0x4000a961 <_dl_runtime_resolve+1>: push %ecx

0x4000a962 <_dl_runtime_resolve+2>: push %edx

0x4000a963 <_dl_runtime_resolve+3>: mov 0x10(%esp,1),%edx     //参数10x10(%esp,1)就是在PLT[4]push0x18

0x4000a967 <_dl_runtime_resolve+7>: mov 0xc(%esp,1),%eax      //参数20xc(%esp,1)就是在PLT[0]pushlGOT[1]

0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740   //调用真正的符号解析函数fixup(),解析出printf

//的真实内存地址,然后保存在GOT[6]里面

0x4000a970 <_dl_runtime_resolve+16>: pop %edx

0x4000a971 <_dl_runtime_resolve+17>: pop %ecx

0x4000a972 <_dl_runtime_resolve+18>: xchg %eax,(%esp,1)

0x4000a975 <_dl_runtime_resolve+21>: ret $0x8         //跳到printf函数真实地址处执行

0x4000a978 <_dl_runtime_resolve+24>: nop

0x4000a979 <_dl_runtime_resolve+25>: lea 0x0(%esi,1),%esi

End of assembler dump.

(gdb) x 0x8049488            //call 0x4000a740 执行之前,我们看到GOT[6]中的值还没有被改变

0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e

(gdb) i reg $eax $esp

(gdb) b * 0x4000a972         //call 0x4000a740 执行完毕之后设置断点

Breakpoint 4 at 0x4000a972: file dl-runtime.c, line 182.

(gdb) c

Continuing.

Breakpoint 4, 0x4000a972 in _dl_runtime_resolve () at dl-runtime.c:182

182 in dl-runtime.c

(gdb) i reg $eax $esp        //此时,call 0x4000a740 执行完毕,$eax中放着fixup()的返回值0x4006804c

eax 0x4006804c 1074167884

esp 0xbffffb64 -1073743004

(gdb) disass 0x4006804c      //0x4006804c printf函数的真实内存地址

Dump of assembler code for function printf:

0x4006804c :    push   %ebp

0x4006804d :  mov    %esp,%ebp

(gdb) x 0x8049488            //此时,我们再次查看GOT[6]中的值,已被改变为printf函数的真实内存地址

0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x4006804c

 

(gdb) si       //单步执行: 0x4000a972  xchg %eax,(%esp,1)  0x4006804c压入堆栈

(gdb) i reg $eax $esp $eip

(gdb) si       //单步执行: 0x4000a975  ret $0x8   ret返回时刚好跳到0x4006804c处执行,也就是执行printf

(gdb) i reg $eax $esp $eip

 

(gdb) disass   

Dump of assembler code for function printf:

0x4006804c : push %ebp

0x4006804d : mov %esp,%ebp

0x4006804f : push %ebx

0x40068050 : call 0x40068055

0x40068055 : pop %ebx

0x40068056 : add $0xa2197,%ebx

0x4006805c : lea 0xc(%ebp),%eax

0x4006805f : push %eax

0x40068060 : pushl 0x8(%ebp)

0x40068063 : mov 0x81c(%ebx),%eax

0x40068069 : pushl (%eax)

0x4006806b : call 0x400325b4

0x40068070 : mov 0xfffffffc(%ebp),%ebx

0x40068073 : leave

0x40068074 : ret

End of assembler dump.

(gdb) x/8x 0x8049470

0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550

0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x4006804c 0x00000000

//可以看到GOT[6]最终被修正为printf的真实地址:0x4006804c

 

 

第一次调用printf()的时候需要经过->->->

以后调用printf()的时候就不需要这么复杂了,只要经过->就可以了

 

===============================================================================

阅读(1410) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~