ELF文件格式

  • 可重定位文件,包含由编译器生成的代码和数据。链接器会将它与其他目标文件链接起来从而创建可执行文件或者目标共享文件,在linux中,这种文件后缀一般为.o

  • 共享目标文件,包含代码和数据,这种文件是我们所称的库文件,一般是以.so结尾。

    一般有两种使用情形:

    • 链接器将其生成为另一个目标文件。
    • 动态链接器将它与可执行文件以及其他共享目标文件组合在一起生成进程镜像。

链接视图

文件开始处是ELF头部,他给出了整个文件的组织情况

如果程序头部表存在的话,他会告诉系统如何创建进程。用于生成进程的目标文件必须有程序头部表,但重定位文件不需要这个表。

节区部分包含在链接视图中要使用的大部分信息:指令,数据,符号表,重定位信息。

节区头部表包含描述文件节区的信息,每个节区在表中都有一个表项,会给出节区名称,节区大小等信息。用于链接的目标文件必须有节区头部表,其他目标文件则无所谓。

数据形式

ELF文件格式支持8位/32位体系结构,当然这种格式是可以扩散的,也可以支持更小的或者更大位数的处理器架构,因此,目标文件会包含一些控制数据,这部分数据表明了目标文件所使用的架构,这也使得它可以被通用的方式来识别和解释。目标文件中的其他数据采用目的处理器的格式进行编码,与在何种机器上创建没有关系。这里其实想表明的意思目标文件可以交叉编译,我们可以在x86平台生成arm平台的可执行代码。

目标文件中的所有数据结构都遵从“自然”大小和对齐规则。

名称 长度 对齐方式 用途
Elf32_Addr 4 4 无符号程序地址
Elf32_Half/Elf64_Half 2 2 无符号半整型
Elf32_Off 4 4 无符号文件偏移
Elf32_Sword/Elf64_Sword 4 4 有符号大整型
Elf32_Word/Elf64_Word 4 4 无符号大整型
unsigned char 1 1 无符号小整型
Elf64_Addr 8 8 无符号程序地址
Elf64_Off 8 8 无符号文件偏移

对于64位的基本一样,除了Elf64_Addr 和 Elf64_Off 是8位以外,还有Elf64_Half是2位,其他的皆为4位。

ELF Header

ELF Header 描述了ELF文件的概要信息,利用这个数据结构可以索引到ELF文件的全部信息,数据结构如下:

该数据位于gcc编译源文件的elf文件夹里的elf.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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;

其中每个成员都是e开头的,他们应该都是ELF的缩写。每个成员的具体说明如下。

e_indent

正如之前所说,ELF提供了一个目标文件框架,以便于支持多种处理器,多种编码格式的机器。该变量给出了用于解码核解释文件中与机器无关的数据的方式。这个数组对于不同的下标的含义如下:

宏名称 下标 目的
EI_MAG0 0 文件标识
EI_MAG1 1 文件标识
EI_MAG2 2 文件标识
EI_MAG3 3 文件标识
EI_CLASS 4 文件类
EI_DATA 5 数据编码
EI_VERSION 6 文件版本
EI_PAD 7 补齐字节开始处

其中,

e_ident[EI_MAG0]e_indet[EI_MAG3],即文件的头四个字节,被称为“魔数”,标识该文件的是一个ELF目标文件。开头为什么是0x7f,对应ASCII字符里面的DEL控制符。几乎所有的可执行文件格式最开始的几个字节都是魔数。比如a.out 格式最开始两个字节为 0x01 , 0x07 ;PE/COFF文件最开始两个字节为0x4d,0x5a,即ASCII字符MZ。

名称 位置
ELFMAG0 0x7f e_ident[EI_MAG0]
ELFMAG1 ‘E’ e_ident[EI_MAG1]
ELFMAG2 ‘L’ e_ident[EI_MAG2]
ELFMAG3 ‘F’ e_ident[EI_MAG3]

e_ident[EI_CLASS]e_ident[EI_MAG3]的下一个字节,标识文件的类型或容量。

名称 意义
ELFCLASSNONE 0 无效类型
ELFCLASS32 1 32 位文件
ELFCLASS64 2 64 位文件

ELF文件的设计使得它可以在多种字节长度的机器之间移植,而不需要强制规定机器的最长字节长度和最短字节长度。ELFCLASS32类型支持文件大小和虚拟地址空间上限为4GB的机器;它使用上述定义中的基本类型。

ELFCLASS64类型用于64位架构。

e_indent[EI_DATA]字节给出了目标文件中的特定处理器数据的编码方式。下面是目前已定义的编码:

名称 意义
ELFDATANONE 0 无效数据编码
ELFDATA2LSB 1 小端
ELFDATA2MSB 2 大端

其他的值被保留,在未来必要时将被赋予新的编码。

文件数据编码方式表明了文件内容的解析方式。正如之前所述,ELFCLASS32类型文件使用了具有 1,2 和 4 字节的变量类型。对于已定义的不同的编码方式,其表示如下所示,其中字节号在左上角。

ELFDATA2LSB编码使用补码,最低有效位(Least Significant Byte)占用最低地址。

ELFDATA2MSB编码使用补码,最高有效位(Most Significant Byte)占用最低地址。

e_ident[EI_DATA] 给出了 ELF 头的版本号。目前这个值必须是EV_CURRENT,即之前已经给出的e_version

e_ident[EI_PAD] 给出了 e_ident 中未使用字节的开始地址。这些字节被保留并置为 0;处理目标文件的程序应该忽略它们。如果之后这些字节被使用,EI_PAD 的值就会改变。

e_type

e_type标识目标文件类型。

名称 意义
ET_NONE 0 无文件类型
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 共享目标文件
ET_CORE 4 核心转储文件
ET_LOPROC 0xff00 处理器指定下限
ET_HIPROC 0xffff 处理器指定上限

虽然核心转储文件的内容没有被详细说明,但 ET_CORE 还是被保留用于标志此类文件。从 ET_LOPROCET_HIPROC (包括边界) 被保留用于处理器指定的场景。其它值在未来必要时可被赋予新的目标文件类型。

e_machine

这一项指定了当前文件可以运行的机器架构。

名称 意义
EM_NONE 0 无机器类型
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_68K 4 motorola 68000
EM_88K 5 motorola 88000
EM_860 6 Intel 80860
EM_MIPS 7 MIPS RS3000

其中EM应该是ELF machine的简写。

其他值被在未来必要时用于新的机器。此外,特定处理器的ELF名称使用机器名称来区分,一般标志会有个前缀EF_(ELF Flag)。例如在EM_XYZ机器上名叫WIDGET的标志将被称为EF_XYZ_WIDGET

e_version

标识目标文件的版本。

标识目标文件的版本。

名称 意义
EV_NONE 0 无效版本
EV_CURRENT 1 当前版本

1 表示初始文件格式;未来扩展新的版本的时候 (extensions) 将使用更大的数字。虽然在上面值EV_CURRENT为 1,但是为了反映当前版本号,它可能会改变,比如 ELF 到现在也就是 1.2 版本。

e_entry

这一项为系统转交控制权给ELF中相应代码的虚拟地址。如果没有相关的入口项,则这一项为0,。

e_phoff

这一项给出程序头部表在文件中的字节偏移。如果文件没有程序头部表,则为0。

e_shoff

这一项给出节头表(也叫段表)在文件中的字节偏移。如果文件没有节头表,则为0。

e_flags

这一项给出文件中与特定处理器相关的标志,这些标志命名格式为EF_machine_flag

e_ehsize

这一项给出ELF文件头部的字节长度。

e_phentsize

这一项给出程序头部表中的每个表项的字节长度。每个表项的大小相同。

e_phnum

这一项给出程序头部表的项数。因此,e_phnume_phentsize的乘积即为程序头部表的字节长度。如果文件中没有程序头部表,则该项值为0。

e_shentsize

这一项给出节头的字节长度。一个节头是节头表中的一项;节头表中所有项占据的空间大小相同。

e_shnum

这一项给出节头表中的项数。因此, e_shnume_shentsize 的乘积即为节头表的字节长度。如果文件中没有节头表,则该项值为 0。

e_shstrndx

这一项给出节头表中与节名字符串表相关的表项的索引值。如果文件中没有节名字符串表,则该项值为SHN_UNDEF

e_shstrndx是”**Section header string table index **”的缩写。我们知道 段表字符串本身也是ELF文件中的一个普通的段,知道他的名字往往叫做 “.shstrtab”。那么这个”e_shstrndx”就表示”.shstrtab”在段表中的下标,即 段表字符串表在段表中的下标。我们可以得出结论,只有分析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个文件。

实践

这里给出一个elf文件头的案例来分析一波

1
2
3
4
7F 45 4C 46   01 01 01 00    00 00 00 00   00 00 00 00
02 00 03 00 01 00 00 00 00 91 04 08 34 00 00 00
98 38 00 00 00 00 00 00 34 00 20 00 0C 00 28 00
1F 00 1E 00 06 00 00 00 34 00 00 00 34 80 04 08
  • 7F 45 4C 46表示这是ELF文件
  • 01 01 01 00 00 00 00 00 00 00 00 00表示这是32位文件,编码为小端序,文件版本为1
  • 02 00 表示这为可执行文件
  • 03 00 表示这是x86架构的文件
  • 01 00 00 00 目标文件版本为1
  • 00 91 04 08 程序的入口为0x8409100
  • 34 00 00 00 程序头部表在偏移为0x34的位置
  • 98 38 00 00 节头表在偏移为0x3898处
  • 00 00 00 00 特定处理器标志位0
  • 34 00 ELF文件头部的字节长度为0x34
  • 20 00 0C 00 表示为程序头部表长度为0x20,项数为0x0c
  • 28 00 1F 00 表示为节头表长度为0x28 ,项数为0x1f
  • 1E 00 节头表中与节名字符串表相关的表项的索引值为0x1E

Program Header Table

概述

program Header Table 是一个结构体数组,每一个元素的类型时Elf32_Phdr ,描述了一个段或者其他系统在准备程序执行时所需要的信息。其中,ELF头中的e_phentsizee_phnum指定了该数组每个元素的大小以及元素的个数。一个目标文件的段包含一个或者多个节。程序的头部只有对于可执行文件和共享目标文件有意义

可以说,Program Header Table 就是专门为ELF文件运行时中的段所准备的。

Elf32_Phdr的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
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;

每个字段的说明如下:

字段 说明
p_type 该字段为段的类型,或者表明了该结构的相关信息。
p_offset 该字段给出了从文件开始到该段开头的第一个字节的偏移。
p_vaddr 该字段给出了该段第一个字节在内存中的虚拟地址。
p_paddr 该字段仅用于物理地址寻找相关的系统中,由于“System V”忽略了应用程序的物理寻找,可执行文件和共享目标文件的该项内容并未被限定。
p_filesz 该字段给出了文件镜像中该段的大小,可能为0。
p_memsz 该字段给出了内存镜像中该段的大小,可能为0。
p_flags 该字段给出了与段相关的标记。
p_align 可加载的程序的段的p_vaddr 以及p_offset 的大小必须是page的整数倍。该成员给出了段在文件以及内存中的对齐方式。如果该值为0或者1的话,表示不需要对齐。除此之外,p_align应该是2的整数指数次方,并且p_vaddr与p_offset在模p_align的意义下,应该相等。

段类型

可执行文件中的段类型如下:

名字 取值 说明
PT_NULL 0 表明段未使用,其结构中其他成员都是未定义的。
PT_LOAD 1 此类型段为一个可加载的段,大小由p_filesz和p_memsz描述。文件中的字节被映射到相应内存段开始处。如果p_memsz大于p_filesz,“剩余”的字节都要被置为0。p_filesz不能大于p_memsz。可加载的段在程序头部按照p_vaddr的升序排列。
PT_DYNAMIC 2 此类型给出动态链接信息。
PT_INTERP 3 此类型段给出了一个以NULL结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅可对可执行文件有意义(也可能出现在共享目标文件中)。此外,这种段在一个文件中最多出现一次,而且这种类型的段存在的话,它必须在所有可加载段项的前面。
PT_NOTE 4 此类型给出附加信息的位置和大小。
PT_SHLIB 5 该段类型被保留,不过语义未指定,而且,包含这种类型的段的程序不符合ABI标准。
PT_PHDR 6 该段类型打的数组元素如果存在的话,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中最多出现一次。此外,只有程序头部表是程序的内存映像的一部分时,它才会出现。如果此类型段存在,则必须在所有可加载段项目的前面。
PL_LOPROC~PT_HIPROC 0x70000000~0x7fffffff 此范围的类型保留给处理器专用语义。

基地址-Base Address

程序头部的虚拟地址可能并不是程序内存镜像中的实际的虚拟地址。通常来说,可执行程序都会包含绝对地址的代码。为了使得程序可以正常执行,段必须在相应的虚拟地址处。另一方面,共享目标文件通常来说包含与地址无关的代码。这可以使得共享目标文件可以被多个进程加载,同时保持程序执行的正确性。尽管系统会为不同的进程选择不同的虚拟地址,但是它仍然保留段的相对地址,因为地址无关代码使用段之间的相对地址来进行寻址,内存中的虚拟地址之间的差必须与文件中的虚拟地址之间的差相匹配。内存中任何段的虚拟地址与文件中对应的虚拟地址之间的差值对于任何一个可执行文件或共享对象来说是一个单一常量值。这个差值就是基地址,基地址的一个用途就是在动态链接期间重新定位程序。

可执行文件或者共享目标文件的基地址是在执行过程中由以下三个数值计算的

  • 虚拟内存加载地址
  • 最大页面大小
  • 程序可加载段的最低虚拟地址

要计算基地址,首先要确定可加载段中p_vaddr最小的内存虚拟地址,之后把内存虚拟地址缩小为与之最近的最大页面的整数倍即是基地址。根据要加载到内存中的文件的类型,内存地址可能与p_vaddr相同也可能不同。

段权限-p_flags

被系统加载到内存中的程序至少有一个可加载的段。当系统为可加载的段创建的内存镜像时,它会按照p_flags将段设置为对应的权限。可能的段权限位有

Name Value Meaning
PF_X 0x1 Execute
PF_W 0x2 Write
PF_R 0x4 Read
PF_MASKPROC 0xf0000000 Unspecified

其中,所有在PF_MASKPROC中的比特位都是被保留用于与处理器相关的语义信息。

如果一个权限位被设置为0,这种类型的段是不可访问的。实际的内存权限取决于相应的内存管理单元,不同的系统可能操作方式不一样。尽管所有的权限组合都是可以的。但系统一般会授予比请求更多的权限。在任何情况下,除非明确说明,一个段不会有写权限。下面给出了所有可能的组合

Flag Value
none 0
PF_X 1
PF_W 2
PF_W+PF_X 3
PF_R 4
PF_R+PF_X 5
PF_X+PF_W 6
PF_X+PF_W+PF_R 7

例如,一般来说,.text段一般具有读和执行权限,但不会有写权限。数据段一般具有写,读,以及执行权限。

段内容

一个段可能包括一到多个节区,但是这并不会影响到程序的加载。尽管如此,我们也必须需要各种各样的数据来使得程序可以执行以及动态链接等等。下面会给出一般情况下的段的内容。对于不同的段来说,它的节的顺序以及所包含的节的个数有所不同。此外,与处理相关的约束可能会改变对应的段的结构。

如下所示,代码段只包含只读的指令以及数据。当然这个例子并没有给出所有的可能的段。数据段包含可写的数据以及指令,通常来说,包含以下内容

Text Segment Data Segment
.text .data
.rodata .dynamic
.hash .got
.dynsym .bss
.dynstr
.plt
.rel.got

程序头部的PT_DYNAMIC类型的元素指向.dynamic节。其中,got表和plt表包含与地址无关的代码相关信息。尽管在这里给出的例子中,plt节出现在代码段,但是对于不同的处理器来说,可能会有所变动。

.bss节的类型为SHT_NOBITS,这表明它在ELF文件中不占用空间,但是它却占用可执行文件的内存镜像的空间。通常情况下,没有被初始化的数据在段的尾部,因此,p_memsz才会比p_filesz大。

注意:

  • 不同的段可能会有所重合,即不同的段包含相同的节。

Section Header Table

其实这个数据结构是在ELF文件的尾部。

该结构用于定位ELF文件中的每个节区的具体位置。

首先,ELF头中的e_shoff项给出了从文件开头到节头表位置的字节偏移。e_shnum告诉了我们节头表包含的项数;e_shentsize给出了每一项的字节大小。

其次,节头表是一个数组,每个数组的元素的类型时ELF32_Shdr,每一个元素都描述了一个节区的概要内容。

ELF32_Shdr

每个节区头部可以用下面的数据结构进行描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

每个字段的含义如下

成员 说明
sh_name 节名称,是节区头字符串表节区的索引,因此该字段实际是一个数字。在字符串表中的具体内容是以NULL结尾的字符串。段名是个字符串,它位于一个叫做“.shstrtab”的字符串表。sh_name是段名字符串在”.shstrtab”中的偏移。
sh_type 根据节的内容和语义进行分类,具体的类型下面会介绍。
sh_flags 每一比特代表不同的标志,描述节是否可写,可执行,需要分配内存等属性。
sh_addr 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为0。
sh_offset 给出节区的第一个字节与文件开始处之间的偏移。SHT_NOBITS类型的节区不占用文件的空间,因此其sh_offset成员给出的是概念性的偏移。
sh_size 此成员给出节区的字节大小。除非节区的类型时SHT_NOBITS,否则该节占用文件中的sh_size字节。类型为SHT_NOBITS的节区长度可能非零,不过却不占用文件中的空间。
sh_link 此成员给出节区头部表索引链接,其具体的解释依赖于节区类型。
sh_info 此成员给出附加信息,其解释依赖于节区类型。
sh_addralign 某些节区的地址需要对齐。例如,如果一个节区有一个doubleword类型的变量,那么系统必须保证整个节区按双字对齐。也就是说sh_addr % sh_addralign = 0。目前它仅允许为0,以及2的正整数幂数。0和1表示没有对齐约束。
sh_entsize 某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。如果为0,则表示该段不包含固定大小的项。

正如之前所说,索引为零(SHN_UNDEF)的节区头也存在,此索引标记的是未定义的节区引用。这一项的信息如下

字段名称 取值 说明
sh_name 0 无名称
sh_type SHT_NULL 限制
sh_flags 0 无标志
sh_addr 0 无地址
sh_offset 0 无文件偏移
sh_size 0 无大小
sh_link SHN_UNDEF 无链接信息
sh_info 0 无辅助信息
sh_addralign 0 无对齐要求
sh_entsize 0 无表项

特殊下标

节头表中比较特殊的几个下标如下:

名称 含义
SHN_UNDEF 0 标志未定义的,丢失的,不相关的或者其他没有意义的节引用。例如,与节号SHN_UNDEF 相关的“定义”的符号就是一个未定义符号。
SHN_LORESERVE 0xff00 保留索引值范围的下界。
SHN_LOPROC 0xff00 处理器相关的下界。
SHN_HIPROC 0xff1f 处理器相关的上界。
SHN_ABS 0xfff1 相关引用的绝对值。例如与节号SHN_ABS相关的符号拥有绝对值,他们不受重定位的影响。
SHN_COMMON 0xfff2 这一节区相定义的符号是通用符号,例如FORTRAN COMMON,C语言中未分配的外部变量。
SHN_HIRESERVE 0xffff 保留索引值范围的上界。

系统保留在SHN_LORESERVE 到SHN_HIRSERVE之间(包含边界)的索引值,这些值不在节头表中引用。也就是说,节头表不包含保留索引项。没特别理解。

sh_type

段的名字只是在链接和编译过程中有意义,但它不能真正地表示段的类型。我们也可以将一个数据段命名为”.text”,对于编译器和链接器来说,主要决定段的属性的是段的类型和段的标志位。

段类型目前有下列可选范围,其中 SHT 是 Section Header Table 的简写。

名称 取值 说明
SHT_NULL 0 该类型节区是非活动的,这种类型的节头中的其它成员取值无意义。
SHT_PROGBITS 1 该类型节区包含程序定义的信息,它的格式和含义都由程序来决定。程序段,代码段,数据段都是这种类型
SHT_SYMTAB 2 该类型节区包含一个符号表(SYMbol TABle)。目前目标文件对每种类型的节区都只 能包含一个,不过这个限制将来可能发生变化。 一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。
SHT_STRTAB 3 该类型节区包含字符串表( STRing TABle )。
SHT_RELA 4 重定位表。该类型节区包含显式指定位数的重定位项( RELocation entry with Addends ),例如,32 位目标文件中的 Elf32_Rela 类型。此外,目标文件可能拥有多个重定位节区。
SHT_HASH 5 该类型节区包含符号哈希表( HASH table )。
SHT_DYNAMIC 6 该类型节区包含动态链接的信息( DYNAMIC linking )。
SHT_NOTE 7 该类型节区包含以某种方式标记文件的信息(NOTE)。
SHT_NOBITS 8 该类型节区不占用文件的空间,其它方面和 SHT_PROGBITS 相似。尽管该类型节区不包含任何字节,其对应的节头成员 sh_offset 中还是会包含概念性的文件偏移。表示该段在文件中没内容,比如.bss段。
SHT_REL 9 重定位信息。该类型节区包含重定位表项(RELocation entry without Addends),不过并没有指定位数。例如,32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。
SHT_SHLIB 10 该类型此节区被保留,不过其语义尚未被定义。
SHT_DYNSYM 11 动态链接的符号表。作为一个完整的符号表,它可能包含很多对动态链接而言不必 要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
SHT_LOPROC 0X70000000 此值指定保留给处理器专用语义的下界( LOw PROCessor-specific semantics )。
SHT_HIPROC OX7FFFFFFF 此值指定保留给处理器专用语义的上界( HIgh PROCessor-specific semantics )。
SHT_LOUSER 0X80000000 此值指定保留给应用程序的索引下界。
SHT_HIUSER 0X8FFFFFFF 此值指定保留给应用程序的索引上界。

sh_flags

节头中sh_flags字段的每个比特位都可以给出其相应的标志信息,其定义了对应节区的内容是否可以被修改,被执行等信息。如果一个标志位被设置,则该位取值为1,未定义的位都为0。目前已定义值如下,其他值保留。

名称 说明
SHF_WRITE 0X1 这种节包含了进程运行过程中可以被写的数据
SHF_ALLOC 0X2 表示该段在进程中需要分配空间。这种节在进程运行时占用内存。对于不占用目标文件的内存镜像空间的某些控制节,该属性处于关闭状态(off),一般代码段和数据段还有bss段都包含这个标志位。
SHF_EXECINSTR 0X4 这种节包含可执行的机器指令(EXECutable INSTRuction),一般指代码段。
SHF_MASKPROC 0XF0000000 所有掩码中的比特位用于特定处理器语义

如果段的类型是与链接相关的(不论是动态链接或是静态链接),比如重定位表,符号表等,那么sh_link和sh_info这两个成员所包含的意义如下:

sh_type sh_link sh_info
SHT_DYNAMIC 该段所使用的字符串表在段表中的下标 0
SHT_HASH 该段所使用的符号表在段表中的下标 0
SHT_REL/SHT_RELA 该段所使用的相应符号表在段表中的下标 重定位应用到的节的节头索引
SHT_SYMTAB/SHT_DYNSYM 操作系统特定信息,Linux中的ELF文件中该项指向符号表所对应的字符串节区在Section Header Table中的偏移 操作系统特定信息
other SHN_UNDEF 0

重定位表

在段表类型里面有一个类型为 SHT_REL,在文件中有一个叫做.rel.text的段,其类型就是 SHT_REL。也就是说他是一个重定位表。正如我们开始所说的,链接器在处理目标文件时,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。比如.rel.text就是针对”.text”段的重定位表,因为”.text”段中至少有一个绝对地址的引用,那就是对”printf“函数的调用。

一个重定位表同时也是ELF中的一个段,那么这个段的类型(sh_type)就是”SHT_REL”类型的,它的“sh_link”表示符号表的下标,它的“sh_info”表示它作用与哪个段。比如”.rel.text“作用于”.text“段。

实践

这里给出elf的段表来实践分析一下

1
2
3
4
5
1B 00 00 00  01 00 00 00   02 00 00 00  B4 81 04 08
B4 01 00 00 13 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00 23 00 00 00 07 00 00 00
02 00 00 00 C8 81 04 08 C8 01 00 00 24 00 00 00
00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00
  • 1B 00 00 00 表示段的节名称(在节头区字符串表节区的索引),节区字符串表就在段表前面不远处。
  • 01 00 00 00 该类型节区包含程序定义的信息,它的格式核含义都由程序来决定。
  • 02 00 00 00 这种节在进程运行时占用内存。对于不占用目标文件的内存镜像空间的某些控制节,该属性处于关闭
  • B4 81 04 08 节区第一个字节应该在进程镜像中的位置为0x080481b4
  • B4 01 00 00 节区的第一个字节与文件开始处的偏移为0x1b4
  • 13 00 00 00 节区的字节大小为0x13
  • 00 00 00 00 00 00 00 00 节头区头部表索引链接和附加信息为0
  • 01 00 00 00 00 00 00 00 节区对齐没有约束。

Sections

节区包含了目标文件中除了ELF头部,程序头部表,节区头部表的所有信息。节区满足以下条件

  • 每个节区都有对应的节头来描述它。但是反过来,节区头不一定会对应着一个节区,因为节区可能为0。
  • 每个节区在目标文件中是连续的,但是大小可能为0。
  • 任意两个节区不能重叠,即一个字节不能同时存在于两个节区中。
  • 目标文件中可能会有闲置空间,各种头和节不一定会覆盖到目标文件中的所有字节,这是因为文件对齐的原因,闲置区域的内容未指定,但在文件中是0x00

许多在ELF文件中的节都是预定义的,它们包含程序和控制信息。这些节被操作系统使用,但是对于不同的操作系统,同一节区可能会有不同的类型以及属性。

可执行文件是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析引用(不同文件中的子例程的引用以及数据的引用,调整对象文件中的绝对引用)并且重定位指令。加载与链接过程需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节区中,比如 .dynamic

每一种操作系统都会支持一组链接模型,但这些模型都大致可以分为两种

  • 静态链接 静态链接的文件中使用的库文件或者第三方库都被静态绑定了,其引用已经被解析了。
  • 动态链接 动态链接的文件中所使用的库文件或者第三方库只是单纯地被链接到可执行文件中。当可执行文件被执行时使用到相应函数时,相应的函数地址才会被解析。

有一些特殊的节可以支持调试,比如说 .debug 以及.line 节;支持程序控制的节有.bss,.data,.data1, .rodata, .rodata1。

名称 类型 属性 含义
.comment SHT_PROGBITS 包含版本控制信息。
.debug SHT_PROGBITS 此节区包含用于符号调试的信息。
.dynamic SHT_DYNAMIC SHF_ALLOC SHF_WRITE 此节区包含动态链接信息。SHF_WRITE 位设置与否是否被设置取决于具体的处理器。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数 情况下这些字符串代表了与符号表项相关的名称。
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含动态链接符号表。
.got SHT_PROGBITS 此节区包含全局偏移表。
.line SHT_PROGBITS 此节区包含符号调试的行号信息,描述了源程序与机器指令之间的对应关系,其内容是未定义的。
.plt SHT_PROGBITS 此节区包含过程链接表(procedure linkage table)。
.relname SHT_REL 这些节区中包含重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.relaname SHT_RELA
.shstrtab SHT_STRTAB 此节区包含节区名称。

注意:

  • 以”.”开头的节区名称是系统保留的,当然应用程序也可以使用这些节区。但是为了避免与系统节区冲突,应用程序应该尽量使用没有前缀”.”的节区名称。
  • 目标文件格式允许有不在上述列表中的节区,可以包含多个名字相同的节区。
  • 保留给处理器体系结构的节区名称一般命名规则为: 处理器体系结构名称简写+节区名称。其中,处理器名称应该与e_machine中使用的名称相同。例如.FOO.pset 节区是FOO体系结构中的psect 节区。

Code Section

概述

在动态链接器创建了进程镜像,并且执行了重定位后,每一个共享目标文件都有机会去执行一些初始化的代码。所有的共享目标文件会在可执行文件获得权限之前进行初始化。

在调用目标文件A的初始化代码之前,会首先调用所有A依赖的共享目标文件的初始化代码。比如说,如果目标文件A依赖于另一个目标文件B,那么B就会在A的依赖列表中,这会被记录在动态结构的DT_NEEDED中。循环依赖的初始化是未被定义的。

目标文件的初始化通过递归每一个被依赖项的表项来完成。只有当一个目标文件依赖的所有目标文件都处理完自己的依赖后,这个目标文件才会执行初始化代码。

类似的,共享目标文件也会有结束的函数,这些函数在进程完成自己的终止序列时通过atexit机制来执行。动态链接器调用终止函数的顺序恰好与上面的顺序相反。动态链接器将会确保它只会执行初始化或者终止函数最多一次。

共享目标文件通过动态结构中的DT_INIT和DT_FINI来指定它们的初始化以及结束函数。在一般情况下,这些函数在.init 节与 .fini节中。

注意:

尽管atexit终止处理函数通常来说会被执行,但它并不会保证在程序消亡时被执行。更特殊的是,如果程序调用了_exit 函数或者进程由于接收到了一个信号后消亡了,那么它将不会执行对应的函数。

动态链接器并不负责调用可执行文件的.init节或者利用atexit注册可执行文件的.fini节。由用户通过atexit 机制指定的终止函数必须在所有共享目标文件的结束函数前执行。

.init & .init_array

此节区包含可执行指令,是进程初始化代码的一部分。程序开始执行时,系统会在开始调用主程序入口(通常指C语言的main函数)前执行这些代码。

.text

此节区包含程序的可执行指令。

.fini & .fini_array

此节区包含可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将执行这里的代码。

.BSS Section

未初始化的全局变量对应的节。此节区不占用ELF文件空间,但占用程序的内存映像中的空间。当程序开始执行时,系统将把这些数据初始化为0。bss其实是block started by symbol 的简写。

.data Section

这些节区包含初始化了的数据,会在程序的内存映像中出现。

.rodata Section

这些节区包含只读数据,这些数据通常参与进程映像的不可写段。

String Sections

ELF文件中用到了很多字符串,比如段名,变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串表集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。

通过这种方法,在ELF文件中引用字符串只需要给出一个数字下标即可,不用考虑字符串长度的问题。一般字符串表在ELF文件中也以段的形式保存,常见的段名为”.strtab”或”.shstrtab”。这两个字符串表分别为 字符串表(string Table)段表字符串表(section Header String Table)。顾名思义,字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名(sh_name)。

可以看出

  • 字符串表索引可以引用节区中任意字节。
  • 字符串可以出现多次
  • 可以存在对子字符串的引用
  • 字符串表中也可以存在未引用的字符串。

.strtab信息在进行 strip后就会消失。而且strip后.symtab和.rel和.group段也会被除去。

Symbol Table

概述

每个目标文件都会有一个符号表,熟悉编译原理的就会知道,在编译程序时,必须有相应的结构来管理程序中的符号以便于对函数和变量进行重定位。

此外,链接过程的本质就是要把多个不同的目标文件之间相互“粘”在一起。或者说像玩具积木一样,可以拼装形成一个整体。为了使不同目标文件之间能够相互粘连,这些目标文件之间必须有固定的规则才行,就像积木模块必须有凹凸部分才能够拼合。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。

每个函数和变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为 符号(Symbol),函数名或者变量名就是 符号名(Symbol Name)

在程序编译成可执行文件后,这个文件中会有一个表专门来保存函数名,变量名,段名和代码或者数据的对应关系,这个表就是符号表。符号表在链接时起着按符号寻址的作用,但在运行的时候就没有什么作用了,因此这个表即使去掉之后,也并不会影响程序的运行。但是如果是动态链接的函数,比如用到了libc的printf函数,那么这个printf符号如果去掉了,在运行的时候就没法找到这函数了,所以这个符号就不能在去符号表的时候被去掉。

所以ELF文件里有两张符号表,一张叫符号表(.symtab),另一张叫动态符号表(.dynsym)。当去符号的时候,只用去掉符号表,而保留动态符号表

Elf32_Sym

ELF文件中的符号表往往是文件中的一个段,段名一般叫做”.symtab”。符号表的结构简单,它是一个Elf32_Sym(32位ELF文件)的数组,每个Elf32_Sym结构对应一个符号。

1
2
3
4
5
6
7
8
9
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;

每个字段的含义如下

字段 说明
st_name 符号在字符串表中对应的索引。如果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没有名称。 注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。
st_value 给出与符号相关联的数值,具体取值依赖于上下文,可能是一个正常的数值、一个地址等等。不同的符号,它所对应的值含义不同。
st_size 给出对应符号所占用的大小。如果符号没有大小或者大小未知,则此成员为 0。对于包含数据的符号,这个值是该数据类型的大小。
st_info 给出符号的类型和绑定属性。之后会给出若干取值和含义的绑定关系。
st_other 目前为 0,其含义没有被定义。
st_shndx 如果符号定义在该文件中,那么该成员为符号所在节在节区头部表中的下标;如果符号不在本目标文件中,或者对于某些特殊的符号,该成员具有一些特殊含义。

其中,符号表中下标 0 存储了符号表的一个元素,同时这个元素也相对比较特殊,作为所有未定义符号的索引。

st_info

st_info 中包含符号类型和绑定信息,这里给出了控制它的值的方式具体信息如下

1
2
#define ELF32_ST_TYPE(i)    ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))

可以看出st_info的低四位表示符号类型,具体定义如下

名称 取值 说明
STT_NOTYPE 0 符号的类型没有定义
STT_OBJECT 1 该符号是个数据对象,比如变量,数组等
STT_FUNC 2 该符号是个函数或其他可执行代码
STT_SECTION 3 该符号是一个段,这种符号必须是STB_LOCAL的
STT_FILE 4 该符号表示文件名,一般都是该目标文件所对应的源文件名,它一定是STB_LOCAL类型的,并且他的st_shndx一定是SHN_ABS
STT_LOPROC ~ STT_HIPROC 13~15 保留用于特定处理器

共享目标文件中的函数符号比较特殊,当另一个目标文件从共享目标文件中引用一个函数时,链接器自动为被引用符号创建过程链接表项,共享目标中除了STT_FUNC,其他符号将不会通过过程链接表自动被引用。

根据#define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4)可以看出st_info的高四位表示符号绑定的信息。而这部分信息确定了符号的链接可见性及其行为,具体定义如下

名称 取值 说明
STB_LOCAL 0 局部符号,对目标文件的外部不可见。相同名称的局部符号可以存在于多个文件中,互不影响。
STB_GLOBAL 1 全局符号,外部可见,对所有将被组合在一起的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。我们称初始化非零的全局符号为强符号,只能定义一次。
STB_WEAK 2 弱符号与全局符号类似,不过它们的定义优先级比较低。
STB_LOPROC~STB_HIPROC 3~15 这个范围的取值是保留给处理器专用语义的。

此外,全局符号与弱符号的主要区别如下:

  • 当链接器在链接多个可重定位目标文件时,不允许定义多个相同名字的 STB_GLOBAL 符号。另一方面,如果存在一个已定义全局符号,则同名的弱符号的存在不会引起错误。链接器会优先选择全局定义,忽略弱符号定义。类似的,如果一个公共符号存在 (st_shndx域为SHN_COMMON的符号),则同名的弱符号的存在不会引起错误。链接器会选择公共定义,忽略弱符号定义。

st_shndx

符号所在段,如果符号定义在本目标文件中,那么这个成员表示符号所在段在段表中的下标;但是如果符号不是定义在本目标文件中的,或者对于有些特殊符号,st_shndx 的值有些特殊,具体定义如下

名称 取值 说明
SHN_ABS 0xfff1 表示该符号的取值具有绝对性,不会因为重定位而发生变化,比如表示文件名的符号就属于这种类型。
SHN_COMMON 0xfff2 符号标记了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的sh_addralign成员类似。就是说,链接编辑器将在地址位于st_value的倍数处为符号分配空间。符号的大小给出了所需要的字节数。
SHN_UNDEF 0 此索引表示符号没有定义。这个符号表示该符号在本目标文件被引用到,但是定义在其他目标文件中。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置。

st_value

符号值,我们前面介绍过,每个符号都有一个相应的值,如果这个符号是一个函数或者变量的定义,那么符号的值就是这个函数或变量的地址,更准确地讲应该按下面几种情况区别对待。

  • 在可重定位文件中,st_value保存了节区索引WieSHN_COMMON的符号的对齐约束,比如global_uninit_var。
  • 在可重定位文件中,st_value保存了已定义符号的节区偏移。也就是说st_value保留了st_shndx所标识的节区的头部到符号位置的偏移。
  • 在可执行和共享目标文件中,st_value包含了一个虚地址。为了使得这些文件的符号对动态链接更有用,节区偏移(针对文件的解释)给出了与节区号无关的虚拟地址(针对内存的解释)

如何定位

那么对于一个符号来说如何定位其对应字符串的地址呢?具体步骤如下

  1. 根据 Section Header Table 中符号节头中的 sh_link 获取该符号节中对应符号字符串节在 Section Header Table 中的下标。进而我们就可以获取对应符号节的地址。
  2. 根据该符号的定义中的 st_name 获取该符号的偏移,即在对应符号节中的偏移。
  3. 根据上述两者就可以定位一个符号对应的字符串的地址了。