
观众老爷们大家好 这里是邪修KING的独家频道
本文属于系列Linux系统篇 ——动静态库
一起学Linux的小伙伴可订阅专栏: Linux系统篇
前面我们讲了编译、链接、静态库与动态库,一直在和 .o、可执行程序、.so 打交道。但很少有人往下深挖一层:这些二进制文件内部到底是什么结构?代码、数据、符号是怎么组织的?操作系统凭什么能把它加载到内存运行? 答案就是 ELF(Executable and Linkable Format),它是 Linux 世界里二进制文件的统一组织规范。 本篇我们从整体视角切入,讲清楚 ELF 是什么、里面装了什么、Section 与 Segment 的核心区别,以及它在整个程序生命周期里的角色。
一、一句话认识 ELF
ELF 是 Linux 系统中二进制文件的标准组织格式。 它不是某一个具体的程序,而是一套约定:规定了程序里的代码、数据、符号、加载信息应该按照什么结构、什么顺序存放在文件里。
我们日常接触的三类二进制文件,本质上都遵循 ELF 格式:
- 目标文件 .o
- 可执行程序 a.out
- 动态库 .so
尽管用途不同、阶段不同,但它们的内部结构都符合 ELF 规范,都包含统一的头部、段表、程序头表等结构。
ELF 文件整体结构
┌─────────────────────┐
│ ELF Header 头部 │ → 标识文件类型、版本、架构等基础信息
├─────────────────────┤
│ Section 各个段 │ → 代码、数据、符号等具体内容分区存放
├─────────────────────┤
│ Section Table 段表 │ → 记录所有 Section 的位置、大小、属性
├─────────────────────┤
│ Program Header Table│ → 程序头表,描述运行时如何加载
└─────────────────────┘
二、ELF 里核心装了什么?5 个经典 Section
ELF 内部按功能分成不同的 Section(段),每个段存放一类内容。最核心、最常见的有 5 个。
2.1 .text:代码段
存放程序的机器指令,也就是我们写的代码编译后生成的二进制指令。 特点:只读、可执行。 比如 int main() { printf("hello"); },main 函数编译后的机器码,就存在 .text 段里。
2.2 .data:数据段
存放已经初始化的全局变量、静态变量。 特点:可读写。 比如 int global = 10;,这个初始值为 10 的全局变量,就存在 .data 段。
2.3 .rodata:只读数据段
存放只读数据,比如字符串常量、const 修饰的常量。 特点:只读,不可写。 比如 printf("hello world"); 里的字符串 "hello world",就存放在 .rodata 段。
2.4 .bss:未初始化数据段
存放未初始化的全局变量、静态变量。 特点:不占用实际文件空间,程序加载时由内核清零。 比如 int g_val; 这种没给初始值的全局变量,就归属 .bss 段。它在文件里只是占位,不存真实数据,运行时自动初始化为 0。
2.5 .symtab:符号表
存放程序里所有的符号信息:函数名、变量名、对应的地址、类型等。 比如 main、global、printf 这些符号的信息,都记录在符号表里。 作用:给链接器做符号解析、重定位用,也给调试工具提供信息。
💡 一个简单的对应示例
int global = 10; // .data 段
const char *str = "hi"; // "hi" 在 .rodata,str 指针在 .data
int main() // .text 段
{
static int s; // .bss 段
printf(str);
return 0;
}
// main、global、s、printf 这些名字 → .symtab 符号表
三、最容易混淆:Section 与 Segment 不是一回事
这是 ELF 最核心的辨析点,90% 的初学者都会搞混。 两者是完全不同视角下的概念:Section 是文件视角,Segment 是运行 / 加载视角。
3.1 Section:文件组织视角
- 关注的问题:文件里面具体存了什么?
- 代表:.text、.data、.rodata、.symtab、.debug 等
- 服务对象:链接器、调试器、分析工具
- 特点:分类细、数量多,按功能划分
3.2 Segment:内存加载视角
- 关注的问题:程序运行的时候,怎么装进内存?
- 代表:可执行段、可读段、可写段
- 服务对象:操作系统加载器
- 特点:把属性相同的多个 Section 合并到一起,作为一个整体映射到内存
3.3 两者的关系
多个属性相同的 Section,会被组织到同一个 Segment 里。 比如:
- .text、.rodata 都是只读的,会被合并到一个「只读可执行 Segment」
- .data、.bss 都是可读写的,会被合并到一个「可读写 Segment」
一句话总结: Section 是给链接器看的,用来做文件内的功能划分;Segment 是给操作系统看的,用来做内存映射加载。
四、Program Header Table 是干什么的?
Program Header Table(程序头表),就是专门用来描述 Segment 的表。 可以把它理解成「操作系统加载说明书」:告诉操作系统,这个程序运行时应该怎么映射到内存。
里面记录了每个 Segment 的:
- 在文件中的偏移位置
- 加载到内存的虚拟地址
- 大小
- 权限(读 / 写 / 执行)
操作系统加载程序的时候,就是照着程序头表,把一个个 Segment 按要求映射到内存的对应位置,程序就能跑起来了。
整体关系梳理
ELF 文件
├── Section 们 + Section Table → 文件内部怎么组织,给链接器用
└── Program Header Table → 运行时怎么加载,给操作系统用
五、ELF 最终要解决什么?
在 ELF 之前,不同系统、不同工具的二进制格式五花八门,编译、链接、加载各玩各的,兼容性极差。 ELF 的出现,提供了一套统一的二进制标准,贯穿了程序的整个生命周期:
💡 ELF 就像整个程序世界的「通用集装箱」 不管里面装的是什么货物(代码 / 数据 / 符号),不管是运输(链接)还是卸货(加载),都用统一的规格,所有工具都能识别、都能处理。 它是连接「编译、链接、加载、运行」全流程的统一二进制载体。
全文总结・一张核心图
ELF
│
┌──────────┴──────────┐
↓ ↓
Section Segment
↓ ↓
文件组织 内存加载
│ │
└──────────┬──────────┘
↓
贯穿编译 / 链接 / 加载 / 运行
核心结论
下篇预告: 整体框架搭完,下一篇我们深入细节:用 readelf、objdump 工具实地解析 ELF 文件,看每个段里到底存了什么内容。 


