☆二进制文件结构
从编译器角度看,二进制结构是最终产物的“布局说明书”。
• 它决定了编译结果如何装配、如何被 loader 或 JIT 执行。
• 理解这些结构对你开发:
• 后端(codegen)
• 链接器(linker)
• 调试信息生成(DWARF 等)
• 动态链接支持(PLT / GOT)
• LTO(Link Time Optimization)
是必须的。
最常见的二进制文件格式是 ELF (Executable And Linkable Format) 文件,我们会从实操视角来展示其构成
ELF
下面以 X86+ubuntu 为例
首先任意编辑一个简单 C 代码:
// hello.c
#include <stdio.h>
int main() {
printf("Hello, ELF!\n");
return 0;
}
然后编译:
gcc -g -O0 hello.c -o hello
,于是我们会得到一个 hello
这个可执行文件。我们可以直接查看一下他的整体结构(只是其基本信息)。使用:
readelf -h hello
得到(不一致的输出可以忽略):
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (Position-Independent Executable file)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x1060
程序头起点: 64 (bytes into file)
Start of section headers: 14632 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes) # ELF HEADER 大小
Size of program headers: 56 (bytes) # 一个PHDR大小
Number of program headers: 13 # PHDR数量
Size of section headers: 64 (bytes) # 每个 Section Head 大小
Number of section headers: 37 # Section Header 数量
Section header string table index: 36 # 字符串节索引,对应上面37
我们可以从其中获得以下信息(即对应着 Magic 后的 16 进制内容):
- 前四字节 7f 45 4c 46 是固定标识:0x7F + “ELF”
- 这是一个 64 位机器可执行的文件,小端序:02 01
- 下面是版本号:01
- 后续 0 均为保留内容
- 这个是基于 System V ABI 的标准 ELF 文件
- DYN 表示这是一个 可重定位的可执行文件
接下来我们可以看一下程序头表(Program Headers), 这是给 Loader(加载器)使用并读取的内容:
输出:readelf -l hello
开头的信息含义是:Elf 文件类型为 DYN (Position-Independent Executable file) Entry point 0x1060 There are 13 program headers, starting at offset 64 程序头: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8 INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000628 0x0000000000000628 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000175 0x0000000000000175 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x00000000000000f4 0x00000000000000f4 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000258 0x0000000000000260 RW 0x1000 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000030 0x0000000000000030 R 0x8 NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000030 0x0000000000000030 R 0x8 GNU_EH_FRAME 0x0000000000002010 0x0000000000002010 0x0000000000002010 0x0000000000000034 0x0000000000000034 R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000248 0x0000000000000248 R 0x1 Section to Segment mapping: 段节... 00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.got .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .dynamic .got
- 类型 DYN:说明是 PIE 可执行文件,用 ASLR(地址空间随机化),入口地址是相对的。
- Entry point: 0x1060:是 .text 段中 _start 或入口函数地址,相对于加载基址。
- 13 个 Program Headers:说明有较复杂的加载要求,比如堆栈策略、==动态链接==等。
以上内容在编译器开发者来说往往是忽略的!
PHDR Offset: 0x40, VirtAddr: 0x40, FileSiz: 0x2d8
描述的是Program Header Table 本身。Loader 会加载段表,然后按其描述映射其他段。R 的含义是只读,毕竟段表不需要执行或写。
INTERP(解释器路径):
INTERP Offset: 0x318, 内容:/lib64/ld-linux-x86-64.so.2
表示程序依赖动态链接器(动态加载器)的路径。这是启动时由内核调用的真正“第一步”,会处理 .dynamic section。这段会映射 .interp section。
后续几个 LOAD 的对照关系可以看
Section to Segment mapping:
的对应内容,并且标注了 E
的才是真正程序可执行内容。最后也有他们的对齐方式。再后续几个这边直接合在一起列举:
DYNAMIC Offset: 0x2dc8, VirtAddr: 0x3dc8, Flags: RW
:描述了 .dynamic 节,告诉动态链接器哪些需要重定位、依赖哪些共享库、GOT 表位置等等。- NOTE:指示兼容的 ABI,Linux 用来版本判断
- GNU_PROPERTY:标记编译器、代码模型(如 CET, IBT)
- BUILD-ID:GDB 用来识别 ELF 文件身份
GNU_EH_FRAME Offset: 0x2010
:用于异常处理的预处理信息(快速 unwinding)GNU_STACK Flags: RW
:表示程序的用户栈是否是可执行的(一般为不可执行),RW 是默认,除非 -z execstackGNU_RELRO Offset: 0x2db8, VirtAddr: 0x3db8, Size: 0x248
:启动后将这段标记为只读,防止 GOT 被覆盖,强化安全。Relro 段起始于数据段,对应 .got, .dynamic, .init_array 等节。
WASM
MacO
PE
附录
A-Program-header、ELF-header、Section-header等区别
从“流程”视角理解它们的职责
我们从程序的生命周期角度来看它们的作用:
🧱 编译 & 链接阶段:
• 编译器生成 .o 文件,每个函数/变量被组织为多个 Section
• 链接器将多个 Section 合并、重排、构造 Section Header Table
• Section Header 表是为链接器和调试器准备的元数据
• 编译器也会在 ELF Header 中设置基本架构信息、节区偏移等
🚀 加载 & 运行阶段(Loader):
• ==Loader 只关心 ELF Header 和 Program Header==
• Loader 从 ELF Header 找到 Program Header Table,然后读取:
• 哪些段需要加载到内存
• 每个段加载到哪里、大小是多少、权限是啥(R/W/X)
🧵 调试阶段:
• GDB/Binary Ninja 根据 ELF Header 找到 Section Header Table
• 读取 .symtab, .strtab, .debug_ 等节,实现:
• 符号还原
• 行号映射
• 变量作用域跟踪
一句话记住三者区别:
• ELF Header:整张蓝图,告诉工具文件怎么解读
• Program Header:==给操作系统看的,“我有哪些内存段要加载执行”==
• *Section Header:给链接器和调试器看的,“我有哪些编译产物要处理”
名称 | ELF Header | Program Header | Section Header |
---|---|---|---|
位置 | 文件开头第 0 字节起 | ELF Header 紧后,偏移由其指定 | 通常在文件尾部,偏移由 ELF Header 指定 |
作用对象 | 描述整个 ELF 文件的“总览” | 描述运行时段(Segment) | 描述编译产物的节区(Section) |
读它的是谁? | 所有 ELF 工具 | 操作系统 Loader(如 ld-linux.so) | 链接器(ld)、调试器(gdb)、反汇编器 |
是否参与执行? | 是(必须) | 是(必须) | 否(Loader 忽略它) |
是否参与链接/调试? | 是 | 否 | 是(必需) |
你用 readelf 的命令? | readelf -h | readelf -l | readelf -S |
描述的内容? | 文件结构、偏移、架构、入口等 | 描述要加载的内存段:代码段、数据段、堆栈权限等 | 描述编译时的节:.text, .data, .symtab, .debug_info 等 |
个数 | 1 个 | 若干个段(比如 5~15 个) | 若干个节(几十个很常见) |
结构体名 | Elf64_Ehdr | Elf64_Phdr | Elf64_Shdr |
B-万变不离其宗——ELF. h
C-Segment 与 Section,我们究竟有多少误解?
• 一个 Segment 可以包含多个 Section
• Section 更偏向源结构、编译产物
• Segment 是运行时内存布局的最小单位
🔸 一、Segment 是操作系统 Loader 的“映射单元”
• 每一个 PT_LOAD 类型的 Segment,都会被 loader 映射(mmap)到内存中。
• 映射时是以页为单位(通常 4KB)对齐的。
• 这是为了让它们直接适配页表(page table)机制,操作系统不会逐字节加载 ELF,而是按页加载。
🔸 二、对齐规则(Align 字段)
在 Program Header 中,每个 Segment 都有一个 Align 字段:Align: 0x1000 # 通常是 0x1000(4096 字节 = 一页)
VirtAddr(虚拟地址)和 Offset(文件偏移)都必须满足:VirtAddr % Align == Offset % Align
🔸 三、为什么页对齐很重要?
✅ 原因 1:提升内存映射效率
• 页为最小的内存分配单元,mmap 和 page fault 都以页为粒度操作。
• 段不对齐 → 多映射、多浪费。
✅ 原因 2:控制权限(R/W/X)
• 页表控制每一页的访问权限(read/write/exec)
• Segment 对齐到页边界,才能设置 .text 为 R-X、.data 为 RW- 等。
• 如果段跨页且混合了不同权限,就不能独立设置,存在安全隐患(如 W+X)
D-readelf 使用方法
readelf -h hello # ELF Header:结构总览
readelf -l hello # Program Header Table:加载段信息
readelf -S hello # Section Header Table:链接节区