理解PLT表和GOT表

news/2025/1/31 9:18:22 标签: c++

1 简介

  现代操作系统都是通过库来进行代码复用,降低开发成本提升系统整体效率。而库主要分为两种,一种是静态库,比如windows的.lib文件,macos的.a,linux的.a,另一种是动态库,比如windows的dll文件,macos的.dylib,linux的so。静态库本身就是中间产物的ar打包link阶段会参与直接的产物生成,而动态库本身已经是完整的二进制文件,link阶段只会进行符号定位。
  传统意义上认为静态链接的函数加载和执行效率要高于动态链接,这是由于静态链接在编译-链接阶段就能够确定函数的库入口地址。而动态链接并不是所有场景下都能够提前知道入口地址,可能只有需要加载的时候才需要确定。为了实现这一点,和提升加载效率,便诞生了PLT和GOT表。

2 PLT表和GOT表

2.1 GOT表

  GOT(Global Offset Table,全局偏移表)是为了实现地址无关代码而引入的一个偏移表格。函数调用或者数据访问时先访问GOT表,再通过该表中对应表项的偏移在动态库映射内存中找到具体的函数地址和数据地址。

  为什么需要使用GOT表进行重定位?

  1. 动态库需要生成地址无关代码方便动态库加载时定位函数地址或者数据地址,否则动态库的动态共享的优势不再存在,因此需要生成地址无关代码;
  2. GOT表格存储在数据区而不属于代码段,这样可以保证各个进程各自持有一份各自的GOT表根据自己的内存映射地址进行调整。

  GOT表需要考虑哪些内容?

  1. 模块间数据和函数地址访问。模块内不需要考虑,模块内使用模块内相对偏移即可。
  2. 全局数据,比如extern表示的数据。

2.2 PLT表

  PLT(Procedure Link Table,过程绑定表)是为了实现延迟绑定的地址表格。由于动态链接是在运行期链接并且进行重定位,本来直接访问的内存可能变成间接访问,会导致性能降低。ELF采用延迟绑定来缓解性能问题,其假设就是动态库中并不是所有的函数与数据都会用到,类似copy-on-write,仅仅在第一次符号被使用时才进行相关的重定位工作,避免对一些不必要的符号的重定位。
  ELF使用PLT(Procedure Linkage Table)实现延迟绑定。在进行重定位时每个符号需要了解符号在那个模块(模块ID)以及符号。当调用外部模块中的函数时,PLT为每个外部函数符号添加了PLT项,然后通过PLT项跳转到GOT表再到最终的函数地址。也就是说第一次调用会间接调用,之后可以直接通过PLT确认调用地址调用。

  PLT解决了哪些性能问题?

  1. 符号解析。动态库加载时不需要加载所有符号,只需要加载部分能够大幅度降低加载耗时;
  2. 避免重复解析。当外部调用动态库内函数或者访问数据地址时需要搜索符号表访问找到对应的项,对于比较大的动态库这个过程比较耗时。通过延迟加载只会在第一次比较耗时,之后不会重复解析;

3 深入理解GOT和PLT

  我们简单做个试验研究下PLT和GOT。下面是一段简单的代码,我们将其编译成动态库libadd.so

#include <cstdio>
#include <cmath>

static int myadd(const int a, const int b){
    return a + b;
}

int myabs(const int a){
    return std::abs(a);
}

void test(const int a, const int b){
    printf("%d %d", myadd(a, b), myabs(a));
}

  下面是生成的动态库的反汇编:

0000000000000630 <_Z5myabsi>:
 630:	55                   	push   %rbp
 631:	48 89 e5             	mov    %rsp,%rbp
 634:	89 7d fc             	mov    %edi,-0x4(%rbp)
 637:	8b 45 fc             	mov    -0x4(%rbp),%eax
 63a:	89 c1                	mov    %eax,%ecx
 63c:	f7 d9                	neg    %ecx
 63e:	0f 49 c1             	cmovns %ecx,%eax
 641:	5d                   	pop    %rbp
 642:	c3                   	retq   
 643:	66 66 66 66 2e 0f 1f 	data16 data16 data16 nopw %cs:0x0(%rax,%rax,1)
 64a:	84 00 00 00 00 00 

0000000000000650 <_Z4testii>:
 650:	55                   	push   %rbp
 651:	48 89 e5             	mov    %rsp,%rbp
 654:	48 83 ec 10          	sub    $0x10,%rsp
 658:	89 7d fc             	mov    %edi,-0x4(%rbp)
 65b:	89 75 f8             	mov    %esi,-0x8(%rbp)
 65e:	8b 7d fc             	mov    -0x4(%rbp),%edi
 661:	8b 75 f8             	mov    -0x8(%rbp),%esi
 664:	e8 27 00 00 00       	callq  690 <_ZL5myaddii>
 669:	89 45 f4             	mov    %eax,-0xc(%rbp)
 66c:	8b 7d fc             	mov    -0x4(%rbp),%edi
 66f:	e8 bc fe ff ff       	callq  530 <_Z5myabsi@plt>
 674:	8b 75 f4             	mov    -0xc(%rbp),%esi
 677:	89 c2                	mov    %eax,%edx
 679:	48 8d 3d 2d 00 00 00 	lea    0x2d(%rip),%rdi        # 6ad <_fini+0x9>
 680:	b0 00                	mov    $0x0,%al
 682:	e8 99 fe ff ff       	callq  520 <printf@plt>
 687:	48 83 c4 10          	add    $0x10,%rsp
 68b:	5d                   	pop    %rbp
 68c:	c3                   	retq   
 68d:	0f 1f 00             	nopl   (%rax)

0000000000000690 <_ZL5myaddii>:
 690:	55                   	push   %rbp
 691:	48 89 e5             	mov    %rsp,%rbp
 694:	89 7d fc             	mov    %edi,-0x4(%rbp)
 697:	89 75 f8             	mov    %esi,-0x8(%rbp)
 69a:	8b 45 fc             	mov    -0x4(%rbp),%eax
 69d:	03 45 f8             	add    -0x8(%rbp),%eax
 6a0:	5d                   	pop    %rbp
 6a1:	c3                   	retq   

  从上面能够看到对于内部函数的调用直接使用的内部偏移,比如myadd2中调用myadd就是callq 690 <_ZL5myaddii>。而调用printfmyabs就是callq 520 <printf@plt>callq 530 <_Z5myabsi@plt>
  下来我们分析下这个跳转指令。e8表示偏移跳转,后面跟的就是跳转地址偏移,即0xfffffebc,实际跳转地址便是off + rip=0xfffffebc + 674=0x530。(需要注意的是执行 callq 指令之前,RIP 指向 callq 指令的下一条指令,因此RIP是674)。

66f:	e8 bc fe ff ff       	callq  530 <_Z5myabsi@plt>

  接下来我们找到0x530的地址能够看到该地址又跳转到了510即plt表项,最终跳转到0x200af2(%rip)从注释中能够看到是GOT的表项。

0000000000000510 <.plt>:
 510:	ff 35 f2 0a 20 00    	pushq  0x200af2(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 516:	ff 25 f4 0a 20 00    	jmpq   *0x200af4(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 51c:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000000520 <printf@plt>:
 520:	ff 25 f2 0a 20 00    	jmpq   *0x200af2(%rip)        # 201018 <printf@GLIBC_2.2.5>
 526:	68 00 00 00 00       	pushq  $0x0
 52b:	e9 e0 ff ff ff       	jmpq   510 <.plt>

0000000000000530 <_Z5myabsi@plt>:
 530:	ff 25 ea 0a 20 00    	jmpq   *0x200aea(%rip)        # 201020 <_Z5myabsi@@Base+0x2009f0>
 536:	68 01 00 00 00       	pushq  $0x1
 53b:	e9 d0 ff ff ff       	jmpq   510 <.plt>

  接下来要查看GOT需要运行时查看,我们用GDB调试即可。首先在调用myabs的地方断点,单步进入,可以看到当前的代码:

(gdb) x /10i $pc
=> 0x7fffff1f0530 <_Z5myabsi@plt>:      jmpq   *0x200aea(%rip)        # 0x7fffff3f1020
   0x7fffff1f0536 <_Z5myabsi@plt+6>:    pushq  $0x1
   0x7fffff1f053b <_Z5myabsi@plt+11>:   jmpq   0x7fffff1f0510
   0x7fffff1f0540 <__cxa_finalize@plt>: jmpq   *0x200a9a(%rip)        # 0x7fffff3f0fe0
   0x7fffff1f0546 <__cxa_finalize@plt+6>:       xchg   %ax,%ax

  从上面的代码中能够看到需要跳转的地址是RIP+off=0x7fffff1f0536+0x200aea=0x7fffff3f1020。从下面的内容可以看到这个地址存储的是当前指令下一条执行的地址,即0x7fffff1f0536,也就是说这不是真正的函数地址还没有重定位。而上面的push $0x1就是预期这个符号在plt中的槽位编号。

(gdb) x /gx 0x7fffff3f1020
0x7fffff3f1020: 0x00007fffff1f0536
(gdb) x /gx 0x00007fffff1f0536
0x7fffff1f0536 <_Z5myabsi@plt+6>:       0xffd0e90000000168

  我们再单步几次就能看到基本能够确认这个过程是在进行符号解析:

(gdb) si
_dl_runtime_resolve_xsavec () at ../sysdeps/x86_64/dl-trampoline.h:71
71      ../sysdeps/x86_64/dl-trampoline.h: No such file or directory.
(gdb) x /3i $pc
=> 0x7fffff4178f0 <_dl_runtime_resolve_xsavec>: push   %rbx
   0x7fffff4178f1 <_dl_runtime_resolve_xsavec+1>:       mov    %rsp,%rbx
   0x7fffff4178f4 <_dl_runtime_resolve_xsavec+4>:       and    $0xffffffffffffffc0,%rsp

  退出当前函数,我们再看PLT表中的表项,可以看到已经被修改为_Z5myabsi的函数地址了。

(gdb) disass '_Z5myabsi@plt'
Dump of assembler code for function _Z5myabsi@plt:
   0x00007fffff1f0530 <+0>:     jmpq   *0x200aea(%rip)        # 0x7fffff3f1020
   0x00007fffff1f0536 <+6>:     pushq  $0x1
   0x00007fffff1f053b <+11>:    jmpq   0x7fffff1f0510
End of assembler dump.
(gdb) x /gx 0x7fffff3f1020
0x7fffff3f1020: 0x00007fffff1f0630
(gdb) x /gx 0x00007fffff1f0630
0x7fffff1f0630 <_Z5myabsi>:     0x8bfc7d89e5894855

4 总结

  PLT 和 GOT 是现代动态链接的核心机制,通过延迟绑定和地址无关性,提升了动态库的加载效率和灵活性。这些机制确保了代码复用及共享的优势,同时优化了性能。


http://www.niftyadmin.cn/n/5838538.html

相关文章

深入解析JUnit中的@ClassRule注解

在Java开发中&#xff0c;JUnit是一个非常流行的单元测试框架&#xff0c;它为开发者提供了强大的工具来编写和执行测试用例。今天&#xff0c;我们来深入探讨一下JUnit中的ClassRule注解&#xff0c;看看它是如何工作的&#xff0c;并通过一个实际的示例来加深理解。 一、Clas…

记录一次Sqoop从MySQL导入数据到Hive问题的排查经过

个人博客地址:记录一次Sqoop从MySQL导入数据到Hive问题的排查经过 | 一张假钞的真实世界 问题描述 MySQL中原始数据有790W+的记录数,在Sqoop抽取作业成功的情况下在Hive中只有500W左右的记录数。 排查过程 数据导入脚本Log 通过Log可以发现以下信息: 该Sqoop任务被分解…

Java---猜数字游戏

本篇文章所实现的是Java经典的猜数字游戏 , 运用简单代码来实现基本功能 目录 一.题目要求 二.游戏准备 三.代码实现 一.题目要求 随机生成一个1-100之间的整数(可以自己设置区间&#xff09;&#xff0c;提示用户猜测&#xff0c;猜大提示"猜大了"&#xff0c;…

低代码产品插件功能一览

下图是统计的目前市面上流行的低代码、零代码产品的插件功能。 产品名称 产品类型 官方插件数量 支持拓展 官方插件功能 宜搭 零代码 3 暂不支持 云打印、CAD看图、打印表单详情 微搭 低代码 1 暂不支持 小程序 明道云 低代码 2 支持 视图、工作流节点 简道…

Vue 3 30天精进之旅:Day 09 - 组合式API

在Vue 3中&#xff0c;组合式API&#xff08;Composition API&#xff09;是一个引入的新特性&#xff0c;它为开发者提供了一种更灵活的方式来构建和组织组件。与传统的选项API相比&#xff0c;组合式API更注重逻辑的复用和逻辑的组合&#xff0c;让我们更容易处理大型应用中的…

【Linux系统】进程间通信:认识命名管道

认识命名管道 1、命名管道的概念 管道应用的一个限制就是只能在具有共同祖先&#xff08;具有亲缘关系&#xff09;的进程间通信&#xff0c;即需要存在子进程继承父进程资源这层关系。如果我们想在不相关的进程之间交换数据&#xff08;即在任意两个进程间通信&#xff09;&a…

走近科学IT版:Windows里Ollama关闭后还在占用11434端口

走近科学IT版&#xff1a;Ollama关闭后还在占用11434端口 退出Ollama之后&#xff0c;再次启动Ollama服务&#xff0c;报错&#xff1a; (py311) E:\ai\ollama>ollama serve Error: listen tcp 127.0.0.1:11434: bind: Only one usage of each socket address (protocol/ne…

CSS-in-JS详解

CSS-in-JS是一种将CSS样式直接嵌入到JavaScript代码中的技术。这种技术允许开发者使用JavaScript的语法和功能来描述和管理组件的样式&#xff0c;从而带来了一系列的优势和特性。以下是对CSS-in-JS的详细解析&#xff1a; 一、CSS-in-JS的定义与原理 CSS-in-JS通过将CSS样式…