1. 程序从磁盘到内存:ELF 与运行时加载的基本原理
理解 C/C++ 程序的内存布局,不能从".text 在哪里、.data 有多大"这种结果入手,而必须先回答一个更根本的问题:一个可执行文件是如何从磁盘上的字节,变成内存中可运行的程序的。只有把这个过程想清楚,后面讨论任何段(section)才不会流于记忆名词。
1.1 ELF 文件的双重视角:Section 与 Segment 的本质差异
ELF(Executable and Linkable Format)并不是只服务于"程序运行",而是同时服务于编译、链接、加载这三个阶段,因此它天然具有两套视角:
- Section(节):面向 编译器 / 链接器 的逻辑划分
- Segment(段):面向 内核 / 动态加载器 的运行时映射单元
很多初学者会困惑:既然已经有 .text、.data 这些 section,为什么还需要 segment?原因在于:
section 解决的是"语义归类",segment 解决的是"如何映射进内存并赋予权限"。
从设计哲学上看,这种分层非常接近一句话所描述的状态:"人们往往以为自己看到的是世界本身,其实看到的是经过抽象后的模型。"ELF 的 section 正是编译阶段的抽象模型,而 segment 才是内核真正相信的"现实"。
Section 的核心特征
- 数量多、粒度细
- 用于描述"这是什么"(代码、只读数据、初始化表、调试信息)
- 只存在于文件语义中,运行时不直接使用
Segment 的核心特征
- 数量少、粒度粗
- 用于描述"如何加载"(地址、权限、是否映射文件)
- 由
Program Header描述,内核只看 segment
1.2 从 execve 开始:内核如何加载一个 ELF 程序
当用户空间调用 execve() 启动一个程序时,内核并不会"把整个 ELF 文件拷贝到内存"。实际过程可以拆解为几个关键步骤:
- 解析 ELF Header
- 校验魔数、架构、ABI
- 找到 Program Header Table 的位置
- 遍历 Program Header(PT_LOAD)
- 每一个
PT_LOAD描述一个需要映射进进程虚拟地址空间的区域 - 内核根据其中的:
- 虚拟地址(VADDR)
- 文件偏移(OFFSET)
- 文件大小(FILESZ)
- 内存大小(MEMSZ)
- 权限(R/W/X)
来建立映射关系
- 每一个
- 建立虚拟内存映射
- 对有文件内容的部分:使用 文件映射(file-backed mmap)
- 对超过文件大小的部分:使用 匿名页并清零
- 跳转到入口点(_start)
- 控制权交给运行时启动代码(crt)
这一点非常关键:内核只负责"把段映射好",并不会理解 C/C++ 的语义。构造函数、异常表、虚表是否有意义,完全是运行时(libc / libstdc++ / runtime)自己的事情。
1.3 "加载"并不等于"拷贝":三种典型内存来源
理解 ELF 加载时,最容易混淆的是:
"这些段的数据是不是都被拷贝进内存了?"
答案是否定的。现代系统中至少存在三种不同的数据进入内存的方式:
| 数据来源方式 | 是否占文件空间 | 是否立即分配物理页 | 典型示例 |
|---|---|---|---|
| 文件映射(file-backed) | 是 | 否(按需) | .text, .rodata, .data |
| 匿名零页(zero-filled) | 否 | 否(按需) | .bss, .tbss |
| 运行时主动访问 | 是 | 访问时触发 | .init_array, .eh_frame |
这里的"按需"是理解性能和内存占用的关键:
即便 .text 映射完成,只要某个函数从未被执行,对应的物理页也可能从未真正加载。
1.4 为什么 Section 的布局顺序不等于内存布局
在 ELF 文件中,你可能会看到 .init_array、.rodata、.data 在文件里的顺序非常接近,但这并不意味着它们在内存中"挨着放"。真正决定内存布局的是:
- Segment 的虚拟地址区间
- 对齐要求(page alignment)
- RELRO、PIE 等安全机制
因此,讨论 section 的"位置",一定要回到 segment 层面。脱离 PT_LOAD 谈 section 在哪里,本质上是在讨论一个不会被内核采纳的视角。
小结
- ELF 同时服务于编译、链接和运行,天然存在 section / segment 两套视角
- 内核只关心 segment,不关心 section
- "加载"更多是建立映射关系,而不是拷贝数据
- 后续所有 C / C++ 内存区域,本质上都是:
先被归类为 section,再被折叠进若干个 segment 中统一加载
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C / C++ 通用的核心内存区域及其加载方式
在理解了 ELF 的加载模型之后,可以回到大家最熟悉、也最容易被误解的一组概念:.text、.data、.bss 等内存区域。它们之所以"经典",并不是因为名字固定,而是因为它们对应了程序中最稳定、最基础的语义分类。无论是 C 还是 C++,只要生成 ELF,可执行文件最终都要被压缩进这些区域中。
2.1 代码段 .text:程序真正可执行的部分
.text 段存放的是函数的机器指令,是程序可以被 CPU 执行的唯一区域。从加载角度看,它通常具有以下特征:
- 归属于
PT_LOADsegment - 权限为
R-X(可读、可执行,不可写) - 通过文件映射方式进入进程地址空间
这意味着 .text 中的内容并不会在 execve 时被整体拷贝进物理内存,而是
