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

news2024/9/23 13:18:38

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

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

一、重定位

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

extern int foo;

int function(void) {
    return foo;
}

保存为 a.c 然后编译并查看 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() 函数中。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

@Scheduled注解 定时任务讲解

用于在Java Spring框架中定时执行特定任务的注解 Scheduled&#xff0c;它能够指定方法在特定时间间隔或特定时间点执行。默认参数是cron&#xff0c;cron参数被用来定义一个Cron表达式&#xff0c;它代表了任务执行的时间规则 参数如下 Cron 这是是一种时间表达式&#xff…

Python版本切换的解决方法,升级Python 2.7 到 Python 3.7

之前解决问题的时候&#xff0c;记录的一个Python版本切换的解决方法&#xff0c;今天在解决其他问题的时候&#xff0c;查看了下之前写的这个解决方法。还是很适用的。然后加入了下linux系统中python配置修改的问题。 Mac 升级Python 2.7 到 Python 3.7 1、python3.7官网下载…

【SpringBoot】 This application has no explicit mapping for 解决方法

This application has no explicit mapping for 解决方法 This application has no explicit mapping for 解决方法一、背景二、原因三、解决方案方式一&#xff1a;方式二&#xff1a; 四、解决 This application has no explicit mapping for 解决方法 一、背景 在SpringBo…

EXCEL小技巧

1、两列文本合并显示&#xff1a; CONCATENATE(B6,E6) &#xff08;如果显示公式而非文本&#xff0c;就是公式输错了&#xff0c;比如后缺少空格&#xff09;

Word中如何实现 图片 | 表格 自动编号与文中引用编号对应

当我们在进行大篇幅word文档的编写时&#xff0c;为了节约修改文章中图片或表格所花费的大量时间&#xff0c;可以将图片自动编号&#xff0c;且让文中引用的顺序跟着图片顺序的变化而变化&#xff0c;具体操作如下&#xff1a; 1. 将鼠标定位在图片或者表格欲加编号的下方或上…

ChatGPT有效开通方法*建议收藏*

ChatGPT Plus和API开通方法*建议收藏* 前期准备&#xff1a; 一个ChatGPT账户 一张虚拟卡 开通ChatGPT Plus会员&#xff0c;很多朋友担心虚拟卡订阅会封号&#xff0c;只要不勤换登陆IP不会使出现问题。而且目前大部分用户都是使用虚拟卡开通的会员 第一步&#xff1a;登…

基于STC12C5A60S2系列1T 8051单片读写掉电保存数据IIC总线器件24C02地址码并显示在液晶显示器LCD1602上应用

基于STC12C5A60S2系列1T 8051单片读写掉电保存数据IIC总线器件24C02地址码并显示在液晶显示器LCD1602上应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍液晶显示器…

大数据-计算框架选型与对比

计算框架选型与对比 一、大数据平台二、计算框架分类1.批处理架构2.实时流处理架构3.流批一体处理架构 三、计算框架关键指标1.处理模式2.可伸缩性3.消息传递3.1 至少一次&#xff08;at least once&#xff09;3.2 至多一次&#xff08;ai most once&#xff09;3.3 恰好一次&…

交叉编译安装时报错 ./install.sh: 15: ./install.sh: Bad substitution

报错信息截图如下&#xff1a; 解决方法 vim install.sh #!/bin/sh -e 修改为 !/bin/bash -e重新执行 sudo ./install.sh 成功运行

Leetcode算法系列| 1. 两数之和(四种解法)

目录 1.题目2.题解解法一&#xff1a;暴力枚举解法二&#xff1a;哈希表解法解法三&#xff1a;双指针(有序状态)解法四&#xff1a;二分查找(有序状态) 1.题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数…

Endnote软件添加期刊引用格式

在下述网址中&#xff0c;找到你想要添加的期刊&#xff0c;下载引用格式文件&#xff08;后缀为.ens格式&#xff09; https://endnote.com/downloads/styles/?wpv_post_searchInformationfusion&wpv_aux_current_post_id12829&wpv_view_count12764-TCPID12829 下载…

C语言——从键盘输人一个表示年份的整数,判断该年份是否为闰年,并显示判断结果。

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int year 0;printf("请输入年份&#xff1a;");scanf("%d",&year);if((year%4 0) && (year%100!0) || (year%400 0)){printf("%d是闰年\n",year);}else{p…

人工标签不准确的一种解决方案:PCA降维可视化筛选正样本

背景 在实际的业务场景里&#xff0c;用会话文本构建模型&#xff08;机器学习/深度学习&#xff09;来做意图分类之类的任务时&#xff0c;经常会出现人工打标不够准确的问题&#xff0c;标签都不准确的话模型当然无法学习到有效信息了。这个问题真的非常头疼…除了与业务沟通…

【CCF-PTA】第03届Scratch第03题 -- 参观食品厂

参观食品厂 【题目描述】 光明小学定期组织学生参与社会实践&#xff0c;最近的活动主题是参观自动化食品厂。小明看到生产线可以自动筛选罐头重量是否合格&#xff0c;感到十分好奇。回来后&#xff0c;小明准备写一个模拟程序&#xff0c;输入10盒罐头的重量&#xff0c;程…

openGauss学习笔记-131 openGauss 数据库运维-启停openGauss

文章目录 openGauss学习笔记-131 openGauss 数据库运维-启停openGauss131.1 启动openGauss131.2 停止openGauss131.3 示例131.3.1 启动openGauss131.3.2 停止openGauss 131.4 错误排查 openGauss学习笔记-131 openGauss 数据库运维-启停openGauss 131.1 启动openGauss 以操作系…

前端 vue 面试题(二)

文章目录 如何让vue页面重新渲染组件间通信vue为什么要mutation、 action操作插槽、具名插槽、作用域插槽vue编译使用的是什么库&#xff1f;vue怎么实现treeshakingwebpack实现treeshaking为什么只有es module 能支持 tree shaking mixin 的作用mixin的底层原理nexTick原理vue…

python -opencv 图像锐化

python -opencv 图像锐化 图像锐化其实&#xff0c;是一种增强图片对比度的技术&#xff0c;我们可以通过计算图像的导数&#xff0c;把导数绝对值数值大于零的数值加回原图像&#xff0c;通过这种方法&#xff0c;可以增强图像的对比度。 实现代码如下&#xff1a; import c…

Django之中间件与CSRF_TOKEN

文章目录 一、什么是中间件二、中间件有什么用三、Django自定义中间件中间件中主要方法及作用创建自定义中间件的步骤&#xff1a;process_request与process_response方法process_view方法process_exceptionprocess_template_response&#xff08;不常用&#xff09; 四、CSRF_…

Django DRF序列化器serializer

以下案例由浅到深&#xff0c;逐步深入&#xff0c;通过实例介绍了序列化器的使用方法&#xff0c;和遇到的常见问题的解决方法。 一、序列化器serializers.Serializer 1、urls.py urlpatterns [path("api/<str:version>/depart/",views.DepartView.as_vie…

找论文找论文

这里写目录标题 找到的&#xff0c;待筛选识别检测 OCR综述&#xff0c;经典论文综述OCR识别OCR检测端到端文本识别问题集怎么看一篇论文有没有代码怎么直接找比如某一年的CVPR关于OCR方面的最新论文拿到一篇论文&#xff0c;根据论文名字怎么检索到期刊和发表日期 功能技巧找顶…