【Linux】动静态库链接原理

📝前言:

这篇文章我们来讲讲Linux——动静态库链接原理

🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏


目录

  • 一,目标文件
  • 二,ELF文件
    • ELF文件格式的特点
    • 1. ELF形成可执行
    • 2. ELF可执行加载
      • 具体查看
        • Section查看
        • Segment查看
        • ELF Header查看
  • 三,理解链接与加载
    • 1. 静态链接与静态库加载
      • 查看编译后的符号表
        • 符号表
      • 查看反汇编目标文件的内容
    • 2. ELF加载与进程地址空间
      • 静态链接总结
    • 3. 动态链接与动态库加载
      • 动态链接器
      • 库间的依赖
      • PLT

一,目标文件

在这里插入图片描述
我们都知道,形成可执行需要经过 编译 + 链接两个步骤。当.c文件经过编译后形成的.o文件就叫做可重定位/可重定向目标文件。
当我们只有一个.c文件被修改时,我们只需要对修改的文件进行重新编译就行了,其他文件不需要。

二,ELF文件

.o文件,动静态库,可执行文件,内核转储(core dumps)都是ELF格式的二进制文件。

ELF文件格式的特点

ELF文件被划分成很多个
在这里插入图片描述

  • ELF Header:描述文件的全局属性,主要作用是定位⽂件的其他部分
  • Program Header Table 列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度。(链接阶段,有合并以后才会生成 Program Headers)【segments是什么后面讲】
  • 用来描述整个ELF文件
  • Section就是,不同的数据会被存储到不同的节中。如代码节存储了可执行代码,数据节存储了全局变量和静态数据等
  • Section Header Table用来描述每个节的信息

1. ELF形成可执行

  1. 将多份 C/C++ 源代码,翻译成为⽬标 .o⽂件
  2. 将多份 .o ⽂件section进行合并(合并是:链接的过程之一)

在这里插入图片描述
简单来说,就是把多个.o文件 中具有相同特性的Section合并成一个大的Segment

2. ELF可执行加载

  • 一个ELF文件在加载到内存的时候,也会把这个文件中具有相同特性(比如:把只读的代码段和只读数据合并)的Section合并,形成segment
  • 这个合并⼯作也已经在形成ELF的时候,合并⽅式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中

为什么要将Section合并?

  1. 为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为 4096 字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为 512 字节,那么它们将占用 3 个页面(.text两个 + .init一个),而合并后,它们只需 2 个页面。
  2. 将具有相同属性的section合并成⼀个大的segment,可以实现不同的访问权限,从而优化内存管理和权限访问控制

具体查看

Section查看

查看可执行程序的Section(我的可执行名称叫test):

readelf -S test

在这里插入图片描述
我们可以看到Section header table对每个Section的描述

查看可执行程序的Segment

readelf -l test

在这里插入图片描述
在图片中,我们就可以看到有哪些Section被合并成了一个Segment

提几个重要的Section

  • text节 :保存了程序代码指令的代码节。
  • data节 :保存了初始化的全局变量和局部静态变量等数据。
  • .rodata节 :保存了只读的数据,如一行C语⾔代码中的字符串。
  • .bss节 :为未初始化的全局变量和局部静态变量预留位置(对于未初始化的全局变量,我们没必要真正开辟空间,只需要在.bss里面描述出有多少未初始化的就行)
  • .symtab节 : Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。
  • .got.plt节 (全局偏移表 - 过程链接表):.got节保存了全局偏移表。.got节和.plt节⼀起提供了对导⼊的共享库函数的访问⼊⼝,由动态链接器在运行时进行修改。
Segment查看

我们还可以看到其他信息:
在这里插入图片描述
我们可以看到Program Header table对每个段的描述

你会不会很好奇,为什么可执行程序既有Section又有Segment
其实这只是ELF 文件提供 2 个不同的视图/视角来让我们理解这两个部分:

  • Section 是链接视图(Linking View),面向开发者/工具链。用于编译和链接阶段,供编译器、链接器和调试工具使用
  • Segment 是执行视图(Execution View),面向操作系统。用于程序加载和运行时,指导操作系统如何将文件映射到内存
ELF Header查看

用命令:

readelf -h test

在这里插入图片描述

  • 我们可以看到ELF Header保存着一些大小 / 入口信息,用于定位⽂件的其他部分
  • 系统通过Magic来判断文件是不是ELF的格式。Entry point(标识可执行程序的入口地址【虚拟地址】)

三,理解链接与加载

1. 静态链接与静态库加载

因为静态库就是都是.o文件打包的,并且静态库在形成可执行的时候,会把库中的函数实现直接拷贝一份到可执行里面。所以研究静态链接,本质上是在研究.o文件是如何链接的。

test.c文件内容

  1 #include "mystring.h"                                                                                                                                                                                        2 3 int main()4 {5     char* msg = (char*)"hello world\n";6     print(msg); // 调用自定义的print7     return 0;8 }

查看编译后的符号表

符号表

符号表用于记录了目标文件中定义和引用的符号相关信息,如:函数名、变量名、全局常量名等。
会用一个长字符串表来存储,像这样:
在这里插入图片描述
然后通过\0来划分他们,通过\0我们可以记录每个符号在串中的起始结束下标,就可以很快得到这个符号的名称。

readelf -s test.o

在这里插入图片描述
可以发现printUND的,就是:没有定义

查看反汇编目标文件的内容

 objdump -d test.o

在这里插入图片描述

  • 这里,调用print函数,但是它的跳转地址被设置成了 0
  • 这是因为:在编译 test.c 的时候,编译器知道有print函数(因为有声明)但是不知道具体的实现(即:不知道print在内存的哪里)。因此,编译器只能将这两个函数的跳转地址先暂时设为0
  • 链接的时候!.o文件被合并,就会修改call中不确定的地址。(这就是静态链接,也是外部符号的地址重定位步骤)

在这里插入图片描述

2. ELF加载与进程地址空间

—个ELF程序,在没有被加载到内存的时候,有没有地址呢?
答案:有的,有虚拟地址

—个ELF程序,在没有被加载到内存的时候,采用"平坦模式"(就是地址下标从 0 开始连续编址),对自己的代码和数据进行统⼀编址
在这里插入图片描述
最左侧的就是ELF的虚拟地址!严格意义上应该叫做逻辑地址(起始地址 + 偏移量)

  • 进程的mm_structvm_area_struct在进程刚刚创建的时候,就是用ELF的统一编址的信息来初始化的。(每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start, end]等范围数据)
  • 同时,记载到内存中的可执行文件也有对应的物理内存地址
  • 这样mm_struct的虚拟地址有了,程序的物理内存地址也有了,就可以填写页表了!!!
  • 所以:虚拟地址机制,不光OS要⽀持,编译器也要支持

静态链接总结

在这里插入图片描述
通过这张图梳理一遍静态链接:

  • 首先,ELF文件在没有加载到内存时,已经有了统一编址
  • 链接前,.o文件彼此不知道对方,所以没有办法call函数调用的具体地址
  • 在链接阶段,会把可执行程序中需要的静态库的库方法,拷贝一份给可执行程序。(这个时候,方法有了明确的地址,就可以进行地址重定位,把call的内容修改成具体的方法地址)
  • 当程序加载到内存中时,用统一编址初始化mm_struct,再结合实际物理内存地址,就可以构建好页表
  • 并将程序的入口Entry被传入到CPU的寄存器EIP中,就可以拿着EIP中的Entry进入程序并执行

也就是说:静态链接在链接阶段,已经完成了地址重定位操作,运行阶段已经不需要静态库了,所以是编译时(链接阶段)链接!

3. 动态链接与动态库加载

  • 对于动态链接,动态库并不会直接拷贝到可执行程序的代码中。
  • 所有程序是共用内存中的一份动态库代码的

那么,进程之间,又是如何共享库的呢?
在这里插入图片描述
先不挖细节,先说整体轮廓:

  • 动态库也是文件,需要独立加载到内存中,有自己的内存区域
  • 当动态库加载到内存中时,动态库的ELF格式会用来初始化进程mm_struct的共享区
  • 当在运行代码区的代码时,遇到了动态库的方法,就会从代码区跳转到共享区,得到对应方法的虚拟地址,然后就可以用虚拟地址通过页表映射找到内存中的代码了

看似没啥问题,但是,我们把目标放在从代码区跳转到共享区这一步:
如果要跳转,则代码区应该知道对应方法的内存地址。可是,如果动态库是独立的文件,只有程序加载的时候,动态库才能真真被加载ELF的虚拟内存地址里。才能有对应方法的地址。所以动态链接,也就被推迟到了加载时

所以:

  • 因为动态库也是独立的文件,也要加载到进程的mm_struct ,但是在加载之前,动态库还没有映射到mm_struct上(即:动态库的同一编址还没有用来初始化对应的mm_struct里面对应的区域)
  • 所以在编译链接时:可执行程序里面的代码段,就不知道对应动态库方法call。(无法像静态链接一样,直接填上方法具体的地址)
  • 只能等到程序加载到内存里以后,再填上。(这就是加载时链接
  • 但是,因为当可执行程序加载到内存中以后,代码区具有只读性,无法修改。所以我们需要借助一个中间层,来修改call的地址。
  • 这个中间层就是GOT(全局偏移量表),我们让GOT表位于.data区(可修改),每一个位置存放着:方法 + 对应方法的库名称【本质是:方法在库中的偏移量 + 库名称】
  • 而,原来的代码中call:GOT表的起始地址 + 要调用的方法在GOT表中的偏移量
  • 当我们加载程序时,动态库被加载到了mm_struct,就知道了动态库的虚拟起始地址。
  • GOT表就会被修改,里面每个位置存储的(通过:方法在库中的偏移量 + 库名称)就变成了对应方法的绝对虚拟地址【这样就相当于间接改了代码区的call】,此时页表也会被填写
  • 这就完成了重定位,完成了动态链接

和文件系统关联起来:
在这里插入图片描述
这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT

下面在谈几个更细节的知识

动态链接器

【以下内容由AI生成】
在这里插入图片描述
/lib64/ld - linux - x86 - 64.so.2这就是动态连接器,加载动态库、符号解析与重定位、处理库依赖、初始化库函数…都是由它完成的。

在C/C++程序中,当程序开始执行时,并不会直接跳转到 main 函数。实际上,程序的入口是 _start ,这是⼀个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。在 _start 函数中,会执⾏⼀系列初始化操作,其中就包括动态链接:
_start 函数会调⽤动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调⽤和变量访问能够正确地映射到动态库中的实际地址。

库间的依赖

库也会调⽤其他库!!库之间是有依赖的,如何做到库和库之间互相调⽤也是与地址⽆关的呢?
答:库中也有.GOT,和可执行⼀样。
在这里插入图片描述

PLT

PLT:延迟绑定(Lazy Binding)
作用:

  • 避免在程序启动时解析所有动态库函数(如果库函数很多的话,就很浪费时间,因为有些库函数可能没被使用)
  • 而是在函数首次被调用时才进行地址解析

更具体的比较复杂,就不讲述了。


🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

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

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

相关文章

第八节第三部分:认识枚举、枚举的作用和应用场景

认识枚举 枚举的概述 枚举的特点 枚举的应用场景 代码: 代码一:认识枚举 A(枚举) package com.d6_enum;public enum A {//注意:枚举类的第一行必须罗列的是枚举对象的名字X,Y,Z;private String name;public String…

Android framework 中间件开发(二)

上篇文章中我们讲述了怎么去开发中间件 Android framework 中间件开发(一) 这篇我们讲一下怎么打包中间件给外部应用使用 目录 1.新建项目 2.编写jar包代码 3.打包jar包 4.使用jar包 我们可以直接将系统编译出来的framework的jar包拿出来直接用,但是为了安全起见,防止用户调用…

FC7300 IO 无法正常输出高低电平问题排查

现象:Port、Dio配置正常的情况下,IO写或者翻转函数正常执行后,IO电平未按照预期切换电平。 排查: 第一步:检查PORTx_PCRy寄存器值: DWP: 域写保护:此字段指示允许哪个内核或 DMA 写…

7 个正则化算法完整总结

哈喽!我是我不是小upper~之前和大家聊过各类算法的优缺点,还有回归算法的总结,今天咱们来深入聊聊正则化算法!这可是解决机器学习里 “过拟合” 难题的关键技术 —— 想象一下,模型就像个死记硬背的学生&am…

如何有效的开展接口自动化测试?

🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 一、简介 接口自动化测试是指使用自动化测试工具和脚本对软件系统中的接口进行测试的过程。其目的是在软件开发过程中,通过对接口的自动化测试来提高测…

我设计的一个安全的 web 系统用户密码管理流程

作为一名有多年经验的前端,在刚开始学习web后端的时候,就对如何设计一个安全的 web 系统用户密码管理流程有很多疑问。之前自己也实践过几种方法,但一直觉得不是十分安全。 我们知道,用户在注册或登录界面填写的密码是明文的&…

炼丹学习笔记3---ubuntu2004部署运行openpcdet记录

前言 环境 cuda 11.3 python 3.8 ubuntu2004 一、cuda环境检测 ylhy:~/code_ws/OpenPCDet/tools$ nvcc -V nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2021 NVIDIA Corporation Built on Sun_Mar_21_19:15:46_PDT_2021 Cuda compilation tools, release 11.3…

在 Linux 系统中过滤文件中的字符串

在 Linux 系统中过滤文件中的字符串,可以使用多种命令行工具实现。以下是几种常见方法及详细说明: 一、使用 grep 命令(最常用) grep 是 Linux 中最强大的文本搜索工具,支持正则表达式。 基础语法: grep…

基于PXIE 总线架构的Kintex UltraScale 系列FPGA 高性能数据预处理板卡

基于PXIE 总线架构的Kintex UltraScale 系列FPGA 高性能数据预处理板卡 一款基于3U PXIE 总线架构的高性能数据预处理FMC 载板,板卡具有1 个FMC(HPC)接口,1 个X8 GTH 背板互联接口,可以实现1 路PCIe x8。板卡采用Xili…

Java 使用 PDFBox 提取 PDF 文本并统计关键词出现次数(附Demo)

目录 前言1. 基本知识2. 在线URL2.1 英文2.2 混合 3. 实战 前言 爬虫神器,无代码爬取,就来:bright.cn Java基本知识: java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)【Java项目】实战CRUD…

Vue百日学习计划Day16-18天详细计划-Gemini版

重要提示: 番茄时钟: 每个番茄钟为25分钟学习,之后休息5分钟。每完成4个番茄钟,进行一次15-30分钟的长休息。动手实践: DOM 操作和事件处理的理解高度依赖于实际编码。请务必在浏览器中创建 HTML 页面,并配…

SearchClassUtil

路径扫描工具SearchClassUtil,用于扫描指定包(XXXX)下的所有.class文件,并将它们的全限定类名(如tomcat.SearchClassUtil)收集到列表中返回。该工具使用递归文件遍历和反射机制,是实现 Spring 框…

云服务器的运用自如

云服务器的运用自如:从基础到高阶的实战指南(2025版) 云服务器作为数字化转型的核心工具,其灵活性和高效性已覆盖从个人开发者到企业级应用的广泛场景。以下是基于当前技术趋势的云服务器深度运用策略,涵盖核心应用、…

解密企业级大模型智能体Agentic AI 关键技术:MCP、A2A、Reasoning LLMs-docker MCP解析

解密企业级大模型智能体Agentic AI 关键技术:MCP、A2A、Reasoning LLMs-docker MCP解析 这里面有很重要的原因其中一个很其中一个原因是因为如果你使用docker的方式,你可以在虚拟环境下就类似于这个沙箱的这个机制可以进行隔离。这对于安全,…

快慢指针算法(Floyd 判圈算法)

快慢指针(又称龟兔赛跑算法)是一种常用的链表操作技巧,通过两个移动速度不同的指针遍历链表,用于解决链表中环检测、中点查找等问题。以下是其核心应用场景和实现方法: 1. 链表环检测 问题描述: 判断链表中…

独立开发者利用AI工具快速制作产品MVP

在当今快速发展的科技时代,独立开发者面临着前所未有的机遇与挑战。曾经需要花费数天甚至数周才能完成的产品MVP(Minimum Viable Product,最小可行性产品),如今借助强大的AI工具,可以在短短1小时内实现。 …

Spark处理过程-转换算子和行动算子

(一)RDD的处理过程 RDD经过一系列的“转换”操作,每一次转换都会产生不同的RDD,以供给下一次“转换”操作使 用,直到最后一个RDD经过“行动”操作才会真正被计算处理。 1.延迟。RDD中所有的转换都是延迟的&…

设置环境变量启动jar报

1. 环境变量设置 set PATHC:\Program Files\java17\jdk-17.0.9\bin;%PATH%2. 启动jar java -jar jar包名3. 记录原因 PATH路径前添加java执行文件路径才会管用。添加后可以试试以下命令 直接输入PATH 回车 PATH进行java版本测试 java -version

589. N叉树的前序遍历迭代法:null指针与栈的巧妙配合

一、题目描述 给定一个N叉树的根节点,返回其节点值的前序遍历结果。前序遍历的定义是:先访问根节点,再依次遍历每个子节点(从左到右)。例如,对于如下N叉树: 1/ | \3 2 4 / \ 5 6前序遍历结果…

显性知识的主要特征

有4个主要特征: 客观存在性静态存在性可共享性认知元能性