前因

之前大一的时候,什么都不会,老是觉得这难,那也难。从最开始的C语言开始学。不说关于底层的东西了,就是内存这个概念都没有理解到。什么虚拟内存,映射,操作系统,内核。mmd,头皮发麻。一点一点总结,一点一点学习吧,之前是看了很多书,很多文章,但是一直都没有记录下来。这一段时间其实是一直在补之前的的一些东西。刚看到wiki上有关于elf文件格式的一些更新,想想,我也记一下吧,以下记录以wiki上摘录的为主,同时以俞甲子前辈等人的《程序员的自我修养》的内容,以及其它资料为补充,实时更新。

–====——————————————-===—

正文

简介

ELF(Executable and Linktable Format)文件,也就是在Linux中的文件目标,主要有以下三种类型

  • 可重定位文件(Relocatable File),包含由编译器生成的代码以及数据。链接器会将它与其他目标文件连接起来,从而创建一个可执行文件或者共享目标文件。在Linux系统中,这种文件的后缀名一般为.o。这一般是中间文件,一般还不可以直接在操作系统上运行。

  • 可执行文件(Executable File),就是我们通常在Linux中执行的程序(通常是没有后缀名的)。

  • 共享目标文件(Shared Object File),包括代码和数据,这种文件是我们所称的库文件,一般以.so结尾,一般情况,它有以下两种使用情景:

    • 链接器(Link eDitor,ld)可能会处理它和其它可重定位文件以及共享文件目标,生成另外一个目标文件。
    • 动态连接器(Dynamic Linker)将它与可执行文件以及其它共享目标组合在一起生成镜像

关于 Link eDitor 的命名,https://en.wikipedia.org/wiki/GNU_linker

em….看一下,知道就行。。。

目标文件由汇编器和连接器串讲,是文本程序的二进制形式,可以直接在处理器上运行。那些需要虚拟机才能执行的程序(java)不属于这一范围。

这里,主要说一下ELF的文件格式。

在这之前们还是要说一下,内存这个概念

首先问自己,看自己有没有想过,电脑是如何辨析这些文件(可执行文件)的信息?

应该明白,绝不是靠单纯搜索特征字符和特征值.

既然,抛出内存这个词,就应该想到另一个关系密切的词偏移,这也是结构体的终极奥义。

其实,不严谨的说,程序也是一个结构题。

我们看文件格式

目标文件即会参与程序连接又会参与程序执行。处于方便性和效率考虑,根据过程的不同,目标文件格式提供了其内容的两种并行视图,如下(此处直接截取wiki上的图,链接视图和可执行文件视图)

img

首先,我们看,链接视图

文件开始处事ELF头部(ELF header),这里是整个文件的组织情况。

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

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

节区头部表(Section Header Table)包含了描述文件节区的信息,每个节区在表中斗殴一个表项,会给出节区名称,节区大小等信息。用于描述链接的目标文件必须由节区头部表,其它目标文件则无所谓,可以有,也可以没有

这里给出一个关于链接视图比较形象的展示(依然是wiki上的)

img

对于执行试图来说,其主要的不同点在于没有了section,而有了多个segment。其实这里的大部分的segment大都是来源于链接视图的section。

attention:

尽管图中是按照ELF头,程序头部表,节区头部表的顺序排列的。但实际除了ELF头部表以外,其它部分都没有严格的顺序

数据形式

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

我们可以在x86平台生成arm平台的可执行代码。

目标文件的所有数据都遵从“自然”大小和对其规则。如下

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

如果必要,数据结构(结构体)可以包含显式的不起来确保四字节对象按4字节对齐,强制数据结构大小是4字节的整数倍等等。数据同样适用对齐的。因此,包含一个Elf32_Addr类型成员的数据结构会在文件的4字节边界处对齐。

为了具有可移植性,ELF文件不适用位域

字符表示

等待补充。。。。。。

注:以下介绍,我们以32位为主(还在用,也不是太难)。

ELF Header

ELF Header描述ELF文件的概要信息,利用这个数据结构可以索引到ELF文件的全部信息,其往往和文件执行的环境相关(即硬件环境).机器字长,文件版本等等。

数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT   16

typedef struct {
unsigned char e_ident[EI_NIDENT];
ELF32_Half e_type;
ELF32_Half e_machine;
ELF32_Word e_version;
ELF32_Addr e_entry;
ELF32_Off e_phoff;
ELF32_Off e_shoff;
ELF32_Word e_flags;
ELF32_Half e_ehsize;
ELF32_Half e_phentsize;
ELF32_Half e_phnum;
ELF32_Half e_shentsize;
ELF32_Half e_shnum;
ELF32_Half e_shstrndx;
} Elf32_Ehdr;

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

e_ident

正如之前所说,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_ident[EI_MAG3],即文件的头4个字节,被称为“魔数(magic number)”。表示该文件的格式,是一个ELF目标文件。开头的0x7f才是真正的标识,后三个字符“E”,“L”,“F”并没有太大的意义。操作系统会检查文件的魔数,如果不正确,会拒绝加载。

魔数的具体由来则是则是一些列历史因素,没有什么特别的意义,所谓“路径依赖”吧

拓展:

a.out的魔数是0x01,0x07

PE/CPFF的魔术师0x4d,0x5a

名称 位置
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_ident[EI_DATA]字节给出了目标文件中的特定处理器数据的编码方式。下面是目前已定义的编码。

说明文件的编码方式,是小端序还是大端序?

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

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

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

ELFDATA2LSB编码使用补码,最低有效(Least Signficant Byte)占最低地址(即小端序)。

img

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

img

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

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

e_type,

即ELF文件的类型

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 7 Intel 80860
EM_MIPS 8 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)将使用更大的数字。虽然上面的值EVCURRENT为1,但是为了反映当前版本号,它可能会改变,比如ELF到现在也就1.2的版本

e_entry

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

e_phoff

这一项给出程序头部表在文件中的字节偏移(Program Header table OFFset).如果这项文件没有节头表,则为0.

e_flags

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

e_ehsize

这一项给出ELF文件头部的字节长度(ELF Header Size).

e_phentsize

这一项给出程序头部表中每个表项的字节长度(Program Header ENTry SIZE).每个表项的大小相同。

e_phnum

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

e_shentsize

这一项给出节头的字节长度(Section Header ENTry SIZE).一个节头是节头表中的一项;节头表中所占据的空间大小相同。

e_shnum

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

e_shstrndx

这一项给出头节表中与节名字符表相关的表项的索引值(Section Header table InDex related with section name STRing table).

如果文件中没有节名字符串表则该项为SHN_UNDEF关于细节的介绍,参考后面的”节”和“字符串表”部分。

Program Header Table
概述

program Header Table是一个结构体数组,每一个元素都是dElf32_Phdr,描述了一个或者其它系统在准备程序执行时所需要的信息。其中,ELF头中的e_phentsizee_phnum指定了该数组每个元素的大小以及元素的个数。一个目标文件的段包含一个或者多个节。程序的头部只有用于可执行文件和共享目标文件有意义,即操作系统视角,stack,heap,bss,libc.so

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

一个可执行文件及其依赖的共享目标文件被完全成功地装载到进程的内存地址空间中之后,这个可执行文件或共享目标文件中的 程序头部表 (Program Header Table)就是必须存在的、不可缺少的必需品,程序头部表是一个数组,数组中的每一个元素就称为一个 程序头 (Program Header),每一个程序头描述一个 内存段 (Segment)或者一块用于准备执行程序的信息;内存中的一个目标文件中的段包含一个或多个 节 ;也就是ELF文件在磁盘中的一个或多个节可能会被映射到内存中的同一个段中;程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略;

Elf32_Phdr的数据结构如下

1
2
3
4
5
6
7
8
9
10
typedef struct {
ELF32_Word p_type;
ELF32_Off p_offset;
ELF32_Addr p_vaddr;
ELF32_Addr p_paddr;
ELF32_Word p_filesz;
ELF32_Word p_memsz;
ELF32_Word p_flags;
ELF32_Word p_align;
} 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 的意义下,应该相等。

p_type

段类型

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

PT(Program Table)

名字 取值 说明
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 该段类型的数组元素如果存在的话,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中最多出现一次。此外,只有程序头部表是程序的内存映像的一部分时,它才会出现。如果此类型段存在,则必须在所有可加载段项目的前面。
PT_LOPROC~PT_HIPROC 0x70000000 ~0x7fffffff 此范围的类型保留给处理器专用语义。

基地址

Base Address

程序头部的虚拟地址可能并不是程序内存镜像中的实际虚拟地址。通常来说,可hi自行程序都会包含绝对地址的代码。为了使得程序可以才正常进行,段必须在相应的虚拟地址处。另一方面,共享目标文件通常来说包含与地址无关的代码。这可以使得共享目标文件可以被多个进程加载,同时保持程序执行的正确性。尽管系统会为不同的进程选择不同的虚拟地址,但是都他人然保留段的相对地址,因为地址无关代码使用段之间的相对地址进行寻址,内存中的虚拟地址之间的差必须与文件中的虚拟地址之间的差相匹配。

内存中任何段的虚拟地址与文件中对应的虚拟地址之间的差值对于任何可执行文件或共享对象来说时一个单一常量值。这个值就是基地址,基地址的一个用途就是动态链接求见重新定位程序。

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

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

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

段权限

p_flags

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

img

即“读”,“写”,“执行”。

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

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

img

例如:

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

段内容

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

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

img

数据段包含可写的数据以及指令,通常来说,包含以下内容

img

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

。bss节的类型为SHT_NOBITS,这表明她在ELF文件中不占空间,但是对于不同的处理器来说,可能会有所变动。

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

注意
  • 不同段可能会有所重合,即不同的段可能会包含相同的节。
Section Header Table

与之对应,程序自身观察视角

这里需要补充一下,段和节的区别

很多地方对“段”和“节”的区分不严密

有的情景中,段可能是包含多个节的,也有可能是只有一个节。

而有些情境中,“段”同时也叫“节”。

目前,我个人比较倾向把stack,heap,bss,.text,,data等称为段

把.init_array .fini_array .got等称为节。

这是一个普通程序,有多个段(几乎都有),也有各个节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
blacktea@ubuntu:~/Q$ readelf -l test

Elf 文件类型为 EXEC (可执行文件)
入口点 0x400430
共有 9 个程序头,开始于偏移量 64

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000774 0x0000000000000774 R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000228 0x0000000000000230 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x0000000000000648 0x0000000000400648 0x0000000000400648
0x0000000000000034 0x0000000000000034 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1

Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got

这是一个用汇编写的hello world程序

可以看到,没有栈段,只有两个段,也只有两个节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
blacktea@ubuntu:~/nasm$ readelf -l test

Elf 文件类型为 EXEC (可执行文件)
入口点 0x8048080
共有 2 个程序头,开始于偏移量 52

程序头:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x000a2 0x000a2 R E 0x1000
LOAD 0x0000a4 0x080490a4 0x080490a4 0x0000d 0x0000d RW 0x1000

Section to Segment mapping:
段节...
00 .text
01 .data

这里讲讲section

Sections

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

  • 每个节区都有对应的节头来描述。但是反过来,截取头部并不一定会对应一个节区。
  • 每个街区在目标文件中是连续的,但是大小可能是0
  • 任意两个节区不能重叠,即一个字节不能同时存在于两个区。
  • 目标文件中可能会有闲置空间(inactive space),各种头和节不一定会覆盖到目标文件中的所有字节,闲置区域的内容未指定

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

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

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

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

这里留下坑,打算简短地讲一下动态链接和静态链接


有一些特殊的节可以支持调试,比如说.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.psect节区是FOO体系结构中的psect节区。

Code Section(代码区)

概述

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

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

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

下面的例子解释了两种正确的可以生成给定例子的顺序。这个例子中,a.out依赖于b,d以及e。b依赖于d和f。并且的依赖于e和g。根据这些信息。我们可以画出如下的依赖图。那么我们上面所说的算法,将允许我们按照如下的顺序进行初始化。

img

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

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

注意:

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

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

.init&.int_arry

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

.text

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

.fini&.fini_array

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

在CTF的PWN题里面,就有一种利用方式。将.fini_array里的内容改写为main函数,从而使程序循环。

Data Related Sections

.BSS Section

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

bss的名字由来有好几种传言,但应该是block started by symbol的简写(至少这种会感觉可靠点)

.data Section

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

.rodata Section

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

.symtab:Symbol Table

概述

每个目标文件都会有一个符号表,熟悉编译原理的就会知道,在编译程序时,必须有相应的结构来管理程序中的符号以便于对函数和变量进行重定位。(里面储存的往往是需要链接的函数的字符串名字,例,”puts”)

此外,连接的本质就是把多个不同的文件互相“粘”在一起。实际上,目标文件相互粘合时目标文件之间对地址的引用,基函数和变量的地址的相互引用。而在粘合的过程中,符号就是其中的粘合剂。

目标文件的符号表包含了一些通用的符号,这部分信息在进行了strip操作后就会消失。这些符号信息可能包括变量名,函数名。

符号表可以视为一个数组,数组中的每一个元素都是一个结构体,具体如下

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_name 0 无名称
st_value 0 0 值
st_size 0 无大小
st_info 0 无类型,局部绑定
st_other 0 无附加信息
st_shndx 0 无节区

st_value

在linux的ELF文件中,具体变量说明如下

  1. 该符号对应这一个变量,呢么表明该变量在内存中的偏移。我们可由这个值获取文件偏移
    a. 获取该符号对应的st_shndx,进而获取到相关的节区。

    b.根据节区头元素可以获取节区的虚拟基地址 和文件基地址。

    c.value-内存虚拟地址 = 文件偏移 - 文件基地址。

2.该符号对应着一个函数,那么这表明该函数在文件中的其实地址

这一部分,暂时写到这里吧,在CTF的PWN中就有一种题目利用到symtable(即ret2dl_resolve)

其实这个数据结构是在ELF文件的尾部(为什么要放在文件的尾部呢??)。但为了讲解方便,这里将这个表放在这里讲解。

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

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

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

ELF32_Shdr

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

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
ELF32_Word sh_name;
ELF32_Word sh_type;
ELF32_Word sh_flags;
ELF32_Addr sh_addr;
ELF32_Off sh_offset;
ELF32_Word sh_size;
ELF32_Word sh_link;
ELF32_Word sh_info;
ELF32_Word sh_addralign;
ELF32_Word sh_entsize;
} Elf32_Shdr;

每个字段的含义如下

成员 说明
sh_name 节名称,是节区头字符串表节区中(Section Header String Table Section)的索引,因此该字段实际是一个数值。在字符串表中的具体内容是以 NULL 结尾的字符串。
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。

正如之前所说,索引为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 相关的 “定义” 的符号就是一个未定义符号。注:虽然 0 号索引被保留用于未定义值,节头表仍然包含索引 0 的项。也就是说,如果 ELF 头的 e_shnum 为 6,那么索引应该为 0~5。更加详细的内容在后面会说明。
SHN_LORESERVE 0xff00 保留索引值范围的下界。
SHN_LOPROC 0xff00 处理器相关的下界
SHN_HIPROC 0xff1f 处理器相关的上界
SHN_ABS 0xfff1 相关引用的绝对值。例如与节号 SHN_ABS 相关的符号拥有绝对值,它们不受重定位的影响
SHN_COMMON 0xfff2 这一节区相定义的符号是通用符号,例如 FORTRAN COMMON,C 语言中未分配的外部变量。
SHN_HIRESERVE 0xffff 保留索引值范围的上界。

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

部分节头字段
sh_type

节类型目前有下列可选范围,其中 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 中还是会包含概念性的文件偏移。
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)。
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

例子

这里给出一个 elf 文件比较经典的例子。

img

2022-03-08

⬆︎TOP