参考:
《程序员的自我修养—链接、装载与库》
《Linux二进制分析》

ELF 文件

ELF,全称 Executable and Linking Format,旨在为不同操作环境下提供一组通用的ABI(二进制接口),也是目前Unix和类Unix操作系统使用的标准二进制格式。ELF文件格式定义了可执行程序的静态文件格式,包括文件信息头、段及节等结构,并约定了程序在运行时,程序文件的内容是如何动态加载到内存中以及起始运行地址。

ELF 文件类型

ELF文件从字面的意义上来讲,描述的是可执行和链接格式,因此包括可执行文件、目标文件以及动态库都可以采用ELF文件格式进行存储。ELF标准中定义的ELF文件类型主要有以下四种:

ELF文件类型 类型标记 说明
可重定位文件 ET_REL 可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码,如.o文件
可执行文件 ET_EXEC 即可运行的程序,是一个进程开始执行的入口,平时使用的shell、find等工具都属于此类
共享目标文件 ET_DYN 动态可链接目标文件,即共享库,会在程序运行时被装载并连接到程序的进程镜像中,如.so文件
核心转储文件 ET_CORE 在程序崩溃或者进程传递了一个SIGSEGV信号时,会在核心文件中记录整个进程的镜像信息,如Linux的进程Coredump文件

需要注意的是, .a 文件(静态链接库)通常是由多个 .o 文件(可重定位目标文件)打包而成的归档文件。虽然 .a 文件本身不是 ELF 文件格式,但其中包含的 .o 文件是 ELF 格式的。因此,.a 文件可以包含多个 ELF 格式的可重定位目标文件。而 .so 文件(动态链接库)本身就是 ELF 文件格式的动态可链接目标文件。

Linux 下可以使用 file 命令来查看不同 ELF 文件类型的格式:

image.png

ELF 文件的内容

一个标准的 ELF 文件通常包含了如下几类信息:

  • 文件头信息:描述关于文件的整体信息,包括版本号、系统类型以及文件类型等信息;

  • 指令和数据:由编译器或汇编器生成的二进制指令和数据;

  • 符号信息:该模块中定义的全局符号,以及从其它模块导入的或者有链接器定义的符号;

  • 重定位信息:记录文件中符号的重定位信息,对于后续要链接的ELF文件,链接器会使用重定位信息对目标代码进行调整;

  • 调试信息:目标代码中与链接无关但会被调试器使用到的其它信息,包括源代码文件和行号信息、本地符号以及目标代码使用的数据结构描述信息。

为了管理上面的信息,ELF文件通常会将这些信息按照不同的属性,以段(Segment)或节(Section)的形式进行存储,常用的段或节包括代码段、数据段、.bss段等。

在实际的开发过程中,我们可能更加关心程序中使用的代码和数据在ELF文件中是如何存储的,对应于上面的指令和数据部分。如下是一个简单的程序被编译成ELF文件后的结构:

image.png

程序代码被编译后主要分为两种类型的数据:程序指令和程序数据,其中程序指令通常存储于代码段,程序数据按照其数据属性通常存储于数据段、.bss段等区域:

  • 代码段(.text):存储程序源代码编译后的机器指令;

  • 数据段(.data):存储已初始化全局变量和局部静态变量;

  • 未初始化数据段(.bss):存储未初始化的或初始化为0的全局变量和局部静态变量。

其他可执行文件格式

除了ELF文件格式外,还有一些其它的可执行文件格式:

  • a.out:Unix系统早期使用的可执行文件格式,尽管历史悠久,现在已经基本不使用这种格式,但是Linux编译时仍然会使用a.out作为程序的默认名称,算是一种致敬吧;

  • .COM:DOS时代使用的文件格式,学生时代如果玩过8086汇编,应该都会遇到;

  • PE(Portable Executable):Windows 平台使用的可执行文件格式,典型文件后缀名 .exe

ELF 文件结构

一个典型的ELF文件结构如下图所示:

image.png

  • ELF文件头:出现在ELF文件的开头,描述了整个文件的基本属性,包括ELF文件类型、运行平台以及其它头部表的属性信息等等;

  • 程序头部表:对于可加载文件是必须的,对于可重定位文件是可选的;

  • 节区头部表:对于可重定位文件是必须的,对于可加载文件是可选的;

  • 节区或段:存储可装载的数据、重定位以及字符串和符号等信息。

ELF 文件头

ELF文件头(ELF Header)主要标记了ELF文件类型、结构和程序开始执行的入口地址,并提供了其他ELF头(如节头表和程序头表)在文件中的偏移位置。使用 readelf 工具来查看一个ELF文件的文件头:

image.png

ELF 文件头的数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type; /* ELF文件类型 */
uint16_t e_machine; /* 硬件平台 */
uint32_t e_version; /* ELF头部版本 */
ElfN_Addr e_entry; /* 程序执行的入口地址 */
ElfN_Off e_phoff; /* 程序执行的入口地址 */
ElfN_Off e_shoff; /* 节头表偏移量 */
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; /* 节头表字符索引 */
} ElfN_Ehdr;

程序头部表

程序头部表(Program Header Table)是对二进制文件中段的描述,本质上是记录段表项的数组,是程序装载必需的一部分,因此对于可加载文件,该表是必须项。段在程序被系统加载运行时被解析,其描述了磁盘上可执行文件的内存布局以及如何映射到内存中。ELF文件头中名为 e_phoff(程序头部表偏移量)的偏移量记录了程序头部表的位置。使用 readelf 工具查看ELF文件中的程序头部表信息:

image.png

由上面的输出信息可以看出,程序头部表记录了ELF文件中所有段的属性信息,包括段的类型、文件偏移以及映射到内存时的地址信息。程序头部表本质上是段信息的数组,对于每个段(数组项),ELF使用如下数据结构进行描述:

1
2
3
4
5
6
7
8
9
10
 typedef struct {
uint32_t p_type; /* 段的类型 */
uint32_t p_flags; /* 段的标记 */
Elf64_Off p_offset; /* 段的位置相对于文件开始处的偏移 */
Elf64_Addr p_vaddr; /* 段在内存中的首字节地址,即虚拟地址 */
Elf64_Addr p_paddr; /* 段的物理地址 */
uint64_t p_filesz; /* 段在文件映像中的字节数 */
uint64_t p_memsz; /* 段在内存映像中的字节数 */
uint64_t p_align; /* 短在内存中对齐标记 */
} Elf64_Phdr;

ELF文件格式定义了5中不同类型的段,其描述如下:

  1. PT_LOAD :描述可装载的段,该类型的段会被装载或被映射到内存中。典型的包括代码段和数据段;
  2. PT_DYNAMIC :动态链接文件特有,包含了动态链接所必需的一些信息;
  3. PT_NOTE:保存了与特定供应商或者系统相关的附加信息;
  4. PT_INTERP:记录动态链接器的位置信息,本质是一个以null为终止符的字符串;
  5. PT_PHDR :保存程序头部表本身的位置和大小。

节区头部表

节区头部表(Section Header Table)用于描述ELF文件的各个节区的位置和大小,主要用于链接和调试。须注意的是,节区头部表对于程序的执行不是必需的,没有节头表,程序仍可以正常执行。使用 readelf 工具查看ELF文件的所有节:

image.png

image.png

与程序头部表类似,节区头部表是由节构成的数组。对于节的数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
uint32_t sh_name; /* 小节名在字符表中的索引 */
uint32_t sh_type; /* 小节的类型 */
uint64_t sh_flags; /* 小节属性 */
Elf64_Addr sh_addr; /* 小节在运行时的虚拟地址 */
Elf64_Off sh_offset; /* 小节的文件偏移 */
uint64_t sh_size; /* 小节的大小.以字节为单位 */
uint32_t sh_link; /* 链接的另外一小节的索引 */
uint32_t sh_info; /* 附加的小节信息 */
uint64_t sh_addralign; /* 小节的对齐 */
uint64_t sh_entsize;
} Elf64_Shdr;

节的类型与作用

ELF文件中定义的一些比较重要的节和节类型罗列如下:

节(section)类型 描述
.text 已编译程序的机器代码,即代码段
.rodata 保存了只读的数据
.data 已初始化的全局和静态C变量,即数据段
.bss 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量
.rel.text 被模块引用或定义的外部函数的重定位信息,对应于.text
.rel.data 被模块引用或定义的所有全局变量的重定位信息,对应于.data
.debug 调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表
.line 原始C源程序中行号和.text节中机器指令之间的映射,用于调试
.symtab 符号表,存放在程序中定义和引用的函数和全局变量的信息
.strtab 字符串表,包含 .symtab.debug 节中的符号表,以及节头部的节名字。字符串表是以null结尾的字符串的序列
.shstrtab 节名字符串表,在可执行文件是必须存在的
.ctors .dtors C++构造函数和析构函数存放的段
.dynsym 用于保存与动态链接相关的符号
.dynstr 动态符号字符串表,与静态链接使用 .strtab 对应
.hash 在动态链接的情况下,用于加快符号查找过程
.interp 保存可执行文件使用的动态链接器的路径
.plt 程序链接表,和 .got .plt 配合实现函数调用的延迟绑定
.got 用于动态链接,保存外部全局变量引用的地址
.got.plt 用于动态链接,用于保存外部函数引用的地址

ELF 文件的符号管理

符号是程序对某些类型的数据或者代码的引用,典型的符号包括全局变量、函数名等,这些符号会在程序链接、运行以及调试时发挥作用。ELF文件中管理的符号包括:

  • 定义在本目标文件中并且可以被其它目标文件引用的全局符号,对应于C程序中非静态的函数和全局变量;
  • 定义在其它目标文件中并且在本目标文件中引用的全局符号,这些符号称为外部符号,对应于C程序中在其它目标文件中定义的非静态函数和全局变量;
  • 只在当前目标文件定义和引用的局部符号,对应C程序中的静态函数和静态全局变量;
  • 节名,由编译器产生,用于记录该节的起始地址,如 .text.data 等。

符号表

ELF文件通常会使用独立的符号表记录所有引用到的符号,对应的节名为 .symtab。ELF 符号表保存了ELF文件中定义的全局变量和函数的符号信息,也包含了引用自外部文件的全局符号。在Linux下,可以使用readelf工具查看ELF文件符号表:

image.png

符号表本质上是一个由符号描述信息构成的数组,对于每个符号描述信息的数据结构定义如下:

1
2
3
4
5
6
7
8
typedef struct {
uint32_t st_name; /* 符号名,包含符号在字符串表中的下标信息 */
unsigned char st_info; /* 符号类型和绑定信息 */
unsigned char st_other; /* 未使用 */
uint16_t st_shndx; /* 符号所在节 */
Elf64_Addr st_value; /* 符号相对应的值,一般为符号地址 */
uint64_t st_size; /* 符号大小 */
} Elf64_Sym;

符号类型

ELF文件格式中定义的几种常用的符号类型如下:

  • STT_NOTYPE:符号类型未定义;
  • STT_FUNC:表示该符号符号是函数或者关联了其它可执行代码;
  • STT_OBJECT:表示该符号是数据对象;
  • STT_SECTION:表示该符号是一个节名,如 .text 节、.data 节等;
  • STT_FILE:表示该符号是文件名,如对于目标文件,通常是目标文件名。

符号绑定类型

根据符号的可见性和优先级,ELF定义了多种符号绑定类型,常见的符号绑定如下:

  • STB_LOCAL:本地符号,对于目标文件之外是不可见的,如 static 函数;
  • STB_GLOBAL:全局符号,对于所有需要链接的目标文件来说是可见的。一个全局符号在一个文件中进行定义后,另外一个文件可以对这个符号进行引用;
  • STB_WEAK:弱引用符号,类似于全局符号,但优先级比 STB_GLOBAL 低。被标记为 STB_WEAK 的符号有可能被同名的未被标记为 STB_WEAK 的符号覆盖。

符号所在节

符号表项中成员 st_shndx 记录了符号所在节的信息,若符号定义在当前目标文件中,这个成员记录符号所在的节在节区头部表中的索引;但若符号定义在其它目标文件中,或者一些特殊符号,这个成员记录了一些特殊的符号常量:

  • SHN_ABS:表示该符号包含了一个绝对的值,如表示文件名的符号;
  • SHN_UNDEF:表示未定义的符号,即在当前目标文件中,但是定义在其它目标文件中的符号;
  • SHN_COMMON:表示还未被分配位置的未初始化的符号,如未初始化的全局变量。这里可以注意与.bss进行对比,.bss通常存储未初始化的静态变量,以及初始化为0的全局或静态变量。

动态符号表

在大多数动态链接可执行文件以及共享库中,还存在另外一张符号表,称作动态符号表,对应的节名为 .dynsym。与符号表保存了文件中所有的符号信息不同,动态符号表只保存了引用自外部文件的全局符号,因此动态符号表中存储的符号实际上是符号表中的符号的一个子集,并且二者的作用也不相同:

  • .symtab 符号表只是用来进行链接和调试的,因此只在可重定位文件进行链接时,需要symtab符号表中的内容,对于程序的运行则不是必要的数据,也不会被装载到内存中;
  • .dynsym 符号表保存的符号只能在运行时被解析,因此程序运行时会将 .dynsym 符号表加载到内存中。