Chinaunix首页 | 论坛 | 博客
  • 博客访问: 274544
  • 博文数量: 150
  • 博客积分: 2396
  • 博客等级: 大尉
  • 技术积分: 1536
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-19 09:55
文章分类

全部博文(150)

文章存档

2021年(1)

2015年(9)

2014年(7)

2013年(50)

2012年(33)

2011年(1)

2010年(13)

2009年(36)

我的朋友

分类: LINUX

2009-05-24 23:48:18

这篇文章以一个具体的例子讲述了动态连接的可执行程序里的函数的symbol resolving的过程是怎样的。最好对动态连接的概念先有点基本的认识。有一些基础的概念我没有讲,可以看看后面参考资料里的书和文章。
动态链接的应用程序或共享库中,ELF的程序头描述表具有一个PT_DYNAMIC类型的描述符,它指出了.dynamic段的位置,dynamic段用来描述动态链接过程。当应用程序调用的共享库函数时,要通过.plt段进行跳转。plt段又称为过程连接表,它是连接器ld所生成的一组静态的 trampline,是只读的可执行的段,包含在.text段一起映射到内存。plt每16个字节为一个槽位,plt的第1个槽位保留给动态解析器使用,其余的槽位表示对不同共享库函数的调用。plt依赖于全局偏移量表(.got段),GOT表是一可写的数据段,包含在.data段中一起映射到内存,用来存放共享符号的绝对地址。应用程序调用共享库函数就是通过plt槽位上的一条jmp指令跳转到GOT表所指的一个共享函数指针。这样,共享库的重定位就化为对GOT表项的重定位。GOT表的第1个指针指向.dynamic段,第2、3个指针与plt段的第1个槽位对应,用来安装动态解析器。为了少做无用功,Linux采用了动态解析技术,就是说在加载共享库时,并不进行函数的解析,而是安装动态解析器,让共享库调用指向解析器,只有当函数调用发生时才进行解析。为此,在ld在生成可执行程序时,让其GOT共享函数指针指向各自plt槽位上的两条指令,一条是pushl指令,将该函数所对应GOT重定位表的索引作为参数压入堆栈,然后通过另一条jmp指令跳转到plt槽位1,它再跳转到GOT表第3个指针所表示的动态解析器入口。这样当发生并成功解析目标函数在共享库中的地址时,该函数在程序GOT表中的指针就被实际的地址刷新。

 

每一个外部定义的符号在全局偏移表(Global Offset Table GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目。对外部定义函数解析可能是整个ELF文件规范中最复杂的,下面是函数符号解析过程的一个描述。

1:代码中调用外部函数func,语句形式为call 0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。

2:PLT表的形式如下


          .PLT0: pushl   4(%ebx)    /* GOT表的地址保存在寄存器ebx中 */
jmp     *8(%ebx)
          nop; nop
          nop; nop
   .PLT1: jmp    
)
          pushl   $offset
          jmp    

   .PLT2: jmp     )
          pushl   $offset
          jmp    

         

3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。

4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl $offset,其中$offset是符号func的重定位偏移量。注意到这是一个二次跳转。

5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目,把GOT[1]的内容压栈,并跳转到GOT[2]对应的地址。

6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。

7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。

动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其最终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。




一个简单的小程序:hello.c
代码:
#include int main(){ puts("hello"); }
编译:
代码:
$ gcc hello.c -o hello -mpreferred-stack-boundary=2
反汇编plt段:
代码:
$ objdump -d -j .plt hello hello: file format elf32-i386 Disassembly of section .plt: 08048288 : 8048288: ff 35 94 95 04 08 pushl 0x8049594 804828e: ff 25 98 95 04 08 jmp *0x8049598 8048294: 00 00 add %al,(%eax) ... 08048298 : 8048298: ff 25 9c 95 04 08 jmp *0x804959c 804829e: 68 00 00 00 00 push $0x0 80482a3: e9 e0 ff ff ff jmp 8048288 <_init+0x18> ...
gdb调试hello:
代码:
$ gdb -q hello Using host libthread_db library "/lib/libthread_db.so.1". (gdb) disass main Dump of assembler code for function main: 0x08048384 : push %ebp 0x08048385 : mov %esp,%ebp 0x08048387 : sub $0x4,%esp 0x0804838a : movl $0x80484a4,(%esp) 0x08048391 : call 0x8048298 <_init+40> 0x08048396 : leave 0x08048397 : ret
注意看一下, call 0x08048298,这个地址就是puts@plt
然后下面就是jmp *0x804959c,间接跳转,跳到0x0804959c上放置的地址去执行,0x0804959c就是GOT(global offset table)的第4项
而在puts没有被solve之前那个位置上放置的地址就是jmp *0x804959c的下一句的地址,就是0x0804829e
所以接着运行,pushl $0x0,将$0x0推入堆栈。这个是fixup()要用的一个参数。这个0是相对于JMPREL的一个offset。后面有解释。
跳到plt的开始,jmp 8048288
然后把GOT的第二项推入堆栈,pushl 0x80495a4,这个是一个struct link_map的指针,靠着它,进程地址空间里所有的so将被连成一个链表。这个也是fixup()要用的参数。
代码:
struct link_map { ElfW(Addr) l_addr; char *l_name; ElfW(Dyn) *l_ld; struct link_map *l_next, *l_prev; };
然后跳到GOT第三项保存的地址上去执行,这个地址就是符号解析函数的入口。在glibc源代码的这个文件sysdeps/i386/dl- machine.h有定义,形式是一个macro,名字叫ELF_MACHINE_RUNTIME_TRAMPOLINE。在这个函数里,会调用 fixup(),这个才是真正做事情的函数。fixup会通过那个struct link_map指针遍历所有的so来找puts。是通过so里的hash table来找的,所以速度会比较快。找到以后通过JMPREL+0这个地址来保存。JMPREL里放置的是和PLT相关的relocation entry
代码:
typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
JMPREL+0就是里面第一个entry
r_offset的值就是GOT里面我们要改写的项,对于本例的puts来说就是第四项,就是 0x0804959c。来看一下
得到JMPREL的地址:
代码:
$ readelf -d hello Dynamic section at offset 0x4c4 contains 20 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000c (INIT) 0x8048270 0x0000000d (FINI) 0x8048480 0x00000004 (HASH) 0x8048168 0x00000005 (STRTAB) 0x80481e0 0x00000006 (SYMTAB) 0x8048190 0x0000000a (STRSZ) 74 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x8049590 0x00000002 (PLTRELSZ) 16 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x804825c
回到gdb(所以你最好开两个terminal):
代码:
(gdb) x/2x 0x0804825c 0x804825c: 0x0804959c 0x00000107
0x0804959c,是不是和前面反汇编puts@plt的第一句间接跳转的地址一样?:-D
fixup在解析完之后就会把puts的真实地址放到0x0804959c上去
以后就不必在解析了

参考资料:

1.glibc 2.3.5 src
2.<>
3.<>
4.<> write by the grugq
5.<> by alert7
阅读(1214) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~