聊聊Linux动态链接中的PLT和GOT(4)—— 穿针引线

版权声明:本文为博主原创文章,承蒙转载请注明作者和出处 https://blog.csdn.net/linyt/article/details/51893258

编译时的PLT和GOT关系图

前几篇文章一直在讨论PLT和GOT的结构细节,编译完成之后,PLT和GOT的对应关系是怎么样的呢,下面是编译完成之后,PLT和GOT关系图。

编译时PLT和GOT关系图

图中重点标注了从调用printf函数语句的汇编指令call puts@plt跳转过程,图中使用编号来表标跳转顺序。

PLT表结构有以下特点:

  1. PLT表中的第一项为公共表项,剩下的是每个动态库函数为一项(当然每项是由多条指令组成的,jmp *0xXXXXXXXX这条指令是所有plt的开始指令)
  2. 每项PLT都从对应的GOT表项中读取目标函数地址

GOT表结构有以下特点:

  1. GOT表中前3个为特殊项,分别用于保存 .dynamic段地址、本镜像的link_map数据结构地址和_dl_runtime_resolve函数地址;但在编译时,无法获取知道link_map地址和_dl_runtime_resolve函数地址,所以编译时填零地址,进程启动时由动态链接器进行填充
  2. 3个特殊项后面依次是每个动态库函数的GOT表项

如果将PLT和GOT抽象起来描述,可以写成以下的伪代码:

plt[0]:
  pushl got[1]
  jmp  *got[2]

plt[n]:                // n >= 1
  jmp *got[n+2]        // GOT前3项为公共项,第3项开始才是函数项,plt[1]对应的GOT[3],依次类推
  push (n-1)*8
  jmp plt[0]

got[0]  = address of .dynamic section
got[1]  = address of link_map object( 编译时填充0)
got[2]  = address of _dl_runtime_resolve function (编译时填充为0)
got[n+2]  = plt[n] + 6 (即plt[n]代码片段的第二条指令)

进程起动后的GOT表

PLT属于代码段,在进程加载和运行过程都不会发生改变,PLT指向GOT表的关系在编译时已完全确定,唯一能发生变化的是GOT表。

Linux加载进程时,通过execve系统调用进入内核态,将镜像加载到内存,然后返回用户态执行。返回用户态时,它的控制权并不是交给可执行文件,而是给动态链接器去完成一些基础的功能,比如上述的GOT[1],GOT[2]的填写就是这个阶段完成的。下图是动态链接器填完GOT[1],GOT[2]后的GOT图:

进程加载完成后的GOT表

估计大家比较好奇的是,动态链接器怎么知道GOT的首地址?这个秘密就藏在ELF的.dynamic段里面,详见下面readelf -d test输出结果中的PLTGOT项:

ivan@ivan:~/test/test$ readelf -d test

Dynamic section at offset 0x600 contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8048274
 0x0000000d (FINI)                       0x8048488
 0x00000019 (INIT_ARRAY)                 0x80495f4
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x80495f8
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x00000004 (HASH)                       0x8048168
 0x00000005 (STRTAB)                     0x80481e0
 0x00000006 (SYMTAB)                     0x8048190
 0x0000000a (STRSZ)                      74 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x80496ec
 0x00000002 (PLTRELSZ)                   24 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x804825c
 0x00000011 (REL)                        0x8048254
 0x00000012 (RELSZ)                      8 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x8048234
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x804822a
 0x00000000 (NULL)                       0x0

其实.dynamic段还藏着很多其它信息,都是跟动态运行相关的信息,有兴趣的读者可以自行分析,这里不详细介绍。

动态重定位执行过程

Linux 动态链接器提供动态重位功能,所有外部函数只有调用时才做重定位,实现延迟绑定功能。下面是以调用puts函数为例画出了整个动态重定位的执行过程:

以puts函数为例,画出整个动态重定位的执行过程

在 _dl_runtime_resolve函数内完成puts符号查找后,将该函数地址地址重定位到对应的GOT表项,并调用。

重定位之后的调用

GOT表项已完成重定位的情况下,PLT利用GOT表直接调用到真实的动态库函数,下面puts函数的调用过程:

重定位后的调用过程

总结

对于PLT和GOT的原理,一共分享了以下知识点:
1. 为什么会有PLT和GOT表,它完成什么功能
2. Linux如何通过 PLT和GOT表配合,完成延迟重定位功能
3. PLT和GOT的结构是怎么样的,并且介绍每种场景下PLT的执行过程

关于PLT/GOT的基本知识写到这样就有清晰的认识了,但是Linux还有其它场景也会使用PLT/GOT,以后遇到时再展开讨论。

最后,本系列文章所有二进制分析,都是基于以下代码编译出来的可执行文件(32位)进行分析。

#include <stdio.h>

void print_banner()
{
    printf("Welcome to World of PLT and GOT\n");
}

int main(void)
{
    print_banner();

    return 0;
}

具体编译方法参考《聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT

猜你喜欢

转载自blog.csdn.net/linyt/article/details/51893258