ELF
hahahanba Lv1

ELF文件结构描述

一个 ELF 文件通常有ELF文件头、段表、字符串表、符号表等组成,其总体结构如下图所示。不同的文件部分描述不同的文件内容:

  • ELF文件头(ELF Header),它包含了描述整个文件的基本属性,如ELF文件版本、程序入口地址等。
  • ELF 文件中的各个段(section)。段表(section header table)描述了 ELF 文件包含的所有段的信息,包含每个段的段名、段的长度、在文件中的偏移量等属性。
  • 其他一些辅助结构,如字符串表,符号表等。

image

ELF Header

在ELF文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。我们可以使用readelf命令来详细查看ELF文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h SimpleSection.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1104 (bytes into file)
Flags: 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: 13
Section header string table index: 12

ELF文件头结构及相关常数被定义在/usr/include/elf.h中,为了兼容各种平台ELF文件有32位版本和64位版本,分别叫做Elf32_EhdrElf64_Ehdr。同时elf.h使用typedef定义了一套自己的变量体系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Type for a 16-bit quantity.  */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;

/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef int32_t Elf64_Sword;

/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;

/* Type of addresses. */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;

/* Type of file offsets. */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;

/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;

/* Type for version symbol information. */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT (16)
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;

ELF魔数

我们可以看到readelf的输出中最前面的Magic的16个字节刚好对应Elf64_Ehdre_ident这个成员。这16个字节用来标识ELF文件的平台属性,比如ELF字长、字节序、ELF文件版本等。

1
Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F0x450x4c0x46,第一个字节对应于ASCII字符里的DEL控制符,后面三个字对应ELF这3个字母的ASCII码。这四个字节又被称为ELF文件的魔数。通过对魔数的判断可以确定文件的格式和类型。如果被执行的是Shell脚本或perl、python等解释型语言的脚本,那么它的第一行往往是#!/bin/sh#!/usr/bin/perl#!/usr/bin/python,此时前两个字节#!就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。

接下来的一个字节是用来标识ELF的文件类的,0x01、表示是32位,0x02表示是64位;第6个字是字节序,规定该文件是大端还是小端。第7个字节规定ELF文件的主版本号,一般是1。后面9个字节ELF标准没有定义,一般填0。

ELF Section Header Table

ELF 段表保存了各个段的基本属性,是除了文件头之外最重要的结构,它描述了ELF中各个段的信息,如段名、段的大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过段表来定位和访问各个段的属性的。

我们可以使用readelf工具来查看段表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x450:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000057 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000340
0000000000000078 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000098
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a4
000000000000002a 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000003b8
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000128
0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002c0
000000000000007c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003e8
0000000000000061 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)

段表的结构比较简单,它是一个以Elf64_Shdr结构体为元素的数组。数组的元素个数等于段的个数,每个Elf64_Shdr结构体对应一个段。Elf64_Shdr又被称为段描述符(Section Descriptor)。对于SimpleSection.o来说,段表就是有13个元素的数组,其中第一个元素是类型为NULL的无效段描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

段的类型

段的名字只是在链接和编译过程中有意义,但它并不能真正地表示段的类型。对于编译器和链接器来说,主要决定段的属性是段的类型(sh_type)和段的标志位(sh_flags)。

段的类型相关常量以SHT_开头,常见的段类型如下表所示:

常量 含义
SHT_NULL 0 无效段
SHT_PROGBITS 1 程序段。代码段、数据段都是这种类型。
SHT_SYMTAB 2 符号表
SHT_STRTAB 3 字符串表
SHT_RELA 4 重定位表。该段包含了重定位信息。
SHT_HASH 5 符号表的哈希表
SHT_DYNAMIC 6 动态链接信息
SHT_NOTE 7 提示性信息
SHT_NOBITS 8 表示该段在文件中没有内容。如`.bss段
SHT_REL 9 该段包含了重定位信息
SHT_SHLIB 10 保留
SHT_DNYSYM 11 动态链接的符号表

段标志位(sh_flag)

段标志位表示该段在进程虚拟地址空间中的属性。如是否可写、是否可执行等。相关常量以SHF_开头。常见的段标志位如下表所示:

常量 含义
SHF_WRITE 1 表示该段在进程空间中可写
SHF_ALLOC 2 表示该段在进程空间中需要分配空间。有些包含指示或控制信息的段不需要在进程空间中分配空间,就不会有这个标志。像代码段、数据段和.bss段都会有这个标志位。
SHF_EXECINSTR 4 表示该段在进程空间中可以被执行,一般指代码段

段链接信息(sh_link、sh_info)

如果段的类型是与链接相关的(无论是动态链接还是静态链接),如重定位表、符号表、等,则sh_linksh_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

重定位表

链接器在处理目标文件时须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。对于每个需要重定位的代码段和数据段都会有一个相应的重定位表。一个重定位表同时也是ELF的一个段,那么这个段的类型就是SHT_REL或者SHT_RELA,它的sh_link表示符号表的下标,它的sh_info表示它作用于哪一个段。比如.rela.text作用于.text段,而.text的下标为1,那么.rela.textsh_info为1。

字符串表

ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。比如下述这个字符串表:

偏移 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9
+0 \0 h e l l o w o r l
+10 d \0 M y v a r i a b
+20 l e \0

那么偏移与它们对应的字符串如下表所示:

偏移 字符串
0 空字符串
1 helloworld
6 world
12 Myvariable

符号表

ELF文件中的符号表往往是文件中的一个段,段名一般叫.symtab。符号表的结构是一个Elf64_Sym结构(64位ELF文件)的数组,每个Elf64_Sym结构对应一个符号。其结构定义如下:

1
2
3
4
5
6
7
8
9
typedef struct {
uint32_t st_name;
unsigned char st_info;
unsigned char st_other;
uint16_t st_shndx;
Elf64_Addr st_value;
uint64_t st_size;
} Elf64_Sym;
// 成员变量的具体的含义可参考ELF手册,见参考资料

我们可以利用readelf来查看ELF文件的符号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -s SimpleSection.o

Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1802
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1803
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000024 51 FUNC GLOBAL DEFAULT 1 main

readelf的输出格式与上面描述的Elf64_Sym的各个成员几乎一一对应,第一列Num表示符号表数组的下标,从0开始,共17个符号;第二列Value就是符号值,即st_value;第三列Size为符号大小,即st_size;第四列和第五列分别为符号类型和绑定信息;第六列Vis在C/C++中未使用,第七列Ndx即st_shndx,表示该符号所属的段;最后一列即符号名称。对于另外的符号解释如下:

  • func1main函数都是定义在SimpleSection.c里面的,它们所在的位置都为代码段,所以Ndx为1,即SimpleSection.o里面的.text。他们是函数,所以类型是STT_FUNC;它们是全局可见的,所以是STB_GLOBAL;Size表示函数指令所占的字节数;Value表示函数相对于该段起始位置的偏移量。
  • printf这个符号在SimpleSection.c里面被引用,但是没有被定义。所以它的Ndx是SHN_UNDEF。(具体含义可参考ELF手册)
  • global_init_var是已初始化的全局变量,它被定义在.bss段,即下标为3。
  • global_uninit_var是未初始化的全局变量,它是一个SHN_COMMON类型的符号。
  • static_var.1533static_var2.1534是两个静态变量(符号修饰导致变量名变化),它们的绑定属性是STB_LOCAL,即只是编译单元内部可见。
  • STT_SECTION类型的符号表示下标为Ndx的段的段名(符号名未显示)。
  • SimpleSection.c表示编译单元的源文件名。

参考资料

  1. Executable and Linkable Format
  2. 《程序员的自我修养——链接、装载与库》
  3. ELF man page
 评论