聊一聊Linux动态链接和GOT、PLT

共享动态库是现代系统的一个重要组成部分,大家肯定都不陌生,但是通常对背后的一些细节上的实现机制了解得不够深入。当然,网上有很多关于这方面的文章。希望这篇文章能够以一种与其他文章不同的角度呈现,可以对你产生一点启发。

关于动态链接与静态链接,看到过这么与一个比方:如果我的文章引用了别人的一部分文字,在我发布文章的时候把别人的段落复制到我的文章里面就属于静态连接,而做一个超链接让你们自己去看就属于动态链接

一、重定位

首先是重定位,先看一个例子:

extern int foo;

int function(void) {
    return foo;
}

保存为 a.cpp 然后编译并查看 a.o 中的重定位表信息:

$ gcc -c a.c
$ readelf --relocs ./a.o

输出如下:

Relocation section '.rela.text' at offset 0x1c8 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000006  000900000002 R_X86_64_PC32     0000000000000000 foo - 4

在创建a.o时,foo的值是未知的,因此编译器会留下一个(类型为R_X86_64_PC32的)重定位,它表示“在最终的二进制文件中,在这个目标文件的偏移0x4处,用符号foo的地址来替换值”。如果查看输出,我们能看到在偏移0x4处有4字节的零,表示亟待一个真实的地址:

$ objdump --disassemble ./a.o


./test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z8functionv>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # a <_Z8functionv+0xa>
   a:   5d                      pop    %rbp
   b:   c3                      retq

二、动态库访问数据

对于动态库 .so 来说,它可以被任意多个进程使用,这样在每个进程中都保留一份代码副本就没有意义。如果代码是只读的话,永远不会被修改,那么每个进程都可以共享相同的代码。

但是实际情况是,动态库在每个进程中仍然必须有一个唯一的数据实例。虽然在运行时可以将库数据放在任何地方,但这将需要重定位来告诉它实际的数据在哪个地址,从而破坏代码的始终只读属性,更是破坏了代码的可共享性。

所以现在的解决方案是将读写数据部分始终放置在已知偏移处。通过虚拟内存,每个进程都能看到自己的数据部分 .data,但可以共享未修改的指令代码也就是 .text 。访问数据需要的只是一些简单的加减运算;我想要访问变量的地址=我的当前地址+已知的固定偏移。

再看下以下示例:

$ cat test.c

static int foo = 100;

int function(void) {
    return foo;
}

$ gcc -fPIC -shared -o libtest.so test.c

看一下它的反汇编:

0000000000000589 <_Z8functionv>:
 589:   55                      push   rbp
 58a:   48 89 e5                mov    rbp,rsp
 58d:   8b 05 8d 0a 20 00       mov    eax,DWORD PTR [rip+0x200a8d]        # 201020 <_ZL3foo>
 593:   5d                      pop    rbp
 594:   c3                      ret

这表示“将当前指令指针(rip)偏移 0x200a8d 的位置的值放入 eax 中”。也就是我们知道数据在那个固定的偏移量上,所以我们可以找到它。

再看下下面的代码,我们引用外部的变量 foo :

$ cat test.c

extern int foo;

int function(void) {
    return foo;
}

$ gcc -shared -fPIC -o libtest.so test.c

请注意,foo是 extern 的;假定由其他库提供。

继续看下反汇编和段还有重定位信息:

$ objdump --disassemble libtest.so

[...]
00000000000005b9 <_Z8functionv>:
 5b9:   55                      push   rbp
 5ba:   48 89 e5                mov    rbp,rsp
 5bd:   48 8b 05 24 0a 20 00    mov    rax,QWORD PTR [rip+0x200a24]        # 200fe8 <foo@Base>
 5c4:   8b 00                   mov    eax,DWORD PTR [rax]
 5c6:   5d                      pop    rbp
 5c7:   c3                      ret
$ readelf --sections libtest.so

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [19] .got              PROGBITS         0000000000200fd8  00000fd8
       0000000000000028  0000000000000008  WA       0     0     8
  [20] .got.plt          PROGBITS         0000000000201000  00001000
       0000000000000020  0000000000000008  WA       0     0     8
$ readelf --relocs libtest.so

Relocation section '.rela.dyn' at offset 0x3e0 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
[...]
000000200fe8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 foo + 0

能够从反汇编看出要返回的值是从当前 rip 偏移 0x200a24 的位置加载的,即0x0200fe8。查看部分 Section Headers 信息,我们看到 .got 表的大小是 0x28,位于偏移 0x200fd8 处,所以0x0200fe8 是 .got 的一部分,它位于GOT表的偏移量为16的地方。继续看重定位时,我们看到了一个R_X86_64_GLOB_DAT重定位,它表示“查找符号 foo 的值并将其放入偏移 0x2008fe8 处。

因此,当加载此库时,动态加载器将检查重定位,查找 foo 的值,并根据需要修补 .got 入口。当代码加载该值时,它将找到正确的位置,一切都正常,而无需修改任何代码值,从而不破坏代码的可共享性。

总结一下,用一张图来表示:

这只展示了对数据变量的调用,但函数调用呢?

三、动态库调用函数

对于函数的调用与上面类似,不同的是GOT中相应的项保存的是目标函数的地址,当模块需要调用目标函数时,使用了称为过程链接表(PLT, Procedure Linkage Table)。代码不直接调用外部函数,而只通过 PLT 调用:

$ cat test.c

int foo(void);

int function(void) {
    return foo();
}

$ gcc -shared -fPIC -o libtest.so test.c
$ objdump --disassemble libtest.so

[...]
00000000000005c9 <_Z8functionv>:
 5c9:   55                      push   %rbp
 5ca:   48 89 e5                mov    %rsp,%rbp
 5cd:   e8 1e ff ff ff          callq  4f0 <_Z3foov@plt>
 5d2:   5d                      pop    %rbp
 5d3:   c3                      retq

因此,我们看到 function 调用了地址 0x4f0 的代码。反汇编此代码:

$ objdump --disassemble-all libtest.so

Disassembly of section .plt:

00000000000004e0 <.plt>:
 4e0:   ff 35 22 0b 20 00       pushq  0x200b22(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 4e6:   ff 25 24 0b 20 00       jmpq   *0x200b24(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 4ec:   0f 1f 40 00             nopl   0x0(%rax)

00000000000004f0 <_Z3foov@plt>:
 4f0:   ff 25 22 0b 20 00       jmpq   *0x200b22(%rip)        # 201018 <_Z3foov@Base>
 4f6:   68 00 00 00 00          pushq  $0x0
 4fb:   e9 e0 ff ff ff          jmpq   4e0 <.plt>

我们可以看到这里跳转到 0x200b22(%rip) 也就是 201018,我们可以看到其重定位为符号 foo。

$ readelf --relocs libtest.so

Relocation section '.rela.plt' at offset 0x490 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000201018  000300000007 R_X86_64_JUMP_SLO 0000000000000000 _Z3foov + 0

再继续看,可以看到 201018 处是 04f6:

$ objdump --disassemble-all libtest.so

Disassembly of section .got.plt:

0000000000201000 <_GLOBAL_OFFSET_TABLE_>:
  201000:       20 0e                   and    %cl,(%rsi)
  201002:       20 00                   and    %al,(%rax)
        ...
  201018:       f6 04 00 00             testb  $0x0,(%rax,%rax,1)
  20101c:       00 00                   add    %al,(%rax)
  20101e:       00 00                   add    %al,(%rax)
  201020:       06                      (bad)
  201021:       05 00 00 00 00          add    $0x0,%eax

这就又回到了foo@plt的第二项,这里把 0 push到了栈上.

实际上,这里就是延迟绑定的实现,第一次访问这个函数的时候,链接器并没有把函数的真实地址填到 GOT 中,而是将 foo@plt 的第二条指令 pushq $0x0  的地址填到了 GOT 中,也就是说第一次访问函数的时候,foo@plt 中 第一条指令的效果就是跳转到 foo@plt 的第二条指令,push 的 0 就是 foo 这个符号引用在重定位表  .rel.plt 中的下标,接着又是一条跳转指令到 4e0。

00000000000004f0 <_Z3foov@plt>:
 4f0:   ff 25 22 0b 20 00       jmpq   *0x200b22(%rip)        # 201018 <_Z3foov@Base>
 4f6:   68 00 00 00 00          pushq  $0x0
 4fb:   e9 e0 ff ff ff          jmpq   4e0 <.plt>

4e0是啥呢?这里又到了 plt 上,又把 0x200b22(%rip) 入栈,然后继续跳转到*0x200b24(%rip)处存储的地址上:

$ objdump --disassemble-all libtest.so

Disassembly of section .plt:

00000000000004e0 <.plt>:
 4e0:   ff 35 22 0b 20 00       pushq  0x200b22(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 4e6:   ff 25 24 0b 20 00       jmpq   *0x200b24(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 4ec:   0f 1f 40 00             nopl   0x0(%rax)

从上面不难看出,其实 

0x200b22(%rip)    # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>      和 

*0x200b24(%rip)      # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>

都是 GOT 里面的偏移为 8 和 16 的条目。

第一条,pushq 0x200b22,是将地址压到栈上,也即向最终调用的函数传递参数。
第二条,jmp *0x200b24,这是跳到最终的函数去执行,不过猜猜就能想到,这是跳到能解析动态库函数地址的代码里面执行。

*0x200b24里面放着的到底是什么呢?其实就是函数 _dl_runtime_resolve,那么_dl_runtime_resolve 是怎么干活的呢?

还记得前面的 pushq $0x0 吗,我们刚才说 0 就是 foo 这个符号引用在重定位表 .rel.plt 中的下标,也就是相当于函数 foo 的 id,有了它 _dl_runtime_resolve 就能够知道要去解析哪个函数了,然后它在进行一系列的解析工作之后将 foo 的真正地址填入到 GOT 中去。

一旦 foo 的地址被解析完之后,当再次调用 foo@plt 的时候,第一条 jmp 指令就能直接跳转到真正的 foo() 函数中。

最后用一张图再来总结一下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/173065.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【开源】基于Vue和SpringBoot的服装店库存管理系统

项目编号&#xff1a; S 052 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S052&#xff0c;文末获取源码。} 项目编号&#xff1a;S052&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 角色管理模块2.3 服…

什么是Jmeter ?Jmeter使用的原理步骤是什么?

1.1 什么是 JMeter Apache JMeter 是 Apache 组织开发的基于 Java 的压力测试工具。用于对软件做压力测试&#xff0c;它最初被设计用于 Web 应用测试&#xff0c;但后来扩展到其他测试领域。 它可以用于测试静态和动态资源&#xff0c;例如静态文件、Java 小服务程序、CGI 脚本…

Continuity” of stochastic integral wrt Brownian motion

See https://imathworks.com/math/math-continuity-of-stochastic-integral-wrt-brownian-motion/

5.基于飞蛾扑火算法(MFO)优化的VMD参数(MFO-VMD)

代码的使用说明 基于飞蛾扑火算法优化的VMD参数 优化算法代码原理 飞蛾扑火优化算法&#xff08;Moth-Flame Optimization&#xff0c;MFO&#xff09;是一种新型元启发式优化算法&#xff0c;该算法是受飞蛾围绕火焰飞行启发而提出的&#xff0c;具有搜索速度快、寻优能力强的…

梯度引导的分子生成扩散模型- GaUDI 评测

GaUDI模型来自于以色列理工Tomer Weiss的2023年发表在预印本ChemRxiv上的工作 《Guided Diffusion for Inverse Molecular Design》。原文链接&#xff1a;Guided Diffusion for Inverse Molecular Design | Materials Chemistry | ChemRxiv | Cambridge Open Engage GaUDI模型…

LeSS敏捷框架高效生产力实践

每个团队可能都有一套适合自己的敏捷方法&#xff0c;本文介绍了ResponseTap工程团队通过采用LeSS框架、引入准备周&#xff0c;从而提升迭代冲刺研发效能的实践。原文: LeSS Agile, More Productive — Part 1: Pain[1], LeSS Agile, More Productive — Part 2: Promise, LeS…

开源的进销存系统都有哪些?

开源的进销存系统有很多&#xff0c;以下是其中一些比较流行的: OpenERP&#xff1a;一个集成了多个业务功能的开源ERP软件&#xff0c;可以实现进销存管理&#xff0c;会计&#xff0c;仓库管理&#xff0c;销售管理等业务功能。 Odoo&#xff1a;是OpenERP的一个分支&#x…

【智能优化算法】从蚁群到动物园

目录 引言蚁群优化算法&#xff08;ACO&#xff09;ACO 机理ACO 模型描述ACO 移动策略 粒子群优化算法&#xff08;PSO&#xff09;PSO 机理PSO 模型描述 萤火虫群优化算法&#xff08;GSO&#xff09;GSO 机理GSO 模型描述 群智能优化算法 引言 21世纪&#xff0c;人类社会已经…

Python绘图库Plotly用超简单代码实现丰富的数据可视化图表

文章目录 前言Plotly 概述散点图时间序列分析 高级绘图功能散点图矩阵关系热图自定义主题 在 Plotly 图表工坊&#xff08;Plotly Chart Studio&#xff09;里编辑技术交流关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Pyt…

万字长文深入理解 cache,写出高性能代码

CACHE的一致性 Cache的一致性有这么几个层面 1. 一个CPU的icache和dcache的同步问题 2. 多个CPU各自的cache同步问题 3. CPU与设备&#xff08;其实也可能是个异构处理器&#xff0c;不过在Linux运行的CPU眼里&#xff0c;都是设备&#xff0c;都是DMA&#xff09;的cache同…

react等效memo的方法

视频教程 前端技术&#xff5c;Dan博客&#xff5c;在你写memo()之前_哔哩哔哩_bilibili 把与ExpensiveTree的无关的dom做成一个组件 第二种情况&#xff0c;color在ExpensiveTree组件的父级dom 创建一个组件&#xff0c;将state的color和input写上&#xff0c;而ExpensiveTr…

接入电商数据平台官方开放平台API接口获取商品实时信息数据,销量,评论,详情页演示

要接入电商数据平台官方开放平台API接口获取商品实时信息数据、销量、评论和详情页演示&#xff0c;需要按照以下步骤进行操作&#xff1a; 找到可用的API接口&#xff1a;首先&#xff0c;需要找到支持查询商品信息的API接口。可以在电商数据平台的官方开放平台上查找相应的AP…

springboot多环境配置

前言 在实际项目研发中&#xff0c;需要针对不同的运行环境&#xff0c;如开发环境、测试环境、生产环境等&#xff0c;每个运行环境的数据库…等配置都不相同&#xff0c;每次发布测试、更新生产都需要手动修改相关系统配置。这种方式特别麻烦&#xff0c;费时费力&#xff0…

n-皇后问题(DFS回溯)

n−皇后问题是指将 n 个皇后放在 nn的国际象棋棋盘上&#xff0c;使得皇后不能相互攻击到&#xff0c;即任意两个皇后都不能处于同一行、同一列或同一斜线上。 现在给定整数 n&#xff0c;请你输出所有的满足条件的棋子摆法。 输入格式 共一行&#xff0c;包含整数 n。 输出…

2023年【四川省安全员A证】复审考试及四川省安全员A证考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 四川省安全员A证复审考试根据新四川省安全员A证考试大纲要求&#xff0c;安全生产模拟考试一点通将四川省安全员A证模拟考试试题进行汇编&#xff0c;组成一套四川省安全员A证全真模拟考试试题&#xff0c;学员可通过…

Gitlab安装与操作

GitLab 是一个用于仓库管理系统的开源项目&#xff0c;使用Git作为代码管理工具&#xff0c;并在此基础上搭建起来的Web服务。 可通过Web界面进行访问公开的或者私人项目。它拥有与Github类似的功能&#xff0c;能够浏览源代码&#xff0c;管理缺陷和注释。可以管理团队对仓库的…

五年程序员兼职接单的肺腑之言

不知不觉我已经参加工作&#xff0c;当一个程序员五年了&#xff0c;从一个职场菜鸟逐渐变成老油条&#xff0c;个中辛酸只有自己知道。这五年做过各种兼职接单&#xff0c;踩过不少坑&#xff0c;今天就把我在程序员接单上的一些心得体会分享给大家&#xff0c;希望能对兼职接…

【每日OJ —— 225.用队列实现栈(队列)】

每日OJ —— 225.用队列实现栈&#xff08;队列&#xff09; 1.题目&#xff1a;225.用队列实现栈&#xff08;队列&#xff09;2.解法2.1.解法讲解&#xff1a;2.1.1.算法讲解2.1.2.代码实现2.1.3.提交通过展示 1.题目&#xff1a;225.用队列实现栈&#xff08;队列&#xff0…

Jieba库——中文自然语言处理的利器

中文作为世界上最广泛使用的语言之一&#xff0c;其复杂的结构和丰富的表达方式给中文文本处理带来了挑战。为了解决这些问题&#xff0c;Python开发者开发了一系列用于处理中文文本的工具和库&#xff0c;其中最受欢迎和广泛应用的就是Jieba库。Jieba是一个开源的中文分词工具…

【SA8295P 源码分析 (三)】132 - GMSL2 协议分析 之 GPIO/SPI/I2C/UART 等通迅控制协议带宽消耗计算

【SA8295P 源码分析】132 - GMSL2 协议分析 之 GPIO/SPI/I2C/UART 等通迅控制协议带宽消耗计算 一、GPIO 透传带宽消耗计算二、SPI 通迅带宽消耗计算三、I2C 通迅带宽消耗计算四、UART 通迅带宽消耗计算系列文章汇总见:《【SA8295P 源码分析 (三)】Camera 模块 文章链接汇总 -…