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

从.o到ELF-静态库动态库与可执行文件的生命周期

从 .o 到 ELF:静态库、动态库与可执行文件的完整生命周期

系列第三篇,时间跨度 2026.07.18 – 07.20,主题正式从「文件系统」转入「库文件」。
主线是三个问题:
① 库到底是什么?怎么造、怎么用、链接期和运行期分别发生了什么?
② 二进制文件凭什么能被加载?—— 走进 ELF 格式
③ 进程的虚拟地址空间从哪来?—— Program Header Table 是那张「段蓝图」


一、库的本质:一堆 .o 的打包

编译四步走:预处理 → 编译 → 汇编 → 链接。汇编之后得到 .o(目标文件,准确叫可重定位目标文件),它是「变成标准二进制之前的最后一步」。

而库,就是把一堆 .o 打包起来:

# 静态库:ar 归档工具,把当前目录所有 .o 打包
ar -rc libmystdio.a *.o

# 动态库:gcc -shared 把多个 .o 链接成共享库
gcc -shared -o libmystdio.so *.o # 每个 .o 需先用 -fPIC 编译

几条铁律:

  • 命名规范:库文件名必须以 lib 开头、以 .a(静态)或 .so(动态)结尾——这不是迷信,是 -l 参数的自动补全规则要求的;
  • 库绝对不能包含 main 函数:一个进程只有一个入口,main 属于可执行文件,不属于库;
  • 给别人交付库 = 库文件 + 头文件:头文件就是这份库的「操作手册」。

  • 二、静态库:链接期就「焊死」进可执行文件

    gcc -c mystdio.c -o mystdio.o # 编译成 .o
    ar -rc libmystdio.a mystdio.o # 打包成静态库

    gcc -o main main.c -I./include -L./lib -lmystdio

    使用时的三个参数,各司其职:

    参数作用
    -I(大写 i) 头文件搜索路径(编译期用)
    -L 库文件搜索路径(链接期用)
    -l(小写 L) 指定库名,不加 lib、不加后缀

    为什么头文件不用指明路径,库却必须指明? 这是初学时最别扭的一点:

    • 头文件是你在代码里 #include 的,名字你已经写死了(#include "mystdio.h"),编译器按 < > 先系统目录、" " 先本地再系统、最后 -I 的顺序去找就行;
    • 而库文件代码里根本没提过它的名字!一个 .o 集合的名字是用户(你)起的,编译器无从猜测「这个文件该链谁」——所以必须显式给 -l。而且同一个路径下可能有多个库,不指明就会歧义。

    静态链接的最终形态:库里的函数代码被直接复制进 main 的可执行文件。这就是为什么静态链接出来的程序体积大(几 MB),但拷到任何地方都能跑,不依赖外部库。


    三、动态库:链接期只「记名字」,运行期才「找库」

    gcc -fPIC -c mystdio.c -o mystdio.o # -fPIC:生成位置无关代码
    gcc -shared -o libmystdio.so mystdio.o
    gcc -o main main.c -I./include -L./lib -lmystdio
    ./main # ← 经常在这里翻车:找不到库!

    -fPIC(Position Independent Code,位置无关代码) 是动态库的关键:库要被加载到进程地址空间里不确定的位置(共享区),所以里面的代码不能写死绝对地址,必须全部用相对寻址——让 .o 在编译期就采用相对地址。

    3.1 为什么运行时找不到库?

    因为动态库有两道关卡,静态库只有一道:

    链接期运行期
    静态库 ✅ 需要库文件(.a) ❌ 不需要(已复制进可执行文件)
    动态库 ✅ 需要库文件(识别名字) ✅ 还需要库文件
    • 链接期:链接器把「我用了哪个动态库」这个名字写进可执行文件的一个段里(这就是后面要讲的 .dynamic / DT_NEEDED);
    • 运行期:系统的动态加载器 ld.so 拿这个名字,去几个默认路径查找 .so,找不到就报 error while loading shared libraries。

    3.2 运行期找库的四条路(本质是四合一)

    方式做法说明
    ① 环境变量 LD_LIBRARY_PATH=/my/lib 最常用,等价于「库的 PATH」
    ② 系统默认路径 把库拷到 /lib64、/usr/lib 等 正规做法(需要权限)
    ③ 配置文件 /etc/ld.so.conf + ldconfig 本质仍是方式①,只是集中登记
    ④ 软链接 在默认目录建软链接指向真实库 本质仍是方式①,软链接只存路径字符串

    记住这条类比:我们执行命令靠 PATH 找可执行文件,程序在运行期则靠 LD_LIBRARY_PATH 找动态库。


    四、静态库和动态库同时存在,用哪个?

    答案很干脆:有动态库就用动态库,没有才退回静态库。

    理由是体积账:同一份功能,静态库版本可能 8000 字节、动态库版本 800000 字节(相差上百倍)——动态库代码只有一份,被所有进程共享,当然更划算。

    要强制静态链接,加 -static。

    静态链接动态链接
    时机 链接期完成全部工作 链接期记名字,运行期装载
    可执行文件体积 大(含库代码)
    运行期依赖 需要 .so 存在
    内存/磁盘占用 每份程序各存一份 多进程共享一份
    升级库 需重新链接程序 换 .so 即可(无需重编)

    五、走进 ELF:二进制文件的格式

    可执行文件不是「随意堆在一起的 01」,它有严格的格式,这个格式叫 ELF(Executable and Linkable Format)。以下都是 ELF 文件:

    • .o 可重定位目标文件;
    • 可执行文件(a.out);
    • 动态库 .so;
    • core dump 文件。

    一句话总结链接:链接的本质,就是把一堆 ELF 合并为一个 ELF。

    5.1 ELF 的四大组成部分

    结构作用查看命令类比
    ELF Header 描述整个文件结构;最重要的字段是 Entry Point Address(入口地址) readelf -h 磁盘的 Super Block
    Section Header Table 描述所有节(磁盘视角) readelf -S 目录索引
    Program Header Table 描述所有段(内存/虚拟地址空间视角) readelf -l 段蓝图
    节内容本体 真正存放代码与数据

    为什么 ELF 也需要这么一套「管理结构」?跟磁盘一模一样:要管理磁盘就需要 Super Block 和 GDT;要管理这些节,就需要 ELF Header 和两张 Header Table。 先描述,再组织——又一次印证。

    5.2 常见的节(Section)

    节名内容
    .text 程序代码(只读、可执行)
    .rodata 只读常量
    .data 已初始化的全局变量
    .bss 未初始化的全局变量(不占文件空间,只占内存)
    .symtab 符号表(函数/变量名与地址的对应)
    .strtab 符号名字符串表
    .shstrtab 节名字符串表
    .debug* 调试信息
    .rela.dyn / .plt / .init 重定位、延迟绑定、初始化等

    5.3 节(Section)vs 段(Segment)

    这是整篇笔记最核心的一组区分:

    节,是描述 ELF 磁盘文件的概念;段,是描述内存(进程地址空间)的概念。

    那为什么要有两套?关键在于内存比磁盘珍贵得多:

    • 内存(和外存 IO)都以 4KB 为基本单位,一次换入 4KB;
    • 如果按节加载,.text、.rodata、.data 这些小节各自占不满一个 4KB,会造成大量内部碎片、浪费宝贵的物理内存;
    • 于是加载器把「权限相同、类型相同」的节合并成大段,凑满 4KB 的整数倍,最大化内存利用效率。

    节(磁盘,细粒度) ──合并规则(权限一致 / 类型一致)──▶ 段(内存,4KB 对齐)
    .text + .rodata ────────────────────────────────▶ 代码段(r-x)
    .data + .bss ────────────────────────────────▶ 数据段(rw-)

    这条规则同时也解释了为什么代码段是只读的:它由一批只读节合并而成,权限是合并时就定好的。

    这也是为什么 readelf -S 能看到几十个节,而 readelf -l 只有寥寥几个段——多个节,合成一个段。


    六、重谈进程地址空间:虚拟地址是从哪来的?

    6.1 先回答问题:程序还没加载,有没有地址?

    有!一定有虚拟地址。

    • 代码反汇编时,每条 call 后面跟着的就是函数地址——这些地址是编译器编出来的;
    • 32 位系统下,虚拟地址空间从全 0 到全 1(4GB)平坦编址(平坦模式 = 0 + 偏移量,逐行编址);
    • 但物理地址必须等加载进内存才有:没加载 → 虚拟地址存在、物理地址为 0(空)→ 一旦访问触发缺页异常,再由内核把代码和数据从磁盘载入并建立映射。

    所以:

    虚拟地址空间不只是操作系统的事,它一半是编译器的活。

    6.2 Program Header Table:编译器留下的「段蓝图」

    创建进程的顺序是:先创建 PCB/mm_struct 等数据结构,再加载数据与代码。

    那 struct mm_struct(进程的虚拟地址空间)拿什么初始化?答案:

    读 ELF Header → 定位 Program Header Table
    ↓ Program Header Table 里记录了每个段的虚拟地址、权限、对齐等
    用这份「段蓝图」初始化进程的 mm_struct

    虚拟地址空间就此建立(段与段的边界、权限全部就位)

    Program Header Table 是怎么来的?编译器生成的:它在生成 ELF 时,已经按「权限 + 类型」的规则总结好了「哪些节合并成哪个段、这个段占哪些虚拟地址」,于是才有了 .text 段的第一个虚拟地址等一堆地址信息。

    6.3 一张表说清「谁记载了什么」

    需要的信息存在 ELF 的哪里
    整个文件的架构 ELF Header
    程序入口(main 起始虚拟地址) ELF Header 的 Entry Point Address
    代码与数据本体 节(Section) 里
    段的虚拟地址蓝图(mm_struct 的来源) Program Header Table
    用了哪些动态库 .dynamic 段(链接器写入)

    6.4 CPU 从哪开始执行?

  • 内核把 Entry Point Address 写进 PC / RIP 寄存器——它就是 .text 段的第一个虚拟地址;
  • CPU 按 RIP 取指执行,call 把下一个函数的起始虚拟地址取来,rip 计数器加上指令长度继续往下走,压栈保存返回地址;
  • 取到的始终是虚拟地址,交给硬件 MMU;
  • MMU 通过 CR3 寄存器找到当前进程的页表,把虚拟地址翻译成物理地址;
  • 32 根地址总线进去、出来,CPU 拿到真正的物理地址去取数据。
  • 这就是虚拟地址空间落地为物理内存的完整闭环。


    七、动态库是怎么加载的?

    最后一环:动态库被加载到进程地址空间的哪里?

    加载到 共享区(mmap 区)。

    正因为在共享区,同一个 libc.so 的物理内存页可以被成百上千个进程共同映射——这就是动态库又叫 共享库(Shared Object,.so) 的原因,也是它节省内存的根源。

    而不论是动态库还是可执行文件:

  • 虚拟地址在 ELF 里(Program Header Table);
  • 代码和数据在节(Section)里;
  • 入口地址在 ELF Header 里;
  • 没加载时物理地址为空,访问即触发异常(缺页),内核据此把对应内容从磁盘载入。
  • 顺便回扣文件系统:缺页时内核要找到「文件的第几个块」,走的正是我们前面学的那条链——路径缓存拿 inode → inode 的 i_block[] 算块号 → 读磁盘。两套知识在这里合流了。


    八、小结

    .c ──编译──▶ .o(可重定位目标文件,ELF)
    .o ──ar──▶ .a(静态库) ──链接期复制进可执行文件──▶ 无运行期依赖,体积大
    .o ──shared+fPIC──▶ .so(共享库)──链接期只记名字、运行期由 ld.so 找──▶ 体积小、可共享
    一堆 ELF ──链接──▶ 一个 ELF(可执行文件)
    可执行文件 ──加载──▶ 按 Program Header Table 的段蓝图建 mm_struct
    → Entry Point 入 RIP → MMU 翻译 → 真正跑起来

    四条最值得记住的结论:

  • 库是 .o 的打包,lib 前缀 + 后缀是 -l 自动补全的规则;
  • 链接期 vs 运行期:静态库只关心链接期,动态库两期都要管,运行期靠 LD_LIBRARY_PATH 这一套;
  • 节是磁盘的、段是内存的,合并规则是「权限相同」,目的是填满 4KB 页;
  • 虚拟地址来自 ELF,物理地址来自加载;mm_struct 是 Program Header Table 喂出来的。

  • 本文基于 2026-07-18 ~ 07-20 的学习笔记(静态库/动态库生成、动静态库双向、目标文件 ELF、Program Header Table 来源机制、回归动态库加载)整理。后续笔记(7.21 ELF 动态库、7.28 动态库为什么是共享的)继续深入。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从.o到ELF-静态库动态库与可执行文件的生命周期
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!