ELF 文件解析 3-段
ELF 文件中段及其相关概念
在这前的介绍中,在 ELF 文件中包含很多段(Segment),所有这些段都登记在一张称为 程序头表(Program Header Table)的数组里。段头表的每一个表项是一个Elf64_Phdr
结构,通过每一个表项可以定位到对应的段,所以也可称之为段头表。程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。
下方给出一张图(图 1),清晰明确地阐述关于 段 的有关概念的文件结构图。
对于图1的注释:(1)一般在分析段的结构的时候,考虑使用执行视图。原图来自官方文档,之前介绍前述的时候使用过这张图。我在这张图的基础之上添加了一些方便理解的注释(2)在介绍 ELF 文件头的时候提到过,文件头中e_phoff
成员给出程序头表在 ELF 文件中的起始位置,相对于文件开始处的偏移量;e_phnum
成员指明程序头表中包含多少个表项;e_phentsize
成员指明了每一个表项的大小。知晓这三个数据成员,就有了程序头表的基础信息。(3)而程序头表中是由多个子表(称为程序头(Segment Header))构成的,上图中之画出了一条虚线示意,而每个程序头的数据结构是Elf64_Phdr
结构(针对 64 位 ELF 文件),该数据结构中存放关于节的有关信息,通过这些数据,可以知晓一些节的位置,特性或结构等等的信息(这部分细致的介绍与分析在之后给出)。在上图中,我是用三种颜色的线来分别诠释出:通过程序头表中程序头的数据信息(图中 "Elf64_Phdr" 示意数据结构中存放的数据)检索到节的位置和一些关于节的描述信息。
注:由于本人的研究目标是 64 位 ELF 文件,故当前及之后的内容都只展示 64 位 ELF 文件相关的内容,对于 32 位 ELF 文件的详细的定义都请参考源码和官方文档,再次不多赘述。
至此,ELF 文件中段及其相关概念的介绍部分结束。
ELF 文件程序头表中程序头(Program Header)
在此直接给出程序头的数据结构的定义。这部分的介绍逻辑:按照Elf64_Phdr
数据结构中数据成员的先后顺序,为了方便理解数据成员的意义,对于每个数据成员的取值的介绍在数据成员之后直接给出,并非如官方文档中集中给出数据成员的取值。下方为源码中的定义及其对应的含义。
注:下方代码中//
符号后是我注释的具体对应的字节数目,这个数目对应的是 64 位 ELF 文件的数据成员的字段长度,该数据结构的数据成员的顺序与 32 位 ELF 文件有较大的差异。
/* Program segment header. */
typedef struct
{
Elf64_Word p_type; /* Segment type */ // 4 bytes
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */ // 8 bytes
Elf64_Addr p_vaddr; /* Segment virtual address */ // 8 bytes
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */ // 8 bytes
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
各成员按照数据结构中定义的先后顺序,依次介绍其意义。
p_type
(Program Header-Type):此字段(4 字节)说明本程序头所描述的段的类型,或者如何解析本程序头的信息。下方为源码中部分常用定义、对应取值及其含义。想要了解其余的定义请参见源码及其对应的注释。
/* Legal values for p_type (segment type). */
#define PT_NULL 0 /* Program header table entry unused */
#define PT_LOAD 1 /* Loadable program segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
... ...
PT_NULL(Program Header Type-Null):此类型表明本程序头是未使用的,本程序头内的其它成员值均无意义。具 有此种类型的程序头应该被忽略。
PT_LOAD(Program Header Type-Loadable):此类型表明本程序头指向一个可装载的段。段的内容会被从文件中拷贝到内存中。段在文件中的大小是 p_filesz,在内存中的大小是 p_memsz。如果 p_memsz 大于 p_filesz,在内存中多出的存储空间应填 0 补充,也就是说,段在内存中可以比在文件中占用空间更大;而相反,p_filesz 永远不应该比 p_memsz 大,因为这样的话,内存中就将无法完整地映射段的内容。在程序头表中,所有 PT_LOAD 类型的程序头按照 p_vaddr 的值做升序排列。
PT_DYNAMIC(Program Header Type-Dynamic):此类型表明本段指明了动态连接的信息。
PT_INTERP(Program Header Type-Interpreter):本段指向了一个以 ”null” 结尾的字符串,这个字符串是一个 ELF 解析器的路径。这种段类型只对可执行程序有意义,当它出现在共享目标文件中时, 是一个无意义的多余项。在一个 ELF 文件中它多只能出现一次,而且必须出现在其它可装载段的表项之前。
PT_NOTE(Program Header Type-Note):本段指向了一个以 ”null” 结尾的字符串,这个字符串包含一些附加的信息。
PT_SHLIB:该段类型是保留的,而且未定义语法。UNIX System V 系统上的应用程序不会包含这种表项。
p_flags
(Program Header-Flags):此字段(4 字节)给出本段内容的属性,指明了段的权限。虽然 ELF 文件格式中没有规定,但是一个可执行程序至少会有一个可加载的段。当为可加载段创建内存镜像时,系统会按照 p_flags 的指示给段赋予一定的权限。下方为源码中定义、对应取值及其含义。
/* Legal values for p_flags (segment flags). */
#define PF_X (1 << 0) /* Segment is executable */
#define PF_W (1 << 1) /* Segment is writable */
#define PF_R (1 << 2) /* Segment is readable */
#define PF_MASKOS 0x0ff00000 /* OS-specific */
#define PF_MASKPROC 0xf0000000 /* Processor-specific */
对应字段的值与属性的关系,值为 0x1:可执行;值为 0x2:可写;值为 0x4:可读。特殊地,被 PF_MASKOS 所覆盖的权限值是为特殊操作系统保留的,被 PF_MASKPROC 所覆盖的权限值是为特殊处理器保留的。
如果权限值为 0,表示无任何权限。实际的读写权限还要依赖于内存管理器, 在不同的操作系统上,内存管理单元的做法可能会不同。在有些组合方式下,系统给出的权限会比所指定的权限大,但可写权限 PF_W 除外,如果 p_flags 中没有指 定 PF_W 的话,系统一定不会给出写权限。
上述代码中介绍的段的权限只是单独的一个属性值(例如,可写,可读),而在实际中可能会出现属性叠加的情况(例如,可读且可写),这样就出现了属性的组合,属性的叠加对应的就是值的叠加(例如,可读且可写对应的属性值为:0x2+0x4=0x6)。理解总结:(1)可读与可执行是通用的,有其中一个就等于也有了另一个;(2)可写权限是高权限,可以覆盖另外两个,有了可写权限,所有权限 就都有了。
p_offset
(Program Header-File Offset):此字段(8 字节)给出本段内容在文件中的位置,即段内容的开始位置相对于文件开头的偏移量。
p_vaddr
(Program Header-Virtual Address):此字段(8 字节)给出本段内容的开始位置在进程空间中的虚拟地址。
p_paddr
(Program Header-Physical Address):此字段(8 字节)给出本段内容的开始位置在进程空间中的物理地址。对于目前大多数现代操作系统而言,应用程序中段的物理地址事先是不可知的,所以目前这个 成员多数情况下保留不用,或者被操作系统改作它用。
p_filesz
(Program Header-File Size):此字段(8 字节)给出本段内容在文件中的大小,单位是字节,可以是 0。
p_memsz
(Program Header-Memory Size):此字段(8 字节)给出本段内容在内容镜像中的大小,单位是字节,可以是 0。
p_align
(Program Header-Alignment ):此字段(8 字节)指明本段内容如何在内存和文件中对齐。如果该值为 0 或 1,表明没有对齐要求;否则,p_align 应该是一个正整数,并且是 2 的幂次数。p_vaddr 和 p_offset 在对 p_align 取模后应该相等。注:对于可装载的段来说,其 p_vaddr 和 p_offset 的值至少要向内存页面大小对齐。
至此,ELF 文件中程序头数据结构部分介绍结束。
针对 ELF 可执行文件中的程序头(表)的分析
之前 样例的构建 部分生成的可执行文件hello
,使用 010 Editor 导入 ELF 分析模板得到hello
的程序头表(包括程序头)部分的 16 进制的信息(也可使用命令:$ hexdump -C hello
,在终端实现类似的效果)。首先需要明确一个事情:由于篇幅有限,而且程序头表中的程序头的数据结构完全一样(数据存储或意义可能天差地别),这里不可能将程序头表中的所有程序头都进行分析,但是对于程序头表的分析方法是统一的。按照上述的各成员的先后顺序与字段大小进行分析,按意义块划分得到下图(图 2)。
注:下图中,蓝色选中的区域是程序头表中某一程序头,红色框是节头数据结构Elf64_Shdr
的每个数据成员。
根据上述的介绍,不难从其中得到如下表信息。
数据成员名称 | 起始地址偏移 | 16进制值 | 字节数 | 意义 |
---|---|---|---|---|
p_type | 0xb0 | 0x1 | 4 | 该段的类型:PT_LOAD,是可加载段 |
p_flags | 0xb4 | 0x5 | 4 | 该段的属性:可读可执行 |
p_offset | 0xb8 | 0x0 | 8 | 段起始位置的偏移量:0x0 |
p_vaddr | 0xc0 | 0x400000 | 8 | 段在虚拟内存中的地址是:0x400000 |
p_paddr | 0xc8 | 0x400000 | 8 | 段在实际内存中的地址是:0x400000 |
p_filesz | 0xd0 | 0x6d0(1744) | 8 | 该段在文件中的大小:0x6d0 |
p_memsz | 0xd8 | 0x6d0 | 8 | 该段在内存中的大小:0x6d0 |
p_align | 0xe0 | 0x200000 | 8 | 向内存页面大小对齐 |
程序头的分析注释:
- 文件头中存储的有关节的三个数据成员的体现。可从文件头易知:
e_phoff=0x40(64)
,e_phnum=0x9(9)
和e_phentsize=0x38(56)
。从上图(图2)中,可知该节的大小是 64 字节的,对应的参数是e_phentsize
;e_phoff
表明的是节头表开始的偏移量,当前节的开始地址的偏移量为0xb0(176)
,不难发现这几个量之间存在关系:0xb0(176)=0x40(64)+0x38(56)*2
,式子中数字2
的意义就是该节在节头表中的索引为 3,是程序头表中第三个程序头。
- 程序头属性相关。对于本程序头来说,段的属性是可读可执行,值是 0x5=0x1(可执行)+0x4(可读),属性的叠加对应的即为属性对应的值的叠加。
- 可加载段的对齐值的大小。Linux 会以页为单位管理内存,无论是将磁盘中的数据加载到内存中,还是将内存中的数据写回磁盘,操作系统都会以页面为单位进行操作。绝大多数处理器上的内存页的默认大小都是 4KB,虽然部分处理器会使用 8KB、16KB 或者 64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流。若页面的最大尺寸为 4KB,则段的虚拟地址和文件内偏移量要向 4KB 或者 4KB 的整数倍对齐。这样便于整页的换入换出,可以提高效率。
作为验证或直接查看:可以直接使用命令$ readelf -l hello
,得到hello
的程序头表的部分信息。信息的截图如下所示(图 3),与上述自行分析的结果完全相同。下图中红色框中的部分就是上述分析的程序头。
对于文件有关段的内容的介绍全部结束,如有错误,敬请指正。
前一节:ELF 文件解析 2-节-补充。