ELF 文件解析 3-段

ELF 文件中段及其相关概念

在这前的介绍中,在 ELF 文件中包含很多(Segment),所有这些段都登记在一张称为 程序头表(Program Header Table)的数组里。段头表的每一个表项是一个Elf64_Phdr结构,通过每一个表项可以定位到对应的段,所以也可称之为段头表。程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。

下方给出一张图(图 1),清晰明确地阐述关于 段 的有关概念的文件结构图。

图1 ELF 文件中有关节的概念的结构图

对于图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_typeProgram 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_NULLProgram Header Type-Null):此类型表明本程序头是未使用的,本程序头内的其它成员值均无意义。具 有此种类型的程序头应该被忽略。

PT_LOADProgram Header Type-Loadable):此类型表明本程序头指向一个可装载的段。段的内容会被从文件中拷贝到内存中。段在文件中的大小是 p_filesz,在内存中的大小是 p_memsz。如果 p_memsz 大于 p_filesz,在内存中多出的存储空间应填 0 补充,也就是说,段在内存中可以比在文件中占用空间更大;而相反,p_filesz 永远不应该比 p_memsz 大,因为这样的话,内存中就将无法完整地映射段的内容。在程序头表中,所有 PT_LOAD 类型的程序头按照 p_vaddr 的值做升序排列。

PT_DYNAMICProgram Header Type-Dynamic):此类型表明本段指明了动态连接的信息。

PT_INTERPProgram Header Type-Interpreter):本段指向了一个以 ”null” 结尾的字符串,这个字符串是一个 ELF 解析器的路径。这种段类型只对可执行程序有意义,当它出现在共享目标文件中时, 是一个无意义的多余项。在一个 ELF 文件中它多只能出现一次,而且必须出现在其它可装载段的表项之前。

PT_NOTEProgram Header Type-Note):本段指向了一个以 ”null” 结尾的字符串,这个字符串包含一些附加的信息。

PT_SHLIB:该段类型是保留的,而且未定义语法。UNIX System V 系统上的应用程序不会包含这种表项。

p_flagsProgram 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_offsetProgram Header-File Offset):此字段(8 字节)给出本段内容在文件中的位置,即段内容的开始位置相对于文件开头的偏移量。

p_vaddrProgram Header-Virtual Address):此字段(8 字节)给出本段内容的开始位置在进程空间中的虚拟地址。

p_paddrProgram Header-Physical Address):此字段(8 字节)给出本段内容的开始位置在进程空间中的物理地址。对于目前大多数现代操作系统而言,应用程序中段的物理地址事先是不可知的,所以目前这个 成员多数情况下保留不用,或者被操作系统改作它用。

p_fileszProgram Header-File Size):此字段(8 字节)给出本段内容在文件中的大小,单位是字节,可以是 0。

p_memszProgram Header-Memory Size):此字段(8 字节)给出本段内容在内容镜像中的大小,单位是字节,可以是 0。

p_alignProgram 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的每个数据成员。

图2 程序头表某一程序头的分析

根据上述的介绍,不难从其中得到如下表信息。

数据成员名称起始地址偏移16进制值字节数意义
p_type0xb00x14该段的类型:PT_LOAD,是可加载段
p_flags0xb40x54该段的属性:可读可执行
p_offset0xb80x08段起始位置的偏移量:0x0
p_vaddr0xc00x4000008段在虚拟内存中的地址是:0x400000
p_paddr0xc80x4000008段在实际内存中的地址是:0x400000
p_filesz0xd00x6d0(1744)8该段在文件中的大小:0x6d0
p_memsz0xd80x6d08该段在内存中的大小:0x6d0
p_align0xe00x2000008向内存页面大小对齐

程序头的分析注释

  • 文件头中存储的有关节的三个数据成员的体现。可从文件头易知:e_phoff=0x40(64)e_phnum=0x9(9)e_phentsize=0x38(56)。从上图(图2)中,可知该节的大小是 64 字节的,对应的参数是e_phentsizee_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),与上述自行分析的结果完全相同。下图中红色框中的部分就是上述分析的程序头。

图3 readelf 工具分析的结果

对于文件有关段的内容的介绍全部结束如有错误敬请指正

前一节:ELF 文件解析 2-节-补充

编辑于 2023-03-17 00:12・IP 属地未知