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 节区构成,包含共享库依赖列表、符号重定位信息等。当程序启动时,动态链接器会读取这个段,来加载所有需要的共享库。