【Linux指南】动静态库系列(九):动态库如何进入进程地址空间:从磁盘 .so 到共享内存映射

文章目录

前面我们已经知道,动态库 .so 不是在编译链接阶段完整拷贝进可执行程序,而是在程序运行时被动态链接器加载。

那么问题来了:磁盘上的 libxxx.so 到底是怎么进入进程地址空间的?多个程序都依赖同一个动态库时,系统真的会给每个进程都复制一份库代码吗?为什么说动态库可以节省内存?

这篇文章就围绕"动态库如何映射到进程地址空间"展开。

一、动态库为什么比静态库更常用

静态链接会把用到的库代码合并进可执行文件。这样程序可以独立运行,但也带来两个问题:

  1. 可执行文件变大。
  2. 多个程序使用同一个库时,会重复保存同一份代码。

假设系统里有 100 个程序都用到了 C 标准库。如果每个程序都把 libc 的代码静态链接进去,磁盘和内存都会浪费大量空间。

动态库的思路是:

text 复制代码
把公共代码单独放在 .so 文件中;
程序运行时再加载;
多个进程尽量共享同一份库代码。

这就是动态库广泛使用的重要原因。

二、动态库也是文件

先明确一点:动态库不是内存里凭空出现的东西,它首先是磁盘上的一个普通文件。

例如:

bash 复制代码
ls -l /lib64/libc.so.6
file /lib64/libc.so.6

你会发现它是一个 ELF shared object。

所以加载动态库的第一步,仍然是文件操作:

text 复制代码
找到 .so 文件
打开 .so 文件
读取 ELF 信息
把需要的 segment 映射进内存

只不过这个过程通常由动态链接器和内核协作完成,程序员平时不直接感知。

三、动态库加载的整体流程

一个动态链接程序启动时,大致流程如下:

text 复制代码
1. 内核加载主程序 ELF
2. 发现主程序需要动态链接器
3. 动态链接器读取主程序依赖的 .so 列表
4. 找到每个 .so 文件
5. 将 .so 的代码段、数据段等映射到进程地址空间
6. 完成必要的符号解析和重定位
7. 进入程序 main 函数

如果第 4 步找不到库,就会出现我们前一篇讲的:

text 复制代码
libxxx.so => not found

如果能找到库,下一步就是把库映射到进程地址空间中。

四、从磁盘 .so 到物理内存

当程序依赖 libmyc.so 时,动态链接器会找到磁盘上的库文件。

然后系统会把库中需要加载的 segment 放入物理内存,尤其是代码段 .text 对应的内容。

可以先粗略理解为:

text 复制代码
磁盘 libmyc.so
    -> 加载到物理内存中的若干页

现代系统很多时候还会用文件映射、按需分页等机制,并不是一开始就把整个库所有内容都读入内存。但从理解动态库共享的角度,我们先抓住核心:

text 复制代码
库文件的内容最终要对应到物理内存页。

五、从物理内存到进程虚拟地址空间

进程不能直接使用物理地址。每个进程看到的是自己的虚拟地址空间。

所以动态库加载后,还需要在当前进程的虚拟地址空间中划出一段区域,用来映射这份库。

可以理解为:

text 复制代码
进程虚拟地址空间中的一段共享区
    -> 通过页表
    -> 映射到物理内存中的 libmyc.so 代码页

例如,进程 A 中 libmyc.so 可能映射到:

text 复制代码
0x7f1000000000 ~ 0x7f1000010000

这只是进程 A 自己看到的虚拟地址范围。

真正的物理内存在哪里,进程并不直接关心。CPU 会通过页表完成转换。

六、多个进程如何共享同一个动态库

假设进程 A 和进程 B 都依赖 libmyc.so。

进程 A 启动时:

text 复制代码
磁盘 libmyc.so -> 物理内存代码页
进程 A 的虚拟共享区 -> 映射到这些物理页

进程 B 启动时,如果系统发现这份库的代码页已经在物理内存中,就不必再加载一份完整代码。

它只需要:

text 复制代码
给进程 B 分配自己的虚拟共享区
让进程 B 的页表也映射到同一批物理代码页

于是形成:

text 复制代码
进程 A 虚拟地址 0x7f1000000000 -> 同一份物理库代码
进程 B 虚拟地址 0x7f2000000000 -> 同一份物理库代码

注意:

text 复制代码
两个进程的虚拟地址可以不同;
但它们可以映射到同一份物理内存。

这就是动态库节省内存的关键。

七、共享的是代码,不是什么都共享

动态库中并不是所有内容都能随便共享。

一般来说:

text 复制代码
只读代码段可以共享;
只读数据可以共享;
可写数据通常不能直接在进程间共享。

原因很简单:如果动态库中的全局变量被多个进程共享,那进程 A 修改变量会影响进程 B,这显然不符合普通进程隔离原则。

所以动态库的可写数据部分通常会为每个进程提供独立映射,或者通过写时拷贝等机制保证进程隔离。

动态库节省内存主要依赖:

text 复制代码
代码段只读,可以被多个进程共享。

这也是为什么动态库代码要尽量做到位置无关,不能随便修改代码段本身。

八、为什么动态库加载地址不固定

每个进程都有自己的虚拟地址空间。不同进程加载了不同的程序、不同的库,地址空间中空闲区域也不同。

所以动态链接器不能假设所有进程都把 libmyc.so 放到同一个虚拟地址。

它通常会在当前进程地址空间中选择一段合适的空闲区域,把动态库映射进去。

这就带来一个关键问题:

text 复制代码
动态库如果被加载到任意地址,里面的函数调用和全局变量访问怎么保证正确?

这就是下一篇 GOT/PIC 要解决的问题。

九、使用 /proc 查看动态库映射

Linux 提供了 /proc/[pid]/maps,可以查看进程地址空间映射。

先运行一个程序,让它保持一段时间:

bash 复制代码
./main

如果程序很快退出,可以在代码中加 sleep(100)。

然后查看:

bash 复制代码
pidof main
cat /proc/进程PID/maps

你会看到类似:

text 复制代码
7f... r-xp ... /lib64/libc.so.6
7f... r--p ... /lib64/libc.so.6
7f... rw-p ... /lib64/libc.so.6

这些行说明 libc.so.6 的不同部分被映射到了进程虚拟地址空间中,并且权限不同:

text 复制代码
r-xp:可读可执行,通常对应代码段
r--p:只读
rw-p:可读可写,通常对应数据段

这能非常直观地看到动态库确实进入了进程地址空间。

十、使用 pmap 查看进程映射

也可以使用:

bash 复制代码
pmap 进程PID

它会以更简洁的形式展示进程内存映射。

例如:

bash 复制代码
pmap $(pidof main)

可以看到主程序、堆、栈、动态库等区域。

学习动态库时,/proc/[pid]/maps 是非常有价值的观察工具。

十一、动态链接和静态链接再对比一次

静态链接:

text 复制代码
库代码在链接阶段进入可执行文件;
程序运行时不再找库;
多个程序各自包含一份库代码。

动态链接:

text 复制代码
可执行文件记录依赖;
程序运行时加载 .so;
多个进程可以共享同一份库代码物理页。

动态链接本质上把一部分链接工作推迟到了程序加载和运行阶段。

这带来灵活性,也带来复杂性。

十二、为什么动态库需要后续重定位

动态库被映射到进程地址空间后,它的加载地址才真正确定。

但库代码中可能需要访问:

text 复制代码
库内函数
库内全局变量
其他动态库函数
主程序中的符号

有些地址必须在加载后才能知道。

所以动态链接器还要做符号解析和重定位,让这些引用指向正确位置。

问题是:代码段通常是只读且共享的,不能每个进程都直接修改代码段里的地址。

因此动态链接需要一种更巧妙的机制:

text 复制代码
把可能变化的地址放到可写表中,代码通过查表跳转。

这张表就是 GOT,全局偏移表。

十三、总结

动态库加载不是简单地"把 .so 拷贝进进程"。更准确的理解是:

text 复制代码
动态链接器找到 .so
内核把 .so 的 segment 映射进进程虚拟地址空间
多个进程可以通过各自页表映射到同一份物理代码页
动态链接器再完成必要的符号解析和重定位

动态库节省内存的关键在于:只读代码段可以共享。

但动态库加载地址不固定,这就要求动态库不能把绝对地址写死在代码里。下一篇我们继续讲 GOT 与 PIC,看动态库如何做到"加载到哪里都能运行"。

相关推荐
脚踏实地,坚持不懈!1 小时前
Linux 内核源码解析:从 secondary_startup_64 到 pick_eevdf 的完整调用栈分析
android·linux·arm开发
TeamDev1 小时前
JxBrowser 9.5.2 版本发布啦!
java·前端·windows·chromium·混合应用·jxbrowser·浏览器控件
鬼手点金1 小时前
Claude Code示范案例-进阶学习路径
java·服务器·前端·学习·计算机视觉·前向传播
浪子明X1 小时前
用 CMake Presets 固定跨平台构建:从本地开发到 CI 的依赖一致性实践
java·spring·ci/cd
茉莉玫瑰花茶1 小时前
GO [ 文件 ]
服务器·golang
xbzb1 小时前
Linux iSCSI 存储部署与 CHAP 认证完全指南
linux·服务器·iscsi·chap·共享硬盘
苏supper2 小时前
记一次Nacos鉴权报错unknown user,403排查|SecurityProxy源码,fastjson2扩展包缺失
java·后端
IT大白鼠2 小时前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 5 篇 · 多模态与虚拟化AI 能「看」图:多模态与虚拟化管理
linux·运维·人工智能
\光辉岁月/2 小时前
6.mybatisplus学习-BaseMapper、Service、常用注解
java·mybatis·mybatisplus