PE文件解析(一)
基本概念
什么是PE文件
- PE文件的全程:Portable Executable,即可移植的可执行文件
- 常见的PE文件:EXE文件、DLL文件、OCX文件、SYS文件、COM文件
- PE文件通常是指32位的,而64位的PE文件通常称为PE32+、PE+、PE64
文件偏移地址、虚拟地址与相对虚拟地址
- 文件偏移地址:PE文件存储在磁盘中时,某个数据的位置相对于文件头部的偏移量,通常将其称为文件偏移地址(File Offset Address)或物理地址(RAW Offset)
- 虚拟地址:在Windows系统中,PE文件会被系统加载器映射到内存中,而每个PE文件都有其自己的独立的虚拟空间,这个虚拟空间的内存地址就被称为虚拟地址(Virtual Address)
- 相对虚拟地址:当PE文件映射到内存之后,某个数据相对于文件载入点地址(即基地址,ImageBase)的偏移量,通常称其为相对虚拟地址(Relative Virtual Address),虚拟地址与相对虚拟地址存在如下关系:虚拟地址(VA) = 基地址(ImageBase) + 相对虚拟地址(RVA)
PE结构图
PE文件框架结构
![]()
PE文件的详细结构
PE文件磁盘结构与内存结构(对齐原因)
![]()
PE Headers解析
首先需要明确的是,严格意义上的PE文件头是指IMAGE_NT_HEADERS,但为了方便解析,此处将
- IMAGE_DOS_HEADER(DOS头)
- IMAGE_NT_HEADERS(NT头)
- IMAGE_FILE_HEADER(映像文件头)
- IMAGE_OPTIONAL_HEADER(可选映像头)
- IMAGE_SECTION_HEADER(区块表)
这五个部分都视作PE的头部部分一并进行解析
IMAGE_DOS_HEADER
MS-DOS头部,大小为64字节,每个PE文件都是以一个DOS程序开始的,且DOS可以识别出一个文件是不是一个有效的执行体,若其首部的e_magic被置为0x5A4D(即ASCII的 “MZ”,该值对应于winnt.h文件中的一个宏定义,IMAGE_DOS_SIGNATURE),那么该文件就是一个DOS可执行文件
1 | typedef struct _IMAGE_DOS_HEADER { |
其中比较重要的两个字段分别是e_magic与e_lfanew,前者的作用已经解释过,而e_lfanew是真正的PE文件头IMAGE_NT_HEADERS的相对偏移(lfanew = long file address of new exe)
用十六进制编辑器打开exe文件可以发现,起始位置e_magic字段的值为”MZ”,而e_lfanew的值为”0x000000F0”,在相对文件起始位置0x000000F0的位置我们可以找到真正的PE文件头标记”PE00”

我们可以观察到在e_lfanew和真正的PE头之间还有一些数据,这部分数据被称为DOS stub(即DOS块),DOS stub实际上是一个有效的exe,在不支持PE文件格式的操作系统中,它将显示一个错误提示,即”This program cannot be run in DOS mode”,DOS stub的数据大多由编译器自动生成,可根据自己的需要修改其中的内容,我们将IMAGE_DOS_HEADER与DOS stub合称为DOS文件头
IMAGE_NT_HEADERS
紧跟着DOS stub的就是真正的PE文件头了,这部分也被称为NT映像头,在一个有效PE文件中,其Signature字段被置为0x00004550(即ASCII的”PE00”,该值对应于winnt.h文件中的一个宏定义,IMAGE_NT_SIGNATURE),而紧跟在Signature字段之后的就是IMAGE_FILE_HEADER映像文件头,在此之后紧跟的是IMAGE_OPTIONAL_HEADER可选映像头
1 | typedef struct _IMAGE_NT_HEADERS { |
在十六进制编辑器中,NT影响头的结构如下图所示,首个字段即为”PE00”标记,紧跟其后红色框所示部分就是映像文件头,紧跟映像文件头之后的蓝色框所示的部分就是可选映像头

IMAGE_FILE_HEADER
映像文件头中包含PE文件的一些基本信息,大小为20字节,其中较为重要的两个字段为NumberOfSections字段与SizeOfOptionalHeader字段,前者指出了区块Section的数量(同时也指明了IMAGE_SECTION_HEADER区块表的数量,因为每一个区块表记录了对应区块的相关信息),后者指出了IMAGE_OPTIONAL_HEADER可选映像头的大小
1 | typedef struct _IMAGE_FILE_HEADER { |
这里对字段进行详细的解释:
1.
Machine:可执行文件的目标CPU类型,因为不同平台上指令集不同,因此需要该字段标识运行的平台,如Inter
i386及其之后的处理器,该字段的值都为0x14C
2. NumberOfSections:区块数
3.
TimeDateStamp:文件创建的时间,将该值翻译为易读字符串需要使用_ctime函数
4.
PointerToSymbolTable:COFF符号表的文件偏移位置(FOA),现较为少见
5.
NumberOfSymbols:如果有文件符号表,其指出了文件符号表中符号的数目
6.
SizeOfOptionalHeader:可选映像头的大小,其大小通常依赖于文件是32位还是64位的,若是32位文件,这个值默认为0x00E0,若是64位文件,这个值默认为0x00F0,这表示了选映像头大小的最小值,因此该值是可以修改的
7.
Characteristics:文件属性,其结果为若干个有效值的和,有效值在winnt.h定义
1 |
IMAGE_OPTIONAL_HEADER
虽然称为可选映像头,但该结构是必不可少的,其中定义了更多的数据,32位下最小大小为E0
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
33typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // 标志字
BYTE MajorLinkerVersion; // 链接器主版本号
BYTE MinorLinkerVersion; // 连接器次版本号
DWORD SizeOfCode; // 所有含有代码的区块的大小
DWORD SizeOfInitializedData; // 所有初始化数据区块大小
DWORD SizeOfUninitializedData; // 所有未初始化数据区块大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码区块起始RVA
DWORD BaseOfData; // 数据区块起始RVA
DWORD ImageBase; // 程序默认载入基地址
DWORD SectionAlignment; // 内存中块的对齐值
DWORD FileAlignment; // 磁盘文件中块的对齐值
WORD MajorOperatingSystemVersion; // 操作系统主版本号
WORD MinorOperatingSystemVersion; // 操作系统次版本号
WORD MajorImageVersion; // 用户自定义主版本号
WORD MinorImageVersion; // 用户自定义次版本号
WORD MajorSubsystemVersion; // 所需子系统主版本号
WORD MinorSubsystemVersion; // 所需子系统此版本号
DWORD Win32VersionValue; // 保留,通常设置为0
DWORD SizeOfImage; // 映像载入内存后的总大小
DWORD SizeOfHeaders; // DOS头、PE文件头、区块表的总大小
DWORD CheckSum; // 映像校验和
WORD Subsystem; // 文件子系统
WORD DllCharacteristics; // 显示DLL特性的旗标
DWORD SizeOfStackReserve; // 初始化时栈的大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际保留的堆大小
DWORD LoaderFlags; // 调试相关,默认值为0
DWORD NumberOfRvaAndSizes; // 数据目录项的数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表数组
} IMAGE_OPTIONAL_HEADER32
这里对一些较为关键的字段进行详细的解释:
1.
Magic:标志字,ROM映像为0x107,32位可执行映像为0x010B,64位可执行映像位0x020B
2.
SizeOfCode:所有含有IMAGE_SCN_CNT_CODE属性的区块的总大小,必须是FileAlignment的整数倍,由编译器填写,通常情况下,大多数文件只有一个Code块,所以该字段与.text块的大小匹配
3.
SizeOfUninitializedData:所有未初始化数据区块大小,装载程序需要在虚拟地址空间中位这些数据分配空间,这些块在磁盘文件中不占空间,在程序开始运行时没有指定值,未初始化数据通常在.bss块中
4.
AddressOfEntryPoint:程序执行入口RVA。对于DLL,这个入口点在进程初始化和关闭时与线程创建和销毁时被调用,在大多数可执行文件中,这个地址不直接指向Main、WinMain或DllMain,而是指向运行时的库代码,并由它来调用上述函数
5.
ImageBase:程序默认载入基地址,如果PE文件在这个地址载入,加载器将会跳过应用基址重定位的步骤
6.
SectionAlignment:载入内存时,内存中块的对齐值,也就是说每个区块被载入的地址必定是本字段指定数值的整数倍,默认的对齐尺寸是目标CPU的页尺寸(通常是0x10000,也就是4KB)
7.
FileAlignment:磁盘文件中块的对齐值,区块在磁盘文件中存储的首地址必定是本字段指定数值的整数倍,对于x86可执行文件,这个值常为0x200或0x1000,这是为了保证块总是从磁盘的扇区开始,该值必须是2的幂
8.
SizeOfImage:映像载入内存后的总大小,即从ImageBase到最后一个块结束,且按照SectionAlignment对齐的大小
9.
SizeOfHeaders:DOS头、PE文件头、区块表的总大小,按FileAlignment对齐
10.
CheckSum:映像校验和,CheckSumMappedFile函数可以计算该值,通常情况下,普通的EXE文件该值为0,但内核模式的驱动程序和系统DLL必须有一个校验和
11. NumberOfRvaAndSizes:数据目录项的数量,该值至今一直为16
12.
DataDirectory[16]:数据目录数组,由数个相同的IMAGE_DATA_DIRECTORY结构组成,其具体的结构如下
1 | typedef struct _IMAGE_DATA_DIRECTORY { |
数据目录表成员的结构如下所示
| 序号 | 表名 | 结构 |
|---|---|---|
| 0 | Export Table | IMAGE_DIRECTORY_ENTRY_EXPORT |
| 1 | Import Table | IMAGE_DIRECTORY_ENTRY_IMPORT |
| 2 | Resources Table | IMAGE_DIRECTORY_ENTRY_RESOURCE |
| 3 | Exception Table | IMAGE_DIRECTORY_ENTRY_EXCEPTION |
| 4 | Security Table | IMAGE_DIRECTORY_ENTRY_SECURITY |
| 5 | Base Relocation Table | IMAGE_DIRECTORY_ENTRY_BASERELOC |
| 6 | Debug | IMAGE_DIRECTORY_ENTRY_DEBUG |
| 7 | Copyright | IMAGE_DIRECTORY_ENTRY_COPYRIGHT |
| 8 | Global Ptr | IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
| 9 | Thread Local Storage (TLS) | IMAGE_DIRECTORY_ENTRY_TLS |
| 10 | Load Configuration | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
| 11 | Bound Import | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
| 12 | Import Address Table (IAT) | IMAGE_DIRECTORY_ENTRY_IAT |
| 13 | Delay Import | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
| 14 | COM Descriptor | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
| 15 | 保留,必须为0 | - |
IMAGE_SECTION_HEADER
区块表中记录了区块的具体信息,每个区块表分别指向了不同的区块实体,紧跟在 IMAGE_OPTIONAL_HEADER 之后,每个区块表大小都是40字节
1 | typedef struct _IMAGE_SECTION_HEADER { |
块属性中的一些重要字段值如下所示
1 |
在十六进制编辑器中的区块表信息如下图所示,可以观察到该exe文件包含4个区块表,其中四个区块的信息名称分别为
- .text
- .rdata
- .data
- .rsrc

区块解析
首先需要注意的是,区块名称只是为了方便辨识,但对于操作系统来说是无关紧要的,如当寻找输出表、输入表信息时,不应该默认到.text和.rdata区块中寻找,而是要严格依据数据目录数组DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]中的信息进行查找,常见区块如下
| 名称 | 描述 |
|---|---|
| .text | 默认的代码区块,其中的内容全是指令代码 |
| .data | 默认的读、写区块,全局变量、静态变量通常放在此处 |
| .rdata | 默认的只读数据区块,程序较少用到该块中的数据,但至少有两种情况会用到,一是在Microsoft链接器产生的exe文件中,用于存放调试目录;二是用于存放说明字符串,如果程序的DEF文件中指定了DESCRIPTION,字符串就会在出现在该块中 |
| .idata | 输入表,包含其他外来DLL的函数及数据信息,通常将其合并到其他区块中,如.rdata |
| .edata | 输出表,当创建一个输出API或数据的可执行文件时(如DLL),链接器会创建一个.exp文件,.exp文件将会包含一个.edata区块,并加入到最后的可执行文件中,通常将.edata合并到其他块中,如.text区块中 |
| .rsrc | 资源,包含模块的全部资源,例如图标、菜单、位图等,该区块是只读的,无论如何都不应该命名为为.rsrc以外的名字,也不能被合并到其他区块中 |
| .reloc | 可执行文件的基址重定位,通常只是DLL需要,而exe不需要,通常在Release模式下,链接器不会给exe文件加上基址重定位 |

