云计算百科
云计算领域专业知识百科平台

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

文章目录

    • 一、动态库为什么比静态库更常用
    • 二、动态库也是文件
    • 三、动态库加载的整体流程
    • 四、从磁盘 .so 到物理内存
    • 五、从物理内存到进程虚拟地址空间
    • 六、多个进程如何共享同一个动态库
    • 七、共享的是代码,不是什么都共享
    • 八、为什么动态库加载地址不固定
    • 九、使用 /proc 查看动态库映射
    • 十、使用 pmap 查看进程映射
    • 十一、动态链接和静态链接再对比一次
    • 十二、为什么动态库需要后续重定位
    • 十三、总结

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

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

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

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

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

  • 可执行文件变大。
  • 多个程序使用同一个库时,会重复保存同一份代码。
  • 假设系统里有 100 个程序都用到了 C 标准库。如果每个程序都把 libc 的代码静态链接进去,磁盘和内存都会浪费大量空间。

    动态库的思路是:

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

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

    二、动态库也是文件

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

    例如:

    ls -l /lib64/libc.so.6
    file /lib64/libc.so.6

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

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

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

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

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

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

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

    image.png

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

    libxxx.so => not found

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

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

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

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

    可以先粗略理解为:

    磁盘 libmyc.so
    -> 加载到物理内存中的若干页

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

    库文件的内容最终要对应到物理内存页。

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

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

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

    可以理解为:

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

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

    0x7f1000000000 ~ 0x7f1000010000

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

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

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

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

    进程 A 启动时:

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

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

    它只需要:

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

    于是形成:

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

    注意:

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

    image.png

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

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

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

    一般来说:

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

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

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

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

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

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

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

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

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

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

    这就带来一个关键问题:

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

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

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

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

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

    ./main

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

    然后查看:

    pidof main
    cat /proc/进程PID/maps

    你会看到类似:

    7f… r-xp … /lib64/libc.so.6
    7f… r–p … /lib64/libc.so.6
    7f… rw-p … /lib64/libc.so.6

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

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

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

    十、使用 pmap 查看进程映射

    也可以使用:

    pmap 进程PID

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

    例如:

    pmap $(pidof main)

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

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

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

    静态链接:

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

    动态链接:

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

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

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

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

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

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

    库内函数
    库内全局变量
    其他动态库函数
    主程序中的符号

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

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

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

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

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

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

    十三、总结

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

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

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

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux指南】动静态库系列(九):动态库如何进入进程地址空间:从磁盘 .so 到共享内存映射
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!