﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-SongGweRe的Cpp学习笔记-文章分类-Cpp转载</title><link>http://www.cppblog.com/shaoxie1986/category/14881.html</link><description /><language>zh-cn</language><lastBuildDate>Sun, 19 Sep 2010 01:38:57 GMT</lastBuildDate><pubDate>Sun, 19 Sep 2010 01:38:57 GMT</pubDate><ttl>60</ttl><item><title>深入理解 WIN32 PE 文件格式</title><link>http://www.cppblog.com/shaoxie1986/articles/126142.html</link><dc:creator>松娃</dc:creator><author>松娃</author><pubDate>Wed, 08 Sep 2010 02:07:00 GMT</pubDate><guid>http://www.cppblog.com/shaoxie1986/articles/126142.html</guid><wfw:comment>http://www.cppblog.com/shaoxie1986/comments/126142.html</wfw:comment><comments>http://www.cppblog.com/shaoxie1986/articles/126142.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/shaoxie1986/comments/commentRss/126142.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shaoxie1986/services/trackbacks/126142.html</trackback:ping><description><![CDATA[<p>译自：An In-Depth Look into the Win32 Portable Executable File Format </p>
<p>翻译：Jason Sun（木水鱼） </p>
<p>邮件：sjshjz@hotmail.com </p>
<p>2004年7月12日 </p>
<p><strong></strong>&nbsp; </p>
<p><strong></strong></p>
<p><strong>Windows </strong><strong>内幕</strong><strong></strong> </p>
<h2><strong>深入理解</strong><strong> Win32 PE </strong><strong>文件格式</strong></h2>
<p>Matt Pietrek </p>
<p>这篇文章假定你熟悉C++和Win32。 </p>
<p><strong>概述 
</strong>理解可移植可执行文件格式（PE）可以更好地了解操作系统。如果你知道DLL和EXE中都有些什么东西，那么你就是一个知识渊博的程序员。这一系列文章的第一部分，讨论最近这几年PE格式所发生的变化。 
</p>
<p>这次更新后，作者讨论了PE格式如何适应于用.NET开发的应用程序，包括PE节，RVA，数据目录，以及导入函数。附录中包含了相关的映像头结构以及它们的描述。 
</p>
<hr align="center" size="2" width="100%" noshade="">

<p>很早以前，我为微软系统期刊（现在叫做MSDN）写了一篇文章。那篇文章&#8220;Peering Inside the PE: A Tour of the Win32 
Portable Executable File 
Format&#8221;比我所期望的更受人欢迎。直到现在，我仍然能收到使用那篇文章的人（甚至Microsoft里的人）的来信，那篇文章在MSDN中仍然能够找到。不幸的是，那篇文章中存在一些问题。这几年Win32发生了很大变化，那篇文章已经过时了。从这个月开始我将在一篇分成两部分的文章中改正那些问题。 
</p>
<p>你也许会奇怪为什么应该关心可执行文件的格式呢。答案还和过去一样：一个操作系统可执行文件的格式和数据结构揭示了这个底层操作系统的许多东西。通过理解EXE和DLL中到底有些什么，你会成为一个知识更加渊博的程序员。 
</p>
<p>当然，你从微软的规范中也能学到我所告诉你的许多东西。然而，微软的规范为了涵盖全面而牺牲了可读性。而我这篇文章的焦点主要就是讨论 PE 
文件的格式，填补了不适合出现在正式的说明规范中的部分。另外，在这篇文章中也有一些在任何微软官方文档中都没有的好东西。 </p>
<p><strong>Bridging the Gap</strong> </p>
<p>先给出几个自从1994年我写了那篇文章之后 PE 文件格式都发生了哪些变化的例子。由于16位的 Windows 已经成为历史，所以没有必要再和 Win16 
可执行文件格式进行比较了。而另一个淡出人们视野的是 Win32s&#174;。在 Windows3.1 上运行 Win32 程序非常的不稳定，这也是最让人痛恨的事。 
</p>
<p>回到当时，Windows 95 (当时代号为"Chicago") 还没有发布。Windows NT&#174; 仍然是3.5版，Microsoft 
的链接器还没有被有效地优化。然而，当时已经有了 MIPS 和 DEC Alpha 上的 Windows NT 实现。 </p>
<p>那么，自从那篇文章以后又有什么新的东西出现了呢？64位 Windows 在 PE 格式中引入了它自己的变化。Windows CE 
添加了许多新的处理器类型。对动态加载 DLL、节的合并和绑定进行了优化。有许多新的东西被添加进来。 </p>
<p>不要忘了Microsoft&#174; 
.NET。该把它放在什么位置呢？对于操作系统来说，.NET可执行文件就是普通的老的Win32可执行文件。然而，.NET运行时把这些可执行文件中的数据看作对.NET很重要的元数据和中间语言。在本文中，我将敲开 
.NET 元数据格式的大门，但把对它的全部光彩的全面审视留给下一篇文章。 </p>
<p>即使 Win32 
所发生的变化不足以重写一片文章来描述现在的特殊效果，在以前的那篇文章中也存在一些错误，这使我很惭愧。例如，我关于线程本地存储（TLS）支持的描述是错误的。同样的，我对于文件中的时间戳的描述只是当你在美国西部标准时间地区时才精确！ 
</p>
<p>另外，有许多东西以前正确但现在不正确了。我陈述过 .rdata 节并不是真的为每个重要的东西都使用了。而现在，它是。我也说过 .idata 
节是一个可读写的节，但一些尝试进行 API 拦截的人发现它经常不正确。 </p>
<p>随着在这篇文章中对新的 PE 格式的讨论，我也对用于显示 PE 文件内容的 PEDUMP 程序进行了大的修改。PEDUMP 可在 x86 和 IA-64 
平台上编译和运行，可以 dump 32位和64位的 PE 文件。最重要的是，PEDUMP 
的完整源代码可从这篇文章顶部的链接下载得到，这样你就有了本篇文章中所描述的概念和数据结构的一个示例程序。 </p>
<p><strong>PE</strong><strong>文件格式总揽</strong> </p>
<p>Microsoft 
引入了PE文件格式，也就是大家都熟悉的PE格式，是Win32规范的一部分。然而，PE文件来源于更早的基于VAX/VMS的公共对象文件格式（COFF）。由于最初的Windows 
NT小组成员很多都来自数字设备公司（DEC），于是很自然的这些开发者使用已存在的代码以加速新的Windows NT平台的开发。 </p>
<p>使用术语&#8220;可移植可执行&#8221;的目的是为了在所有Windows平台和所有支持的CPU上都有一个统一的文件格式。Windows NT及其以后版本，Windows 
95及其以后版本和Windows CE都使用了这个相同的格式，所以说在很大程度上， 这个目的达到了。 </p>
<p>Microsoft编译器生成的OBJ文件使用COFF格式。通过观察COFF格式的一些域你能知道它有多么老了，那些域使用八进制编码！COFF OBJ 
文件中有许多和PE文件一样的数据结构和枚举，随后我将提到它们中的一些。 </p>
<p>对于64位的Windows， 
PE格式只是进行了很少的修改。这种新的格式被叫做PE32+。没有加入新的域，只有一个域被去除。剩下的改变只是一些域从32位扩展到了64位。在这种情况下，你能写出和32位与64位PE文件都能一起工作的代码。对于C++代码，Windows头文件的能力使这些改变很不明显。 
</p>
<p>EXE和DLL文件之间的不同完全是语义上的。它们都使用完全相同的PE格式。仅有的区别是用了一个单个的位来指出这个文件应该被作为EXE还是一个DLL。甚至DLL文件的扩展名也是不固定的，一些具有完全不同的扩展名的文件也是DLL，比如.OCX控件和控制面板程序（.CPL文件）。 
</p>
<p>PE文件一个方便的特点是磁盘上的数据结构和加载到内存中的数据结构是相同的。加载一个可执行文件到内存中 
(例如，通过调用LoadLibrary)主要就是映射一个PE文件中的几个确定的区域到地址空间中。因此，一个数据结构比如IMAGE_NT_HEADERS 
(稍后我将会解释)在磁盘上和在内存中是一样的。关键的一点是如果你知道怎么在一个PE文件中找到一些东西，当这个PE文件被加载到内存中后你几乎能找到相同的信息。 
</p>
<p>要注意到PE文件并不仅仅是被映射到内存中作为一个内存映射文件。代替的，Windows加载器分析这个PE文件并决定映射这个文件的哪些部分。当映射到内存中时文件中偏移位置较高的数据映射到较高的内存地址处。一个项目在磁盘文件中的偏移也许不同于它被加载到内存中时的偏移。然而，所有被表现出来的信息都允许你进行从磁盘文件偏移到内存偏移的转换 
(参见<strong>图</strong><strong>1</strong>)。 </p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title="偏移" border="0" alt="偏移" src="http://images.cnblogs.com/cnblogs_com/jasonsun/indepthlookpe/offset.gif"> 
</p>
<p><strong>图 1 </strong><strong>偏移</strong> </p>
<p>通过Windows加载器加载PE文件到内存后，内存中的版本被称作一个模块。文件被映射到的起始地址称为HMODULE。有一点值得记住：得到一个HMODULE, 
你就知道那个地址处有些什么数据结构，并且你能找到内存中其它所有的数据结构。这是个很有用的功能，能被用做一些其它目的例如拦截API（Windows 
CE下HMODULE和加载地址并不相同，这些以后再讲）。 </p>
<p>内存中的模块描绘一个进程所需要的可执行文件的所有代码，数据，和资源。PE文件另一些部分只被读取，但不会被映射 
(例如重定位信息)。一些部分根本就不被映射，例如，文件末尾的调试信息。PE头中的一个域可以告诉系统映射一个可执行文件到内存中需要多少内存。不被映射的数据放在文件末尾，这些数据之前的部分将会被映射。 
</p>
<p>描述PE格式（以及COFF文件）的主要地方是在WINNT.H文件中。在这个头文件中，你可以找到要和PE文件一起工作所必须的每个结构定义，枚举，和#define定义。当然，其它地方也有相关文档。例如，MSDN中有&#8220;Microsoft 
Portable Executable and Common Object File Format Specification&#8221; 这篇文章。但WINNT.H 
文件最终决定了PE文件的格式。 </p>
<p>有很多检查PE文件的工具。在它们之中有包含于Visual Studio中的Dumpbin，和包含于Platform 
SDK的Depends。我比较喜欢Depends因为它有一个检查一个文件的导入表和导出表的简洁的方式。Smidgeonsoft(http://www.smidgeonsoft.com)的PEBrowse专业版是一个很优秀的免费的PE观察器。这篇文章中包括的PEDUMP程序功能也很全面，实现了几乎Dumpbin的所有功能。 
</p>
<p>从API的角度来说，Microsoft的IMAGEHLP.DLL 提供了读取和编辑PE文件的机制。 </p>
<p>在我开始讨论PE文件的详细内容之前，让我们首先回顾几个基本概念，这些概念贯穿于整个PE文件格式。下面，我将讨论PE文件的节，相对虚拟地址（RVAs），数据目录，和导入函数的方法。 
</p>
<p><strong>PE</strong><strong>文件的节</strong> </p>
<p>PE文件节包含了代码或某种数据。代码就是程序中的可执行代码，而数据却有很多种。除了可读写的程序数据（例如全局变量）之外，节中的其它类型的数据包括导入和导出表，资源，和重定位表。每个节在内存中都有它自己的属性，包括这个节是否含有代码，它是只读的还是可写的，这个节中的数据是否可在多个进程之间共享。 
</p>
<p>一般而言，一个节中所有的代码和数据都通过一些方法逻辑地联系起来。一个PE文件中通常至少有两个节：一个代码节，一个数据节。一般地，在一个PE文件中至少有一个其它类型的数据节。在这篇文章的第二部分我将讨论这几种节。 
</p>
<p>每个节都有一个不同的名字。这个名字被用来意指节的作用。例如，一个叫做.rdata的节表示一个只读数据节。使用节名只是为了人们方便，对操作系统来说没有任何意义。一个命名为FOOBAR的节和一个命名为.text.的节一样有效。Microsoft通常以一个句点作为节名的前缀，但这不是必需的。多年来，Borland链接器就一直使用像CODE和DATA.这样的节名。 
</p>
<p>编译器有一组它们生成的标准的节，对于它们没有什么不可思议的东西。你可以创建并命名你自己的节，链接器很乐意在可执行文件中包括它们。在Visual 
C++中，你可以让编译器把代码或数据放到通过#pragma 语句命名的节中。例如，下面这条语句 </p>
<p><font color="#000080">#pragma data_seg( "MY_DATA" ) </font></p>
<p>它会使Visual 
C++把它生成的所有数据放到一个命名为MY_DATA的节中，而不是缺省的.data节。大多数程序都使用编译器产生的默认节，但偶尔你也许会有把代码或数据放到一个单独的节中的需求。 
</p>
<p>节并不是全部由链接器生成的，它们其实存在于OBJ文件中，通常由编译器把它们放到那儿。链接器的工作是合并OBJ文件中所有必须的节并且最终放到PE文件相应节中。例如，你的工程中的每个OBJ文件都至少有一个包含代码的.text节。链接器合并这些OBJ文件中的.text节到一个PE文件中的单个的.text节中。同样地，这些OBJ文件中的叫做.data的节被合并到PE文件中一个单个的.data节中。.LIB文件中的代码和数据通常也被包含在可执行文件中，但那个主题已经超出本文的范围了。 
</p>
<p>链接器遵循一整套规则来决定哪些节该被合并以及如何合并。OBJ文件中的某个节也许是提供给链接器使用的，并不会放到最终的可执行文件中去。像这样的节是由编译器用来以传递信息给链接器。 
</p>
<p>节有两种对齐值，一个是在磁盘文件中的偏移另一个是在内存中的偏移。PE文件头指定了这两个对齐值，它们可以是不同的。每个节起始于那个对齐值的倍数的位置。例如，在PE文件中，典型的对齐值是0x200。因此，每个节开始于一个0x200的倍数的文件偏移处。 
</p>
<p>一旦加载到内存中，节总是起始于至少一个页边界。就是说，当一个PE节被映射到内存中后，每个节的第一个字节都符合一个内存页。对于x86 
CPUs，页是4KB，而IA-64，页是8KB。下面显示了PEDUMP输出的Windows XP KERNEL32.DLL 
的.text节和.data节的一小部分。 </p>
<p><font color="#000080">节表 </font></p>
<p><font color="#000080"></font></p><pre><font color="#000080">  01 .text     VirtSize: 00074658  VirtAddr:  00001000
    raw data offs:   00000400  raw data size: 00074800
...
  02 .data     VirtSize: 000028CA  VirtAddr:  00076000
    raw data offs:   00074C00  raw data size: 00002400</font></pre>
<p>&#160;</p>
<p>.text节在PE文件中的偏移为0x400，而在内存中位于KERNEL32加载地址之上第0x1000个字节处。同样的，.data节在PE文件中的偏移为0x74C00，而在内存中位于KERNEL32加载地址之上第0x76000个字节处。 
</p>
<p>创建一个节在文件中的偏移和在内存中的偏移相同的PE文件是可能的。这会使可执行文件变得很大，但在Windows 9x或Windows 
Me.下可以提高加载速度。缺省的/OPT:WIN98 链接器选项(Visual Studio 6.0引入)可以以这种方式创建PE文件。在Visual 
Studio&#174; .NET中，也许会或者也许不会使用/OPT:NOWIN98，这依赖于文件是否足够小。 </p>
<p>链接器的一个有趣的特点是可以合并节。如果两个节有类似的，兼容的特性，它们通常可以在链接时被合并到一个节中。这可通过/merge 
选项做到。例如，下面的链接器选项合并.rdata和.text节到一个单个的命名为.text的节中。 </p>
<p><font color="#000080">/MERGE:.rdata=.text</font> </p>
<p>合并节的好处是可以节省磁盘文件和内存空间。每个节至少要占用一个内存页。如果你能把可执行文件中节的数量从4个减少到3个，你就可以少占用一个内存页。当然，这取决于这两个被合并的节的未使用空间是否达到一页。 
</p>
<p>对于合并节没有什么硬性的规定。例如，可以合并.rdata到.text中，但你不应该把.rsrc，.reloc，或者.pdata合并到其它节中。在Visual 
Studio .NET之前，你可以合并.idata到其它节中。Visual Studio 
.NET,，就不允放过样做了，但当链接一个发布版的时候，链接器经常合并.idata中的一部分到其它节中，例如.rdata。 </p>
<p>既在一部分导入数据是当它们被加载到内存中时由加载器写入的，你也许很奇怪它们怎么能被写入一个只读内存节。这是因为在加载时系统临时把包含导入数据的页面的属性设为可读写。一旦导入表被初始化后，这些页被设置回它们最初的保护属性。 
</p>
<p><strong>相对虚拟地址</strong> </p>
<p>在一个可执行文件中，有许多在内存中的地址必须被指定的位置。例如，当引用一个全局变量时就必须指定它的地址。PE文件可以被加载到进程地址空间的任何位置。虽然它们有一个首选加载地址，但你不能依赖于可执行文件真的会被加载到那个位置。因为这个原因，指定一个地址而不依赖于可执行文件的加载位置就很重要。 
</p>
<p>为了消除PE文件中对内存地址的硬编码，于是产生了RVA。一个RVA是在内存中相对于PE文件被加载的地址的一个偏移。例如，如果一个EXE文件被加载到地址0x400000，它的代码节位于地址0x401000处。那么代码节的RVA就是： 
</p>
<p>(目标地址) 0x401000 - (加载地址)0x400000 = (RVA)0x1000. </p>
<p>要把一个RVA转换为实际地址，进行相反的步骤就行了：把RVA和实际加载地址相加就可得到实际内存地址。顺便说一下，实际内存地址在PE中被称为虚拟地址（VA）。另外也可以认为一个VA是加上首选加载地址的RVA。不要忘了我以前说过的，加载地址和HMODULE是一样的。 
</p>
<p>你是否想研究一下一些DLL在内存中的数据结构呢？这里有一个方法。以这个DLL的名字作为参数调用GetModuleHandle函数。返回的HMODULE是一个加载地址；你可以应用你的PE文件结构的知识找到这个模块中的任何你想要的东西。 
</p>
<p><strong>数据目录</strong> </p>
<p>在可执行文件中有许多数据结构需要被快速定位。一些明显的例子是导入表，导出表，资源，和基址重定位表。所有这些众所周知的数据结构都可通过一致的方式被找到，就是数据目录。 
</p>
<p>数据目录是一个由16个结构组成的数组。每个数组元素都预定义了它所代表的含意。IMAGE_DIRECTORY_ENTRY_ xxx 
定义了数据目录的数组索引(从0到15)。图2描述了每个IMAGE_DATA_DIRECTORY_xxx值分别表示了什么。这篇文章的第2部分包含了对其所指向的数据结构的更详细的描述。 
</p>
<p><strong>图 2 IMAGE_DATA_DIRECTORY 值</strong> 
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">
<p>值</p></td>
<td valign="top">
<p>描述</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_EXPORT</p></td>
<td valign="top">
<p>指向导出表(一个IMAGE_EXPORT_DIRECTORY结构)。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_IMPORT</p></td>
<td valign="top">
<p>指向导入表(一个IMAGE_IMPORT_DESCRIPTOR结构数组)。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_RESOURCE</p></td>
<td valign="top">
<p>指向资源(一个IMAGE_RESOURCE_DIRECTORY结构。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_EXCEPTION</p></td>
<td valign="top">
<p>指向异常处理表(一个IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)。CPU特定的并且基于表的异常处理。用于除x86之外的其它CPU上。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_SECURITY</p></td>
<td valign="top">
<p>指向一个WIN_CERTIFICATE结构的列表，它定义在WinTrust.H中。不会被映射到内存中。因此，VirtualAddress域是一个文件偏移，而不是一个RVA。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_BASERELOC</p></td>
<td valign="top">
<p>指向基址重定位信息。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_DEBUG</p></td>
<td valign="top">
<p>指向一个IMAGE_DEBUG_DIRECTORY结构数组，其中每个结构描述了映像的一些调试信息。早期的Borland链接器设置这个IMAGE_DATA_DIRECTORY结构的Size域为结构的数目，而不是字节大小。要得到IMAGE_DEBUG_DIRECTORY结构的数目，用IMAGE_DEBUG_DIRECTORY 
的大小除以这个Size域。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_ARCHITECTURE</p></td>
<td valign="top">
<p>指向特定架构数据，它是一个IMAGE_ARCHITECTURE_HEADER结构数组。不用于x86或IA-64，但看来已用于DEC/Compaq 
Alpha。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_GLOBALPTR</p></td>
<td valign="top">
<p>在某些架构体系上VirtualAddress域是一个RVA，被用来作为全局指针（gp）。不用于x86，而用于IA-64。Size域没有被使用。参见2000年11月的Under 
The Hood 专栏可得到关于IA-64 gp的更多信息。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_TLS</p></td>
<td valign="top">
<p>指向线程局部存储初始化节。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG</p></td>
<td valign="top">
<p>指向一个IMAGE_LOAD_CONFIG_DIRECTORY结构。IMAGE_LOAD_CONFIG_DIRECTORY中的信息是特定于Windows 
NT、Windows 2000和 Windows XP的(例如 GlobalFlag 
值)。要把这个结构放到你的可执行文件中，你必须用名字__load_config_used 
定义一个全局结构，类型是IMAGE_LOAD_CONFIG_DIRECTORY。对于非x86的其它体系，符号名是_load_config_used 
(只有一个下划线)。如果你确实要包含一个IMAGE_LOAD_CONFIG_DIRECTORY，那么在 C++ 
中要得到正确的名字比较棘手。链接器看到的符号名必须是__load_config_used (两个下划线)。C++ 
编译器会在全局符号前加一个下划线。另外，它还用类型信息修饰全局符号名。因此，要使一切正常，在 C++ 中就必须像下面这样使用： </p>
<p>extern "C" </p>
<p>IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...}</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT</p></td>
<td valign="top">
<p>指向一个 
IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组，对应于这个映像绑定的每个DLL。数组元素中的时间戳允许加载器快速判断绑定是否是新的。如果不是，加载器忽略绑定信息并且按正常方式解决导入API。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_IAT</p></td>
<td valign="top">
<p>指向第一个导入地址表(IAT)的开始位置。对应于每个被导入DLL的IAT都连续地排列在内存中。Size域指出了所有IAT的总的大小。在写入导入函数的地址时加载器使用这个地址和Size域指定的大小临时地标记IAT为可读写。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT</p></td>
<td valign="top">
<p>指向延迟加载信息，它是一个CImgDelayDescr结构数组，定义在Visual 
C++的头文件DELAYIMP.H中。延迟加载的DLL直到对它们中的API进行第一次调用发生时才会被装入。Windows中并没有关于延迟加载DLL的知识，认识到这一点很重要。延迟加载的特征完全是由链接器和运行时库实现的。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR</p></td>
<td valign="top">
<p>在最近更新的系统头文件中这个值已被改名为IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可执行文件中.NET信息的最高级别信息，包括元数据。这个信息是一个IMAGE_COR20_HEADER结构。</p></td></tr></tbody></table></p>
<p><strong>导入函数</strong> </p>
<p>当你使用其它DLL中的代码或数据时，就要导入它。加载一个PE文件时，Windows 
加载器的一个工作就是查找所有被导入的函数和数据并让那此函数和数据的地址可被加载的文件使用。完成这个工作所用到的数据结构的细节放到这篇文章的第二部分进行讨论，在这里学习一下这些概念。 
</p>
<p>当你直接调用到一个DLL的代码或数据时，你就是正在隐式地链接到这个DLL。要使被导入的API的地址可被你的代码使用你不需要做任何事情。加载器会完成所有需要做的工作。另外还有显式链接。意思就是说显式地加载目标DLL并查找API的地址。这几乎总是通过LoadLibrary和GetProcAddress来实现的。 
</p>
<p>当你隐式地链接一个API时，类似LoadLibrary和GetProcAddress的代码仍然被执行了，只不过是由加载器代替你自动执行的。加载器也会确保被加载的PE文件所需要的任何附加的DLL也被加载。例如，由Visual 
C++&#174;链接器创建的每个正常的程序都要链接KERNEL32.DLL。而KERNEL32.DLL又从NTDLL.DLL导入函数。同样，如果你从GDI32.DLL导入函数，也将会依赖于USER32，ADVAPI32，NTDLL和KERNEL32 
DLL。加载器会保证这些DLL都被加载并且解决所有导入问题。(Visual Basic 6.0和Microsoft .NET 
可执行文件直接链接到另外一个DLL而不是KERNEL32，但原理是相同的。) </p>
<p>隐式链接时，对主EXE文件和所有依赖的DLL的处理发生在程序第一次启动时。如果出现了任何问题（例如，一个被引用的DLL没有找到），进程将被终止。 </p>
<p>Visual C++ 
6.0引入了延迟加载的功能，它是隐式链接和显式链接的混合体。在延迟加载一个DLL时，链接器生成一些和正常导入一个DLL时非常相似的数据。然而，操作系统忽略这些数据。代替的，第一次调用一个延迟加载的API时，DLL才会被加载（如果还没有加载到内存中），然后调用GetProcAddress方法得到被调用API的地址。以后如果再调用这个API将会和这个API被正常导入时有着一样的效率。 
</p>
<p>在PE文件中，对于每个被导入的DLL都有一个数据结构的数组。这些结构给出被导入DLL的名称并指向一个函数指针数组。这个函数指针数组就是导入地址表（IAT）。每个被导入的API在IAT中都有它自己的位置，导入函数的地址由Windows加载器写入到那个位置中。最后一点非常重要：一旦一个模块被加载，IAT中包含所要调用导入函数的地址。 
</p>
<p>IAT的优点是在一个PE文件中只有一个地方保存了被导入API的地址。不管源文件中多少次调用一个API，都会通过IAT中同一个函数指针来完成。 </p>
<p>让我们看一下怎样调用一个被导入的API。需要考虑两种情况：高效的和低效的。最好的情况，调用一个导入API看起来应该像下面这样： </p>
<p><font color="#000080">CALL DWORD PTR [0x00405030] </font></p>
<p>这是通过函数指针进行调用。无论怎样，0x405030地址处的DWORD值就是这个CALL指令将把控制转移到的地址。在前面例子中，地址0x405030就位于IAT中。 
</p>
<p>低效的调用看起来像下面这样： </p>
<p><font color="#000080">CALL 0x0040100C </font></p>
<p><font color="#000080">... </font></p>
<p><font color="#000080">0x0040100C: </font></p>
<p><font color="#000080">JMP DWORD PTR [0x00405030] </font></p>
<p>这种情况下，CALL把控制转到一个小的程序段处。这段程序通过JMP指令跳转到0x405030地址处。记住0x405030位于IAT中。低效调用导入函数用到了五个字节的额外代码，并且由于使用JMP指令花费了更长的执行时间。 
</p>
<p>你也许会奇怪为什么要使用低效的方法呢。有一个很好的解释。编译器无法区分导入函数调用和普通函数调用。因此，编译器生成同样形式的CALL指令 </p>
<p><font color="#000080">CALL XXXXXXXX </font></p>
<p>XXXXXXXX是一个稍后由链接器填充的实际地址。要注意这个CALL指令后面的地址并不是一个函数指针，而是一段实际代码的地址。链接器必须提供一块代码来替换这个XXXXXXXX。这样做的最简单的方法就是调用到一个JMP 
stub，就像你在上面看到的那样。 </p>
<p>这个JMP stub从哪儿来呢？很令人惊奇，它来自于导入函数的导入库。如果你检查一个导入库，并且用导入API的名称来检查代码，你将会发现和上面JMP 
stub很相似的代码。这就是说缺省情况下将使用低效形式调用导入API。 </p>
<p>那么，下一个要问的问题就是怎样才能得到优化的形式。答案是给编译器一个提示。__declspec(dllimport)函数修饰符告诉编译器这个函数位于其它DLL中，于是编译器将生成指令 
</p>
<p><font color="#000080">CALL DWORD PTR [XXXXXXXX] </font></p>
<p>而不是： </p>
<p><font color="#000080">CALL XXXXXXXX </font></p>
<p>另外，编译器也生成一些信息以告诉链接器把这个指令的函数指针部分解析为一个符号名__imp_functionname。例如，如果你正在调用MyFunction，符号名就是__imp_MyFunction。查看一个导入库，你会发现除了正常的符号名外，也有一个加了__imp__前缀的符号。__imp__ 
symbol可以直接定位到IAT入口，而不是通过那个JMP stub。 </p>
<p>那么这对你以后每天的生活有什么影响呢？如果你正在编写导出函数并为它们提供一个头文件，记住要使用这个__declspec(dllimport)修饰符： 
</p>
<p><font color="#000080">__declspec(dllimport) void Foo(void); </font></p>
<p>如果你查看Windows系统头文件，你会发现Windows 
API都使用了__declspec(dllimport)。它并不容易被发现。你可在WINNT.H头文件中找到DECLSPEC_IMPORT 
宏定义，而这个宏被用在一些文件中例如WinBase.H。到这里你就会明白__declspec(dllimport)是如何被用在系统API声明上的。 </p>
<p><strong>PE </strong><strong>文件结构</strong> </p>
<p>现在来让我们研究PE文件的实际格式。我将从文件的开头开始，并描述在每个PE文件中都会出现的数据结构。然后，我将描述在一个PE节中的更特殊的数据结构（例如导入表和资源）。下面我将讨论的所有数据结构都定义在WINNT.H中，除非另有说明。 
</p>
<p>通常，这些结构都有 32 位和 64 位之分---例如 IMAGE_NT_HEADERS32 
和IMAGE_NT_HEADERS64。这些结构除了一些域被扩展为 64 位外几乎是一样的。如果你正在试着编写可移植的代码，WINNT.H 文件中有一些 
#defines 定义可以用来选择使用32位还是 64 
位的结构并且给它们起了一个与大小无关的别名(对于前面的例子这个别名就是IMAGE_NT_HEADERS)。具体选择哪一个结构依赖于你正在以哪种模式编译(是否定义了_WIN64)。只有在 
PE 文件的目标执行平台的大小属性与正在编译的平台的大小属性不同时才需要使用特定的 32 位或 64 位版本的结构。 </p>
<p><strong>MS-DOS</strong><strong>头</strong> </p>
<p>每个PE文件都以一个小的MS-DOS可执行体开头。在Windows早期很多消费者并没有安装Windows，所以就需要存在这个MS-DOS可执行体。当在没有安装Windows的机器上执行时，这段程序至少能打印一条信息来说明必须在Windows上才能执行这个可执行文件。 
</p>
<p>PE文件以一个传统的MS-DOS头开头，被称为IMAGE_DOS_HEADER。其中只有两个重要的值，它们是e_magic和e_lfanew。e_lfanew域包含PE头的文件偏移。e_magic域(一个WORD)必须被设为0x5A4D。对于这个值有个常量定义，叫做IMAGE_DOS_SIGNATURE。用ASCII字符表示, 
0x5A4D就是&#8220;MZ&#8221;，这是MS-DOS最初设计者之一Mark Zbikowski名子的首字母大写。 </p>
<p><strong>IMAGE_NT_HEADERS</strong><strong>头</strong> </p>
<p>IMAGE_NT_HEADERS 结构是存储 PE 文件细节信息的主要位置。它的偏移由这个文件开头的 IMAGE_DOS_HEADER 的 
e_lfanew 域给出。实际上有两个版本的IMAGE_NT_HEADER 结构，一个用于 32 位可执行文件，另一个用于 64 
位版本。它们之间的区别很小，在讨论中我将认为它们是相同的。区别这两种格式的唯一正确的、由Microsoft 认可的方法是通过 
IMAGE_OPTIONAL_HEADER 结构（马上就会讲到）的 Magic 域的值。 </p>
<p>IMAGE_NT_HEADER由三个字段组成： </p>
<p><font color="#000080">typedef struct _IMAGE_NT_HEADERS { </font></p>
<p><font color="#000080">DWORD Signature; </font></p>
<p><font color="#000080">IMAGE_FILE_HEADER FileHeader; </font></p>
<p><font color="#000080">IMAGE_OPTIONAL_HEADER32 OptionalHeader; </font></p>
<p><font color="#000080">} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;</font> </p>
<p>在一个有效的PE文件中，Signature字段的值是0x00004550，用ASCII表示就是&#8220;PE00&#8221;。 #define 
IMAGE_NT_SIGNATURE定义了这个值。第二个域是一个IMAGE_FILE_HEADER类型的结构，它包含了关于这个文件的一些基本的信息，最重要的是其中一个域指出了其后的可选数据的大小。在PE文件中，这个可选数据是必须的，但仍然被称为IMAGE_OPTIONAL_HEADER。 
</p>
<p>图3显示了IMAGE_FILE_HEADER 结构的域以及对这些域的注释。这个结构在COFF格式的OBJ文件开头也可以找到。图 4 
列出了IMAGE_FILE_xxx通常的取值。图5显示了IMAGE_OPTIONAL_HEADER 结构的成员。 </p>
<p>IMAGE_OPTIONAL_HEADER结构末尾的数据目录数组用来定位可执行文件中的重要数据的地址。每个数据目录条目看起来就像下面这样： </p>
<p><font color="#000080">typedef struct _IMAGE_DATA_DIRECTORY { </font></p>
<p><font color="#000080">DWORD VirtualAddress; // RVA of the data </font></p>
<p><font color="#000080">DWORD Size; // Size of the data </font></p>
<p><font color="#000080">}; </font></p>
<p><strong>图 3 IMAGE_FILE_HEADER </strong>
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">
<p>大小</p></td>
<td valign="top">
<p>域</p></td>
<td valign="top">
<p>描述</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>Machine</p></td>
<td valign="top">
<p>可执行文件的目标CPU。通常的值是： </p>
<p><font color="#000080">IMAGE_FILE_MACHINE_I386 0x014c // Intel 386 </font></p>
<p><font color="#000080">IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 
64</font></p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>NumberOfSections</p></td>
<td valign="top">
<p>指出节表中有多少个节。节表紧跟在IMAGE_NT_HEADERS之后。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>TimeDateStamp</p></td>
<td valign="top">
<p>指出这个文件被创建的时间。这个值是用格林尼治时间(GMT)计算的自从1970年1月1日以来所经过的秒数。这个值比文件系统的日期/时间更准确地指出了文件被创建的时间。使用_ctime 
函数(对时区敏感)可以很容易地把这个值转换为人们可读的字符串形式。另一个有用的函数是gmtime。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>PointerToSymbolTable</p></td>
<td valign="top">
<p>COFF符号表的文件偏移，描述于Microsoft规范的5.4节。COFF符号表在PE文件中很少见，因为出现了新的调试格式。Visual Studio 
.NET之前，可通过指定链接器选项/DEBUGTYPE:COFF来创建COFF符号表。COFF符号表几乎总是会出现在OBJ文件中。如果没有符号表则设此值为0。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>NumberOfSymbols</p></td>
<td valign="top">
<p>如果存在COFF符号表，此域表示其中的符号的数目。COFF符号是一个固定大小的结构，要找到COFF符号表的末尾就必须用到此域。紧跟COFF符号之后是一个用来保存较长符号名的字符串表。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>SizeOfOptionalHeader</p></td>
<td valign="top">
<p>IMAGE_FILE_HEADER 
之后的可选数据的大小。在PE文件中，这个数据称为IMAGE_OPTIONAL_HEADER。这个大小在32位和64位的文件中是不同的。对于32位PE文件，这个域通常是224。对于64位PE32+文件，它通常是240。然而，这些值只是所要求的最小值，更大的值也可能会出现。</p></td></tr>
<tr>
<td valign="top">
<p>WORD </p></td>
<td valign="top">
<p>Characteristics</p></td>
<td valign="top">
<p>一组指示文件属性的位标。这些标记的有效值是定义于WINNT.H文件中的IMAGE_FILE_xxx值。一些常用的值在图4中列出。</p></td></tr></tbody></table></p>
<p><a name="fig4"><strong></strong></a></p>
<p><strong>图 4 IMAGE_FILE_XXX</strong> 
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">
<p>值</p></td>
<td valign="top">
<p>描述</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_RELOCS_STRIPPED </p></td>
<td valign="top">
<p>文件中不包括重定位信息。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_EXECUTABLE_IMAGE </p></td>
<td valign="top">
<p>文件是可执行的。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_AGGRESIVE_WS_TRIM</p></td>
<td valign="top">
<p>让操作系统强制整理工作区。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_LARGE_ADDRESS_AWARE</p></td>
<td valign="top">
<p>应用程序可处理超过2GB的地址。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_32BIT_MACHINE</p></td>
<td valign="top">
<p>需要一个32位的机器。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_DEBUG_STRIPPED </p></td>
<td valign="top">
<p>调试信息位于一个.DBG文件中。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP</p></td>
<td valign="top">
<p>如果映像在可移动媒体中，那么复制到交换文件并从交换文件中运行。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_NET_RUN_FROM_SWAP</p></td>
<td valign="top">
<p>如果映像在网络上，那么复制到交换文件并从交换文件中运行。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_DLL</p></td>
<td valign="top">
<p>是一个DLL文件。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_FILE_UP_SYSTEM_ONLY</p></td>
<td valign="top">
<p>只能在单处理器机器中运行。</p></td></tr></tbody></table></p>
<p><strong>图 5 IMAGE_OPTIONAL_HEADER</strong> </p>
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">
<p>Size</p></td>
<td valign="top">
<p>Structure Member</p></td>
<td valign="top">
<p>Description</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>Magic</p></td>
<td valign="top">
<p>一个签名，确定这是什么类型的头。两个最常用的值是IMAGE_NT_OPTIONAL_HDR32_MAGIC 
0x10b和IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b.</p></td></tr>
<tr>
<td valign="top">
<p>BYTE</p></td>
<td valign="top">
<p>MajorLinkerVersion</p></td>
<td valign="top">
<p>创建可执行文件的链接器的主版本号。对于Microsoft的链接器生成的PE文件，这个版本号的Visual 
Studio的版本号相一致(例如，版本6表示Visual Studio 6.0)。</p></td></tr>
<tr>
<td valign="top">
<p>BYTE</p></td>
<td valign="top">
<p>MinorLinkerVersion</p></td>
<td valign="top">
<p>创建可执行文件的链接器的次版本号。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfCode</p></td>
<td valign="top">
<p>所有具有IMAGE_SCN_CNT_CODE属性的节的总的大小。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfInitializedData</p></td>
<td valign="top">
<p>所有包含已初始数据的节的总的大小。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfUninitializedData</p></td>
<td valign="top">
<p>所有包含未初始化数据的节的总的大小。这个域总是0，因为链接器可以把未初始化数据附加到常规数据节的末尾。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>AddressOfEntryPoint</p></td>
<td valign="top">
<p>文件中将被执行的第一个代码字节的RVA。对于DLL，这个进入点将在进程初始化和关闭时以及线程被创建和销毁时调用。在大多数可执行文件中，这个地址并不直接指向main，WinMain或DllMain函数，而是指向运行时库代码，由运行时库调用前述函数。在DLL中，这个域可以被设为0，这样的话上面所说的通知就不能被接收到。链接器选项/NOENTRY可以设置这个域为0。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>BaseOfCode</p></td>
<td valign="top">
<p>加载到内存后代码的第一个字节的RVA。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>BaseOfData</p></td>
<td valign="top">
<p>理论上，它表示加载到内存后数据的第一个字节的RVA。然而，这个域的值对于不同版本的Microsoft链接器是不一致的。在64位的可执行文件中这个域不出现。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>ImageBase</p></td>
<td valign="top">
<p>文件在内存中的首选加载地址。加载器尽可能地把PE文件加载到这个地址(就是说，如果当前这块内存没有被占用，它是对齐的并且是一个合法的地址，等等)。如果可执行文件被加载到这个地址，加载器就可以跳过进行基址重定位（在这篇文章的第二部分描述）这一步。对于EXE，缺省的ImageBase是0x400000。对于DLL，缺省是0x10000000。在链接时可以通过/BASE 
选项来指定ImageBase，或者以后用REBASE工具重新设置。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SectionAlignment</p></td>
<td valign="top">
<p>加载到内存后节的对齐大小。这个值必须大于等于FileAlignment（下一个域）。缺省的对齐值是目标CPU的页大上。对于运行在Windows 
9x或Windows Me下的用户模式可执行文件，最小对齐大小是一页(4KB)。这个域可以通过链接器选项/ALIGN来设置。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>FileAlignment</p></td>
<td valign="top">
<p>在PE文件中节的对齐大小。对于x86下的可执行文件，这个值通常是0x200或0x1000。不同版本的Microsoft链接器缺省值不同。这个值必须是2的幂，并且如果SectionAlignment小于CPU的页大小，这个域必须和SectionAlignment相匹配。链接器选项/OPT:WIN98可设置x86可执行文件的文件对齐为0x1000，/OPT:NOWIN98设置文件对齐为0x200。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>MajorOperatingSystemVersion</p></td>
<td valign="top">
<p>所要求的操作系统的主版本号。随着那么多版本Windows的出现，这个域的值就变得很不确切。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>MinorOperatingSystemVersion</p></td>
<td valign="top">
<p>所要求的操作系统的次版本号。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>MajorImageVersion</p></td>
<td valign="top">
<p>这个文件的主版本号。不被系统使用并可设为0。可以通过链接器选项/VERSION来设置。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>MinorImageVersion</p></td>
<td valign="top">
<p>这个文件的次版本号。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>MajorSubsystemVersion</p></td>
<td valign="top">
<p>可执行文件所要求的操作子系统的主版本号。它曾经被用来表示需要较新的Windows 95或Windows NT用户界面，而不是老版本的Windows 
NT界面。今天随着各种不同版本Windows的出现，这个域已不被系统使用，并且通常被设为4。可通过链接器选项/SUBSYSTEM设置这个域的值。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>MinorSubsystemVersion</p></td>
<td valign="top">
<p>可执行文件所要求的操作子系统的次版本号。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>Win32VersionValue</p></td>
<td valign="top">
<p>另一个不被使用的域，通常设为0。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfImage</p></td>
<td valign="top">
<p>映像的大小。它表示了加载文件到内存中时系统必须保留的内存的数量。这个域的值必须是SectionAlignmnet的倍数。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfHeaders</p></td>
<td valign="top">
<p>MS-DOS头，PE头和节表的总的大小。PE文件中所有这些项目出现在任何代码或数据节之前。这个域的值被调整为文件对齐大小的整数倍。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>CheckSum</p></td>
<td valign="top">
<p>映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算出这个值。校验和用于内核模式的驱动和一些系统DLL。对于其它的，这个域可以为0。当使用链接器选项/RELEASE时校验和被放入文件中。</p></td></tr>
<tr>
<td valign="top">
<p>WORD </p></td>
<td valign="top">
<p>Subsystem</p></td>
<td valign="top">
<p>指示可执行文件期望的子系统（用户界面类型）的枚举值。这个域只用于EXE。一些重要的值包括：</p><pre><font color="#000080">IMAGE_SUBSYSTEM_NATIVE
		// 映像不需要子系统
IMAGE_SUBSYSTEM_WINDOWS_GUI
		// 使用Windows GUI
IMAGE_SUBSYSTEM_WINDOWS_CUI
		// 作为控制台程序运行。
		// 运行时，操作系统创建一个控制台
		// 窗口并提供stdin，stdout和stderr
		// 文件句柄。</font></pre></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>DllCharacteristics</p></td>
<td valign="top">
<p>标记DLL的特性。对应于IMAGE_DLLCHARACTERISTICS_xxx定义。当前的值是：</p><pre><font color="#000080">IMAGE_DLLCHARACTERISTICS_NO_BIND
		// 不要绑定这个映像
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER
		// WDM模式的驱动程序
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE
		// 当终端服务加载一个不是
		// Terminal- Services-aware 的应用程
		// 序时，它也加载一个包含兼容代码
		// 的DLL。</font></pre></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfStackReserve</p></td>
<td valign="top">
<p>在EXE文件中，为线程保留的堆栈大小。缺省是1MB，但并不是所有的内存一开始都被提交。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfStackCommit</p></td>
<td valign="top">
<p>在EXE文件中，为堆栈初始提交的内存数量。缺省情况下，这个域是4KB。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfHeapReserve</p></td>
<td valign="top">
<p>在EXE文件中，为默认进程堆初始保留的内存大小。缺省是1MB。然而在当前版本的Windows中，堆不经过用户干涉就能超出这里指定的大小。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfHeapCommit</p></td>
<td valign="top">
<p>在EXE文件中，提交到堆的内存大小。缺省情况下，这里的值是4KB。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>LoaderFlags</p></td>
<td valign="top">
<p>不使用。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>NumberOfRvaAndSizes</p></td>
<td valign="top">
<p>在IMAGE_NT_HEADERS结构的末尾是一个IMAGE_DATA_DIRECTORY结构数组。此域包含了这个数组的元素个数。自从最早的Windows 
NT发布以来这个域的值一直是16。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_</p></td>
<td valign="top">
<p>DataDirectory[16]</p></td>
<td valign="top">
<p>一个IMAGE_DATA_DIRECTORY结构数组。每个结构都包含了可执行文件中一些重要数据的RVA和大小（例如导入表，导出表和资源）。</p></td></tr></tbody></table>
<p><strong>节表</strong> </p>
<p>IMAGE_NT_HEADERS之后紧跟着节表。节表是一个IMAGE_SECTION_HEADER结构数组。IMAGE_SECTION_HEADER提供了和它关联的节的信息，包括位置，长度和属性。图6描述了IMAGE_SECTION_HEADER结构的各域。在IMAGE_FILE_HEADER结构中的NumberOfSections 
域中提供了IMAGE_SECTION_HEADER 结构的数目。 </p>
<p>可执行文件中的节的文件对齐对最终的文件大小有很大的影响。在Visual Studio 6.0中， 
链接器缺省的对齐大小为4KB，除非使用了/OPT:NOWIN98或/ALIGN选项。Visual Studio 
.NET链接器也缺省使用了/OPT:WIN98选项，但它检测可执行文件的大小是否小于某个值，如果是则使用0x200字节进行对齐。 </p>
<p>另一个值得注意的对齐方式来自.NET文件规范。它规定.NET可执行文件的内存对齐值是8KB，而不是x86平台的4KB。这是为了保证在x86平台上创建的可执行文件在IA-64平台上仍然可以运行。如果节的内存对齐值是4KB，IA-64加载器就不能加载这个文件，因为64位Windows的页大小是8KB。 
</p>
<p><strong>图 6 IMAGE_SECTION_HEADER </strong>
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">
<p>大小</p></td>
<td valign="top">
<p>域</p></td>
<td valign="top">
<p>描述</p></td></tr>
<tr>
<td valign="top">
<p>BYTE </p></td>
<td valign="top">
<p>Name[8]</p></td>
<td valign="top">
<p>节的ASCII名称。节名不保证一定是以NULL结尾的。如果你指定了长于8个字符的节名，链接器会把它截短为8个字符。在OBJ文件中存在一个机制允许更长的节名。节名通常以一个句点开始，但这并不是必须的。节名中有一个&#8220;$&#8221;时链接器会对之进行特殊处理。前面带有&#8220;$&#8221;的相同名字的节将会被合并。合并的顺序是按照&#8220;$&#8221;后面字符的字母顺序进行合并的。关于名字中带有&#8220;$&#8221;的节以及这些节怎样被合并有很多的主题，但这些细节已超出本文所讨论的范围了。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>Misc.VirtualSize</p></td>
<td valign="top">
<p>指出实际被使用的节的大小。这个域的值可以大于或小于SizeOfRawData域的值。如果VirtualSize的值大，SizeOfRawData就是可执行文件中已初始化数据的大小，剩下的字节用0填充。在OBJ文件中这个域被设为0。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>VirtualAddress</p></td>
<td valign="top">
<p>在可执行文件中，是节被加载到内存中后的RVA。在OBJ文件中应该被设为0。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>SizeOfRawData</p></td>
<td valign="top">
<p>在可执行文件或OBJ文件中该节所占用的字节大小。对于可执行文件，这个值必须是PE头中给出的文件对齐值的倍数。如果是0，则说明这个节中的数据是未初始的。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>PointerToRawData</p></td>
<td valign="top">
<p>节在磁盘文件中的偏移。对于可执行文件，这个值必须是PE头部给出的文件对齐值的倍数。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>PointerToRelocations</p></td>
<td valign="top">
<p>节的重定位数据的文件偏移。只用于OBJ文件，在可执行文件中被设为0。对于OBJ文件，如果这个域的值不为0的话，它就指向一个IMAGE_RELOCATION结构数组。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>PointerToLinenumbers</p></td>
<td valign="top">
<p>节的COFF样式行号的文件偏移。如果非0，则指向一个IMAGE_LINENUMBER结构数组。只在COFF行号被生成时使用。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>NumberOfRelocations</p></td>
<td valign="top">
<p>PointerToRelocations 指向的重定位的数目。在可执行文件中应该是0。</p></td></tr>
<tr>
<td valign="top">
<p>WORD</p></td>
<td valign="top">
<p>NumberOfLinenumbers</p></td>
<td valign="top">
<p>NumberOfRelocations 域指向的行号的数目。只在COFF行号被生成时使用。</p></td></tr>
<tr>
<td valign="top">
<p>DWORD</p></td>
<td valign="top">
<p>Characteristics</p></td>
<td valign="top">
<p>被或到一起的一些标记，用来表示节的属性。这些标记中很多都可以通过链接器选项/SECTION来设置。常用值在图7中列出。</p></td></tr></tbody></table></p>
<p><strong>图 7</strong><strong> Flags </strong>
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">
<p>值</p></td>
<td valign="top">
<p>描述</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_CNT_CODE</p></td>
<td valign="top">
<p>节中包含代码。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_MEM_EXECUTE</p></td>
<td valign="top">
<p>节是可执行的。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_CNT_INITIALIZED_DATA</p></td>
<td valign="top">
<p>节中包含已初始化数据。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_CNT_UNINITIALIZED_DATA</p></td>
<td valign="top">
<p>节中包含未初始化数据。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_MEM_DISCARDABLE</p></td>
<td valign="top">
<p>节可被丢弃。用于保存链接器使用的一些信息，包括.debug$节。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_MEM_NOT_PAGED</p></td>
<td valign="top">
<p>节不可被页交换，因此它总是存在于物理内存中。经常用于内核模式的驱动程序。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_MEM_SHARED</p></td>
<td valign="top">
<p>包含节的数据的物理内存页在所有用到这个可执行体的进程之间共享。因此，每个进程看到这个节中的数据值都是完全一样的。这对一个进程的所有实例之间共享全局变量很有用。要使一个节共享，可使用/section:name,S 
链接器选项。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_MEM_READ</p></td>
<td valign="top">
<p>节是可读的。几乎总是被设置。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_MEM_WRITE</p></td>
<td valign="top">
<p>节是可写的。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_LNK_INFO</p></td>
<td valign="top">
<p>节中包含链接器使用的信息。只在OBJ文件中存在。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_LNK_REMOVE</p></td>
<td valign="top">
<p>节中的数据不会成为映像的一部分。只出现在OBJ文件中。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_LNK_COMDAT</p></td>
<td valign="top">
<p>节中的内容是公共数据(comdat)。公共数据是指可被定义在多个OBJ文件中的数据。链接器将选择一个包含到可执行文件中。Comdat 
对于支持C++模板函数和在函数级别上的链接是至关重要的。Comdat节只出现在OBJ文件中。</p></td></tr>
<tr>
<td valign="top">
<p>IMAGE_SCN_ALIGN_XBYTES</p></td>
<td valign="top">
<p>在最终的可执行文件中这个节中数据的对齐大小。它可有许多取值(_4BYTES，_8BYTES，_16BYTES等)。如果没有被指定，缺省是16字节。这些标记只在OBJ文件中被设置。</p></td></tr></tbody></table></p>
<p><strong>结束语</strong> </p>
<p>PE 
文件头就讨论到这儿。在这篇文章的第2部分我将继续讨论经常遇到的一些节。然后我将描述那些节中的主要的数据结构，包括导入表，导出表以及资源。最后，我会讨论一下最新的、具有很大改进的 
PEDUMP 程序的源代码。&nbsp;</p>
<img src ="http://www.cppblog.com/shaoxie1986/aggbug/126142.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shaoxie1986/" target="_blank">松娃</a> 2010-09-08 10:07 <a href="http://www.cppblog.com/shaoxie1986/articles/126142.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>句柄和指针的区别</title><link>http://www.cppblog.com/shaoxie1986/articles/126140.html</link><dc:creator>松娃</dc:creator><author>松娃</author><pubDate>Wed, 08 Sep 2010 01:42:00 GMT</pubDate><guid>http://www.cppblog.com/shaoxie1986/articles/126140.html</guid><wfw:comment>http://www.cppblog.com/shaoxie1986/comments/126140.html</wfw:comment><comments>http://www.cppblog.com/shaoxie1986/articles/126140.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/shaoxie1986/comments/commentRss/126140.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shaoxie1986/services/trackbacks/126140.html</trackback:ping><description><![CDATA[
<p style="TEXT-INDENT: 2em"><font size="2">这是初学者最常问及的问题，一些面试官也很喜欢问这个问题 
。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">当把硬盘上的资源调入内存以后，将有一个句柄指向它，但是句柄只能指向一个资源。而且句柄知道所指的内存有多大。还有指针，指针指向地址，它不知道分配的内存有多大。&nbsp;&nbsp; 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">但是如果你定义一个句柄，然后在VC里面右击鼠标，选择"go to 
definition of 
handle&#8221;，你会发现它的本质就是一个指针，但是它的作用不同于指针。它和通常意义上的指针是有区别的。句柄借用了指针的思想，有它的逻辑特点，但没有它的物理功能。句柄是WINDOWS分配给窗口等资源的唯一标识,是一个整数。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">一、书上定义：</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&lt;&lt;Microsoft Windows 3 
Developer''s Workshop&gt;&gt;(Microsoft Press,by Richard Wilton)</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">在Windows环境中，句柄是用来标识项目的，这些项目包括：模块(module)、任务(task)、实例 
(instance)、文件(file)、内存块(block of 
memory)、菜单(menu)、控制(control)、字体(font)、资源(resource)，包括图标(icon)，光标 
(cursor)，字符串(string)等、GDI对象(GDI 
object)，包括位图(bitmap)，画刷(brush)，元文件（metafile）,调色板(palette)，画笔(pen)，区域 
(region)，以及设备描述表(device context)。 </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&lt;&lt;WINDOWS编程短平快&gt;&gt;(南京大学出版社)：</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">句柄是WONDOWS用来标识被应用程序所建立或使用的对象的唯一整数，WINDOWS使用各种各样的句柄标识诸如应用程序实例，窗口，控制，位图，GDI对象等等。WINDOWS句柄有点象C语言中的文件句柄。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">二、MFC</font><a href="http://www.jtzjsc.com.cn/tag-272-1.html"><font size="2">源代码</font></a><font size="2">：</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">#ifdef STRICT</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">typedef void *HANDLE;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">#define DECLARE_HANDLE(name) struct 
name##__ { int unused; }; typedef struct name##__ *name</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">#else</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">typedef PVOID HANDLE;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">#define DECLARE_HANDLE(name) typedef 
HANDLE name</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">#endif</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HMODULE); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HINSTANCE); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HLOCAL); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HGLOBAL); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HDC); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HRGN); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HWND); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HMENU); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HACCEL); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">DECLARE_HANDLE(HTASK); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">三、理解： </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">句柄是一个32位的整数，实际上是windows在内存中维护的一个对象（窗口等）内存物理地址列表的整数索引。因为windows的内存管理经常会将当前空闲对象的内存释放掉，当需要时访问再重新提交到物理存储，所以对象的物理地址是变化的，不允许程序直接通过物理地址来访问对象。程序将想访问的对象的句柄传递给系统，系统根据句柄检索自己维护的对象列表就能知道程序想访问的对象及其物理地址了。 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">句柄是一种指向指针的指针。我们知道，所谓指针是一种内存地址。应用程序启动后，组成这个程序的各个对象是驻留在内存的。如果简单地理解，似乎我们只要获知这个内存的首地址，那么就可以随时用这个地址访问对象了。但是，如果真这么认为，那么就大错特错了。我们知道windows是一个虚拟内存为基础的操作系统。在这种情况下，windows内存管理器经常在内存中来回移动对象，以此来满足各种应用程序的内存需要，对象被移动意味着它的地址变化了。如果地址总是如此的变化，我们应该去那里找对象呢？为了解决这个问题，windows操作系统为各个应用程序腾出一些内存地址，用来专门登记各个应用对象在内存中的地址变化，而这个地址（存储单元的位置）本身是不变的。windows内存管理器移动对象在内存中的位置后，把对象新的地址告知这个句柄地址来保存。这样我们只需要记住这个句柄地址就可以间接地知道对象具体在内存中哪个位置了。这个地址是在对象装载（load）时由系统分配的，当系统卸载时又释放给系统。句柄地址（稳定）----&gt;记载着对象在内存中的地址----&gt;对象在内存中的地址（不稳定）----&gt;实际对象。但是必须注意，程序每次重新启动，系统不保证分配跟这个程序的句柄还是原来哪个句柄，而绝大多数情况下的确不一样。假如我们把进入电影院看电影看成是一个应用程序的启动运行，那么系统给应用程序分配的句柄总是不一样，这和每次电影院给我们的门票总是不同的座位是一个道理。 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp; 对于Wind32 
API，尽管为每个对象分配了数据块，但是微软不想向用户程序返回指针。对于一个聪明的程序员来说，指针包含了太多的信息。它给出了对象存储的确切位置。指针一般允许对对象的内部表示进行读写操作，而这些内部表示也许正是操作系统想隐瞒的。指针还使越过进程地址空间共享对象变得困难。为了对程序员进一步隐藏信息，Win32对象创建程序实例一般会返回对象句柄。对象可以映射到唯一句柄，句柄也可由映射到唯一的对象。为了保证句柄能够完成信息隐藏的的任务，对象和句柄之间的映射没有任何文档记载，不保证固定不变，而已仅有微软知道这种映射，或者还有少数系统级工具开放商知道。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
对象指针和句柄之间的映射可以由函数Encode和Decode来实现，原型如下：</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; HANDLE Encode(void* 
pObject);</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; Void* Decode(HANDLE 
hObject);</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
在极端情况下，句柄可以和对象指针相同，Encode和Decode只需做类型转换，对象和句柄之间的映射主要是全等映射。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 在Win32 
API中，实例句柄(HINSTANCE)或者模块句柄(HMODULE)是指向映射到内存的PE文件映像的指针。LockResource用来锁住全局资源句柄得到指针，但实际上它们的值相同。LockResource返回的资源句柄只是伪装后的内存映射资源的指针。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
通常情况下，对象指针和句柄之间的映射是基于表格的映射。操作系统创建表格或者是一级表示保存所有要考虑的对象。需要创建新对象时，首先要在表格中找到空入口。然后就把表示对象的数据添入其中。当对象被删除时，它的数据成员和它在中的入口被释放，以便再利用入口。用这种基于表的对象管理方法，表中的索引可以很好的组成对象的句柄，编码和解码也很简单。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; （在Win32 
API中，内核对象是用进程表实现的。为了容纳大量内核对象，每个进程都有自己的内核对象表。NT/2000内核执行体中一部分是对象管理器，它只管理内核对象。对象管理器提供函数ObReferenceObjectByHandle。根据DDK（Driver 
Develepment 
Kits）文档，它提供对象指针的解码全过程，如果存取操作被批准，则会返回对象体相应的指针。因此对于一个把对象句柄翻译称为对象指针的解码全程来说，额外的安全检查很重要。</font><a href="http://www.internals.com/"><font size="2">www.internals.com</font></a><font size="2">上面有个非常好的工具HandleEx，它能够列出Windows NT/2000的内核对象。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
只有句柄是不够的，尽管句柄提供了近乎完美的抽象，信息隐藏和保护，但是它也是程序员遭受挫折的地方。在像Win32 
API这样以句柄为中心的API中，微软没有任何文档记载对象的内部表示以及对象是如何管理的，也没有提供参考实现，程序员只有函数原型，微软文档和或多或少基于微软文档的书籍。程序员面临的首要问题包括系统资源。当对象被创建，对象的句柄被返回时，谁都不知道对象用了什么资源，因为对象的内部表示是不知道的。程序员是应该保护该对象还是应该在对象没有用时尽快把它删除呢？GDI支持的三种位图，为了减少系统资源消耗，应该使用哪一种呢？CPU时间时计算机的主要资源。当内部表示对程序员隐藏时，程序员就很难在复杂的算法设计中判断这种操作的复杂性如果你用GDI组成复杂区域，算法的复杂度是O(n)（问题规模n），O( 
)（问题规模）还是O（）。随着程序的隐藏，调试也成问题。程序运行5分钟显示了一些垃圾数据，猜测由资源泄漏，但是泄漏在哪儿？怎么解决？如果是处理系统中几百个应用程序的管理员，当系统资源很少时，如果找出问题？唯一可以用的资源泄漏工具是BoundsChecker，它依赖API窥视技术查出对象创建和删除之间的不匹配之处。最让人受挫的地方可能是程序的兼容性。程序为什么能在Windows95下把GDI对象从一个进程传递到另外一个进程，而Windows 
NT/2000不行？为什么Windows95不能处理大的设备无关图？</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
以GDI对象为例子，创建了GDI对象，就会得到该对象的句柄。句柄的类型有可能是HPEN，HBRUSH,HFONT或者是HDC中的一种。但最普通的 
GDI对象类型是HGDIOBJ，它被定义成为空指针。HPEN的实际编译类型是随着时间宏STRICT的不同而不同。不同GDI句柄的定义模仿了GDI 
对象不同类的类层次结构，但是没有真正的类层次结构。GDI对象一般有多个创建函数和一个接受HGDIOBJ的析构函数——DeleteObject。也可以用GetStockObject取得预先创建好的GDI对象句柄，无论GetStockObject调用顺序是如何，它返回的句柄看起来总是常数。甚至当运行一个程序的两个实例时，它在每个进程中返回相同的，唯一解释是对象句柄堆是不变的，系统初始化，堆对象被创建并被所有进程重复使用。尽管 
Windows头文件把GDI句柄定义成为指针，但是检查这些句柄的值时，它们根本不像指针。生成几个GDI对象句柄并看一下返回句柄的十六进制显示，就会发现结果从0x01900011变化到0xba040389。如果HGDIOBJ像在Windows头文件里面定义的那样是指针，则前者是指向用户地址空间中未分配的无效指针，而后者是执行内核地址空间。这就暗示GDI句柄不是指针。另一个发现是GetStockObject（BLACK_PEN）和 
GetStockObject(NULL_PEN)返回值只相差一，如果句柄真的是指针的话，这不可能是存储内部GDI对象的空间，因此可以肯定的说 
GDI对象句柄不是指针。系统GDI句柄数限制为16384个，进程GDI句柄数限制为12000个。这样单独的进程不会搞乱整个GDI系统。但是 Windows 
2000第一版没有对每个进程加以限制。现在在同一个系统下运行两个GDIHandles，在每一个进程中调用8192次CreatePen。第一个很好的创建了对象，第二个在7200左右会停止。第二个进程失败后，整个屏幕一团糟，这个试验表示GDI对象同样是从同一个资源池分配的。系统中的进程使用 
GDI资源时会互相影响。把8192和7200相加。考虑到GDIHandle属性页面和其它进程的页面使用的GDI对象句柄，可以猜测，GDI句柄数目有系统范围限制：16384。GDI对象存储于系统范围内的固定大小的对象表中，称之为对象句柄表。它是一个大小固定的表，而不是一个会动态增长的数据结构。这就意味着简明和效率。但是&nbsp;&nbsp;&nbsp;&nbsp; 
缺点就是前面说的，限制了GDI句柄数：16384个。下面看看HGDIOBJ的构成，Windows 
NT/2000下，GDI返回的对象句柄是32位值，组成8位十六进制数。如果用GDIHandles创建很多GDI对象，注意到其中显示的双字句柄的低位字，会发现它们都在0x000到0x3FFF之间。低位字在进程中总是唯一的，出了堆对象外，低位字甚至在进程中也是唯一的。句柄的低位有时候按照递增的顺序排列，有时候又递减。在进程间也是这样。例如，某些情况下，CreatePen在低位返回0x03C1，另一个进程中的下一个CreatePen在低位返回0x03C3。对这些现象的解释是HGDIOBJ的低位字是对系统范围的16384个GDI对象所组成的表的索引。再来关注高4位的十六进制数。创建几个画刷，几个画笔，几个字体，DC等。不难看出相同类型的GDI对象句柄有个共同特点：相同类型的对象句柄的第三位和第四位十六进制数几乎都是相同的。画刷句柄的第三位和第四位总是0x90和0x10，画笔总是0x30和0xb0等等。最高位是1（二进制）的对象句柄都是堆对象。因此可以有足够的证据说对象句柄的第三位和第四位十六进制数是对象类型的编码和堆对象标记。在32位GDI句柄值中余下的两个十六进制位暂时还没找到有意义的模式。总结一下，GDI对象句柄由8位位置高位，一位堆对象标记，7位对象类型信息和高四位为0的16位索引组成。因为GDI对象表是由系统中所有过程和系统DLL所共享的，桌面，浏览器，字处理器以及DirectX游戏都在为同一个GDI句柄的储存区而竞争。而在Windows 
2000中，DirectX则使用一个独立的对象句柄表。GDI句柄表一般存储在内核模式的地址空间里以使图形引擎能很容易访问它，通过一定技巧，将为每个使用GDI的进程在用户模式存储空间里面建立表的只读视图。在Windows 
2000终端服务中，每个对话都有它自己的Windows图形引擎和视窗管理器（WIN32K.SYS）的拷贝，以至于系统中有多个GDI对象表。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
GDI句柄表的每一个入口都是一个16字节的结构体，如下面代码所示：</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; Typedef struct</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; void*&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
pKernel;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; unsigned short 
nPaid;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; unsigned short 
nCount;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; unsigned short 
nUnique;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; unsigned short 
nType;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; void*&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pUser;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; } GdiTableEntry;</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 
可见：GDI对象有一个指向它的内核模式对象的指针，一个指向其用户模式的指针，一个过程ID，一个种类的计数，一个唯一性的标准值以及一个类型标识符。多么完美，优雅的设计！</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">&nbsp;&nbsp;&nbsp; 尽管Win32 
API不是面相对象的API，但它也面临着和面相对象语言一样要解决的问题，即信息的隐藏和抽象数据类型，而且Win32 
API比面相对象语言更想隐藏对象。用于创建Win32对象的Win32函数隐藏了该对象的大小和储存位置，而且没有返回指向对象的指针，而是仅仅返回该对象的句柄。Win32 
API句柄是Win32对象一一对应的变量，它仅能被操作系统识别。分析一下，你会发现Win32使用了相当多的抽象数据类型，如文件对象，它包括了许多具体的对象类型，我们可以用CreateFile来创建文件，管道，通讯端口，控制台，目录以及设备等，但是这些操作都返回一种类型的句柄。跟踪到 
WriterFile中，会发现最后的操作其实是操作系统中不同例程甚至是不同产商提供的设备驱动程序来处理的，这就像是C++的虚函数和多态机制。同样，在GDI域中，设备上下文被当作一个抽象对象类型看待。创建或者检索打印机设备上下文，显示设备上下文，内存设备上下文和图元文件上下文的程序都将返回同类型的设备上下文句柄。显而易见的是，同年国国句柄的类属绘图调用是通过不同的例程来处理的，而这些例程但是由GDI或者图形设备驱动程序通过物理设备结构中的函数指针表来实现的。因此实现这样的机制也会像C++一样有一个类似于虚函数表的函数指针表，一个变量和函数指针通过这个表形成映射，方便的实现这种虚函数和多态机制，这个变量就是句柄....&nbsp; 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">因此，句柄和指针其实是两个截然不同的概念。windows系统用句并标记系统资源，用句并隐藏系统信息。你只需要知道有这个东西，然后去调用它就行了，它是32bit的uint。指针则标记某个物理内存的地址，是不同的概念。 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">指针对应着一个数据在内存中的地址，得到了指针就可以自由地修改该数据。Windows并不希望一般程序修改其内部数据结构，因为这样太不安全。所以Windows给每个使用GlobalAlloc等函数声明的内存区域指定一个句柄(本质上仍是一个指针，但不要直接操作它)，平时你只是在调用API函数时利用这个句柄来说明要操作哪段内存。当你需要对某个内存进行直接操作时，可以使用GlobalLock锁住这段内存并获得指针来直接进行操作。 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">句柄是指针的&#8220;指针&#8221;，使用句柄主要是为了利于windows在进程内存地址空间移动分配的内存块，以防止进程的内存空间被撕的四分五裂而存在过多的碎片。 
</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">句柄是一些表的索引也就是指向指针的指针。间接的引用对象，windows可以修改对象的"物理"地址和 
描述器的值，但是句柄的值是不变的。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">句柄可以在获得窗口的时候使用,指针可以进行调用窗口,两个使用的地方不一样.一个括号外,一个括号内.</font></p>
<p style="TEXT-INDENT: 2em"><font size="2"></font></p>
<p style="TEXT-INDENT: 2em"><font size="2">从窗口指针获取窗口句柄：GetSafeHwnd();</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">从窗口句柄获取临时窗口指针：FromHandle(); </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">从窗口句柄获取永久窗口指针： 
FromHandlePermanent();</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">其实两者被没有关系，实际上是MFC在创建窗口的时候用钩子函数沟住HCBT_CREATEWND消息，</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">然后通过CWnd::Attach()函数把二者捆绑在一起。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">以后就可以用GetSafeHwnd()，FromHandle()，FromHandlePermanent()这三个函数可以互相得到了。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">MFC之所以要这样做，主要是为了使原来的SDK面向过程的编程遍成面向对象的编程，所有的MFC的窗口都共用一窗口过程函数，在窗口过程函数里，通过窗口句柄(HWND)找到窗口对象指针(CWnd 
*)从而把消息分发到窗口对象中，这样以后就可以在窗口类中实行消息响应编程处理了。</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">附注一：获得窗口句柄三种方法</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">1.HWND FindWindow(LPCTSTR lpClassName, 
LPCTSTR lpWindowName) </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">HWND FindWindowEx(HWND hwndParent, HWND 
hwndChildAfter,LPCTSTR lpClassName, LPCTSTR lpWindowName) </font></p>
<p style="TEXT-INDENT: 2em"><font size="2">2.HWND WindowFromPoint(POINT&amp; 
Point)//获得当前鼠标光标位置的窗口HWND</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">3.BOOL CALLBACK EnumChildProc(HWND 
hwnd,LPARAM lParam)</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">BOOL CALLBACK EnumChildWindows(HWND 
hWndParent, WNDENUMPROC lpEnumFunc,LPARAM lParam)</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">BOOL CALLBACK EnumWindows(WNDENUMPROC 
lpEnumFunc, LPARAM lParam)</font></p>
<p style="TEXT-INDENT: 2em"><font size="2">BOOL CALLBACK EnumWindowsProc(HWND 
hwnd, LPARAM lParam)</font></p>
<p style="TEXT-INDENT: 2em"><font size="2"></font>&nbsp;</p>
<p><font size="2">附注二：指针 句柄之间的转换</font></p>
<p><font size="2">a.由指针获得句柄 <br>CWnd * pWnd; <br>CWnd HWnd; <br>HWnd = 
pWnd-&gt;GetSafeHWnd();</font></p>
<p><font size="2">b.由句柄得到指针:<br>CWnd* pWnd=FromeHandle(hMyHandle); 
<br>pWnd-&gt;SetWindowText("Hello World!"); <br>or CWnd* pWnd; 
pWnd-&gt;Attach(hMyHandle);</font></p>
<p><font size="2">MFC类中有的还提供了标准方法，比如Window 句柄 ： <br>static CWnd* PASCAL 
FromHandle( HWND hWnd ); <br>HWND GetSafeHwnd( ) const;</font></p>
<p><font size="2">对于位图： <br>static CBitmap* PASCAL FromHandle( HBITMAP hBitmap ); 
<br>static CGdiObject* PASCAL FromHandle( HGDIOBJ hObject ); <br>HGDIOBJ 
GetSafeHandle( ) const;</font></p><img src ="http://www.cppblog.com/shaoxie1986/aggbug/126140.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shaoxie1986/" target="_blank">松娃</a> 2010-09-08 09:42 <a href="http://www.cppblog.com/shaoxie1986/articles/126140.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>