ELF探究 之 ELF文件介绍(一)
概述
elf(Executable and Linking Format)是一种文件格式。文件格式从来不是以后缀(如.exe .dll等)为标志的,而是其内部结构符合一定的规范。elf文件格式用于Linux的可执行文件(个人认为就是Linux可加载进内存的文件都必须符合这种格式规范),如果不是ELF文件格式加载时Linker(Linux中加载可执行文件的代码)无法识别。
elf文件规范是应用程序二进制接口(Application Binary Interface,ABI)的一部分。
在linux中可执行文件、so(动态链接库)、.o(可重定位文件)、ko(内核模块)都是elf文件格式,这说明了elf文件的重要性。
ELF文件格式
image.pngimage.png
对于ELF文件有两种视图执行视图和链接视图,我们可以看到节表和段表都指向了同一部分的内存,所以这只是对这段内存的不同描述。一个段一般都包含了多个节。
程序加载时主要是看执行视图的,很多程序的加壳就修改了节头表以此来干扰静态分析.
我们先来看看这张图,可以看出图有4个部分
ELF header:elf文件头这个结构描述了我们整个文件的一些关键信息
Program Header Table:程序头表(段头表)这是一个数组描述了每个段的信息
sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件
Section Header Table:节头表这是一个数组描述了每个节的信息
文件无论是在硬盘或内存中都是二进制文件,linux是将其解释为结构体来进行解释的。
因为ELF文件是linux上的可执行文件,所以Linux中是有相关结构体的头文件的,这个头文件路径是/usr/include/elf.h,后面的结构体都出自这个头文件(我们都以32位为标准)。
ELF Header
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* ELF的一些标识信息,前四位为.ELF,其他的信息比如大小端等 */
Elf32_Half e_type; /* 目标文件类型 */
Elf32_Half e_machine; /* 文件的目标体系架构,比如ARM*/
Elf32_Word e_version; /* 0为非法版本,1为当前版本 Linker源码中表示必须为1 */
Elf32_Addr e_entry; /* 程序入口的虚拟地址 */
Elf32_Off e_phoff; /* 程序头部表偏移地址 */
Elf32_Off e_shoff; /* 节区头部表偏移地址 */
Elf32_Word e_flags; /* 保存与文件相关的,特定于处理器的标志 */
Elf32_Half e_ehsize; /* ELF头的大小 */
Elf32_Half e_phentsize; /* 每个程序头部表的大小 */
Elf32_Half e_phnum; /* 程序头部表的数量 */
Elf32_Half e_shentsize; /* 每个节区头部表的大小 */
Elf32_Half e_shnum; /* 节区头部表的数量 */
Elf32_Half e_shstrndx; /* 节区字符串表位置 */
} Elf32_Ehdr;
现在对结构体Elf32_Ehdr的一些成员进行取值解释,只列出部分具体请参考elf.h文件中定义.
e_ident
e_ident是一个16字节的数组,这个数组按位置从左到右都是有特定含义,每个数组元素的下标在标准中还存在别称,如byte0的下标0别名为EI_MAG0,具体如下:
名称 | 元素下标值 | 含义 |
---|---|---|
EI_MAG0 | 0 | 文件标识 '.' |
EI_MAG1 | 1 | 文件标识 'e' |
EI_MAG2 | 2 | 文件标识 'l' |
EI_MAG3 | 3 | 文件标识 'f' |
EI_CLASS | 4 | 文件类 |
EI_DATA | 5 | 数据编码 |
EI_VERSION | 6 | 文件版本 |
EI_PAD | 7 | 补齐字节开始处 初始化为 0 |
EI_NIDENT | 16 | e_ident[]大小 |
EI_CLASS取值:
名称 | 取值 | 含义 |
---|---|---|
ELFCLASSNONE | 0 | 非法类别 |
ELFCLASS321 | 32 | 位目标 |
ELFCLASS642 | 64 | 位目标 |
EI_DATA取值:
名称 | 取值 | 含义 |
---|---|---|
ELFDATANONE | 0 | 非法数据编码 |
ELFDATA2LSB | 1 | 高位在前 |
ELFDATA2MSB | 2 | 低位在前 |
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 | 0xff00~0xffff | 特定处理器文件 |
e_machine
e_machine表示目标体系结构类型:
名称 | 取值 | 含义 |
---|---|---|
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 |
others | 9~ | 其他 |
e_entry
可执行文件test和动态库.so都存在所谓的进入点,且可执行文件的e_entry指向C库中的_start,而动态库.so中的进入点指向 call_gmon_start。
e_shstrndx
这一项描述的是字符串表在Section Header Table中的索引,因为解析需要用到表项的字符串,而识别表项需要用到字符串表,所以在这里先给出位置方便直接定位。
注意点
目前e_ehsize = 52字节,e_shentsize = 40字节,e_phentsize = 32字节,这些值都是固定值,某些加固会修改这些值造成静态解析失败,可以修改回这些固定值
整个so的大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1
e_phoff = ELF头的大小
Section Header Table
#这个是每个节项的结构体
typedef struct{
Elf32_Word sh_name; //节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。
Elf32_Word sh_type; //为节区类型
Elf32_Word sh_flags; //节区标志
Elf32_Addr sh_addr; //如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。
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_type取值
名称 | 取值 | 说明 |
---|---|---|
SHT_NULL | 0 | 此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。 |
SHT_PROGBITS | 1 | 此节区包含程序定义的信息,其格式和含义都由程序来解释。 |
SHT_SYMTAB | 2 | 此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。 |
SHT_STRTAB | 3 | 此节区包含字符串表。目标文件可能包含多个字符串表节区。 |
SHT_RELA | 4 | 此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。 |
SHT_HASH | 5 | 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除。 |
SHT_DYNAMIC | 6 | 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制。 |
SHT_NOTE | 7 | 此节区包含以某种方式来标记文件的信息。 |
SHT_NOBITS | 8 | 这 种 类 型 的 节 区 不 占 用 文 件 中 的 空 间 , 其 他 方 面 和SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移 |
SHT_REL | 9 | 此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。 |
SHT_SHLIB | 10 | 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。 |
SHT_DYNSYM | 11 | 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。 |
SHT_LOPROC | 0X70000000 | 这一段(包括两个边界),是保留给处理器专用语义的。 |
SHT_HIPROC | OX7FFFFFFF | 这一段(包括两个边界),是保留给处理器专用语义的。 |
SHT_LOUSER | 0X80000000 | 此值给出保留给应用程序的索引下界。 |
SHT_HIUSER | 0X8FFFFFFF | 此值给出保留给应用程序的索引上界。 |
sh_flag
sh_flag标志着此节区是否可以修改,是否可以执行,如下定义:
名称 | 取值 | 含义 |
---|---|---|
SHF_WRITE | 0x1 | 节区包含进程执行过程中将可写的数据。 |
SHF_ALLOC | 0x2 | 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0。 |
SHF_EXECINSTR | 0x4 | 节区包含可执行的机器指令。 |
SHF_MASKPROC | 0xF0000000 | 所有包含于此掩码中的四位都用于处理器专用的语义。 |
sh_link 和 sh_info
sh_link和sh_info字段的具体含义依赖于sh_type的值
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 此节区中条目所用到的字符串表格的节区头部索引 | 0 |
SHT_HASH | 此哈希表所适用的符号表的节区头部索引 | 0 |
SHT_REL | ||
SHT_RELA | 相关符号表的节区头部索引 | 重定位所适用的节区的节区头部索引 |
SHT_SYMTAB | ||
SHT_DYNSYM | 相关联的字符串表的节区头部索引 | 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一 |
其它 | SHN_UNDEF | 0 |
常用Section
有些节区是系统预订的,一般以点开头号,因此,我们有必要了解一些常用到的系统节区。
名称 | 类型 | 属性 | 含义 |
---|---|---|---|
.bss | SHT_NOBITS | SHF_ALLOC +SHF_WRITE | 包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。 |
.comment | SHT_PROGBITS | (无) | 包含版本控制信息。 |
.data | SHT_PROGBITS | SHF_ALLOC +SHF_WRITE | 这些节区包含初始化了的数据,将出现在程序的内存映像中。 |
.data1 | SHT_PROGBITS | SHF_ALLOC +SHF_WRITE | 这些节区包含初始化了的数据,将出现在程序的内存映像中。 |
.dynamic | SHT_DYNAMIC | 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器。 | |
.dynstr | SHT_STRTAB | SHF_ALLOC | 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。 |
.dynsym | SHT_DYNSYM | SHF_ALLOC | 此节区包含了动态链接符号表。 |
.fini | SHT_PROGBITS | SHF_ALLOC +SHF_EXECINSTR | 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。 |
.got | SHT_PROGBITS | 此节区包含全局偏移表。 | |
.hash | SHT_HASH | SHF_ALLOC | 此节区包含了一个符号哈希表。 |
.init | SHT_PROGBITS | SHF_ALLOC +SHF_EXECINSTR | 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。 |
.interp | SHT_PROGBITS | 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。 | |
.line | SHT_PROGBITS | (无) | 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。 |
.note | SHT_NOTE | (无) | 此节区中包含注释信息,有独立的格式。 |
.plt | SHT_PROGBITS | 此节区包含过程链接表(procedure linkage table)。 | |
.relname.relaname | SHT_REL SHT_RELA | 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。 | |
.rodata .rodata1 | SHT_PROGBITS | SHF_ALLOC | 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。 |
.shstrtab | SHT_STRTAB | 此节区包含节区名称。 | |
.strtab | SHT_STRTAB | 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含SHF_ALLOC 位,否则该位为 0。 | |
.symtab | SHT_SYMTAB | 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0。 | |
.text | SHT_PROGBITS | SHF_ALLOC +SHF_EXECINSTR | 此节区包含程序的可执行指令。 |
符号表(.dynsym)
符号是对某些类型的数据或者代码(如全局变量或函数)的符号引用。例如,printf()函数会在动态符号表 .dynsym 中存有一个指向该函数的符号条目。
在大多数共享库和动态链接可执行文件中,存在两个符号表。
.dynsym 保存了引用来自外部文件符号的全局符号,如 printf 这样的库函数,.dynsym 保存的符号是 .symtab 所保存符号的子集,.symtab 中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数等。因此,.symtab 保存了所有的符号,而 .dynsym 只保存动态/全局符号。
因此,就存在这样一个问题:既然.symtab 中保存了.dynsym 中所有的符号,那么为什么还需要两个符号表呢?
.dynsym 是被标记了 ALLOC 的,而.symtab 则没有标记。ALLOC 表示有该标记的节会在运行时分配并装载进入内存,而 .symtab 不是在运行时必需的,因此不会被装载到内存中。
.dynsym 保存的符号只能在运行时被解析,因此是运行时动态链接器所需要的唯一符号。
.dynsym 符号表对于动态链接可执行文件的执行来说是必需的,而 .symtab 符号表只是用来进行调试和链接的,有时候为了节省空间,会将 .symtab 符号表从生产二进制文件中删掉。
符号表包含用来定位、重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。
例如符号_ZL15global_static_a,就是由 global_static_a 变量名经过修饰而来。
符号表项结构体如下:
typedef struct {
Elf32_Word st_name; //符号表项名称。如果该值非0,则表示符号名的字符串表索引(offset),否则符号表项没有名称。
Elf32_Addr st_value; //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。
Elf32_Word st_size; //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。
unsigned char st_info; //符号的类型和绑定属性。
unsigned char st_other; //该成员当前包含 0,其含义没有定义。
Elf32_Half st_shndx; //每个符号表项都以和其他节区的关系的方式给出定义。此成员给出相关的节区头部表索引。
} Elf32_sym;
st_info操纵方式:
#define ELF32_ST_BIND(i) ((i)>>4)
#define ELF32_ST_TYPE(i) ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf)
符号类型(ELF32_ST_TYPE)取值
名称 | 取值 | 说明 |
---|---|---|
STT_NOTYPE | 0 | 符号的类型没有指定 |
STT_OBJECT | 1 | 符号与某个数据对象相关,比如一个变量、数组等等 |
STT_FUNC | 2 | 符号与某个函数或者其他可执行代码相关STT_SECTION3符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL绑定。STT_FILE4传统上,符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL绑定,其节区索引是SHN_ABS,并且它优先于文件的其他 STB_LOCAL符号(如果有的话) |
STT_LOPROC | 13 | 此范围的符号类型值保留给处理器专用语义用途。 |
STT_HIPROC | 15 | 此范围的符号类型值保留给处理器专用语义用途。 |
在共享目标文件中的函数符号(类型为 STT_FUNC)具有特别的重要性。当其他目标文件引用了来自某个共享目标中的函数时,链接编辑器自动为所引用的符号创建过程链接表项。类型不是 STT_FUNC 的共享目标符号不会自动通过过程链接表进行引用。
字符串表(.dynstr .shstrtab)
.dynstr和.shstrtab结构完全相同,不过一个存储的是符号名称的字符串,而另一个是Section名称的字符串。
重定位表(.rel)
重定位表确定了需要被重定位的地址,符号表决定了被置换成什么值
重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。
换句话来说,重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。
具体来说,就是把符号的value进行重新定位。
可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映象的正确信息。这就是重定位表项做的工作。重定位表项的格式如下:
typedef struct {
Elf32_Addr r_offset; //重定位动作所适用的位置(受影响的存储单位的第一个字节的偏移或者虚拟地址)
Elf32_Word r_info; //要进行重定位的符号表索引,以及将实施的重定位类型(哪些位需要修改,以及如何计算它们的取值)
//其中 .rel.dyn 重定位类型一般为R_386_GLOB_DAT和R_386_COPY;.rel.plt为R_386_JUMP_SLOT
} Elf32_Rel;
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Word r_addend; //给出一个常量补齐,用来计算将被填充到可重定位字段的数值。
} Elf32_Rela;
对 r_info 成员使用 ELF32_R_TYPE 宏运算可得到重定位类型,使用 ELF32_R_SYM 宏运算可得到符号在符号表里的索引值。 三种宏的具体定义如下:
#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
Program Header Table
程序头部(Program Header)描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建映像所必须的信息。
可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息。目标文件的“段”包含一个或者多个“节区”,也就是“段内容(Segment Contents)”。程序头部仅对可执行文件和共享目标文件有意义。
程序头部的数据结构如下:
typedef struct {
Elf32_Word p_type; //此数组元素描述的段的类型,或者如何解释此数组元素的信息。
Elf32_Off p_offset; //此成员给出从文件头到该段第一个字节的偏移
Elf32_Addr p_vaddr; //此成员给出段的第一个字节将被放到内存中的虚拟地址
Elf32_Addr p_paddr; //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
Elf32_Word p_filesz; //此成员给出段在文件映像中所占的字节数。可以为0。
Elf32_Word p_memsz; //此成员给出段在内存映像中占用的字节数。可以为0。
Elf32_Word p_flags; //此成员给出与段相关的标志。
Elf32_Word p_align; //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;
p_type取值
名称 | 取值 | 说明 |
---|---|---|
PT_NULL | 0 | 此数组元素未用。结构中其他成员都是未定义的。 |
PT_LOAD | 1 | 此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载的段在程序头部表格中根据 p_vaddr 成员按升序排列。 |
PT_DYNAMIC | 2 | 数组元素给出动态链接信息。 |
PT_INTERP | 3 | 数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面。 |
PT_NOTE | 4 | 此数组元素给出附加信息的位置和大小。 |
PT_SHLIB | 5 | 此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符。 |
PT_PHDR | 6 | 此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面。 |
PT_LOPROC~PT_HIPROC | 0x70000000~0x7fffffff | 此范围的类型保留给处理器专用语义。 |
尾言
本文资料大量来自以下链接:
ELF文件格式解析:https://blog.csdn.net/mergerly/article/details/94585901
ELF文件格式解析器 原理 + 代码:https://mp.weixin.qq.com/s/S4ZCvhZe-FIvO1UMeKlyxA
《北京大学信息科学技术学院操作系统实验室 ELF文件格式分析》