一、格式概述
1.1 elf文件类型
elf文件有三种类型:可重定向文件、可执行文件和共享目标文件
适于连接的可重定位文件(relocatable file):这种文件是由编译器编译成的.o文件,可与其它目标文件一起创建可执行文件和共享目标文件。此外,.ko 文件也是可重定位文件
适于执行的可执行文件(executable file):这种文件是可以直接用于执行的elf文件(如vim、gdb等),用于提供程序的进程映像,加载的内存执行。
共享目标文件(shared object file):这种文件就是动态库文件,即.so文件。连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
1.2 elf文件视图
因为elf文件既要参与链接又要参与执行,所以elf文件提供了链接视图和执行视图两种视图,其结构示意图如下

链接视图:静态链接器会以链接视图解析elf。使用链接视图时可以没有程序头表
执行视图:动态连接器(即加载器,如 x86 架构 linux 下的 /lib/ld-linux.so.2 或者安卓系统下的 /system/linker)会以执行视图解析elf。使用执行视图时可以没有节表
二、ELF Header
ELF Header结构给出了程序的类别、架构、入口等基本信息。在linux中可以使用readelf -h命令来查看一个elf文件的elf头,如下图所示

对于32位elf程序来说,其长度固定为0x34,而64位elf文件的elf头长度为0x40。3。以32位为例,其如下(这个结构的定义位于/usr/include/elf.h中),:
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
其各个字段的含义如下(其个字段取值可以查阅/usr/include/elf.h):
e_ident : ELF的一些标识信息
e_type:用于标记文件类别,即本文开头所说的三种三类别;除此之外,还有在linux系统异常时产生的coredump类型和用于标记特定处理器的类别
e_machine : 文件的目标体系架构,比如ARM等
e_version : 目标文件的版本,0为非法版本,1为当前版本
e_entry : 程序入口的虚拟地址。如果程序没有入口,该值可以为0
e_phoff : 程序头部表偏移地址。如果程序没有程序头部表格,可以为0,如果文件有程序头部表,那么一般来说,这个值等于elf头的大小。因为在elf头之后紧接着就是程序头表。
e_shoff : 节区头部表偏移地址。如果文件没有节区头部表格,可以为0。
e_flags :保存与文件相关的,特定于处理器的标志
e_ehsize :ELF头的大小。一般来说,32位下elf头大小为0x34,64位下为0x40
e_phentsize : 每个程序头部表表项的大小。一般来说,32位下其大小为0x2A,64位下其大小为0x36。
e_phnum :程序头部表表项的数量
e_shentsize:每个节区头部表表项的大小。一般来说,32位下其大小为0x2E,64位下其大小为0x3A。
e_shnum : 节区头部表表项的数量
e_shstrndx:节区名称节表的位置。如果文件没有节区名称字符串表,此参数可以为SHN_UNDEF;如果文件存在节区名称节,那么一般来说,这个值为e_shnum – 1,即最后一个节。
32位的elf header结构(Elf32_Ehdr)与64位的elf header(Elf64_Ehdr)字段完全相同,差异在于用于存放地址的三个字段——程序入口地址(e_entry)、程序头偏移(e_phoff)和节头偏移(e_shoff)的三个字段在64位中长度为8字节,而在32位中长度为4字节。
2.1 e_ident
e_ident是一个结构体,其存放了elf诸如文件类型、编码类型等的标识性信息。这个结构在32位和64位elf中都存在,并且完全相同。其读取不受编码类型等信息的影响。其各个字段含义及取值如下:
| 名称 | 元素下标值 | 含义 | 说明 |
|---|---|---|---|
| EI_MAG0 | 0 | 魔数(Magic Number)的一部分,标志此文件是一个ELF 目标文件 | 如果为elf文件,改字段取值为e_ident[ET_MAG0]=0x7f |
| EI_MAG1 | 1 | 魔数(Magic Number)的一部分,标志此文件是一个ELF 目标文件 | 如果为elf文件,该字段取值为e_ident[EI_MAGl]=’E’ |
| EI_MAG2 | 2 | 魔数(Magic Number)的一部分,标志此文件是一个ELF 目标文件 | 如果为elf文件,该字段取值为e_ident[EI_MAG2]=’L’ |
| EI_MAG3 | 3 | 魔数(Magic Number)的一部分,标志此文件是一个ELF 目标文件 | 如果为elf文件,该字段取值为e_ident[EI_MAG3]=’F’ |
| EI_CLASS | 4 | 标识文件的类别,即32位还是64位等 | 其宏定义、取值和含义如下: ELFCLASSNONE 0 非法类别 ELFCLASS32 1 32 位目标 ELFCLASS64 2 64 位目标 |
| EI_DATA | 5 | 数据编码类型 | 其宏定义、取值和含义如下: ELFDATANONE 0 非法编码类型 ELFDATA2LSB 1 小端序 ELFDATA2MSB 2 大端序 |
| EI_VERSION | 6 | ELF头的版本标记 | 当值为宏定义E_CURRENT (1)时意为当前版本或版本可用,为E_NONE (0)时意为非法版本 |
| EI_PAD | 7 | 补齐字节开始处 | 这段内容是elf头未使用的字节,其内容为0 |
| EI_NIDENT | 16 | e_ident[]大小 |
2.2 e_entry
e_entry即程序的入口地址,其指向的是加载程序后程序所执行的第一行代码。但是对于不需要执行的elf文件,例如可重定位文件来说,其入口地址可以为0,因为其只需要用于链接即可,并不需要执行。
对于共享文件来说,通过给动态链接库编写main函数的方式可以给其添加入口地址,但是并不会实际执行,所以动态链接库也可以没有入口地址。
而可执行文件是一定拥有入口地址的,其入口地址指向的函数是start函数(非main函数)。_start函数的作用就是在程序执行前进行一些初始化函数,然后调用main函数,最后进行一些清理工作。
所以可以通过自己实现start函数的方式来定义函数的入口。使用-nostartfiles参数来告知编译器不链接标准启动文件,然后使用如下代码来自己实现_start函数,这样程序的入口地址会直接指向自己实现的_start函数。
#include <stdio.h>
#include <stdlib.h>
int _start()
{
printf(hello start);
exit(0);
}
上述代码中必须要添加exit(0),这是因为_start函数执行结束后程序就结束了,需要执行exit()函数来进行一些清理工作。
2.3 二进制文件中的elf头
elf文件可能由于加壳等手段使得elf结构遭到破坏,从而使得分析工具无法正常分析,这是就需要自己手动来分析二进制文件,并对其进行修复,所以可以识别elf结构在二进制文件中的状态是很有必要的。下面是32位elf文件的elf头的示例(64位仅地址长度不同):

三、节区(Sections)
节区中包含了目标文件的除ELF 头、程序头表、节区头表在内的所有信息。
3.1 section_header_table
节区头表(SHT)存放了所有的节区信息,其决定了目标文件内有那些节区。在elf头中已经通过e_shnum字段给出目标文件内共有多少节区,也就是SHT的表项数目。SHT中的项是节区头结构体,其代码如下(以32位为例):
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
其字段解释和说明如下:
| 名称 | 含义 | 说明 |
|---|---|---|
| sh_name | 当前节的名称在节名称表中的索引。 | 节名称表在节区的索引会被记录在elf头的e_shtrndx字段中,但是目标文件也可以没有节区名称表。 |
| sh_type | 节区的类型,用于标记不同作用和内容的节区,例如SHT_STRTAB类型的节区意味着当前节区存放了节区名称表。 | 详细类型及说明见下文 |
| sh_flags | 用于记录节区内容内存属性,即该节区的数据是否可以被读写执行。其属性分为基本可分为读写执行三个。该字段会将目标文件所拥有的基本属性的取值进行或操作作为最终的取值。例如某文件的某字段具有读写两种属性,该字段值应为0x1 | 0x2(即0b1 | 0b10) | 该字段取值的宏定义和含义如下: SHF_WRITE 0x1 数据可写 SHF_ALLOC 0x2 数据可读 SHF_EXECINSTR 0x4 数据可执行 SHF_MASKPROC 0xF0000000 |
| sh_addr | 当前节在目标文件执行时被映射进内存时的虚拟地址。如果当前节并不需要被映射,这个值为0。 | |
| sh_offset | 当前节的数据在目标文件中的偏移地址。这个字段和sh_addr并不一定相等,这是因为目标文件的结构和其映射到内存中的结构并不完全相同。 | |
| sh_size | 当前节区所对应数据的大小。 | |
| sh_link | 该字节表明了当前节区所关联使用的表的节区的索引,主要与链接或重定位有关的节会使用。受节类型影响,会具有不同的含义。例如SHT_DYNAMIC类型的节区的该字段就可以存放其所需要的动态链接符号表的索引。 | |
| sh_info | 附加信息,受节类型的影响具有不同的含义。 | |
| sh_addralign | 该值表明了地址的对齐值。该字段目前只能取值0、1或2的幂。当值为0或1时,表明该节的地址不用对齐,否则则需要向这个字段的值对齐。sh_addr的值必须向这个字段对齐,即可以被这个字段整除。 | |
| sh_entsize | 如果节区中包含类似表格的大小固定的数据项目,该字段存储了表项的大小;如果不存在这样的字段,则该字段为0。 |
SHT的0号项也是存在的,但是其是一个空的表项(所以sh_link为0的表项实际上就是无关联而节),并不具有实际内容,在文件中的内容也是固定的,如下:
| 名字 | 值 | 意义 |
|---|---|---|
| sh_name | 0 | 无名字 |
| sh_type | SHT_NULL | 非活动类型 |
| sh_flags | 0 | 无标志 |
| sh_addr | 0 | 无地址 |
| sh_offset | 0 | 无文件内偏移量 |
| sh_size | 0 | 无大小 |
| sh_link | SHN_UNDEF | 无节头表对应项 |
| sh_info | 0 | 无附加信息 |
| sh_addralign | 0 | 无对齐格式 |
| sh_entsize | 0 | 无表项大小 |
3.2 sh_type
sh_type字段是elf节区的种类,其种类和相关节区及说明见下表:
| 类型名称 | 常见节区名称 | 取值 | 说明 |
|---|---|---|---|
| SHT_NULL | – | 0 | 无实际内容的节区,如上文介绍的0号节区。 |
| SHT_PROGBITS | .comments .data .data1 .debug .fini .got .init .interp .line .pit .rodata .rodata1 .text |
1 | 此节区包含程序需要的各种数据,例如代码、变量等,其格式和含义都由程序来解释。 |
| SHT_SYMTAB/SHT_DYNSYM | .dynstr .symtab |
2(11) | 这两种节区都包含符号表,其中SHT_DYNSYM为动态链接符号表 |
| SHT_STRTAB | .shstrtab | 3 | 此节区包含字符串表。目标文件可能包含多个字符串表节区。 |
| SHT_RELA | .relaname | 4 | 存放重定位偏移信息的节区 |
| SHT_HASH | .hash | 5 | 此节区包含符号对应的哈希表,主要用于快速检索。 |
| SHT_DYNAMIC | .dynamic | 6 | 此节区包含动态链接的信息。 |
| SHT_NOTE | .note | 7 | 此节区包含以某种方式来标记文件的信息。 |
| SHT_NOBITS | .bss | 8 | 这种类型的节区不占用文件中的空间,但是仍然具有offset等字段, 其往往只代表逻辑上的位置。其他方面和SHT_PROGBITS 相似。 |
| SHT_REL | .relname | 9 | 该字节包含了重定位信息 |
| SHT_SHLIB | 10 | 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。 | |
| SHT_LOPROC | 0X70000000 | 这一段(包括两个边界),是保留给处理器专用语义的。 | |
| SHT_HIPROC | OX7FFFFFFF | 这一段(包括两个边界),是保留给处理器专用语义的。 | |
| SHT_LOUSER | 0X80000000 | 此值给出保留给应用程序的索引下界。 | |
| SHT_HIUSER | 0X8FFFFFFF | 此值给出保留给应用程序的索引上界。 |
3.3 不同类型节区的数据结构
不同类型的节区,其数据的数据结构也都不相同。其数据大体分为两种,一种是需要被加载器进行解析,给加载过程提供辅助信息;另一种就是程序代码和数据,加载器直接将其 copy 到对应的虚拟地址即可,二进制代码和数据由 CPU 执行。
3.3.1 SHT_SYMTAB
该类型的节区存放的数据为符号表,其为一个数组,数组的每一个元素为一个用于描述符号的结构体,其定义如下(以32位为例):
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
各字段和其含义如下:
| 名称 | 含义 | 说明 |
|---|---|---|
| st_name | 符号的名称,该符号名称在字符串表(即strtab)中的偏移 | |
| st_value | 符号的取值,即该符号所对应的偏移值。 | 对于符号_start来说,其st_value值就等于程序的入口地址,而对于一个变量符号来说,其值就等于这个变量的指针 |
| st_size | 符号所占用的大小。而对于函数来说,其符号占用的为函数的大小。 | |
| st_info | 符号的类型和绑定属性。该字节的高四位用于描述绑定信息,低四位表示类型。 | 其类型如下: 宏定义 取值 含义 STT_NOTYPE 0 未指定类型. STT_OBJECT 1 该符号是一个数据对象 STT_FUNC 2 该符号是一个代码对象,虽然名称为 FUNC,但是它也可以是汇编代码中的标号,比如 _start,所以用代码对象描述更合适 STT_SECTION 3 与 section 相关联的符号 STT_FILE 4 该符号是文件名 STT_COMMON 5 通常是 weak 类型的符号(比如目标文件中未初始化的全局变量) STT_TLS 6 线程的本地数据 STT_RELC 8 复杂的重定位表达式 其绑定状态如下: |
| st_other | 无意义 | |
| st_shndx | 相关联的 section 索引。比如 main 函数对应的 section 为 .text,所以该字段就是 .text 的索引。 |
其绑定状态如下:
| 宏定义 | 取值 | 含义 |
|---|---|---|
| STB_LOCAL | 0 | 内部符号,外部不可见 |
| STB_GLOBAL | 1 | 全局符号 |
| STB_WEAK | 2 | 和全局变量一样,优先级较低 |
对于符号表,虽然其存放的是地址,但是在程序中,地址会直接被编译进指令中。所以实际程序运行的时候用不到符号表。符号表中存放的地址的作用是在链接过程用于重定位,即在符号所对应值在内存中地址还未确定的时候辅助其确定在内存中的地址。
3.3.2 SHT_NOTE
SHT_NOTE 类型的节区用于标记文件的相关信息,属于在加载过程中起到辅助作用的节区。其数据结构如下:
typedef struct
{
Elf32_Word n_namesz; /* Length of the note's name. */
Elf32_Word n_descsz; /* Length of the note's descriptor. */
Elf32_Word n_type; /* Type of the note. */
} Elf32_Nhdr;
namesz:name 的长度
descsz:desc 即描述部分的长度。
type: desc 部分数据的类型,即如何解析它。
SHT_NOTE的数据部分长度是不固定的,namez和descsz字段决定了其长度。因其数据长度不定,所以没有直接将数据定义在结构体中,不过在elf中,这个区块的数据就存放在上述三个结构体后。
3.4 sh_link 和 sh_info
这两个字段受到其节区类型的影响,含义也不同,其含义和类型对照如下:
| sh_type | sh_link | sh_info |
|---|---|---|
| SHT_DYNAMIC | 此节区中条目所用到的字符串表格的节区头部索引 | |
| SHT_HASH | 此哈希表所使用的符号表的节区头部索引 | |
| SHT_RELA | 相关符号表的节区头部索引 | 重定位所适用的节区的节区头部索引 |
| SHT_DYNSYM | 相关联的字符串表的节区头部索引 | 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一 |




