从编译器角度看,二进制结构是最终产物的“布局说明书”。
• 它决定了编译结果如何装配、如何被 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 execstack
  • GNU_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 HeaderProgram HeaderSection Header
位置文件开头第 0 字节起ELF Header 紧后,偏移由其指定通常在文件尾部,偏移由 ELF Header 指定
作用对象描述整个 ELF 文件的“总览”描述运行时段(Segment)描述编译产物的节区(Section)
读它的是谁?所有 ELF 工具操作系统 Loader(如 ld-linux.so)链接器(ld)、调试器(gdb)、反汇编器
是否参与执行?是(必须)是(必须)否(Loader 忽略它)
是否参与链接/调试?是(必需)
你用 readelf 的命令?readelf -hreadelf -lreadelf -S
描述的内容?文件结构、偏移、架构、入口等描述要加载的内存段:代码段、数据段、堆栈权限等描述编译时的节:.text, .data, .symtab, .debug_info 等
个数1 个若干个段(比如 5~15 个)若干个节(几十个很常见)
结构体名Elf64_EhdrElf64_PhdrElf64_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:链接节区