深入理解ELF文件格式(一)
ELF 是 Executable and Linking Format 的缩写,它是 Linux 平台上通用的二进制文件格式。在 Android 的 NDK 开发中,几乎都是和 ELF 打交道,因为理解 ELF 格式对我们深入安全领域有很大的帮助。
前言
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 的缩写。其中名称中的 Executable
和 Linking
表明 ELF 有两种重要的特性。
-
Executable: 可执行的。ELF 文件将参与程序的执行(Execution)过程。包括二进制程序的运行以及动态库 .so 文件的加载。
-
Linking: 可连接的。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,有兴趣深入的小伙伴可以参考这份资料。
更多推荐
所有评论(0)