前言

ELF 是 Executable and Linking Format 的缩写,它是 Linux 平台上通用的二进制文件格式。在 Android 的 NDK 开发中,几乎都是和 ELF 打交道,比如:

  • c / c++ 文件编译得到的 .o(或者 .obj)文件就是 ELF 格式的文件;
  • 动态库(.so)文件、可执行文件也是 ELF 文件;
  • 动态库的字符串擦除、动态库加壳、动态库修复等都离不开 ELF;

笔者在学习 ELF 格式的时候,为了加深对 ELF 格式的理解,创建了一个分别通过用 C 和 Java 解析 ELF 的文件 Android工程,有兴趣的小伙伴可以直接戳链接查看。

ELF文件格式

前面提到 ELF 是 Executable and Linking Format 的缩写。其中名称中的 ExecutableLinking 表明 ELF 有两种重要的特性。

  • Executable: 可执行的。ELF 文件将参与程序的执行(Execution)过程。包括二进制程序的运行以及动态库 .so 文件的加载。

  • Linking: 可连接的。ELF 文件参与编译链接过程。

    ELF文件视图

上面两种视图表示 ELF 格式可以通过两种角度(View)来对其进行分析。个人觉得这两种视图只是提供 ELF 格式它是如何布局 table 的,但实际上如 Linking View 它的 Program Header Table 是通过 ELF 头文件来确定每个 program_table_element 的,也就是说 Program Header Table 是概念上的意义,不真实存在。当然,Section Header table 也同样如此。

ELF Header

ELF 文件支持 64 位和 32 位的 CPU 指令架构,ELF 是通过定义更长的字段类型(相对 32 位)来支持 64 位的。下文主要是通过对 32 位的 ELF 文件规范进行分析。

#define EI_NIDENT 16
typedef struct elf32_hdr {
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half e_type;
  Elf32_Half e_machine;
  Elf32_Word e_version;
  Elf32_Addr e_entry;
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
  Elf32_Half e_ehsize;
  Elf32_Half e_phentsize;
  Elf32_Half e_phnum;
  Elf32_Half e_shentsize;
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
} Elf32_Ehdr;

下面对上述代码片段进行解析:

e_ident[EI_NIDENT] e_ident为 16 个字节长度的数组:

  • e_ident[0-3] 前 4 个字节表示 Magic Number,分别取值为 ‘0x7f’、‘E’、‘L’、‘F’,一般用于校验是否为 ELF 文件。
  • e_ident[EI_CLASS=4] 表示该 ELF 文件是 32 位文件(取值为 1)还是 64 位文件(取值为 2)。
  • e_ident[EI_DATA=5] 表示该 ELF 文件的数据的字节序是小端序(取值为 1)还是大端序(取值为 2)。
  • e_ident[EI_VERSION=5] 表示 ELF 文件的版本,通常取值为 1。
  • e_ident[6-15] 目前置为零,做字节对齐用。

**e_type ** 该字段长度为 2 个字节,表示 ELF 的类型。
e_machine 该字段长度为 2 个字节,表示该 ELF 文件对应哪种 CPU 架构。

e_version 该字段取值同 e_ident[EI_VERSION=5]。

e_entry 该字段表示程序入口的虚拟地址。当该 ELF 文件为可执行文件的时候,操作系统加载它后会跳到 e_entry 的位置去执行程序的代码。

e_phoff ph 是 program header 的缩写。e_phoff 表示 program header 第一个元素起始的位置(记录的是偏移量),值得注意的是 program header 的元素是连续的。

e_shoff sh 是 section header 的缩写。同 e_phoff 作用类似,如果该 ELF 包含 Section 的话,该变量表示 Section 元素在文件的起始位置。

e_flags 表示处理器相关特定的标志位。

e_ehsize eh 是 elf header 的缩写,表示 ELF 文件头的大小。

e_phentsize 表示 program header’s entry size。

e_phnum 表示 program header number。

e_shentsize 表示 sections header’s entry size。

e_shnum 表示 sections header 的数量。

e_shstrndx 关联每个 section 名称的字符串表,通过 section 的 sh_name 做下标索引。如果 section 没有名称,则此值应设置为 SHN_UNDEF。

Program Header

typedef struct elf32_phdr {
  Elf32_Word p_type;
  Elf32_Off p_offset;
  Elf32_Addr p_vaddr;
  Elf32_Addr p_paddr;
  Elf32_Word p_filesz;
  Elf32_Word p_memsz;
  Elf32_Word p_flags;
  Elf32_Word p_align;
} Elf32_Phdr;

Execution View 中 ELF 必须包含 Program Header 元素。Program Header 的每个字段解析如下:

p_type program_table_element (segment)的类型。
p_offset 该 program_table_element (segment)位于文件的起始位置。

p_vaddr 该 program_table_element (segment)加载进虚拟内存是指定为相对内存位置。
p_paddr 表示 program_table_element (segment)对应的物理地址。对于可执行文件和动态库而言,这个值并没有意义。
p_filesz 表示 program_table_element (segment)在文件中占据的大小,其值可以为 0。因为 segment 是由 section 组成的,而有些 section 是不占空间的。
p_memsz 该 program_table_element (segment)在内存中占据的空间,其值可以为 0。
p_flags segment 的标志。
p_align program_table_element (segment)加载进内存以后需要按照 p_align 的要求对其。

Section Header

typedef struct elf32_shdr {
  Elf32_Word sh_name;
  Elf32_Word sh_type;
  Elf32_Word sh_flags;
  Elf32_Addr sh_addr;
  Elf32_Off sh_offset;
  Elf32_Word sh_size;
  Elf32_Word sh_link;
  Elf32_Word sh_info;
  Elf32_Word sh_addralign;
  Elf32_Word sh_entsize;
} Elf32_Shdr;

Section Header 元素的数据结构如上代码片段。
sh_name 该变量指定 Section 的名称。ELF 有一个专门存储 Section 名字的 Section(Section Header String Table Section,简写为 shstrtab)。这里的 sh_name 指向 shstrtab 的某个位置(可以理解为 sh_name 为 shstrtab 的下标),该位置存储了本 Section 名字的字符串。
sh_type Section 的类型,不同类型的Section存储不同的内容。
sh_flags Section 的属性。
sh_addr 如果该Section被加载到内存的话(可执行程序或动态库),sh_addr指明应该加载到内存什么位置。
sh_offset 表明该 Section 相对于文件的起始位置。
sh_size Section 本身的大小。

字符串表

上面提到 ELF 有一个专门存储 Section 名字的 Section(Section Header String Table Section,简写为 shstrtab),该 Section 即表示的是字符串表的信息。在目标文件中, 这些字符串通常是符号的名字或者节的名字。在目标文件的其它部分中,当需要引 用某个字符串时,只需要提供该字符串在字符串表中的序号即可。

字符串表中的第一个字符串(序号为 0)永远是空串,即 null,它可以用于表 示一个空的名字或者没有名字。所以,字符串表的第一个字节是 \0。由于每一个字符串都是以 null 结尾,所以字符串表的最后一个字节也必然为 null

字符串表也是可以为空的,不含任何的字符串,但是 ELF 文件头中的 sh_size 必须为零。

字符串表

从上图可以看出,通过下标可以引用一个完整定义的字符串,即被 \0 包裹的整个串,也可以为它的一部分。

小结

本文主要围绕着 ELF 格式来详述 ELF 格式规范,暂还没深入到它的每一个细节。主要是先提供一个 ELF 文件格式的一个大致的布局视图,这样后面才可以更好的往里面补充完善知识点。上文主要参考的是 elf - format of Executable and Linking Format (ELF) files,有兴趣深入的小伙伴可以参考这份资料。

Logo

更多推荐