ELF手册
ELF文件
ELF 是 Executable Linkable Format 的简称. 它是Linux平台的可执行文件的存储格式. 它是一种基于 COFF( Common File Format )文件标准的变种.
ELF可执行文件和链接格式最初由UNIX系统实验室(USL)开发并发布,作为应用程序二进制接口(ABI)的一部分。工具接口标准委员会(TIS)选择
了不断发展的ELF标准作为一种可移植对象文件格式,适用于各种操作系统的32位英特尔体系结构环境。
ELF标准旨在通过为开发人员提供一组跨多个操作环境扩展的二进制接口定义来简化软件开发。这将减少不同接口实现的数量,从而减少重新编码
和重新编译代码的需要。
1.2版 ELF 格式规范是工具接口标准委员会(TIS)工作的成果。TIS是一个由微型计算机行业成员组成的协会,旨在实现32位英特尔体系结构
操作环境开发工具可见的软件接口标准化。这样的接口包括对象模块格式、可执行文件格式以及调试记录信息和格式。
该委员会的目标是帮助简化整个微型计算机行业的软件开发过程,目前专注于32位操作环境。为此,委员会制定了一些规范,其中一些是针对
可在领先的行业操作系统中移植的文件格式,另一些则描述了32位Windows操作系统的格式。最初作为TIS可移植格式规范1.1版一起分发,
现在这些规范被单独分离和分发。
TIS委员会成员包括来自Absoft、Autodesk、Borland International、IBM、Intel、Lahey、Lotus、MetaWare、Microtec Research、Microsoft、Novell、
The Santa Cruz Operation和WATCOM International的代表。PharLap软件公司和赛门铁克公司也参与了规范定义工作。 与TIS规范集合中的其他规
范一样,该规范基于现有的、经过验证的格式,符合TIS委员会的目标,即采用现有标准,并在必要时扩展现有标准,而不是发明新标准。
一,ELF格式综述
ELF(Executable and Linkable Format)是一种用于可执行文件、目标代码、共享库和核心转储的标准文件格式,常用于类Unix系统,如Linux和Mac OS。
ELF是Linux下的一种格式标准,Linux中的ELF格式文件一共有四种:
●可重定位文件(Relocatable File):这类文件包含了代码和数据,可被用来链接成可执行文件或者共享目录文件,扩展名为.o
●可执行文件(Executable File):这类文件包含了可以直接执行的程序,一般没有扩展名
●共享目录文件(Shared Object File):这类文件包含了代码和数据,扩展名为.so。
共享目录文件一般可以在以下两种情况下使用:
①链接器可以使用这类文件,与其他的共享目录文件和可重定位文件进行链接,生成新的目标文件;
②动态链接器可以将几个这种共享目录文件与可执行文件结合,作为进程映像的一部分来运行。
●核心转储文件(Core Dump File):当进程意外终止时,系统可以将该进程的地址空间的内容以及终止时的一些其他信息转储在核心转储文件中。
一个标准的ELF文件通常包含以下几类信息:
文件头信息:描述关于文件的整体信息,包括版本号、系统类型以及文件类型等信息。
指令和数据:由编译器或汇编器生成的二进制指令和数据。
符号信息:该模块中定义的全局符号,以及从其他模块导入的或者有链接器定义的符号。
重定位信息:记录文件中符号的重定位信息,对于后续要链接的ELF文件,链接器会使用重定位信息对目标代码进行调整。
调试信息:目标代码中与链接无关但会被调试器使用到的其他信息,包括源代码文件和行号信息、本地符号以及目标代码使用的数据结构描述信息。
ELF文件的结构
以下讲解ELF中存在的数据结构与他们的作用与意义
一个完整的ELF文件一般会包括如下几个内容:ELF头、Section头、Program头和Section。
Section
中文称为节,ELF文件中我们最终要用的东西都是存在各个节里边的,包含代码,数据,符号等内容,在ELF中节是一个接一个排列的,组成的是一个节列表.
Section头
一个Section头指向一个Section,Section头中包括所指向Section的名字、类型、其在ELF文件中的偏移地址、大小等信息。
Segment
一个Segment由一系列连续的Section构成,连续的Section拥有相同的权限,如只读、读写、可读可执行等.
这里需要注意,在ELF文件镜像中, Segemnt是一个逻辑概念,其加载内容指向的是Section列表中由数个连续的Section组成的一个段落,
该区间的实际范围可以从Segment header的p_offset和p_filesz属性得到.而且多个Segment的区间是可以重叠的是允许有交集的.
Program头
一个Program头指向一个Segment,Program头中包括所指向Segment的类型、其在ELF文件中的偏移地址、大小,映射到内存的虚拟地址等信息。
Sement的范围设定比较自由,不是所有链接器都把ELF header 算在段的范围内.
Segment是独立的, 地址范围是不允许出现交叠情况的, 编译器、汇编器和链接器等工具在生成ELF文件时会进行相应的检查和调整,以确保段的地址范围不会发生交叠.
Section头表
一个结构体数组,数组中的每个结构体元素是一个节头 (section header)
Program头表
一个结构体数组,数组中的每个结构体元素是一个程序头 (program header)
ELF 头
一个ELF头内包含有:Section头表的在ELF文件中的偏移地址、单个Section头的大小、Section头表中Section头的个数;
Program头表的在ELF文件中的偏移地址、单个Program头的大小、Program头表中Program头的个数;该ELF文件的类
型,若是可执行文件的话,还包含的有程序的入口地址。
所以我们拿到的不同的ELF格式文件中:
可执行文件一定有程序头表,但是不一定有节头表,因为可执行文件只需要进行执行操作,而执行时用到的是程序头表。
可重定位文件一定有节头表,但是不一定有程序头表,因为可重定位文件只需要进行链接操作,而链接时用到的是节头表。
二,ELF格式内部结构详细分析
ELF各种头结构中成员的数据类型
名称 大小 说明
Elf32_Addr 4 无符号程序地址
Elf32_Half 2 无符号中等整数
Elf32_Off 4 无符号文件偏移
Elf32_SWord 4 有符号大整数
Elf32_Word 4 无符号大整数
unsigned char 1 无符号字符型整数
2.1.ELF文件头表
ELF 文件的最前面是文件头,描述了ELF文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址。
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf
(Note:ELF格式的定义在/usr/include目录下的“elf.h”头文件中。)
●e_ident
[EI_NIDENT]是一个数组,它有16个字节,这16个字节被分为好几个部分,分别代表不同的涵义:
⑴e_ident[0]-e_ ident[3]:这四个字节被称为“魔数“(Magic),它们分别是“0x7F“,”E“,”L“,”F“的ASCII码,对所有ELF格式的文件来说,
这四个字节是固定的,也就是说,只有当这四个字节是这些值的时候,才代表这个文件是ELF格式的文件。相当于ELF文件的身份证。
(2)e_ ident[4]:这个字节给出了这个ELF文件是多少位的:当取“0x1”时,代表这个ELF文件是32位的;当取”0x2”时,代表这个ELF文件是64位的。
(3)e_ ident[5]:这个字节给出了这个ELF文件使用的字节顺序:当取”0x1”时,代表小端序,当取”0x2”时,代表大端序。
(4)e_ ident[6]:这个字节给出了这个ELF文件头的版本,当前只有一个版本,所以这个字节目前只能取”0x1”
(5)e_ ident[7]到e_ ident[15]:这九个字节目前还没有实际意义,一般为0,也有的文件会用它们做一些特殊的标记。
●e_ type
用于区分ELF文件的类型:(系统通过这个数值来判断文件类型,而不是后缀名)
目标文件类型:
名称 取值 含义
ET_NONE 0 未知目标文件格式
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 共享目标文件
ET_CORE 4 Core 文件(转储格式)
ET_LOPROC 0xff00 特定处理器文件
ET_HIPROC 0xffff 特定处理器文件
ET_LOPROC 和 ET_HIPROC 之间的取值用来标识与处理器相关的文件格式。
●e_ machine给出了文件所需的CPU体系结构:
给出文件的目标体系结构类型
名称 取值 含义
EM_NONE 0 未指定
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_68K 4 Motorola 68000
EM_88K 5 Motorola 88000
EM_860 7 Intel 80860
EM_MIPS 8 MIPS RS3000
其它值都是保留的。特定处理器的ELF名称会使用机器名来进行区分。
ELF 文件格式被设计成可以在多个平台下使用。这并不表示同一个ELF文件可以在不同的平台下使用,而是表示不同平台下的ELF文件都遵循同一套ELF标准。
e_machine 成员就表示该ELF文件的平台属性,比如3表示该ELF文件只能在Intel x86 机器下使用,这也是我们最常见的情况。
●e_ version给出了ELF的版本号,当前只有一个版本,所以只能取”0x1”
●e_ entry给出了程序在虚拟内存中的入口地址,是程序开始执行的位置。这一值只在程序映射到内存当中后才有效。
(只有可执行文件的e_ entry才是有意义的,对于可重定位文件来说,这个值为0,因为它没有入口地址)
●e_ phoff给出了程序头表相对于ELF文件开始的处偏移量,根据这个偏移量我们可以找到程序头表的位置
●e_ shoff给出了节头表相对于ELF文件开始处的偏移量,根据这个偏移量我们可以找到节头表的位置
●e_ flags给出了这个ELF文件平台相关属性的标志位。
●e_ ehsize给出了这个ELF头表的大小,以字节为单位
●e_ phentsize给出了程序头表中一项的长度,以字节为单位,
●e_ phnum给出了程序头表中有几条信息(有几个段)
●e_ shentsize给出了节头表中一项的长度,以字节为单位
●e_ shnum给出了节头表中有几条信息(有几个节)
●e_ shstrndx这里保存了包含各节名称的字符串表在节头表中的索引位置
2.2 Section Header Table 节区头部表格
ELF 头部中, e_shoff 成员给出从文件头到节头表的偏移字节数; e_shnum
给出表格中条目数目; e_shentsize 给出每个项目的字节数。 从这些信息中可以确切地定位节区的具体位置、长度。
节头表是一个结构体数组(一个数组,这个数组的每个元素都是一个Section Header结构体)
节头表中比较特殊的几个数组下标如下:
名称 取值 说明
SHN_UNDEF 0 标记未定义的、缺失的、不相关的,或者没有含义的
SHN_LORESERVE OXFF00 保留索引的下界
SHN_LOPROC 0XFF00 保留给处理器特殊的语义
SHN_HIPROC 0XFF1F 同上
SHN_ABS 0XFFF1 包含对应引用量的绝对取值。这些值不会被重定位所影响
SHN_COMMON 0XFFF2 相对于此节区定义的符号是公共符号。如 FORTRAN中 COMMON 或者未分配的 C 外部变量。
SHN_HIRESERVE 0XFFFF 保留索引的上界
介于 SHN_LORESERVE 和 SHN_HIRESERVE 之间的表项不会出现在节区头部表中。
2.3 Section Header 节区头
typedef struct{
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
}Elf32_Shdr;
●sh_ name给出了节的名称,节名是一个字符串,保存在.shstrtab节中。sh_ name的值是这个字符串在字符串表.shstrtab中的偏移量
●sh_ type给出了节的类型,具体类型如下:
(1) SHT_NULL代表该节为空
(2) SHT_PROGBITS类型的节中具体存的是什么东西,需要程序给出具体的解释(保存程序,代码,数据的节都是这种类型)。
(3) SHT_SYMTAB类型的节是一个符号表,里边保存了一系列例如“int a;“语句中的”a”这样的符号。
(4) SHT_STRTAB类型的节是一个字符串表,其中保存了一系列的字符串
(5) SHT_RELA类型的节中保存了重定位信息
(6) SHT_HASH类型的节中保存了一个散列表,用于实现符号的快速访问
(7) SHT_DYNAMIC类型的节中保存了一个结构数组,该结构数组中包含了大量的动态链接相关信息,后面会着重分析
(8) SHT_NOTE类型的节中保存了一些提示性的信息。
(9) SHT_NOBITS类型的节在文件中没有内容,比如.bss节。
(10) SHT_REL类型的节保存了重定位信息
(11) SHT_DYNSYM类型的节保存了动态链接的符号表
●sh_ flags:给出了节在虚拟进程空间的属性,如是否可写,是否可执行等等:
如果一个标志位被设置,则该位取值为 1。未定义的各位都设置为 0。
sh_flags字段取值
名称 取值
SHF_WRITE 0x1
SHF_ALLOC 0x2
SHF_EXECINSTR 0x4
SHF_MASKPROC 0xF0000000
其中已经定义了的各位含义如下:
SHF_WRITE: 节区包含进程执行过程中将可写的数据。
SHF_ALLOC: 此节区在进程执行过程中占用内存。 某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0。
SHF_EXECINSTR: 节区包含可执行的机器指令。
SHF_MASKPROC: 所有包含于此掩码中的四位都用于处理器专用的语义。
●sh_ addr:这个成员给出了节的虚拟地址。如果该节可以被加载,则这个数值代表该节被加载后在进程地址空间中的虚拟地址;否则为0。
●sh_ offset:这个成员给出了节在ELF文件中的偏移量。如果该节存在于ELF文件中,则这个数值代表节在ELF文件中的偏移量;否则无意义。比如.bss节的sh_ offset成员就没有意义。
●sh_ link 和sh_ info:这两个成员是节的链接信息,如果节的类型是与链接相关的(不论是动态链接还是静态链接),比如重定位表,符号表等节,那么这两个成员的意义如下图:(对于其他节,这两个成员没有实际意义)
根据节区类型的不同, sh_link 和 sh_info 的具体含义也有所不同:
表 10 sh_link 和 sh_info 字段解释
sh_type |
sh_link |
sh_info |
SHT_DYNAMIC |
此节区中条目所用到的字符串表格的节区头部索引 |
0 |
SHT_HASH |
此哈希表所适用的符号表的节区头部索引 |
0 |
SHT_REL |
相关符号表的节区头部索引 |
重定位所适用的节区的 |
SHT_SYMTAB |
相关联的字符串表的节区头部索引 |
最后一个局部符号(绑 |
其它 |
SHN_UNDEF |
0 |
2.4 section 节区(或者节)
节区中包含目标文件中的所有信息,除了: ELF 头部、程序头部表格、节区头部表格。节区满足以下条件:
(1). 目标文件中的每个节区都有对应的节区头部描述它, 反过来, 有节区头部不意味着有节区。
(2). 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。
(3). 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。
(4). 目标文件中可能包含非活动空间(INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。
ELF文件中有很多的节,他们分别保存着不同的信息,比如有的节保存了代码,有的节保存了数据,有的节保存了调试信息等等。这样分开存的好处就是
可以分别给他们不同的权限,像代码就是可读可执行的,而数据则是可读可写的,分开存可以防止在不经意之间将代码改掉带来预期之外的错误。
要判断一个节里边具体保存的是什么类型的信息,可以通过节头表来了解到。下面介绍一些特殊的节区
特殊节区
很多节区中包含了程序和控制信息。 下面的表中给出了系统使用的节区, 以及它们的类型和属性。
表 11 常见特殊节区
名称 |
类型 |
属性 |
含义 |
.bss |
SHT_NOBITS |
SHF_ALLOC + |
包含将出现在程序的内存映像中的为初始 |
.comment |
SHT_PROGBITS |
(无) |
包含版本控制信息。 |
.data |
SHT_PROGBITS |
SHF_ALLOC + |
这些节区包含初始化了的数据, 将出现在程 |
.data1 |
SHT_PROGBITS |
SHF_ALLOC + |
|
.debug |
SHT_PROGBITS |
(无) |
此节区包含用于符号调试的信息。 |
.dynamic |
SHT_DYNAMIC |
此节区包含动态链接信息。 节区的属性将包 |
|
.dynstr |
SHT_STRTAB |
SHF_ALLOC |
此节区包含用于动态链接的字符串, 大多数 |
.dynsym |
SHT_DYNSYM |
SHF_ALLOC |
此节区包含了动态链接符号表。 |
.fini |
SHT_PROGBITS |
SHF_ALLOC + |
此节区包含了可执行的指令, 是进程终止代 |
.got |
SHT_PROGBITS |
此节区包含全局偏移表。 |
.hash |
SHT_HASH |
SHF_ALLOC |
此节区包含了一个符号哈希表。 |
.init |
SHT_PROGBITS |
SHF_ALLOC + |
此节区包含了可执行指令, 是进程初始化代 |
.interp |
SHT_PROGBITS |
此节区包含程序解释器的路径名。 如果程序 |
|
.line |
SHT_PROGBITS |
(无) |
此节区包含符号调试的行号信息, 其中描述 |
.note |
SHT_NOTE |
(无) |
此节区中包含注释信息,有独立的格式。 |
.plt |
SHT_PROGBITS |
此节区包含过程链接表(procedure
linkage |
|
.relname |
SHT_REL |
这些节区中包含了重定位信息。 如果文件中 |
|
.relaname |
SHT_RELA |
||
.rodata |
SHT_PROGBITS |
SHF_ALLOC |
这些节区包含只读数据, 这些数据通常参与 |
.rodata1 |
SHT_PROGBITS |
SHF_ALLOC |
|
.shstrtab |
SHT_STRTAB |
此节区包含节区名称。 |
|
.strtab |
SHT_STRTAB |
此节区包含字符串, 通常是代表与符号表项 |
|
.symtab |
SHT_SYMTAB |
此节区包含一个符号表。 如果文件中包含一 |
|
.text |
SHT_PROGBITS |
SHF_ALLOC + |
此节区包含程序的可执行指令。 |
在分析这些节区的时候,需要注意如下事项:
以“.”开头的节区名称是系统保留的。 如果应用程序需要自定义节区名称,可以使用没有前缀的节区名称, 以避免与系统节区冲突。
目标文件格式允许人们定义不在上述列表中的节区。
目标文件中也可以包含多个名字相同的节区。
保留给处理器体系结构的节区名称一般构成为:处理器体系结构名称简写 + 节区名称。
处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 街区是由FOO体系结构定义的 psect 节区。
需要注意的是,不是所有的工具和操作系统都能够正确处理多个.text段。因此,在实际使用中,除非有明确的理由和需求,否则一般建议保持只有一个.text段,.data和.bbs同理.
另外, 有些编译器对如上节区进行了扩展,这些已存在的扩展都使用约定俗成的名称,如:
.sdata
.tdesc
.sbss
.lit4
.lit8
.reginfo
.gptab
.liblist
.conflict
…
.dynamic:这个节中保存的是一个结构体数组,这个数组每一个元素都是一个结构体,其中保存的是一些与动态链接有关的信息,
比如:依赖于哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址等。
结构体如下:(定义在/usr/include目录下的elf.h头文件中)
typedef struct
{
Elf64_Sxword d_tag
union
{
Elf64_Xword d_val;
Elf64_Addr d_ptr;
}
}
其中d_ tag是一个标志,它取不同的值的时候,下面的共用体所存内容有不同含义。它的取值有以下几种可能:
从上面这个表可以看出来“.dynamic”节中保存的全都是有关动态链接的各种细节信息,因此,这个节在动态链接的过程中起到了很重要的作用,这个节相当于有关动态链接的一个“头表”。
2.5 Segment header 程序头
可执行文件或者共享目标文件的程序头部是一个结构数组, 每个结构描述了一个段或者系统准备程序执行所必需的其它信息。
目标文件的“段”包含一个或者多个“节区”,也就是“段内容( Segment Contents)”。程序头部仅对于可执行文件和共享目标文件
有意义。
可执行目标文件在 ELF 头部的 e_phentsize 和 e_phnum 成员中给出其自身程序头部的大小。
程序头部的数据结构如下图:
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_phdr;
●p_ type给出了这个段的类型:
(1)PT_NULL 代表空段
(2)PT_LOAD 代表这是一个可装载段
(3)PT_DYNAMIC 代表这个段中存的是用于动态链接的信息
(4)PT_INTERP 代表了这个段中存的是表明链接器绝对路径的字符串
(5)PT_NOTE 代表了这个段中存的是专有的编译器信息
●p_ flags给出了该段的访问权限
(1)PF_R代表可读
(2)PF_W代表可写
(3)PF_X代表可执行
●p_ offset 给出了该段的起始地址在ELF文件中的偏移量
●p_ vaddr 给出了该段需要映射到进程虚拟地址空间中的位置。
●p_ paddr 在只支持物理寻址,不支持虚拟寻址的系统当中才使用。
●p_ filesz 给出了该段的大小,以字节为单位。
●p_ memse 给出了该段在虚拟地址空间当中的长度,单位为字节。与p_ filesz不等时会通过截断数据或者以0填充的方式处理。
●p_ align 给出了段内存和二进制文件当中的对齐方式,即p_ offset和p_ vaddr必须是p_ align的整数倍。
可加载的进程段的 p_vaddr 和 p_offset 取值必须合适, 相对
于对页面大小的取模而言。 此成员给出段在文件中和内存中如何
对齐。数值 0 和 1 表示不需要对齐。否则 p_align 应该是个
正整数, 并且是 2 的幂次数, p_vaddr 和 p_offset 对 p_align
取模后应该相等。
关于ARM的ELF的链接
其中Load Region(加载域)与Exceution Region(执行域) 是相对于链接视图而言的
其实我认为输出节和输入节都是相对于链接视图而言的,因为只有链接器才会区分输入和输出,obj和exe都只是普通的ELF文件, 输入节和输出节本质上就是ELF的section.
只是链接器把obj中的section叫成输入节,可以理解为将输入节作为原材料经过链接器的加工处理,
按照它自己定的规则对这些节进行重新排序组合生成新的节,而输入的原材料我们称为输入节 input section 输出的结果称为输出节 output section
下面这段链接脚本的例子可以帮助理解
SECTIONS
{
. = 0×10000;
.text : { *(.text) } //output section1.1
. = 0×8000000;
.data : { *(.data) } //output section 1.2
.bss : { *(.bss) } //output section 1.3
}
我的理解是把他分成三个SECTIONS跟容易理解
SECTIONS
{
. = 0×10000;
.text : { *(.text) } //output section1.1
}
SECTIONS
{
. = 0×8000000;
.data : { *(.data) } //output section 1.2
}
SECTIONS
{
.bss : { *(.bss) } //output section 1.3
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」