ELF文件类型
- ET_NONE:未知类型:这个标记表明文件类型不确定,或者还未定义。
- ET_REL:重定位文件:ELF 类型标记为 relocatable 意味着该文件被标记为了一段可重定位的代码,有时也称为目标文件。可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码position independent code)。在编译完代码之后通常可以看到一个.o 格式的文件,这种文件包含了创建可执行文件所需要的代码和数据。
- ET_EXEC:可执行文件:ELF 类型为 executable,表明这个文件被标记为可执行文件。这种类型的文件也称为程序,是一个进程开始执行的入口。
- ET_DYN:共享目标文件:ELF 类型为 dynamic,意味着该文件被标记为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在程序运行时被装载并链接到程序的进程镜像中。ET_CORE:核心文件。在程序崩溃或者进程传递了一个 SIGSEGV 信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用 GDB 读取这类文件来辅助调试并查找程序崩溃的原因
ELF程序头
Linux 的 ELF(5)手册, 可以了解 ELF 头部的结构:
#define EI_NIDENT 16 typedef struct{ unsigned char e_ident[EI_NIDENT]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; ElfN_Addr e_entry; ElfN_Off e_phoff; ElfN_Off e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; }ElfN_Ehdr;
ELF 程序头是对二进制文件中段的描述,是程序装载必需的一部分
5 种常见的程序头类型:
- PT_LOAD:一个可执行文件至少有一个 PT_LOAD 类型的段。这类程序头描述的是可装载的段,也就是说,这种类型的段将被装载或者映射到内存中
- PT_DYNAMIC——动态段的 Phdr:动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些
信息 - PT_NOTE:PT_NOTE 类型的段可能保存了与特定供应商或者系统相关的附加信息
- PT_INTERP:PT_INTERP 段只将位置和大小信息存放在一个以 null 为终止符的字符串
中,是对程序解释器位置的描述 - PT_PHDR:PT_PHDR 段保存了程序头表本身的位置和大小
可以使用 readelf –l <filename>命令查看文件的 Phdr 表:
我的电脑是64位的:
1 readelf -l fork 2 3 Elf file type is DYN (Shared object file) 4 Entry point 0x1260 5 There are 13 program headers, starting at offset 64 6 7 Program Headers: 8 Type Offset VirtAddr PhysAddr 9 FileSiz MemSiz Flags Align 10 PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 11 0x00000000000002d8 0x00000000000002d8 R 0x8 12 INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 13 0x000000000000001c 0x000000000000001c R 0x1 14 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] 15 LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 16 0x00000000000009b0 0x00000000000009b0 R 0x1000 17 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 18 0x00000000000005b5 0x00000000000005b5 R E 0x1000 19 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 20 0x00000000000001c8 0x00000000000001c8 R 0x1000 21 LOAD 0x0000000000002d38 0x0000000000003d38 0x0000000000003d38 22 0x00000000000002d8 0x00000000000002e0 RW 0x1000 23 DYNAMIC 0x0000000000002d48 0x0000000000003d48 0x0000000000003d48 24 0x00000000000001f0 0x00000000000001f0 RW 0x8 25 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 26 0x0000000000000020 0x0000000000000020 R 0x8 27 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 28 0x0000000000000044 0x0000000000000044 R 0x4 29 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 30 0x0000000000000020 0x0000000000000020 R 0x8 31 GNU_EH_FRAME 0x0000000000002074 0x0000000000002074 0x0000000000002074 32 0x0000000000000044 0x0000000000000044 R 0x4 33 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 34 0x0000000000000000 0x0000000000000000 RW 0x10 35 GNU_RELRO 0x0000000000002d38 0x0000000000003d38 0x0000000000003d38 36 0x00000000000002c8 0x00000000000002c8 R 0x1 37 38 Section to Segment mapping: 39 Segment Sections... 40 00 41 01 .interp 42 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 43 03 .init .plt .plt.got .plt.sec .text .fini 44 04 .rodata .eh_frame_hdr .eh_frame 45 05 .init_array .fini_array .dynamic .got .data .bss 46 06 .dynamic 47 07 .note.gnu.property 48 08 .note.gnu.build-id .note.ABI-tag 49 09 .note.gnu.property 50 10 .eh_frame_hdr 51 11 52 12 .init_array .fini_array .dynamic .got
ELF 节头
段(segment)和节(section)的区别:
节,不是段, 段是程序执行的必要组成部分,在每个段中,会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试。节头对于程序的执行来说不是必需的,没有节头表,程序仍可以正常执行,因为节头表没有对程序的内存布局进行描述,对程序内存布局的描述是程序头表的任务。节头是对程序头的补充。readelf –l 命令可以显示一个段对应有哪些节,可以很直观地看到节和段之间的关系
32 位 ELF 节头的结构:
typedef struct { uint32_t sh_name; // offset into shdr string table for shdr name uint32_t sh_type; // shdr type I.E SHT_PROGBITS uint32_t sh_flags; // shdr flags I.E SHT_WRITE|SHT_ALLOC Elf32_Addr sh_addr; // address of where section begins Elf32_Off sh_offset; // offset of shdr from beginning of file uint32_t sh_size; // size that section takes up on disk uint32_t sh_link; // points to another section uint32_t sh_info; // interpretation depends on section type uint32_t sh_addralign; // alignment for address of section uint32_t sh_entsize; // size of each certain entries that may be in section } Elf32_Shdr;
- .text 节: .text 节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr,.text 节就会存在于 text 段中。由于.text 节保存了程序代码,因此节的类型为 SHT_PROGBITS
- .rodata节: 保存了只读的数据,如一行 C 语言代码中的字符串,因为.rodata 节是只读的,所以只能存在于一个可执行文件的只读段中
- .plt 节: .plt 节中包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于其存在于 text 段中,同样保存了代码,因此节类型为 SHT_PROGBITS
- .data 节: .data 节存在于 data 段中,保存了初始化的全局变量等数据。由于其保存了程序的变量数据,因此类型被标记为SHT_PROGBITS。
- .bss 节: .bss 节保存了未进行初始化的全局数据,是 data 段的一部分,占用空间不超过 4 字节,仅表示这个节本身的空间。程序加载时数据被初始化为 0,在程序执行期间可以进行赋值。由于.bss 节未保存实际的数据,因此节类型为SHT_NOBITS
- .got.plt 节: . got 节保存了全局偏移表,.got 节和.plt 节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改
- .dynsym 节: .dynsym 节保存了从共享库导入的动态符号信息,该节保存在 text 段中,
- .dynstr 节: .dynstr 节保存了动态符号字符串表,表中存放了一系列字符串,字符串代表了符号的名称,以空字符作为终止符。节类型被标记为 SHT_DYNSYM
- .rel.*节: 重定位节保存了重定位相关的信息,这些信息描述了如何在链接或者运行时,对 ELF 目标文件的某部分内容或者进程镜像进行补充或修改。在本章的ELF 重定位一节(2.5 节)会深入讨论。重定位节保存了重定位相关的数据,因此节类型被标记为 SHT_REL。
- .hash 节: .hash 节有时也称为.gnu.hash,保存了一个用于查找符号的散列表
- .symtab : .symtab 节保存了 ElfN_Sym 类型的符号信息
- .ctors 和.dtors : .ctors(构造器)和.dtors(析构器)这两个节保存了指向构造函数和析构函数的函数指针,构造函数是在 main 函数执行之前需要执行的代码,析构函数是在 main 函数之后需要执行的代码
text 段的布局如下: [.text]:程序代码 [.rodata]:只读数据。 [.hash]:符号散列表。 [.dynsym]:共享目标文件符号数据。 [.dynstr]:共享目标文件符号名称。 [.plt]:过程链接表。 [.rel.got]:G.O.T 重定位数据。 data 段布局如下。 [.data]:全局的初始化变量。 [.dynamic]:动态链接结构和对象。 [.got.plt]:全局偏移表。 [.bss]:全局未初始化变量。
注意:可重定位文件(类型为 ET_REL 的 ELF 文件)中不存在程序头,因为.o类型的文件会被链接到可执行文件中,但是不会被直接加载到内存中,所以使用 readelf –l test.o 命令不会得到想要的结果
ELF 重定位
重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使得可执行文件和共享目标文件能够保存进程的程序镜像所需的正确信息。重定位条目就是我们上面说的相关信息。
64 位的重定位条目:
typedef struct{ Elf64_Addr r_offset; Uint64_t r_info; }Elf64_Rel;
有的重定位条目还需要 addend 字段:
typedef struct{ Elf64_Addr r_offset; Uint64_t r_info; int64_t r_addend; }Elf64_Rela;
- r_offset 指向需要进行重定位操作的位置。重定位操作详细描述了如何对存放在 r_offset 中的代码或数据进行修改。
- r_info 指定必须对其进行重定位的符号表索引以及要应用的重定位类型。
- r_addend 指定常量加数,用于计算存储在可重定位字段中的值。
参考学习资料:
《linux二进制分析》
https://pdos.csail.mit.edu/6.828/2018/readings/elf.pdf