Sealessland logo Sealessland
OS

可执行文件与 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: 31

Section 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文件映射到进程中,此时程序由链接器接管,完成动态库的加载后回到程序的入口开始执行。

## 动态链接

![dll](img/dl.png)

动态链接的核心价值在于**实现运行时的抽象,从而将应用层代码与底层物理实现严格隔离**。

在内存管理层面,为了确保多个进程能够安全地共享物理内存中同一份只读的 `.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 输出从头看到尾,而是先回答下面四个问题:

  1. 它是静态链接还是动态链接?
  2. 入口点在哪里?解释器是谁?
  3. 有几个 PT_LOAD,它们分别对应什么权限?
  4. 是否存在 .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、页表、动态链接器启动过程连起来看,这样对“程序是怎么被操作系统执行起来的”会有更完整的把握。