跳转至

ELF

ELF 文件核心概念

ELF (Executable and Linkable Format) 是类 Unix 系统的标准二进制格式,包含:

  • 核心作用:定义程序如何被操作系统加载到内存、如何执行、如何动态链接依赖库,以及存储调试信息。
  • 适用场景
    • 可执行文件(如 Linux 的 /bin/bash
    • 动态链接库(如 .so.dll
    • 编译中间文件(如 .o 目标文件)
    • Core Dump(程序崩溃时的内存快照)

ELF(Executable and Linkable Format)是一种跨平台、跨操作系统的标准二进制文件格式,用于描述可执行程序、共享库、目标文件等。

ELF(Executable and Linkable Format)是 可执行文件、库文件、目标文件 的标准格式。
通俗理解:

  • 像一本书的目录:告诉操作系统和硬件如何加载、运行程序。
  • 跨平台兼容:RISC-V、x86、ARM 等架构都有自己的 ELF 文件变体。
  • 统一规范:链接器、加载器、调试工具都依赖 ELF 的结构解析程序。

ELF 文件的核心内容(三大部分)

ELF 头

ELF 头(ELF Header)

  1. 作用:文件的全局描述符,标识文件类型和架构信息。
  2. 关键字段
    • Magic Number:前4字节固定为 7F 45 4C 46(ASCII为 \x7FELF),表明这是一个 ELF 文件。
    • 文件类型:e_type 标识是可执行文件ET_EXEC)、共享库ET_DYN)还是目标文件ET_REL)。
    • 目标架构:e_machine 字段指定 CPU 架构(如 EM_X86_64EM_ARMEM_RISCV)。
    • 入口地址:e_entry是程序开始执行的地址(如_start函数)。
    • 程序头表/节头表位置:指向段(Segment)和节(Section)的元数据表。
    • 其他信息:32位/64位、大端/小端等。

节(Sections)

  • 作用:存储程序的代码、数据、调试信息等,主要用于编译和链接阶段
  • 常见节及其作用:
节名 作用
.text 存放 RISC-V 指令代码(如函数、跳转指令)。
.data 已初始化的全局变量(如int a=10;)。
.rodata 只读数据(如字符串常量"Hello")。
.bss 未初始化的全局变量(运行时置零)。
.symtab 符号表(记录函数、变量名和地址的对应关系)。
.strtab 字符串表(存储符号名称、节名称等字符串)。
.riscv.attributes RISC-V 特有:记录支持的指令集扩展(如 C 扩展、浮点指令等)。
.comment 编译器/工具链的版本信息(如GCC: (x.x.x))。
.rela.text 重定位表:记录 .text 节中需要重定位的指令(用于链接阶段)。
.debug_* 调试信息:DWARF 格式的源码行号、变量类型等(如 .debug_info)。

段(Segments / Program Headers)

  • 作用:告诉操作系统如何将程序加载到内存,用于程序执行阶段
  • 关键段类型:
段类型 作用
PT_LOAD 需要加载到内存的段(如代码段.text、数据段.data)。
PT_DYNAMIC 动态链接信息(如依赖的共享库.so)。
PT_INTERP 动态链接器的路径(如/lib/ld-linux-riscv64-lp64d.so.1)。
PT_GNU_STACK 栈的可执行权限(决定是否允许栈上执行代码,防范安全漏洞)。

查看 ELF 文件信息

  • 文件类型查看
file program.elf
  • readelf:查看 ELF 结构
readelf -h program.elf    # 查看 ELF 头
readelf -S program.elf    # 查看所有节(Sections)
readelf -l program.elf    # 查看所有段(Segments)
readelf -s program.elf    # 查看符号表(Symbols)
readelf -a program.elf    # 查看所有信息(头、节、段、符号表等)
  • objdump:反汇编与节内容查看
objdump -d program    # 反汇编代码段
objdump -s -j .data program  # 查看数据段原始内容

ELF 文件的生命周期

  1. 编译阶段
    • 编译器将源码编译为目标文件(.o),生成 .text.data 等节,但未解决外部符号引用。
  2. 链接阶段
    • 链接器合并多个目标文件,解析符号引用,生成可执行文件或共享库。
    • 重定位表(.rela.text)指导链接器修正代码中的地址偏移。
  3. 加载阶段
    • 操作系统读取 PT_LOAD 段,将代码和数据加载到内存指定地址。
    • 动态链接器(ld.so)处理 PT_DYNAMIC 段,加载依赖的共享库。
  4. 执行阶段
    • CPU 从 e_entry 地址开始执行指令,程序启动。

调试信息

DWARF 格式:ELF 文件中通过 .debug_* 节存储调试信息,包括:

  • .debug_info:源码文件、函数、变量类型。
  • .debug_line:机器指令与源码行号的映射。
  • .debug_frame:栈帧布局(用于回溯调用栈)。

调试工具:

  • gdb:加载 ELF 文件后,可结合调试信息进行源码级调试。
  • addr2line:将内存地址转换为源码位置。
addr2line -e program 0x401000   # 查看地址 0x401000 对应的源码位置

剥离调试信息:

strip program    # 删除符号表和调试信息,减小文件体积


重定位

重定位(Relocation)

  • 作用:解决跨文件符号引用问题。
  • 示例:
    • 在目标文件 a.o 中调用另一个目标文件 b.o 的函数 foo(),编译时 a.o 无法确定 foo() 的地址。
    • 链接阶段,链接器根据重定位表将 call foo 的地址修正为实际地址。

常见重定位类型:

  • R_X86_64_PC32(x86-64 相对地址调用)
  • R_AARCH64_CALL26(ARM64 函数调用)
  • R_386_32(x86 绝对地址引用)

动态链接

  • 核心机制:

    • 动态链接库(Shared Library):代码在运行时加载,多个程序可共享同一份库代码。
    • 全局偏移表(GOT):存储外部符号(如库函数)的实际地址。
    • 过程链接表(PLT):实现延迟绑定(Lazy Binding),首次调用函数时通过 GOT 解析地址。
  • 工具支持:

    • ldd program:查看程序依赖的共享库。
    • LD_PRELOAD 环境变量:预加载自定义库(用于调试或劫持函数)。