ELF(Executable and Linkable Format)文件是一种标准文件格式,用于在类Unix操作系统中表示可执行文件、可重定位文件、共享库和核心转储。
1 ELF文件的种类
ELF文件主要有以下几种类型:
可重定位文件(Relocatable File):这类文件包含了代码和数据,可被用来链接成可执行文件或者共享目标文件。在Linux系统中,这种文件的后缀一般为.o。它们是由编译器生成的,用于后续的链接过程。
可执行文件(Executable File):这类文件包含了可以直接执行的程序,一般没有扩展名。它们是链接器处理可重定位文件后输出的文件,用于创建进程映像。这种类型的文件也可以分为两类Executable file和Position-Independent Executable file。这两种文件的区别可以看我的这个回答:
共享目标文件(Shared Object File):这类文件就是我们通常所称的动态库文件,一般以.so结尾。它可以在以下两种情况下使用:
-
链接器可以使用动态库文件和可重定位文件链接,生成可执行文件。通过这种方式生成的可执行文件可以叫做动态链接生成的文件
-
程序可以在运行时使用dlopen函数加载动态库文件,执行动态库文件中的代码。
核心转储文件(Core Dump File):当进程意外终止时,系统可以将该进程的地址空间的内容以及终止时的一些其他信息转储在核心转储文件中。这些文件通常用于调试和分析程序崩溃的原因。
2 ELF文件的组成
ELF 文件由以下几个主要部分组成:
(1)ELF 头部(ELF Header):包含了文件的总体信息,比如入口点地址、程序头表和节头表的位置等。
(2)程序头表(Program Header Table):由一组program header组成。program header给操作系统/动态链接器加载程序到内存中提供了必要的信息。
PS:操作系统本身就有一个elf loader用于加载elf可执行文件,对于Executable file,elf loader会将其直接加载到内存中;对于Position-Independent Executable file,elf loader会将动态链接器(ld.so)加载到内存中,再由动态链接器加载Position-Independent Executable file到内存中(需要注意的是动态链接器加载Position-Independent Executable file是在用户态完成的)。
(3)节头表(Section Header Table):由一组section header组成。section header描述了文件中节(section)的信息,比如节的类型、大小、加载到内存中的地址、在文件中的偏移等。
(4)节(Sections):包含了实际的数据和代码,比如.text中保存了程序的代码,.data中保存了初始化不为0全局变量,.rodata中保存了一些只读的数据(比如字符串常量)。

2.1 ELF Header
记录着elf文件的大致信息:
-
e_ident:这是一个 16 字节的数组,用于标识 ELF 文件的格式和属性。它的前几个字节(通常是前 4 个字节)被称为“魔数”,用于快速识别文件是否为 ELF 文件,以及它的字节序和位数。例如,前 4 个字节通常为 0x7F 'E' 'L' 'F'。
-
e_type:指定了 ELF 文件的类型,比如是可重定位文件、可执行文件、动态库文件还是核心转储文件。
-
e_machine:指定了生成此 ELF 文件的处理器架构,比如 x86-64、ARM 等。
-
e_version:指定了对象文件的版本,通常是 1,表示当前版本的 ELF 规范。
-
e_entry:如果这个 ELF 文件是一个可执行文件或共享对象,这个字段指定了程序的入口点地址,即程序开始执行的虚拟地址。
-
e_phoff:如果存在程序头表,这个字段指定了程序头表在文件中的偏移量。
-
e_shoff:如果存在节头表,这个字段指定了节头表在文件中的偏移量。
-
e_flags:用于指定与处理器相关的特定标志,这些标志可能会影响程序的链接和执行。
-
e_ehsize:指定了 ELF 头部自身的大小,单位为字节。
-
e_phentsize:指定了程序头表中每个条目的大小,单位为字节。
-
e_phnum:指定了程序头表中条目的数量。
-
e_shentsize:指定了节头表中每个条目的大小,单位为字节。
-
e_shnum:指定了节头表中条目的数量。
-
e_shstrndx:指定了节头字符串表在节头表中的索引。节头字符串表包含了所有节的名称。
elf header对应的结构体定义如下:
/* The ELF file header. This appears at the start of every ELF file. */
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

2.2 Program Header
program header给操作系统或动态链接器加载程序到内存中提供了必要的信息。一个program header对应着一个segment(称作段),一个segment包含着多个section,在动态库加载时,动态链接器是以segment为基本单位将动态库中的内容加载到内存中去的(只需要加载类型为LOAD的segment到内存中)。
PS:不是所有的section都被包含在segment中的,有些section不需要被加载到内存中,例如.debug开头的一些section。
下面是一些常见的segment类型(p_type中保存着segment的类型):
-
PT_LOAD:表示一个需要被加载到内存中的程序段。这是最常见的段类型,通常包括可执行文件的.text、.data等部分。
-
PT_DYNAMIC:包含动态链接的信息,用于动态链接器解析和处理动态链接库。
-
PT_INTERP:指定程序运行时需要的解释器路径,通常用于动态链接的可执行文件,指向动态链接器(如ld-linux.so)。
-
PT_NOTE:包含辅助信息,如系统特定或硬件特定的信息。
-
PT_PHDR:描述程序头表本身的一个条目。
-
PT_TLS:表示线程本地存储段。
下面是program header中各字段的作用:
-
p_type:指定了段的类型。不同的类型代表不同的段。例如LOAD、DYNAMIC、NOTE等。
-
p_flags:指定了段的属性和权限,例如是否可读、可写、可执行。这些标志影响动态链接器如何将段加载到内存中。(其实就是mmap参数的设置)
-
p_offset:指定了段在 ELF 文件中的偏移量,即从文件开头到段的起始位置的距离。
-
p_vaddr:如果这个段被加载到内存中,这个字段指定了段的虚拟地址。这是段在内存中的起始地址。
-
p_paddr:在某些系统中,这个字段指定了段的物理地址。但在现代操作系统中,这个字段通常不被使用。
-
p_filesz:指定了段在文件中的大小,即段实际占用的字节数。
-
p_memsz:指定了段在内存中的大小,它大于或等于 p_filesz,因为某些段可能需要在加载时进行填充,比如包含.bss section的段,它需要填一堆0。
-
p_align:指定了段在内存中的对齐要求。这个字段保证了段的虚拟地址能够按照特定的边界对齐,通常这个对齐边界是 p_align 字段值的倍数。
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;

2.3 Section Header
描述了文件中的节(section)的信息。
-
sh_name:节的名称。这是一个索引值,记录着节的名称在.shstrtab中的起始位置。
-
sh_type:节的类型。这个字段指定了节的用途,例如PROGBITS表示这个节保存着程序定义的数据,如代码或初始化数据;GNU_HASH表示这个节保存着GNU版本的符号哈希表;DYNSYM表示这个节保存着动态符号表,里面包含着动态链接时使用的符号。
-
sh_flags:节的标志。这是一个位掩码,用于指定节的属性,比如是否可写、是否可执行、是否分配内存等。
-
sh_addr:节的虚拟地址。当程序被加载到内存中时,这个字段指定了节的地址。
-
sh_offset:节在文件中的偏移量。这个字段指定了节在ELF文件中的起始位置。
-
sh_size:节的大小。这个字段指定了节在文件中所占的字节数。
-
sh_link:链接到另一个节。这个字段用于链接到其他节,通常用于符号表和重定位表。
-
sh_info:附加的节信息。这个字段提供了额外的信息,具体含义取决于节的类型。
-
sh_addralign:节的对齐。这个字段指定了节在内存中的对齐要求。
-
sh_entsize:节中条目的大小。如果节包含一个表格(例如符号表),这个字段指定了表中每个条目的大小。
/* Section header. */
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

2.4 Section
在 ELF文件格式中,section是保存数据和代码的基本单元。下面是一些常见的section中保存的内容:
-
.bss:包含未初始化的数据。
-
.comment:包含版本控制信息。
-
.data:包含初始化的数据。
-
.debug:包含用于符号调试的信息。
-
.dynamic:包含动态链接的信息。
-
.hash:包含一个符号哈希表。
-
.line:包含用于符号调试的行号信息。
-
.rodata:包含只读数据。
-
.shstrtab:包含节名。
-
.strtab:包含字符串,通常是符号表条目的名称。
-
.symtab:包含完整的符号表。
-
.text:包含程序的可执行指令。
3 总结
简单来说,elf header描述了elf文件的基本信息,程序头表描述了elf文件该如何被加载到内存中(例如哪些部分需要加载到内存中,辅助加载的信息保存在哪里),节头表描述了elf文件中各个section的具体信息,section中保存着elf文件的数据或代码。
PS:对于可执行文件和动态库文件来说,节头表不一定存在,它是可以被剪裁掉的,程序头表是必须存在的,而对于可重定位文件来说,节头表是必须存在的,程序头表不一定存在。从用途来看,程序头表在可执行文件或动态库文件加载时使用,节头表在可重定位文件链接时使用。
