Linux 动态链接与动态库加载深度解析

Linux 动态链接与动态库加载深度解析
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 Linux 程序开发中,动态库是实现代码复用、减小程序体积、节省系统资源的核心技术,而动态链接则是程序运行时加载和使用动态库的底层机制。相较于静态链接将库代码直接合并到可执行程序的方式,动态链接把链接过程推迟到程序运行阶段,实现了多个进程共享同一份库代码,大幅提升了系统的资源利用率。本文将从进程与动态库的关联、动态链接的工作原理、GOT/PLT 机制等方面,深度解析 Linux 下的动态链接与动态库加载过程,带你吃透这一 Linux 开发的核心知识点。

一. 进程如何感知并加载动态库

动态库本质上是一个符合 ELF 格式的二进制文件,进程要使用动态库中的函数和数据,首先要让动态库被加载到内存并映射到进程的虚拟地址空间中,这是进程能访问动态库的前提。

1.1 进程对动态库的 “可见性”

进程本身并不能直接识别磁盘上的动态库文件,而是通过操作系统的文件操作和内存映射机制实现对动态库的访问。当程序运行时,操作系统会根据程序的依赖信息,找到对应的动态库文件并打开,随后通过mmap系统调用将动态库的代码段、数据段等映射到进程的虚拟地址空间的共享区,让进程在虚拟地址层面能 “看到” 动态库的内容。

在这里插入图片描述

1.2 多进程共享动态库的实现

Linux 系统中,多个依赖同一动态库的进程,并不会在物理内存中加载多份库的副本,而是通过虚拟内存的页表映射机制实现共享:

  • 动态库被加载到物理内存后,操作系统会为其建立一份物理内存映射;
  • 每个使用该动态库的进程,其页表会将虚拟地址空间共享区的一段地址,映射到这份物理内存;
  • 进程对动态库的访问,最终都会转化为对同一份物理内存的访问,从而实现物理内存层面的库共享。

这种机制极大节省了物理内存资源,也是动态链接相比静态链接的核心优势之一。

在这里插入图片描述

二. 动态链接的核心工作原理

动态链接的核心是将符号解析和地址重定位从编译链接阶段推迟到程序运行阶段。编译器编译生成可执行程序时,并不会将动态库的函数地址、变量地址直接写入程序,而只是记录下依赖的动态库和符号信息;当程序运行时,动态链接器会完成符号的解析和地址的重定位,让程序能正确调用动态库中的函数。

2.1 程序运行前的动态链接准备

C/C++ 程序的入口并非我们编写的main函数,而是链接器提供的_start函数,动态链接的初始化工作正是在_start函数中完成的,其流程如下:

  • 设置堆栈:为程序创建初始的堆栈环境,保证函数调用的栈操作正常;
  • 初始化数据段:将初始化的全局变量、静态变量从可执行程序复制到内存,清零未初始化的bss段;
  • 加载动态链接器:调用系统接口加载 Linux 的动态链接器ld-linux.so,由其负责后续的动态链接工作;
  • 解析库依赖:动态链接器读取可执行程序的动态段信息,解析出程序依赖的所有动态库(可通过ldd命令查看程序的库依赖);
  • 加载并映射动态库:按依赖顺序加载所有动态库,将其映射到进程的虚拟地址空间;
  • 调用__libc_start_main:完成信号处理、线程库初始化等工作后,最终调用main函数,将程序控制权交给用户代码。

其中,动态链接器是动态链接的核心执行者,Linux 下的ld-linux.so负责处理所有动态库的加载、符号解析和地址重定位。

$ ldd main.exe linux-vdso.so.1 =>(0x00007ffefd43f000) libc.so.6 => /lib64/libc.so.6 (0x00007f533380b000) /lib64/ld-linux-x86-64.so.2 (0x00007f5333bd9000)# ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表。
在这里插入图片描述

2.2 动态库的地址无关性:PIC 编译

动态库被加载到进程虚拟地址空间的地址是不固定的,操作系统会根据当前内存的使用情况,为动态库分配合适的虚拟地址区间。为了让动态库能在任意地址加载后都能正常运行,动态库必须采用位置无关代码(Position Independent Code,PIC) 编译,也就是编译时添加-fPIC参数。
PIC 的核心是相对编址:动态库中的函数调用、变量访问,均使用相对于当前指令的偏移量进行编址,而非绝对地址。这样无论动态库被加载到虚拟地址空间的哪个位置,只要根据偏移量计算,就能正确找到目标函数或变量,实现地址无关性。

✅️ 注意:我们的程序,怎么和库具体映射起来的动态库也是一个文件,要访问也是要被先加载,要加载也是要被打开的让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中
在这里插入图片描述

2.3 运行时的地址重定位:从符号到实际地址

当动态库被加载到进程的虚拟地址空间后,其虚拟起始地址就被确定了。动态链接器会完成两步核心工作,实现程序对动态库符号的访问:

  • 符号解析:根据可执行程序记录的符号名(如函数名、变量名),在已加载的动态库中找到对应的符号;
  • 地址计算:结合动态库的虚拟起始地址和符号在库中的相对偏移量,计算出符号的实际虚拟地址;
  • 地址重定位:将计算出的实际虚拟地址写入程序的指定位置,让程序能通过该地址调用访问动态库函数。

简单来说,程序调用动态库函数的地址,最终是动态库起始虚拟地址 + 函数在库中的相对偏移量,这也是进程能正确调用动态库函数的关键。

在这里插入图片描述

三. GOT/PLT:动态链接的核心实现机制

程序的代码段在内存中是只读的,无法直接在代码段中修改函数调用的地址,因此 Linux 通过全局偏移量表(GOT)过程链接表(PLT) 解决这一问题,实现了只读代码段的动态地址重定位,也是 PIC 的核心实现。

3.1 全局偏移量表(GOT)

GOT 是位于程序数据段(.data) 的一片可读写内存区域,其核心作用是存放动态库符号的实际虚拟地址。数据段的可读写属性,让动态链接器能在程序运行时,动态修改 GOT 表中的地址值。

  • GOT 表中的每一项,对应一个程序需要访问的动态库符号(函数或变量);
  • 编译时,编译器会为每个动态库符号在 GOT 表中分配一个条目,此时条目值为无效地址;
  • 程序运行时,动态链接器会将解析后的符号实际虚拟地址,写入 GOT 表对应的条目;
  • 程序访问动态库符号时,会先从 GOT 表中读取实际地址,再通过该地址进行访问。
  • 还有些没提到的东西可以看看下面图中的细节补充一下

同时,GOT 表与动态库的相对位置是固定的,程序可以通过CPU 的相对寻址找到 GOT 表,保证了地址无关性。需要注意的是,每个进程都有自己独立的 GOT 表,因为不同进程的动态库加载地址可能不同,进程间无法共享 GOT 表。

在这里插入图片描述

3.2 过程链接表(PLT):延迟绑定优化

动态链接器如果在程序启动时,就对所有动态库符号进行解析和重定位,会增加程序的启动时间 —— 因为程序运行过程中,很多动态库函数可能一次都不会被调用。为了解决这一问题,Linux 引入了延迟绑定(Lazy Binding) 机制,其核心实现就是过程链接表(PLT)
PLT 是一段位于程序代码段的桩代码(stub code),每个动态库函数对应一个 PLT 条目,其工作流程分为第一次调用和后续调用:

(1)函数第一次被调用

  • 程序调用动态库函数时,首先跳转到该函数对应的 PLT 条目;
  • PLT 条目会读取 GOT 表中对应的条目,此时 GOT 表中的值指向 PLT 条目的下一条指令;
  • 该指令会调用动态链接器的符号解析函数,动态链接器会解析出函数的实际虚拟地址,并将其写入 GOT 表对应的条目;
  • 动态链接器跳转到函数的实际地址,执行函数逻辑。

(2)函数后续被调用

  • 程序再次跳转到 PLT 条目时,会直接读取 GOT 表中的值,此时该值已经是函数的实际虚拟地址;
  • 程序直接跳转到该地址执行函数,不再经过动态链接器的解析,实现了调用的优化。

延迟绑定将符号解析的工作推迟到函数第一次被调用时,大幅减少了程序的启动时间,是 Linux 动态链接的重要优化手段。

✅️ 思路是:GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。
在这里插入图片描述

3.3 库间依赖的处理

动态库之间也存在依赖关系(如库 A 依赖库 B),其处理方式与程序依赖动态库一致:

  • 动态链接器会按依赖顺序加载所有的动态库,包括库的依赖库;
  • 每个动态库也都有自己独立的 GOT 表,动态链接器会依次解析所有库间的符号依赖,完善各个 GOT 表;
  • 库间的函数调用,同样通过GOT 表 + 相对偏移的方式实现,保证了库间调用的地址无关性。

所有动态库的 GOT 表完善后,整个程序的动态链接过程才算完成,程序才能正常运行。

由于GOT表中的映射地址会在运行时去修改,我们可以通过gdb调试去观察GOT表的地址变化。在这里我们只用知道原理即可,大家有兴趣的可以参考:使用gdb调试GOT

四. 动态链接与静态链接的核心对比

为了更清晰地理解动态链接的优势和特点,我们将其与静态链接做核心维度的对比,如下表所示:

对比维度静态链接动态链接
链接时机编译链接阶段程序运行阶段
可执行程序体积,包含所有库代码,仅记录库依赖和符号信息
内存占用,每个进程加载一份库代码,多进程共享物理内存中的库副本
磁盘占用,多个程序包含重复库代码,系统中仅存一份动态库文件
程序更新需重新编译链接整个程序仅更新动态库文件,无需重新编译程序
运行性能略高,无运行时链接开销略低,存在启动时的动态链接开销
兼容性,可执行程序独立运行依赖库版本,库版本不兼容可能导致程序崩溃
  • 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,生成最终的可执行文件。
  • 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
  • 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。

可以看到,动态链接以微小的运行性能开销,换来了系统资源的高效利用和程序的灵活更新,这也是 Linux 系统中绝大多数程序都采用动态链接的原因。

✅️ 小补充:那为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧?
静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。
这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。
动态链接到底是如何工作的??
首先要交代一个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:动态链接与动态库加载是 Linux 操作系统和程序编译链接体系的核心技术,其核心是将符号解析和地址重定位推迟到程序运行阶段,通过操作系统的内存映射、动态链接器的符号解析、GOT/PLT 的地址重定位,实现了代码的共享和程序的高效运行。本文从进程与动态库的关联出发,讲解了动态库的加载和共享机制,随后深入分析了动态链接的工作流程、PIC 编译的地址无关性,以及 GOT/PLT 的核心实现,最后对比了动态链接与静态链接的差异。理解这些底层机制,不仅能帮助我们解决开发中遇到的库依赖、符号未定义、库版本兼容等问题,更能让我们对 Linux 程序的运行流程有更深刻的认识。在实际 Linux 开发中,动态库的制作(-fPIC -shared)、库依赖的管理(ldd、LD_LIBRARY_PATH)、动态链接的调试(objdump、readelf)都是必备的技能,而这些技能的基础,正是对动态链接和动态库加载底层原理的理解。掌握这些内容,能让我们在 Linux 开发中更加游刃有余。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

[安卓] Kotlin中的架构演进:从MVC到MVVM

从Java到Kotlin,不仅仅是语法的升级,更是架构思维的进化。本文将带你全面掌握Kotlin在Android开发中三种核心架构模式:MVC、MVP、MVVM,揭示如何用Kotlin特性写出更优雅的架构代码。 引言:为什么Android开发者需要架构模式? 如果你有过Java Android开发经验,一定遇到过这些问题:Activity/Fragment代码臃肿(动辄上千行)、业务逻辑与UI逻辑混杂、测试困难、代码难以维护。架构模式正是为了解决这些问题而生的。而Kotlin,以其简洁的语法和强大的特性,让这些架构模式的实现变得更加优雅。 让我们从一个真实案例开始——用户登录模块,看看在不同架构模式下的演变: // 糟糕的无架构代码(Java风格)class BadLoginActivity :AppCompatActivity(){privatelateinitvar editUsername: EditText privatelateinitvar editPassword: EditText privatelateinitvar buttonLogin: Button

By Ne0inhk
【Kafka进阶篇】深入Kafka内部:日志存储的设计思路,藏着中间件高性能的真相

【Kafka进阶篇】深入Kafka内部:日志存储的设计思路,藏着中间件高性能的真相

🍃 予枫:个人主页 📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 💻 Debug 这个世界,Return 更好的自己! 引言 做分布式开发的同学,几乎都用过Kafka,但多数人只停留在“生产者发消息、消费者收消息”的表层使用,很少深究:百万级消息并发下,Kafka如何快速定位目标消息?底层的.log、.index、.timeindex文件各司其职,又是如何配合实现高效读写的?今天就从物理层面拆解Kafka日志存储与索引机制,吃透这部分,不仅能搞定面试难点,更能在生产环境中精准优化Kafka性能。 文章目录 * 引言 * 一、前言:为什么要搞懂Kafka日志与索引机制? * 二、核心前提:Kafka日志的“分段存储”设计 * 三、深度拆解:三种核心文件的作用与结构 * 3.1 .log文件:消息的“真正存储载体” * 关键细节: * 3.2

By Ne0inhk

PostgreSQL:详解 PostGIS 地理信息数据处理

更多内容请见: 《深入掌握PostgreSQL数据库》 - 专栏介绍和目录 文章目录 * 一、PostGIS 简介 * 1.1 什么是 PostGIS? * 1.2 PostGIS 能做什么? * 二、安装与启用 PostGIS * 2.1 安装方式(以 Ubuntu 为例) * 2.2 验证安装 * 三、核心数据类型 * 3.1 geometry(几何类型) * 3.2 geography(地理类型) * 3.3 常见几何子类型(OGC 标准) * 四、空间数据表示与输入 * 4.1 WKT(Well-Known Text)

By Ne0inhk
Flutter 组件 fam 适配鸿蒙 HarmonyOS 实战:文件资产监控,构建分布式媒体管理与全场景资源分发治理架构

Flutter 组件 fam 适配鸿蒙 HarmonyOS 实战:文件资产监控,构建分布式媒体管理与全场景资源分发治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 fam 适配鸿蒙 HarmonyOS 实战:文件资产监控,构建分布式媒体管理与全场景资源分发治理架构 前言 在鸿蒙(OpenHarmony)生态迈向万物智联、涉及海量分布式文件同步、社交应用图片即时刷新及严苛的沙箱存储审计背景下,如何实现一套既能覆盖系统级文件变动感知、又能保障低功耗运行且具备“亚秒级”事件回调能力的“文件监控中心”,已成为决定应用数据发现速率与存储交互稳健性的基石。在鸿蒙设备这类强调分布式文件系统(HMDFS)协同且存储分区高度隔离的场景下,如果应用依然采用低效的定时轮询扫描,由于由于 I/O 资源的无效损耗,极易由于由于“电量泄露”导致鸿蒙应用在后台由于由于由于由于后台任务限制而被系统强制挂起。 我们需要一种能够解耦物理路径、支持递归监听且符合鸿蒙事件驱动模型的文件监控方案。 fam(File Asset Monitor)为 Flutter 开发者引入了“存储感知”范式。它不是简单的 File 操作扩展,

By Ne0inhk