本文为 Maldev Academy 中的 Module 50 小节的笔记内容,通过代码实现解析 PE 文件头。(完整学习内容请自行前往跳转链接查看)。
虽然之前也接触过 PE 文件头之类的内容,但并没有直接写代码解析获取各部分的内容,通过这节的内容刚好学习下。在网上可以找到现成的 PE 文件结构图。

包括课程原文章也有示意图。但后面写代码时参考起来总感觉用起来不是那么顺手。便参考 010-Editor 工具载入PE文件后下面的字段解析,画了张图。
010-Editor 示例图:

PE文件头解析:

提前需要先理解一个概念 Relative Virtual Addresses (RVAs)。
相对虚拟地址(RVA)是用于引用 PE 文件内位置的地址。它们用于指定 PE 文件中各种数据结构和部分的位置,例如代码、数据和资源。
RVA 是一个32位值,指定数据结构或节从PE文件开头的偏移(offset)。它被称为“相对”地址,因为它指定的是从文件开头的偏移,而不是内存中的绝对地址。这允许同一个文件在内存中的不同地址加载,而无需对文件中的RVA进行任何更改。
RVA 在 PE 文件格式中被广泛使用,以指定文件中各种数据结构和节的位置。例如,PE 头包含几个 RVA,指定代码和数据节、导入和导出表以及其他重要数据结构的位置。
要将相对虚拟地址(RVA)转换为虚拟地址(VA),只需要操作系统将模块的基地址(模块加载在内存中的位置)加上 RVA 即可。这使得操作系统能够访问模块内指定位置的数据,而不管模块在内存中的加载位置。
剩下的就好说了,直接使用对应的代码开始解析 PE 文件的各个段即可。
一、DOS Header
由于 DOS Header 位于 PE 文件的最开始,检索 DOS Header 只需将pPE变量类型转换为PIMAGE_DOS_HEADER即可。
|
|
pPE 变量为在内存中加载的要解析的 PE 原始文件的字节数据。通过CreateFileA、GetFileSize、HeapAlloc获取即可。
二、NT Header (IMAGE_NT_HEADERS)
NT Header包括三部分:Signature、File Header、Optional Header。
DOS 头结构中的 e_lfanew 成员是一个指向 IMAGE_NT_HEADERS 结构的 RVA 偏移。为了访问 NT Header,只需要把 PE 文件在内存中的基址与这个偏移(e_lfanew)相加即可。下面的代码片段演示了这一点。
|
|
1、Signature
一个常量签名值(4D 5A - MZ)。
2、File Header
由于 File Header 是 IMAGE_NT_HEADERS 结构体的一个成员,因此可以通过下面这行代码来访问:
|
|
File Header 的成员包括: Machine、NumberOfSections、TimeDateStamp、PointerToSymbolTable、NumberOfSymbols、SizeOfOptionalHeader、Characteristics。
3、Optional Header
由于 Optional Header 是 IMAGE_NT_HEADERS 结构的一个成员,因此可以通过下面的方式访问:
|
|
我们还可以通过下面的方式获取 Optional Header:
|
|
Optional Header 的重要成员包括: Magic、MajorLinkerVersion、MinorLinkerVersion、SizeOfCode、SizeOfInitializedData、SizeOfUninitializedData、AddressOfEntryPoint、BaseOfCode、BaseOfData、ImageBase、MajorOperatingSystemVersion、MinorOperatingSystemVersion、MajorImageVersion、MinorImageVersion、DataDirectory。
3-1、DataDirectory:
Data Directory 可以从 Optional Header 的最后一个成员获取。这个成员是一个 IMAGE_DATA_DIRECTORY 数组,也就是说数组中的每一个元素都是一个 IMAGE_DATA_DIRECTORY 结构,用于引用某一个特殊的数据目录。IMAGE_DATA_DIRECTORY 结构如下所示:
|
|
PE 文件中一些预定义的数据目录包括:
IMAGE_DIRECTORY_ENTRY_EXPORT—— 包含 PE 文件导出的函数与数据的信息IMAGE_DIRECTORY_ENTRY_IMPORT—— 包含从其它模块导入的函数与数据的信息IMAGE_DIRECTORY_ENTRY_RESOURCE—— 包含 PE 文件中的资源(例如图标、字符串、位图等)的信息IMAGE_DIRECTORY_ENTRY_EXCEPTION—— 包含 PE 文件中异常处理表的信息
可以通过下面的代码访问 data directories:
|
|
例如,获取 Export Directory 的 Data Directory 可以这样写:
|
|
3-1-1、Export Table
Export Table 是一个名为 IMAGE_EXPORT_DIRECTORY 的结构,定义如下:
|
|
IMAGE_EXPORT_DIRECTORY 结构用于存储 PE 文件导出的函数和数据的信息。这些信息存放在 Data Directory 数组中,索引为 IMAGE_DIRECTORY_ENTRY_EXPORT。从 IMAGE_OPTIONAL_HEADER 中获取该结构可以这样写:
|
|
3-1-2、INT & IAT Table
在解析 PE 文件的逻辑流程中,每一个被引入的 DLL(如 kernel32.dll、user32.dll)都必须拥有自己独立的 INT 和 IAT。
-
INT (Import Name Table):记录了该 DLL 中所有需要导入的函数名。
-
IAT (Import Address Table):记录了该 DLL 中所有导入函数对应的内存地址。
如果引入了 5 个 DLL,那么在逻辑解析时,就会看到 5 个不同的 INT 表和 5 个不同的 IAT 表。
虽然逻辑上是分开的,但在 PE 文件的二进制结构中,为了方便管理(比如修改内存属性),编译器通常会将所有 DLL 的 IAT 块连续地排在一起,形成一个巨大的“IAT 数据区”。
- DataDirectory[1] (Import Directory):它指向的是
IMAGE_IMPORT_DESCRIPTOR数组。每个描述符就像是一个“指针”,指明了总数据区中的哪一段属于当前的 DLL。 - DataDirectory[12] (IAT Directory):它指向的就是这个连续的、总的 IAT 数据区的起点。
获取 Import Directory (索引 1):
|
|
获取 Import Address Table Directory (索引 12):
|
|
关于 INT Table 和 IAT Table 的分析。

IMAGE_IMPORT_DESCRIPTOR 结构在 Winnt.h 头文件 中定义如下,但微软并未正式记录该结构:
|
|
IMAGE_IMPORT_DESCRIPTOR 结构体是 PE 文件导入表的核心入口。每一个被程序引用的 DLL 都会对应一个 IMAGE_IMPORT_DESCRIPTOR 结构。
- OriginalFirstThunk (桥1):指向 INT (Import Name Table) 的 RVA(相对虚拟地址)。它是查找函数名的依据。
- Name:指向一个以
NULL结尾的字符串,包含该 DLL 的名称(如kernel32.dll)。 - FirstThunk (桥2):指向 IAT (Import Address Table) 的 RVA。它是程序运行时实际跳转执行的地址所在地。
(1)INT Table(Import Name Table):
INT 是一个结构体数组,其对应的底层定义是 IMAGE_THUNK_DATA。它是一份只读的“索引清单”。
|
|
-
桥1的终点:
OriginalFirstThunk指向的就是这个数组的开头。 -
在磁盘文件里,它的
u1.AddressOfData存储着一个 RVA,指向真正的函数名结构体IMAGE_IMPORT_BY_NAME。
|
|
(2)IAT Table(Import Address Table):
IAT 在代码层面的定义与 INT 完全一样(也是 IMAGE_THUNK_DATA 数组),但其字段在不同阶段代表不同的含义。
|
|
-
桥2的终点:
FirstThunk指向这个数组的开头。 -
文件状态(静止):其内容通常和 INT 一样,也指向
IMAGE_IMPORT_BY_NAME。 -
内存状态(运行):Windows 加载器会根据 INT 找到函数名,查询地址后,直接覆写这个
u1.Function字段。此时它不再是 RVA 偏移,而是一个硬编码的 VA(虚拟地址),如0x77E11234。
(3)整体联动过程:
静态阶段(磁盘上的 PE 文件)。程序还没运行、存储在硬盘中时。
IMAGE_IMPORT_DESCRIPTOR里的OriginalFirstThunk(桥1)指向INT。IMAGE_IMPORT_DESCRIPTOR里的FirstThunk(桥2)指向IAT。- 此时,INT 和 IAT 里的内容是完全一样的:它们都存储着一系列 RVA(相对虚拟地址),这些地址都指向同一个目的地——
IMAGE_IMPORT_BY_NAME结构体(即函数名字符串,如 “ExitProcess”)。
动态阶段(Windows 加载器的工作)。当双击运行程序时,Windows 加载器(Loader)会将 PE 文件映射到内存,并开始“整体联动”。
- 定位 DLL:加载器读取
IMAGE_IMPORT_DESCRIPTOR的Name字段(如 “kernel32.dll”),并在系统中找到该 DLL,将其加载到内存中。 - 寻找函数(通过桥1):加载器查看
OriginalFirstThunk指向的 INT。它读取里面的IMAGE_THUNK_DATA,找到函数名称字符串,如 “ExitProcess”。 - 获取真实地址:加载器在已加载的
kernel32.dll导出表中搜索 “ExitProcess”,计算出它在当前内存中的绝对地址(例如0x77E11234)。 - 填充结果(通过桥2):加载器将这个绝对地址写回
FirstThunk指向的 IAT 对应位置。
最终状态。
- 桥1 (OriginalFirstThunk -> INT):依然指向函数名 “ExitProcess”。它就像备份清单,永远记录着最初想要什么。
- 桥2 (FirstThunk -> IAT):现在指向的是真实的函数地址。
(4)Windows 加载器全量初始化:
Windows 加载器通过以下逻辑确保所有依赖环境就绪。
外层循环:遍历 DLL 列表
- 动作:加载器从
DataDirectory[1]指向的地址开始,挨个读取IMAGE_IMPORT_DESCRIPTOR。 - 联动:读取
Name字段,加载目标 DLL(如user32.dll)到进程空间。 - 终止:直到遇到一个全 0 的描述符结构体,外层循环结束。
内层循环:遍历函数名与填充地址
-
初始化双指针:
-
指针 A (INT):指向
OriginalFirstThunk指向的IMAGE_THUNK_DATA数组。 -
指针 B (IAT):指向
FirstThunk指向的IMAGE_THUNK_DATA数组。
-
-
查名(INT 侧):加载器读取指针 A 处的 RVA,定位到
IMAGE_IMPORT_BY_NAME结构,获取函数名字字符串(如 “MessageBoxW”)。 -
解析地址:在 DLL 的导出表中查找该名字对应的真实内存虚拟地址 (VA)。
-
填位(IAT 侧):加载器将得到的 VA 地址直接覆盖掉指针 B 所在的槽位。
-
同步步进:指针 A 和 指针 B 同时向后移动 4 字节(32位)或 8 字节(64位)。
-
全量填充:即便程序的代码只用了一个函数,加载器也会把该 DLL 在导入表中列出的所有函数全部填完,以备不时之需。直到遇到 0x00000000 结束该 DLL 的内层循环。
获取每个 DLL 对应的 INT Table 和 IAT Table:
|
|
运行效果:
[+] DLL Name: KERNEL32.dll
[#] INT RVA: 0x0001B250 (Address: 0x008CB250)
[#] IAT RVA: 0x0001B000 (Address: 0x008CB000)
[+] DLL Name: USER32.dll
[#] INT RVA: 0x0001B2E0 (Address: 0x008CB2E0)
[#] IAT RVA: 0x0001B090 (Address: 0x008CB090)
[+] DLL Name: VCRUNTIME140D.dll
[#] INT RVA: 0x0001B310 (Address: 0x008CB310)
[#] IAT RVA: 0x0001B0C0 (Address: 0x008CB0C0)
[+] DLL Name: ucrtbased.dll
[#] INT RVA: 0x0001B360 (Address: 0x008CB360)
[#] IAT RVA: 0x0001B110 (Address: 0x008CB110)
3-1-3、TLS DIRECTORY
IMAGE_TLS_DIRECTORY —— 该结构用于存储 PE 文件中关于线程本地存储(Thread-Local Storage,TLS)数据的信息。此时需要了解的重点是如何从 IMAGE_OPTIONAL_HEADER 中获取这个结构;至于它的详细内容,会在后续模块中需要使用时再进行说明。
|
|
3-1-4、EXCEPTION DIRECTORY
IMAGE_DIRECTORY_ENTRY_EXCEPTION —— 包含 PE 文件中异常处理表(Exception Handling Tables)的信息。
IMAGE_RUNTIME_FUNCTION_ENTRY —— 该结构用于存储 PE 文件中某个运行时函数的信息。运行时函数(runtime function)指由 Windows 操作系统的异常处理机制(SEH/C++ Exception)调用,用于执行某个异常的异常处理代码的函数。此时需要了解的是如何从 IMAGE_OPTIONAL_HEADER 中获取该结构;至于详细的内容,会在后续模块中需要使用时再进行说明。
|
|
获取方式:
|
|
3-1-5、Base Relocation Table
IMAGE_BASE_RELOCATION —— 该结构用于存储 PE 文件中的基址重定位(Base Relocation)信息。基址重定位用于在 PE 文件被加载到与其链接时不同的内存地址时,对文件中导入的函数和变量的地址进行修正。此时需要了解的是如何从 IMAGE_OPTIONAL_HEADER 中获取该结构;至于详细内容,会在后续模块中需要使用时再进行说明。
|
|
三、Section Headers (IMAGE_SECTION_HEADERS)
需要注意几个重要的 PE 段,比如 .text、.data、.reloc、.rsrc 等。此外,根据编译器及其设置的不同,可能还会存在更多的 PE 段。每个段都有一个 IMAGE_SECTION_HEADER 结构用来描述该段的信息。IMAGE_SECTION_HEADER 结构定义如下:
|
|
IMAGE_SECTION_HEADER 结构会以数组的形式存放在 PE 文件的头部中。要访问数组的第一个元素,需要越过 IMAGE_NT_HEADERS,因为各个 section 就紧跟在 NT Header 后面。下面的代码片段展示了如何获取 IMAGE_SECTION_HEADER 结构,其中 pImgNtHdrs 是指向 IMAGE_NT_HEADERS 的指针。
|
|
遍历数组:
遍历这个数组需要知道数组的大小,可以通过 IMAGE_FILE_HEADER.NumberOfSections 成员获取。数组中后续的每一个元素,都位于当前元素偏移 sizeof(IMAGE_SECTION_HEADER) 的位置。
|
|
至此,便解析出 PE 文件中的大部分关键内容了。