PE文件格式学习

说明:Matt Pietrek 著的《Windows 95 System Programming SECRETS》(中文译名:《Windows 95 系統程式設計大奧秘》)是深入了解WINDOWS系统的一部难得的好书，台湾的侯俊杰不仅把它译为中文还开放了繁体中文电子版免费提供下载。书中第八章专门介绍WINDOWS的PE文件格式，非常有价值，偶把这章单独整理成HTML文件，对一些台湾的电脑术语做了替换，难免有疏漏，建议参考原繁体中文电子版。

书名:《Windows 95 System Programming SECRETS》(中文译名:《Windows 95 系統程式設計大奧秘》
作者:Matt Pietrek(美国)
译者:侯俊杰(台湾)
繁体中文电子版下载:http://www.csdn.net/expert/jjhou/windows95-system-programming-secrets.pdf
  1. PE 与COFF OBJ 文件格式
  2. PEDUMP，Win32 和PE 的基础观念
  3. PE 表头（PE Header）
  4. The Section Table
  5. 常会遇到的Sections
  6. PE 文件的输入（imports）
  7. PE 文件的输出（exports）
  8. PE 档的资源
  9. PE 档的基底重定位（Base Relocations）
  10. COFF 符号表格 （存目）
  11. COFF 除错信息 （存目）
  12. COFF 行号表格 （存目）
  13. PE 档和COFF 档之间的差异
  14. COFF LIB 档
  15. 摘要

    16. 附录:VC++ 6.0 Include目录下winnt.h中的PE相关部分
注：繁体中文版将原文书中的"COFF 符号表格"、"COFF 除错信息"以及"COFF 行号表格"三节略去。这三节都与除错信息有关，在原文书的 p.605~p.614 ，共约9 页。这是此书唯一略去原文内容的部分。


目录 | 下一页
1.PE 与COFF OBJ 文件格式

一个操作系统的可执行文件格式，从许多角度来看，都是操作系统内建行为的一面镜子。
虽然可执行档格式通常并不是一个程序员认为迫切需要学习的东西，但操作系统的许多
有用的知识却可以在这个过程中获得。动态联结、加载器行为、以及内存管理，是特
别容易在这个学习过程中推理而得的三个主题。

在这一章，我要带你走一趟真正的旅游 -- 游览 Portable Executable 文件格式。PE 档是
微软设计用于其所有 Win32 位操作系统（Windows NT 、Windows 95 、Win32s ）的可
执行档格式。

你或许会惊讶为什么我要在这本书中涵盖这一章，毕竟在Microsoft Developer Network
CD-ROM 之中已有数篇相关文章。主要的原因是，PE 文件中的结构，同时也是 Windows
95 的关键性数据结构。例如，Windows 95 把 PE 档的表头段（header section ）映像到
内存中，并以之表示一个被加载的模块。为了了解 Windows 95 的核心如何工作，你
需要了解PE 格式。就是这么简单！

我探讨 PE 文件的另一个理由是，就像所有出自于微软的文件一样，微软的 PE 文件彷
佛假设你已经和 PE 文件融为一体，因此它非常地不够充份，不够详细。我这一章的目
标是要补充那些文件，并且把它和你每天的经验关联在一起。沿着这样的方向，我会展
示 PE 文件影响操作系统的各种方式。

在可见的未来，PE 格式将在微软的操作系统（包括 Cairo ）中扮演一个吃重的角色。甚
至即使你使用 Visual C++ 在 Windows 3.1 中开发程序，你还是要使用PE 文件（因为
Visual C++ 的 32 位 DOS 扩充组件也是使用这种格式）。如果你打算在 Windows 95
内设计任何层面的系统程序，PE 格式的知识更是不可或缺。

在讨论 PE 格式的过程中，我不打算费劲地显示无穷无尽的hex dump （16 位倾印码），
然后解释个别位的重要性（译注）。取而代之的是，我要解释内嵌在其中的观念，并
把它和你每天的 Win32 程序经验关联起来。例如，线程区域变量（thread local variable ）
的概念就几乎把我逼疯，直到我了解它是如何精致优雅地在可执行档中实作出来。由于
许多Win32 程序员拥有 Win16 背景，我会想办法让 PE 格式与其 16 位兄弟（NE 格
式）尽量产生一些关联。
译注：事实上，适当追踪具代表性的16 位倾印码，不但不枯躁（我已经在我所主持 的多次研讨会中验证了这个论点），而且经由「铁证如山」的追踪，我们才能从模模糊 糊的视野中解脱出来。我将在我自己的新书中详细解剖 PE 文件格式。
在微软引入这个可执行档格式的同时，它也引入了一个新的目标模块（object module ）格
式和函数库（library ）格式。这些格式由新的编译器和组译器产生。新的 LIB 格式基本
上只不过是一群 OBJ 档绑在一起，再加上一些索引。所以当我说 OBJ 档时，我说的是
COFF OBJ 和 LIB 档。这些新的 OBJ 和 LIB 格式分享了 PE 格式的许多观念。直到
最近，都还没有公开可用的信息，介绍微软的 OBJ 和 LIB 档。甚至在我下笔的时刻，
资料都还很缺乏。所以，把 OBJ 和 LIB 文件格式也涵盖进来是值得的。

大家都知道，Windows NT （第一个 Win32 操作系统）有 VAX VMS 和 UNIX 的血统。
NT 开发小组中的许多关键人物在进入微软之前就是在那些平台上设计并写码。当他们
开始设计 NT ，很自然地他们会想到使用过去写过并且测试过的工具。那些工具所产生
的可执行档以及目标模块（object module ）的格式称为COFF （Common Object File
Format ）。

COFF 格式本身已有良好的基础，但还需要扩充以满足现代操作系统如 Windows NT 的
需要。更新的结果就是 PE （Portable Executable ）格式。称为可移植性（portable ）是因
为任何机器（Intel 386 、MIPS 、Alpha 、Power PC 等等）上的 NT 都可以使用相同的可
执行档格式。当然啦，CPU 指令的二进制编码是完全不同的，你不可能把一个在 MIPS 机
器编译好的 PE 文件拿到 Intel 系统来跑。重要的是，程序加载器以及程序开发工具不需
要针对每一个新的操作系统重写。

微软对于 Windows NT 的高度企图心可以从一件事获得证明：它放弃了自己原有的32 位
元工具和文件格式。Windows 3.x 的虚拟装置驱动程序（VxDs ）就是以不同的 32 位
文件布局（LE 格式）呈现，而它早在 NT 跳上舞台之前就出现了。由于 Windows 的
性格是 "if it ain't broke, don't fix it"，所以Windows 95 同时使用 PE 和 LE 两种格式。
这使得微软能够继续大规模地使用既有的 Windows 3.x 码。

虽然，预期一个全新的操作系统（如 Windows NT ）有一个全新的可执行档格式是合理
的，但面对 object module （.OBJ 和 .LIB ）又不尽然是这么回事。在 Visual C++ 32 位
元版 1.0 之前，所有的微软编译器都使用 Intel 的OMF （Object Module Format ）规格，
但 Win32 编译器改采 COFF 格式的 OBJ 档。微软的一些竞争者（如 Borland 公司）
则坚持使用 Intel 的 OMF 格式。

一些微软阴谋论者可能会认为改变 OBJ 格式正是微软企图打击对手的证据。因为如果
要宣称能够「与微软兼容」于 OBJ 层次，其它厂商就必须把它们的所有 32 位工具
都转换为 COFF OBJ 和 LIB 格式。简单地说，OBJ 和 LIB 文件格式可视为是微软为
求某种利益而放弃既有标准的又一个例子。

WINNT.H 之中有一些 PE 格式的说明（虽然是极其模糊暧眛），它定义了一些COFF OBJ
用的结构。我将在稍后细部解释时使用这些 WINNT.H 中定义的字段名称。在 WINNT.H
之中有一段名为 "Image Format"，从这里开始就是旧的 MZ-DOS 格式和 NE 格式的表
头，然后才是 PE 格式资料。WINNT.H 提供了 PE 文件所需要的数据结构的明确定义，
但只以少量文字说明那些结构、字段、旗标的意义。PE 格式的表头设计者 Michael J.
O'Leary 想必是一位「长名称，具自身说明意义」的忠实拥护者，以及一位「巢状结构和
宏」的忠实拥护者。当面对 WINNT.H 写码时，你得面对像这样的句子：

pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;

除了读取 PE 格式的成份，你还需要把一些 PE 档倾印出来，看看呈现其中的观念。如
果你使用微软的 Win32 开发工具，Visual C++ 和 Win32 SDK 提供的 DUMPBIN 程序
可以解析 PE 档和 COFF OBJ/LIB 档，并以人类可读的形式输出。DUMPBIN 甚至有一
项极佳能力，可以反汇编文件中的 code section 。有鉴于微软曾经宣称你不可以反汇编其
产品，这就有趣了。它竟然供应一个工具，如此轻易地反汇编它的程序和 DLLs 。如果
反汇编 EXEs 和 OBJs 的能力没有用，为什么微软要把这项功能加到 DUMPBIN 之中
呢？似乎这又是一个 "Do as we say ，not as we do"（照着我们所说的做，不要照着我们所
做的做）的例子。

Borland 的使用者可以使用 TDUMP 观察 PE 档，但 TDUMP 并不了解 COFF OBJ
档，这不是个大问题，因为 Borland 根本就不产生 COFF OBJ 档。我自己也写了一个PE
档和 COFF OBJ/LIB 档的倾印工具，名为 PEDUMP ，并且我认为我提供了比DUMPBIN
更容易了解的输出格式。它没有反汇编能力，但其它功能都类似DUMPBIN ，并且还有
一些新功能使它身价高一些。PEDUMP 的原始码放在书附磁盘中，所以我不打算在书中
列出太多。取而代之的是，我要列出 PEDUMP 的一些输出结果，用来解释我所要说的
观念。
目录 | 下一页


2.PEDUMP

PEDUMP 是一个命令列程序，用来倾印 PE 档和 COFF OBJ/LIB 档的格式。它使用
Win32 console 能力，去除了使用者接口的负担。PEDUMP 的使用语法如下：

PEDUMP [switches] filename

执行 PEDUMP 但不给任何参数，你就可以看到选项（switches ）有多少种。PEDUMP 支
援下列选项：

/A include everything in dump (也就是把所有选项置能起来）
/H include hex dump of sections
/I include Import Address Table thunk addresses
/L include line number information
/R show base relocations （只适用于 PE 文件）
/S show symbol table （适用于 PE 档和 COFF OBJ 档）

预设情况下没有任何选项是置能的。

PEDUMP 把它的输出送到标准输出文件（例如屏幕），所以它的输出可以被重定向（利用 >
符号）。PEDUMP 原始码附含在本书磁盘之中，它是以 Visual C++ 2.0 建造完成的，但
我也以 Borland C++ 4.x 编译它，一切顺利。

Win32 和PE 的基础观念

在探讨 PE 档的布局之前，我需要察看一些基础观念，以便渗透其设计。我将使用模块
（module ）一词表示一个 EXE 或 DLL 被加载内存后的程序代码、资料和资源。除了
程序代码和资料是你的程序直接使用的之外，模块还内含一些支持性数据，Windows 用它
来决定程序代码和资料放在内存的什么地方。在 Win16 之中，这些支持性数据结构位于
module database （一个由 HMODULE 指出的节区）。在 Win32 ，这些信息保留在 PE 表
头中（以一个 IMAGE_NT_HEADERS 结构），稍后我将有详细的说明。

关于 PE 档，你必须知道的一件最重要的事情就是：磁盘中的可执行文件格式非常类似内存
中的模块。因此 Windows 加载器无须非常辛苦地工作就能根据磁盘文件产生一个
行程。加载器使用 Win32 内存映像档，把适当的 PE 文件加载程序的地址空间中。
以建筑学的方式来说，PE 档就像是一栋预先制造组合配件的房屋：配件不多，每一个配
件可以啪答一声就定位 -- 只要经过一些些努力。并且，就像组合屋很容易接上电路和水
管一样，PE 档也很容易与外界产生关系（我是指像连接到 DLLs 之类的动作）。

加载 DLLs 也是一样地简单。一旦一个 DLL 或 EXE 模块被加载内存，Windows 就
把它们当成其它内存映像档一样。这与 16 位 Windows 是明显不同的对比。16 位
元 NE 文件加载器读入文件的一部份，并产生个别的数据结构，以便在内存中显现模
组。当一个 code segment 或 data segment 需被加载，加载器必须从global heap 中配置
内存，找出资料存放在可执行文件中的什么地方，读入资料，并做任何必要的修订。此
外，每一个 16 位模块有责任记住它目前使用的所有selectors 、哪一个节区被拋弃
（discarded ）了等等。

Win32 就不是这样了。用来放置模块的码、资料、资源、import tables 、export tables 、以
及其它东西的内存，是位于一块连续的线性地址空间中。所有你需要知道的，就是这
个地址在哪里。然后你就可以轻轻松松根据储存在这个 "image"（译注）中的指针，找
到模块的每一样资料。
译注：文件中的模块资料被加载内存后，我们称其为模块的 "image" 。我保留这个原文，不译它。
在我们开始之前，你必须知道的另一个观念是所谓的相对虚拟地址（Relative Virtual
Address ，RVA ）。PE 文件中的许多字段内容都是以 RVA 表示。一个 RVA 是某一资
料项的 offset （偏移）值 -- 从文件被映像进来的起点算起。举个例子，我们说 Windows
加载器把一个 PE 档映像到虚拟地址空间的 0x400000 处，如果此一 image 有一个表格
开始于 0x401464 ，那么这个表格的 RVA 就是 0x1464 ：

虚拟地址 0x401464 - 基地址 0x400000 = RVA 0x1464

只要把 RVA 加上基地址，RVA 就可以被转换为一个有用的指针。「基地址（base
address ）」是另一个重要观念，用来描述被映像到内存中的EXE 或 DLL 的起始地址。
为了方便，Windows NT 和 Windows 95 都以模块的基地址做为模块的 instance handle
（HINSTANCE ）。在 Win32 之中，称呼一个模块的基地址为一个 HINSTANCE 似
乎有点混淆，因为 instance handle 一词根本就是来自于 16 位 Windows 。Win16 每
一个程序的副本都拥有它自己的资料节区（以及一个对应的全域性 handle ），用以和同
一程序的其它副本有所区分，因此才有instance handle 这样的词儿。

在 Win32 ，程序并不需要和其它人有所区分，因为它们根本就不共享地址空间。但是
HINSTANCE 这个术语还是保留，以便至少在外观上使得 Win16 和 Win32 有着一致
性。在 Win32 中重要的是你可以针对行程所使用之 DLLs ，呼叫 GetModuleHandle ，取
得一个指针。你可以用它来处理模块组件。我所谓的组件是指被输入（imported ）函数或
被输出（exported ）函数、它们的位置、它们的code section 和data section...等等等。

另一个观念是 section 。PE 档或 COFF OBJ 档的一个 section 大略相当于16 位 NE
档的一个 segment 。Sections 内含程序代码或资料。某些 section 内含的是你的程序宣告并
直接使用的程序代码或资料，另有一些存放资料的 sections 是由联结器和 librarian （制造
LIB 的工具）产生，内含对操作系统很重要的信息。在微软的某些文件中，也把 sections
称为 objects 。后者已经是如此地泛滥，所以我还是使用 sections 一词。我将在「通常会
遇到的 Sections 」一节再详细讨论 sections ，目前，你只要知道 section 到底是什么就好
了。

在跳到 PE 档细节之前，好好看看图8-1 ，它显示出 PE 文件的大致布局。我将分别解
释每一块内容，但是大局观是很有帮助的。

图8-1 PE 文件的大致布局
上一页 | 目录 | 下一页


3.PE 表头（PE Header）

我们的 PE 之旅第一站是 PE 表头。像其它的微软可执行档格式一样，PE 文件有一系列
的字段，固定在一个已知（并且很容易找到）的位置上，定义文件的其它部份。PE 表头
内含的重要信息包括程序代码和资料区域的大小位置、适用的操作系统、堆栈（stack）的
最初大小等等。

和其它的微软可执行档格式一样，PE 表头并非在文件的最起头处。文件最前面的数百个
字节是所谓的 DOS stub ：一个极小的 DOS 程序，用来输出像 "This Program cannot be
run in DOS mode" 这样的讯息。如果你在一个不支持Win32 的操作系统上跑一个 Win32
程序，就会获得这个错误讯息。当 Win32 加载器把一个 PE 档映像到内存，内存
映像文件（memory mapped file ）的第一个字节对应到 DOS Stub 的第一个字节。好得
很，于是，你每执行一个 Win32 程序，就获得了一个免费的DOS 程序（在Win16 中
这个 DOS stub 并不加载内存）。

和其它的微软可执行档格式一样，你可以在 DOS stub 的表头中找到「真正的表头」。
WINNT.H 为 DOS stub 表头定义了一个结构，循此我们将非常容易找到 PE 表头。
e_lfanew 字段是一个相对偏移值（或说是 RVA ），指向真正的 PE 表头。为了获得指
标，你必须为 RVA 加上 image 的基地址：

// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;

一旦你拥有指向 PE 表头的指针，真正的趣味就开始了。PE 表头整个是个
IMAGE_NT_HEADERS 结构，定义于 WINNT.H 。这个结构正是 Windows 95 的module
database 。每一个被加载的 EXE 或 DLL 都以一个 IMAGE_NT_HEADERS 结构表现出
来。此一结构有一个 DWORD 和两个子结构：

DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;

Signature 字段内容应该是 ASCII 的 PE\0\0 。如果 DOS stub 表头中的 e_lfanew 字段
指向一个 NE Signature 而不是一个 PE Signature ，表示你面对的是一个 Win16 NE 可执
行档。如果是 LE Signature 则表示你面对的是一个 VxD 档。如果是 LX Signature 则表
示 OS/2 文件。

再来是一个IMAGE_FILE_HEADER 结构。此结构内含最基础的文件信息。和 COFF 比
较起来，它似乎没有做什么更改。除了是 PE 表头的一部份，它也出现在微软 32 位
编译器所产生的 COFF OBJs 档的最前端。IMAGE_FILE_HEADER 的字段如下。

WORD Machine

文件使用于哪一种 CPU 。下面是 CPU 识别码的定义：

Intel I386 0x14C
Intel i860 0x14D
MIPS R300 0x162
MIPS R400 0x166
DEC Alpha AXP 0x184
Power PC 0x1F0 （little endian ）
Motorola 68000 0x268
PA RISC 0x290 （Precision Architecture ）

译注：但是在我的 Visual C++ 4.2 WINNT.H 文件中，只有如下定义：
#define IMAGE_FILE_MACHINE_I386 0x14c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x162 //MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_ALPHA 0x184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x1F0 // IBM PowerPC Little-Endian

WORD NumberOfSections

EXE 的 OBJ 中的 sections 个数

DWORD TimeDateStamp

联结器产生此一文件的时刻。其格式是自从1969 年12 月31 日4:00 P.M. 之后的总秒数。

DWORD PointerToSymbolTable

COFF 符号表格的偏移位置。此字段只对 COFF 除错信息有用。PE 档支持数种除错格
式，所以除错器应该参考 data directory （稍后描述）字段中的
IMAGE_DIRECTORY_ENTRY_DEBUG 。

DWORD NumberOfSymbols

COFF 符号表格中的符号个数。请看前一字段。

WORD SizeOfOptionalHeader

一个可有可无的表头（出现在本结构之后）的大小。在 EXE 档中，这也就是
IMAGE_OPTIONAL_HEADER 的大小。在 OBJ 档中，微软说它总是 0 。然而，观察
KERNEL32.LIB ，有一个 OBJ 在其中，而它的这个值不是 0 。所以呢，微软的话总是要
打点折扣。

WORD Characteristics

描述此一文件的性质。一些比较重要的性质如下：

0x0001 文件中没有重定位（relocation ）
0x0002 文件是个可执行档（也就是说不是 OBJ 或 LIB ）
0x2000 文件是动态联结函数库，不是程序。

PE 表头的第三个成份是 IMAGE_OPTIONAL_HEADER 结构。对于 PE 档而言，这一
部份其实并不是可有可无。要知道，COFF 格式允许不同的生产者在标准的
IMAGE_FILE_HEADER 之后定义一个结构。IMAGE_OPTIONAL_HEADER 正是 PE 设
计者认为在基本的 IMAGE_FILE_HEADER 信息之外还需要的一些重要信息。

你并不需要知道 IMAGE_OPTIONAL_HEADER 的所有字段。最重要的两个字段是
ImageBase 和 Subsystem 。如果你喜欢，你可以跳着读或是整个略过下面这些字段说明。

WORD Magic

这值用来定义 image 的状态：
0x0107 一个 ROM image
0x010B 一个正常的（一般的）EXE image 。大部份 PE 档都含此值。

BYTE MajorLinkerVersion
BYTE MinorLinkerVersion

产生此 PE 文件之联结器版本。以十进制而非十六进制表示。例如 2.23 版。

DWORD SizeOfCode

所有 code sections 的总和大小。大部份文件只有一个 code section ，所以此字段通常也
就是 .text section 的大小。

DWORD SizeOfInitializedData

所有内含「初始值资料」的sections （但不包括code sections ）的总和大小。然而它似乎
并不包括 initialized data sections 在内。

DWORD SizeOfUninitializedData

这是「需要加载器将之付诸虚拟地址空间，但却不占用磁盘文件」的所有 sections 大小
总和。这些 sections 在程序激活时并不需要什么特别内容，所以才导至 Uninitialized data
这一称呼。未初始化的资料通常集中在 .bss section 中。

DWORD AddressOfEntryPoint

这是 image 开始执行的地址。这是一个RVA （Relative Virtual Address ），通常会落在 .text
section 。此字段对于 DLLs 或 EXEs 都适用。

DWORD BaseOfCode

这是一个 RVA （Relative Virtual Address ），表示文件中的 code section 从何开始。code
section 通常在data section 之前，在 PE 表头之后。微软联结器所产生的 EXEs 中，此
值通常为 0x1000 。Borland 的 TLINK32 则通常指定此值为 0x10000 ，因为预设情况下
它是以 64KB 为排列边界，不像微软的联结器采用 4K 为边界。

DWORD BaseOfData

这是一个 RVA （Relative Virtual Address ），表示文件中的 data section 从何开始。data
section 通常在code section 和 PE 表头之后。

DWORD ImageBase

一旦联结器产生了一个可执行档，它就假设这个文件将被映像到特定位置的内存中。
这个字段放的就是内存起始地址。假设出这样一个地址，联结器才能完成最佳化。如
果文件真的被加载到这个地址，程序代码就完全不需要修补。稍后我将对此有更多的讨论。
在 NT 3.1 EXE 中，预设的 image base 是 0x10000 ，DLL 则是 0x400000 。在 Windows
95 ，0x10000 不再适用，因为那将落在一个被所有行程共享的地址空间中。因而到了 NT
3.5 ，微软把预设的 Win32 EXE ImageBase 改为0x400000 。原来那些NT EXE 程序在
Windows 95 之中将需要比较长的加载时间，因为加载器必须重新定位。稍后我会详细讨
论所谓的「重新定位（relocation ）」。

DWORD SectionAlignment

一旦映像到内存中，每一个 section 保证从一个「此值之倍数」的虚拟地址开始。

DWORD FileAlignment

在 PE 档中，组成每一个 section 的原始资料（raw data ）保证是从一个「此值之倍数」
的虚拟地址开始。默认值是 0x200 ，如此一来 section 总是能够从磁盘的 sector 开始（因
为 sector 长度总是0x200 个字节）。这个字段相当于 NE 档中的 segment/resource
alignment 大小。与 NE 档不同的是，PE 档不会有上百个 sections ，所以因其特定排列
而浪费的空间不大。

WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion

使用此一可执行文件之操作系统的最小版本。这个字段与稍后即将出现的另一字段
"subsystem" 有类似的用途，因此颇令人困惑。Win32 文件的这两个字段通常指出 1.0 。

WORD MajorImageVersion
WORD MinorImageVersion

使用者自定的字段，允许你拥有不同版本的 EXE 或 DLL 。你可以利用联结器的
/VERSION 选项设定其值。例如：

LINK /VERSION:2.0 myobj.obj

WORD MajorSubsystemVersion
WORD MinorSubsystemVersion

使用此一可执行文件的子系统的最小版本。典型的数值是 4.0 ，代表 Windows 4.0 ，也就是
Windows 95 。

DWORD Reserved1

似乎总是 0 。

DWORD SizeOfImage

加载器必须在意的整个 image 大小。它的范围从 image base 开始，直到最后一个section
为止。最后一个 section 的尾端必需是 SectionAlignment 的倍数。

DWORD SizeOfHeaders

PE 表头以及 section table 的大小。各个 sections 的资料就从其后展开。

DWORD CheckSum

此文件的一个 CRC checksum 。就像微软的其它可执行档一样，此字段通常被忽略并被
设为 0 。然而，所有的 driver DLLs 、所有在开机时间加载的DLLs 、以及 server DLLs 都
必须有一个合法的 checksum 。其算法可以在 IMAGEHLP.DLL 中获得。
IMAGEHLP.DLL 的原始码可以在 Win32 SDK 中找到。

WORD Subsystem

此一可执行文件用来作为使用者接口的子系统。WINNT.H 定义了下列常数：

NATIVE=1 不需要子系统（例如驱动程序）
WINDOWS_GUI=2 在 Windows GUI 子系统中执行
WINDOWS_CUI=3 在 Windows 字符模式子系统中执行（也就是 console 应用程序）
OS2_CUI=5 在 OS/2 字符模式子系统中执行（也就是 OS/2 1.x 应用程序）
POSIZ_CUI=7 在 Posix 字符模式子系统中执行

WORD DllCharacteristics

一组旗标值，用来指示 DLL 的初始化函数（例如 DllMain ）在什么环境下被呼叫。这
个值总是 0 ，但操作系统却还是会在四个 events 发生时呼叫 DLL 的初始化函数。此值
的四个数值分别意义如下：

1 ：当 DLL 被加载一个行程的地址空间时呼叫之。
2 ：当一个线程结束时呼叫之。
4 ：当一个线程开始时呼叫之。
8 ：当 DLL 退出时呼叫之。

DWORD SizeOfStackReserve

线程初始堆栈的保留大小。然而并不是所有这些内存都被系统委派（committed ）。
此值预设为 0x100000 （1MB ）。如果你在程序中呼叫 CreateThread 并指定其堆栈大小
为 0 ，获得的线程就有一个与此值相同大小的的堆栈。

DWORD SizeOfStackCommit

一开始即被委派（committed ）给线程初始堆栈的内存数量。微软的联结器预设此一
字段为0x1000 （一个page ），Borland 的TLINK32 则把它设为 0x2000 （两个 pages ）。

DWORD SizeOfHeapReserve

保留给最初的 process heap 的虚拟内存数量。这个 heap 的 handle 可以利用
GetProcessHeap 获得。注意，并不是所有这些内存都已被委派（committed ）。

DWORD SizeOfHeapCommit

一开始即被委派（committed ）给process heap 的内存数量。此值预设为0x1000 个位
组。

DWORD LoaderFlags （在 Windows NT 3.5 中被注明为老旧字段）

这个字段似乎和除错有关。我从来没有看过哪一个可执行文件的这些位是设立的，也不
曾清楚知道联结器如何设定它们。下面是其可能的数值与意义：

1 ：在开始这个行程之前先引发一个断点（breakpoint ）？
2 ：在行程被加载之后引发一个除错器执行起来？

DWORD NumberOfRvaAndSizes

在 DataDirectory （下一字段）数组中的项目个数。目前的工具总是把此值设为 16 。

IMAGE_DATA_DIRECTORY DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]

数组一开始的元素内含可执行文件重要部位的RVA （Relative Virtual Address ）及大小。阵
列最后的一些元素目前还用不着。数组的第一个元素代表 exported function table （如果
有的话）的地址和大小，第二个元素代表 imported function table 的地址和大小，依此类
推。完整的列表请看 WINNT.H 中的定义。
译注：以下就是完整的列表。
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // Description String
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // Machine Value (MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
这个数组企图让加载器能够迅速在 image 中找到特定的 section ，不需要一一经过每一
个 sections 、比对名称、再继续寻找...。

大部份的数组元素描述一整个 section 资料，但 IMAGE_DIRECTORY_ENTRY_DEBUG
却只包含 .rdata section 中的一小部份。更多信息将在「The .rdata section 」一节中披露。


4.The Section Table

在 PE 表头和真正的 section 资料之间，横躺着一个 section table 。其中内含 image 的
每一个sections 的信息。sections 是以其起始地址来排列，而不是以其字母次序来排列。

现在是澄清 section 意义的时候了。在 NE 文件中，程序代码和资料储存在文件的不同节区
（segments ）之中。NE 表头的一部份是一个结构数组（译注：作者指的是 segment table ），
数组的每一个元素恰对应一个节区。每一个结构内含节区的一些信息，包括节区的型态
（code 或 data ）、大小、位置。PE 档的 section table 正类似于 NE 档的 segment table 。

但是，和 NE 档 segment table 不同，PE 档的 section table 并不为每一块code 或 data 存
放一个对应的 selector 值。取而代之的是，section table 的每一笔资料贮存了一个地址，
在那里，文件的原始资料被映像到内存。虽然sections 类似 32 位的 segments ，事
实上它们不是一个个的 segments ，而是行程虚拟地址空间中一块内存范围。

PE 和 NE 之间的另一个差异就是它们对于那些「不为程序所用，而为操作系统所用」
的资料的管理方式。我以两笔资料为例，一是 EXE 所用到的 DLLs 的列表，一是 fixup
table 的位置。在 NE 档中，资源并未被视为一个节区，甚至虽然有 selector 指派给资
源，但 NE 表头的 segment table 中并未存有资源的相关信息。资源被归类为一个个别
表格，接近 NE 表头的尾端。imported 函数信息和 exported 函数信息也不能担保有自
己的节区，它们只能够填塞在 NE 表头的边缘。

PE 档的故事就不同了。任何被视为重要的 code 或 data 都会被存放在一个如假包换的
section 之中。因此，imported 函数的信息被储存在自己一个 section 中，exported 函数
的信息也被储存在自己一个 section 中。relocation data 的情况亦复如此。程序或作业系
统所需的任何 code 或 data 都可能有它自己的 section 。

马上我要开始讨论各式各样的 sections ，但首先我必须描述操作系统管理sections 时所用
的资料。就在PE 表头之后是一个IMAGE_SECTION_HEADER 结构数组，该数组的元素
个数已记录在PE 表头之中（也就是 IMAGE_NT_HEADER.FileHeader.NumberOfSections
字段）。PEDUMP 程序可以输出section table 以及所有 section 的字段和属性。图8-2
显示PEDUMP 对于一个典型 EXE 文件的 section table 的输出。图8-3 显示的则是
PEDUMP 对于一个 OBJ 档的 section table 的输出。
01 .text VirtSize: 00005AFA VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00005C00
relocation offs: 00000000 relocations: 00000000
line # offs: 00009220 line #’s: 0000020C
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ

02 .bss VirtSize: 00001438 VirtAddr: 00007000
raw data offs: 00000000 raw data size: 00001600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE

03 .rdata VirtSize: 0000015C VirtAddr: 00009000
raw data offs: 00006000 raw data size: 00000200
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ

04 .data VirtSize: 0000239C VirtAddr: 0000A000
raw data offs: 00006200 raw data size: 00002400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE

05 .idata VirtSize: 0000033E VirtAddr: 0000D000
raw data offs: 00008600 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE

06 .reloc VirtSize: 000006CE VirtAddr: 0000E000
raw data offs: 00008A00 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
图8-2 一个典型的 EXE 档的 section table

01 .drectve PhysAddr: 00000000 VirtAddr: 00000000
raw data offs: 000000DC raw data size: 00000026
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: 00100A00
LNK_INFO LNK_REMOVE

02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000
raw data offs: 00000102 raw data size: 000016D0
relocation offs: 000017D2 relocations: 00000032
line # offs: 00000000 line #’s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

03 .data PhysAddr: 000016F6 VirtAddr: 00000000
raw data offs: 000019C6 raw data size: 00000D87
relocation offs: 0000274D relocations: 00000045
line # offs: 00000000 line #’s: 00000000
characteristics: C0400040
INITIALIZED_DATA MEM_READ MEM_WRITE

04 .text PhysAddr: 0000247D VirtAddr: 00000000
raw data offs: 000029FF raw data size: 000010DA
relocation offs: 00003AD9 relocations: 000000E9
line # offs: 000043F3 line #’s: 000000D9
characteristics: 60500020
CODE MEM_EXECUTE MEM_READ

05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000
raw data offs: 00004909 raw data size: 00000030
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #’s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
图8-3 一个典型的 OBJ 档的 section table

每一个 IMAGE_SECTION_HEADER 是 EXE 文件或 OBJ 文件中的一个 section 的完
整资料，格式如下：

BYTE Name[IMAGE_SIZEOF_SHORT_NAME]

这是一个8 八字节的 ANSI 名称（不是 unicode ），表示 section 名称。大部份 section
名称都以句点开始（例如 ".text"），但此非必要条件。你也可以自己命名：或是在组合
语言中以 segment directive 方式，或是在 C/C++ 之中以 #pragma code_seg 或 #pragma
data_seg 方式，都行。Borland C++ 的使用者则应该以 #pragma codeseg 为之。注意，如
果 section 名称全长 8 字节，那么就没有 NULL 结束符号。Borland C++ 4.0x 的
TDUMP 程序忽略了这一点，以至于在某些情况下会吐出一些垃圾。如果你是 printf 的
热爱者，可以使用 "%.8s" 打印格式。

union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;

这个字段的意义视其应用于 EXE 或 OBJ 而定。在 EXE 中它代表 code section 或 data
section 的虚拟大小，这是在它们被调整为「最接近之 file-alignment 倍数」之前的大小。
稍后出现的 SizeOfRawData 字段则是调整后的大小。有趣的是，Borland 的 TLINK32 把
这个字段和 SizeOfRawData 字段的意义颠倒了，却仍能正常运作。对于 OBJ 档，这个
字段则是代表 section 的实际地址。第一个 section 从地址 0 开始。上一个 section 的
实际地址再加 SizeOfRawData 值即为下一个 section 的起始地址。

DWORD VirtualAddress

在 EXE 中，这个字段代表加载器应该将 section 映像过去的 RVA 位置。为了计算
section 的真正起始地址，你必须将此值再加上基地址。微软的工具把第一个 section 的
此一字段设为 0x1000 。对于 OBJ 档，这个字段没有意义，总是为 0 。

DWORD SizeOfRawData

在 EXE 中，这个字段代表 section 大小被调整为「最接近之 file-alignment 倍数」后的
值。假设 file-alignment 大小为 0x200 ，VirtualSize 的值是 0x35A ，那么这个字段的值
就将会是 0x400 （字节）。在 OBJ 中，这个字段表示由编译器或组译器指定的真正
section 大小。换句话说，对于 OBJ ，这个字段等同于 SizeOfRawData 字段。

DWORD PointerToRawData

这是「以文件起头为基准」的偏移值，section 的原始资料可以在该处寻获。如果你自己
以内存映像的方式映像了一个 PE 档或 COFF 档（而不是经由操作系统的加载器），
这个字段就比 VirtualAddress 重要多了。因为这种情况下你对整个文件有一个完全的线
性映像，所以你必须根据此值找到 section 的资料，而不是根据 VirtualAddress 中的 RVA
（Relative Virtual Address ）值。

DWORD PointerToRelocations

在OBJs 中，这是「以文件起头为基准」的偏移值，用来指向section 的「重定位信息」。
每一个 OBJ section 的「重定位信息」紧跟在 section 资料之后。在 EXEs 中，这个栏
位（以及下一字段）没有意义，总是为 0 。当联结器产生一个 EXE ，它会决定大部份的
fixups （待修正记录），只剩下基地址的重定位地址以及 imported 函数的重定位地址，
留待加载时再解决。两份相关信息放在 base relocation section 和imported functions section
之中，所以 EXEs 不需要在每一个 section 之后又有「重定位信息」。

DWORD PointerToLinenumbers

这是行号表的偏移值（以文件起头为基准）。行号表把原始码行号和其被映像至内存
的地址扯上关系。在 CodeView 除错格式中，行号被储存为除错信息的一部份，然而在
COFF 除错格式中，行号在观念上和符号名称以及符号型态等信息是分开来的。通常，
只有 code sections （例如 .text 或CODE ）才有行号信息。在 EXE 文件中，行号信息被收
集放在文件的最尾端。在 OBJ 档中，行号表就放在每一个 section 的原始资料以及复位
位表格的后面。我将在本章的「COFF 除错信息」一节详细描述行号表的格式。

WORD NumberOfRelocations

重定位表格（由PointerToRelocations 指向）中的重定位项目个数。此字段只用于OBJ 档。

WORD NumberOfLinenumbers

行号表格（由 PointerToLinenumbers 指向）中的行号个数。

DWORD Characteristics

大部份程序员口中说的 "flags"，在 COFF/PE 格式中称为 "characteristics"。这个字段是
一组旗标值，用来表示 section 中的属性（code 或 data 、可读或可写…等等等）。完整
列表请看 WINNT.H 中的常数定义（IMAGE_SCN_XXX_XXX ）。最重要的旗标值列于
表8-1 。

有没有注意到什么东西遗失未提？首先，请注意，没有提到 PRELOAD 属性。NE 格式
允许你针对节区指定 PRELOAD 属性，使它能够在模块加载之后立刻被加载内存。OS/2
2.0 的 LX 格式也有类似属性，允许你指定最多 8 个 pages 为PRELOAD 。但是 PE 格
式没有！我们只好假设微软对于其在 Win32 平台上「有需要才加载（demand-paged
loading ）」的效率颇有自信。

表8-1 COFF section 属性旗标值
flag(旗标值) | 用途
0x00000020 | 这是一个 code section 。常与 0x80000000 （executable flag ）联用。
0x00000040 | 这个 section 内含初始化资料。几乎所有 sections （除了可执行者以 及 .bss ）都有此旗标设定。
0x00000080 | 这个 section 内含未初始化资料（例如 .bss section ）。
0x00000200 | 这个 section 内含文字说明或其它型态的信息。典型的例子就是编译器 制造出来的 .drectve section ，其中内含的是给联结器看的命令。
0x00000800 | 这个 section 的内容不应该被放到最终的 EXE 文件中。这是编译器或 组译器用来传递信息给联结器的场所。
0x02000000 | 表示这个 section 可以被拋弃，原因是一旦被加载后行程再也不需要用 到它。最常见的例子就是 base relocations section （.reloc ）
0x10000000 | 可共享的 section 。如果在 DLL 中，储存于此 section 的资料将可被所 有使用此 DLL 之行程共享。预设情况下并不设立此旗标，也就是说同 一 DLL 的每一个雇主（一个行程）有它们自己的一份资料拷贝。 以更技术的角度来说，一个 shared section 告诉内存管理器说，请为 此 section 设定 page mappings ，使所有用到这个 DLL 的行程们都能够 参考到相同的实际内存。为了做出一个可共享的section ，请在联结时 使用SHARED 属性，例如： LINK /SECTION:MYDATA,RWS... 告诉联结器说名为 MYDATA 的 section 应该是可读的（R ）、可写的 （W ）、可共享的（S ）。Borland C++ DLL 的 data section 预设使用此 一属性。
0x20000000 | 这个section 可执行。此属性通常和 0x00000020 旗标联用。
0x40000000 | 这个section 内容可读。EXE 的任何一个 sections 几乎都设立此旗标。
0x80000000 | 这个 section 允许被读入资料。如果EXE 之中没有任何一个 section 设 立此一旗标，加载器就应该把所有的内存映像页都做上「只读」或「仅 能执行」的记号。.data 和 .bss 通常会设立此旗标。
另一个不见踪影的东西是所谓的 page lookup table （页次查询表格）。
IMAGE_SECTION_HEADER 在 OS/2 LX 格式中的对等物并不直接指出一个 section 可
以在文件的何处找到，反而是，OS/2 LX 文件内含一个 page lookup table ，用来指定 section
之中某些pages 的属性和位置。PE 格式免除了这些东西，保证一个section 的资料一定连
续储存于文件之中。比较这两种格式，LX 允许较多的弹性，但是PE 比较简单也比较容
易使用 -- 在写过两种格式下的文件倾印器以及反组译器之后，我可以这么说。

另一个受欢迎的改变是，在原来的 NE 格式中，各个项目的位置只以一个 DWORD 表
示。NE 文件中几乎每一样东西的位置都以 sector 为单位。为了找到真正的文件偏移值，
你必须首先从 NE 表头中找出 alignment 的单位大小，把它转换为 sector 大小（通常不
是 16 就是 512 个字节）。然后你必须把每一个 sector 偏移值再乘以 sector 大小，
才是真正的文件偏移位置。PE 文件中各个项目的位置一律是以「距离内存映像地址起
始处」的偏移值表示，相当简单。结论是，PE 格式远比 NE 、LX 或 LE 格式简单易用 --
假设你会使用内存映像档的话。


5.常会遇到的Sections

关于 sections 的意义以及它如何定位，相信你已有个概念。现在我们要看看在 EXE 和
OBJ 档中的一些常见的 sections 。虽然我所列的并不是全部，但已经涵盖了你每天会接
触到（但也许你自己并不知道）的 sections 。排列次序是根据其重要性以及遭遇它们的频
繁度。

.text section

.text 内含所有一般性的程序代码。由于 PE 文件在 32 位模式下跑，并且不受约束于 16 位
元节区，所以没有理由把程序代码分开放到不同的 sections 中。联结器把所有来自 .OBJ
的 .text 集合到一个大的 .text 中。如果你使用Borland C++，其编译器制作出来的 code
section 名为 CODE 而不是 .text 。请看稍后的「Borland CODE 以及 .icode sections 」一
节。

我很惊讶地发现，在 .text 中除了编译器制作出来的码，以及runtime library 的码之外，
还有一些其它东西。在 PE 文件中，当你呼叫另一模块中的函数（例如 USER32.DLL 中
的 GetMessage ），编译器制造出来的 CALL 指令并不会把控制权直接传给 DLL 中的
函数，而是传给一个JMP DWORD PTR [XXXXXXXX]指令，后者也位于 .text 中。JMP 指
令跳到一个地址去，此地址储存在 .idata 的一个 DWORD 之中。这个DWORD 内含该
函数的真正地址（函数进入点），如图8-4 所示。

图8-4 一个 PE 档呼叫 imported function 。

沉思良久，我终于了解为什么 DLL 的呼叫需要以这种方式实现。把对同一个DLL 函数
的所有呼叫都集中到一处，加载器就不再需要修补每一个呼叫 DLL 的指令。PE 加载器
需要做的，就只是把 DLL 函数的真实地址放到 .idata 的那个 DWORD 之中，根本就
没有程序代码需要修补。这和 NE 档有极明显的差异。NE 档的每一个节区内含一串待修
正记录（fixup records ），如果某一节区呼叫同一个 DLL 函数 20 次，加载器就必须忙
碌 20 次，将函数地址拷贝到待修正记录之中。PE 档这种处理方式也有缺点：你不能够
以 DLL 函数的真正地址初始化一个变量。例如：

FARPROC pfnGetMessage = GetMessage;

是把 GetMessage 函数地址放到 pfnGetMessage 变量中。在 Win16 这没问题，在
Win32 ，变量中放的其实将是稍早我说过的JMP DWORD PTR [XXXXXXXX]指令的地址。如
果你根据这个函数指针来呼叫函数，事情会如你所预期。但如果你要以此指针读取
GetMessage 的前数个字节，幸运之神不会站在你那边。稍后我将在「PE 文件的输出
（exports ）」一节中再继续讨论这个主题。

在我写完本章的第一个版本之后，Visual C++ 2.0 推出了。它介绍另一种新的呼叫方式。
如果你看过 Visual C++ 2.0 的系统表头文件（例如 WINBASE.H ），你将看到和过去不同
的东西。在 Visual C++ 2.0 中，API 函数原型都有一个 __declspec(dllimport ）作为原型
的一部份。当你呼叫一个这样的函数，编译器不会在模块的另一个地方产生JMP DWORD PTR
[XXXXXXXX]指令，而是产生一个CALL DWORD PTR [XXXXXXXX]函数呼叫。XXXXXXXX位
址位于 .idata 内，作用与原先在JMP DWORD PTR [XXXXXXXX]指令中的地址相同。就我
所知，Borland C++ 4.5 编译器并没有这样的性质。

Borland CODE 以及 .icode sections

Borland C++ 4.5 编译器和联结器不能够使用 COFF OBJ 档，它们固守 Intel OMF 32 位
元格式。Borland 编译器当然可以吐出一个名为 .text 的section ，但它却选择 "CODE" 这
个名称。为了决定 PE 档中的一个 section 名称，Borland C++ 联结器（TLINK32.EXE ）
从OBJ 档中取出 section 名称并把它拦断为8 个字符（如果必要）。所以，Borland C++ 有
一个 CODE section 而不是 .text section 。

名称不同不算什么，更重要的不同存在于 Borland 工具联结出来的 PE 档中。稍早我说
过，所有对 OBJ 的呼叫都经由一个JMP DWORD PTR[XXXXXXXX] 指令。在微软的系统中，
这指令来自一个 import library 的 .text section 。也就是说联结器不需要知道如何产生这
个指令。import library 可视为「需要联结到 PE 档中」的更多的码和资料。

Borland 系统的处理方式就不一样，它比较类似 16 位 NE 文件所采行的方法。Borland
联结器所使用的 import library 真正只是函数名称和 DLL 名称的列表而已。TLINK32 有
责任决定哪一些待修正记录（fixups ）是针对外部 DLLs ，然后为它们产生JMP DWORD PTR
[XXXXXXXX] 指令。Borland C++ 4.0 的TLINK32 把它所产生的这个指令存放在 .icode
section 中，但是到了Borland C++ 4.02 ，TLINK32 又改变了，把所有这些 JMP 指令放
到 CODE section 中。

.data section

这是你的初始化资料的存放区。所谓初始化数据，包括全域变量和静态变量（global and
static variable ），在编译器时期就给定初值。它也包括字符串常数，像是 C/C++ 程序中的
"Hello World"。联结器把 OBJ 和 LIB 文件中所有的 .data 组合起来放到 EXE 文件
的 .data 。区域变量（local variable ）位于线程堆栈之中，不占用 .data 或 .bss 空间。

DATA SECTION

Borland C++ 以 DATA 作为其预设的资料区域。相当于微软编译器所制作的 .data 。

.bss section

这是任何未初始化的静态变量和全域变量的存放区。联结器把 OBJ 和 LIB 文件中所有
的 .bss 组合起来放到 EXE 文件的 .bss 。在 section table 中，.bss 的 RawDataOffset 栏
位总是为 0 ，表示这个 section 不占用文件的任何一点空间。TLINK32 并不吐出一
个 .bss ，它的作法是扩充 DATA section 的虚拟大小，以接纳未初始化的资料。

.CRT section

这是微软的 C/C++ runtime library （CRT ）所使用的另一个初始化的 data section 。这里所
放的资料用于「在 main 或 WinMain 之前执行的 static C++ 类别建构式」中。

.rsrc section

此处内含模块资源。早期的 NT ，16 位 RC.EXE 所输出的 .RES 档并不被微软的联
结器所了解，那个时候的 CVTRES 程序就是用来把一个 .RES 档转换为一个 COFF
OBJ ，把资源放到 OBJ 档的一个 .rsrc 之中。联结器于是就可以产生一个 resource OBJ 。
也就是说，联结器不需要知道任何有关于资源的事情。后来的微软联结器已经能够直接
处理 .RES 档。我将在「PE 文件的资源」一节中涵盖资源 section 的格式。

.idata section

这个 section 内含有关于「模块从其它 DLLs 中输入（import ）函数和资料」的相关资
讯。它相当于 NT 档的 module reference table 。关键性的差异是，每一个输入函数都被
列在这个 section 之中。如果要在 NE 文件中找出对等的信息，你必须深掘每一个节区的
原始内容的重定位资料。我将在「PE 文件的输入（imports ）」一节中涵盖 import table 的
格式。

.edata section

这是 PE 档输出函数（export function ）的相关信息。它的 NE 对等物是entry table 、resident
names table 和 nonresident names table 的组合。和 Win16 不同的是，很少有机会从一个
EXE 中输出一个函数出去，所以通常你只在 DLL 中才会看到 .edata 。Borland C++ 所
产生的EXE 是个例外，它总是有一个输出函数（__GetExceptDLLinfo ）给runtime library
的内部使用。

export table 的格式将于本章的「PE 文件的输出（exports ）」一节讨论。如果使用微软
工具，.edata 的资料来自 .EXP 档，但是联结器没有能力产生这个文件，必须依赖函数
库管理器 LIB32.EXE 扫描 OBJ 文件然后才产生 EXP 档，然后才能交给联结器。是的，
那是真的，EXP 档其实就是拥有不同扩展名的 OBJ 档罢了。使用 PEDUMP /S 观察 EXP
档，你可以看到其中的输出函数（export functions ）。

.reloc section

这个 section 内含一表格的 base relocations 。所谓 base relocation 是一个指令或初始化
变量的调整值。如果加载器没有办法把 EXE 或 DLL 文件加载到预设的地址的话，就
必须做这样的调整；否则加载器可以忽略「重定位」这件事情。

如果你希望加载器总是能够把 image 加载到预定的基地址，你可以使用 /FIXED 选
项，告诉联结器剥除本项信息。虽然这可以节省 EXE 的文件空间，却可能使得EXE 档

没办法在其它 Win32 平台上执行。例如，你为 NT 开发了一个 EXE ，基地址为
0x10000 。如果你告诉联结器把这信息剥除，这个 EXE 就没有办法在 Windows 95 上跑，
因为 0x10000 不适用（Windows 95 的最低加载地址是0x400000 ，也就是 4MB ）。

注意一点，编译器所产生的JMP 和CALL 指令，其所使用的 offset 值是与该指令成相对
地址关系，而不是真正的 32 位平滑节区的 offset 值。如果 image 被加载到一个并非
联结器指定的基地址去，JMP 和 CALL 指令不需修改，因为它们用的是相对寻址。
也就是说，其实没有如你想象中那么多的重定位动作要做。只有使用32-bit offset 的指令
才需要重定位动作。假设你有下面的全域变量宣告：

int i;
int *ptr = &i;

如果联结器设定基地址是 0x10000 ，变量 i 的地址是 0x12004 。在被用来存放 ptr 的
内存中，联结器将写入 0x12004 ，因为那是变量 i 的地址。如果加载器为了某种理由
把文件加载到 0x70000 处，i 的地址将是 0x72004 ，然而，预先初始化过的 ptr 值变成
错误值，因为 i 现在的位置已经提升了 0x60000 。

这就是需要重定位信息参一脚的场合了。.reloc 用来表示「联结器所假设的加载地址」
和「真正的加载地址」之间的差异。我将在「PE 档的 Base Relocations 」一节有比较详
细的讨论。

.tls section

当你使用编译器的 "__declspec(thread)" 性质，你定义的资料并没有进入 .data 或 .bss 之
中，倒是有一份拷贝进入 .tls 之中。.tls 的名称是因为thread local storage 而来，和 TlsAlloc
函数家族有密切关系。

为了简单描述所谓的 thread local storage ，请把它想象成「让每一个线程拥有各自的全
域变量」的一种方法。也就是说，每一个线程可以拥有它自己的一组静态资料，使用
这些资料的程序代码，不需在意现在是哪一个线程正在执行。假设某程序有数个线程，
处理相同的工作。也因此执行相同的码。如果你宣告一个 tls ，像这样：

__declspec(thread) int i = 0; // this is a global variable declaration

每一个线程将因此拥有变量 i 的一个副本。

你可以明白地在执行时期索求并使用 tls ，相关函数是 TlsAlloc 、TlsSetValue 、TlsGetValue
等（第３ 章对于 TlsXXX 函数的描述比较详细）。通常，以 __declspec(thread) 在程序中
宣告你的资料，比使用 TlsAlloc 简单得多。

这里有一个坏消息。在 NT 和 Windows 95 中，tls 机制不能够有效运作 -- 如果运作对
象是以 LoadLibrary 动态加载的 DLL 。至于在一个 EXE 或是一个隐式加载(implicitly
loaded ，译注）的DLL 之中，每一件事情都没问题。如果你不能够以隐晦方式加载 DLL ，
但又需要让每一个线程有自己的资料，那你只好使用 TlsAlloc 和 TlsGetValue 。注意，
每一线程真正的内存区块并不是放在 .tls section 中，也就是说，当切换线程的时
候，内存管理器并不改变「实际映像至模块之 .tls section 」的内存。.tls 内只不过是
一些资料，用来初始化真正的线程专属区块。初始化动作是靠操作系统与runtime library
的合作，过程之中需要另外一些储存在 .rdata 之中的资料：TLS directory 。
译注：如果程序与 DLL 的 import library 联结，我们说这是 implicitly link ，并导至 DLL 被 implicitly loaded 。如果程序没有与 DLL 的 import library 联结，而是在需要时（执 行时期）呼叫 LoadLibrary 和 GetProcAddress 以取得函数地址，再呼叫之，我们称此为 explicitly linked ，并导至 DLL 被 explicitly loaded 。
.rdata section

.rdata 至少有四个用途。第一，在被微软联结器产生的 EXEs 之中，.rdata 内含debug
directory （OBJ 档中并没有 debug directory ）。而在 TLINK32 所产生的EXEs 之中，debug
directory 是一个名为 .debug 的 section 。debug directory 是一个由
IMAGE_DEBUG_DIRECTORY 结构所组成的数组。这些结构持有文件之中各种除错资
讯的型态、大小、位置。除错信息可能有三种型态：CodeView 、COFF 、FPO 。图8-5
显示 PEDUMP 对一典型的 debug directory 的输出结果。

Type | Size | Address | FilePtr | Charactr | TimeData | Version
COFF | 000065C5 | 00000000 | 00009200 | 00000000 | 2CF83F3D | 0.00
(unknown) | 00000114 | 00000000 | 0000F7C8 | 00000000 | 2CF83F3D | 0.00
FPO | 000004B0 | 00000000 | 0000F8DC | 00000000 | 2CF83F3D | 0.00
CODEVIEW | 0000B0B4 | 00000000 | 0000FD8C | 00000000 | 2CF83F3D | 0.00
图8-5 一个典型的 debug directory 。

debug directory 并不一定会在 .rdata 的起始处被发现。要找到它，你必须使用data directory
的第 7 笔资料（IMAGE_DIRECTORY_ENTRY_DEBUG ）。还记得吗，data directory 位
于 PE 表头的尾端。为了确定微软联结器所做出来的debug directory 的项目个数，请把
debug directory 的大小（可从debug directory 的 "size" 字段获得）除以
IMAGE_DEBUG_DIRECTORY 的结构大小。至于 TLINK32 则是把 debug directories 的
真正数量记录在 "size" 字段中，而不是字节总长度。PEDUMP 可以处理这两种情况。

.rdata 的第二个有用部份是 description string 。如果你在程序的 .DEF 档中指定
DESCRIPTION ，被指定的字符串就会出现在 .rdata 之中。在 NE 档中，description string 总
是 nonresident names table 的第一个项目。description string 主要是用来设定一个有用的
字符串，用以描述这个文件。不幸的是我还没有发现什么好方法来找到它。我曾经看过有
些PE 档的description string 放在debug directory 之前，有些却在debug directory 之后。

.rdata 的第三个用途是为了 OLE 程序设计所需的 GUIDs 。UUID.LIB 内含一系列的 128
位 GUIDs ，当作 interface IDs 。这些 GUIDs 都放在EXE 或 DLL 的 .rdata 中。

.rdata 的最后一个用途是用来放置 TLS （Thread Local Storage ）的 directory 。TLS directory
是一个特殊数据结构，被编译器的runtime library 使用，以便能够透明化地提供 TLS 给
程序中宣告的变量。TLS directory 的格式可以在 MSDN （Microsoft Developer Network ）
光盘片中找到："Portable Executable and Common Object File Format"。我们对 TLS directory
的主要兴趣是指向资料（用来初始化每一个 tls 区块）的起头和结尾的指针，TLS directory
的 RVA （Relative Virtual Address ）可以在 PE 表头的 data directory 的
IMAGE_DIRECTORY_ENTRY_TLS 项目中获得。至于真正用来初始化 TLS 区块的资
料可以在 .tls section 中找到。

.debug$S 和 .debug$T sections

.debug$S 和 .debug$T 只出现于 COFF OBJs 之中，内含 CodeView 的符号和型态资
讯。看来十分奇怪的 section 名称系衍生自前一版微软编译器的节区名称（$$SYMBOLS
和 $$TYPES ）。.debug$T 的唯一目的是为了放置 .PDB 档（内有项目中所有 OBJs 的
CodeView 型态信息）的路径名称。联结器利用 .PDB 为 EXE 档产生出一部份的
CodeView 信息。

.drectve section

这个 section 只出现在 OBJ 档，内含联结器命令列参数的文字表达。例如，在微软的
Visual C++ 编译器，下面字符串一定会出现在 .drectve 中：

-defaultlib:LIBC -defaultlib:OLDNAMES

当你在程序代码中使用 __declspec(export) ，编译器会制造出命令列上的对应东西，放
在 .drectve 之中（例如 export:MyFunction ）。

含有 $ 的sections （只针对OBJs/LIBs ）

在 OBJ 档中，名称含有 $ 的 sections （例如 .idata$2 ）将被联结器特别对待。联结器把
所有拥有相同名称（直至 $ 字符）的 sections 组合成为单一一个section 。例如，如果
联结器遭遇 .idata$2 和 .idata$6 ，它会把它们整合为一个 .idata 。

被整合的 sections 的次序是以 $ 之后的字符为准。联结器以字母顺序排列之，所
以 .idata$2 在 .idata$6 之前。.idata$A 则在 .idata$B 之前。

那么到底带有 $ 的 section 做什么用？最普遍的用法就是 import library 利用它们来存
放最终的 .idata （import section ）的各部份资料。这可有趣了，联结器本身并不需要从头
产生 .idata ，最终的 .idata 是由 OBJ 和 LIB 各贡献一部份而来。

杂项的sections

有时候我会从PEDUMP 的输出中看到其它一些sections 。例如Windows 95 的GDI32.DLL
内含一个名为 _GPFIX 的 data section ，我们推测它大概与GP fault 的处理有关。

这有双重意义。第一，不要以为你只能使用编译器或组译器提供的标准 sections 。若有需
要，别犹豫不决。在微软的 C/C++ 编译器中，你可以使用 #pragma code_seg 和 #pragma
data_seg 。Borland 的使用者则可以使用 #pragma codeseg 和 #pragma dataseg 。若是组合
语言，你只要产生一个 32 位节区并给予不同于「标准 sections 」的名称即可。TLINK32
会把同类别的 code segments 组合在一起，所以你要不就得为每一个 code segment 指定
一个类别名称，要不就关闭 "code segment packing" 这个性质。


6.PE 文件的输入（imports ）
译注：我把 import 译为「输入」，import function 译为「输入函数」；export 译为「输 出」，export function 译为「输出函数」。
稍早我曾说过，呼叫外部的 DLLs 其实并不是直接呼叫 DLL 本身，而是跳到一块存放
着JMP DWORD PTR [XXXXXXXX]指令的区块去（可能放在 .text 或是 .icode ）。如果你在
VC++ 中使用 __declspec(dllimport)，函数呼叫就会变成CALL DWORD PTR [XXXXXXXX]。
不论哪一种情况，JMP 或 CALL 指令中的地址都存放在 .idata 。JMP 或 CALL 指令会
把控制权转给该地址。如果你仍然不清楚这一部份，请回头看看图8-4 。

在被加载内存之前，存放在 PE 档的 .idata 中的信息是给加载器用来决定函数地址并
修补它们，以便完成 image 用的。而在被加载之后，.idata 内含的是指针，指向 EXE/DLL
的输入函数。请注意，这里我所讨论的所有数组和结构都内含在 .idata 之中。

.idata section （或称为 import table ）是以一个 IMAGE_IMPORT_DESCRIPTOR 数组开
始。被PE 档隐晦联结（implicitly link ）的 DLLs 都会在此有一个对应的
IMAGE_IMPORT_DESCRIPTOR 结构。最后一个 IMAGE_IMPORT_DESCRIPTOR 结
构的内容全部为 NULL ，以此做为结束符号。IMAGE_IMPORT_DESCRIPTOR 的格式
描述于下。

DWORD Characteristics/OriginalFirstThunk

这是一个偏移值（一个RVA ，Relative Virtual Address ），指向一个 DWORD 数组。每
一个 DWORD 事实上是一个 IMAGE_THUNK_DATA union ，对应于一个输入函数。稍
后我会描述IMAGE_THUNK_DATA DWORD 的格式。

DWORD TimeDateStamp

这是文件的产生时刻。通常此栏为 0 。然而微软的 BIND 程序可以将此一
IMAGE_IMPORT_DESCRIPTOR 所对应的 DLL 的产生时刻写到这里来。

DWORD ForwarderChain

这个字段关系到所谓的 forwarding （转交），意味一个 DLL 函数再参考（呼叫、利用）
另一个 DLL 。例如在 Windows NT 中，KERNEL32.DLL 将它的某些输出函数转交给
NTDLL.DLL 。应用程序可能以为它呼叫 KERNEL32.DLL ，而事实上呼叫的是
NTDLL.DLL 。这个字段内含一个索引，指向 FirstThunk 数组（稍后叙述）。被这个索
引所指定的函数就是一个转交函数。不幸的是，函数如何转交并未在微软的文件中公开。
关于转交，请看稍后「输出函数的转交（Export forwarding ）」一节。

DWORD Name

这是一个RVA （Relative Virtual Address ），指向一个以 null 为结束字符的 ASCII 字符串，
内含imported DLL 的名称。

PIMAGE_THUNK_DATA FirstThunk

这是一个 RVA ，指向一个 DWORDs （IMAGE_THUNK_DATA ）数组。大部份情况下，
该 DWORD 被解释为一个指向 IMAGE_IMPORT_BY_NAME 结构的指针。然而，以函
式序号（而非函数名称）输入，也是有可能的。

IMAGE_IMPORT_DESCRIPTOR 结构中，最重要的部份是 imported DLL 的名称以及两
个IMAGE_THUNK_DATA DWORDs 数组。每一个 IMAGE_THUNK_DATA DWORDs
对应到一个输入函数。在 EXE 文件中，两个数组（分别由 Characteristics 和FirstThunk 栏
位指向）平行存在，并且都以 NULL 为结束符号。

为什么需要两个平行数组呢？第一个数组（由 Characteristics 指向）从不被修改，有时
候它又被称为 hint-name table 。第二个数组（由 FirstThunk 指向）则被加载器改写。载
入器一一检阅每一个 IMAGE_THUNK_DATA 并找出它所记录的函数的地址，然后把位
址写入 IMAGE_THUNK_DATA 这个 DWORD 之中。

稍早我曾经说过对 DLL 函数的呼叫会导至一个JMP DWORD PTR [XXXXXXXX]指令。
[XXXXXXXX]事实上参考到 FirstThunk 数组中的一个元素。由于这个
IMAGE_THUNK_DATA 数组内容已被加载器改写为输入函数的地址，所以它又被称做
Import Address Table （IAT ）。图8-6 显示这两个数组。

对于 Borland 使用者，上述叙述需要一点点修改。TLINK32 所产生的 PE 档遗漏了一
个数组：Characteristics （也就是 hint-name array ）总是 0 。显然Win32 加载器根本不需
要它。

图8-6 PE 档如何处理输入函数（imported functions ）

故事即将结束，但当我撰写 PEDUMP 时遭遇了一个有趣的问题。在「从不停止追求最
佳化」的脚步中，微软把 Windows NT system DLLs 的IMAGE_THUNK_DATA 数组也
给最佳化了。于是，IMAGE_THUNK_DATA 不再内含「能够找到输入函数」的信息，
而是内含输入函数的地址。换句话说加载器不再需要辛苦寻找函数地址并改写数组内容，
Win32 SDK 的 BIND 可以执行此一最佳化动作。不幸的是这会造成 PE 倾印工具程序
一些问题。可能你会说『嘿 Matt ，为什么你不使用 hint-name table 呢？』主意是不错，
问题是 Borland 文件没有提供 hint-name table 。好，别为我担心，PEDUMP 可以处理所
有的情况，但程序代码无可避免会有些零乱，不够优雅。

由于 import address table 通常是一个可写区域，所以要拦截 EXE 或 DLL 对其他DLL
的呼叫就相当容易了。你只要修改 import address table 中指向欲拦截函数的那个数组元
素即可。不需要修改呼叫端或被呼叫端的程序代码。我在第 10 章建立了一个 Win32 API spy
程序，就是依赖这个性质。

微软所产生的 PE 档并不是完全由联结器合成。事实上，呼叫 DLL 函数所需的所有资
讯都是由相关的 DLL import library 提供。当你对 DLL 做联结动作，函数库管理器
（LIB.EXE ）扫描所有 OBJ 文件并产生一个 import library 。它可不同于 16 位 NE 文件
的 import library 。这个由 32 位 LIB.EXE 产生的import library 有 .text section 和数
个 .idata$ sections 。.text 内含JMP DWORD PTR [XXXXXXXX]指令，每一个这样的程序代码
段落有一个名称记录在 OBJ 文件的符号表格中。符号名称与 DLL 输出函数的名称相同（例
如 _DispatchMessage@4 ）。

import library 中的一个 .idata$ section 内含上述 JMP 指令中的 DWORD 。另一个 .idata$
section 提供一个空间给 "hint ordinal"（后面紧跟着输入函数的名称）。这两个字段组成
一个 IMAGE_IMPORT_BY_NAME 结构，当你使用这个 import library 联结 PE 档时，
这个 import library 的 sections 会被加到来自 OBJ 的其它 sections 之中。由于这个「JMP
指令区段」有着与输入函数相同的名称，所以联结器认为这个指令区段其实就是输入函
式。

除了提供输入函数的「JMP 指令区段」外，import library 还提供 PE 档的 .idata section
（或称为 import table ）。那些片段都是来自于 .idata$ sections 。总之，联结器并不知道
输入函数（imported functions ）和「位于其它 OBJ 之函数」之间的差异。联结器只是遵
循其预先调整好的规则，产生并组合 sections ，然后每一件事情自然就会各就各位。

IMAGE_THUNK_DATA DWORD

一如我稍早所说，每一个 IMAGE_THUNK_DATA DWORD 都关系到一个输入函数。这
个DWORD 的解释视文件被加载与否，以及函数被以名称输入或以序号输入而定。以名
称输入是比较共同的方式。

当一个函数以序号输入，EXE 档的 IMAGE_THUNK_DATA DWORD 中的最高位
（0x80000000 ）设立。例如，考虑一个 IMAGE_THUNK_DATA ，其值为 0x80000112 ，
放在GDI32.DLL 数组中。表示这IMAGE_THUNK_DATA 将输入GDI32.DLL 中的第 112
号输出（exported ）函数。以序号输入的一个问题是，微软并未让Win32 API 函数的序
号在 Windows NT 、Windows 95 和 Win32s 中保持一致。

如果函数以名称输入，IMAGE_THUNK_DATA DWORD 就内含一个 RVA （Relative
Virtual Address ），指向IMAGE_IMPORT_BY_NAME 结构。这个结构非常简单：

WORD Hint

对此字段的最佳揣测就是输入函数（imported function ）的输出序号（exported ordinal ）。
加载器利用它做为一个建议起始值，用于对函数的二元搜寻法上面。

BYTE[?]

这是一个 ASCII Zstring （译注），内放输入函数的名称。

IMAGE_THUNK_DATA DWORD 的最后意义（译注：别忘了它是一个 union ）是在 PE 档
被加载后才决定的，Win32 加载器使用 IMAGE_THUNK_DATA 的原始内容（可能是
函数名称也可能是函数序号）来寻找输入函数的地址。然后，加载器就以获得的地址改
写 IMAGE_THUNK_DATA 的内容。
译注：所谓 Zstring ，是指字符串最后以 Null 为结束符号。C 语言的字符串便是 Zstring 。所 谓 Lstring ，是指字符串最前有一个字符计数器。Pascal 语言的字符串便是 Lstring 。
把IMAGE_IMPORT_DESCRIPTORs 和IMAGE_THUNK_DATAs 放在一
起

你已经看过了 IMAGE_IMPORT_DESCRIPTOR 和 IMAGE_THUNK_DATA 两种结构。
把一个 EXE 或 DLL 所用到的输入函数做出一份列表来已经不是什么困难的事，你只
要一一察看 IMAGE_IMPORT_DESCRIPTORs 表格元素（每一个元素对应一个 DLL ），
然后找出其相关的 IMAGE_THUNK_DATA DWORD 并适当解释它的意义即可。图8-7
显示PEDUMP 的输出结果，未列出函数名称者，表示是以序号输入。

Imports Table:
USER32.dll
Hint/Name Table: 0001F50C
TimeDateStamp: 2EB9CE9B
ForwarderChain: FFFFFFFF
First thunk RVA: 0001FC24
Ordn Name
268 GetScrollInfo
133 DispatchMessageA
333 IsRectEmpty
431 SendMessageCallbackA
255 GetMessagePos
// Rest of table omitted...

GDI32.dll
Hint/Name Table: 0001F178
TimeDateStamp: 2EB9CE9B
ForwarderChain: FFFFFFFF
First thunk RVA: 0001F890
Ordn Name
31 CreateCompatibleDC
309 SetTextColor
276 SetBkColor
99 ExtTextOutA
9 BitBlt
// Rest of table omitted...

MPR.dll
Hint/Name Table: 0001F2F0
TimeDateStamp: 2EAF4824
ForwarderChain: FFFFFFFF
First thunk RVA: 0001FA08
Ordn Name
26 35 34 33 55
// Rest of table omitted...

KERNEL32.dll
Hint/Name Table: 0001F1CC
TimeDateStamp: 2EB9DA61
ForwarderChain: FFFFFFFF
First thunk RVA: 0001F8E4
Ordn Name
636 SetEvent
348 GetTimeFormatA
375 GlobalGetAtomNameA
301 GetProcAddress
572 RtlZeroMemory
// Rest of table omitted...

COMCTL32.dll
Hint/Name Table: 0001F0DC
TimeDateStamp: 2EAD4AE5
ForwarderChain: FFFFFFFF
First thunk RVA: 0001F7F4
Ordn Name
152
21 ImageList_Draw
354 352
28 ImageList_GetIconSize
// Rest of table omitted...

ADVAPI32.dll
Hint/Name Table: 0001F0A0
TimeDateStamp: 2EA8A148
ForwarderChain: FFFFFFFF
First thunk RVA: 0001F7B8
Ordn Name
149 RegQueryValueA
119 RegCloseKey
142 RegOpenKeyExA
131 RegEnumKeyExA
126 RegDeleteKeyA
// Rest of table omitted...

图8-7 一个典型的 import table （来自 EXPLORER.EXE ）


7.PE 文件的输出（exports ）

「输入一个函数」的相反动作就是「输出一个函数」给其它的 EXE 或 DLL 使用。PE 档
把它的输出函数相关信息存放在 .edata 。一般而言，微软的 LINK.EXE 所产生的PE EXE
档并不输出任何东西，所以它们也就不会有 .edata 。TLINK32.EXE 则输出一个符号，所
以它们有一个 .edata 。大部份的 DLLs 都会输出一些函数，并因此有.edata 。.edata （或
称为 export table ）中的主要成份是一个表格，内含函数名称、进入点地址、输出序号。
NE 档中与此 export table 对等的东西是 entry table 、resident names table 和 nonresident
names table 。在 NE 文件中，这些表格是 NE 表头的一部份而不是节区或资源的一部份。

在 .edata 一开始处是一个 IMAGE_EXPORT_DIRECTORY 结构，之后紧跟着的是由该
结构的某个字段所指向的资料。IMAGE_EXPORT_DIRECTORY 结构长像如下：

DWORD Characteristics

此字段没有用途，总是为 0 。

DWORD TimeDateStamp

文件被产生的时刻。

WORD MajorVersion
WORD MinorVersion

这两个字段没有用途，总是为 0 。

DWORD Name

一个 RVA （Relative Virtual Address ）值，指向一个 ASCIIZ 字符串（DLL 名称，例如
MYDLL.DLL ）。

DWORD Base

被此模块输出的函数的起始序号。如果输出函数的序号是 10, 11, 12 ，此字段即为 10 。

DWORD NumberOfFunctions

AddressOfFunctions 数组（稍后描述）中的元素个数。此值同时也是输出函数的个数。
通常这个值和 NumberOfNames 字段相同，但也可以不同。

DWORD NumberOfNames

AddressOfNames 数组（稍后描述）中的元素个数。此值表示以名称输出的函数个数。通
常（但不总是）和输出函数的总数相同。

PDWORD *AddressOfFunctions

这是一个 RVA （Relative Virtual Address ）值，指向一个由函数地址所构成的数组。我所
谓的函数地址是指此一模块中的每一个输出函数的进入点的 RVA 值。

PDWORD *AddressOfNames

这是一个 RVA 值，指向一个由字符串指针所构成的数组。字符串的内容是此一模块中的每
一个「以名称输出的输出函数」的名称（译注，天啊，有点拗口）。

PWORD *AddressOfNameOrdinals

这是一个 RVA 值，指向一个 WORD 数组。WORD 的内容是此一模块中的每一个「以
名称输出的输出函数」的序号。别忘了要加上 Base 字段中的起始序号。

Export table 的布局有点诡异。稍早我就说过，输出一个函数的基本需求就是函数地址和
输出序号。如果你是以名称输出，那么还需要一个函数名称。你会以为PE 格式的设计
者应该把这三样信息放在一个结构中，然后设计一个由此结构组成的数组。结果，你却
必须在三个分离的数组中把它们找齐！

最重要的一个数组是 AddressOfFunctions 所指的数组。这是一个 DWORD 数组，每一
个 DWORD 是一个输出函数的 RVA 地址。每一个输出函数的序号都与函数在此数组
中的位置一致。假设起始序号为1 ，那么输出序号为1 者其函数地址在此数组中排在第
一个位置。输出序号为2 者其函数地址在此数组中则排在第二个位置，依此类推。

关于 AddressOfFunctions 数组，有两件重要的事情需记住。第一，输出序号必须加上 Base
值。如果 Base 是 10 ，则此数组之第一个 DWORD 应该对应序号 11 ，第二个 DWORD
应该对应序号 12 ，依此类推。第二，序号可以跳号。如果此一模块输出两个函数，序号
为 1 和 3 ，AddressOfFunctions 数组还是需要三个元素。数组中的元素如果不对应到一
个输出函数，那么元素内容应该是 0 。

当 Win32 加载器欲修正（fixs up ）一个函数呼叫，而此函数系以序号输入，它的工作其
实很少。加载器只要以序号做为数组索引，找到正确的 DLL 中的AddressOfFunctions 阵
列的元素即可。当然，加载器还需考虑起始序号是否为 1 。

然而大部份函数是以名称（而非序号）输出。这时候前面说的另两个结构才准备发挥它
们的效用。AddressOfNames 和 AddressOfNameOrdinals 两个数组允许加载器根据函数
名称快速找到对应的序号。这两个结构有相同的元素个数（记录在
IMAGE_EXPORT_DIRECTORY 的 NumberOfNames 字段中）。AddressOfNames 是由
函数名称的指针所构成的数组，AddressOfNameOrdinals 数组则是 AddressOfNames 阵
列索引值所构成的一个数组。

让我们看看 Win32 加载器如何修正（fixs up ）对于一个「以名称输出」的函数的呼叫。
首先，加载器搜寻 AddressOfNames 数组所指的各个字符串，假设在第３ 个元素中找到目
标。接下来加载器利用它获得的这个索引值（3 ）寻找 AddressOfNameOrdinals 数组中的
对应元素（第３ 个元素），获得的 WORD 值代表 AddressOfFunctions 数组索引。最后
一步就是到 AddressOfFunctions 数组中以此索引找出函数地址。

在 C 语言中，你可以这样获得一个「以名称输出」的函数的地址：

WORD nameIndex = FindIndexOfString( AddressOfNames, "GetMessageA" );
WORD functionIndex = AddressOfNameOrdinals[ nameIndex ];
DWORD functionAddress = AddressOfFunctions[ functionIndex - OrdinalBase ];

图8-8 显示 export section 的格式及其中的三个数组。图8-9 显示 PEDUMP 对
KERNEL32.DLL 观察得到的 export section 。

图8-8 一个典型的 EXE export table

Name: KERNEL32.dll
Characteristics: 00000000
TimeDateStamp: 2C4857D3
Version: 0.00
Ordinal base: 00000001
# of functions: 0000021F
# of Names: 0000021F

Entry Pt Ordn Name
00005090 1 AddAtomA
00005100 2 AddAtomW
00025540 3 AddConsoleAliasA
00025500 4 AddConsoleAliasW
00026AC0 5 AllocConsole
00001000 6 BackupRead
00001E90 7 BackupSeek
00002100 8 BackupWrite
0002520C 9 BaseAttachCompleteThunk
00024C50 10 BasepDebugDump
// Rest of table omitted...
图8-9 PEDUMP 对 KERNEL32.DLL 观察获得的 export section 。

附带一提，如果你倾印 system DLLs （例如 KERNEL32.DLL 和 USER32.DLL ）的输出
函数，你会看到许多函数只有最后一个字符的差别。例如 CreateWindowExA 和
CreateWindowExW 。这是「unicode 透明化支持」的一种方式。以 'A' 结束的是 ASCII
（或 ANSI ）的兼容函数，以 'W' 结束的则是其 unicode 版本。你的程序不需要明白指
出使用哪一种版本，WINDOWS.H 中有许多 #ifdef 编译前置动作会处理。下面就是 NT
的 WINDOWS.H 的一小片段：

#ifdef UNICODE
#define DefWindowProc DefWindowProcW
#else
#define DefWindowProc DefWindowProcA
#endif //!UNICODE
译注：Windows 95 亦复如此处理。但 Windows 95 并不支持 unicode 。
输出函数的转交（Export forwarding ）

有时候会发生「经由一个 DLL 输出一个函数，而此函数实际上位于另一个 DLL 之中」
的情况。这时候 DLL 可以将此函数转交（forward ）给另一个 DLL 。当 Win32 加载器
遇到这样的函数，它必须把函数呼叫修正为对真正含有函数码的那DLL 的呼叫。

举个例子就清楚多了。下面是 PEDUMP 对 Windows NT 3.5 KERNEL32.DLL 的输出列
表：

00043FC3 335 HeapAlloc (forward -> NTDLL.RtlAllocateHeap)
00044005 339 HeapFree (forward -> NTDLL.RtlFreeHeap)
0004402C 341 HeapReAlloc (forward -> NTDLL.RtlReAllocateHeap)
0004404D 342 HeapSize (forward -> NTDLL.RtlSizeHeap)
0004466F 442 RtlFillMemory (forward -> NTDLL.RtlFillMemory)
00044691 443 RtlMoveMemory (forward -> NTDLL.RtlMoveMemory)
000446AF 444 RtlUnwind (forward -> NTDLL.RtlUnwind)
000446CD 445 RtlZeroMemory (forward -> NTDLL.RtlZeroMemory)

其中的每一个函数都转交给 NTDLL 中的另一个函数。因此，呼叫 HeapAlloc 事实上是
呼叫 NTDLL.DLL 中的 RtlAllocateHeap ，呼叫 HeapFree 事实上是呼叫 NTDLL.DLL 中
的 RtlFreeHeap 。

好，你如何告诉联结器说某个函数需要转交呢？唯一可能的地方是export table （.edata ）
中的函数地址。这种情况下，所谓的函数地址将放的是一个 RVA ，指向一个字符串，内含
转交目标的 DLL 名称和函数名称。例如，在上面的 PEDUMP 输出中我们看到
HeapAlloc 的 RVA 是 0x43FC3 ，而KERNEL32.DLL 的 0x43FC3 偏移处正位于 .edata
之中。其值指向一个字符串，内容为 "NTDLL.RtlAllocateHeap"。PEDUMP 中的
DumpExprtsSection 函数可以示范转交函数的鉴定方法。

虽然转交输出看起来不赖，微软并没有公开说明如何在你自己的 DLL 中使用这个性质。
而且到目前为止我也只在 Windows NT 的 KERNEL32.DLL 中看到这种用法。虽然我没
有在 Windows 95 的任何 DLLs 中看到使用这个性质，Windows 95 加载器还是有支持
这项特性（我曾在第３ 章展示过）。


8.PE 档的资源

和 NE 做比较，在 PE 档中寻找资源是比较麻烦的。资源的格式并没有什么改变，但是
你必须在一个复杂的阶层架构中移来移去，才能找到目标。

航行于资源目录的阶层架构中，有点像是航行在磁盘之中。一个根目录，许多子目录，
子目录又有许多子目录。你可能在子目录中找到文件。文件就类似于资源的原始资料，
如对话盒面板（dialog template ）。在 PE 文件中，根目录和子目录都是一种
IMAGE_RESOURCE_DIRECTORY 结构，长像如下：

DWORD Characteristics

理论上这个值可以放置资源的特性旗标。不过它总是 0 。

DWORD TimeDateStamp

资源的产生时刻。

WORD MajorVersion WORD MinorVersion

理论上这里可以放置资源的版本号码。不过它总是 0 。

WORD NumberOfNamedEntries

数组之中「使用名称的元素」的个数。此数组紧跟在这个结构之后。请看稍后的
DirectoryEntries[] 解说。

WORD NumberOfIdEntries

数组之中「使用整数 ID 的元素」的个数。此数组紧跟在这个结构之后。请看稍后的
DirectoryEntries[] 解说。

IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[ ]

这个字段并非正式的 IMAGE_RESOURCE_DIRECTORY 结构的一部份，而是紧跟在它
之后的一个由 IMAGE_RESOURCE_DIRECTORY_ENTRY 组成的数组。其中的元素个
数是NumberOfNamedEntries 和 NumberOfIdEntries 的总和。「带有名称之元素」统统
位在「带有 ID 之元素」的前面。
译注：在 Windows 95 的 WINNT.H 中，并未视 DirectoryEntries 为有用字段。

一笔目录资料（directory entry ）可以指向一个子目录（也就是另一个
IMAGE_RESOURCE_DIRECTORY ）或是一个 IMAGE_RESOURCE_DATA_ENTRY 。
后者用来描述在文件什么地方找出资源的原始资料。一般而言，在你找到
IMAGE_RESOURCE_DATA_ENTRY 之前最少需经过三层目录。最上层目录（只有一个）
一定在 .rsrc 一开始处获得。其子目录对应于文件中的资源型态。如果 PE 档内有三笔
资源：对话盒、字符串表格和选单，那么就会有三个对应的子目录。每一个这样的「型态
子目录」又会有一个「ID 子目录」。一种型态的资源如果产生出三份实体，就有三个「ID
子目录」。以上例而言，如果程序中有四个对话盒，那么就应该有四个「对话盒 ID 子
目录」。每一个 ID 子目录中要不是有名称，就是有资源 ID ，用以识别 RC 档中的资
源。图8-10 显示资源目录的阶层架构。

图8-10 一个典型的 PE 档资源架构

图8-11 是 PEDUMP 对于 Windows NT 3.5 的 CLOCK.EXE 的资源倾印输出。请注意
缩排的第二层，你将看到 icons 、menus 、dialogs 、stringtables 、group icons 、version 等
等资源。而在第三层，有两个 icons （ID 为 1 、2 ），两个 menus （名为CLOCK 和
GENERICMENU ），两个 dialogs （一个名为 ABOUTBOX ，另一个 ID 为 0x64 ）。缩
排第四层告诉你，icon 1 的资料位于 RVA 0x9754 ，有 0x130 个字节。CLOCK menu 的
数据位于 RVA 0x952C ，有 0xEA 个字节。

Resources
ResDir (0) Named:00 ID:06 TimeDate:2E601E3C Vers:0.00 Char:0
   ResDir (ICON) Named:00 ID:02 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (1) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 000001E0
         Offset: 09754 Size: 00130 CodePage: 0
      ResDir (2) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 000001F0
         Offset: 09884 Size: 002E8 CodePage: 0
   ResDir (MENU) Named:02 ID:00 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (CLOCK) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000200
         Offset: 0952C Size: 000EA CodePage: 0
      ResDir (GENERICMENU) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000210
         Offset: 09618 Size: 0003A CodePage: 0
   ResDir (DIALOG) Named:01 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (ABOUTBOX) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000220
         Offset: 09654 Size: 000FE CodePage: 0
      ResDir (64) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000230
         Offset: 092C0 Size: 0026A CodePage: 0
   ResDir (STRING) Named:00 ID:02 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (1) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000240
         Offset: 09EA8 Size: 000F2 CodePage: 0
      ResDir (2) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000250
         Offset: 09F9C Size: 00046 CodePage: 0
   ResDir (GROUP_ICON) Named:01 ID:00 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (CCKK) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000260
         Offset: 09B6C Size: 00022 CodePage: 0
   ResDir (VERSION) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (1) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000270
         Offset: 09B90 Size: 00318 CodePage: 0
图8-11 CLOCK.EXE 的资源阶层架构

每一笔资源目录资料是一个 IMAGE_RESOURCE_DIRECTORY_ENTRY （嘿，这名称可
真长）。IMAGE_RESOURCE_DIRECTORY_ENTRY 的格式如下：

DWORD Name

这个字段内含一个整数 ID ，或是一个指针，指向一个内含字符串名称的结构。如果最高位
元（0x80000000 ）是 0 ，这字段就被解释为一个整数 ID 。如果最高位不是 0 ，剩余的 31
个位就是 IMAGE_RESOURCE_DIR_STRING_U 结构的偏移位置。该结构内含一个
WORD 字符计数器，然后是代表资源名称的 unicode 字符串。是的，即使 PE 档适用于
non-Unicode 的 Win32 平台，这里还是使用了 unicode 。欲将 unicode 转换为 ANSI 字
串，请参考 WideCharToMultiByte 函数。

DWORD OffsetToData

这字段若不是另一个资源目录的偏移位置，就是指向特定资源实体的一个指针。如果最
高位（0x80000000 ）设立，表示这个目录项目指向另一个子目录，而剩余的 31 个位
元就是另一个 IMAGE_RESOURCE_DIRECTORY 的偏移位置（从resource section 算
起）。如果最高位没有设立，剩余的 31 个位就是某一个
IMAGE_RESOURCE_DATA_ENTRY 的偏移位置（从resource section 算起）。
IMAGE_RESOURCE_DATA_ENTRY 结构内含资源原始资料的位置、大小、以及code
page 。

如果要再进一步挖掘资源格式，我就必须探讨各种类型的资源（dialog 啦、menu 啦等等）。
这些题目很容易就会填满另一大章，而且我也应该多拯救几棵树才是。如果你有兴趣，
请看 Win32 SDK 中的 RESFMT.TXT 文件，那里面对于所有资源型态的格式都有详细的
描述。PEDUMP 可以显示资源的阶层架构，但是没有分解个别的资源实体。


9.PE 档的基底重定位（Base Relocations）

当联结器产生一个 EXE 档，它假设这个文件会被加载内存的某处，并且把code 和 data
的相关假设地址都写入 EXE 文件中。如果可执行档最终被加载到虚拟地址空间的另一
个地址，没有按预期来，联结器所登记的那个地址就是错误的。储存在 .reloc 的信息就
是用来帮助 PE 加载器修正加载模块的地址。如果加载器能够把模块加载到预定地址
上，.reloc 就可以弃而不用。.reloc 中的资料项被称为「基底重定位（base relocations ）
资料项」，原因是它们的用途视被加载模块的基地址而定。

和NE 格式的重定位方式不同，PE 档的作法十分简单。它们并不参考到外部DLLs 或模
组中的其它sections ，而是把image （译注）中所有可能需要修改的地址串成一个串行。
译注：文件中的模块资料被加载内存后，我们称其为模块的 "image" 。我保留这个原 文，不译它。
下面是一个例子。假设有一个可执行文件，基地址是 0x400000 。在这个 image 偏移位
置 0x2134 处是一个指针，指向一个字符串。字符串始于实际地址 0x404002 处，所以指针
内容应该是 0x404002 。你可以把文件加载，但是加载器决定把它映像到实际地址
0x600000 处。联结器假设的基地址和实际加载的起始地址之间的差额称为 delta 。此
例之 delta 为 0x20000 。整个 image 的位置提高了 0x20000 ，其中的字符串当然也是（现
在应该是 0x604002 ）。所以指向字符串的指针就错误了，delta 应该加到指针值中。

为了让 Windows 加载器有能力做这样的调整，可执行档内含许多个「基底重定位资料
项」，给那些存放指针的位置（本例为 0x2134 ）使用。加载器必须把 delta 加到各个位
址上。本例之中加载器应该把 0x20000 加给原来的指针值（0x404002 ），并将结果0x604002
写回原处。图8-12 显示这个过程。

图8-12 PE 文件的基底重定位动作。

「基底重定位资料项」的格式有点儿奇怪。它们被包装为一系列连续区段，长短不一。
每一个区段描述 image 中的一个 4K page 的重定位信息。它们以一个
IMAGE_BASE_RELOCATION 结构做为开始，格式如下：

DWORD VirtualAddress

此一字段内含这些个「基底重定位资料项」的起始 RVA 值。每一个「基底重定位资料
项」的偏移位置必须加上此值才能够构成一个真正的 RVA ，指向「基底重定位资料项」。

DWORD SizeOfBlock

结构大小，再加上所有跟随在后的「基底重定位资料项」（都是 WORDs ）。为了决定
区块中的「基底重定位资料项」的个数，先把此值减去IMAGE_BASE_RELOCATION 结
构大小（8 个字节），再除以 2 （WORD 大小）。如果此字段为 44 ，就表示有 18 个
「基底重定位资料项」：

( 44 - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof(WORD) = 18

WORD TypeOffset

这并不是单独一个 WORD ，事实上它是一个 WORDs 数组。数组元素个数可由上一个
式子计算获得。每一个 WORD 的最底部 12 个位代表「基底重定位资料项」偏移位
置，但必须再加上「基底重定位资料项」区块表头的 VirtualAddress 字段值。最高的4
个位是「基底重定位资料项」的型态。对于在 Intel CPUs 中执行的 PE 档，你将看到
两种型态：

·0 （IMAGE_REL_BASED_ABSOLUTE ）：此一「基底重定位资料项」无意义，
只是用来充数而已，使所有「基底重定位资料项」的总共大小成为 DWORD 的
倍数。

·3 （IMAGE_REL_BASED_HIGHLOW ）：把delta 值加到欲计算的RVA 值去。

另外还有其它型态，在 WINNT.H 中定义。它们大部份是给 i386 以外的CPU 使用。

图8-13 描绘出一些「基底重定位资料项」，这是 PEDUMP 的输出结果。请注意图中
的RVA 值已经被 IMAGE_BASE_RELOCATION 结构中的 VirtualAddress 字段校正过
了。

Virtual Address: 00001000 Size: 0000012C
00001032 HIGHLOW
0000106D HIGHLOW
000010AF HIGHLOW
000010C5 HIGHLOW
// Rest of chunk omitted...
Virtual Address: 00002000 Size: 0000009C
000020A6 HIGHLOW
00002110 HIGHLOW
00002136 HIGHLOW
00002156 HIGHLOW
// Rest of chunk omitted...
Virtual Address: 00003000 Size: 00000114
0000300A HIGHLOW
0000301E HIGHLOW
0000303B HIGHLOW
0000306A HIGHLOW
// Rest of chunk omitted...
图8-13 一个 EXE 档的「基底重定位资料项」（base relations ）


13.PE 档和COFF 档之间的差异

前面的讨论之中，我已经告诉你，在 COFF OBJ 档和 PE 档之间有许多共通的结构和表
格。两者一开始都有一个 IMAGE_FILE_HEADER ，后面紧跟着一个section table 。两者
共享相同的行号信息格式以及符号表格格式 -- 虽然 PE 文件可以有额外的 non-COFF 符
号表格。存在于 PE 和 COFF OBJ 之间的共通性，其规模可以从 PEDUMP 原始码看
得出来。这个程序中最大的一个文件是 COMMON.C ，内含所有可以共享于 PE- 和 OBJ-
文件的倾印函数。

两个文件格式的相似性并非偶然。这样设计的目的是为了让联结器的工作降至最少。理
论上，从单独一个 OBJ 档产生一个 EXE 档，应该只是安插一些表格，并修改一些偏移
值而已。你可以因此想象 COFF 档是 PE 档的胚胎。只有一些东西不一样，我把它们列
在下面：

·COFF OBJ 一开始就是 IMAGE_FILE_HEADER 结构，没有 DOS stub 。而且
该结构一开始也没有 PE 签名字符。

·OBJ 档没有 IMAGE_OPTIONAL_HEADER 。PE 文件中的这个结构则是紧跟在
IMAGE_FILE_HEADER 之后。有趣的是，某些存放于 COFF LIB 档内的 OBJs
倒是有此结构。

·OBJ 文件没有「基底重定位资料项」。但是它们有正规的符号修正表。我没有
讨论COFF OBJ 中的重定位方式，因为那十分艰涩。如果你要挖掘这一部份，
section table 资料项中的 NumberOfRelocations 和 PointerToRelocations 两个
字段就指向每一个 section 的重定位信息。这些重定位信息是
IMAGE_RELOCATION 结构（定义于WINNT.H ）所组成的数组。PEDUMP 可
以显示 OBJ 文件的重定位信息 -- 如果你选用适当的命令列选项的话。

·OBJ 文件中的 CodeView 信息存放在 .debug$S 和 .debug$T 两个 sections
中。当联结器处理 OBJ 文件，它不把这两个 sections 放到 PE 档中。而是
收集所有的 sections 并建立一个简单的符号表格，放在文件尾端。这个符号
表格并不是正式的 section （也就是说在 PE 档的 section table 中没有和它相
关的资料项）。


14.COFF LIB 档

一旦你了解 COFF OBJ 档，使用 COFF LIB 档就不会太困难。COFF LIB 档基本上只
是 COFF OBJ 的集合，再加上一些初始资料，让你能够很快速地搜寻 LIB 中的某个 OBJ
的位置。有些技术文件把 COFF LIB 说成是一个 archives ，这点我必须先告诉你。

所有的 LIB 档以相同的 8 个签名字符做为开始。这 8 个字符定义在 WINNT.H 中：

#define IMAGE_ARCHIVE_START 　　 "!\n"

文件的剩余部份是一大堆可变长度的记录，每一个记录以一个
IMAGE_ARCHIVE_MEMBER_HEADER 打头阵：

typedef struct _IMAGE_ARCHIVE_MEMBER_HEADER {
BYTE Name[16];
BYTE Date[12];
BYTE UserID[6];
BYTE GroupID[6];
BYTE Mode[8];
BYTE Size[10];
BYTE EndHeader[2];
} IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER;

每一个 IMAGE_ARCHIVE_MEMBER_HEADER 对应到的，要不是 LIB 中的一个
OBJ ，要不就是一些特殊记录的集合。这些特殊记录来自 LIB 的最前头，为的是让 LIB 能
够快速寻找 OBJ 。在 IMAGE_ARCHIVE_MEMBER_HEADER 之后的资料是一个个的
记录（称为 archive member records ）。大部份的archive member records 就是OBJ 档的内
容。事实上当你倾印LIB 档，PEDUMP 呼叫的倾印函数也用于处理OBJ 档。图8-16 显
示 LIB 文件格式。

让我们仔细看看 IMAGE_ARCHIVE_MEMBER_HEADER 的字段：

BYTE Name[16]

这是 archive member 的名称。如果一个 '/' 出现在一个 ASCII 字符串最后（例如
"FOO.OBJ/"），那么在 '/' 之前的就是 member 的名称。如果字符串是以 '/' 开始并紧跟着
十进制数（例如 "/104"），这个号码就是archive member 名称的偏移位置。所谓偏移
是指对 LIB 中的Longnames member 而言。因此 "/104" 的意思就是表示 member 名称
出现在Longname 区域的第 104 个字节。

另外还有一些特殊名称，用来识别特殊的 archive members ：

#define IMAGE_ARCHIVE_LINKER_MEMBER "/ "
#define IMAGE_ARCHIVE_LONGNAMES_MEMBER "// "

至于那些记录在 import library 中的 OBJ 档，此字段放的是 DLL （内含被输入函数）
的名称。
“!\n”
IMAGE_ARCHIVE_MEMBER_HEADER (“/”)
First Linker Member data
IMAGE_ARCHIVE_MEMBER_HEADER (“/”)
Second Linker Member data
IMAGE_ARCHIVE_MEMBER_HEADER (“//”)
Longnames Member Data
IMAGE_ARCHIVE_MEMBER_HEADER (“FOO.OBJ”)
OBJ file data
IMAGE_ARCHIVE_MEMBER_HEADER (“/104”)
OBJ file data
...

图8-16 COFF LIB 档的格式布局

BYTE Date[12]

Member 被产生的时刻。以 ASCII 十进制格式储存。

BYTE UserID[6]

使用者的 ID ，以 ASCII 十进制格式储存。是一个以 NULL 为结束字符的字符串。

BYTE GroupID[6]

Group ID ，以 ASCII 十进制格式储存。是一个以 NULL 为结束字符的字符串。

BYTE Mode[8]

文件模式，以 ASCII 十进制格式储存。总是为 0 。

BYTE Size[10]

跟随在后的 member 资料的大小，以 ASCII 十进制格式储存。至于资料的格式，则视
其型态而定（请参考前面的 Name 字段）。

BYTE EndHeader[2]

ASCII 字符串 \n 。

Linker members

每一个 LIB 档有两个 Linker member sections ，作用像是文件中的其它内容的信息表格。
这两个 member 都有名称 '/'，可以经由出现在文件中的前后次序区分之。第一个linker
member 是第一个 archive member 的名称再加上 '/'，第二个 linker member 是第二个
archive member 的名称再加上 '/'。

两个 Linker member 基本上都用来列出 LIB 文件中的公开符号，以及「内含公开符号之
OBJ members 」的偏移位置。两个 Linker member 有着不同的格式。为什么同一份信息
要有两份拷贝？第一个 Linker member 所放信息是以 OBJ 出现次序排列，导至无法进
行最佳搜寻。第二个 Linker member 所放信息是以符号的字母次序排列，因此对联结器
而言它是比较有用的。微软的文件上说，联结器会忽略第一个 Linker member ，使用第二
个 Linker member 。

第一个 Linker member 有以下格式：

DWORD NumberOfSymbols

这是 LIB 之中公开符号的个数。这是一个 big-endian 格式（译注），反应出「COFF 并
不是继承自 i386 机器」这一点。PEDUMP 的 LIBDUMP.C 中的ConvertBigEndian 函
式可以处理 big-endian 和 little-endian 之间的转换。
译注：所谓 big-endian 是把「most significant 字节」放在前面，例如 Motorola 和 IBM 370 。所谓 little-endian 是把「least significant 字节」放在前面，例如 Intel 和 DEC VAX 。所以如果你以工具软件如 PC-Tools 或 DEBUG.EXE 观察 Intel 机器的内存内 容或是文件内容，屏幕上出现的 "00 40" 其实真正是 "40 00"。
DWORD Offsets[NumberOfSymbols]

这是一个文件偏移值构成的数组，每一个偏移值指向另一个 archive members 。偏移值是
以 big-endian 形式出现。每一个 member 都是一个 OBJ-type member 。这个数组中的每
一个元素对应于相同排列次序的数组（请看下一个字段），后者的元素是 ASCII 字符串，
代表符号名称。

BYTE StringTable[?]

这是一系列没有被打断的 C-style 字符串（译注：所谓 C-style 字符串就是 ZString ，也就是
以 0 为结束字符的字符串）。

基本上，Offsets 数组中的每一个元素都对应到一个公开符号，而其名称出现于StringTable
之中。例如，Offsets 的第三个元素关系到 StringTable 的第三个字符串。看看 PEDUMP 的
输出，你会比较清楚：

First Linker Member:
Symbols: 00000006
MbrOffs Name
-------- ----
00000180 _DumpCAP@0
00000180 _StartCAP@0
00000180 _StopCAP@0
...

第二个 Linker member 的格式比较复杂，因为要提供快速搜寻的资料。格式如下：

DWORD NumberOfMembers

LIB 文件中的 OBJ archive members 个数。

DWORD Offsets[NumberOfSymbols]

「其它 archive members 的文件偏移位置」所组成的数组。与第一个Linker member 不
同的是，这些偏移位置都是机器原生（native ）格式，也就是说，如果在 i386 机器上，
它将是 little-endian 形式。

DWORD NumberOfSymbols

这是 StringTable 中的公开符号个数，同时也就是整个 LIB 的公开符号个数。

WORD Indices[NumberOfSymbols]

这个数组内含「从1 起算」的Offsets 数组索引。这个数组与StringTable 数组平行运作。

BYTE StringTable[NumberOfSymbols]

这是一系列没有被打断的 C-style 字符串。

为了以第二个 Link member 找出使用某一公开符号的 OBJ ，联结器首先寻找StringTable
并获得其相关索引值，然后联结器以此索引值查询 Indices 数组。最后，联结器再将
Indices 数组元素的值（WORD ）减 1 ，当做是 Offsets 数组的索引。找到 Offsets 数组
元素（DWORD ）之后就是 OBJ 的偏移位置了。PEDUMP 的 LIBDUMP.C 中的
DumpSecondLinkerMember 函数实作出这样的寻找程序。

The Longnames member

存放在 Longnames archive member section 中的数据其实只是一些 C-style 字符串，一个跟
着一个。如果一个字符串太长，没办法塞入 IMAGE_ARCHIVE_MEMBER_HEADER 的
Name 字段（16 字节）的话，它就会被放到这里来。这种情况下，Name 字段会放置
一个 '/' 字符，后随着一个字符串偏移位置（Longnames section 的偏移位置）。偏移值是
以 ASCII 十进制值表示。


上一页 | 目录
15.摘要

由于 Win32 的降临，微软对于 OBJ 和 EXE （DLL ）格式做了全盘的改变。这些改变
建立在其它操作系统上的既成结果，使微软得以节省时间。改头换面的主要目的是为了
强化在不同平台之间的可移植性。COFF OBJ 格式在 Win32 诞生之前就存在了。PE 格
式则是 COFF 格式的延伸，使用于 Win32 平台上。

OBJ 和 EXE （DLL ）文件的有用部份是从 IMAGE_FILE_HEADER 结构展开。在那结
构之下是一个 section table ，此一表格内含文件中所有 sections 的属性和位置。所谓
section 是一群「逻辑上应该放在一起的资料或程序代码」。为了让资料的搜寻能够更快速，
PE 档内含一个data directory ，指向文件中各个有用的区段（例如文件的 export table ）。
除了表头、section tables ，以及 sections 的原始资料外，COFF OBJ 文件以及 PE 档也内
含符号名称和行号信息。这些信息存放在文件尾端。
上一页 | 目录


Microsoft HTML Help Compiler 4.74.8702

Compiling e:\pe\toye.chm

Compile time: 0 minutes, 6 seconds
13	Topics
93	Local links
1	Internet link
6	Graphics

Created e:\pe\toye.chm, 223,729 bytes
Compression decreased file by 37,763 bytes.


// Image Format
//

#ifndef _MAC

#include "pshpack4.h"                   // 4 byte packing is the default

#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ
#define IMAGE_OS2_SIGNATURE                 0x454E      // NE
#define IMAGE_OS2_SIGNATURE_LE              0x454C      // LE
#define IMAGE_VXD_SIGNATURE                 0x454C      // LE
#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00

#include "pshpack2.h"                   // 16 bit headers are 2 byte packed

#else

#include "pshpack1.h"

#define IMAGE_DOS_SIGNATURE                 0x4D5A      // MZ
#define IMAGE_OS2_SIGNATURE                 0x4E45      // NE
#define IMAGE_OS2_SIGNATURE_LE              0x4C45      // LE
#define IMAGE_NT_SIGNATURE                  0x50450000  // PE00
#endif

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

typedef struct _IMAGE_OS2_HEADER {      // OS/2 .EXE header
    WORD   ne_magic;                    // Magic number
    CHAR   ne_ver;                      // Version number
    CHAR   ne_rev;                      // Revision number
    WORD   ne_enttab;                   // Offset of Entry Table
    WORD   ne_cbenttab;                 // Number of bytes in Entry Table
    LONG   ne_crc;                      // Checksum of whole file
    WORD   ne_flags;                    // Flag word
    WORD   ne_autodata;                 // Automatic data segment number
    WORD   ne_heap;                     // Initial heap allocation
    WORD   ne_stack;                    // Initial stack allocation
    LONG   ne_csip;                     // Initial CS:IP setting
    LONG   ne_sssp;                     // Initial SS:SP setting
    WORD   ne_cseg;                     // Count of file segments
    WORD   ne_cmod;                     // Entries in Module Reference Table
    WORD   ne_cbnrestab;                // Size of non-resident name table
    WORD   ne_segtab;                   // Offset of Segment Table
    WORD   ne_rsrctab;                  // Offset of Resource Table
    WORD   ne_restab;                   // Offset of resident name table
    WORD   ne_modtab;                   // Offset of Module Reference Table
    WORD   ne_imptab;                   // Offset of Imported Names Table
    LONG   ne_nrestab;                  // Offset of Non-resident Names Table
    WORD   ne_cmovent;                  // Count of movable entries
    WORD   ne_align;                    // Segment alignment shift count
    WORD   ne_cres;                     // Count of resource segments
    BYTE   ne_exetyp;                   // Target Operating system
    BYTE   ne_flagsothers;              // Other .EXE flags
    WORD   ne_pretthunks;               // offset to return thunks
    WORD   ne_psegrefbytes;             // offset to segment ref. bytes
    WORD   ne_swaparea;                 // Minimum code swap area size
    WORD   ne_expver;                   // Expected Windows version number
  } IMAGE_OS2_HEADER, *PIMAGE_OS2_HEADER;

typedef struct _IMAGE_VXD_HEADER {      // Windows VXD header
    WORD   e32_magic;                   // Magic number
    BYTE   e32_border;                  // The byte ordering for the VXD
    BYTE   e32_worder;                  // The word ordering for the VXD
    DWORD  e32_level;                   // The EXE format level for now = 0
    WORD   e32_cpu;                     // The CPU type
    WORD   e32_os;                      // The OS type
    DWORD  e32_ver;                     // Module version
    DWORD  e32_mflags;                  // Module flags
    DWORD  e32_mpages;                  // Module # pages
    DWORD  e32_startobj;                // Object # for instruction pointer
    DWORD  e32_eip;                     // Extended instruction pointer
    DWORD  e32_stackobj;                // Object # for stack pointer
    DWORD  e32_esp;                     // Extended stack pointer
    DWORD  e32_pagesize;                // VXD page size
    DWORD  e32_lastpagesize;            // Last page size in VXD
    DWORD  e32_fixupsize;               // Fixup section size
    DWORD  e32_fixupsum;                // Fixup section checksum
    DWORD  e32_ldrsize;                 // Loader section size
    DWORD  e32_ldrsum;                  // Loader section checksum
    DWORD  e32_objtab;                  // Object table offset
    DWORD  e32_objcnt;                  // Number of objects in module
    DWORD  e32_objmap;                  // Object page map offset
    DWORD  e32_itermap;                 // Object iterated data map offset
    DWORD  e32_rsrctab;                 // Offset of Resource Table
    DWORD  e32_rsrccnt;                 // Number of resource entries
    DWORD  e32_restab;                  // Offset of resident name table
    DWORD  e32_enttab;                  // Offset of Entry Table
    DWORD  e32_dirtab;                  // Offset of Module Directive Table
    DWORD  e32_dircnt;                  // Number of module directives
    DWORD  e32_fpagetab;                // Offset of Fixup Page Table
    DWORD  e32_frectab;                 // Offset of Fixup Record Table
    DWORD  e32_impmod;                  // Offset of Import Module Name Table
    DWORD  e32_impmodcnt;               // Number of entries in Import Module Name Table
    DWORD  e32_impproc;                 // Offset of Import Procedure Name Table
    DWORD  e32_pagesum;                 // Offset of Per-Page Checksum Table
    DWORD  e32_datapage;                // Offset of Enumerated Data Pages
    DWORD  e32_preload;                 // Number of preload pages
    DWORD  e32_nrestab;                 // Offset of Non-resident Names Table
    DWORD  e32_cbnrestab;               // Size of Non-resident Name Table
    DWORD  e32_nressum;                 // Non-resident Name Table Checksum
    DWORD  e32_autodata;                // Object # for automatic data object
    DWORD  e32_debuginfo;               // Offset of the debugging information
    DWORD  e32_debuglen;                // The length of the debugging info. in bytes
    DWORD  e32_instpreload;             // Number of instance pages in preload section of VXD file
    DWORD  e32_instdemand;              // Number of instance pages in demand load section of VXD file
    DWORD  e32_heapsize;                // Size of heap - for 16-bit apps
    BYTE   e32_res3[12];                // Reserved words
    DWORD  e32_winresoff;
    DWORD  e32_winreslen;
    WORD   e32_devid;                   // Device ID for VxD
    WORD   e32_ddkver;                  // DDK version for VxD
  } IMAGE_VXD_HEADER, *PIMAGE_VXD_HEADER;

#ifndef _MAC
#include "poppack.h"                    // Back to 4 byte packing
#endif

//
// File header format.
//

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER             20

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
//
// Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

//
// Optional header format.
//

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_ROM_OPTIONAL_HEADER {
    WORD   Magic;
    BYTE   MajorLinkerVersion;
    BYTE   MinorLinkerVersion;
    DWORD  SizeOfCode;
    DWORD  SizeOfInitializedData;
    DWORD  SizeOfUninitializedData;
    DWORD  AddressOfEntryPoint;
    DWORD  BaseOfCode;
    DWORD  BaseOfData;
    DWORD  BaseOfBss;
    DWORD  GprMask;
    DWORD  CprMask[4];
    DWORD  GpValue;
} IMAGE_ROM_OPTIONAL_HEADER, *PIMAGE_ROM_OPTIONAL_HEADER;

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD       SectionAlignment;
    DWORD       FileAlignment;
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;
    DWORD       SizeOfHeaders;
    DWORD       CheckSum;
    WORD        Subsystem;
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

#define IMAGE_SIZEOF_ROM_OPTIONAL_HEADER      56
#define IMAGE_SIZEOF_STD_OPTIONAL_HEADER      28
#define IMAGE_SIZEOF_NT_OPTIONAL32_HEADER    224
#define IMAGE_SIZEOF_NT_OPTIONAL64_HEADER    240

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER     IMAGE_SIZEOF_NT_OPTIONAL64_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER     IMAGE_SIZEOF_NT_OPTIONAL32_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_ROM_HEADERS {
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_ROM_OPTIONAL_HEADER OptionalHeader;
} IMAGE_ROM_HEADERS, *PIMAGE_ROM_HEADERS;

#define IMAGE_FIRST_SECTION64( ntheader ) ((PIMAGE_SECTION_HEADER)        \
    ((UINT_PTR)ntheader +                                                  \
     FIELD_OFFSET( IMAGE_NT_HEADERS64, OptionalHeader ) +                 \
     ((PIMAGE_NT_HEADERS64)(ntheader))->FileHeader.SizeOfOptionalHeader   \
    ))

#define IMAGE_FIRST_SECTION32( ntheader ) ((PIMAGE_SECTION_HEADER)        \
    ((UINT_PTR)ntheader +                                                  \
     FIELD_OFFSET( IMAGE_NT_HEADERS32, OptionalHeader ) +                 \
     ((PIMAGE_NT_HEADERS32)(ntheader))->FileHeader.SizeOfOptionalHeader   \
    ))

#ifdef _WIN64
typedef IMAGE_NT_HEADERS64                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64                 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader)       IMAGE_FIRST_SECTION64(ntheader)
#else
typedef IMAGE_NT_HEADERS32                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32                 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader)       IMAGE_FIRST_SECTION32(ntheader)
#endif