Linux动态链接之GOT与PLT

来源:互联网 发布:php 排列组合算法 编辑:程序博客网 时间:2024/05/17 08:51

我们知道函数名就是一个内存地址,这个地址指向函数的入口。调用函数就是压入参数,保存返回地址,然后跳转到函数名指向的代码。问题是,如果函数在共享库中,共享库加载的地址本身就不确定,函数地址也就不确定了,那如何调用共享库中的函数呢?这就是本文要回答的。

我们先来看一小段代码(test.c):

复制代码
#include <stdio.h>void hello_world(void){    printf("Hello world!\n");    return;}int main(int argc, char* argv[]){    hello_world();    return 0;}
复制代码

编译并反汇编:

gcc -g test.c -o test
objdump -S test

 

复制代码
void hello_world(void){ 80483b4:       55                      push   %ebp 80483b5:       89 e5                   mov    %esp,%ebp 80483b7:       83 ec 08                sub    $0x8,%esp        printf("Hello world!\n"); 80483ba:       c7 04 24 b4 84 04 08    movl   $0x80484b4,(%esp) 80483c1:       e8 2a ff ff ff          call   80482f0 <puts@plt>        return;} 80483c6:       c9                      leave 80483c7:       c3                      ret080483c8 <main>:int main(int argc, char* argv[]){ 80483c8:       8d 4c 24 04             lea    0x4(%esp),%ecx 80483cc:       83 e4 f0                and    $0xfffffff0,%esp 80483cf:       ff 71 fc                pushl  -0x4(%ecx) 80483d2:       55                      push   %ebp 80483d3:       89 e5                   mov    %esp,%ebp 80483d5:       51                      push   %ecx 80483d6:       83 ec 04                sub    $0x4,%esp        hello_world(); 80483d9:       e8 d6 ff ff ff          call   80483b4 <hello_world>        return 0; 80483de:       b8 00 00 00 00          mov    $0x0,%eax}
复制代码

 

调用hello_world时,汇编代码对应于call   80483b4 <hello_world>,这是个绝对地址。hello_world是在可执行文件中,可执行文件是加载到一个固定地址的,因此hello_world的地址是确定的。

调用printf时,汇编代码对应于call   80482f0 <puts@plt>,这是个绝对地址。但函数名却是puts@plt,这是怎么回事呢?puts@plt显然是编译器加的一个中间函数,我们看一下这个函数对应的汇编代码:

080482f0 <puts@plt>: 80482f0:   ff 25 2c 96 04 08       jmp    *0x804962c 80482f6:   68 10 00 00 00          push   $0x10 80482fb:   e9 c0 ff ff ff          jmp     <_init+0x30>

现在我们用调试器分析一下:

gdb test

(gdb) b main
Breakpoint 1 at 0×80483d9: file test.c, line12.
(gdb) r
Starting program: /root/test/plt/test

Breakpoint 1, main () at test.c:1212 hello_world();

puts@plt先跳到*0×804962c,我们看看*0×804962c里有什么?
(gdb) x 0×804962c 0×804962c <_GLOBAL_OFFSET_TABLE_+20>:  0×080482f6

*0×804962c等于0×080482f6,这正是puts@plt中的第二行汇编代码的地址。也就是说puts@plt整个函数会顺序执行,直到跳转到0×80482c0.

再来看看0×80482c0处有什么,通过汇编可以看到:
ff 25 20 96 0408 jmp    *0×8049620

又跳到了*0×8049620,转的弯真多,没关系,我们再看*0×8049620
(gdb) x 0×8049620 0×8049620 <_GLOBAL_OFFSET_TABLE_+8>:   0×009ce4c0
(gdb) x /wa 0×009ce4c0 0×9ce4c0 <_dl_runtime_resolve>: 0×8b525150

原来转来转去就是为了调用函数_dl_runtime_resolve, _dl_runtime_resolve的功能就是找到要调用函数(puts)的地址。

为什么不直接调用_dl_runtime_resolve,而要转这么多圈子呢?

先执行完这个函数hello_world:
(gdb) n

再回头来看看puts@plt的第一行代码:

80482f0:   ff 25 2c96 0408 jmp    *0×804962c

(gdb) x 0×804962c 0×804962c <_GLOBAL_OFFSET_TABLE_+20>:  0xa39a60 <puts>
对比前面的:
(gdb) x 0×804962c 0×804962c <_GLOBAL_OFFSET_TABLE_+20>:  0×080482f6

也就是说第一次执行时,通过_dl_runtime_resolve解析到函数地址,并保存puts的地址到0×804962c里,以后执行时就直接调用了。

 

转自:http://apps.hi.baidu.com/share/detail/24654313

--------------------------------------------

 

         /*如果是第一次的函数调用,它所走的路线就是我在上图中用红线标出的,而要是在第二次以后调用,那就是蓝线所标明的。*/

 

最后我们讨论ELF文件的动态连接机制。每一个外部定义的符号在全局偏移表 (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            *name1@GOT(%ebx)
pushl          $offset
jmp            .PLT0@PC
.PLT2: jmp            *func@GOT(%ebx)
pushl          $offset
jmp            .PLT0@PC

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

4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl offset

offset是符号func的重定位偏移量。注意到这是一个二次跳转。

5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目(.PLT0),把GOT[1]的内容(放置了用来标识特定库的代码)压栈,并跳转到GOT[2]对应的地址。

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

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

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


全局偏移表(GOT)和过程链接表(PLT)

 

ELF 格式的共享库使用 PIC 技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC 在代码中的跳转和分支指令不使用绝对地址。PIC 在 ELF 可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表 GOT

对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址;对于本模块内的静态变量和静态函数,用 GOT 表的首地址作为一个基准,用相对于该基准的偏移量来引用,因为不论程序被加载到何种地址空间,模块内的静态变量和静态函数与 GOT 的距离是固定的,并且在链接阶段就可知晓其距离的大小。这样,PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。

对于 PIC 代码,代码段内不存在重定位项,实际的重定位项只是在数据段的 GOT 表内。共享目标文件中的重定位类型有 R_386_RELATIVE、R_386_GLOB_DAT 和 R_386_JMP_SLOT,用于在动态链接器加载映射共享库或者模块运行的时候对指针类型的静态数据、全局变量符号地址和全局函数符号地址进行重定 位。

1.2 PLT 表
过程链接表用于把位置独立的函数调用重定向到绝对位置。通过 PLT 动态链接的程序支持惰性绑定模式。每个动态链接的程序和共享库都有一个 PLT,PLT 表的每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。程序对某个函数的访问都被调整为对 PLT 入口的访问。

每个 PLT 入口项对应一个 GOT 项,执行函数实际上就是跳转到相应 GOT 项存储的地址,该 GOT 项初始值为 PLTn项中的 push 指令地址(即 jmp 的下一条指令,所以第 1 次跳转没有任何作用),待符号解析完成后存放符号的真正地址。动态链接器在装载映射共享库时在 GOT 里设置 2 个特殊值:在 GOT+4( 即 GOT[1]) 设置动态库映射信息数据结构link_map 地址;在 GOT+8(即 GOT[2])设置动态链接器符号解
析函数的地址_dl_runtime_resolve。

PLT 的第 1 个入口 PLT0 是一段访问动态链接器的特殊代码。程序对 PLT 入口的第 1 次访问都转到了 PLT0,最后跳入 GOT[2]存储的地址执行符号解析函数。待完成符号解析后,将符号的实际地址存入相应的 GOT 项,这样以后调用函数时可直接跳到实际的函数地址,不必再执行符号解析函数

操作系统运行程序时,首先将解释器程序即动态链接器ld.so 映射到一个合适的地址,然后启动 ld.so。ld.so 先完成自己的初始化工作,再从可执行文件的动态库依赖表中指定的路径名查找所需要的库,将其加载映射到内存。

Linux用一个全局的库映射信息结构 struct link_map链表来管理和控制所有动态库的加载,动态库的加载过程实际上是映射库文件到内存中,并填充库映射信息结构添加到链表中的过程。结构 struct link_map 描述共享目标文件的加载映射信息,是动态链接器在运行时内部使用的一个结构,通过它保持对已装载的库和库中符号的跟踪。
link_map 使用双向链接中间件“l_next”和“l_prev”链接进程中所有加载的共享库。当动态链接器需要去查找符号的时候,可以向前或向后遍历这个链表,通 过访问链表上的每一个库去搜索需要查找的符号。Link_map 链表的入口由每个可执行映像的全局偏移表的第 2 个入口(GOT[1])指向,查找符号时先从 GOT[1]读取 link_map 结点地址,然后沿着link-map 结点进行搜索。

动态库的加载映射过程主要分 3 步:
(1) 动态链接器调用 __mmap 函数对动态库的所有PT_LOAD 可加载段进行整体映射:

l_map_start=(ElfW(Addr))__mmap ((void *)0, maplength, prot,
MAP_COPY | MAP_FILE, fd, mapoff);

返回值 l_map_start 是实际映射的虚拟地址,和段结构成员 p_vaddr 指定的虚拟地址不一定相同,这对于位置无关代码不会产生影响。但是对于数据段和 link_map 结构中其它相关的位置描述信息还要进行修正。共享库映射的内存位置关系如图 1,l_addr 是实际映射地址和原来指定的映射地址的差值,用于其它位置信息的修正,即简单地将原来指定的虚拟地址加上 l_addr 就可以得到实际加载的虚拟地址

(2)共享文件映射完毕,动态链接器处理共享库的PT_DYNAMIC 动态段,将各项动态链接信息主要是哈希表、符号表、字符串表、重定位表、PLT 重定位项表等地址填写到 link_map 的 l_info 数组结构中。l_info 是 link_map 最重要的字段之一,几乎所有与动态链接管理相关的内容都与 l_info数组有关。动态链接器还要加载处理当前共享库的所有依赖库。

(3)由于实际的映射地址和指定的虚拟地址有可能不同,因此还要对动态库及其依赖库进行重定位。设置动态库的第1 个和第 2 个 GOT 表项:

Elf32_Addr *got =
(Elf32_Addr *) lmap->l_info[DT_PLTGOT].d_un.d_ptr;
got[1]=lmap;
got[2]=&_dl_runtime_resolve;

对动态库的所有重定位项进行重定位,在重定位项指定的偏移地址处加上修正值 l_addr。动态项 DT_REL 给出了重定位表的地址,DT_RELSZ 给出重定位表项的数目。
映射完毕后,动态链接器调用共享库(包括所有相关的依赖库)自备的初始化函数进行初始化。

程序连接表(Procedure Linkage Table)可以使被感染的文件调用外部的函数。这要比修改LD_PRELOAD环境变量实现调用的重定向优越的多,首先不牵扯到环境变量的修改

程序连接表(PLT)

  在ELF文件中,全局偏移表(Global Offset Table,GOT)能够把位置无关的地址定位到绝对地址,程序连接表也有类似的作用,它能够把位置无关的函数调用定向到绝对地址。连接编辑器(link editor)不能解决程序从一个可执行文件或者共享库目标到另外一个的执行转移。结果,连接编辑器只能把包含程序转移控制的一些入口安排到程序连接表 (PLT)中。在system V体系中,程序连接表位于共享正文中,但是它们使用私有全局偏移表(private global offset table)中的地址。动态连接器(例如:ld-2.2.2.so)会决定目标的绝对地址并且修改全局偏移表在内存中的影象。因而,动态连接器能够重定向 这些入口,而勿需破坏程序正文的位置无关性和共享特性。可执行文件和共享目标文件有各自的程序连接表。

elf的动态连接库是内存位置无关的,就是说你可以把这个库加载到内存的任何位置都没有影响。这就叫做position independent。在编译内存位置无关的动态连接库时,要给编译器加上 -fpic选项,让编译器产生的目标文件是内存位置无关的还会尽量减少对变量引用时使用绝对地址。把库编译成内存位置无关会带来一些花费,编译器会保留一 个寄存器来指向全局偏移量表(global offset table (or GOT for short)),这就会导致编译器在优化代码时少了一个寄存器可以使用,但是在最坏的情况下这种性能的减少只有3%,在其他情况下是大大小于3%的。


0 0
原创粉丝点击