Matt Pietrek
1994 年3月
Matt Pietrek 是Windows Internals (Addison-Wesley, 1993)的作者。他就职于Nu-Mega 技术有限公司,可通过CompuServe: 71774,362联系到他。
这篇文章出自1994年3月发行的Microsoft系统期刊。版权所有﹫1994 Miller Freeman, Inc.保留所有权利。未经Miller Freeman同意,这篇文章的任何部分不得以任何形式被复制(除了在评论文章里以摘要引用)。
一个操作系统的可执行文件的格式在很多方面是这个操作系统的一面镜子。虽然学习一个可执行文件格式不是大多数程序员的首要任务,但是从中你可学到大量的知识。这篇文章中,我将给出Microsoft为他们的基于Win32的系统所设计的PE文件格式的详细说明。可以预知在未来,PE文件格式在Microsoft的所有操作系统包括Windows 2000中都将扮演着很重要的角色。如果你在使用Win32s或WinNT,那么你已经在使用PE文件了。甚至你只是在Windows3.1下用Visual C++编程,你也已在使用PE文件了(Visual C++的32位DOS扩展组件使用此格式)。简而言之,PE格式已得到普遍应用并且在不短的将来也不会取消。现在是时间找出这种新的可执行文件格式为操作系统所带来的影响了。
我不会让你盯住无穷无尽的16进制Dumps和详细讨论页面中每个单独位的重要性。代替的,我将介绍PE文件格式中内含的概念并且把它们和你每天都会遇到的东西联系起来。例如,线程局部变量的概念,比如
declspec(thread) int i;
它使我快要发疯了,直到我明白它是怎样在可执行文件里优雅而简单的实现的。既然你们大多数都有使用16位Windows的背景,我将把Win32 PE文件格式的结构和与其等价的16位的NE文件格式联系起来。
除了一个不同的可执行文件格式之外, Microsoft还引入了一个由它的编译器和汇编器生成的新的目标模块格式。这个新的OBJ文件格式和PE格式有许多相同的东西。为了找到这个新的OBJ文件格式的文档我做了许多徒劳的搜索。所以我以自己的理解来解释它,并且除了PE格式之外我会在这里描述它其中的一部分。
大家都知道Windows NT继承了VAX® VMS® 和 UNIX®。许多Windows NT的创建者在进入Microsoft之前都在那些平台上进行设计和编码。当开始设计Windows NT时, 很自然的他们设法使用以前编写的并经过测试的工具以最小化项目启动时间。这些工具生成的并且与之一起工作的可执行文件和目标模块格式被叫做COFF(Common Object File Format的首字母缩写)。从 COFF 的一些域用八进制来表示可以看出其已很老了。COFF格式自身是一个好的起点,但需要被扩展以满足一个现代操作系统如Windows NT或者Windows 95的所有需要。这个扩展的结果就是PE格式。它被称为“可移植”是因为Windows NT在不同的平台(x86, MIPS®, Alpha, 等等)上的所有实现都使用这个相同的可执行格式。当然,也有不同的地方比如CPU指令的二进制编码。重要的是操作系统加载器和程序设计工具不必为每种CPU完全重写。
Microsoft抛弃了现存的32位工具和文件格式的事实证明了他们想让Windows NT升级并且运行的更快的决心。为16位Windows编写的虚拟设备驱动使用一个不同的32位文件布局-LE 格式-它在Windows NT出现很早以前就存在了。比那更重要的是OBJ格式的改变。在Windows NT 的C编译器以前,所有的Microsoft编译器使用Intel OMF(Object Module Format)规范。以前提到,Microsoft的Win32编译器生成COFF格式的OBJ文件。一些Microsoft的竞争者例如Borland 和 Symantec 选择放弃 COFF 格式的 OBJs 而坚持使用 Intel OMF格式。结果导致生成OBJs或LIBs的公司为了使用不同的编译器就必须回去为不同的编译器发布他们产品的不同版本 (如果他们还没有那么做)。
PE格式在WINNT.H头文件中被文档化了 (比较松散)。大约在WINNT.H文件的中间一个标题为“Image Format”的区域。这块区域的开头是我们熟悉的老的MS-DOS MZ格式和NE格式文件头接下来才是更新的PE格式的信息。WINNT.H 提供PE文件用到的原始数据结构的定义,但只包含了很少有用的以助于理解这些结构和标志的意思的注释。无论是谁为PE格式写的头文件(突然持续冒出 Michael J. O'Leary这个名字),他一定非常喜欢长的、描述性的名称以及嵌套很深的结构和宏。当使用WINNT.H编码时, 类似这样的表达式很常见:
pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
要帮助逻辑地理解WINNT.H中的信息,可以阅读PE和COFF规范,这些规范可以在 MSND 库每季度的CD-ROM 中找到(直到并包含 2001 年 10 月)。
即刻转到COFF格式的OBJ文件的主题上来,WINNT.H头文件中包含了COFF格式的OBJ和LIB文件的structure定义和typedef定义。不幸的是,与上面提到的可执行文件一样,我没有找到关于它们的任何文档。既然PE文件和COFF OBJ文件那么相似,我认为是时候把这些文件格式公布出来并为它们写些文档了。
阅读了PE文件由什么组成后,你自己也想Dump一些PE文件来看看这些概念。如果你使用Microsoft的工具进行基于Win32的开发,DUMPBIN程序可以分析并把PE文件和COFF OBJ/LIB文件输出为可读的形式。在所有的Dump工具中,DUMPBIN是容易的和最全面的。它甚至有一个极好的选项来反汇编它正在解析的文件的代码节(code sections)。Borland 用户可使用TDUMP查看PE可执行文件,但TDUMP不能解析COFF OBJ文件。这不是一个大的问题因为Borland编译器首先就不生成COFF格式的OBJ文件。
我写了一个PE和COFF OBJ文件的Dump程序,PEDUMP(见表1),我是想提供比DUMPBIN更可理解的输出。虽然它没有反汇编器也不能和LIB文件一起工作,但它在其它方面和DUMPBIN的功能是一样的,并且添加了一些新的特性以使它值得被认同。PEDUMP的源代码在任何MSJ电子公告板都可找到,因此我不把它在这儿全部列出。代替的,我将会列出一些PEDUMP输出的例子以举例说明我描述到的概念。
表 1. PEDUMP.C
001 |
//-------------------- |
002 |
// PROGRAM: PEDUMP |
003 |
// FILE: PEDUMP.C |
004 |
// AUTHOR: Matt Pietrek - 1993 |
005 |
//-------------------- |
006 |
#include <windows.h> |
007 |
#include <stdio.h> |
008 |
#include "objdump.h" |
009 |
#include "exedump.h" |
010 |
#include "extrnvar.h" |
011 |
|
012 |
// Global variables set here, and used in EXEDUMP.C and OBJDUMP.C |
013 |
BOOL fShowRelocations = FALSE; |
014 |
BOOL fShowRawSectionData = FALSE; |
015 |
BOOL fShowSymbolTable = FALSE; |
016 |
BOOL fShowLineNumbers = FALSE; |
017 |
|
018 |
char HelpText[] = |
019 |
"PEDUMP - Win32/COFF .EXE/.OBJ file dumper - 1993 Matt Pietrek/n/n" |
020 |
"Syntax: PEDUMP [switches] filename/n/n" |
021 |
" /A include everything in dump/n" |
022 |
" /H include hex dump of sections/n" |
023 |
" /L include line number information/n" |
024 |
" /R show base relocations/n" |
025 |
" /S show symbol table/n" ; |
026 |
|
027 |
// Open up a file, memory map it, and call the appropriate dumping routine |
028 |
void DumpFile( LPSTR filename) |
029 |
{ |
030 |
HANDLE hFile; |
031 |
HANDLE hFileMapping; |
032 |
LPVOID lpFileBase; |
033 |
PIMAGE_DOS_HEADER dosHeader; |
034 |
|
035 |
hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL, |
036 |
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); |
037 |
|
038 |
if ( hFile = = INVALID_HANDLE_VALUE ) |
039 |
{ printf ( "Couldn't open file with CreateFile()/n" ); |
040 |
return ; } |
041 |
|
042 |
hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); |
043 |
if ( hFileMapping = = 0 ) |
044 |
{ CloseHandle(hFile); |
045 |
printf ( "Couldn't open file mapping with CreateFileMapping()/n" ); |
046 |
return ; } |
047 |
|
048 |
lpFileBase = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0); |
049 |
if ( lpFileBase = = 0 ) |
050 |
{ |
051 |
CloseHandle(hFileMapping); |
052 |
CloseHandle(hFile); |
053 |
printf ( "Couldn't map view of file with MapViewOfFile()/n" ); |
054 |
return ; |
055 |
} |
056 |
|
057 |
printf ( "Dump of file %s/n/n" , filename); |
058 |
|
059 |
dosHeader = (PIMAGE_DOS_HEADER)lpFileBase; |
060 |
if ( dosHeader->e_magic = = IMAGE_DOS_SIGNATURE ) |
061 |
{ DumpExeFile( dosHeader ); } |
062 |
else if ( (dosHeader->e_magic = = 0x014C) // Does it look like a i386 |
063 |
&& (dosHeader->e_sp = = 0) ) // COFF OBJ file??? |
064 |
{ |
065 |
// The two tests above aren't what they look like. They're |
066 |
// really checking for IMAGE_FILE_HEADER.Machine = = i386 (0x14C) |
067 |
// and IMAGE_FILE_HEADER.SizeOfOptionalHeader = = 0; |
068 |
DumpObjFile( (PIMAGE_FILE_HEADER)lpFileBase ); |
069 |
} |
070 |
else |
071 |
printf ( "unrecognized file format/n" ); |
072 |
UnmapViewOfFile(lpFileBase); |
073 |
CloseHandle(hFileMapping); |
074 |
CloseHandle(hFile); |
075 |
} |
076 |
|
077 |
// process all the command line arguments and return a pointer to |
078 |
// the filename argument. |
079 |
PSTR ProcessCommandLine( int argc, char *argv[]) |
080 |
{ |
081 |
int i; |
082 |
|
083 |
for ( i=1; i < argc; i++ ) |
084 |
{ |
085 |
strupr(argv[i]); |
086 |
|
087 |
// Is it a switch character? |
088 |
if ( (argv[i][0] = = '-' ) || (argv[i][0] = = '/' ) ) |
089 |
{ |
090 |
if ( argv[i][1] = = 'A' ) |
091 |
{ fShowRelocations = TRUE; |
092 |
fShowRawSectionData = TRUE; |
093 |
fShowSymbolTable = TRUE; |
094 |
fShowLineNumbers = TRUE; } |
095 |
else if ( argv[i][1] = = 'H' ) |
096 |
fShowRawSectionData = TRUE; |
097 |
else if ( argv[i][1] = = 'L' ) |
098 |
fShowLineNumbers = TRUE; |
099 |
else if ( argv[i][1] = = 'R' ) |
100 |
fShowRelocations = TRUE; |
101 |
else if ( argv[i][1] = = 'S' ) |
102 |
fShowSymbolTable = TRUE; |
103 |
} |
104 |
else // Not a switch character. Must be the filename |
105 |
{ return argv[i]; } |
106 |
} |
107 |
} |
108 |
|
109 |
int main( int argc, char *argv[]) |
110 |
{ |
111 |
PSTR filename; |
112 |
|
113 |
if ( argc = = 1 ) |
114 |
{ printf ( HelpText ); |
115 |
return 1; } |
116 |
|
117 |
filename = ProcessCommandLine(argc, argv); |
118 |
if ( filename ) |
119 |
DumpFile( filename ); |
120 |
return 0; |
121 |
} |
Win32和PE基本概念
让我们复习一下几个基本概念,这些基本概念渗透于整个PE文件的设计(见图1)。我将用术语“模块(module)”来表示一个可执行文件或DLL加载到内存中的代码,数据和资源。除了你的程序直接使用的代码和数据之外,一个模块还包括Windows用来确定代码和数据在内存中被载入的位置的支撑数据结构。在16位Windows中,这些支撑数据结构位于模块数据库中(HMODULE指向的一个段)。在Win32中,这些数据结构位于PE头中,我将简要地介绍一下这些。
图 1. PE文件格式
对于PE文件重要的是要知道磁盘上的可执行文件和在被Windows调入内存后是很相似的。Windows加载器从磁盘文件创建一个进程时不必很费力。加载器使用内存映射文件机制把文件中适当的部分映射到虚拟地址空间中。使用建筑作比喻的话,PE 文件就像一座提前造好的房子。它只是被完整的安装到合适的地方,随后只需要再进行很少的工作把它与其它部分链接起来就行了(即链接到它使用的动态链接库等)。这种方式应用到PE格式的DLL也同样容易。一旦模块被载入,Windows就能有效的把它和其它内存映射文件同等对待。
这和16位Windows明显不同。16位NE文件加载器读取文件的一部分并且创建完全不同的数据结构来描述内存中的模块。当一个代码或数据段需要被调入时,加载器必须从全局堆中分配一个新的段,从可执行文件中找到原始数据的存储位置,转到这个位置,读入原始数据,并且进行适当的修正。另外,每个16位模块都有责任记住它用到的所有段选择器,不管这个段是否已被丢弃,等等。
对Win32来说,模块中的代码,数据,资源,导入表,导出表,和其它必需的模块数据结构用到的所有内存都在一个连续的内存块中。这种情况下你所要知道的就是加载器把文件映射到内存中的位置。通过存储在映像中的指针你可以很容易地找到模块中的所有部分。
你需要熟悉的另一个概念是相对虚拟地址(RVA)。PE文件中的许多域都用术语RVA指定。一个RVA只是一些项目相对于映射到内存后的文件的偏移。例如,让我们假定加载器把一个PE文件映射到了虚拟地址空间中起始地址为0x10000的位置。如果映像中某个表的起始地址是0x10464,那么这个表的RVA是0x464。
(Virtual address 0x10464)-(base address 0x10000) = RVA 0x00464
要把一个RVA转换成一个有用的指针,只要把RVA和模块的基址相加就行了。基址是一个EXE或DLL内存映射的起始地址,在Win32中是一个重要的概念。为了方便,Windows NT和Windows 95使用模块的基址作为这个模块的实例句柄(HINSTANCE)。在Win32中,把模块的基址称为HINSTANCE可能有点混淆,因为术语“实例句柄”来自于16位Windows。16位Windows中一个应用程序的每个拷贝都有它自己的单独的数据段(和一个关联的全局句柄) 把它和这个应用程序的其它拷贝区别开来,因此就形成了术语实例句柄。在Win32中,应用程序不必和其它程序区分开,因为它们不会共享相同的地址空间。尽管如此,术语HINSTANCE 仍被用来保持16位Windows和Win32之间的连续性。Win32中重要的是你可以为任何DLL调用GetModuleHandle方法得到一个指针用来访问这个模块的组件。
你要知道的关于PE文件的最后的概念是“节(Section)”。PE文件中的一个节和NE文件中的一个段或者资源大致等价。节中包含的不是代码就是数据。和段不同,节是内存中的连续的空间并且没有大小限制。一些节中包含你的程序中直接声明和使用的代码和数据,另一些被链接器和库为你创建的数据节中包含操作系统要用到的重要的信息。在PE格式的一些描述中,节也被称为“对象(objects)”。术语“对象(object)”有太多的含义,因此我将把代码和数据区称为“节(Section)”。
PE头
就像所有其它的可执行文件格式一样,PE文件中在一个大家都知道的 (或者容易找到)位置有一个包括很多字段的集合,它定义了文件其余部分的样式。这个头中包含了一些信息例如代码和数据区的位置和大小,是什么操作系统下的文件,初始堆栈大小,和另外一些我将要讨论到的重要的信息块。和Microsoft的其它一些可执行格式不一样,这个主要的头部不在文件的最开始。PE文件开始的几百个字节被MS-DOS stub占用了。这个stub是一个很小的程序,它输出一些东西比如“This program cannot be run in MS-DOS mode.”。因此如果你在一个不支持Win32的环境中运行一个Win32程序,你将会得到这个错误信息。当Win32加载器映射一个PE文件时,映像文件的第一个字节就是MS-DOS stub的第一个字节。非常正确。在你启动任何一个Win32程序的同时,都有一个基于MS-DOS的程序连带被载入!
和Microsoft的其它可执行格式一样,你可以通过查找它的起始偏移来找到真正的头部,这个偏移被存储在MS-DOS stub头中。WINNT.H头文件中包括一个MS-DOS stub头的结构定义,使得找到PE头的起始位置很容易。e_lfanew域是到真实PE头部的相对偏移(或者叫做 RVA,如果你更喜欢)。要得到内存中PE头的指针,把那个成员的值和映像基址相加即可:
// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;
一旦你有了一个PE主头部的指针,有趣的事情就开始了。 主PE头是一个IMAGE_NT_HEADERS 类型的结构,它被定义在WINNT.H中。这个结构由一个DWORD和两个子结构组成,布局如下:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
Signature域被看作ASCII文本就是“PE/0/0”。如果用了MS-DOS头的域e_lfanew后,你得到了一个NE签名而不是一个PE签名,那么你正在处理的是16位Windows的NE文件。同样地,如果在Signature域中是LE则表示是一个Windows 3.x的虚拟设备驱动程序(VxD)。如果是LX则表示是OS/2 2.0的文件。
PE头中在PE签名DWORD之后是一个IMAGE_FILE_HEADER类型的结构。这个结构的域中只包含了关于这个文件的最基本的信息。这个结构是从最初的 COFF 格式来的,并且似乎并未修改过。除了是PE头的一部分之外,它也出现在Microsoft Win32编译器生成的COFF OBJ文件的最开始。IMAGE_FILE_HEADER结构的各域在表 2中列出。
表 2. IMAGE_FILE_HEADER Fields
WORD Machine
文件运行于哪种类型的CPU之上。下面是已定义的一些CPU ID。
0x14d |
Intel i860 |
|
0x14c |
Intel I386 (same ID used for 486 and 586) |
|
0x162 |
MIPS R3000 |
|
0x166 |
MIPS R4000 |
|
0x183 |
DEC Alpha AXP |
WORD NumberOfSections
文件中节(Section)的数量。
DWORD TimeDateStamp
链接器(或者编译器如果是OBJ文件)生成这个文件的时间。这个域保存的是从1969年12月31日下午4点到生成这个文件时经过的秒数。
DWORD PointerToSymbolTable
COFF符号表的文件偏移量。这个域只在有COFF调试信息的OBJ文件和PE文件中使用。PE文件支持多种调试格式,因此调试器应该指到数据目录(在后面被定义)的IMAGE_DIRECTORY_ENTRY_DEBUG 入口。
DWORD NumberOfSymbols
COFF符号表中的符号的数量。见上面。
WORD SizeOfOptionalHeader
此结构后面的一个可选头的字节大小。在OBJ文件中,这个域是0。在可执行文件中它是这个结构后面紧跟的IMAGE_OPTIONAL_HEADER结构的大小。
WORD Characteristics
关于这个文件的信息的标记。一些重要的域如下:
0x0001 |
文件中没有重定位信息 |
|
0x0002 |
文件是一个可执行的映像(不是一个OBJ或LIB) |
|
0x2000 |
文件是一个DLL不是一个程序 |
其它域定义在WINNT.H中。
PE头的第三个组成部分是一个IMAGE_OPTIONAL_HEADER类型的结构。对于PE文件,这一部分当然不是可选的。COFF格式允许具体的实现超出标准的IMAGE_FILE_HEADER结构定义一个附加信息结构。IMAGE_OPTIONAL_HEADER中的域是PE的设计者认为超出IMAGE_FILE_HEADER中的基本信息外很重要的信息。
并不是所有IMAGE_OPTIONAL_HEADER中的域都是必须要了解的(参见图4)(译注:应该是表 3)。重要的域是ImageBase和Subsystem。你可以跳过其它域的描述。
表 3. IMAGE_OPTIONAL_HEADER Fields
WORD Magic
表现为一些类别的魔数。始终是0x010B。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
生成这个文件的链接器的版本号。这个数字显示成十进制比十六进制要好。比如一个典型的链接器版本号2.23。
DWORD SizeOfCode
所有代码节的大小。通常,大多数文件只有一个代码节,因此这个域就是.text节的大小。
DWORD SizeOfInitializedData
想象中它是已初始化的数据(不包括代码段)组成的所有的节的大小。然而,它似乎和文件中显示的并不一致。
DWORD SizeOfUninitializedData
加载器在虚拟地址空间中提交空间但在磁盘文件中并不占用任何空间的节的大小。这些节在程序启动时不需要指定值,因此有了“未初始化数据”这个术语。未初始化数据通常在一个名称为“.bss”的节中。
DWORD AddressOfEntryPoint
加载器将要开始执行程序的地址。这是一个RVA,并且通常位于“.text”节中。
DWORD BaseOfCode
代码节起始位置的RVA。在内存中,代码节通常在数据节之前PE头之后。在Microsoft的链接器生成的EXE文件中这个RVA通常是0x1000。Borland的TLINK32好像是把映像的基址和第一个代码节的RVA相加存在这个域中。
DWORD BaseOfData
数据节起始位置的RVA。在内存中,数据通常在最后,在PE头和代码节之后。
DWORD ImageBase
当链接器创建一个可执行文件时,它假定这个文件将被映射到内存中的一个指定的位置。那个地址被存在这个域中,假如一个载入地址可以使链接器最优化。如果这个文件真的被加载器映射到那个地址,代码不需要任何修补在运行它之前。在为Windows NT生成的可执行文件中,缺省的ImageBase是0x10000。对于DLL文件,缺省是0x400000。在Windows 95中, 地址0x10000不能被用来加载32位的EXE因为它位于一个被所有进程共享的线性地址空间中。因为此,Microsoft把Win32可执行文件的缺省基址改为0x400000。假定基址为0x10000的老程序在Windows 95下加载将需要更长的时间,因为加载器需要重定位基址。
DWORD SectionAlignment
被映射到内存时,每个节都被保证开始于这个值的整数倍的虚拟地址。为了便于分页缺省的SectionAlignment是0x1000。
DWORD FileAlignment
在PE文件中,组成每个节的原始数据都被保证开始于这个值的整数倍。缺省值是0x200 字节,也是是为了确保每个节总是位于一个磁盘扇区的开头(磁盘扇区的长度也是0x200字节)。这个域的值等价于NE文件中段/资源的对齐大小。不像NE文件,PE文件通常没有好几百那么多的节,因此为了对齐节而浪费的空间几乎总是很少。
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
运行这个可执行文件所必需的最小的操作系统的版本号。这个域有点不明确因为Subsystem 域 (后面会提到) 可以提供一个类似的功能。目前为止在所有的Win32 EXE中这个域缺省是1.0。
WORD MajorImageVersion
WORD MinorImageVersion
一个可由用户定义的域。这允许你的EXE和DLL可以有不同的版本。你可以通过链接器的/VERSION 选项来设置这个域的值。例如,“LINK /VERSION:2.0 myobj.obj”。
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
包含运行这个可执行文件所需要的最小子系统版本号。这个域的一个典型的值是3.10 (表示Windows NT 3.1)。
DWORD Reserved1
似乎总是0。
DWORD SizeOfImage
加载器必须关心的这个映像的总的大小。它是从映像的最开始一直到最后一个节的末尾的大小。最后一个节的末尾按SectionAlignment对齐。
DWORD SizeOfHeaders
PE头和节表的大小。节的实际数据紧跟在所有头部组件之后。
DWORD CheckSum
这个文件的CRC校验和。和Microsoft其它的可执行格式一样,这个域被忽略并被设为0。这个规则的一个例外是信任服务,这些EXE必须有一个有效的校验和。
WORD Subsystem
这个可执行文件为它的用户界面使用的子系统类型。WINNT.H中定义了以下值:
NATIVE |
1 |
不需要子系统(例如一个设备驱动程序) |
|
WINDOWS_GUI |
2 |
在Windows GUI子系统下运行 |
|
WINDOWS_CUI |
3 |
在Windows字符子系统下运行(控制台程序) |
|
OS2_CUI |
5 |
在OS/2 字符子系统下运行(仅对OS/2 1.x) |
|
POSIX_CUI |
7 |
在Posix字符子系统下运行 |
WORD DllCharacteristics
指示一个DLL的初始化方法(例如DllMain)在哪种情况下应该被调用的一组标记。这个值总被设为0,然而操作系统仍然调用DLL的初始化方法在下面所有的四种情况下。
下面的值被定义:
1 |
当DLL第一次被加载到一个进程的地址空间中时调用 |
2 |
当一个线程结束时调用 |
4 |
当启动一个线程时调用 |
8 |
DLL被卸载时调用 |
DWORD SizeOfStackReserve
为初始线程的堆栈保留的虚拟内存数量。然而,并不是所有这些内存都被提交(参见下一个域)。这个域缺省是0x100000 (1MB)。如果你用CreateThread创建线程时指定堆栈大小为0,创建出来的线程也会使用这个域的值作为堆栈大小。
DWORD SizeOfStackCommit
为初始线程的堆栈最开始提交的内存的数量。对于Microsoft的链接器这个域缺省是0x1000字节(1页),然而对于TLINK32是两页。
DWORD SizeOfHeapReserve
为初始进程的堆保留的虚拟内存的数量。可以通过调用GetProcessHeap方法来获得这个堆的句柄。并不是所有的这些内存都被提交。(参见下一个域)。
DWORD SizeOfHeapCommit
在进程堆中最开始被提交的内存数量。缺省是一页。
DWORD LoaderFlags
从WINNT.H文件知道,这些是与调试支持相关的字段。我从来没有见过一个可执行文件中的这些位被置位过,也不清楚链接器怎么使用它们。以下的值被定义:
1. |
在启动进程前调用一个断点指令 |
|
2. |
进程被加载后调用一个调试器 |
DWORD NumberOfRvaAndSizes
数据目录数组中条目的数量(见下面)。这个值总是被当前的工具设为16。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
一个IMAGE_DATA_DIRECTORY结构的数组。开始的数组元素包含可执行文件的重要部分的起始RVA和大小。数组末尾的几个元素当前并没有使用。数组的第一个元素总是导出函数表的址和大小。第二个数组条目是导入函数表的地址和大小,等等。至于完整的已定义数组条目的列表,参见WINNT.H中的IMAGE_DIRECTORY_ENTRY_XXX #defines。这个数组允许加载器快速的定位映像中一个特定的块 (例如导入函数表),而不必通过比较名字来编历映像中的每一个节。大多数数组条目描述一个整个节的数据。然而,IMAGE_DIRECTORY_ENTRY_DEBUG 元素只包含.rdata节中一小部分字节的数据。
(未完待续……)