在Linux、BSD变体以及其他OS中,ELF格式可用于可执行文件、共享库、目标文件、coredump文件,甚至内核引导镜像文件。在Linux中,程序就是以ELF二进制的格式执行的。

0x01 ELF文件类型

一个ELF文件可以被标记为一下几种类型之一。

  • ET_NONE:未知类型。这个标记表明文件类型不确定,或者未定义。
  • ET_REL:重定位文件。ELF类型标记为relocatable意味着该文件被标记为了一段可重定位的代码,有时也称为目标文件。可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码(position independent code)。在编译完代码之后通常可以看到一个.o格式的文件,这种文件包含了创建可执行文件所需要的代码和数据。
  • ET_EXEC:可执行文件。executable,表明该文件被标记为可执行文件,也称为程序,是一个进程开始执行的入口。
  • ET_DYN:共享目标文件。dynamic,意味着文件被标记为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在程序运行时被装载并链接到程序的进程镜像中。
  • ET_CORE:核心文件。在程序崩溃或者进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用GDB读取这类文件来辅助调试并查找程序崩溃的原因。

使用readelf -h命令查看ELF文件,可以看到原始的ELF文件头。ELF文件头从文件的0偏移量开始,是除了文件头之后剩余部分文件的一个映射。文件头主要标记了ELF类型、结构和程序开始执行的入口地址,并提供了其他ELF头(节头和程序头)的偏移量。

可以通过查看Linux的ELF(5)手册来了解ELF头部结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff;
ElfN_Off e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} ElfN_Ehdr;

0x02 ELF程序头

ELF程序头是对二进制文件中的描述,是程序装载必需的一部分。

段(segment)是在内核装载时被解析的,描述了磁盘上可执行文件的内存布局以及如何映射到内存中。可以通过引用原始ELF头中名为e_phoff(程序头表偏移量)来得到程序头表(如前面的ElfN_Ehdr结构中所示)。

程序头描述了可执行文件(包括共享库)中的段及其类型。

Elf32_Phdr结构体如下,它构成了32位ELF可执行程序头表的一个程序头条目:

1
2
3
4
5
6
7
8
9
10
typedef struct {
uint32_t p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
} Elf32_Phdr;

下面说下5种常见的程序头类型。

2.1 PT_LOAD

一个可执行文件至少有一个PT_LOAD类型的段,该段描述的是可装载的段,即该类段将被装载或者映射到内存中。