理解PLT表和GOT表

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   %rbp631:	48 89 e5             	mov    %rsp,%rbp634:	89 7d fc             	mov    %edi,-0x4(%rbp)637:	8b 45 fc             	mov    -0x4(%rbp),%eax63a:	89 c1                	mov    %eax,%ecx63c:	f7 d9                	neg    %ecx63e:	0f 49 c1             	cmovns %ecx,%eax641:	5d                   	pop    %rbp642:	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   %rbp651:	48 89 e5             	mov    %rsp,%rbp654:	48 83 ec 10          	sub    $0x10,%rsp658:	89 7d fc             	mov    %edi,-0x4(%rbp)65b:	89 75 f8             	mov    %esi,-0x8(%rbp)65e:	8b 7d fc             	mov    -0x4(%rbp),%edi661:	8b 75 f8             	mov    -0x8(%rbp),%esi664:	e8 27 00 00 00       	callq  690 <_ZL5myaddii>669:	89 45 f4             	mov    %eax,-0xc(%rbp)66c:	8b 7d fc             	mov    -0x4(%rbp),%edi66f:	e8 bc fe ff ff       	callq  530 <_Z5myabsi@plt>674:	8b 75 f4             	mov    -0xc(%rbp),%esi677:	89 c2                	mov    %eax,%edx679:	48 8d 3d 2d 00 00 00 	lea    0x2d(%rip),%rdi        # 6ad <_fini+0x9>680:	b0 00                	mov    $0x0,%al682:	e8 99 fe ff ff       	callq  520 <printf@plt>687:	48 83 c4 10          	add    $0x10,%rsp68b:	5d                   	pop    %rbp68c:	c3                   	retq   68d:	0f 1f 00             	nopl   (%rax)0000000000000690 <_ZL5myaddii>:690:	55                   	push   %rbp691:	48 89 e5             	mov    %rsp,%rbp694:	89 7d fc             	mov    %edi,-0x4(%rbp)697:	89 75 f8             	mov    %esi,-0x8(%rbp)69a:	8b 45 fc             	mov    -0x4(%rbp),%eax69d:	03 45 f8             	add    -0x8(%rbp),%eax6a0:	5d                   	pop    %rbp6a1:	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  $0x052b:	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  $0x153b:	e9 d0 ff ff ff       	jmpq   510 <.plt>

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

(gdb) x /10i $pc
=> 0x7fffff1f0530 <_Z5myabsi@plt>:      jmpq   *0x200aea(%rip)        # 0x7fffff3f10200x7fffff1f0536 <_Z5myabsi@plt+6>:    pushq  $0x10x7fffff1f053b <_Z5myabsi@plt+11>:   jmpq   0x7fffff1f05100x7fffff1f0540 <__cxa_finalize@plt>: jmpq   *0x200a9a(%rip)        # 0x7fffff3f0fe00x7fffff1f0546 <__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   %rbx0x7fffff4178f1 <_dl_runtime_resolve_xsavec+1>:       mov    %rsp,%rbx0x7fffff4178f4 <_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)        # 0x7fffff3f10200x00007fffff1f0536 <+6>:     pushq  $0x10x00007fffff1f053b <+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.rhkb.cn/news/9896.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visual Studio使用GitHub Copilot提高.NET开发工作效率

GitHub Copilot介绍 GitHub Copilot 是一款 AI 编码助手&#xff0c;可帮助你更快、更省力地编写代码&#xff0c;从而将更多精力集中在问题解决和协作上。 GitHub Copilot Free包含哪些功能&#xff1f; 每月 2000 代码补全&#xff0c;帮助开发者快速完成代码编写。 每月 …

[JavaWeb]搜索表单区域

一.注意事项 设置外边距:margin:(参数可省去部分)上 下 左 右 二.源代码 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>Tlias智能学习辅助系统</title> <style> /* 导航栏样…

NLP自然语言处理通识

目录 ELMO 一、ELMo的核心设计理念 1. 静态词向量的局限性 2. 动态上下文嵌入的核心思想 3. 层次化特征提取 二、ELMo的模型结构与技术逻辑 1. 双向语言模型&#xff08;BiLM&#xff09; 2. 多层LSTM的层次化表示 三、ELMo的运行过程 1. 预训练阶段 2. 下游任务微调 四、ELMo的…

Eureka 服务注册和服务发现的使用

1. 父子工程的搭建 首先创建一个 Maven 项目&#xff0c;删除 src &#xff0c;只保留 pom.xml 然后来进行 pom.xml 的相关配置 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xs…

OpenCV:二值化与自适应阈值

目录 简述 1. 什么是二值化 2. 二值化接口 2.1 参数说明​​​​​ 2.2 示例代码 2.3 运行结果 3. 自适应阈值 3.1 参数说明 3.2 示例代码 3.3 运行结果 4. 总结 4.1 二值化 4.2 自适应阈值 相关阅读 OpenCV&#xff1a;图像的腐蚀与膨胀-CSDN博客 简述 图像二值…

Java面试题2025-设计模式

1.说一下开发中需要遵守的设计原则&#xff1f; 设计模式中主要有六大设计原则&#xff0c;简称为SOLID &#xff0c;是由于各个原则的首字母简称合并的来(两个L算一个,solid 稳定的)&#xff0c;六大设计原则分别如下&#xff1a; 1、单一职责原则 单一职责原则的定义描述非…

Win11下帝国时代2无法启动解决方法

鼠标右键点图标&#xff0c;选择属性 点开始&#xff0c;输入启用和关闭

JAVA实战开源项目:在线文档管理系统(Vue+SpringBoot) 附源码

本文项目编号 T 038 &#xff0c;文末自助获取源码 \color{red}{T038&#xff0c;文末自助获取源码} T038&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

Python设计模式 - 组合模式

定义 组合模式&#xff08;Composite Pattern&#xff09; 是一种结构型设计模式&#xff0c;主要意图是将对象组织成树形结构以表示"部分-整体"的层次结构。这种模式能够使客户端统一对待单个对象和组合对象&#xff0c;从而简化了客户端代码。 组合模式有透明组合…

算法每日双题精讲 —— 前缀和(【模板】一维前缀和,【模板】二维前缀和)

在算法竞赛与日常编程中&#xff0c;前缀和是一种极为实用的预处理技巧&#xff0c;能显著提升处理区间和问题的效率。今天&#xff0c;我们就来深入剖析一维前缀和与二维前缀和这两个经典模板。 一、【模板】一维前缀和 题目描述 给定一个长度为 n n n 的整数数组 a a a&…

VLLM性能调优

1. 抢占 显存不够的时候&#xff0c;某些request会被抢占。其KV cache被清除&#xff0c;腾退给其他request&#xff0c;下次调度到它&#xff0c;重新计算KV cache。 报这条消息&#xff0c;说明已被抢占&#xff1a; WARNING 05-09 00:49:33 scheduler.py:1057 Sequence gr…

知识管理系统塑造企业文化与学习型组织的变革之路

内容概要 知识管理系统&#xff08;Knowledge Management System, KMS&#xff09;是指组织内部为有效获取、存储、共享和应用知识而建立的结构和技术体系。这一系统不仅是信息技术的运用&#xff0c;更是推动企业文化和学习型组织发展的重要工具。在当今快速变化的商业环境中…

智能汽车网络安全威胁报告

近年来随着智能汽车技术的快速发展&#xff0c;针对智能汽车的攻击也逐渐从传统的针对单一车辆控制器的攻击转变为针对整车智能化服务的攻击&#xff0c;包括但不限于对远程控制应用程序的操控、云服务的渗透、智能座舱系统的破解以及对第三方应用和智能服务的攻击。随着WP.29 …

Python练习(2)

今日题单 吃鱼还是吃肉 PTA | 程序设计类实验辅助教学平台 降价提醒机器人PTA | 程序设计类实验辅助教学平台 幸运彩票 PTA | 程序设计类实验辅助教学平台 猜帽子游戏 PTA | 程序设计类实验辅助教学平台 谁管谁叫爹 PTA | 程序设计类实验辅助教学平台 就不告诉你 PTA | 程…

Ubuntu-手动安装 SBT

文章目录 前言Ubuntu-手动安装 SBT1. SBT是什么?1.1. SBT 的特点1.2. SBT 的基本功能1.3. SBT 的常用命令 2. 安装2.1. 下载2.2. 解压 sbt 二进制包2.3. 确认 sbt 可执行文件的位置2.4. 设置执行权限2.5. 创建符号链接2.6. 更新 PATH 环境变量2.7. 验证 sbt 安装 前言 如果您觉…

240. 搜索二维矩阵||

参考题解&#xff1a;https://leetcode.cn/problems/search-a-2d-matrix-ii/solutions/2361487/240-sou-suo-er-wei-ju-zhen-iitan-xin-qin-7mtf 将矩阵旋转45度&#xff0c;可以看作一个二叉搜索树。 假设以左下角元素为根结点&#xff0c; 当target比root大的时候&#xff…

Golang笔记——常用库context和runtime

大家好&#xff0c;这里是Good Note&#xff0c;关注 公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍Golang的常用库context和runtime&#xff0c;包括库的基本概念和基本函数的使用等。 文章目录 contextcontext 包的基本概念主要类型和函数1. **…

Leetcode:350

1&#xff0c;题目 2&#xff0c;思路 首先判断那个短为什么呢因为我们用短的数组去挨个点名长的数组主要用map装长的数组max判断map里面有几个min数组的元素&#xff0c;list保存交集最后用数组返回list的内容 3&#xff0c;代码 import java.util.*;public class Leetcode…

《多线程基础之互斥锁》

【互斥锁导读】互斥锁是大家使用最多的线程同步手段&#xff0c;但仅仅知道怎么用还是不够的&#xff1f;比如&#xff1a;面试官问你"互斥锁是属于内核层还是应用层的同步保护机制&#xff1f;性能怎样&#xff1f;"&#xff0c;"频繁加解锁&#xff0c;会有什…

【Rust自学】15.0. 智能指针(序):什么是智能指针及Rust智能指针的特性

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 15.0.1 指针的基本概念 指针是一个变量在内存中包含的是一个地址&#xff0c;指向另一个数据。 Rust 中最常见的指针是引用&#xff0c…