PE 文件格式

PE 简介

PE 文件的全称是 Portable Executable ,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE 文件,PE 文件是微软Windows操作系统上的程序文件,可能是间接被执行,如DLL)。 一个 32-bits 的 PE 文件布局如下图所示:

+-------------------------------+ \
|     MS-DOS MZ header          |  |
+-------------------------------+  |
| MS-DOS Real-Mode Stub program |  |
+-------------------------------+  |
|     PE Signature              |  | -> PE file header
+-------------------------------+  |
|     IMAGE_FILE_HEADER         |  |
+-------------------------------+  |
|     IMAGE_OPTIONAL_HEADER     |  |
+-------------------------------+ /
|     section header #1         | 
+-------------------------------+ 
|     section header #2 
+------------------------- 
: 
: 
 
+------------------------------+ 
|        section #1            | 
+------------------------------+ 
|        section #2 
+-------------------- 
: 
: 

接下来将会以一个 32-bit 的 PE 文件作为标本介绍一下 PE 文件。

通过 Devcpp 软件的 TDM-GCC 4.9.2 32-bit Release 方式编译文件生成 test.exe,作为示例文件。

常用术语及其含义

  • 映像文件 因为 PE 文件通常需要加载到内存中才能执行,相当于内存中的映像,所以 PE 文件也叫做映像文件。

  • RVA 相对虚拟地址,映像文件在虚拟内存中相对于加载基址的偏移。

  • VA 虚拟地址,映像文件在虚拟内存中的地址。

  • FOA 文件偏移地址,映像文件在磁盘文件中相对于文件开头的偏移。

因为不论是在磁盘文件上,或是在虚拟内存中,数据相对于其所在节的相对偏移是固定的,据此可以实现 RVA 与 FOA 之间的转换,即RVA - 节区RVA = FOA - 节区FOA

假设某一个属于 .data 节的数据的 RVA 是 0x3100,.data 节的 节区RVA 为 0x3000,那么该数据相对于 .data 节的相对偏移就是 0x100。而 .data 节在的 节区FOA 为 0x1C00,那么该数据在磁盘文件中的 FOA 就是 0x1D00。完整的计算公式是:FOA = 节区FOA + (RVA - 节区RVA)。如果该映像文件的加载基址为0x40000000,那么该数据的 VA 就是 0x40003100。

PE文件头

PE 文件的最开始便是 PE 文件头,它由 MS-DOS 文件头IMAGE_NT_HEADERS 结构体组成。

MS-DOS 文件头

MS-DOS 文件头 包含 IMAGE_DOS_HEADERDOS Stub 两个部分。

IMAGE_DOS_HEADER 结构体的定义如下:

IMAGE_DOS_HEADER 结构体中有 2 个重要成员:

  • e_magic 单字。DOS 签名 "4D5A",即 ASCII 值 "MZ"。所有 PE 文件的开头都有 DOS 签名。

  • e_lfanew 单字。IMAGE_NT_HEADER相对于文件起始处的偏移。

示例程序的 IMAGE_DOS_HEADER 如图 2 所示:

IMAGE_DOS_HEADER

IMAGE_DOS_HEADER 结构体后紧接着是 DOS Stub,它的作用很简单,当系统为 MS-DOS 环境时,输出 This program cannot be run in DOS mode. 并退出程序,表明该程序不能在 MS-DOS 环境下运行。这使得所有的 PE 文件都对 MS-DOS 环境兼容。利用该特性可以创建出一个在 MS-DOS 和 Windows 环境中都能运行的程序,在 MS-DOS 中执行 16-bit MS-DOS 代码,在 Windows 中执行 32-bit Windows 代码。

示例程序的 DOS Stub 如图 3 所示:

DOS Stub

IMAGE_NT_HEADERS

IMAGE_NT_HEADERS 结构体,俗称 NT 头。紧跟在 DOS Stub 之后,其定义如下:

示例程序的 IMAGE_NT_HEADERS 如图 4 所示:

NT 头

接下来详细说一下 NT 头。

PE Signature

NT 头的第一个成员是PE Signature,它是一个4字节大小的ASCII码字符串 PE\0\0,用于指明当前文件是一个 PE 格式的映像文件。其位置可以通过 IMAGE_DOS_HEADERe_lfanew 成员的值确定。

IMAGE_FILE_HEADER

PE Signature 后紧跟着是 IMAGE_FILE_HEADER 结构体,又称作 COFF 头(标准通用文件格式头)。其定义如下:

接下来依次对每一个字段做出解释:

  • Machine 单字。用于指明 CPU 类型。详细了解所支持的 CPU 类型请参考 微软 PE 格式 COFF 文件头 Machine 类型

  • NumberOfSections 单字。文件中存在的节区数量。PE 文件将代码、数据、资源的依据属性分类到不同节区中存储。

  • TimeDateStamp 双字。低 32 位表示从 1970 年 1 月 1 日 00:00 到文件创建时经过的秒数。

  • PointerToSymbolTable 双字。符号表的文件偏移。如果不存在符号表,其值为 0。

  • NumberOfSymbols 双字。该字段表示符号表中的符号数量。由于字符串表紧跟在符号表之后,所有能通过该值定位字符串表。

  • SizeOfOptionalHeader 单字。表示可选头的大小。在 32-bit 机器上默认是 0x00E0,在 64-bit 机器上默认是 0x00F0。

  • **Characteristics 单字。用于标识文件属性,以 bit OR 方式组合。**下面是一些已定义的文件属性标志:

示例程序的 IMAGE_FILE_HEADER 如下:

IMAGE_OPTIONAL_HEADER

之所以IMAGE_OPTIONAL_HEADER 叫做可选头,是因为对于目标文件,它没有任何作用,只是平白增加了目标文件的大小;但对于映像文件来说,它提供了加载时必需的信息。定义如下:

  • Magic 单字。指明映像文件的类型。0x0107h 表示 ROM 映像;0x010B 表示 PE32;0x020B 表示 PE32+,即 64-bit 的 PE 文件。

  • MajorLinkerVersion 字节。指定链接器主要版本号。

  • MinorLinkerVersion 字节。指定链接器次要版本号。

  • SizeOfCode 双字。所有包含代码的节的总大小。这里的大小指文件对齐后的大小。判断某个节是否包含代码的方法是根据节属性是否包含 IMAGE_SCN_CNT_CODE 标志。

  • SizeOfInitializedData 双字。所有包含已初始化数据节的总大小。

  • SizeOfUninitializedData 双字。所有包含未初始化数据节的总大小。

  • AddressOfEntryPoint 双字。入口点函数的指针相对于映像文件加载基址的偏移量。对于可执行文件,这是启动地址;对于设备驱动,这是初始化函数的地址;入口点函数对于 DLL 文件是可选的,如果不存在入口点,该成员必须置 0。

  • BaseOfCode 双字。代码节的 RVA,代码节起始处相对于映像文件加载基址的偏移量。通常代码节紧跟在 PE 头 后面,节名为 ".text"。

  • BaseOfData 双字。数据节的 RVA,数据节起始处相对于映像文件加载基址的偏移量。通常数据节位于文件末尾,节名为 ".data"。

  • **ImageBase 双字。映像文件加载时的优先载入地址,值必须是 64KB 的整数倍。**应用程序的默认值是 0x00400000;DLL 的默认值是 0x10000000。当一个程序用到了多个 DLL 文件时,PE 加载器会调整 DLL 的载入地址,使所有 DLL 文件都能够被正确载入。

  • SectionAlignment 双字。内存中的节对齐粒度。该成员的值必须不小于 FileAlignment 成员的值。默认的值与系统的页大小相等。

  • FileAlignment 双字。映像文件中原始数据的对齐粒度。值必须是在 512-64K 范围内的 2 的幂。默认值为512,但如果 SectionAlignment 成员的值小于系统页大小,则 FileAlignmentSectionAlignment 两者成员的值必须相同。

  • MajorOperatingSystemVersion 单字。操作系统主要版本号。

  • MinorOperatingSystemVersion 单字。操作系统次要版本号。

  • MajorImageVersion 单字。映像文件主要版本号。

  • MinorImageVersion 单字。映像文件次要版本号。

  • MajorSubsystemVersion 单字。子系统主要版本号。

  • MinorSubsystemVersion 单字。子系统次要版本号。

  • Win32VersionValue 双字。保留。置0。

  • SizeOfImage 双字。映像文件在虚拟内存中所占的大小。值必须为 SectionAlignment 的整数倍。

  • SizeOfHeaders 双字。PE 文件头和所有节表大小的总和按照 FileAlignment 对齐后的大小。第一节区在文件开始偏移为 SizeOfHeaders 处。

  • CheckSum 双字。映像文件的校验值。需要在装载时校验的文件有所有的驱动,任何在启动时装载的 DLL,以及任何加载到关键系统进程中的 DLL。

  • Subsystem 单字。运行映像文件所需的子系统。已定义的子系统标志如下:

  • DllCharacteristics 单字。映像文件的 DLL 属性,以 bit OR 方式组合。各标志位的含义如下:

  • SizeOfStackReserve 双字。初始化时保留的栈内存大小,默认值是 1MB。具体说是初始化时为栈保留的虚拟内存的大小,但并不是所有保留的虚拟内存都能直接作为栈使用。初始化时实际提交的栈大小由 SizeOfStackCommit 成员指定。

  • SizeOfStackCommit 双字。初始化时实际提交的栈内存大小。

  • SizeOfHeapReserve 双字。初始化时保留的堆内存大小,默认值为 1MB。每一个进程至少为会有一个默认的进程堆,在进程启动的时候被创建,并且在进程的声明周期内不会被删除。

  • SizeOfHeapCommit 双字。初始化时实际提交的堆内存大小,默认大小为 1 页。可以通过链接器的 "-heap" 参数指定起始保留的堆内存大小和实际提交的堆内存大小。

  • LoaderFlags 成员已弃用。

  • NumberOfRvaAndSizes 双字。数据目录结构的数量。通常为 0x00000010,即 16 个。

  • DataDirectory 结构体。由 IMAGE_DATA_DIRECTORY 结构体组成的数组,数组的每项都有被定义的值。结构体定义如下:

各数组项如下:

示例程序的 IMAGE_OPTIONAL_HEADER 如下图: 可选头上 可选头下

PE 数据主体

PE 数据主体包括 Section Header 和所有的节区。

Section Header

紧跟在可选头后面的是 Section Header,也称作节表。PE 文件种所有节的属性都被定义在节表中。节表由一系列的 IMAGE_SECTION_HEADER 结构体组成,结构体大小均为 40 字节。每一个结构体描述一个节的信息,定义如下:

  • Name 节名称字符串。长度最多 8 个字节。

  • Misc

    • PhysicalAddress 双字。文件地址。

    • VirtualSize 双字。虚拟内存中的节区所占内存大小。

  • VirtualAddress 双字。虚拟内存中节区 RVA。

  • SizeOfRawData 双字。对于映像文件,表示磁盘上初始化数据的大小,值必须为 FileAlignment 的整数倍;对于目标文件,表示节的大小。

  • PointerToRawData 双字。磁盘文件中节区起始处的 FOA。值必须是 FileAlignment 的整数倍。

  • PointerToRelocations 双字。在对象文件中使用,指向重定位表的指针。

  • PointerToLinenumbers 双字。行号信息位置(供调试用)。如果没有行号信息则置 0;同时因为不建议使用 COFF 调试信息,在映像文件中应置 0。

  • NumberOfRelocations 单字。重定位入口的数量,在映像文件中置 0。

  • NumberOfLinenumbers 单字。行号数量(供调试用)。因为不建议使用 COFF 调试信息,所以在映像文件中应置 0。

  • Characteristics 双字。节区属性。,以 bit OR 方式组合。各标志位的含义如下:

示例文件的节区头如下:

Sections

紧跟在 Section Header 后面的就是各个 sections,即节区。PE 文件一般至少要求有两个节区,用于存储可执行数据的代码节区 .text,和存储数据的数据节区 .data。通过节区名可以猜测节区的用途,但节区名不是决定节区用途的因素,只作为一种参考。比如也可以将代码节区的节区名修改为 .data,对于程序执行不会有影响。这里讲一下常见节区的用途:

其中有一些 Section 需要重点关注,比如保存着库文件导入相关数据的 .idata 节,或者与线程私有存储相关的 .tls 节等等。对这些重要节进行分析,就是之后学习的主要内容。

Last updated