当前位置: 首页 > news >正文

动态链接库工作原理 PLT GOT

PLT,内部函数表,表中的内容是静态链接系统添加的。
GOT,全局函数表,表中的内容:

  • 初始时:静态链接系统添加的
  • 运行时:动态链接器更新的

我们用一个非常简单的例子来讲解,代码如下:

void win() {printf("you win\n");
}int main() {char a[100];gets(a);printf("you lose\n");return 0;
}

依赖的动态库

编译后要依赖的动态库如下:

zuanmin.lzm@localhost:~/test/pltgot$ ldd ./a.outlinux-vdso.so.1 (0x00007ffcbf9c1000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f05bc8ab000)/lib64/ld-linux-x86-64.so.2 (0x00007f05bcec4000)

PLT 和 GOT 段

生成的 ELF 里面的 “段” 如下:

zuanmin.lzm@localhost:~/test/pltgot$ readelf -S ./a.out
There are 34 section headers, starting at offset 0x1d08:节头:[] 名称              类型             地址              偏移量大小              全体大小          旗标   链接   信息   对齐[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .interp           PROGBITS         0000000000000238  00000238000000000000001c  0000000000000000   A       0     0     1[ 5] .dynsym           DYNSYM           00000000000002b8  000002b800000000000000d8  0000000000000018   A       6     1     8[ 6] .dynstr           STRTAB           0000000000000390  0000039000000000000000a2  0000000000000000   A       0     0     1[ 9] .rela.dyn         RELA             0000000000000478  0000047800000000000000c0  0000000000000018   A       5     0     8[10] .rela.plt         RELA             0000000000000538  000005380000000000000048  0000000000000018  AI       5    22     8[11] .init             PROGBITS         0000000000000580  000005800000000000000017  0000000000000000  AX       0     0     4[12] .plt              PROGBITS         00000000000005a0  000005a00000000000000040  0000000000000010  AX       0     0     16[13] .plt.got          PROGBITS         00000000000005e0  000005e00000000000000008  0000000000000008  AX       0     0     8[14] .text             PROGBITS         00000000000005f0  000005f000000000000001e2  0000000000000000  AX       0     0     16[15] .fini             PROGBITS         00000000000007d4  000007d40000000000000009  0000000000000000  AX       0     0     4[16] .rodata           PROGBITS         00000000000007e0  000007e00000000000000015  0000000000000000   A       0     0     4[17] .eh_frame_hdr     PROGBITS         00000000000007f8  000007f80000000000000044  0000000000000000   A       0     0     4[18] .eh_frame         PROGBITS         0000000000000840  000008400000000000000128  0000000000000000   A       0     0     8[19] .init_array       INIT_ARRAY       0000000000200da8  00000da80000000000000008  0000000000000008  WA       0     0     8[20] .fini_array       FINI_ARRAY       0000000000200db0  00000db00000000000000008  0000000000000008  WA       0     0     8[21] .dynamic          DYNAMIC          0000000000200db8  00000db800000000000001f0  0000000000000010  WA       6     0     8[22] .got              PROGBITS         0000000000200fa8  00000fa80000000000000058  0000000000000008  WA       0     0     8[23] .data             PROGBITS         0000000000201000  000010000000000000000010  0000000000000000  WA       0     0     8[24] .bss              NOBITS           0000000000201010  000010100000000000000008  0000000000000000  WA       0     0     1[31] .symtab           SYMTAB           0000000000000000  000012e800000000000006a8  0000000000000018          32    48     8[32] .strtab           STRTAB           0000000000000000  000019900000000000000234  0000000000000000           0     0     1[33] .shstrtab         STRTAB           0000000000000000  00001bc4000000000000013e  0000000000000000           0     0     1

PLT 表

从上面的“段”看出,PLT 的地址是0x5a0,可以明显看出共有4个表项。

  • PLT[0],公共 plt 代码,会跳转到 GOT[2]
  • PLT[1],puts@plt,第一句跳转 0x200fc0,位于GOT表中
  • PLT[2],__stack_chk_fail&plt,第一句跳转 0x200fc8,位于GOT表中
  • PLT[3],gets@plt,第一句跳转 0x200fd0,位于GOT表中
gdb-peda$ x /56i 0x5a00x5a0:	push   QWORD PTR [rip+0x200a0a]        # 0x200fb00x5a6:	jmp    QWORD PTR [rip+0x200a0c]        # 0x200fb80x5ac:	nop    DWORD PTR [rax+0x0]0x5b0 <puts@plt>:	jmp    QWORD PTR [rip+0x200a0a]        # 0x200fc00x5b6 <puts@plt+6>:	push   0x00x5bb <puts@plt+11>:	jmp    0x5a00x5c0 <__stack_chk_fail@plt>:	jmp    QWORD PTR [rip+0x200a02]        # 0x200fc80x5c6 <__stack_chk_fail@plt+6>:	push   0x10x5cb <__stack_chk_fail@plt+11>:	jmp    0x5a00x5d0 <gets@plt>:	jmp    QWORD PTR [rip+0x2009fa]        # 0x200fd00x5d6 <gets@plt+6>:	push   0x20x5db <gets@plt+11>:	jmp    0x5a0

更直观的 PLT 表:

zuanmin.lzm@localhost:~/test/pltgot$ objdump -D -S ./a.outDisassembly of section .plt:00000000000005a0 <.plt>:5a0:	ff 35 0a 0a 20 00    	pushq  0x200a0a(%rip)        # 200fb0 <_GLOBAL_OFFSET_TABLE_+0x8>5a6:	ff 25 0c 0a 20 00    	jmpq   *0x200a0c(%rip)        # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x10>5ac:	0f 1f 40 00          	nopl   0x0(%rax)00000000000005b0 <puts@plt>:5b0:	ff 25 0a 0a 20 00    	jmpq   *0x200a0a(%rip)        # 200fc0 <puts@GLIBC_2.2.5>5b6:	68 00 00 00 00       	pushq  $0x05bb:	e9 e0 ff ff ff       	jmpq   5a0 <.plt>00000000000005c0 <__stack_chk_fail@plt>:5c0:	ff 25 02 0a 20 00    	jmpq   *0x200a02(%rip)        # 200fc8 <__stack_chk_fail@GLIBC_2.4>5c6:	68 01 00 00 00       	pushq  $0x15cb:	e9 d0 ff ff ff       	jmpq   5a0 <.plt>00000000000005d0 <gets@plt>:5d0:	ff 25 fa 09 20 00    	jmpq   *0x2009fa(%rip)        # 200fd0 <gets@GLIBC_2.2.5>5d6:	68 02 00 00 00       	pushq  $0x25db:	e9 c0 ff ff ff       	jmpq   5a0 <.plt>

GOT 表

从上面的“段”看出,GOT 的地址是0x200fa8,仔细看:

  • GOT[0],dynamic 段地址,fill by 静态链接器,如下 0x200db8
  • GOT[1],link_map,fill by 动态链接器
  • GOT[2],__dl_runtime_resolve,fill by 动态链接器
  • GOT[3],0x05b6,为PLT[1]的第二行语句
  • GOT[4],0x05c6,为PLT[2]的第二行语句
  • GOT[5],0x05d6,为PLT[3]的第二行语句
gdb-peda$ x /72x 0x200fa8
0x200fa8:	0xb8	0x0d	0x20	0x00	0x00	0x00	0x00	0x00
0x200fb0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x200fb8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x200fc0:	0xb6	0x05	0x00	0x00	0x00	0x00	0x00	0x00
0x200fc8:	0xc6	0x05	0x00	0x00	0x00	0x00	0x00	0x00
0x200fd0:	0xd6	0x05	0x00	0x00	0x00	0x00	0x00	0x00
0x200fd8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x200fe0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x200fe8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

使用 xx@plt

编译后通过 disas main
函数中的反编译代码如下:

000000000000070d <main>:int main() {70d:	55                   	push   %rbp70e:	48 89 e5             	mov    %rsp,%rbp711:	48 83 ec 70          	sub    $0x70,%rsp715:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax71c:	00 0071e:	48 89 45 f8          	mov    %rax,-0x8(%rbp)722:	31 c0                	xor    %eax,%eaxchar a[100];gets(a);724:	48 8d 45 90          	lea    -0x70(%rbp),%rax728:	48 89 c7             	mov    %rax,%rdi72b:	b8 00 00 00 00       	mov    $0x0,%eax730:	e8 9b fe ff ff       	callq  5d0 <gets@plt>printf("you lose\n");735:	48 8d 3d b0 00 00 00 	lea    0xb0(%rip),%rdi        # 7ec <_IO_stdin_used+0xc>73c:	e8 6f fe ff ff       	callq  5b0 <puts@plt>return 0;741:	b8 00 00 00 00       	mov    $0x0,%eax
}746:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx74a:	64 48 33 14 25 28 00 	xor    %fs:0x28,%rdx751:	00 00753:	74 05                	je     75a <main+0x4d>755:	e8 66 fe ff ff       	callq  5c0 <__stack_chk_fail@plt>75a:	c9                   	leaveq75b:	c3                   	retq75c:	0f 1f 40 00          	nopl   0x0(%rax)

我们可以观察到 gets@plt 和 puts@plt,这两个函数,为什么后面加了个@plt,因为这个为 PLT 表中的数据的地址。那为什么反编译中的代码地址为 PLT 表中的地址呢。

原因

运行时,PLT表在代码段,不允许修改,GOT表在数据段,可以修改。PLT 表可以称为内部函数表,GOT 表为全局函数表(也可以说是动态函数表这是个人自称),这两个表是相对应的,什么叫做相对应呢,PLT 表中的数据就是 GOT 表中的一个地址,可以理解为一定是一一对应的,如下图:

在这里插入图片描述

PLT 表中的每一项的数据内容都是对应的GOT 表中一项的地址这个是固定不变的,到这里大家也知道了 PLT 表中的数据根本不是函数的真实地址,而是 GOT 表项的地址。

其实在大家进入带有 @plt 标志的函数时,这个函数其实就是个过渡作用,因为 GOT 表项中的数据才是函数最终的地址,而 PLT 表中的数据又是 GOT 表项的地址,我们就可以通过 PLT 表跳转到 GOT表来得到函数真正的地址。那问题来了,这个 @plt 函数是怎么来的,这个函数是编译系统自己加的(静态链接阶段),大家可以通过disas gets看看里面的代码,如下:

gdb-peda$ disas gets
Dump of assembler code for function gets@plt:0x00000000000005d0 <+0>:	jmp    QWORD PTR [rip+0x2009fa]        # 0x200fd00x00000000000005d6 <+6>:	push   0x20x00000000000005db <+11>:	jmp    0x5a0

大家可以发现,这个函数只有三行代码,

  • 第一行跳转,
  • 第二行压栈,
  • 第三行又是跳转,

解释:
第一行跳转,它的作用是通过 PLT 表跳转到 GOT 表,而在第一次运行某一个函数之前,这个函数PLT 表对应的 GOT 表中的数据为 @plt 函数中第二行指令的地址,针对PLT[3]来说步骤如下:

  1. jmp 指令跳转到 GOT 表 0x200fd0,
  2. GOT 表中的数据为 0x5d6,
  3. 跳转到指令地址为0x5d6,
  4. 执行push 0x2 #这个为.rela.plt 段中的项偏移,参照下面的.rela.plt代码,为 __dl_runtime_resolve准备参数
  5. 再执行jmp 0x5a0,
  6. 而 0x5a0 为 PLT[0] 的地址,
  7. 执行push [0x200fb0] # 为GOT[1]中存储的数据,即link_map的地址, 为 __dl_runtime_resolve 准备函数参数
  8. PLT[0] 的指令会进入 GOT 0x200fb8 # 即 PLT[2]
  9. GOT 0x200fb8 存储 __dl_runtime_resolve 动态链接器的地址,
  10. __dl_runtime_resolve根据 4 和 7 中的 参数,查询相应的符号对应的地址,
  11. 执行查询到的函数,并且将真正的函数地址覆盖到 GOT 表中,

__dl_runtime_resolve 函数:

_dl_runtime_resolve(link_map_obj, reloc_index)
重定位节 '.rela.plt' at offset 0x538 contains 3 entries:偏移量          信息           类型           符号值        符号名称 + 加数
000000200fc0  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000200fc8  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000200fd0  000600000007 R_X86_64_JUMP_SLO 0000000000000000 gets@GLIBC_2.2.5 + 0

几个问题:

  1. PLT[0] 处到底做了什么,按照我们之前的思路它不是应该跳转到 GOT[0] 吗?
  2. 为什么中间要进行 push 压栈操作?
  3. 压入的序号为什么为 0x2,不是最开始应该为 0x0 吗?

解决问题

问题1
看下面代码:

gdb-peda$ x /40 0x5a0
0x5a0:	0xa0a35ff	0x25ff0020	0x200a0c	0x401f0f
0x5b0 <puts@plt>:	0xa0a25ff	0x680020	0xe9000000	0xffffffe0
0x5c0 <__stack_chk_fail@plt>:	0xa0225ff	0x1680020	0xe9000000	0xffffffd0
0x5d0 <gets@plt>:	0x9fa25ff	0x2680020	0xe9000000	0xffffffc0

我们尝试着查看0x5a0地址的数据内容发现一个问题,从0x5a0 - 0x5af之间的数据完全不知道是什么,而真正的PLT[x]中的数据是从 0x5b0 开始的,从这里才有了@plt为后缀的地址,但是我们disas puts 看代码的时候是从0x5a0开始的,我们可以通过x /5i 0x5a0 查看0x5a0 处的代码,如下“代码7”:

gdb-peda$ x /40i 0x5a00x5a0:	push   QWORD PTR [rip+0x200a0a]        # 0x200fb00x5a6:	jmp    QWORD PTR [rip+0x200a0c]        # 0x200fb80x5ac:	nop    DWORD PTR [rax+0x0]

我们看到了后面的#之后又一个16进制数,一看便可以知道是GOT表的地址,为什么这么肯定呢,因为我们可以通过objdump -R ./a.out查看一个程序的GOT函数的地址,如下图:
图8

zuanmin.lzm@localhost:~/test/pltgot$ objdump -R a.outa.out:     文件格式 elf64-x86-64DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE
0000000000200da8 R_X86_64_RELATIVE  *ABS*+0x00000000000006f0
0000000000200db0 R_X86_64_RELATIVE  *ABS*+0x00000000000006b0
0000000000201008 R_X86_64_RELATIVE  *ABS*+0x0000000000201008
0000000000200fd8 R_X86_64_GLOB_DAT  _ITM_deregisterTMCloneTable
0000000000200fe0 R_X86_64_GLOB_DAT  __libc_start_main@GLIBC_2.2.5
0000000000200fe8 R_X86_64_GLOB_DAT  __gmon_start__
0000000000200ff0 R_X86_64_GLOB_DAT  _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT  __cxa_finalize@GLIBC_2.2.5
0000000000200fc0 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5
0000000000200fc8 R_X86_64_JUMP_SLOT  __stack_chk_fail@GLIBC_2.4
0000000000200fd0 R_X86_64_JUMP_SLOT  gets@GLIBC_2.2.5

这里都是些GOT地址,我们发现都是0x200… 这些,所以可以断定 上面“代码7”中的也是GOT地址,那么我们可以猜想出,在正式存储一个函数的GOT地址前,我们的PLT表前面有一项进行一些处理,我们暂且不具体深入剖析这些代码有什么用,但是我们可以肯定puts@plt前面那16个字节也算是PLT表中的内容,这其实就是我们的PLT[0],正如我们之前问题提到的那样,我们的PLT[0]根本没有跳转到GOT[0],它不像我们的PLT[1]这些存储的是GOT表项的地址,它是一些代码指令,换句话说,PLT[0]
是一个函数,这个函数的作用是通过GOT[1] 和 GOT[2] 来正确绑定一个函数的正式地址到GOT表中来。
咦,这里问题好像又产生了,本来按照最开始的思路PLT[1]也是跳转到GOT[1]的,GOT[2]同理,但是这两个数据好像被PLT[0] 利用了,同时GOT[0]好像消失了,这里GOT[0]暂且不说它的作用是什么,针对GOT[1]和GOT[2]被PLT[0]利用,所以我们程序中真实情况其实是从PLT[1]到GOT[3],PLT[2]到GOT[4],所以我们推翻了我们的图4,建立一张新的处理表

在这里插入图片描述

而plt[0]代码做的事情则是:由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址,这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x] 中。这就是PLT表和GOT表,总而言之,我们调用一个函数的时候有两种方法,一个是通过PLT表调用,一个则是通过GOT表调用,因为PLT表最终也是跳转GOT表,GOT表中则是一个函数真正的地址,这里需要注意的是,在一个函数没有运行一次之前,GOT表中的数据为@plt函数中下一条指令的地址。

问题2
中间进行的压栈是为了确定PLT对应的GOT,根据压栈的数字,找到对应的 .rela.plt 项,该项的 offset 即是GOT项的地址,比如 push 0x0 对应的 offset 为0x200fc0,也就是GOT 的 第3项,所以PLT[1]−>GOT[3],0x3就是GOT的下标3,也就是说压栈后我们跳转到PLT[0],接着PLT[0]中的指令会通过这次压栈的序号来确定操作的GOT表项为多少。

问题3
好像都在第一个问题都已经解决了,这里压入0x2的原因是因为:该gets&plt项对应的 .rela.plt 里的项索引。

000000200fd0  000600000007 R_X86_64_JUMP_SLO 0000000000000000 gets@GLIBC_2.2.5 + 0

————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_18661257/article/details/54694748

其它参考:
https://blog.csdn.net/linyt/article/details/51635768
https://blog.csdn.net/linyt/article/details/51637832
https://blog.csdn.net/linyt/article/details/51893258

https://blog.werner.wiki/elf-plt-got-static-analysis/


http://www.mrgr.cn/news/78071.html

相关文章:

  • Java将PDF保存为图片
  • 《硬件架构的艺术》笔记(五):低功耗设计
  • CMS收集器和G1收集器
  • Chen_AdaMV-MoE_Adaptive_Multi-Task_Vision_Mixture-of-Experts 译文
  • 非常简单实用的前后端分离项目-仓库管理系统(Springboot+Vue)part 2
  • 鸿蒙开发——根据背景图片来构建特定颜色的蒙版
  • 【数据挖掘】一、基于LDA的用户兴趣建模(兴趣标签生成模型)--用户兴趣挖掘模型
  • 《硬件架构的艺术》笔记(七):处理字节顺序
  • 车载显示display基础知识和评估
  • 02.02、返回倒数第 k 个节点
  • 如何制作项目网页
  • 【C++11】尽显锋芒
  • 指针测试总结(一)(一维数组)
  • CTF-RE 从0到 N: 高版本 APK 调试 + APK逻辑修改再打包 + os层调试[2024 强网杯青少年专项赛 Flip_over] writeup
  • Apollo9.0源码部署(Nvidia显卡)
  • Nacos学习文档
  • Python + 深度学习从 0 到 1(00 / 99)
  • Qt Graphics View 绘图架构
  • ubuntu中使用ffmpeg和nginx推http hls视频流
  • 大表建/重建索引
  • 【ONE·基础算法 || 动态规划(二)】
  • C++ —— 以真我之名 如飞花般绚丽 - 智能指针
  • 极智嘉嵌入式面试题及参考答案
  • 不同查询构建器的使用方式(Mybatis、Mybatis-Plus、Mybatis-Flex、Spring Data JPA、QueryDsl)
  • 【Unity基础】如何选择渲染管线?
  • Failed to find SV in PRN block of SINEX file (Name svnav.dat)