Linux ELF文件总结
ELF (Executable and Linking Format) 是Linux 下标准的可执行文件格式,一个ELF文件主要用来表示3种类型的文件:
- 可执行文件 : 被操作系统中的加载器从硬盘中读取,加载到内存中去执行。
- 目标文件(.o) :被链接器读取,用来产生一个可执行文件或者共享文件。
- 共享文件(.so) :在动态链接的时候,由
ld-linux.so来读取。
ELF文件结构
ELF文件结构可以从两个不同的角度来看:链接视图和执行视图。
- ELF头描述整个文件的基本属性和布局。
- 程序头表指导操作系统加载器如何将文件映射到内存。它描述的是ELF文件的执行视图。
- 节区头表描述了文件中每个节区的属性。每个在段表中的条目都对应一个节区,包含了该节区的名称、类型、大小、位置、权限等信息。
- 节区是文件的逻辑组织单位,服务于链接器和调试器。
- 段区是文件的物理组织单位,服务于操作系统加载器。
在链接和分析阶段,我们更关心ELF头和节区头表。
在程序加载和执行阶段,操作系统则主要依赖ELF头和程序头表。
链接视图
+---------------------------+
| ELF Header |
| (描述文件基本信息) |
+---------------------------+
| |
| Program Header Table |
| (可选,但通常存在) |
| (描述如何将文件加载到内存)|
+---------------------------+
| |
| .text (代码段) |
| .data (已初始化数据) |
| .rodata (只读数据) |
| .bss (未初始化数据) |
| .symtab (符号表) |
| .rel.text (重定位信息) |
| .strtab (字符串表) |
| ... (其他Section) |
| |
+---------------------------+
| |
| Section Header Table |
| (描述每个节区的属性和位置)|
+---------------------------+
执行视图
+---------------------------+
| ELF Header |
| (描述文件基本信息) |
+---------------------------+
| |
| Program Header Table |
| (描述每个段的属性和位置) |
+---------------------------+
| |
| .text, .rodata |
| (只读代码段) |
| Segment 1 |
+---------------------------+
| |
| .data, .bss |
| (读写数据段) |
| Segment 2 |
+---------------------------+
| .symbltab, .rel.text, |
| .rel.data, .debug, .line |
| .strtab, .shstrtab |
| ... |
| 不加载到内存的 |
| 符号表,调试信息,等等 |
| |
+---------------------------+
| |
| Section Header Table |
| (可选,但通常存在) |
| (描述每个节区的属性和位置)|
+---------------------------+
ELF Header
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // 魔术数(0x7fELF),文件的类型(32/64), 大小端等
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; // 和处理器相关的flag
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;
Program Header Table
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; // 与段相关的flag
ELF32_Word p_align; // 段在文件以及内存中的对齐方式
} Elf32_Phdr;
p_type解释:
- PT_NULL:表示程序头表中的一个无效条目。
- PT_LOAD:代表一个可加载的段。操作系统加载器会根据这个类型,将ELF文件中指定的数据区域(p_offset 和 p_filesz)从文件映射到内存的特定地址(p_vaddr),并赋予它相应的权限(p_flags)。一个可执行文件通常包含至少两个 PT_LOAD 段
- 一个用于代码和只读数据(通常是只读和可执行)。
- 一个用于可读写数据(通常是可读可写)。
- PT_DYNAMIC:表示动态链接信息。它指向一个包含动态链接器所需数据的段,比如共享库的依赖列表、符号重定位信息等。当程序启动时,动态链接器会读取这个段来解析和加载所有需要的共享库。
- PT_INTERP:表示程序解释器(interpreter)的路径,对于动态链接的可执行文件,这个段会指向动态链接器本身的路径,例如在 Linux 上通常是 /lib/ld-linux.so.2。操作系统在加载程序时会首先加载并运行这个解释器。
- PT_NOTE:表示一个注意段。它通常用于存储关于文件或操作系统的附加信息,例如核心转储(core dump)文件中的系统状态信息。
- PT_SHLIB:保留。
- PT_PHDR:型表示程序头表本身,指向 ELF 文件中程序头表的起始位置和大小。
- PT_TLS:表示线程本地存储(Thread-Local Storage,TLS)信息。它指向包含线程本地变量初始值的段。
Section Header Table
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;
节区
节区(Section)是 ELF 文件链接视图的核心组成部分。它们将文件内容划分为逻辑块,每个块都有特定的用途。这些节区包含了程序运行时和链接时所需的所有数据。
代码和数据
- .text: 包含程序的可执行机器代码。这是 CPU 在运行时实际执行的指令。
- .data: 包含已初始化的全局变量和静态变量。这些变量在编译时就已经被赋了初始值,并在程序加载时被放置在可读写的数据段中。
- .rodata: 包含只读数据。比如字符串字面量和 const关键字修饰的变量。这部分数据通常与 .text 节区一起被加载到只读内存中,以防止被意外修改。
- .bss: 包含未初始化的全局变量和静态变量。这个节区在文件中不占空间,编译器只记录需要多少空间。当程序加载时,操作系统会为 .bss 分配一块内存并自动将其清零。这样做可以节省文件大小。
链接和调试信息
- .symtab: 符号表。它列出了程序中定义的或引用的所有符号,如函数名、全局变量名等。链接器使用它来解析符号引用,调试器则用它来帮助你通过名称而不是地址来访问变量和函数。
- .strtab: 字符串表。这个节区存储了符号表(.symtab)和节区头表(Section Header Table)中使用的所有字符串,比如变量名、函数名和节区名。
- .shstrtab: 节区名称字符串表。专门存储所有节区的名称字符串,通过这个表,你可以根据 Section Header Table 中的索引找到每个节区的名称。
- .rela.text, .rela.data 等: 重定位信息。这些节区包含了在链接过程中需要被修改的地址。比如,当一个目标文件调用了另一个文件中的函数时,链接器需要将这个调用指令的地址修正为最终的函数地址。
- .debug: 调试信息。这个节区通常包含源文件和行号信息,以及变量的类型和位置,帮助调试器提供强大的调试功能。
- .dynamic:包含了动态链接器所需的所有信息,主要作用是为动态链接器提供依赖库列表、符号重定位表、符号表和字符串表、哈希表、版本信息等关键信息。
- .got:全局偏移表(Global Offset Table),存储了所有外部全局变量和函数的运行时绝对地址,当程序启动时,加载器会填充 .got 表中的地址。对于函数,它最初存放的是一个特殊的地址,指向 .plt 表中的一个条目。
- .plt:过程链接表(Procedure Linkage Table),包 含了为每个外部函数生成的桩代码(stub code)。当程序第一次调用一个外部函数时,它会先通过 .plt 表中的代码来跳转,由这部分代码负责寻找并调用真正的函数。
- got.plt:是一个特殊的全局偏移表,它的作用与 .got 类似,但专门用于动态链接的函数。为了支持 延迟绑定(lazy binding),即只有当函数第一次被调用时才去解析它的地址。
段区
段区(Segment)是 ELF 文件的执行视图核心,它只存在于可加载的可执行文件和共享库中。段区的目的是为了让操作系统加载器(loader)能够高效地将程序加载到内存中。
段区将相关的节区组合在一起,以便它们可以被一次性映射到内存中一个具有特定权限的连续区域。
段区的类型由 程序头表中的 p_type 字段定义:
- 可执行段:通常包含程序的代码和只读数据,也就是由 .text 和 .rodata 节区组合而成。当加载器将这个段加载到内存后,会给它赋予只读和可执行的权限。
- 可读写段:这个段通常包含已初始化数据和未初始化数据,也就是由 .data 和 .bss 节区组合而成。加载器会给这个段赋予可读和可写的权限。.data 节区的数据会从文件中加载到这个段中,而 .bss 节区则只会在内存中分配空间并清零。
- 动态链接段:这个段包含了动态链接器(dynamic linker)所需的信息。它通常由 .dynamic 节区构成,包含共享库依赖列表、符号重定位信息等。当程序启动时,动态链接器会读取这个段,来加载所有需要的共享库。