感觉这一节基本上写成文档的翻译了……

# ELF 文件格式

ELF (Executable and Linking Format) 是 IA-32 架构下的可移植的对象文件格式。

ELF 文件分为了三个类型:

  • 可重定向文件 (relocatable file):包含了代码、数据,用来和其他对象文件链接,构成可执行文件或者共享文件。
  • 可执行文件 (executable file):包含了可以执行的程序,这个文件也说明了如何去创建进程的镜像。
  • 共享对象文件 (shared object file):可以被链接器和其他的可重定向文件或者共享对象文件创建成新的对象文件。

对象文件参与程序链接和程序的执行,对于这两件事情,对象文件格式提供了两个并行的视图也就是链接视图和执行视图:

其中,**ELF 头 (ELF header)** 在最开始部分,其中存放了程序的文件组织结构信息。** 节 (section)** 用于链接视图,其中存放了链接视图需要的大部分信息,包括了指令、数据、符号表和重定向信息等等。** 段 (segment)** 则是用于执行视图,包括代码段、数据段等等。

** 程序头表 (program header table)** 用来告知系统如何去创建一个进程的镜像,如果一个文件需要用来执行(也就是可执行文件),那么它就必须得有一个程序头表,而像可重定向文件就不一定要有。** 节头表 (section header table)** 则是包括了描述文件中各个节的信息,每个节在表中都有对应的一个表项,其中包括节的名称、大小等信息。如果一个文件需要被用来链接,那么它就必须得有节头表。

在一个 ELF 文件中,上面的这些部分除了 ELF 头必须位于最开始固定的位置,其他的各个部分顺序实际上都是可以打乱的。

# 数据类型

32 位的对象文件中有这些数据类型:

名称大小对齐用途
Elf32_Addr44无符号 程序地址
Elf32_Half22无符号 中等大小整数
Elf32_Off44无符号 文件偏移
Elf32_Sword44有符号 大整数
Elf32_Word44无符号 大整数
unsigned char11无符号 小整数

在必要的情况下,数据中会增加填充来保证 4 字节的对象以 4 字节为单位对齐。

# ELF 头

ELF 头的定义如下:

#define EI_NIDENT   16
typedef struct{
    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;
}

这些字段的作用分别是:

# e_ident

全名是 ELF Identification,实际上就是 magic number 和一些其他的信息,用来标识文件的类型是对象文件,还有机器无关的数据用来解码和翻译文件的内容。

ELF 文件格式这个框架实际上是支持多种处理器架构和编码方式的, e_ident 这个字段的作用也就是为了这个兼容性。

e_ident 的前四字节就是 magic number,用来标识这个文件是 ELF 文件:

名称
ELFMAG00x7f
ELFMAG1'E'(0x45)
ELFMAG2'L'(0x4c)
ELFMAG3'F'(0x46)

第五个字节即 e_ident [4] 是 EI_CLASS ,标识了文件对象是 32 位或是 64 位:

名称含义
ELFCLASSNONE0无效的文件类型
ELFCLASS32132 位文件
ELFCLASS64264 位文件

第六个字节即 e_ident [5] 是 EI_DATA ,标识了文件对象的字节序:

名称含义
ELFDATANONE0无效的字节序
ELFDATA2LSB1小端字节序
ELFDATA2MSB2大端字节序

第七个字节即 e_ident [6] 为 EI_VERSION ,即 ELF 的文件版本。

e_ident 中的剩余部分为 EI_PAD ,作为保留部分填充为 0,尚未使用。

以一个程序为例,我们使用 readelf 读取其 ELF 头部分信息,其中 e_ident 字段和其中的解析结果为:

7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别:                              ELF64
数据:                              2 补码,小端序 (little endian)
Version:                          1 (current)

还挺清楚的哈。

# e_type

这个字段标识了对象文件的类型:

名称含义
ET_NONE0无类型
ET_REL1可重定位文件
ET_EXEC2可执行文件
ET_DYN3共享对象文件
ET_CORE4核心转储
ET_LOPROC0xff00针对处理器特定
ET_HIPROC0xffff针对处理器特定

# e_machine

这个字段标识了文件对应的处理器架构:

名称含义
EM_NONE0
EM_M321AT&T WE 32100
EM_SPARC2SPARC
EM_3863Intel 架构
EM_68K4Motorola 68000
EM_88K5Motorola 88000
EM_8607Intel 80860
EM_MIPS8MIPS RS3000 大端序
EM_MIPS_RS4_BE10MIPS RS4000 大端序
RESERVED11-16保留

# e_version

这个字段指定了对象文件的版本。包括了两个值: EV_NONE 即 0,代表无效版本; EV_CURRENT 即 1 代表了当前版本。

# e_entry

这个字段给了系统转交控制的虚拟地址,也就是进程开始的地址。

如果程序没有入口点,那么这个参数的字段就是 0。

# e_phoff

这个字段是程序头表位置的偏移量。如果没有程序头表,那么这个字段值为 0。

# e_shoff

这个字段是节头表位置的偏移量。如果没有节头表,那么这个字段值为 0。

# e_flags

这个字段是文件有关的处理器特定的标识。

# e_ehsize

这个字段是 ELF 头的长度(以字节为单位)

# e_phentsize

这个字段是文件的程序头表中一个条目的长度,以字节为单位。表中的每个条目的长度都是相等的。

# e_phnum

这个字段标识了程序头表中的条目数。如果没有程序头表,那么这个字段值为 0。通过和 e_phentsize 字段相乘,可以计算出程序头表的大小。

# e_shentsize

这个字段是文件的节头表中一个条目的长度,和前面的类似。

# e_shnum

这个字段是节头表中的条目数,和前面计算类似。

找了一个例子:

gcc hello.c -o hello -mtune=i386 -m32
$ hexdump -C -n 52 hello                                                                                                             0 [23:33:44]
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 03 00 01 00 00 00  60 10 00 00 34 00 00 00  |........`...4...|
00000020  9c 36 00 00 00 00 00 00  34 00 20 00 0c 00 28 00  |.6......4. ...(.|
00000030  1e 00 1d 00                                       |....|
00000034
ELF 头:
  Magic:  7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF32
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              DYN (共享目标文件)
  系统架构:                          Intel 80386
  版本:                              0x1
  入口点地址:              0x1060
  程序头起点:              52 (bytes into file)
  Start of section headers:          13980 (bytes into file)
  标志:             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         12
  Size of section headers:           40 (bytes)
  Number of section headers:         30
  Section header string table index: 29

作为对照,可以看到 ELF 头各个字段的意义。注意,这里数据是小端序的,比如说 60100000 实际上是 0x00001060 ,需要区分一下。

# 节头表

对象文件的节头表是一系列 Elf32_Shdr 结构体构成的数组,给出了各个节的相关信息。

这个数组中一些下标是保留的,对象文件不能使用这些下标:

名称描述
SHN_UNDEF0未定义、缺失、无意义等
SHN_LORESERVE0xff00指定了保留下标的下界
SHN_LOPROC ~ SHN_HIPROC0xff00~0xff1f保留给处理器特定的语义
SHN_ABS0xfff1指定了相应参考的绝对值。相对于这个节号定义的符号不受到重定位的影响
SHN_COMMON0xfff2通用符号
SHN_HIRESERVE0xffff指定了保留下标的上界

节中包括了对象文件除了 ELF 头、程序头表和节头表之外的所有信息。节还需要满足这些条件:

  • 每个节都有一个来描述它的节头(可能存在一个节头没有对应的节)。
  • 每个节都占据一段连续字节的空间(也可能是 0)。
  • 节不能重叠。
  • 头部和节可能不会覆盖文件中的所有空间。

节头的数据结构定义如下:

typedef struct{
  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;

# 具体属性定义

# sh_name

这个字段是节的名字,其中的值这是指向节头的字符串节的地址,对应一个以 0 结尾的字符串。

# sh_type

这个字段表述了节的内容和语义。其中包括了下面这些类型:

名称描述
SHT_NULL0这个节头是 inactive 的,没有其对应的节
SHT_PROGBITS1这个节是程序定义的信息,格式和意义取决于程序
SHT_SYMTAB2符号表
SHT_STRTAB3字符串表
SHT_RELA4有确定的加数的重定位信息,如 ELF32_Rela
SHT_HASH5符号哈希表
SHT_DYNAMIC6动态链接信息
SHT_NOTE7文件标记信息 (information that mark the file)
SHT_NOBITS8在文件中不占用空间,其他类似 SHT_PROGBITS 。尽管这个节不会占据空间,它也会有一个理论上的大小。
SHT_REL9没有确定加数的重定位信息,如 ELF32_Rel
SHT_SHLIB10保留
SHT_DYNSYM11符号表
SHT_LOPROC ~ SHT_HIPROC0x70000000~0x7fffffff保留给处理器特定
SHT_LOUSER0x80000000为程序保留的地址的下界
SHT_HIUSER0xffffffff为程序保留的地址的上界

# sh_flags

表示了各种杂项的 1 比特标识:

名称作用
SHF_WRITE0x1节中存放了在进程执行时可写的数据.
SHF_ALLOC0x2标识了这个节在程序运行时是否会占据内存。有一些控制节是不会驻留在内存镜像中的,这些节的这个属性就会被关闭.
SHF_EXECINSTR0x4标识了这个节含有可执行的机器指令.
SHF_MASKPROC0xf0000000保留给处理器

# sh_addr

如果一个节在会出现在进程的内存镜像中,那么这个成员的值就是节中第一个字节在内存中的地址,否则就是 0。

# sh_offset

从文件开始到这个节头对应的节的第一个字节的偏移量。

上面提到了不占据空间的 SHT_NOBITS 类型,对于这种类型,这个成员的值是这个节在理论上占据的位置。

# sh_size

这个成员以字节为单位给出了节的大小。如果是不占据空间的 SHT_NOBITS 节,这个属性也可能不为 0,只是实际上这个节不占据空间罢了。

# sh_link 和 sh_info

sh_link 是节头表的索引链接, sh_info 是这个节的额外信息。这两个字段的含义取决于节的类型:

| sh_type | sh_link | sh_info |
| SHT_DYNAMIC | 这个节中条目使用到的字符串表在节头表中的下标 | 0|
| SHT_HASH | 这个节中哈希表应用到的符号表在节头表中的下标 | 0|
| SHT_RELSHT_RELA | 相关的符号表在节头表中的下标 | 重定向影响到的节在节头表中的下标 |
| SHT_SYMTABSHT_DYNSYM | 内容和操作系统有关 | 内容和操作系统有关 |
| 其他 | SHN_UNDEF |0|

# sh_addralign

有一些节要求地址对其。这个属性给出了对齐的单位, sh_addr 属性必须是这个值的整数倍。

这个属性的值应该是 0 或者 2 的整数次幂,如果值为 0 或者 1,则表示不需要对齐。

# sh_entsize

如符号表等一些节中的内容是固定长度的一系列条目,对于这样的节,这个属性给出了以字节为单位的每个条目的大小。如果是 0,则表示这个节中没有放置固定长度条目表。

# Index 0 节

前面提到了下标为 0 的节是 SHN_UNDEF ,但是这个节实际上是存在的,也就是说比如这个程序有 4 个节,那么下标是 0~3,其中也就包括了这个 SHN_UNDEF

其中的条目是这样的:

属性作用
sh_name0没有名称
sh_typeSHT_NULLinactive
sh_flags0没有标志
sh_addr0没有地址
sh_offset0没有偏移
sh_size0没有大小
sh_linkSHN_UNDEF没有链接
sh_info0没有信息
sh_addralign0没有对齐
sh_entsize0没有条目

总之啥也没有。

# 特殊的节

可执行文件的创建过程需要经过链接,链接器可以解决不同对象文件之间的依赖关系、调整对象文件之间的引用并重定位代码。这一个过程需要一些特定的节内部存储的信息,如 .dynamic 节中的信息。

实际上,操作系统支持两种链接方式,即动态链接和静态链接。

  • 静态链接:静态地绑定一系列的对象文件、系统库和其他的库文件,解析引用关系并且创建一个自包含的可执行文件。
  • 动态链接:将一系列对象文件、库、系统资源和其他的共享库链接在一起创建一个可执行文件。在加载这个可执行文件的时候,那些动态链接库和共享资源也必须可用。

此外,还有一些节用来支持调试、程序控制等等。

下面是这些节的作用:

# .bss

这个节存储了没有初始化的全局数据,在程序开始运行时,系统会将这些数据置为 0。

这个节实际并不占据文件空间( SHT_NOBITS )。

# .comment

包含了版本控制信息

# .data 和.data1

包含了已经初始化的全局数据。

# .debug

包含了用于符号调试的信息。

# .dynamic

包含了动态链接信息。

# .hash

表明这个节里面有符号哈希表。

# .line

包含了用于调试的行号等信息。

# .note

包含了程序文件标记相关的信息。

# .rodata 和.rodata1

程序中的只读数据。

# .shstrtab

节的名称。

# .strtab

这个节中包括了主要是和符号表有关的字符串。

# .symtab

这个节中包括了符号表。

# .text

这个节中包括了可执行的指令。

以一个程序( readelf -S )为例,其中包括了下面这些节:

# 字符串表

字符串表中存储的是以零字符结尾的字符串,其中包括了符号的名字、节的名字等等。对于字符串表的引用就是一个在字符串表中的下标。

字符串表的第一个字符是零字符,如果一个名称指向这个字符,则表示没有名称或者这个名称为空。同时,字符串表最后一个字符是零字符,这样的目的是使得所有的字符串都能够正常结束。

实际上字符串表可以为空(其 sh_size 字段为 0),这样,但是所有对这个字符串表的引用下标必须都是 0。

通过 readelf -x 命令读取字符串表 .strtab 所在的节,能够得到下面的信息:

可以看到其开头和结尾都是字节 0x00,每个字符串的结束也都是字节 0x00。

# 程序头表

程序头表中存储的一系列数据结构可以用来告知系统执行这个程序需要那些信息。表中的每一个条目代表了对应的一个段的有关信息,一个段可以包括多个节。

程序头表中单个的数据结构定义是这样的:

typedef struct {
  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;
}

其中各个字段的作用如下:

字段名称描述
p_type对应的段的类型或者如何解释这个段中的元素
p_offset段第一个字节相对于文件开始位置的偏移
p_vaddr段在内存中驻留的第一个字节的位置
p_paddr在物理寻址相关的系统上,用来作为段的物理地址(针对特定系统)
p_filesz以字节为单位的,段的大小(可以是 0)
p_memsz以字节为单位的,段需要占据的内存空间(可以是 0)
p_flags段的标志
p_align段对齐,具体和前面节的对齐类似

# p_type

根据 p_type 的取值,段有这些类型:

类型描述
PT_NULL0未使用或者未定义的。
PT_LOAD1表明一个可以加载的段,这个段中的字节会映射到内存中。如果段分配的内存大于文件实际的大小,多余的空间会被置 0。当然分配的内存空间一定要大于文件中段的大小。数据段、代码段都是这种类型。
PT_DYNAMIC2标记了动态链接信息。
PT_INTERP3其中指明了解释器的路径。
PT_NOTE4指明了辅助信息的位置和文件的大小。
PT_SHLIB5保留。
PT_PHDR6指明了在文件中和在内存中程序头表的位置和大小。
PT_LOPROC ~ PT_HIPROC0x70000000~0x7fffffff保留给处理器特定语义

通过 readelf -l 读取一个程序的程序头表信息:

# 总结

这边其实还有好多可以说的内容,比如说 ELF 的符号表、重定向这些,以及程序的链接和装载等,以后开一个坑出来专门讲。

# 参考资料

[1] 《软件逆向工程 —— 原理与实践》,孙聪 等著,西安电子科技大学出版社。

[2] Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification, v1.2

此文章已被阅读次数:正在加载...更新于