本页目录

ELF 文件格式

覆盖: ELF header → section header table → program header table → 手动解析 → readelf/objdump 用法 适用: x86-64 / ARM64 ELF, ET_EXEC / ET_DYN / ET_REL

概述

ELF (Executable and Linkable Format) 是 Linux 上几乎所有二进制文件的标准格式——可执行文件、共享库 (.so)、目标文件 (.o)、core dump 都使用 ELF。理解 ELF 不是学术练习——当你需要排查"为什么这个 .so 加载失败"、"为什么 LD_PRELOAD 不生效"、"为什么 strip 后 coredump 不可分析"时,ELF header 中的细节就是答案。

ELF 三种视图

flowchart LR
    subgraph LINK["🔗 Linking View (编译/链接时)"]
        L_HDR["ELF Header"]
        L_SHDR["Section Header Table"]
        L_TEXT[".text (代码)"]
        L_RODATA[".rodata"]
        L_DATA[".data"]
        L_BSS[".bss"]
        L_SYMTAB[".symtab"]
        L_DYNSYM[".dynsym"]
        L_PLT[".plt"]
        L_GOT[".got"]
    end

    subgraph EXEC["▶️ Execution View (运行时)"]
        E_HDR["ELF Header"]
        E_PHDR["Program Header Table"]
        E_LOAD_RX["PT_LOAD<br/>可读+可执行"]
        E_LOAD_RW["PT_LOAD<br/>可读+可写"]
        E_DYNAMIC["PT_DYNAMIC<br/>动态链接信息"]
        E_INTERP["PT_INTERP<br/>ld.so 路径"]
    end

    L_TEXT -.->|"属于"| E_LOAD_RX
    L_RODATA -.->|"属于"| E_LOAD_RX
    L_DATA -.->|"属于"| E_LOAD_RW
    L_BSS -.->|"属于"| E_LOAD_RW
    L_DYNSYM -.->|"属于"| E_DYNAMIC

    classDef link fill:#e3f2fd,stroke:#1565c0
    classDef exec fill:#fff3e0,stroke:#ef6c00
    class LINK link
    class EXEC exec

Section 是给链接器看的(按功能组织),Segment 是给内核/动态链接器看的(按权限和加载需求组织)。同一个 .text section 属于一个 PT_LOAD segment。

ELF Header

// /usr/include/elf.h
typedef struct {
    unsigned char e_ident[16];   // Magic + Class + Data + Version + ABI
    uint16_t      e_type;        // ET_EXEC, ET_DYN (PIE/.so), ET_REL (.o), ET_CORE
    uint16_t      e_machine;     // EM_X86_64, EM_AARCH64, EM_RISCV
    uint32_t      e_version;     // EV_CURRENT (1)
    uint64_t      e_entry;       // 入口地址 (_start)
    uint64_t      e_phoff;       // Program Header Table offset
    uint64_t      e_shoff;       // Section Header Table offset
    uint32_t      e_flags;
    uint16_t      e_ehsize;      // ELF header size (64 bytes on x86_64)
    uint16_t      e_phentsize;   // 每个 program header 的大小
    uint16_t      e_phnum;       // program header 的数量
    uint16_t      e_shentsize;   // 每个 section header 的大小
    uint16_t      e_shnum;       // section header 的数量
    uint16_t      e_shstrndx;    // section name string table 的索引
} Elf64_Ehdr;

e_ident 魔数

$ xxd /bin/ls | head -1
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............

# byte 0-3: 0x7f 'E' 'L' 'F' — 魔数
# byte 4:   EI_CLASS — 1=32-bit, 2=64-bit
# byte 5:   EI_DATA  — 1=little-endian, 2=big-endian
# byte 6:   EI_VERSION — always 1
# byte 7:   EI_OSABI — 0=UNIX System V, 3=GNU/Linux
# byte 8-15: padding

e_type 关键值

ET_EXEC (2): 传统可执行文件,加载到固定地址 (非 PIE)
ET_DYN  (3): 共享库或 PIE 可执行文件,加载到随机基址 (现代默认)
ET_REL  (1): 可重定位目标文件 (.o),链接器的输入
ET_CORE (4): core dump 文件

现代 Linux (GCC 默认 -pie) 的可执行文件都是 ET_DYN。可用 file /bin/ls 确认。


Section Header Table

每个 section 描述文件中的一个区域——代码、数据、符号表、重定位表等。

typedef struct {
    uint32_t   sh_name;       // section 名字在 .shstrtab 中的偏移
    uint32_t   sh_type;       // SHT_PROGBITS, SHT_SYMTAB, SHT_STRTAB, SHT_RELA, ...
    uint64_t   sh_flags;      // SHF_WRITE, SHF_ALLOC, SHF_EXECINSTR, ...
    uint64_t   sh_addr;       // 加载到内存后的虚拟地址 (可执行文件中)
    uint64_t   sh_offset;     // 在文件中的偏移
    uint64_t   sh_size;       // section 大小
    uint32_t   sh_link;       // 关联的 section 索引 (如 symtab 关联 strtab)
    uint32_t   sh_info;       // 额外信息
    uint64_t   sh_addralign;  // 对齐
    uint64_t   sh_entsize;    // 如果是表,每个 entry 的大小
} Elf64_Shdr;

关键 section

Section类型内容
.textSHT_PROGBITS可执行代码
.rodataSHT_PROGBITS只读数据 (字符串常量等)
.dataSHT_PROGBITS已初始化的全局/静态变量
.bssSHT_NOBITS未初始化的全局变量 (占空间但不占文件大小)
.pltSHT_PROGBITSProcedure Linkage Table (延迟绑定)
.got / .got.pltSHT_PROGBITSGlobal Offset Table
.dynsymSHT_DYNSYM动态符号表 (运行时需要)
.symtabSHT_SYMTAB完整符号表 (strip 后可移除)
.dynstr / .strtabSHT_STRTAB字符串表
.rela.dyn / .rela.pltSHT_RELA重定位表
.interpSHT_PROGBITS动态链接器路径 (如 /lib64/ld-linux-x86-64.so.2)
.dynamicSHT_DYNAMIC动态链接信息 (DT_NEEDED, DT_SONAME, ...)
.init / .finiSHT_PROGBITS初始化/终结代码 (GCC -finit-array)

Program Header Table

Program header 告诉内核"怎么加载这个文件"。每个 segment (PT_LOAD) 指定一块数据要映射到哪个虚拟地址、什么权限。

typedef struct {
    uint32_t   p_type;    // PT_LOAD, PT_DYNAMIC, PT_INTERP, PT_NOTE, PT_GNU_STACK, ...
    uint32_t   p_flags;   // PF_R(4) | PF_W(2) | PF_X(1)
    uint64_t   p_offset;  // segment 在文件中的偏移
    uint64_t   p_vaddr;   // 虚拟地址 (ASLR 会加上随机基址)
    uint64_t   p_paddr;   // 物理地址 (无关,只用于没有 MMU 的系统)
    uint64_t   p_filesz;  // 文件中的大小
    uint64_t   p_memsz;   // 内存中的大小 (memsz > filesz → 剩余补零,用于 .bss)
    uint64_t   p_align;   // 对齐
} Elf64_Phdr;

PT_LOAD 映射过程

// 内核 fs/binfmt_elf.c: elf_map()
// 对每个 PT_LOAD segment:
//   1. mmap(p_vaddr, p_memsz, PROT from p_flags, MAP_PRIVATE, fd, p_offset)
//   2. 如果 p_memsz > p_filesz → 剩余字节映射为 0 (anonymous MAP_ANONYMOUS)
//      这就是 .bss 的运行时实现

// 例: /bin/ls 有 2 个 PT_LOAD:
//   Type  Offset             VirtAddr          PhysAddr         FileSiz  MemSiz   Flg Align
//   LOAD  0x0000000000000000 0x0000000000400000 0x0000000000400000 0x002e58 0x002e58 R E 0x1000
//   LOAD  0x0000000000003000 0x0000000000403000 0x0000000000403000 0x000fb8 0x001640 RW  0x1000
//
//   第一个 LOAD: 代码+只读数据 → R+X (memsz == filesz, 无 .bss)
//   第二个 LOAD: 数据+.bss      → R+W (memsz > filesz, 剩余补齐 .bss)

手动解析

# ELF 头部
readelf -h /bin/ls

# Section headers
readelf -S /bin/ls

# Program headers (segments)
readelf -l /bin/ls

# 动态符号表
readelf --dyn-syms /bin/ls

# 重定位表
readelf -r /bin/ls

# .dynamic section (DT_NEEDED, DT_RPATH, ...)
readelf -d /bin/ls

# 字符串全文搜索
strings /bin/ls | head -20

# 反汇编 (disassemble)
objdump -d /bin/ls | head

# 检查是否 PIE
file /bin/ls
# "ELF 64-bit LSB pie executable" → PIE
# "ELF 64-bit LSB executable" → non-PIE

参考

  • 规范⁠: ELF spec (System V ABI, Chapter 4-5), man 5 elf
  • 头文件⁠: /usr/include/elf.h, /usr/include/link.h
  • 工具⁠: readelf, objdump, nm, size, strip, patchelf
  • LWN: "The ELF object file format" 系列

关键词: ELF header, section, segment, PT_LOAD, SHT_SYMTAB, ET_DYN, ET_EXEC, PIE, readelf, objdump