Loading... > ELF文件格式介绍,来自CTF-wiki --- ELF (Executable and Linkable Format)文件,也就是在 Linux 中的目标文件,主要有以下三种类型: * 可重定位文件(Relocatable File),包含由编译器生成的代码以及数据。链接器会将它与其它目标文件链接起来从而创建可执行文件或者共享目标文件。在 Linux 系统中,这种文件的后缀一般为 `.o` 。 * 可执行文件(Executable File),就是我们通常在 Linux 中执行的程序。 * 共享目标文件(Shared Object File),包含代码和数据,这种文件是我们所称的库文件,一般以 `.so` 结尾。一般情况下,它有以下两种使用情景: * 链接器(Link eDitor, ld)可能会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件。 * 动态链接器(Dynamic Linker)将它与可执行文件以及其它共享目标组合在一起生成进程镜像。 目标文件既会参与程序链接又会参与程序执行。出于方便性和效率考虑,根据过程的不同,目标文件格式提供了其内容的两种并行视图,如下: ![object_file_format](./assets/object_file_format.png) **链接视图** 文件开始处是 ELF 头部( **ELF Header**),它给出了整个文件的组织情况。 如果程序头部表(Program Header Table)存在的话,它会告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。 节区部分包含在链接视图中要使用的大部分信息:指令、数据、符号表、重定位信息等等。 节区头部表(Section Header Table)包含了描述文件节区的信息,每个节区在表中都有一个表项,会给出节区名称、节区大小等信息。用于链接的目标文件必须有节区头部表,其它目标文件则无所谓,可以有,也可以没有。 **执行视图** 对于执行视图来说,其主要的不同点在于没有了 section,而有了多个 segment。其实这里的 segment 大都是来源于链接视图中的 section。 ## ELF Header ELF Header 描述了 ELF 文件的概要信息,利用这个数据结构可以索引到 ELF 文件的全部信息,数据结构如下: ```c #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; ELF文件头的长度 ELF32_Half e_phentsize; 程序头部表中每个表项的长度 ELF32_Half e_phnum; 程序头部表的项数 ELF32_Half e_shentsize; 节区头部表的长度 ELF32_Half e_shnum; 节区头部表的项数 ELF32_Half e_shstrndx; } Elf32_Ehdr; ``` 其中每个成员都是 e 开头的,它们应该都是 ELF 的缩写。 ![Snipaste_2019-03-04_21-14-07](./assets/Snipaste_2019-03-04_21-14-07.png) ## Program Header Table Program Header Table 是一个结构体数组,每一个元素的类型是 `Elf32_Phdr`,描述了一个段或者其它系统在准备程序执行时所需要的信息。其中,ELF 头中的 `e_phentsize` 和 `e_phnum` 指定了该数组每个元素的大小以及元素个数。一个目标文件的段包含一个或者多个节。**程序的头部只有对于可执行文件和共享目标文件有意义。**所以,Program Header Table 就是专门为 ELF 文件运行时中的段所准备的。 `Elf32_Phdr` 的数据结构如下: ```c 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; ``` ![Snipaste_2019-03-04_21-15-53](./assets/Snipaste_2019-03-04_21-15-53.png) 段和节的包含关系: ![Snipaste_2019-03-04_21-16-00](./assets/Snipaste_2019-03-04_21-16-00.png) ### 段内容 一个段可能包括一到多个节区,但是这并不会影响程序的加载。尽管如此,我们也必须需要各种各样的数据来使得程序可以执行以及动态链接等等。下面会给出一般情况下的段的内容。对于不同的段来说,它的节的顺序以及所包含的节的个数有所不同。此外,与处理相关的约束可能会改变对应的段的结构。 代码段只包含只读的指令以及数据。当然这个例子并没有给出所有的可能的段。 ![text_segment](./assets/text_segment.png) 数据段包含可写的数据以及以及指令,通常来说,包含以下内容: ![data_segment](./assets/data_segment.png) 程序头部的 PT_DYNAMIC 类型的元素指向指向 .dynamic 节。其中,got 表和 plt 表包含与地址无关的代码相关信息。尽管在这里给出的例子中,plt 节出现在代码段,但是对于不同的处理器来说,可能会有所变动。 .bss 节的类型为 SHT_NOBITS,这表明它在 ELF 文件中不占用空间,但是它却占用可执行文件的内存镜像的空间。通常情况下,没有被初始化的数据在段的尾部,因此,`p_memsz` 才会比 `p_filesz` 大。 * 不同的段来说可能会有所重合,即不同的段包含相同的节。 ## Section Header Table 该结构用于定位 ELF 文件中的每个节区的具体位置。 首先,ELF 头中的 `e_shoff` 项给出了从文件开头到节头表位置的字节偏移。`e_shnum` 告诉了我们节头表包含的项数;`e_shentsize` 给出了每一项的字节大小。 其次,节头表是一个数组,每个数组的元素的类型是 `ELF32_Shdr` ,每一个元素都描述了一个节区的概要内容。 ```c 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; ``` ![Snipaste_2019-03-05_09-00-40](./assets/Snipaste_2019-03-05_09-00-40.png) ## Sections 节区包含目标文件中除了 ELF 头部、程序头部表、节区头部表的所有信息。节区满足以下条件 * 每个节区都有对应的节头来描述它。但是反过来,节区头部并不一定会对应着一个节区。 * 每个节区在目标文件中是连续的,但是大小可能为 0。 * 任意两个节区不能重叠,即一个字节不能同时存在于两个节区中。 * 目标文件中可能会有闲置空间(inactive space),各种头和节不一定会覆盖到目标文件中的所有字节,**闲置区域的内容未指定**。 许多在 ELF 文件中的节都是预定义的,它们包含程序和控制信息。这些节被操作系统使用,但是对于不同的操作系统,同一节区可能会有不同的类型以及属性。 可执行文件是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析引用(不同文件中的子例程的引用以及数据的引用,调整对象文件中的绝对引用)并且重定位指令。加载与链接过程需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节区中,比如 `.dynamic` 。 ### .strtab: String Table 该节区描述默认的字符串表,包含了一系列的以 NULL 结尾的字符串。ELF 文件使用这些字符串来存储程序中的符号名,包括 * 变量名 * 函数名 该节在运行的过程中不需要加载,只需要加载对应的子集 .dynstr 节。 一般通过对字符串的首个字母在字符串表中的下标来索引字符串。 字符串表的首尾字节都是 NULL。此外,索引为 0 的字符串要么没有名字,要么就是名字为空,其解释依赖于上下文。字符串表也可以为空,相应的,其节区头部的 sh_size 成员将为 0。在空字符串表中索引大于 0 的下标显然是非法的。 一个节区头部的 sh_name 成员的值为其相应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。 ### .shstrtab: Section Header String Table 该节区与 `.strtab` 的存储结构类似,不过该节区存储的是节区名的字符串。 ### .symtab: Symbol Table 每个目标文件都会有一个符号表,熟悉编译原理的就会知道,在编译程序时,必须有相应的结构来管理程序中的符号以便于对函数和变量进行重定位。 此外,链接本质就是把多个不同的目标文件相互 “粘” 在一起,实际上,目标文件相互粘合是目标文件之间对地址的引用,即函数和变量的地址的相互引用。而在粘合的过程中,符号就是其中的粘合剂。 目标文件中的符号表包含了**一些通用的符号**,这部分信息在进行了 `strip` 操作后就会消失。包括 * 变量名 * 函数名 符号表其实是一个数组,数组中的每一个元素都是一个结构体,具体如下 ```c typedef struct { Elf32_Word st_name; 符号在字符串表中对应的索引 Elf32_Addr st_value; Elf32_Word st_size; 符号所占用的大小 unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym; ``` ![Snipaste_2019-03-05_09-29-12](./assets/Snipaste_2019-03-05_09-29-12.png) #### 如何定位 那么对于一个符号来说如何定位其对应字符串的地址呢?具体步骤如下 1. 根据 Section Header Table 中符号节头中的 `sh_link` 获取该符号节中对应符号字符串节在 `Section Header Table` 中的下标。进而我们就可以获取对应符号节的地址。 2. 根据该符号的定义中的 st_name 获取该符号的偏移,即在对应符号节中的偏移。 3. 根据上述两者就可以定位一个符号对应的字符串的地址了。 ### Data Related Sections #### BSS Section 未初始化的全局变量对应的节。此节区不占用 ELF 文件空间,但占用程序的内存映像中的空间。当程序开始执行时,系统将把这些数据初始化为 0。bss 其实是 block started by symbol 的简写,说明该节区中单纯地说明了有哪些变量。 #### .data Section 这些节区包含初始化了的数据,会在程序的内存映像中出现。 #### .rodata Section 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。 ### Common Code Section #### .init & .init_array 此节区包含可执行指令,是进程初始化代码的一部分。程序开始执行时,系统会在开始调用主程序入口(通常指 C 语言的 main 函数)前执行这些代码。 #### .text 此节区包含程序的可执行指令。 #### .fini & .fini_array 此节区包含可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将执行这里的代码。 ### Dynamic Related Sections #### .dynamic 如果一个目标文件参与到动态链接的过程中,那么它的程序头部表将会包含一个类型为 PT_DYNAMIC 的元素。这个段包含了 .dynamic 节,其实这个段就是一个单纯的键值对。 动态节一般保存了 ELF 文件的如下信息 * 依赖于哪些动态库 * 动态符号节信息 * 动态字符串节信息 我们一般使用_DYNAMIC符号来标记这个节,它的结构如下 ```c typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn_DYNAMIC[]; ``` 其中,d_tag 的取值决定了该如何解释 d_un。 * d_val * 这个字段表示一个整数值,可以有多种意思。 * d_ptr * 这个字段表示程序的虚拟地址。正如之前所说的,一个文件的虚拟地址在执行的过程中可能和内存的虚拟地址不匹配。当解析动态结构中的地址时,动态链接器会根据原始文件的值以及内存的基地址来计算真正的地址。为了保持一致性,文件中并不会包含重定位入口来 "纠正" 动态结构中的地址。 ![Snipaste_2019-03-05_10-22-49](./assets/Snipaste_2019-03-05_10-22-49.png) #### .dynsym 动态链接的 ELF 文件具有专门的动态符号表,其使用的结构就是 Elf32_Sym,但是其存储的节为 .dynsym。这里再次给出 Elf32_Sym 的结构 ```c 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 under glibc>=2.2 */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; ``` 需要注意的是 `.dynsym` 是运行时所需的,ELF 文件中 export/import 的符号信息全在这里。但是,`.symtab` 节中存储的信息是编译时的符号信息,它们在 `strip` 之后会被删除掉。 我们主要关注动态符号中的两个成员 * st_name, 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。 * st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。 动态符号与指向它的 Elf_Verdef 保存在 .gnu.version 段中,其中,由 Elf_Verneed 结构体构成的数组的每个元素对应动态符号表的一项。其实,这个结构体就只有一个域:那就是一个 16 位的整数,表示 gnu.verion_r 段中的下标。 在这样的情况下,动态链接器使用 Elf_Rel 结构体成员 r_info 中的下标同时作为 .dynsym 段和 gnu.version 段的下标。这样就可以一一对应到每一个符号到底是那个版本的了。 ![Snipaste_2019-03-05_09-54-44](./assets/Snipaste_2019-03-05_09-54-44.png) ### Relocation Related Sections 链接器在处理目标文件时,需要对目标文件中的某些位置进行重定位,即将符号指向恰当的位置,确保程序正常执行。例如,当程序调用了一个函数时,相关的调用指令必须把控制流交给适当的目标执行地址。 在 ELF 文件中,对于每一个需要重定位的 ELF 节都有对应的重定位表,比如说 .text 节如果需要重定位,那么其对应的重定位表为 .rel.text。 举个例子,当一个程序导入某个函数时,.dynstr 就会包含对应函数名称的字符串,.dynsym 中就会包含一个具有相应名称的动态字符串表的符号(Elf_Sym),在 rel.dyn 中就会包含一个指向这个符号的的重定位表项。 #### .rel(a).dyn & .rel(a).plt .rel.dyn 包含了动态链接的二进制文件中需要重定位的变量的信息,这些信息在加载的时候必须完全确定。而 .rel.plt 包含了需要重定位的函数的信息。这两类重定位节都使用如下的结构 `.rel.plt`节是用于函数重定位,`.rel.dyn`节是用于变量重定位 ```c typedef struct { Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址 Elf32_Word r_info; // 符号表索引 } Elf32_Rel; #define ELF32_R_SYM(info) ((info)>>8) #define ELF32_R_TYPE(info) ((unsigned char)(info)) #define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type)) ``` Elf32_Rela 类型的表项包含明确的补齐信息。 Elf32_Rel 类型的表项在将被修改的位置保存隐式的补齐信息。由于处理器体系结构的原因,这两种形式都存在,甚至是必需的。 ![Snipaste_2019-03-05_10-07-04](./assets/Snipaste_2019-03-05_10-07-04.png) ### Global Offset Table GOT 表在 ELF 文件中分为两个部分 * .got,保存全局变量偏移表 * .got.plt,保存全局函数偏移表,对应着`Elf32_Rel`结构中`r_offset`的值。 其相应的值由能够解析. rel.plt 段中的重定位的动态链接器来填写。 通常来说,地址独立代码不能包含绝对虚拟地址。GOT 表中包含了隐藏的绝对地址,这使得在不违背位置无关性以及程序代码段兼容的情况下,得到相关符号的绝对地址。一个程序可以使用位置独立代码来引用它的 GOT 表,然后提取出来绝对的数值,以便于将位置独立的引用重定向到绝对的地址。 这个表对于 System V 环境中的动态链接来说是必要的,但其具体的内容以及形式依赖于处理器。 初始时,got 表中包含重定向入口所需要的信息。当一个系统为可加载的目标文件创建内存段时,动态链接器会处理重定位项,其中的一些项的类型可能是 R_386_GLOB_DAT,这会指向 got 表。动态链接器会决定相关的符号的值,计算它们的绝对地址,然后将合适的内存表项设置为相应的值。尽管在链接器建立目标文件时,绝对地址还处于未知状态,动态链接器知道所有内存段的地址,因为可以计算所包含的符号的绝对地址。 如果一个程序需要直接访问一个符号的绝对地址,那么这个符号将会有一个 got 表项。由于可执行文件以及共享目标文件都有单独的表项,所以一个符号的地址可能会出现在多个表中。动态链接器在把权限给到进程镜像中的代码段前,会处理所有的 got 表中的重定位项,以便于确定所有的绝对地址在执行过程中是可以访问的。 GOT 表中的第 0 项包含动态结构的地址,用符号 _DYNAMIC 来进行引用。这使得一个程序,例如动态链接器,在没有执行其重定向前可以找到对应的动态结构。这对于动态链接器来说是非常重要的,因为它必须在不依赖其它程序的情况下可以重定位自己的内存镜像。 在不同的程序中,系统可能会为同一共享目标文件选择不同的内存段地址;甚至对于同一个程序,在不同的执行过程中,也会有不同的库地址。然而,一旦进程镜像被建立,内存段的地址就不会再改变,只要一个进程还存在,它的内存段地址将处于固定的位置。 GOT 表的形式以及解释依赖于具体的处理器,对于 Intel 架构来说,`_GLOBAL_OFFSET_TABLE_` 符号可能被用来访问这个表。 `extern Elf32_Addr _GLOBAL_OFFSET_TABLE[];` _GLOBAL_OFFSET_TABLE_ 可能会在 .got 节的中间,以便于可以使用正负索引来访问这个表。 在 Linux 的实现中,.got.plt 的前三项的具体的含义如下 * GOT[0],.dynamic 的地址。 * GOT[1],指向内部类型为 link_map 的指针,只会在动态装载器中使用,包含了进行符号解析需要的当前 ELF 对象的信息。每个 link_map 都是一条双向链表的一个节点,而这个链表保存了所有加载的 ELF 对象的信息。 * GOT[2],指向动态装载器中 _dl_runtime_resolve 函数的指针。 .got.plt 后面的项则是程序中不同 .so 中函数的引用地址。下面给出一个相应的关系。 ![got](./assets/got.png) ![Snipaste_2019-03-05_11-41-18](./assets/Snipaste_2019-03-05_11-41-18.png) ### Procedure Linkage Table GOT 表用来将位置独立的地址重定向为绝对地址,与此类似,PLT 表将位置独立的函数重定向到绝对地址。主要包括两部分 * **.plt**,与常见导入的函数有关,如 read 等函数。 * **.plt.got**,与动态链接有关系。 在动态链接下,程序模块之间包含了大量的函数引用,程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找以及重定位。但是,在一个程序运行过程中,可能很多函数在程序执行完时都不会用到,因此一开始就把所有函数都链接好是一种浪费,所以 ELF 采用了一种延迟绑定的做法,其基本思想是函数第一次被用到时才进行绑定(符号查找,重定位等),如果没有用则不进行绑定。所以程序开始执行前,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器负责绑定。 链接编辑器不能够解析执行流转换(比如程序调用),即从一个可执行文件或者共享目标文件到另一个文件。链接器安排程序将控制权交给过程链接表中的表项。在 Intel 架构中,过程链接表存在于共享代码段中,但是他们会使用在 GOT 表中的数据。动态链接器会决定目标的绝对地址,并且会修改相应的 GOT 表中的内存镜像。因此,动态链接器可以在不违背位置独立以及程序代码段兼容的情况下,重定向 PLT 项。可执行文件和共享目标文件都有独立的 PLT 表。 动态链接器和程序按照如下方式解析过程链接表和全局偏移表的符号引用。 1. 当第一次建立程序的内存镜像时,动态链接器将全局偏移表的第二个和第三个项设置为特殊的值,下面的步骤会仔细解释这些数值。 2. 如果过程链接表是位置独立的话,那么 GOT 表的地址必须在 ebx 寄存器中。每一个进程镜像中的共享目标文件都有独立的 PLT 表,并且程序只在同一个目标文件将控制流交给 PLT 表项。因此,调用函数负责在调用 PLT 表项之前,将全局偏移表的基地址设置为寄存器中。 3. 这里举个例子,假设程序调用了 name1,它将控制权交给了 lable .PLT1。 4. 那么,第一条指令将会跳转到全局偏移表中 name1 的地址。初始时,全局偏移表中包含 PLT 中下一条 pushl 指令的地址,并不是 name1 的实际地址。 5. 因此,程序将一个重定向偏移(reloc_index)压到栈上。重定位偏移是 32 位的,并且是非负的数值。此外,重定位表项的类型为 R_386_JMP_SLOT,并且它将会说明在之前 jmp 指令中使用的全局偏移表项在 GOT 表中的偏移。重定位表项也包含了一个符号表索引,因此告诉动态链接器什么符号目前正在被引用。在这个例子中,就是 name1 了。 6. 在压入重定位偏移后,程序会跳转到 .PLT0,这是过程链接表的第一个表项。pushl 指令将 GOT 表的第二个表项 (got_plus_4 或者 4(%ebx),**当前 ELF 对象的信息**) 压到栈上,然后给动态链接器一个识别信息。此后,程序会跳转到第三个全局偏移表项 (got_plus_8 或者 8(%ebx),**指向动态装载器中_dl_runtime_resolve 函数的指针**) 处,这将会将程序流交给动态链接器。 7. 当动态链接器接收到控制权后,他将会进行出栈操作,查看重定位表项,找到对应的符号的值,将 name1 的地址存储在全局偏移表项中,然后将控制权交给目的地址。 8. 过程链接表执行之后,程序的控制权将会直接交给 name1 函数,而且此后再也不会调用动态链接器来解析这个函数。也就是说,在 .PLT1 处的 jmp 指令将会直接跳转到 name1 处,而不是再次执行 pushl 指令。 ![lazy-plt](./assets/lazy-plt.png) ``` ``` Last modification:January 16th, 2021 at 01:33 pm © 允许规范转载 Support 确定不打赏一下支持博主吗 ×Close Appreciate the author Sweeping payments Pay by AliPay