简介
ELF(Executable and Linkable Format)文件,是linux中的目标文件,主要以下三种类型
1.可重定位文件(Relocatable File):包含由编译器生成的代码以及数据。链接器会将它与其他目标文件链接起来从而创建可执行文件或者共享目标文件。在Linux系统中,这种文件的后缀一般为.o。
2.可执行文件(Executable File),就是我们通常在Linux中执行的程序。
3.共享目标文件(Shared Object File),包含代码和数据,这种文件是我们所称的库文件,一般以.so结尾。一般情况下,它有以下两种使用场景:
- 链接器(Link eDitor, ld)可能会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件。
- 动态链接器(Dynamic Linker)将它与可执行文件以及其他共享目标文件合在一起生成进程镜像。
目标文件由汇编器和链接器创建,是文本程序的二进制形式,可以直接在处理器上运行。那些需要虚拟机才能够执行的程序(java)不属于这以范围。
ELF文件格式
目标文件既会参与程序链接又会参与程序执行。处于方便性和效率考虑,根据过程的不同,目标文件格式提供了其内容的两种并行视图
Linking View Execution View
-----------------------------------------------------
ELF Header ELF Header
Program Header Table optional Program Header Table
Section 1 Segmnent 1
…… Segmnent 2
Section n ……
……
Section Header Table Section Header Table optional
首先,我们先来关注以下链接视图。
文件开始处是ELF头部(ELF Header),它给出了整个文件的组织情况。
如果程序头部表(Program Header Table)存在的话,它会告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。
节区部分包含在链接中要使用的大部分信息:指令、数据、符号表、重定位信息等等。
节区头部表(Section Header Table)包含了描述文件节区的信息,每个节区在表中都有一个表项,会给出节区名称、节区大小等信息。用于链接的目标文件必须有节区头部表,其他目标文件则无所谓,可以有,也可以没有。
对于执行视图来说,其主要的不同点在于没有了Section,而有了多个segment。其实这里的segment大都是来源于链接视图中的section。
注意:
尽管图中是按照ELF头、程序头部表、节区、节区头部表的顺序排列的。但实际上除了ELF头部表以外,其他部分都没有严格的顺序。
数据格式
ELF文件格式支持8位/32位体系结构。当然,这种格式是可以扩展的,也可以支持更小的或者更大位数的处理器架构。因此,目标文件会包含一些控制数据,这部分数据表明了目标文件所使用的架构,这也使得它可以被通用的方式来识别和解释。目标文件中的其他数据采用目的处理器的格式进行编码,与在何种机器上创建没有关系。也就是说,目标文件可以进行交叉编译,我们可以在x86平台生成arm平台的可执行代码。
目标文件中的所有数据结构都遵从“自然”大小的对齐规则。如下:
名称 | 长度 | 对齐方式 | 用途 |
---|---|---|---|
Elf32_Addr | 4 | 4 | 无符号程序地址 |
Elf32_Half | 2 | 2 | 无符号半整型 |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号大整型 |
Elf32_Word | 4 | 4 | 无符号大整型 |
unsigned char | 1 | 1 | 无符号小整型 |
如果必要,数据结构可以包含显示地补齐来确保4字节对象按4字节对齐,强制数据结构的大小是4的整数倍等等。数据同样适用是对齐的。因此,包含一个Elf32_Addr类型成员的结构体会在文件中的4字节边界对齐。
为了具有移植性,ELF文件不使用位域。
字符表示(以32位为主)
ELF Header
ELF Header描述了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;
} Elf32_Ehdr;
- e_ident[EI_NIDENT]:用于给出解码和解释文件中与机器无关的数据的方式.
- e_type:标识目标文件类型.
- e_machine:指定当前文件可以运行的机器架构.
- e_version:标识目标文件版本.
- e_entry:系统转交控制权给ELF中相应代码的虚拟地址。如果没有相关的入口项,则这一项为0.
- e_phoff:程序头部表在文件中的字节偏移,如果没有程序头部表,则为0.
- e_shoff:节头表在文件中的字节偏移,如果没有节头表,则为0.
- e_flags:文件中与特定处理器相关的标志。
- e_ehsize:ELF文件头部的字节长度.
- e_phentsize:程序头部表中每个表项的字节长度,每个表项的大小相同。
- e_phnum:程序头部表的项数。因此,e_pjnum与e_phtentsize的乘积即为程序头部表的字节长度。如果文件中没有程序头部表,则该项值为0.
- e_shentsize:节头的字节长度,一个节头是节头表中的一项,节头表中所有项占据的空间大小相同。
- e_shnum:节头表的项数,因此,e_shnum与e_shentsize的乘积即为节头表的字节长度。如果我文件中没有节头表,则该项值为0.
- e_shstrndx:节头表中与节头名字符串表相关的表项的索引值。如果文件中没有节名字符串表,则该项值为SHN_UNDEF.
Program Header Table
Program Header Table是一个结构体数组,每一个元素的类型是Elf32_Phdr,描述了一个段或者其他系统在准备执行时所需要的信息。其中,EKF头中的e_phentsize和e_phnum指定了该数组每个元素的大小以及元素个数。一个目标文件的段包含一个或者多个节。程序的头部只有对于可执行文件和共享目标文件有意义。
可以说,Program Header Table就是专门为ELF文件运行时中的段所准备的。
Elf32_Phdr的数据结构如下:
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 | 该字段为段的类型,或者表明了该结构的相关信息。 |
p_offset | 该字段给出了从文件开始到该段开头的第一个字节的偏移。 |
p_vaddr | 该字段给出了该段第一个字节在内存中的虚拟地址。 |
p_paddr | 该字段仅用于物理地址寻址相关的系统中, 由于”System V” 忽略了应用程序的物理寻址,可执行文件和共享目标文件的该项内容并未被限定。 |
p_filesz | 该字段给出了文件镜像中该段的大小,可能为 0。 |
p_memsz | 该字段给出了内存镜像中该段的大小,可能为 0。 |
p_flags | 该字段给出了与段相关的标记。 |
p_align | 可加载的程序的段的 p_vaddr 以及 p_offset 的大小必须是 page 的整数倍。该成员给出了段在文件以及内存中的对齐方式。如果该值为 0 或 1 的话,表示不需要对齐。除此之外,p_align 应该是 2 的整数指数次方,并且 p_vaddr 与 p_offset 在模 p_align 的意义下,应该相等。 |
基地址-Base Address
程序头部的虚拟地址可能并不是程序内存镜像中实际的虚拟地址。通常来说,可执行程序都会包含绝对地址的代码。为了使得程序可以正常运行,段必须在相应的虚拟地址处。另一方面,共享目标文件通常来说包含与地址无关的代码。这可以使得共享目标文件可以被多个进程加载,同时保持程序执行的正确性。尽管系统会为不同的进程选择不同的虚拟地址,但是它仍然保留段的相对地址来进行寻址,内存中的虚拟地址之间的差必须与文件中的虚拟地址之前的差匹配。内存中任何段的虚拟地址与文件中对应的虚拟地址之间的差值对于任何一个可执行文件或者共享对象来说是一个单一常量值。这个差值就是基地址,基地址的一个用途就是在动态链接期间重新定位程序。
可执行文件或者共享目标文件的基地址是在执行过程中由以下三个数值计算的:
- 虚拟内存加载地址
- 最大页面大小
- 程序可加载段的最低虚拟地址
要计算基地址,首先要确定可加载段中p_vaddr最小的内存虚拟地址,之后把该内存虚拟地址缩小为与之最近的最大页面的整数倍即是基地址。根据要加载到内存中的文件的类型,内存地址可能与p_vaddr相同也可能不同。
段内容
一个段可能包括一到多个节区,但是这并不会影响程序的加载。尽管如此,我们也必须需要各种各样的数据来使得程序可以执行以及动态链接等等。对于不同的段来说,它的节的顺序以及所包含的节的个数有所不同。此外,与处理相关的约束可能会改变对应的段的结构。
Section Header Table
该结构用于定位ELF文件中的每个节区的具体位置。
首先,ELF头中的e_shoff项给出了从文件开头到节头表位置的偏移。e_shnum告诉了我们节头表包含的项数;e_shentsize给出了每一项的字节大小。
其次,节头表是一个数组,每个数组的元素的类型是ELF32_Shdr ,每一个元素都描述了一个节区的概要内容。
数据结构如下:
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 | 节名称,是节区头字符串表节区中(Section Header String Table Section)的索引,因此该字段实际是一个数值。在字符串表中的具体内容是以 NULL 结尾的字符串。 |
sh_type | 根据节的内容和语义进行分类,具体的类型下面会介绍。 |
sh_flags | 每一比特代表不同的标志,描述节是否可写,可执行,需要分配内存等属性。 |
sh_addr | 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0。 |
sh_offset | 给出节区的第一个字节与文件开始处之间的偏移。SHT_NOBITS 类型的节区不占用文件的空间,因此其 sh_offset 成员给出的是概念性的偏移。 |
sh_size | 此成员给出节区的字节大小。除非节区的类型是 SHT_NOBITS ,否则该节占用文件中的 sh_size 字节。类型为 SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间。 |
sh_link | 此成员给出节区头部表索引链接,其具体的解释依赖于节区类型。 |
sh_info | 此成员给出附加信息,其解释依赖于节区类型。 |
sh_addralign | 某些节区的地址需要对齐。例如,如果一个节区有一个 doubleword 类型的变量,那么系统必须保证整个节区按双字对齐。也就是说,sh_addr%sh_addralign=0。目前它仅允许为0,以及2的正整数幂数。0和1表示没有对齐约束。 |
sh_entsize | 某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。反之,此成员取值为 0。 |
Sections
节区包含目标文件中出了ELF头部、程序头部表、节区头部表的所有信息。节区满足以下条件:
1.每个节区都有对应的节头来描述它。但是反过来,节区头部并不一定会对应着每个节区。
2.每个节区在目标文件中是连续的,但是大小可能为0.
3.任意两个节区不能重叠,即一个字节不能同时存在于两个节区中。
4.目标文件中可能会有闲置空间,各种头和节不一定会覆盖到目标文件中的所有字节,闲置区域的内容未指定。
许多在ELF文件中的节都是预定义的,他们包含程序和控制信息。这些节被操作系统使用,但是对于不同的操作系统,统一节区可能会有不同的类型以及属性。
可执行文件是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析引用并且重定位指令。加载于链接过程中需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节区中,比如.dynamic。
每一种操作系统都会支持一组链接模型,但这些模型都大致可以分为两种:
类型 | 描述 |
---|---|
静态链接 | 静态链接的文件中所使用的库文件或者第三方库都被静态绑定了,其引用已经被解析了。 |
动态链接 | 动态链接的文件中所使用的库文件或者第三方库只是单纯地被链接到可执行文件中。当可执行文件执行时使用到相应函数时,相应的函数地址才会被解析。 |
有一些特殊的节可以支持调试,比如说.debug以及.line节;支持程序控制的节有.bss,.data,.data1,.rodata,.rodata1。
名称 | 类型 | 属性 | 含义 |
---|---|---|---|
.comment | SHT_PROGBITS | 包含版本控制信息。 | |
.debug | SHT_PROGBITS | 此节区包含用于符号调试的信息。 | |
.dynamic | SHT_DYNAMIC | SHF_ALLOC SHF_WRITE | 此节区包含动态链接信息。SHF_WRITE 位设置与否是否被设置取决于具体的处理器。 |
.dynstr | SHT_STRTAB | SHF_ALLOC | 此节区包含用于动态链接的字符串,大多数 情况下这些字符串代表了与符号表项相关的名称。 |
.dynsym | SHT_DYNSYM | SHF_ALLOC | 此节区包含动态链接符号表。 |
.got | SHT_PROGBITS | 此节区包含全局偏移表。 | |
.line | SHT_PROGBITS | 此节区包含符号调试的行号信息,描述了源程序与机器指令之间的对应关系,其内容是未定义的。 | |
.plt | SHT_PROGBITS | 此节区包含过程链接表(procedure linkage table)。 | |
.relname | SHT_REL | 这些节区中包含重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。 | |
.relaname | SHT_RELA | ||
.shstrtab | SHT_STRTAB | 此节区包含节区名称。 |
Program Loading
程序加载过程其实就是系统创建或者扩充进程镜像的过程。它只是按照一定的规则把文件的段拷贝到虚拟内存段中。进程只有在执行的过程中使用了对应的逻辑页面时,才会申请相应的物理页面。通常来说,一个进程中有很多页是没有被引用的。因此,延迟物理读写可以提高系统的性能。为了达到这样的效率,可执行文件以及共享目标文件所拥有的段的文件偏移以及虚拟地址必须是合适的,也就是说他们必须是页大小的整数倍。
在加载段时,可执行文件与共享目标文件有所区别。可执行文件通常来说包含绝对代码。为了能够使得程序正确执行,每个段应该在用于构建可执行文件的虚拟地址处。因此,系统直接使用p_vaddr作为虚拟地址。
另一方面,共享目标文件通常包含地址独立代码。这使得在不同的进程中,同一段的虚拟地址可能会有所不同,但这并不会影响程序的执行行为。尽管系统会为不同的进程选择不同的虚拟地址,但是它仍旧维持了段的相对地址。因为地址独立代码在不同的段中使用相对地址,因此在虚拟内存中的虚拟地址之间的差肯定和在文件中的相应的虚拟地址的差相同。
在Intel架构中,虚拟地址以及文件偏移必须是4KB的整数,或者说是更大的2的整数倍。
Program Link
这里主要讲动态链接。
动态链接主要是在程序初始化或者程序执行的过程中解析变量或者函数引用。ELF文件中某些节区以及头部元素就与动态链接有关。动态链接的模型由操作系统定义并实现。
Dynamic Linker
动态链接器可以用来帮助加载应用所需要的库并解析库所导出的动态符号(函数和全局变量)。
可执行程序和动态链接器会合作起来为程序创建进程镜像,具体的细节如下:
1.将可执行文件的内存段添加到进程镜像中。
2.将共享目标文件的内存段添加到进程镜像中。
3.为可执行文件以及共享目标文件进行重定位。
4.如果传递给了动态链接器一个文件描述符的话,就是将其关闭。
5.将控制权传递给程序。这让我们感觉起来就好像程序直接从可执行文件处拿到了执行权限。
Program Execution Flow
静态执行
动态执行
- sys_execve:该函数主要用于执行一个新的程序,即执行我们想要执行的程序,会检查相应的argv以及envp等参数。
- do_execve:该函数用来打开目标映像文件,并从目标文件的开始处读入指定长度的字节来获取相应目标文件的基本信息。
- search_binary_handler:该函数会搜索支持处理当前类型的二进制文件类型队列,以便于让各种可执行的程序的处理程序进行相应的处理。
- search_binary_handler:该函数的主要处理流程如下:
- 检查并获取elf文件的头部信息。
- 如果目标文件采用动态链接,则使用.interp节来确定loader的路径。
- 将program header中记录的相应的段映射到内存中。program header中有以下重要信息.
- 每一个段需要映射到的地址,
- 每一个段相应的权限。
- 记录哪些节属于哪些段。
- ld.so:该文件有以下功能:
- 主要用于载入 ELF 文件中 DT_NEED 中记录的共享库
- 初始化工作
- 初始化 GOT 表
- 将 symbol table 合并到 global symbol table
- _start:_start 函数会将以下项目交给 libc_start_main
- 环境变量起始地址
- .init
- 启动 main 函数前的初始化工作
- fini
- 程序结束前的收尾工作
内容来源
CTF Wiki ELF File Basic Structure
CTF Wiki Program Loading
CTF Wiki Program Link
CTF Wiki Program Execution Flow