ELF动态解析符号过程
时间:2009-04-23 来源:lxcrist
导读:
本篇文章以linux为平台为例,演示ELF动态解析符号的过程。
不正之处,还请斧正。
通常,ELF解析符号方式称为lazy MODE装载的。这种装载技术是ELF平台上
默认的方式。在不同的体系平台在实现这种机制也是不同的。但是i386和SPARC
在大部分上是相同的。
动态连接器(rtld)提供符号的动态连接,装载共享objects和解析标号的引用。
通常是ld.so,它可以是一个共享object也可以是个可执行的文件。
★★ 符号表(symbol table)
每个object要想使它对其他的ELF文件可用,就要用到符号表(symbol table)中
symbol entry.事实上,一个symbol entry 是个symbol结构,它描述了这个
symbol的名字和该symbol的value.symbol name被编码作为dynamic string
table的索引(index). The value of a symbol是在ELF OBJECT文件内该
symbol的地址。该地址通常需要被重新定位(加上该object装载到内存的基地址
(base load address)). 从而构成该symbol在内存中的绝对地址。
一个符号表入口有如下的格式:
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;
可执行文件他们知道运行时刻他们的地址,所以他们内部的引用符号在编译时候就已
经被重定位了。
★★ GOT(global offset table)
GOT是一个数组,存在ELF image的数据段中,他们是一些指向objects的指针(通常
是数据objects).动态连接器将重新修改那些编译时还没有确定下来地址的符号的
GOT入口。所以说GOT在i386动态连接中扮演着重要的角色。
★★ PLT(procedure linkage table)
PLT是一个这样的结构,它的entries包含了一些代码片段用来传输控制到外部的过程。
在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
当传输控制到一个外部的函数时,它传输执行到PLT 中跟该symbol相关的那个entry
(是在编译时候连接器安装的)。在PLT entry中第一条指令将jump到一个存储在GOT
中的一个指针地址;假如符号还没有被解析,该GOT中存放着的是该PLT entry中的
下一条指令地址。该指令push一个在重定位表中的偏移量到stack,然后下一条指令
传输控制到PLT[0]入口。该PLT[0]包含了调用RTLD解析符号的函数代码。该
解析符号函数地址由程序装载器已经插入到GOT[2]中了。
动态连接器将展开stack并且获取需要解析符号在重定位表地址信息。重定位入口、
符号表和字符串表共同决定着PLT entry引用的那个符号和在进程内存中符号应该
存放的地址。假如可能的话,该符号将被解析出来,它的地址将被存放在被该
PLT entry使用的GOT entry中。下一次该符号被请求时,与之对应的GOT已经包
含了该符号的地址了。所以,所有后来的调用将直接通过GOT传输控制。动态连接器
只解析第一次被二进制文件所引用的符号;这种引用方式就是我们上面所说的
lazy MODE。
★★ 哈希表和链(hash table and chain)
除了符号表(symbol table),GOT(global offset table),PLT(procedure
linkage table),字符串表(string table),ELF objects还可以包含一个
hash table和chain(用来使动态连接器解析符号更加容易)。hash table和chain
通常被用来迅速判定在符号表中哪个entry可能符合所请求的符号名。hash table(总
是伴随着chain的)被作为整型数组存放。在hash表中,一半位置是留给那些buckets的,
另一半是留给在chain中的元素(element)的. hash table直接反映了symbol table
的元素数目和他们的次序。
动态连接器结构提供了所有动态连接的执行是以透明方式访问动态连接器.
然而,明确访问也是可用的。动态连接(装载共享objects和解析符号),
可以通过直接访问RTLD的那些函数来完成:dlopen() , dlsym() and
dlclose() .这些函数被包含在动态连接器本身中。为了访问那些函数,
连接时需要把动态连接函数库(libdl)连接进去。该库包含了一些stub函数
允许编译时候连接器解析那些函数的引用;然而那些stub函数只简单的返回0。
因为事实上函数驻留在动态连接器中,假如从静态连接的ELF文件中调用
那些函数,共享object的装载将会失败。
对于执行动态连接器所必须的是:hash table,hash table元素的数目,
chain,dynamic string table和dynamic symbol talbe。满足了
这些条件,下面算法适用任何symbol的地址计算:
1. hn = elf_hash(sym_name) % nbuckets;
2. for (ndx = hash[ hn ]; ndx; ndx = chain[ ndx ]) {
3. symbol = sym_tab + ndx;
4. if (strcmp(sym_name, str_tab + symbol->st_name) == 0)
5. return (load_addr + symbol->st_value); }
hash号是elf_hash()的返回值,在ELF规范的第4部分有定义,以hash table中元素
个数取模。该号被用来做hash table的下表索引,求得hash值,找出与之匹配的符号
名的chain的索引(line 3)。使用该索引,符号从符号表中获得(line 3).比较获得
的符号名和请求的符号名是否相同(line 5).使用这个算法,就可以简单解析任何符号了。
★★ 演示
#include
int main(int argc, char *argv[])
{
printf("Hello, world ");
return 0;
}
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_JUMP_SLOT的才会出现在GOT中
Symbol table '.dynsym' contains 7 entries:
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]$ gcc -o test test.c
[alert7@redhat]$ ./test
Hello, world
[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
0x80483dd : add $0x4,%esp
0x80483e0 : xor %eax,%eax
0x80483e2 : jmp 0x80483e4
0x80483e4 : leave
0x80483e5 : ret
...
0x80483ef : nop
End of assembler dump.
(gdb) b * 0x80483d8
Breakpoint 1 at 0x80483d8
(gdb) r
Starting program: /home/alert7/test
Breakpoint 1, 0x80483d8 in main ()
(gdb) disass 0x8048308 ① ⑴
Dump of assembler code for function printf:
/****************************************/ //PLT4:
0x8048308 : jmp *0x8049488 //jmp GOT[6]
//此时,GOT[6]中存在的是0x804830e
0x804830e : push $0x18 //$0x18为printf入口在GOT的偏移量
0x8048313 : jmp 0x80482c8 <_init+48> //jmp PLT0
//PLT0处存放着调用RTLD函数的指令
//当函数返回时候,把GOT[6]修改为真正的
//printf函数地址,然后直接跳到printf函数
//执行。
该部分为PLT的一部分
/****************************************/
End of assembler dump.
(gdb) x 0x8049488
0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e
080482c8 <.plt>: ② //PLT0:
80482c8: ff 35 74 94 04 08 pushl 0x8049474 //pushl GOT[1]地址
//GOT[1]是一个鉴别信息,是link_map类型的一个指针
80482ce: ff 25 78 94 04 08 jmp *0x8049478 //JMP GOT[2]
//跳到动态连接器解析函数执行
80482d4: 00 00 add %al,(%eax)
80482d6: 00 00 add %al,(%eax)
80482d8: ff 25 7c 94 04 08 jmp *0x804947c //PLT1:
80482de: 68 00 00 00 00 push $0x0
80482e3: e9 e0 ff ff ff jmp 80482c8 <_init+0x30>
80482e8: ff 25 80 94 04 08 jmp *0x8049480 //PLT2:
80482ee: 68 08 00 00 00 push $0x8
80482f3: e9 d0 ff ff ff jmp 80482c8 <_init+0x30>
80482f8: ff 25 84 94 04 08 jmp *0x8049484 //PLT3:
80482fe: 68 10 00 00 00 push $0x10
8048303: e9 c0 ff ff ff jmp 80482c8 <_init+0x30>
8048308: ff 25 88 94 04 08 jmp *0x8049488 //PLT4:
804830e: 68 18 00 00 00 push $0x18
8048313: e9 b0 ff ff ff jmp 80482c8 <_init+0x30>
(gdb) b * 0x80482c8
Breakpoint 2 at 0x80482c8
(gdb) c
Continuing.
Breakpoint 2, 0x80482c8 in _init ()
(gdb) x/8x 0x8049470
0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550
0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x0804830e 0x00000000
(gdb) x/50x 0x40013ed0 ( * link_map类型)
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
(gdb) disass 0x4000a960 ③
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
0x4000a967 <_dl_runtime_resolve+7>: mov 0xc(%esp,1),%eax
0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740
//调用真正的解析函数fixup(),修正GOT[6],使它指向真正的printf函数地址
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) b * 0x4000a972
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
eax 0x4006804c 1074167884
esp 0xbffffb64 -1073743004
(gdb) b *0x4000a975
Breakpoint 5 at 0x4000a975: file dl-runtime.c, line 182.
(gdb) c
Continuing.
Breakpoint 5, 0x4000a975 in _dl_runtime_resolve () at dl-runtime.c:182
182 in dl-runtime.c
(gdb) si
printf (format=0x1