ELF格式解析
ELF格式解析
此前通过《程序员的自我修养》这本书大抵了解一二,但是基本忘记了,或者说对于当时的我来说,是一种囫囵吞枣。
在认真学习了一遍PE格式和做了南大PA的一部分后,我现在对链接、装载和库的了解愈发显得不够,需要在读一遍,也算是一种螺旋上升吧。
由于先哲说的:“Write programs that do one thing and do it well”,我决定这次阅读笔记拆开来写,而ELF格式就是第一步。
工具
file #查看文件格式
objdump #用来查看object的结构
objcopy #用于修改段
readelf #解析elf
ELF的来源和不同种类
ELF来自于Unix的COFF,PE也是,所以两者很像。
| 文件类型 | 说明 |
|---|---|
| 可重定位文件 Relocatable | .o .obj |
| 可执行文件 Executable | .exe |
| 共享目标文件 Shared Object | .so .dll |
| 核心转储文件 | coredump |
事前须知
以下内容都是AI生成的。
两种视图
ELF (Executable and Linkable Format) 文件格式为同一文件内容提供了两种不同的视图,以适应链接和执行两种不同场景的需求。
- 链接视图 (Linking View): 以 节 (Section) 为单位。这是编译器、汇编器和链接器处理文件时使用的视图。文件中包含了多个节,例如
.text(代码)、.data(已初始化数据)、.symtab(符号表)等。这些节的信息由 节头表 (Section Header Table) 来描述。 - 执行视图 (Execution View): 以 段 (Segment) 为单位。这是加载器(操作系统的一部分)执行程序时使用的视图。加载器将文件中一个或多个属性相似的节(Section)合并成一个 段 (Segment),然后将这些段加载到内存中。段的信息由 程序头表 (Program Header Table) 来描述。
一个段通常包含一个或多个节。例如,一个可执行的段(通常标记为 LOAD 和 executable)可能同时包含 .text 节 (代码) 和 .rodata 节 (只读数据)。
地址
- 虚拟地址 (Virtual Address, VA): 程序被加载到内存后,指令和数据所处的地址。这是程序在运行时实际使用的地址。
- 文件偏移 (File Offset): 数据在ELF文件磁盘上存储时,相对于文件开头的偏移量。
ELF总结构
一个典型的ELF文件由以下部分组成:
ELF 文件结构
├─ ELF 头 (ELF Header)
│ ├─ e_ident: ELF 标识(魔数、位数、字节序等)
│ ├─ e_type: 文件类型(可重定位、可执行、共享对象等)
│ ├─ e_machine: 目标机器架构(如 x86-64, ARM)
│ ├─ e_entry: 程序入口虚拟地址
│ ├─ e_phoff: 指向程序头表的文件偏移
│ ├─ e_shoff: 指向节头表的文件偏移
│ ├─ e_ehsize: ELF 头自身大小
│ ├─ e_phentsize: 程序头表中每个条目的大小
│ ├─ e_phnum: 程序头表中条目的数量
│ ├─ e_shentsize: 节头表中每个条目的大小
│ ├─ e_shnum: 节头表中条目的数量
│ └─ e_shstrndx: 节名字符串表在节头表中的索引
│
├─ 程序头表 (Program Header Table) [可选,可执行文件和共享库必须有]
│ ├─ 数量 = ELF Header.e_phnum
│ └─ 每个程序头表项 (Segment Header) 包含:
│ ├─ p_type: 段类型(如 PT_LOAD, PT_DYNAMIC)
│ ├─ p_flags: 段标志(可读 R、可写 W、可执行 X)
│ ├─ p_offset: 段内容在文件中的偏移
│ ├─ p_vaddr: 段在内存中的虚拟地址
│ ├─ p_paddr: 段在内存中的物理地址(通常忽略)
│ ├─ p_filesz: 段在文件中的大小
│ ├─ p_memsz: 段在内存中的大小
│ └─ p_align: 段的对齐要求
│
├─ 节数据 (Section Data)
│ ├─ .text: 可执行代码
│ ├─ .data: 已初始化的全局变量和静态变量
│ ├─ .bss: 未初始化的全局变量和静态变量(在文件中不占空间)
│ ├─ .rodata: 只读数据(如字符串常量)
│ ├─ .symtab / .dynsym: 符号表 / 动态符号表
│ ├─ .strtab / .dynstr: 字符串表 / 动态字符串表
│ ├─ .rel.text / .rela.text: 代码段的重定位信息
│ ├─ .interp: 指定动态链接器路径
│ ├─ .dynamic: 动态链接所需信息
│ └─ 其他各种节...
│
└─ 节头表 (Section Header Table) [可选,可重定位文件必须有]
├─ 数量 = ELF Header.e_shnum
└─ 每个节头表项 (Section Header) 包含:
├─ sh_name: 节名称(在节名字符串表中的索引)
├─ sh_type: 节类型(如 SHT_PROGBITS, SHT_SYMTAB)
├─ sh_flags: 节属性(如 SHF_WRITE, SHF_ALLOC, SHF_EXECINSTR)
├─ sh_addr: 节在内存中的虚拟地址(如果可加载)
├─ sh_offset: 节在文件中的偏移
├─ sh_size: 节的大小
├─ sh_link: 与其他节的链接信息(如符号表对应的字符串表)
├─ sh_info: 额外信息(依赖于节类型)
├─ sh_addralign: 地址对齐要求
└─ sh_entsize: 如果节是表,则为每个表项的大小
ELF头 (ELF Header)
ELF头位于文件的最开始,描述了整个文件的基本属性。
// 对应 <elf.h> 中的 Elf64_Ehdr
ELF64_Header {
unsigned char e_ident[16]; // ELF标识,包含魔数、位数、字节序等信息
uint16_t e_type; // 文件类型 (可执行、目标文件、共享库)
uint16_t e_machine; // 硬件架构 (如 EM_X86_64)
uint32_t e_version; // ELF版本,通常为1
uint64_t e_entry; // 程序入口的虚拟地址,可重定位文件一般没有入口,这里被设为0
uint64_t e_phoff; // 程序头表(Program Header Table)的文件偏移
uint64_t e_shoff; // 节头表(Section Header Table)的文件偏移
uint32_t e_flags; // 处理器相关的标志
uint16_t e_ehsize; // ELF头自身的大小
uint16_t e_phentsize; // 程序头表中每个条目的大小
uint16_t e_phnum; // 程序头表中的条目数量
uint16_t e_shentsize; // 节头表中每个条目的大小
uint16_t e_shnum; // 节头表中的条目数量
uint16_t e_shstrndx; // 节名字符串表(.shstrtab)在节头表中的索引
}
e_ident: 这个16字节的数组是关键。- 前4个字节是魔数:
0x7F 'E' 'L' 'F'。 - 第5个字节标识文件是32位 (
1) 还是64位 (2)。 - 第6个字节标识字节序(小端或大端)。
- 第7个字节定义主版本号,一般为1,因为ELF更新到1.2后再也没变过。
- 后面9个字节没有定义,一般填0,有些平台也许会有扩展。
- 前4个字节是魔数:
e_type: 文件类型,分别为:ET_REL: 可重定位文件 (.o文件)。枚举量为1ET_EXEC: 可执行文件。为2ET_DYN: 共享对象文件 (.so文件)。为3
e_phoff,e_phnum,e_phentsize: 这三个字段共同定义了程序头表的位置、大小和条目数。e_shoff,e_shnum,e_shentsize: 这三个字段共同定义了节头表的位置、大小和条目数。
程序头表 (Program Header Table)
程序头表描述了系统如何将文件内容加载到内存中形成一个进程镜像。它是一个由多个程序头(Program Header)组成的数组。可执行文件和共享对象必须有程序头表。
// 对应 <elf.h> 中的 Elf64_Phdr
Program_Header {
uint32_t p_type; // 段(Segment)的类型
uint32_t p_flags; // 段的标志 (读、写、执行)
uint64_t p_offset; // 段在文件中的偏移
uint64_t p_vaddr; // 段在内存中的虚拟地址
uint64_t p_paddr; // 段的物理地址 (通常被忽略)
uint64_t p_filesz; // 段在文件中的大小
uint64_t p_memsz; // 段在内存中的大小
uint64_t p_align; // 段在内存和文件中的对齐要求
}
p_type: 段的类型,常见的有:PT_LOAD: 可加载段。这种类型的段会被加载到内存中,如代码段和数据段。PT_DYNAMIC: 动态链接信息。该段包含了动态链接器所需的信息。PT_INTERP: 指定了程序解释器(即动态链接器)的路径,例如/lib64/ld-linux-x86-64.so.2。PT_PHDR: 程序头表自身的位置和大小。
p_offset,p_vaddr,p_filesz,p_memsz: 这四个字段是加载的关键。加载器会从文件偏移p_offset处读取p_filesz大小的数据,并将其映射到内存的虚拟地址p_vaddr处,占用p_memsz大小的内存空间。p_memsz>p_filesz: 这种情况常见于.bss节所在的数据段。.bss节存放未初始化的全局变量和静态变量,在文件中不占用空间,但在内存中需要分配空间并清零。因此,内存大小会比文件大小要大。
节头表 (Section Header Table)
节头表包含了文件中所有节的描述信息。它是一个由节头(Section Header)组成的数组。链接器使用节头表来处理符号、重定位等操作。
// 对应 <elf.h> 中的 Elf64_Shdr
Section_Header {
uint32_t sh_name; // 节名称 (在节名字符串表.shstrtab中的偏移)
uint32_t sh_type; // 节的类型
uint64_t sh_flags; // 节的属性 (可写、可分配、可执行等)
uint64_t sh_addr; // 节在内存中的起始虚拟地址 (如果被加载),否则为0
uint64_t sh_offset; // 节在文件中的偏移
uint64_t sh_size; // 节的大小
uint32_t sh_link; // 与其他节的链接信息
uint32_t sh_info; // 额外信息,含义取决于节类型
uint64_t sh_addralign; // 节的地址对齐约束,如果为3,就表明要与8对齐。如果该值为0或1,则没有对齐要求
uint64_t sh_entsize; // 如果节是表,此字段为每个表项的大小
}
sh_name: 节的名称,如.text、.data。它并不是直接存储字符串,而是一个指向.shstrtab(节名字符串表) 的索引。sh_type: 节的类型,从0开始枚举:SHT_NULL:无效段SHT_PROGBITS: 程序数据(如代码、数据)。SHT_SYMTAB: 符号表 (.symtab)。SHT_STRTAB:字符串表SHT_RELA:重定位表SHT_HASH:符号表的哈希表SHT_DYNAMIC:动态链接信息SHT_NOTE:提示性信息SHT_NOBITS: 不占用文件空间的节,如.bss。SHT_REL: 重定位表。SHT_SHLIB: 保留SHT_DYNSYM: 动态符号表 (.dynsym)。
sh_flags: 描述节的属性,如:SHF_WRITE (1): 可写。SHF_ALLOC (2): 加载时需在内存中分配空间。SHF_EXECINSTR (4): 包含可执行指令。
sh_link和sh_info: 这两个字段的含义与sh_type相关。例如,对于一个符号表节 (SHT_SYMTAB),sh_link指向其对应的字符串表节,sh_info则可能表示第一个非局部符号的索引。
各类表
字符串表
.strtab和.shstrtab
符号表 (.symtab)
符号表包含了定位和重定位程序中符号定义和引用所需的信息。本质上就是下面结构的数组。
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 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
st_info低四位
| 宏定义名 | 值 | 说明 |
|---|---|---|
| STB_LOCAL | 0 | 局部符号,对于目标文件的外部不可见 |
| STB_GLOBAL | 1 | 全局符号,外部可见 |
| STB_WEAK | 2 | 弱引用,详见 “弱符号与强符号” |
高28位
| 宏定义名 | 值 | 说明 |
|---|---|---|
| STT_NOTYPE | 0 | 未知类型符号 |
| STT_OBJECT | 1 | 该符号是个数据对象,比如变量、数组等 |
| STT_FUNC | 2 | 该符号是个函数或其他可执行代码 |
| STT_SECTION | 3 | 该符号表示一个段,这种符号必须是 STB_LOCAL 的 |
| STT_FILE | 4 | 该符号表示文件名,一般都是该目标文件所对应的源文件名,它一定是 STB_LOCAL 类型的,并且它的 st_shndx 一定是 SHN_ABS |
st_shndx
| 宏定义名 | 值 | 说明 |
|---|---|---|
| SHN_ABS | 0xfff1 | 表示该符号包含了一个绝对的值。比如表示文件名的符号就属于这种类型的 |
| SHN_COMMON | 0xfff2 | 表示该符号是一个 “COMMON 块” 类型的符号,一般来说,未初始化的全局符号定义就是这种类型的,比如 SimpleSection.o 里面的 global_uninit_var。有关 “COMMON” 详见 “深入静态链接” 之 “COMMON 块” |
| SHN_UNDEF | 0 | 表示该符号未定义。这个符号表示该符号在本目标文件被引用到,但是定义在其他目标文件中 |
重定位表 (.rel / .rela)
当链接器将多个目标文件合并时,需要修正代码中对某些符号地址的引用,这个过程就是重定位。重定位表记录了所有需要修正的位置。
有两种类型的重定位表:
.rel: 不包含附加数的重定位项。附加数隐式地存在于被修改的位置本身。.rela: 包含一个显式的附加数。x86-64 架构通常使用.rela表。
另外.rel.text代表是对text节的重定位。
// 对应 <elf.h> 中的 Elf64_Rela
Relocation_Entry_with_Addend {
uint64_t r_offset; // 需要重定位的位置的偏移或虚拟地址
uint64_t r_info; // 符号表索引 (高32位) 和重定位类型 (低32位)
int64_t r_addend; // 一个常数加数
}
r_offset: 在可重定位文件中,这是从节的开始到被修改位置的偏移;在可执行文件中,这是被修改位置的虚拟地址。r_info:- 符号索引: 指向符号表中的一个条目,该条目就是本次重定位所引用的符号。
- 重定位类型: 定义了如何计算要填入
r_offset处的最终值。例如,R_X86_64_PC32表示一个相对于程序计数器(PC)的32位有符号偏移,常用于函数调用。
- 重定位计算公式:
最终地址 = 符号地址 + 附加数 - PC地址(这是一个简化的例子,具体公式取决于重定位类型)。
.init、.fini
全局构造函数和析构函数。在main之前和结束后运行。
动态链接相关表
动态链接允许程序在运行时才解析对共享库中函数和变量的引用。这主要通过 .dynamic 段、全局偏移表 (GOT) 和过程链接表 (PLT) 实现。
.dynamic 段
这是一个由 Elf64_Dyn 结构体组成的数组,包含了动态链接器所需的所有核心信息。
// 对应 <elf.h> 中的 Elf64_Dyn
Dynamic_Entry {
int64_t d_tag; // 动态数组项的类型
union {
uint64_t d_val; // 整数值
uint64_t d_ptr; // 地址或偏移
} d_un;
}
d_tag: 常见的类型有:DT_NEEDED: 依赖的共享库名称(值在.dynstr中)。DT_SYMTAB:.dynsym动态符号表的地址。DT_STRTAB:.dynstr动态字符串表的地址。DT_PLTGOT: 全局偏移表/过程链接表的地址。DT_RELA,DT_RELASZ,DT_RELAENT:.rela.dyn或.rela.plt重定位表的位置、总大小和单项大小。
全局偏移表 (GOT) 和过程链接表 (PLT)
为了实现代码段在多进程间共享(地址无关代码,PIC),对外部函数和变量的引用不能使用绝对地址。
- 全局偏移表 (Global Offset Table, .got): 这是一个数据段中的表,存储了外部变量的绝对地址。程序通过访问 GOT 间接引用外部变量。(部分系统将 GOT 拆分为
.got和.got.plt,前者存全局变量,后者存函数地址) - 过程链接表 (Procedure Linkage Table, .plt): 这是一个代码段中的表,每个外部函数在 PLT 中都有一个条目。首次调用函数时,PLT 条目会将控制权交给动态链接器,由其查找函数真实地址并填入 GOT 中。后续调用则直接通过 GOT 跳转到函数真实地址,实现所谓的“延迟绑定” (Lazy Binding)。

浙公网安备 33010602011771号