ELF文件格式
简介
ELF (Executable and Linkable Format)是Linux的可执行文件格式(Windows下的可执行文件格式为PE(Portable Executable)格式), 是 COFF(Common File Format) 格式的变种;
ELF格式文件包括:可执行文件,目标文件(.o),共享链接库(.so),静态连接库(.a)和内核转储(core dumps)文件;
组成
一个ELF文件由以下三部分组成:
ELF头(ELF header):描述文件的主要特性:类型,CPU架构,入口地址,现有部分的大小和偏移等等;
程序头表(Program header table):
列举了所有有效的段(segments)和他们的属性。
程序头表需要加载器将文件中的节加载到虚拟内存段中;
Segment和Section:
段(Segment)由若干区(Section)组成;
段在运行时被加载到进程地址空间中,包含在可执行文件中;
区是段的组成单元,包含在可执行文件和可重定位文件中;
节头表(Section header table): 包含对节(sections)的描述;
文件头(Header)
|
|
魔数(Magic)
每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头4个字节,通常被称为魔数(Magic Number)。
通过对魔数的判断可以确定文件的格式和类型。如:
ELF的可执行文件格式的头4个字节为
0x7F
、e
、l
、f
;Java的可执行文件格式的头4个字节为
c
、a
、f
、e
;如果被执行的是Shell脚本或perl、python等解释型语言的脚本,那么它的第一行往往是
#!/bin/sh
或#!/usr/bin/perl
或#!/usr/bin/python
,此时前两个字节#
和!
就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。
ELF文件类型
ELF文件主要有三种类型,可以通过ELF Header中的e_type
成员进行区分。
- 可重定位文件(Relocatable File):
ETL_REL
。一般为.o
文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。 - 可执行文件(Executable File):
ET_EXEC
。可以直接执行的程序。 - 共享目标文件(Shared Object File):
ET_DYN
。一般为.so
文件。有两种情况可以使用。- 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
- 动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像。
程序头表(Program Header Table)
在可执行文件或者共享链接库中所有的节(sections)都被分为多个段(segments)。
程序头是一个结构的数组,每一个结构都表示一个段(segments)。
|
|
段头表(Section Header Table)
ELF 节头表是一个节头数组;
每一个节头都描述了其所对应的节的信息,如节名、节大小、在文件中的偏移、读写权限等;
编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的;
|
|
ELF Sections
节的分类
上述ELF Section Header Table部分已经简单介绍了节类型。接下来我们来介绍详细一些比较重要的节。
.text节
.text
节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr,则.text
节就会存在于text
段中。由于.text
节保存了程序代码,所以节类型为SHT_PROGBITS
。
.rodata节
rodata
节保存了只读的数据,如一行C语言代码中的字符串。由于.rodata
节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text
段(不是data
段)中找到.rodata
节。由于.rodata
节是只读的,所以节类型为SHT_PROGBITS
。
.plt节(过程链接表)
.plt
节也称为过程链接表(Procedure Linkage Table),其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt
节保存了代码,所以节类型为SHT_PROGBITS
。
.data节
.data
节存在于data
段中,其保存了初始化的全局变量等数据。由于.data
节保存了程序的变量数据,所以节类型为SHT_PROGBITS
。
.bss节
.bss
节存在于data
段中,占用空间不超过4字节,仅表示这个节本省的空间。.bss
节保存了未进行初始化的全局数据。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss
节未保存实际的数据,所以节类型为SHT_NOBITS
。
.got.plt节(全局偏移表-过程链接表)
.got
节保存了全局偏移表。.got
节和.plt
节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got.plt
节与程序执行有关,所以节类型为SHT_PROGBITS
。
.dynsym节(动态链接符号表)
.dynsym
节保存在text
段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM
。
.dynstr节(动态链接字符串表)
.dynstr
保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。
.rel.*节(重定位表)
重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对ELF目标文件的某部分或者进程镜像进行补充或修改。由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL
。
.hash节
.hash
节也称为.gnu.hash
,其保存了一个用于查找符号的散列表。
.symtab节(符号表)
.symtab
节是一个ElfN_Sym
的数组,保存了符号信息。节类型为SHT_SYMTAB
。
.strtab节(字符串表)
.strtab
节保存的是符号字符串表,表中的内容会被.symtab
的ElfN_Sym
结构中的st_name
引用。节类型为SHT_STRTAB
。
.ctors节和.dtors节
.ctors
(构造器)节和.dtors
(析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在main函数执行之前需要执行的代码;析构函数是在main函数之后需要执行的代码。
符号表
节的分类中我们介绍了.dynsym
节和.symtab
节,两者都是符号表。那么它们到底有什么区别呢?存在什么关系呢?
符号是对某些类型的数据或代码(如全局变量或函数)的符号引用,函数名或变量名就是符号名。例如,printf()
函数会在动态链接符号表.dynsym
中存有一个指向该函数的符号项(以Elf_Sym
数据结构表示)。在大多数共享库和动态链接可执行文件中,存在两个符号表。即.dynsym
和.symtab
。
.dynsym
保存了引用来自外部文件符号的全局符号。如printf
库函数。.dynsym
保存的符号是.symtab
所保存符合的子集,.symtab
中还保存了可执行文件的本地符号。如全局变量,代码中定义的本地函数等。
既然.dynsym
是.symtab
的子集,那为何要同时存在两个符号表呢?
通过readelf -S
命令可以查看可执行文件的输出,一部分节标志位(sh_flags
)被标记为了A(ALLOC)、WA(WRITE/ALLOC)、AX(ALLOC/EXEC)。其中,.dynsym
被标记为ALLOC,而.symtab
则没有标记。
ALLOC表示有该标记的节会在运行时分配并装载进入内存,而.symtab
不是在运行时必需的,因此不会被装载到内存中。.dynsym
保存的符号只能在运行时被解析,因此是运行时动态链接器所需的唯一符号。.dynsym
对于动态链接可执行文件的执行是必需的,而.symtab
只是用来进行调试和链接的。
上图所示为通过符号表索引字符串表的示意图。符号表中的每一项都是一个Elf_Sym
结构,对应可以在字符串表中索引得到一个字符串。该数据结构中成员的含义如下表所示:
成员 | 含义 |
---|---|
st_name | 符号名。该值为该符号名在字符串表中的偏移地址。 |
st_value | 符号对应的值。存放符号的值(可能是地址或位置偏移量)。 |
st_size | 符号的大小。 |
st_other | 0 |
st_shndx | 符号所在的节 |
st_info | 符号类型及绑定属性 |
使用readelf工具我们也能够看到符号表的相关信息。
|
|
字符串表
类似于符号表,在大多数共享库和动态链接可执行文件中,也存在两个字符串表。即.dynstr
和.strtab
,分别对应于.dynsym
和symtab
。此外,还有一个.shstrtab
的节头字符串表,用于保存节头表中用到的字符串,可通过sh_name
进行索引。
ELF文件中所有字符表的结构基本一致,如上图所示。
重定位表
重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使可执行文件和共享目标文件能够保存进程的程序镜像所需要的正确信息。
重定位表是进行重定位的重要依据。我们可以使用objdump工具查看目标文件的重定位表:
|
|
重定位表是一个Elf_Rel
类型的数组结构,每一项对应一个需要进行重定位的项。 其成员含义如下表所示:
成员 | 含义 |
---|---|
r_offset | 重定位入口的偏移。 |
对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于节起始的偏移 | |
对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址 | |
r_info | 重定位入口的类型和符号 |
因为不同处理器的指令系统不一样,所以重定位所要修正的指令地址格式也不一样。每种处理器都有自己的一套重定位入口的类型。 | |
对于可执行文件和共享目标文件来说,它们的重定位入口是动态链接类型的。 |
重定位是目标文件链接成为可执行文件的关键。我们将在后面的进行介绍。