可执行文件与 ELF
从 ELF 结构出发,梳理可执行文件的执行视图、链接视图以及加载过程中的关键组成。
可执行文件
Executable and Linkable Format
ELF 文件结构
| 组件名称 | 典型位置 (Offset) | 对应视图 (View) | C 库结构体 | 核心功能描述 |
|---|---|---|---|---|
| 1. ELF Header (ELF 头) | 0 (绝对起始) | 全局索引 | Elf64_Ehdr | 文件的“身份证”。 包含 Magic ( 0x7FELF)、架构、入口点 (_start) 以及 PHT/SHT 的偏移量。 |
| 2. Program Header Table (PHT / 程序头表) | 紧跟 ELF Header | 执行视图 (Loader / Kernel) | Elf64_Phdr | 给操作系统看的 (必需)。 描述 Segments (段),指示内核如何映射内存 (如 PT_LOAD)。 |
| 3. Sections (节区 / 有效载荷) | 文件中间 | 数据载荷 | N/A | 存放代码和数据。 • .text: 机器码• .rodata: 只读常量• .data: 全局变量 |
| 4. Section Header Table (SHT / 节头表) | 文件末尾 | 链接视图 (Linker / GDB) | Elf64_Shdr | 给调试器看的 (可选)。 记录每个 Section 的名称和大小。 运行时不需要, strip 命令删除的就是它。 |
点击查看 readelf -h 的完整输出
```bash ❯ readelf -h -l -S a.out ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400390 Start of program headers: 64 (bytes into file) Start of section headers: 10992 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 32 Section header string table index: 31Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.gnu.bu[…] NOTE 0000000000400318 00000318 0000000000000024 0000000000000000 A 0 0 4 [ 2] .init PROGBITS 000000000040033c 0000033c 000000000000001b 0000000000000000 AX 0 0 4 [ 3] .plt PROGBITS 0000000000400360 00000360 0000000000000030 0000000000000010 AX 0 0 16 [ 4] .text PROGBITS 0000000000400390 00000390 00000000000002cf 0000000000000000 AX 0 0 16 [ 5] .fini PROGBITS 0000000000400660 00000660 000000000000000d 0000000000000000 AX 0 0 4 [ 6] .interp PROGBITS 0000000000401000 00001000 000000000000001c 0000000000000000 A 0 0 1 [ 7] .gnu.hash GNU_HASH 0000000000401020 00001020 0000000000000028 0000000000000000 A 8 0 8 [ 8] .dynsym DYNSYM 0000000000401048 00001048 00000000000000a8 0000000000000018 A 9 1 8 [ 9] .dynstr STRTAB 00000000004010f0 000010f0 0000000000000059 0000000000000000 A 0 0 1 [10] .gnu.version VERSYM 000000000040114a 0000114a 000000000000000e 0000000000000002 A 8 0 2 [11] .gnu.version_r VERNEED 0000000000401158 00001158 0000000000000030 0000000000000000 A 9 1 8 [12] .rela.dyn RELA 0000000000401188 00001188 0000000000000048 0000000000000018 A 8 0 8 [13] .rela.plt RELA 00000000004011d0 000011d0 0000000000000030 0000000000000018 AI 8 23 8 [14] .rodata PROGBITS 0000000000401200 00001200 0000000000000457 0000000000000000 A 0 0 8 [15] .eh_frame_hdr PROGBITS 0000000000401658 00001658 0000000000000034 0000000000000000 A 0 0 4 [16] .eh_frame PROGBITS 0000000000401690 00001690 00000000000000ac 0000000000000000 A 0 0 8 [17] .note.gnu.pr[…] NOTE 0000000000401740 00001740 0000000000000040 0000000000000000 A 0 0 8 [18] .note.ABI-tag NOTE 0000000000401780 00001780 0000000000000020 0000000000000000 A 0 0 4 [19] .init_array INIT_ARRAY 0000000000402df8 00001df8 0000000000000008 0000000000000008 WA 0 0 8 [20] .fini_array FINI_ARRAY 0000000000402e00 00001e00 0000000000000008 0000000000000008 WA 0 0 8 [21] .dynamic DYNAMIC 0000000000402e08 00001e08 00000000000001d0 0000000000000010 WA 9 0 8 [22] .got PROGBITS 0000000000402fd8 00001fd8 0000000000000010 0000000000000008 WA 0 0 8 [23] .got.plt PROGBITS 0000000000402fe8 00001fe8 0000000000000028 0000000000000008 WA 0 0 8 [24] .data PROGBITS 0000000000403010 00002010 0000000000000040 0000000000000000 WA 0 0 16 [25] .bss NOBITS 0000000000403060 00002050 0000000000002028 0000000000000000 WA 0 0 32 [26] .comment PROGBITS 0000000000000000 00002050 000000000000005c 0000000000000001 MS 0 0 1 [27] .annobin.notes PROGBITS 0000000000000000 000020ac 000000000000014f 0000000000000001 MS 0 0 1 [28] .gnu.build.a[…] NOTE 0000000000407088 000021fc 0000000000000144 0000000000000000 0 0 4 [29] .symtab SYMTAB 0000000000000000 00002340 0000000000000438 0000000000000018 30 19 8 [30] .strtab STRTAB 0000000000000000 00002778 000000000000023b 0000000000000000 0 0 1 [31] .shstrtab STRTAB 0000000000000000 000029b3 000000000000013b 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), D (mbind), l (large), p (processor specific)
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000002d8 0x00000000000002d8 R 0x8 INTERP 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000000066d 0x000000000000066d R E 0x1000 LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x00000000000007a0 0x00000000000007a0 R 0x1000 LOAD 0x0000000000001df8 0x0000000000402df8 0x0000000000402df8 0x0000000000000258 0x0000000000002290 RW 0x1000 DYNAMIC 0x0000000000001e08 0x0000000000402e08 0x0000000000402e08 0x00000000000001d0 0x00000000000001d0 RW 0x8 NOTE 0x0000000000000318 0x0000000000400318 0x0000000000400318 0x0000000000000024 0x0000000000000024 R 0x4 NOTE 0x0000000000001740 0x0000000000401740 0x0000000000401740 0x0000000000000040 0x0000000000000040 R 0x8 NOTE 0x0000000000001780 0x0000000000401780 0x0000000000401780 0x0000000000000020 0x0000000000000020 R 0x4 GNU_PROPERTY 0x0000000000001740 0x0000000000401740 0x0000000000401740 0x0000000000000040 0x0000000000000040 R 0x8 GNU_EH_FRAME 0x0000000000001658 0x0000000000401658 0x0000000000401658 0x0000000000000034 0x0000000000000034 R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000001df8 0x0000000000402df8 0x0000000000402df8 0x0000000000000208 0x0000000000000208 R 0x1
Section to Segment mapping:
Segment Sections…
00
01 .interp
02 .note.gnu.build-id .init .plt .text .fini
03 .interp .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame .note.gnu.property .note.ABI-tag
04 .init_array .fini_array .dynamic .got .got.plt .data .bss
05 .dynamic
06 .note.gnu.build-id
07 .note.gnu.property
08 .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
可执行文件用一个非常大(对人)的header描述了程序的初始状态, 清除fork-execve中带来的状态,重新根据header中的PT_LOAD,当然在上面的输出中是LOAD映射虚拟地址。如果header中包含PT_INTERP,内核会去加载对应的链接器,将外部的.so文件映射到进程中,此时程序由链接器接管,完成动态库的加载后回到程序的入口开始执行。
## 动态链接

动态链接的核心价值在于**实现运行时的抽象,从而将应用层代码与底层物理实现严格隔离**。
在内存管理层面,为了确保多个进程能够安全地共享物理内存中同一份只读的 `.text`(代码段),编译器必须生成位置无关代码(PIC),绝不能在可执行指令中硬编码内存的绝对地址。因此,架构上实施了严格的“代码与状态分离”:所有动态变化的地址状态(即外部函数的真实绝对地址)被强行剥离出代码段,集中存储在每个进程私有且具有读写权限的 `.got`(全局偏移表)数据段中。
依托于这种分离机制,ELF 实现了**惰性绑定(Lazy Binding)**。当程序被操作系统加载时,外部函数的地址最初保持未解析状态;直到程序控制流首次对该函数发起调用时,动态链接器才会正式介入,在内存中完成符号的查找与重定位,并将目标函数的真实物理地址回填至 GOT 表的对应槽位中,从而以最小的初始加载开销完成了运行时的地址映射。
### PLT / GOT 是如何配合工作的
如果只记一个主线,可以把第一次外部函数调用理解成下面这个过程:
1. 代码里并不直接写死 `printf` 的绝对地址,而是先跳到 `.plt` 中的一个桩代码。
2. 这个桩代码再去查询 `.got.plt` 里的槽位。
3. 如果槽位还没被解析,它会回到动态链接器,让链接器根据符号表和共享库信息找到真正的函数地址。
4. 动态链接器把结果回填到 GOT 槽位。
5. 下一次再调用同一个外部函数时,就可以直接通过 GOT 跳过去,不需要再次解析。
这也是为什么 `.text` 想保持共享和只读时,地址状态不能直接写在指令里,而必须被挪到进程私有的表项中。
### 静态链接 vs 动态链接
两者最核心的区别,不在“能不能调用库”,而在于**地址和实现是在构建期固定,还是在运行期解析**。
| 维度 | 静态链接 | 动态链接 |
| :-- | :-- | :-- |
| 库代码进入位置 | 直接拷贝进可执行文件 | 保留符号引用,运行时再装入 `.so` |
| 文件体积 | 通常更大 | 通常更小 |
| 运行时依赖 | 较少 | 依赖动态链接器和共享库 |
| 共享代码页 | 难共享 | 多进程可共享同一份只读 `.text` |
| 更新库的方式 | 需要重新链接程序 | 可替换共享库后重新加载 |
静态链接更像“把依赖都焊进二进制里”;动态链接更像“先约定接口,运行时再接上线”。
## 一次 `execve` 到底做了什么
从操作系统角度看,执行一个 ELF 程序并不是“读文件然后跳过去”这么简单,而是一轮对进程状态的重建。
### 1. 读取 ELF Header
内核先确认这是合法 ELF 文件,并读出:
- 程序入口地址 `e_entry`
- Program Header Table 的位置 `e_phoff`
- Program Header 项数 `e_phnum`
这一步相当于拿到“装载说明书”。
### 2. 根据 Program Header 建立虚拟内存映射
真正决定装载行为的是 Program Header,而不是 Section Header。
内核只关心诸如这些 `PT_*` 项:
- `PT_LOAD`:哪些文件区间需要映射到内存
- `PT_INTERP`:是否需要动态链接器
- `PT_DYNAMIC`:动态链接元信息在哪里
- `PT_GNU_STACK`:栈权限
- `PT_GNU_RELRO`:哪些区域在重定位后应转为只读
其中最关键的是 `PT_LOAD`。它告诉内核:
- 从文件的哪个 offset 读
- 映射到哪个虚拟地址区间
- 赋予什么权限(`R/W/X`)
所以加载程序时,内核实际是在创建一组 VMA,并把文件内容按 segment 维度映射进去,而不是按 `.text`、`.data` 这些 section 名字逐个处理。
### 3. 建立用户栈与初始上下文
在真正开始执行前,内核还要准备用户态启动环境,例如:
- `argc / argv`
- 环境变量 `envp`
- 辅助向量 `auxv`
- 初始栈顶位置
随后设置好程序计数器、栈指针等寄存器状态,让 CPU 能够从入口点开始进入用户态。
### 4. 如果存在 `PT_INTERP`,先跳到动态链接器
如果 ELF 是动态链接的,真正最先运行的通常不是你的 `main`,甚至也不是你写的 `_start` 逻辑,而是解释器字段里声明的动态链接器,例如:
```bash
/lib64/ld-linux-x86-64.so.2
动态链接器会负责:
- 装载依赖的共享库
- 处理重定位
- 解析符号
- 初始化 GOT/PLT 等运行时结构
完成这些之后,控制权才会回到程序入口,继续进入 C runtime,再进一步调用 main。
为什么内核看 Segment,而调试器更在意 Section
这也是初学 ELF 最容易混淆的一点。
Segment:给加载器看的
Segment 解决的问题是:
这份文件在运行时应该怎样映射到进程地址空间?
它强调的是运行时布局,因此关注权限、页对齐、虚拟地址、文件偏移。
Section:给链接器和调试器看的
Section 解决的问题是:
这些代码、符号、字符串表、重定位项在构建系统内部如何组织?
它强调的是构建和分析视图,因此会出现:
.symtab.strtab.rela.text.debug_*
这些信息对程序运行往往不是必须的,但对链接、调试、反汇编非常重要。
所以一句话总结就是:
- 执行时看 Segment
- 链接和分析时看 Section
阅读 ELF 时最值得先抓住的四个问题
当你面对一个陌生 ELF,最有效的切入方式通常不是把 readelf 输出从头看到尾,而是先回答下面四个问题:
- 它是静态链接还是动态链接?
- 入口点在哪里?解释器是谁?
- 有几个
PT_LOAD,它们分别对应什么权限? - 是否存在
.dynamic、.got、.plt这些运行时结构?
如果这四个问题能答清楚,你基本就已经掌握了这个可执行文件最重要的运行信息。
常用观察命令
# 看 ELF Header
readelf -h a.out
# 看 Program Headers(运行时最关键)
readelf -l a.out
# 看 Sections
readelf -S a.out
# 看动态依赖
readelf -d a.out
# 看符号表
readelf -s a.out
# 看反汇编
objdump -d a.out
小结
ELF 可以同时被看成两种结构:
- 对内核来说,它是一份“如何建立进程地址空间”的装载说明书;
- 对链接器和调试器来说,它是一份“如何组织代码、数据、符号与重定位信息”的构建产物。
理解这一点之后,很多现象都会自然变得统一:
- 为什么内核主要看 Program Header
- 为什么 strip 后程序还能运行
- 为什么动态链接需要 GOT / PLT
- 为什么共享库能在多个进程间共享代码页
从这里继续往下,最自然的下一步就是把 ELF 和 fork/execve/mmap、页表、动态链接器启动过程连起来看,这样对“程序是怎么被操作系统执行起来的”会有更完整的把握。