ELF文件结构
ELF文件的全称是Executable and Linkable Format,直译为“可执行可链接格式”,包括目标文件(.o)、可执行文件(可以直接运行)、静态链接库、动态链接库、核心转储文件(core dump)。ELF文件的定义可以在/usr/include/elf.h
中找到,本文主要介绍ELF64,ELF文件通常由下列部分组成:
-
ELF头(ELF header):放在ELF文件开头,描述该文件信息。
-
节头表(Section header table):包含对节(section)的描述,对于可重定位文件(relocatable files)是必须的,对于可装载文件(loadable files)是可选的。
-
程序头表(Program header table):对于可装载文件(loadable files)是必须的,对于可重定位文件(relocatable files)是可选的。用来描述加载程序或动态链接库所需要的段(segments)和其他数据结构。
-
节或段的内容,包括符号表等。
ELF头
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
Elf64_Ehdr中的数据结构含义如下:
数据结构名称 | 大小(byte) | 对齐(byte) | 目标 |
---|---|---|---|
Elf64_Addr | 8 | 8 | Unsigned program address |
Elf64_Off | 8 | 8 | Unsigned file offset |
Elf64_Half | 2 | 2 | Unsigned medium integer |
Elf64_Word | 4 | 4 | Unsigned integer |
Elf64_Sword | 4 | 4 | Signed integer |
Elf64_Xword | 8 | 8 | Unsigned long integer |
Elf64_Sxword | 8 | 8 | Signed long integer |
unsigned char | 1 | 1 | Unsigned small integer |
我们用readelf -h hello.o
看一下从源文件到可执行文件:源文件的预处理、编译、汇编、链接中生成的hello.o
文件的ELF头(因为我机器上显示的结果是中文,所以接下来就按照中文来说明,比如ELF头中类别对应Class,类型对应Type)。
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 864 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
ELF头一开始的位置是魔术字符(Magic),在ASCII码中,'E'、'L'、'F'分别对应45、4c、46。当文件被映射到内存中,可以通过魔术字符确定映射地址。 ELF头与Elf64_Ehdr存在对应关系:
Elf64_Ehdr成员 | ELF头 | 含义 |
---|---|---|
e_ident | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 | |
e_type | 类型: REL (可重定位文件) | ELF文件类型(包括Relocatable/Executable/Shared/Core等类型) |
e_machine | 系统架构: Advanced Micro Devices X86-64 | |
e_version | 版本: 0x1 | 版本号,通常为0x1 |
e_entry | 入口点地址: 0x0 | 程序入口点的虚拟地址,操作系统在加载完程序后从该地址开始执行进程的指令。可重定位文件没有入口地址,所以为0。用readelf命令查看前文生成的可执行文件可以看到入口地址 |
e_phoff | 程序头起点:0 (bytes into file) | 程序头表偏移(单位:byte) |
e_shoff | Start of section headers: 864 (bytes into file) | 节头表偏移(单位:byte) |
e_flags | 标志:0x0 | 特定于处理器的标识 |
e_ehsize | Size of this header: 64 (bytes) | ELF头本身的大小(单位:byte) |
e_phentsize | Size of program headers: 0 (bytes) | 程序头大小(单位:byte) |
e_phnum | Number of program headers: 0 | 程序头个数 |
e_shentsize | Size of section headers: 64 (bytes) | 节头大小(单位:byte) |
e_shnum | Number of section headers: 14 | 节头个数 |
e_shstrndx | Section header string table index: 13 | 字符串表在节头表中索引 |
对于魔数字符,再展开介绍一下。Magic共16个字节(Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00),第0~3个字节标识文件;第4个字节标识文件类别(类别: ELF64),1表示32位,2标识64位;第5个字节标识文件数据编码(数据: 2 补码,小端序 (little endian)),1标识小端序,2表示大端序;第6个字节标识文件版本(Version: 1 (current)),值为1;第7个字节标识操作系统和ABI(OS/ABI: UNIX - System V),0表示System V ABI,1表示HP-UX operating system,255表示Standalone (embedded) application;第8个字节标识ABI版本(ABI 版本: 0),值为1;剩余字节被保留为将来使用,设置为0。
节头表
一个目标文件(包括Relocatable/Executable/Shared/Core等类型)中包含很多节,这些节的信息保存在节头表中,表的每一项都是一个Elf64_Shdr结构体(也称为节描述符),节点信息包括节名、节大小、在文件中的偏移、读写权限等,编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的。/usr/include/elf.h
中的Elf64_Shdr内容如下:
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
通过readelf -S hello.o
命令查看hello.o文件的节头表。
There are 14 section headers, starting at offset 0x360:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000027 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000270
0000000000000060 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000067
0000000000000019 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000080
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ac
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 000000b0
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000002d0
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000108
0000000000000138 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000240
0000000000000029 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000002e8
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
正如ELF头中内容所示,hello.o中包含14个节。节名(sh_name)是一个单位为字节的偏移量,表示相对于节名字符串表(section name string table,也就是.shstrtab)起点的偏移,节名实际存放在节名字符串表中,通过查表得到。节类型(sh_type)分为以下几类,readelf命令的结果省略了前缀SHT_。
节类型 | 含义 |
---|---|
SHT_NULL | 无效节 |
SHT_PROGBITS | 程序节。代码节、数据节都是这种类型 |
SHT_SYMTAB | 符号表 |
SHT_STRTAB | 字符串表 |
SHT_RELA | 重定位表 |
SHT_HASH | 符号表的哈希表 |
SHT_DYNAMIC | 动态链接信息 |
SHT_NOTE | 提示性信息 |
SHT_NOBITS | 表示该节在文件中没有内容,不占用空间 |
SHT_REL | 重定位信息 |
SHT_SHLIB | 保留 |
SHT_DYNSYM | 动态链接的符号表 |
SHT_LOOS/SHT_HIOS | 特定环境使用 |
SHT_LOPROC/SHT_HIPROC | 特定处理器使用 |
节标志位(sh_flags)表示该节在进程虚拟地址空间中的属性,1表示可写,2表示该节在进程空间中需要分配空间,有些包含指示或者控制信息的节不需要在进程分配空间,就没有这个标志,4表示该节在进程空间中可以被执行。
节链接信息(sh_link、sh_info),如果节的类型是与链接相关的(无论是动态链接还是静态链接),如重定位表、符号表等,则sh_link、sh_info两个成员所包含的意义如下所示。其他类型的节,这两个成员没有意义。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该节所使用的字符串表在节头表中的下标 | 0 |
SHT_HASH | 该节所使用的符号表在节头表中的下标 | 0 |
SHT_REL | 该节所使用的相应符号表在节头表中的下标 | 该重定位表所作用的节在节头表中的下标 |
SHT_RELA | 该节所使用的相应符号表在节头表中的下标 | 该重定位表所作用的节在节头表中的下标 |
SHT_SYMTAB | 操作系统相关 | 操作系统相关 |
SHT_DYNSYM | 操作系统相关 | 操作系统相关 |
other | SHN_UNDEF | 0 |
重要的节
-
.text节
.text节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr,则.text节就会存在于text段中。由于.text节保存了程序代码,所以节类型为SHT_PROGBITS。 -
.rodata节
rodata节保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,所以节类型为SHT_PROGBITS。 -
.plt节(过程链接表)
.plt节也称为过程链接表(Procedure Linkage Table),其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt节保存了代码,所以节类型为SHT_PROGBITS。 -
.data节
.data节存在于data段中,其保存了初始化的全局变量和局部静态变量等数据。由于.data节保存了程序的变量数据,所以节类型为SHT_PROGBITS。 -
.bss节
.bss节存在于data段中,占用空间不超过4字节,仅表示这个节本身的空间。.bss节保存了未进行初始化的全局数据和局部静态变量,程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,所以节类型为SHT_NOBITS。 -
.got.plt节(全局偏移表-过程链接表)
.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got.plt节与程序执行有关,所以节类型为SHT_PROGBITS。 -
.dynsym节(动态链接符号表)
.dynsym节保存在text段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM。 -
.dynstr节(动态链接字符串表)
.dynstr保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。 -
.rel.*节(重定位表)
重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对ELF目标文件的某部分或者进程镜像进行补充或修改。由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL。 -
.hash节
.hash节也称为.gnu.hash,其保存了一个用于查找符号的散列表。 -
.symtab节(符号表)
.symtab节是一个ElfN_Sym的数组,保存了符号信息。节类型为SHT_SYMTAB。 -
.strtab节(字符串表)
.strtab节保存的是符号字符串表,表中的内容会被.symtab的ElfN_Sym结构中的st_name引用。节类型为SHT_STRTAB。 -
.ctors节和.dtors节
.ctors(构造器)节和.dtors(析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在main函数执行之前需要执行的代码;析构函数是在main函数之后需要执行的代码。
符号表包括.dynsym
和.symtab
,前者是后者的子集。.dynsym
保存了引用自外部文件的符号,只能在运行时被解析(flag为Alloc),而.symtab
还保存了本地符号,用于调试和链接,不会被装载到内存中。
程序头表
在/usr/include/elf.h
,程序头表的结构如下:
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
由于hello.o没有程序头表,所以通过readelf -l hello
来读取可执行文件hello的程序头表,如下所示。
Elf 文件类型为 DYN (共享目标文件)
Entry point 0x1060
There are 13 program headers, starting at offset 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000005f8 0x00000000000005f8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001f5 0x00000000000001f5 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000170 0x0000000000000170 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002020 0x0000000000002020 0x0000000000002020
0x0000000000000044 0x0000000000000044 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
与节类似,段也有几种不同的类型(p_type),如下:
段类型 | 含义 |
---|---|
PT_NULL | 无效段 |
PT_LOAD | 可加载段 |
PT_DYNAMIC | 动态链接表 |
PT_INTERP | 程序解释器路径名 |
PT_NOTE | 信息段 |
PT_SHLIB | 保留 |
PT_PHDR | 程序头表 |
PT_LOOS / PT_HIOS | 特定环境使用 |
PT_LOPROC / PT_HIPROC | 特定处理机使用 |
段标志位(p_flags)表示权限,X表示执行允许,W表示写允许,R表示读允许。
最后再说一下节(section)和(segment)的关系。每个段包括一个或多个节,因为系统不关心这些节的具体内容,只关心这些节的权限(读、写、执行),将具有相同权限的节放到同一个段中。节是链接视角下的ELF文件,段是运行视角下的ELF文件。
参考资料
ELF-64 Object File Format
计算机那些事(4)—— ELF文件结构
CTF竞赛权威指南(Pwn篇)(杨超 编著,吴石 eee战队 审校,电子工业出版社)