ELF 是一种常见的用于可执行文件、可重定位代码 (对象文件,即 .o 文件)、共享库以及核心转储 (core dump) 等的标准文件格式。它是 ABI (应用程序二进制接口, Application Binary Interface) 的一种规范。设计之初,ELF 格式就充分考虑要实现灵活性、可扩展性、跨平台,以及 CPU 架构和 ISA (指令集架构, Instruction Set Architecture) 无关的目标。它可以被内核加载到任意内存地址,并且会根据加载到内存中的地址来自动计算所有内部符号的相对偏移地址。

文件扩展名: 无扩展名、.axf.bin.elf.o.prx.puff.ko.mod.so

魔数0x7F 紧跟 'E'、'L'、'F' 三个字符,即 0x7F 45 4C 46 共四个字节。

图片来源: Wikipedia|416 图片来源: Wikipedia

结构:

  • ELF 首部 (ELF header) (52 or 64 byte long for 32 or 64 bit): 32 位目标会占用 52 字节,64 位目标会占用 64 字节。它定义了当前 ELF 文件使用的机器类型。
  • 程序头表 (Program header table): 描述 0 个或多个 内存段 (segment) 信息。只适用于可执行文件这一类别。它的作用是描述该可执行文件应该如何被装入进程虚拟内存 (process virtual memory) 中,即如何创建进程镜像 (process image)。对于进程镜像、可执行文件、共享库来说是必须的,不过可重定位代码不必提供。
  • 分段头表 (Section header table): 描述 0 个或多个段的链接以及重定位需要的数据 (section)。它描述了 ELF 文件中的各种链接应该如何加载,以及在哪个位置可以找到。表中的每一项都记录了程序各个分段的名称和大小。如果后续还需要对文件进行链接,则必须要提供分段头表,否则无法将多个不同对象文件中的同类分段合并。
  • 数据 (Data): 程序头表和分段头表所具体引用的数据内容。

分段 (Sections) 是从 链接的角度 来划分 ELF 文件的最小结构单元。分段中存储了对象文件中各个分块的信息,可以用于进一步链接。这些数据包括了指令、数据、符号表和用于重定位的信息。

段 (Segments) 是从 可执行文件角度 来划分 ELF 文件的最小结构单元。当 ELF 文件被执行或链接器处理时,它可以用于映射到对应的内存地址或相对地址。

如何区别分段和段: 在一个对象文件中,分段会在链接过程之前形成,而段则会在链接形成可执行文件之后产生。链接器会将一个或多个分段结合成一个段。

要注意的是,分段和段在 ELF 文件中没有固定的位置,只有 ELF 首部是必须要放置在最前面的。

关于 ELF 文件的若干工具:

  • readelf: 用于查看 ELF 文件的信息 (由 GNU binutils 提供)。
  • elfutils: binutils 的替代品。
  • elfdump: 输出 ELF 文件的 ELF 信息。
  • objdump: 输出对象文件的信息。它使用二进制描述符 (Binary Descriptor) 库来组织构造 ELF 数据。
  • file: 可以用于显示 ELF 文件的部分信息,比如这个可重定位文件、可执行文件或共享库所构建的目标 ISA,或是 ELF 内核转储是在什么样的 ISA 上产生的。
  • nm: 可以用于显示一个对象文件包含的符号信息。

使用 readelf 工具的例子: 使用 Git 克隆仓库 https://github.com/bit-Control/elf_examples.git

内核与 ELF:

程序头表中的项需要提供以下几种类型之一,这是通过每项的前 4 个字节给定的:

  • PT_LOAD - 可加载的段,包含了程序运行时内存中的数据 (代码、数据)
  • PT_INTERP - 用于组装完整应用程序所需要的链接器。
  • PT_GNU_STACK - (只适用于 Linux) 如果没有提供含有这个类型的项,说明程序的当前栈是可执行的。

加载 ELF:

  1. 读取 ELF 首部
  2. 查找能够引导可执行镜像的包含代码和数据的程序头段

解析 ELF 可执行文件:

  1. 检查缓冲区大小是否能容纳 ELF 首部和程序段首部信息
  2. 检查 ELF 魔数
  3. 检查程序中的最大段号是否合法
  4. 解析各段和入口信息
  5. 根据程序头表中的信息从解析的结果中找出相应内容,填充形成对应结构

重定位:

  1. 查看 ELF 首部
  2. 获得加载地址
  3. 为程序的各个分段分配空间
  4. 从内存的程序镜像中复制数据到对应的已分配空间中
  5. 解析外部引用中设计的内核符号表
  6. 以首部的入口地址为基址加上偏移量找到对应项的入口地址,或是执行符号查找。之后也能加载上对应的驱动程序了

Github 项目:

LIEF – Library to Instrument Executable Formats

信息来源:

书籍:

Learning Linux Binary Analysis