讲讲 Linux 平台的 ELF 文件结构
ELF 文件的两种视图
理解 ELF 文件的关键在于,它有两个不同的、但相互关联的视图:
- 链接视图(Linking View): 供编译器和链接器使用。它由节(Sections)组成,主要用于编译、链接和重定位
- 执行视图(Execution View): 供操作系统加载器使用。它由段(Segments)组成,用于将程序加载到内存中并执行
这两种视图由 ELF 头部中的两个表来描述:节头部表和程序头部表
ELF 头部
每个 ELF 文件的开头都是一个 ELF 头部,它提供了文件的基本信息,就像 PE 文件的 DOS 头部一样。它定义了文件的类型、机器架构、入口点地址等
ELF 头部最重要的一些字段是:
e_ident[EI_MAG0-3]
:4 字节的魔数,固定为0x7f, 'E', 'L', 'F'
。这是识别 ELF 文件的唯一标志e_ident[EI_CLASS]
:指定文件架构,1
代表 32 位,2
代表 64 位e_ident[EI_DATA]
:指定字节序,1
代表小端序,2
代表大端序e_type
:文件类型,如ET_EXEC
(可执行文件)、ET_DYN
(共享库)、ET_REL
(可重定位文件)e_entry
:程序的入口点地址(虚拟地址)e_phoff
:程序头部表(Program Header Table)的文件偏移量e_shoff
:节头部表(Section Header Table)的文件偏移量e_phentsize
:程序头部表中每个条目的大小e_phnum
:程序头部表的条目数量e_shentsize
:节头部表中每个条目的大小e_shnum
:节头部表的条目数量
链接视图:节
节是 ELF 文件的基本单元,用于组织文件中的各种数据和代码。每个节都有特定的目的
常见的节有:
.text
:包含可执行代码.data
:包含已初始化的全局变量和静态变量.rodata
:包含只读数据,如字符串常量.bss
:包含未初始化的全局变量和静态变量,在文件中不占用空间,加载时由加载器分配和清零.symtab
:符号表,包含了程序中所有符号(函数名、变量名)的信息.strtab
:字符串表,存储符号表中的字符串.debug
:调试信息,用于 GDB 等调试器.got
(Global Offset Table):全局偏移表,用于在运行时解析外部函数地址.plt
(Procedure Linkage Table):过程链接表,用于动态链接
节头部表
节头部表是一个描述所有节的数组。每个条目都是一个 Elf64_Shdr
(对于 64 位)结构体,它包含了每个节的名称、类型、权限、文件偏移、内存地址和大小等信息。链接器和反汇编工具(如 objdump)主要依赖这个表来分析文件结构
执行视图:段
当程序需要被加载到内存中执行时,节会被组合成更大的逻辑单元——段。每个段都具有相同的内存权限(可读、可写、可执行)。这是加载器关心的内容
典型的段有两个:
- 代码段(Code Segment): 通常包含
.text
和.rodata
节。这个段被映射到内存中,并具有可读和可执行权限 - 数据段(Data Segment): 通常包含
.data
和.bss
节。这个段被映射到内存中,并具有可读和可写权限
程序头部表
程序头部表是一个描述所有段的数组。每个条目都是一个 Elf64_Phdr
结构体,它包含了每个段的类型、文件偏移、内存地址、大小和权限等信息。加载器通过遍历这个表,将 ELF 文件中的内容映射到内存中
p_type
:段的类型,如PT_LOAD
(可加载到内存)p_offset
:段在文件中的偏移量p_vaddr
:段在内存中的虚拟地址p_memsz
:段在内存中的大小p_flags
:段的权限,如PF_R
(可读)、PF_W
(可写)、PF_X
(可执行)
ELF 文件加载过程
- 操作系统内核的加载器读取 ELF 头部,找到程序头部表
- 加载器遍历程序头部表中的所有条目
- 对于每个类型为
PT_LOAD
的段,加载器将文件中的相应部分,从p_offset
偏移处开始,映射到内存中的p_vaddr
虚拟地址上 - 加载器根据
p_flags
设置内存页的权限(读、写、执行) - 所有段加载完毕后,加载器将程序控制权交给
e_entry
字段指定的入口点地址,程序开始执行