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

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

系列第三篇,时间跨度 2026.07.18 -- 07.20,主题正式从「文件系统」转入「库文件」。

主线是三个问题:

① 库到底是什么?怎么造、怎么用、链接期和运行期分别发生了什么?

② 二进制文件凭什么能被加载?------ 走进 ELF 格式

③ 进程的虚拟地址空间从哪来?------ Program Header Table 是那张「段蓝图」


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

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

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

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

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

几条铁律:

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

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

bash 复制代码
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),但拷到任何地方都能跑,不依赖外部库。


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

bash 复制代码
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 从哪开始执行?

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

这就是虚拟地址空间落地为物理内存的完整闭环。


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

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

加载到 共享区(mmap 区)

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

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

  1. 虚拟地址在 ELF 里(Program Header Table);
  2. 代码和数据在节(Section)里
  3. 入口地址在 ELF Header 里
  4. 没加载时物理地址为空,访问即触发异常(缺页),内核据此把对应内容从磁盘载入。

顺便回扣文件系统:缺页时内核要找到「文件的第几个块」,走的正是我们前面学的那条链------路径缓存拿 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 翻译 → 真正跑起来

四条最值得记住的结论:

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

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

相关推荐
指尖的爷1 小时前
【地狱级难度】ubuntu系统GOCV编译安装
linux·运维·ubuntu
xx~t1 小时前
嵌入式——ARM——汇编2
linux·汇编·arm开发·嵌入式硬件·启动代码
智购科技自动贩卖机1 小时前
自动售货机嵌入式系统时钟同步与时间管理实战:从RTC校准到断网时间保持的工程实践
大数据·linux·数据库·人工智能·yolo
BizzZ_1 小时前
Linux(2)——权限
linux
j7~1 小时前
【C++微服务项目开发脚手架】(环境篇)虚拟机 + Docker + MySQL/Redis/RabbitMQ/ES/etcd/FastDFS 全套配齐
linux·c++·ubuntu·docker·vmware·项目开发·微服务脚手架
Biomamba生信基地1 小时前
安全访问:FutureTerminal之Jupyter配置及Linux终端、Python与R环境访问
linux·远程连接·生物信息学
M78佐菲1 小时前
ARM学习笔记(三)
linux·arm开发·笔记·嵌入式硬件·学习
邪修king1 小时前
Re:Linux 系统篇(三十一):库的制作与原理Chapter2:静态链接与程序加载 —— 从磁盘 ELF 到运行中进程的完整旅程
android·linux·运维·开发语言
云计算练习生2 小时前
什么是内核?操作系统内核到底管哪些事
linux·windows·操作系统·内核·shell