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_HEADER 和 DOS 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 结构体后紧接着是 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 所示:

IMAGE_NT_HEADERS
IMAGE_NT_HEADERS 结构体,俗称 NT 头。紧跟在 DOS Stub 之后,其定义如下:
示例程序的 IMAGE_NT_HEADERS 如图 4 所示:

接下来详细说一下 NT 头。
PE Signature
NT 头的第一个成员是PE Signature,它是一个4字节大小的ASCII码字符串 PE\0\0,用于指明当前文件是一个 PE 格式的映像文件。其位置可以通过 IMAGE_DOS_HEADER 的 e_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成员的值小于系统页大小,则FileAlignment与SectionAlignment两者成员的值必须相同。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 个字节。MiscPhysicalAddress双字。文件地址。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