ELF文件结构


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文件结构

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战队 审校,电子工业出版社)

发表评论