邪恶八进制信息安全团队技术讨论组's Archiver

金州 2006-2-26 13:20

[转载]探讨改进的系统信息API 新内核API 调试API 安全API和UI API

<P>文章作者:Matt Pietrek  </P>
<P><SPAN id=ContentLabel style="PADDING-RIGHT: 10px; DISPLAY: block; PADDING-LEFT: 10px; PADDING-BOTTOM: 0px; PADDING-TOP: 0px"> </P>
<P><B>摘要</B> 关于 Windows Server 2003,我们有许多话要说。首先,它是第一个内置了 .NET 框架支持的操作系统,也是 Microsoft 推出的第一个 64 位操作系统。 但我们要讨论的不只这些! 这个操作系统版本中还有许多新特性和 API。 例如,Windows Server 2003 采用了“热添加内存”和许多其他巧妙的新功能。 系统中还包括新 API,用于处理线程、目录和文件,以及新特性,如用于管理内存和系统信息的低碎片堆。 此外,还有向量化异常处理和新 UI API。</P>
<P>操作系统内核领域的专家 Matt Pietrek 将讨论他认为最有趣和最有用的新内容,这样,在深入 Windows Server 2003 之前,您将有一个很好的开端。</P>
<P></P>
<DIV style="MARGIN-TOP: 3px; MARGIN-BOTTOM: 10px"><A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/DiscoverImprovedSystemInfoNewKernelDebuggingSecurityandUIAPIs.mspx#top" target=_blank><IMG height=9 alt=返回页首 src="http://www.microsoft.com/library/gallery/templates/MNP2.Common/images/arrow_px_up.gif" width=7 border=0></A><A class=topOfPage href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/DiscoverImprovedSystemInfoNewKernelDebuggingSecurityandUIAPIs.mspx#top">返回页首</A></DIV><A name=EDAA></A>
<H2></H2>
<P>就年纪而言,我可能还不具备谈论这个话题的资格,但我的确记得过去的好时光 — 开发人员热切等待发布新 Windows 版本的那些日子。 新版本中会包含哪些新的特别吸引人的特性呢?这些好的特性又会有哪些用途呢? 随着 Microsoft .NET 框架的出现,在某些圈子里,开始流行这样一种言论 — Windows 没有什么变化、不值得讨论。 有些人似乎认为,用哪个 Windows 版本作为运行平台都没有关系。</P>
<P>您可能已经猜到了,我可不属于这类人。 虽然我像任何其他人一样,是 Microsoft .NET 的忠实爱好者,但我仍然充满热情地扫描头文件,比较来自每个新 Windows 版本的系统 DLL 的导出。 我希望知道,Microsoft Windows 团队中的风云人物最近都在忙些什么。</P>
<P>Microsoft 推出的最新、最酷的操作系统就是 Windows Server? 2003,在我撰写本文时,它还处于发行候选版阶段。 在本文中,我将讨论 Windows Server 2003 中为应用程序级的程序员提供了哪些新的特别吸引人的特性。 不过,在深入探讨新 API 之前,了解一些背景信息会有助于您加深理解。</P>
<P>我们首先要确定这个“新”字的含义。 Windows Server 2003 是为了取代 Windows 2000 Server 系列(Server、Advanced Server 和 Datacenter Server)而推出的。 因此,原来出现在 Windows XP 里的操作系统特性中有许多在技术上可以属于本文中所说的“新”特性类别。</P>
<P>实际上,大多数新 API(相对于 Windows 2000 而言)首先出现在 Windows XP 中,而不是 Windows Server 2003 中。出现这种情况是有充分理由的。 一开始,Windows 2000 的下一个版本的代号为 Whistler,它的测试版包括了工作站和服务器两个版本。 2001 年,Microsoft 决定服务器版本需要酝酿更长时间,因此发布了作为消费者版本和工作站版本的 Windows XP。 Microsoft 原本打算很快就发布服务器版本。 正如您已经知道的那样,发布延迟了一年半。 延迟的部分时间是由“Microsoft 可信赖计算计划”导致。根据该计划,Microsoft 的开发人员停止了新的开发,以寻找代码中的安全问题。</P>
<P>问题的关键在于,到某个日期时,Whistler 的工作站版本和服务器版本基本上都具有完整的特性,并使用了同样的代码库。 Windows XP 和 Windows Server 2003 之间的发布时间差主要用于使操作系统尽可能稳定。 因此,Windows Server 2003 的新特性和 API 中有许多也可以在 Windows XP 中找到 — 这根本不足为奇。</P>
<P>Platform SDK 头文件中使用的版本号可以证明该操作系统的公共接口(也就是 API)在 Windows XP 之后的改变非常小。 为了启用 Windows XP(及更高版本)API,需要将 _WIN32_WINNT 定义为 0x0501(也就是说,在内部,Windows XP 被认为是 Windows 5.01)。 对于 Windows Server 2003,_WIN32_WINNT 所需的 #define 值只变成 0x0502。 您稍后将看到示例程序中使用的 _WIN32_WINNT #defines。</P>
<P>Windows Server 2003 可以保留开发过程中名称更改的最大次数的记录。 起初,它的代号是 Whistler,后来改为 "Windows 2002 Server"。 接着,.NET 计划渗透了 Microsoft 的所有产品,该操作系统被重新命名为 "Windows .NET Server"。 在经历了另一次延迟之后,它的名称改为 "Windows .NET Server 2003"。 最后,深谋远虑的 Microsoft 高层取得了胜利,操作系统更名为 "Windows Server 2003"。 如果我漏掉了其中的一次更名,也不足为奇。</P>
<P>不过,从更名的过程中,我们可以看出一个重点: Windows Server 2003 是第一个将 .NET 框架作为操作系统组成部分的操作系统。 为了实现向后兼容性,该系统同时包括了最新的 .NET 框架 1.1 以及原来的 1.0 版本的 .NET 框架。 由于 <I>MSDN</I><I>Magazine</I> 和其他地方对 .NET 进行了大量介绍,因此我在本文中不打算将 .NET 框架作为新 API。</P>
<P>除了作为内置 .NET 框架支持的第一个操作系统,Windows Server 2003 还是第一个 Microsoft 64 位服务器操作系统,因而与众不同。 Windows XP Professional 虽然有 64 位版本,但到目前为止,对基于 Itanium 的 64 位工作站的需求还不是很大。 现在,Microsoft 终于推出了 64 位服务器操作系统,拥有大型数据库的公司可能会开始转移到 64 位 Windows。 64 位版本的 SQL Server? 预计将与 64 位版本的 Windows Server 2003 一起提供。</P>
<P>Windows Server 2003 最初将提供六种配置。 对于 Intel x86 CPU,低端是用于基本 Web 服务的 Web 版服务器。 再高一级是作为部门级服务器的标准版。 从它再往上一级是面向大中型企业的企业版。 最后,针对运行在多达 32 个处理器上的大规模数据库系统提供数据中心版。 其余两种配置是企业版和数据中心版的 64 位 Itanium 版本。</P>
<P>比较有趣的是,64 位版本的 Windows Server 2003 不包括 .NET 框架。 显然,64 位版本的 .NET 还没有准备就绪,因此将在以后的版本中提供。</P>
<P>Windows Server 2003 中有许多新特性,我不会在此详述,但仍然值得一提,因为这些特性太棒了。 例如,Internet 信息服务 (IIS) 现在提供的版本是 6.0。 它在结构上有了较大改动,并且由于对内核模式侦听器下了更多的工夫,性能也获得很大的提高。 如果需要,您还可以在三种 IIS 5.0 保护级别下运行它。</P>
<P>Windows Server 2003 中其他异乎寻常的新特性包括(但不只限于这些特性):卷影复制、热添加内存和非统一内存访问 (NUMA) 支持。</P>
<P>卷影复制服务提供了一种进行完整备份的方式,即使是备份时打开的文件也可以备份。 虽然我不喜欢谈论备份,但我认为使用驱动程序透明地实现此功能是很不错的。 Mark Russinovich 和 David Solomon 在 2001 年 12 月刊上发表的文章 <A href="http://msdn.microsoft.com/msdnmag/issues/01/12/XPKernel/default.aspx" target=_blank>Windows XP:Kernel Improvements Create a More Robust, Powerful, and Scalable OS</A> 涵盖了卷影复制服务及许多其他新特性。</P>
<P>热添加内存是完全异乎寻常的特性之一,它会使您想知道那些稀奇古怪的硬件设计人员接下来会有什么奇思妙想。 这种特性能够在系统运行时向系统添加 RAM。 当您插入 RAM 后,操作系统自动进行检测,并开始使用 RAM。不过,为了实现此目的,您需要运行一个在设计上支持这种特性的系统。 在 Microsoft 文档中,您会发现来自硬件维修权威的警告:不要在任何旧系统打开时向其中盲目添加 RAM — 这样做会导致发生意外。</P>
<P>NUMA 是一种在企业级多处理器系统中使用的高端技术。 它的中心内容是,将内存和处理器组成单元。 处理器在访问其单元内的内存时,速度会比访问另一个单元中的内存时更快。 Windows 中的 NUMA 支持导致计划程序试图使相关的进程运行在同一个单元中。</P>
<P>在进入 Windows Server 2003 新 API 的核心之前,我需要在这里做一项声明。下面只是我个人的主观看法,并不全面。 我阅读了数百个头文件和 beta 文档页,精选了我认为最有趣的内容。 我不得不做出许多关于包括哪些内容和放弃哪些内容的艰难选择。 通常,对我来说,要选择一个包括在这里的特性,它必须是用户模式的 API,而不是完全深奥的 API 或与某个特定程序相关的 API。 我看到了许多设备驱动程序级的新功能,但它们不在本文的范围内。</P>
<P></P>
<P>新<B>KERNEL API</B></P>
<P>现在,我们即将开始内核级功能的讨论。 您可能会认为内核级功能指的是 KERNEL32(稍后将充分探讨这方面的内容)。 真正的系统编程发烧友知道,NTDLL 是用户模式内核的真正核心。 我已经对 Windows Server 2003 版本的 NTDLL.DLL 的导出与 Windows 2000 版本的 NTDLL.DLL 的导出做了比较。 您可能已经预料到,许多 API 被添加进去,有几个 API 消失了。 重要的是,它们都是未公开的 API,对吗? 不要这么快就得出结论!</P>
<P>当我在不同版本的 Platform SDK 之间比较所有那些 .H 文件时,我偶然发现一个 2002 年 10 月版的非常有趣的文件。 该文件名为 WINTERNL.H。这肯定是一个我们要紧紧抓住的文件。 它也可能在未来的 Platform SDK 中消失。 在 WINTERNL.H 的开头,是一个包括三个段落的警告,警告的内容是,所有数据结构和 API 随时可能更改,仅限 Windows 核心组件使用。 (我们以前好像见过那些警告吧,不是吗?)</P>
<P>不管怎样,WINTERNL.H 中包括什么特别吸引人的东西呢? 不幸的是,并不像您希望的那么多。 但还是有一些众所周知的、但仍未公开的数据结构的线索引人入胜。 例如,您将看到定义的进程环境块 (PEB) 和线程环境块 (TEB)。 不过,大多数字段是作为保留字段列出的。 同样,该文件中还包括调用 NtQueryInformationProcess 和 NtQueryInformationThread 时所必需的结构和原型。 不过,目前有各种图书和 Web 站点提供了关于这些 API 和结构的许多信息,比 WINTERNL.H 要详细得多。 必须承认,该文件对 Windows Server 2003 来说没有什么新东西。不过,该文件首次出现在随 Windows Server 2003 提供的 SDK 中。</P>
<P>为什么我要对这件事情如此小题大做呢? 它的重要意义在于,Microsoft 最终承认,许多有趣的东西没有公开。 WINTERNL.H 中还有其他几个值得注意的 API: NtCreateFile、NtOpenFile、NtClose 和 NtWaitForSingleObject。 这些 API 是公共 KERNEL32 API 的用户模式实现的核心。 同样,RtlUnwind 是结构化异常处理 (SEH) 中使用的关键 API,我于 1997 年 1 月在 <I>Microsoft Systems Journal</I> 上发表的文章 "<A href="http://msdn.microsoft.com/isapi/gomscom.asp?TARGET=/msj/0197/Exception/Exception.aspx" target=_blank>A Crash Course on the Depths of Win32 Structured Exception Handling</A> " 描述了这一内容。 RtlUnwind 发生变化的可能性极小。 如果它发生了变化,那么,很大一部分现有的应用程序会无法运行。</P>
<P></P>
<P><B>进程、线程和纤程,天哪!</B></P>
<P>现在我们开始讨论 KERNEL32。我的第一组 API,如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.html" target=_blank>图 1</A> 所示,与进程和线程相关。出现一个异常时,它们都检索有关进程或线程的某段信息。 GetThreadId 获取线程句柄,返回相关的线程 ID。GetProcessId 也是这样。 很难相信这些函数 10 年前还没有出现在 Win32 API 中! IsWow64Process 会告知调用进程是否为运行在 64 位 Windows 下的 32 位进程。</P>
<P>GetProcessHandleCount 返回指定的进程已经打开的句柄数。 该计数与我们在性能监视器数据或任务管理器中看到的是同一个值。 RestoreLastError 有些不可思议。 它的代码与 SetLastError 完全一样。 我还不清楚为什么将它作为一个单独的 API。</P>
<P>为了示范这些新 API 中的一部分,请查看<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.html" target=_blank>图 2</A>中的 ProcessesAndThreads 程序。 该代码是不言自明的,因此在这里我将不再对其深究。 要编译该程序(及其他示例程序),您至少需要 2002 年 10 月的 Platform SDK,其编译器和链接器的目录搜索顺序的最前面是 Include 目录和 Lib 目录。 如果您运行的是 Windows XP,可以在编译程序时使它只使用 API 的 Windows XP 子集。 只要对开头附近的 #define W2K3SERVER 行取消注释,然后重新编译就可以了。</P>
<P>除了线程处理支持,Windows Server 2003 还添加了一些新纤程 API, 如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.html" target=_blank>图 3</A>中的所示。 这里要提到一条重要消息,新增了纤程本地存储 (FLS)。 这些 API 同样用于其对应的线程本地存储 (TLS)。 通过 FlsAlloc 函数来分配一个槽。 要设置或检索值,可使用 FlsGetValue 和 FlsSetValue 函数。 用完槽后,调用 FlsFree。</P>
<P>我顺便研究了 FLS 函数的实现。 线程环境块中偏移量为 0xFB4 处是一个指向数据结构的指针。 此结构中有八个字节是一组 128 个槽。 这些槽在概念上与 TLS 槽相同。 当线程在纤程之间切换时,偏移量为 0xFB4 处的指针也随之更新。</P>
<P>ConvertFiberToThread API 撤消了 ConvertThreadToFiber 的效果。 调用它之后,无法再对线程调用其他纤程函数。 <A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.html" target=_blank>图 3</A> 中列出的其余两个 API 只是现有 API 的“扩展”版本。 CreateFiberEx 就像 CreateFiber,但它能够指定堆栈保留大小。 不过,ConvertThreadToFiberEx 非常有趣。 在原来的纤程实现中,并不在各纤程切换之间保存和还原浮点寄存器、MMX 寄存器和 SSE 寄存器来改进性能。 而新 API 允许您指定,这些寄存器也需要保存和还原。</P>
<P></P>
<P><B>向量化异常处理</B></P>
<P>也许,KERNEL32 中最令人兴奋的新特性就是向量化异常处理 (VEH)。利用这种特性,可以更灵活地处理异常。 我于 2001 年 9 月在 <I>MSDN Magazine</I> 的<A href="http://msdn.microsoft.com/msdnmag/issues/01/09/hood/default.aspx" target=_blank>Under The Hood</A> 专栏上发表的文章已经深入叙述了向量化异常处理,因此在这里我只参照<B>图 4</B> 中的流程图解提供简单扼要的解释。</P>
<P></P>
<DIV style="WIDTH: 330px"><IMG height=182 alt="" src="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/fig04.gif" width=330 border=0><BR>
<P class=figureCaption><B>图 4 </B><B>向量化异常处理</B></P>
<DIV class=figureRule></DIV></DIV>
<P></P>
<P>具有 __try/__except 机制的传统结构化异常处理 (SEH) 在本质上是特定于线程的。 异常只能由建立处理程序的线程来处理。 (编译器和操作系统处理此问题的所有棘手细节,并提供相对简单的 __try/__except 语法。) 更严重的是,使用 SEH,您可能建立了一个处理程序来处理异常,没想到此异常竟首先被另一个不知道如何正确处理该异常的处理程序捕获了。</P>
<P>向量化异常处理的工作方式更像一个传统的通知回调方案。 要处理异常,应调用 AddVectoredExceptionHandler API,向它传递您的异常回调函数的地址。 当异常发生时,回调函数收到指向 EXCEPTION_POINTERS 结构的指针。 这是 SEH 回调可通过 GetExceptionInformation API 收到的同一个结构。 您可以从 EXCEPTION_POINTERS 结构中的字段获知异常代码(例如,0xC0000005)和寄存器值(通过包括在内的 CONTEXT 结构)。</P>
<P>VEH 回调选择处理异常或将它链接到列表中的下一个处理程序。 它通过从回调返回适当的值来确定即将发生的操作。 每个进程都有一个链接的 VEH 回调列表。 作为处理异常的一部分,操作系统会在 VEH 列表中依次选择,调用处理程序。 要从该列表中删除一个处理程序,可使用 RemoveVectoredExceptionHandler API。</P>
<P>向量化异常处理与 SEH 如何共存呢? 这是一个很好的问题! 在 SEH 链中遍历之前,系统会先遍历向量化异常处理程序列表。 也就是说,VEH 处理程序的优先级要高于 SEH 处理程序。 要了解向量化异常处理是如何起作用的,请查看 <A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 5 </A>中的 VEHDemo 程序。 VEHDemo 安装了两个向量化异常处理程序,使用结构化异常处理程序来说明 VEH 和 SEH 如何协同工作。 运行 VEHDemo 程序后产生的输出如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 6 </A>所示。</P>
<P><B>目录和文件</B></P>
<P>如 <A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 7 </A>所示,“文件和目录”类别中添加了几个新的 API。 SetDllDirectory 添加了当系统查找 DLL 时将搜索的任意一组目录。 系统会在应用程序加载目录后面,但在任何其他地方(如系统目录)之前搜索指定的路径。 SetDllDirectory 的文档描述了确切的搜索顺序,阅读该文档非常有意思。 GetDllDirectory 返回以前通过调用 SetDllDirectory 来设置的任何值。 GetSystemWow64Directory 用于在 64 位系统上查找 32 位系统目录。</P>
<P>关于 NTFS 文件系统,一个几乎不为人所知的事实是,它一个文件中支持多个流。 我们很难在有限的篇幅中解释这个比较复杂的特性,但它的要点是,多个文件可以共同由一个文件名来引用。 大多数文件只有一个与其关联的默认流,这就是大多数 Win32 API 所报告的流。 要创建与默认流不同的流,只需追加一个后面带有流名称的冒号 (:)。 例如,您可以使用记事本来创建一个名为 abc.txt:MyStream 的文件/流。 在 Windows Explorer 窗口中,您将看到一个零字节的 abc.txt 文件。 不过,abc.txt:MyStream 仍然存在。 普通的 Win32 API 只是不报告有关它的任何信息而已。</P>
<P>在 Windows Server 2003 中,这种情况在某种程度上有所改进。 FindFirstStream 和 FindNextStream API 枚举文件中的所有流。 为了说明它们的用法,我编写了如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 8</A></P>
<P>所示的 FindFirstStream 程序。 要使用该程序,只需向它传递一个文件名。 如果有任何默认的未命名流之外的流,该程序会将它们全部列出来。 下面是一个有三个流(abc、def 和 ghi)的文件 a.txt 的输出:</P><PRE class=codeSample>a.txt:abc:$DATA
a.txt:def:$DATA
a.txt:ghi:$DATA
</PRE>
<P>ReOpenFile API 用于接受现有的文件句柄,并获得另一个具有不同的一组访问权限的句柄。 通常,在只有文件句柄、而不知道相关的文件名的代码中会用到它。 如果您的代码需要不同于现有句柄的访问权限或共享模式,ReOpenFile 提供了尝试获得那些权限的方式。 当然,ReOpenFile 确保新请求的访问权限和共享模式是合法的。 它还可以预防管线假冒攻击。</P>
<P>CheckNameLegalDOS8Dot3 具有一个变化无常的 API 名称。 此 API 有助于检查文件名是否可以用在文件分区表 (FAT) 文件系统卷。</P>
<P>您可以想想,随着长文件名(超过 8.3)的出现,操作系统需要一种方式来引用使用标准 8.3 约定的文件。 系统有一种在长版本名称和短版本名称之间进行映射的算法。 这些文件可以很容易地挑出来,因为短版本以波形符 (~) 结束,后面跟一个数字(例如,“foobar~1.txt”)。 新的 SetFileShortName API 允许您重写系统的默认短文件名。 不过,要使用短文件名,目标文件必须在 NTFS 卷上。</P>
<P></P>
<P><B>内存和系统信息</B></P>
<P>在内存分配方面,Windows Server 2003 和 Window XP 有一个被称为低碎片堆的特性。 这种堆算法通过分配来自 128 个预先确定的、不同块大小范围(称为存储桶)的所有块,避免产生碎片。 当应用程序需要从堆中分配内存时,堆选择能够容纳所请求的块并且浪费空间最少的存储桶。 系统将传统的堆用于超过 16KB 的块。 要使用低碎片堆,应调用 HeapSetInformation,向它传递适当的堆句柄和标志值。 在调用 HeapSetInformation 之前,默认情况下,所有堆均具有 "normal" Win32 堆行为。 要确定堆使用的是哪种行为,请调用 HeapQueryInformation API。</P>
<P>在系统信息方面,存在一组各种各样的新接口,如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 9 </A></P>
<P>所示。 GetSystemRegistryQuota 为您提供注册表的当前大小以及所允许的最大大小。 GetSystemTimes 返回所有处理器在空闲状态下、在内核模式下以及在用户模式下所占用的时间长度。</P>
<P>GetNativeSystemInfo 用于运行在 64 位 Windows 下的 32 位程序。 它返回 SYSTEM_INFO 结构,对该结构进行填充就如同它是从一个本机 64 位程序调用那样。 例如,在 Itanium 计算机上运行一个 x86 程序,SYSTEM_INFO.dwPageSize 的值是 8192 个字节,而不是通过调用 GetSystemInfo 所得到的 4096 个字节。 <A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 10 </A></P>
<P>中的 SystemInfo 程序显示了正在使用的 GetSystemInfo,以及几个其他新系统信息 API。</P>
<P>GetLogicalProcessorInformation 返回与 NUMA 系统和 Intel 的 Hyperthreaded CPU(其中,一个 CPU 有多个执行单元)有关的信息。 该 API 返回一组 SYSTEM_LOGICAL_PROCESSOR_INFORMATION 结构。</P>
<P>CreateMemoryResourceNotification 是应用程序在可用物理内存不足时收到通知、而不必一直轮询该值的方式。 该 API 创建一个可传递到 WaitForXxx 系列函数的句柄。 当可用内存降到某个阈值以下时,会向该句柄发出信号。 根据我看到的文档,对于系统上每 4GB 的内存,该阈值为 32MB。 您还可以直接使用 QueryMemoryResourceNotification 检查内存状态。 系统还可以在可用物理内存充足时通知您,但恐怕它不会是一个常用的功能。</P>
<P></P>
<P><B>调试 API</B></P>
<P>在调试方面,也有几个新 API。 最令人兴奋的要算是 DebugSetProcessKillOnExit。 直到现在,如果您在调试另一个进程,您没有办法停止调试。 您不能从正在调试的进程中分离。 当您调试另一个进程时,您的线程之一是调试线程,处理所有调试通知消息。 正常情况下,如果此线程终止,则被调试的进程也会终止。 DebugSetProcessKillOnExit API 改变了这种行为。 通过传递 FALSE,您可以告诉系统停止要求调试线程处理被调试进程的消息。</P>
<P>与该 API 有几分相似的是 DebugActiveProcessStop,它可以通知系统使指定的进程从正在调试它的进程中分离。 它只能被名为 DebugActiveProcess 或 CreateProcess 的线程调用。 由于可以认为调试器线程能同时调试多个进程,DebugActiveProcessStop 需要指示从哪个进程分离的参数。</P>
<P>DebugBreakProcess 就像 DebugBreak 一样,不同的是,它适用于指定的进程,而不是当前线程。 该 API 的工作方式是,在目标进程中创建一个线程,这种做法很像 CreateRemoteThread。 新创建的线程调用一个断点指令,该指令导致普通的 SEH 机制来接管工作。 对于开发人员,这通常意味着实时调试对话框会出现。</P>
<P>最后一个新调试 API 是 CheckRemoteDebuggerPresent。 它类似于 IsDebuggerPresent API,因为它可以告诉您某个进程是否在调试器进程的控制下运行。 IsDebuggerPresent 可以告知您的进程是否正在被调试;而 CheckRemoteDebuggerPresent 则允许查询有关您拥有其句柄的任何进程的信息。</P>
<P></P>
<P><B><I>并行执行</I></B></P>
<P>在 .NET 框架中,已经有不少并行安装和执行功能了。 不过,这些同样的功能也内置在 Windows Server 2003 和 Windows XP 中。 这些功能的关键之处是,新激活上下文 (ActCtx) API,如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 11</A></P>
<P>所示。</P>
<P>激活上下文是一组系统管理的数据结构,它们包含用于使应用程序基于清单文件使用特定 DLL 版本或 COM 对象实例的信息。 清单文件使用 XML 格式(这不足为奇!),看上去很像 .NET 清单。 关于激活上下文的使用,完全可以再写一篇文章专门进行讨论,因此我将在 SDK 文档中安排。 不过,值得注意的是,为进行并行执行,启用了某些系统 DLL,其相应的 .H 文件现在正使用激活上下文 API。 举个最好的例子,我们来研究一下 COMMCTRL.H 的最近版本。目前存在着无数其名称类似 IsolationAwareImageList_Add 这样的内联函数。这些内联函数显示了激活上下文 API 的作用。 您还会看到一些使用 C++ 宏的高明技巧,这些技巧使现有的代码无需任何改动就可以编译。</P>
<P>还剩下最后一个不适合归入任何其他类别的 Kernel32 API。 GetModuleHandleEx 本应该在几年前就包括在 Win32 API 中。 它所增加的关键功能是,当给定了模块内的地址时,可以查找 HMODULE。 如果您曾经编写过调试代码或诊断代码,可能遇到过这样的情况:您知道代码地址,但需要确定它来自哪个 DLL。 我们也可以使用 VirtualQuery 这种比较笨拙的方式来实现此目的,但 GetModuleHandleEx 更加简洁。</P>
<P>与其前身 API (GetModuleHandle) 不同,GetModuleHandleEx 影响模块的引用计数,除非您显式指定它不要这样做。 根据指定的标志,它可以递增引用计数、使之保持不变或在进程的生存期 内一直将此 DLL 定位在内存中。 GET_MODULE_HANDLE_EX_FLAG_PIN 标志解决了我萦绕于心的一个担忧。 假设您调用了 GetModuleHandle 来检索 模块句柄。 在一个多线程程序中,有可能发生这样的情况:另一个线程会在同一个地址卸载 DLL 并加载另一个 DLL。 上述这种情况可能发生在第一个线程 获得 HMODULE、但还没有开始使用它的时候。 通过在内存中定位模块,您可以确保您获得的 HMODULE 在稍后被使用时是有效的。</P>
<P></P>
<P><B><I>用户接口方面的新内容</I></B></P>
<P>在用户接口方面,USER32 的最大新内容就是原始输入 API。 在键盘和鼠标作为接收来自用户输入的唯一方式外,这些 API 提供了另外一种方式。 使用原始输入 API,游戏杆、麦克风或触摸屏等设备,与键盘和鼠标具有同样的作用。</P>
<P>在标准 Windows 输入模型中,键盘和鼠标驱动程序创建低级扫描码和移动事件。 系统接受这些低级事件,将它们转换成更高级的消息,例如,WM_CHAR 或 WM_APPCOMMAND。 虽然这种方式使输入捕获变得非常简单,但对于其他输入设备不是非常适用。</P>
<P>用于原始输入的新 API 如<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 12 </A>所示。 在默认情况下,应用程序不接受原始输入。 取而代之的是,您必须进行注册,通过 RegisterRawInputDevices API 接受输入,该 API 接受您感兴趣的所有设备。</P>
<P>当设备发生输入时,系统就向程序的消息队列发出一个 WM_INPUT 消息。 程序在无缓冲模式(一次读取一个消息)下或缓冲模式(一次读取多个消息)下读取该输入。 正如您预料的那样,有一些 API 可以枚举所有原始输入设备,查询有关它们的信息。</P>
<P>在 USER32 中还有其他几个我认为很有趣的新 API。(请参见<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 12</A>)。 PrintWindow API 将指定的 HWND 的内容复制到指定的设备上下文 (DC)。 IsGUIThread 返回(也可以设置)一个值,该值可确定调用线程是否为 GUI 线程,这意味着该线程已调入 Win32K.SYS,有更大的内核模式堆栈。 BroadcastSystemMessageEx 类似 BroadcastSystemMessage,不同的是,它返回有关已拒绝请求的窗口的更多信息。</P>
<P>虽然文本服务框架 (TSF) 是作为早期操作系统的可重新发布版提供的,但它是第一次随 Windows Server 2003 和 Windows XP 一起出现。 文本服务框架是一个可扩展的系统,它可以用一种独立于输入/输出设备的方式读取和写入文本。 TSF 最擅长的访问方式是,允许应用程序从诸如笔或麦克风这样的设备接受文本输入。</P>
<P>每个不同类型的文本输入/输出设备都是一个“文本服务”。 文本服务和应用程序之间是 TSF 管理器。 如果用数据库来比喻,每个文本服务就像一个 ODBC 驱动程序,TSF 管理器则扮演着 ODBC 管理器的角色。 TSF 由几个 API 和许许多多接口组成。 如果要讨论这个话题,即使不需要一本书,也得占用文章的所有篇幅 — 因此我不打算在这里进一步说明。</P>
<P>近年来,Microsoft 以一种更图形化的表达方式,使 GDI 变得友好,面向对象。 当然,诸如 MFC 这样的应用程序框架已经朝着这个方向努力好几年了,但新的 GDI+ API 是核心操作系统的一部分,您无需再引入应用程序框架的所有那些概念。 大体上来说,GDI+ 功能可以分为以下四类: 二维矢量图形(直线和曲线)、图像处理(位图)、版式(文本显示)和矩阵变换。</P>
<P>虽然 GDI+ API 从技术上来说就像任何其他 Win32 API 一样,但您不可能直接调用它们(至少是从 C++ 代码)。 而 Platform SDK 则有一组定义了大约 40 个 C++ 类的头文件(例如,头文件名为 GDIPlus.H)。 这些类中比较典型的有 Bitmap、Font 和 Region。 这些类的方法通常是调用 GDIPlus.DLL 中的基础 API 的内联函数。</P>
<P>下面是 GDIPlus 类在 C++ 代码中的典型使用示例: <SPAN class=codeSample>VOID OnPaint(HDC hdc) { Graphics graphics(hdc); Pen pen(Color(255, 0, 0, 255)); graphics.DrawLine(&pen, 0, 0, 200, 100); } </SPAN>注意,代码中没有 BeginPaint/EndPaint 这样麻烦的过程。 一切都是面向对象的。 Graphics 对象是相当于设备上下文句柄 (HDC) 的 GDI+。 Pen 对象为您负责基础 GDI 笔的创建和析构。</P>
<P>了解 GDI+ 的最佳方法是,浏览 GDIPlus.H 以及它引用的文件。 要使用 GDI+,您需要在源文件中加入 #include GDIPlus.H,然后将 GDIPlus.lib 文件添加到链接器行。 要注意的是,GDIPlus.DLL 是并行启用的。 因此,您不会在 \windows\system32 目录中找到它。 相反,您会在 \windows\winsxs\ 下的各子目录中看到它的各种版本。</P>
<P></P>
<P><B><I>新公开的接口</I></B></P>
<P>Microsoft 最近公开了 Windows 2000 或更早期的操作系统中就已出现的几百个 API 和 COM 接口。 您可以访问 <A href="http://msdn.microsoft.com/library/en-us/dnapiover/html/api-overview.asp" target=_blank>Settlement Program Interfaces</A> 找到这些 API。 虽然它们在技术上不是新内容,但现在有了公开的文档,因此您可以放心地在 Windows XP 和更高版本的操作系统中调用它们。 坦率地说,当我看到这些 API 时,我认为除了几个以外,确实没有什么新鲜的东西。 当您以挑剔的眼光研究这些 API 之后,您会发现,它们大多数都是在完善现有的 API 集合而已。 此外,该列表中的一些 API 已经公开过了,但这次增加了新的细节。</P>
<P>到目前为止,最大的 API 子集要算是 SHELL32 库。 里面大约包括了 110 个 API,但从名称来看,许多 API 的使用非常有限。 比如说,有人用过 CDefFolderMenu_Create2 吗? 不过,令人高兴的是,超过 20 个的新 COM 接口公开了,包括 IMenuBand、IShellItem 和 IShellTaskScheduler。</P>
<P>新公开的 API 还有一些与密码有关的新函数。 WININET.DLL 有五个新 API,大多数都与代理支持有关。 DirectShow 功能增加了 12 个左右的新 API。 最后,其中有些 API,如 NtQuerySystemTime 和 RtlUnwind,在前面提到的 WINTERNL.H 文件中介绍过了。</P>
<P></P>
<P><B>DEBUGHLP 方面的新内容</B></P>
<P>长期阅读我的文章和 <I>MSDN Magazine</I> 专栏的读者都知道,DBGHELP.DLL 是我最喜爱的 DLL 之一。 自从其在 Windows 2000 出现以来,经历了相当大的变化。 该 DLL 具有如此之多的新用途,以至我真的很难决定从哪里开始说起。</P>
<P>DbgHelp 中最酷的特性之一并不是您调用的 API。 您是否经历过这样的挫折:您的调试符号与系统上的 DLL 变得不同步? 多亏 DBGHELP.DLL 的存在,这种问题(在很大程度上)已经成为过去。 新特性称为符号服务器。 当您要求 DbgHelp 加载模块的符号时,如果它在本地找不到调试文件,则调用符号服务器 DLL 来 定位调试文件。 符号服务器 DLL 可以用它认为合适的任何方式来定位调试文件。 从概念上来说,任何人都可以编写符号服务器 DLL,并且 DbgHelp 都会使用它。</P>
<P>实际上,Microsoft 已经创建了一个几乎所有人都会使用的符号服务器 DLL (SymSrv.DLL)。 此外,Microsoft 还将几乎每个相关的 Windows 版本的调试符号都放在可公开访问的 Web 服务器上。 最后的结果是,调试器和工具本身不用做额外工作即可动态地获得调试文件。 需要做的全部事情就是使用 DbgHelp.DLL 来访问符号。 SymSrv.DLL 是用于 Windows 的调试工具的一部分,可以从 MSDN 站点下载(请参见 <A href="http://msdn.microsoft.com/isapi/gomscom.asp?TARGET=/ddk/debugging/" target=_blank>Microsoft Debugging Tools</A>)。</P>
<P>SymSrv.DLL 在第一次需要 PDB 文件的时候自动下载适当的 PDB 文件,将它存储在本地。 它确保下载的 PDB 文件是用于该 DLL 的正确版本。 在本地存储 PDB 文件时,它使用一种允许多个版本的 DLL 的 PDB 共存的目录命名方案。</P>
<P>Visual Studio .NET 的用户可以使用符号服务器功能。 需要做的全部事情就是,将 SymSrv.DLL 放在 IDE (DevEnv.exe) 所在的同一目录,然后设置一个环境变量。 默认情况下,DBGHELP.DLL 使用 _NT_SYMBOL_PATH 中的路径来定位符号。 为了指示应该使用符号服务器,_NT_SYMBOL_PATH 应类似于如下所示: <SPAN class=codeSample>symsrv*symsrv.dll*c:\winnt\symbols* [url]http://msdl.microsoft.com/download/symbols[/url] </SPAN></P>
<P>显然,您希望路径部分(本示例为 "c:\winnt\symbols")指向硬盘上的一个有效目录。 假设您正确地完成了所有设置,该功能会顺利执行。 我已经在无数台机器上成功地使用了此功能,但不幸的是,如果您遇到问题,我无法提供支持。</P>
<P>DbgHelp 中的下一个重大特性是类型支持。 我于 2002 年 3 月在 <A href="http://msdn.microsoft.com/msdnmag/issues/02/03/hood/default.aspx" target=_blank>Under The Hood</A> 专栏上发表的文章详细地讨论了该主题,因此我在这里只是稍微提几句。 类型支持已经扩展到基元类型的范围之外,包括用户定义的类型。 使用新的 SymFromAddr 和 SymFromName API,您可以获得类型索引。 然后,该类型索引被传递到 SymGetTypeInfo,以获得有关类型的信息。 SymGetTypeInfo 是一个相当难以理解的 API,因此我再次建议您阅读前面提到的、有关此主题的专栏文章。 利用 SymEnumTypes,可以枚举给定的符号表中的所有用户定义的类型。</P>
<P>如果您是一个长期的 DbgHelp 用户,可能会注意到,许多新 API 与现有的接口并行运行。 例如,SymEnumSymbols 的用途看起来与 SymEnumerateSymbols 差不多。 新的 API 存在的理由是,旧的 API 提供的有关符号的信息不很完整。 而更新的 API 则始终使用 SYMBOL_INFO 结构,该结构中关于符号的信息要完整得多。</P>
<P>DbgHelp 还新增了另一个令人兴奋的特性 — 识别局部变量和参数。 过去,您可以枚举模块的符号,但只能是全局符号。 利用新的 SymEnumSymbols API,您可以枚举局部变量和参数。 为实现此目的,需要使用一个非显而易见的技巧 — 预先调用 SymSetContext 函数。 您应该在您感兴趣的特定函数内指定某个地址时调用 SymSetContext。在后台,DbgHelp 找到封闭函数的局部变量和参数,并且只枚举它们。</P>
<P>MiniDumpWriteDump API 也是一个令人兴奋的特性。 只要调用一次,您就可以创建自己的转储文件。 这些文件与您从 Dr. Watson 故障或 UserDump 工具得到的文件是完全一样的。 这些转储文件可以加载到 WinDBG 或 Visual Studio .NET 中,用于总结调试过程。 创建转储文件的原因通常是,您的程序可能在运行时遇到一些意外情况。 用户可以将该文件送回给您,这样您就可以在选择的调试器中仔细研究。</P>
<P>还有许多其他新的 DbgHelp API,但在本节中我只再讨论其中的几个。 现在,您可以通过 SymEnumLines API 枚举源文件行。 利用 SymAddSymbol 和 SymDeleteSymbol,您可以动态地扩展符号文件中定义的符号。 如果有 .NET 元数据方法标记,SymFromToken API 返回 SYMBOL_INFO。 在支持基于 .NET 框架的调试信息时,这对 DBGHELP 来说是很重要的第一个步骤。</P>
<P>现在增加了这么多的新特性,如果不展示其中的一些,简直有点说不过去。 DBGHELP51 程序(随本月的下载内容提供,下载内容位于本文开头的链接)使用了一些刚刚讨论的新 API,包括 SymEnumSymbols、SymEnumLines 和 SymGetTypeInfo。 要测试该程序,请运行 DBGHELP51.EXE,向它传递包括其调试信息的 EXE 文件的名称。 如果调试信息正确加载,DBGHELP51.EXE 首先会列出所有全局符号。 如果类型信息可用,类型名称跟在符号名称的后面。 输出的第二个部分是任何源文件和行号信息(如果找到这些信息)。</P>
<P></P>
<P><B>Windows 错误报告</B></P>
<P>一个新 DLL 只有两个导出的 API? 这就是 Windows 错误报告 API 涉及的内容。 近几年来,您可能已经注意到,Microsoft 已经做了大量工作,尽量减少应用程序错误带给普通用户的困扰。 例如,从 Windows XP 开始,当程序遇到一个未处理的异常时,会弹出一个对话框,询问您是否希望将报告发送给 Microsoft。 利用新的错误报告 API,应用程序可以在这点上更好地与系统集成。</P>
<P>第一个 API 是 ReportFault,在利用 try 块捕获自己的异常应用程序中使用该 API。 该 API 将应用程序绑定到系统的错误报告机制。 ReportFault 采用 EXCEPTION_POINTERS 结构作为参数。 调用 ReportFault 后导致的系统操作与没有捕获异常的情况下发生的系统操作是一样的。 ReportFault 的返回值是一个指示系统做了什么的代码。 例如,返回代码 frrvLaunchDebugger 指示,启动了调试器来连接程序。</P>
<P>第二个 API 是 AddERExcludedApplication,该 API 阻止系统报告有关指定的可执行文件的错误。 例如,如果调用该 API 时传递字符串 foo.exe,则调用 foo.exe 的任何程序如果出现未处理的异常,都不会报告。 AddERExcludedApplication 的参数应该只是一个简单的 EXE 名称,没有任何路径信息。</P>
<P></P>
<P><B>ADVAPI32</B></P>
<P>由于 Microsoft 非常重视安全问题,那么新增了一组被称为凭据 API 的接口也就不足为奇了。 这些 API 获取和管理诸如用户名和密码这样的信息。 它们可以请求 Windows XP 帐户信息,以代替登录时建立的凭据来使用。 这种请求通常发生在登录凭据没有应用程序所需的权限的情况下。</P>
<P><A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 13</A>显示了凭据 API,它们来自新 WINCRED.H 文件。 为了说明某个基本功能,我编写了 Credential 程序,该程序枚举所有当前凭据并显示每个凭据的基本信息(请参见<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 14 </A></P>
<P>)。 请试着在您的系统上运行该程序。 您可能会对看到的结果感到惊讶。</P>
<P>ADVAPI32 中的另一组新安全 API 是 safer API。 这些 API 旨在使启动其他程序的程序可以很容易地查询安全策略,以便在启动可执行文件之前获得批准。 此功能不仅限于可执行文件,因为还可以验证其他种类的活动内容,如脚本。 这些 API 对于处理电子邮件附件尤其有用。 <A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 15</A></P>
<P>显示了 safer API,它们是在 WinSafer.H 中定义的。现在的情况是,在有关如何使用函数的实际示例中,缺少这些 API 的内容。</P>
<P>ADVAPI32 还新增了几个事件跟踪 API(请参见<A href="http://www.microsoft.com/china/MSDN/library/windev/Windows2003/art/figures1.HTML" target=_blank>图 15 </A></P>
<P>)。 事件跟踪是在 Windows 2000 中引入的。正如您预想的那样,TraceMessage 将事件发送到指定的跟踪会话。 TraceMessageVA 在本质上是相同的 API,不同的是,它接受数量可变的参数。 EnumerateTraceGUIDs 返回有关系统的事件跟踪提供程序的信息,而您从名称就可以看出 FlushTrace 的功能。</P>
<P></P>
<P><B>OLE 已经过时</B></P>
<P>过去,OLE32 和 OLEAUT32 DLL 一直是新 API 和接口的温床。 自从 Windows 2000 推出以来,由于将重心放在 .NET 框架上,它的速度大大降低了。 CoRegisterInitializeSpy 是一个似乎很有趣的新 API。 您提供类型 IInitializeSpy 的接口实现,其方法在注册 spy 的线程上的 CoInitialize(Ex) 和 CoUninitialize 之前和之后调用。</P>
<P>CoGetContextToken API 返回当前上下文的 IObjContext。 令我们感兴趣的原因主要是,该值存储在 TEB 中的 ReservedForOle 字段中,它最后记录在 WINTERNL.H 中。</P>
<P>CoFreeUnusedLibrariesEx 函数类似其前身函数,它增加的功能是,立即释放未使用的库,而不是在默认情况下等待 10 分钟。 最后,新的 CoInvalidateRemoteMachineBindings API 通知 OLE 服务控制管理器刷新所指定计算机的任何缓存远程过程调用的绑定句柄。 除了这几个 API,OLE 中没有什么其他新内容了,OLEAUT32 也是一样。</P>
<P></P>
<P><B>群集</B></P>
<P>群集 API 有少量值得一提的新函数。 群集就是使用一个以上的物理资源向外界表示一个逻辑资源、从而使资源(例如,应用程序、硬盘和文件共享)保持高度可用的能力。 新的群集 API 中大多数是我所说的 "EnumCount" 子集。 简单地说,群集对象可分为五种类型: Group、Network、Node、Resource 和 Resource Type。 这些对象可以通过基于句柄的 API 来枚举。 这里提到的新 API(例如,ClusterNodeGetEnumCount)返回枚举句柄表示的对象的数量。</P>
<P>其余两个新 API 是 EvictClusterNodeEx 和 SetClusterServiceAccountPassword。 EvictClusterNodeEx 类似其前身函数,但增加了超时功能。 SetClusterServiceAccountPassword 可更改所有在线节点上的群集服务用户帐户的密码。</P>
<P></P>
<P><B><I>实时客户端 API</I></B></P>
<P>实时客户端 (RTC) API 有一个很大气的名称,但并没有真实地反映出它的功能。 虽然 RTC API 包括相当多的功能,但我发现很容易将它主要视为即时消息 (IM) API。 有关文档已经大致描述了它的作用,然而我一定要在这里转述一下: </P>
<P>利用 RTC 客户端 API,您能构建可发出 PC 到 PC 的呼叫、PC 到电话的呼叫或电话到电话的呼叫,或在 Internet 上创建 IM 会话的应用程序。 语音呼叫和视频呼叫都可以在 PC 到 PC 的呼叫上建立。 它还支持联系人列表上的“存在”信息。 另外,可以添加应用程序共享和白板,以增强任何会话类型的通信能力。</P>
<P>那么,RTC API 究竟是什么样子呢? 它是基于 COM 的,所以不用感到惊讶。 用于与 RTC API 协同工作的根接口是 IRTCClient,您可以使用 CoCreateInstance 获得该接口。 从该接口可以创建(或提供)其他接口,例如,IRTCSession、IRTCParticipant、IRTCBuddy 和 IRTCProfile 等等。 加在一起,共有二十四个以上的 RTC 接口。 如果您有具体的倾向,可以使用 RTC 接口创建自己的自定义即时消息客户端。</P>
<P><B><I>小结</I></B></P>
<P>就上述内容来看,很明显,Windows Server 2003 相比 Windows 2000 有了很大的改进。我一向将工作重点放在用户模式的编程 API,但在后台还有性能和可靠性方面的重大变动和新增内容。 就我个人来说,我非常兴奋地看到,向量化异常处理、并行执行以及对调试符号更好的支持等功能,都在朝着 Windows 的方向发展。</P>
<P>我们都知道: Windows XP 是 Microsoft 推出的最新客户端操作系统。 由于 Windows Server 2003 在各方面都涵盖了 Windows XP,您至少可以考虑使用 Windows XP 提供的新 API。 就个人来说,我在自己的一些机器上运行 Windows XP,在其他机器上运行 Windows Server 2003,在日常工作方面,我看不出它们有什么区别。 我希望您着眼于未来,自己努力探索 Windows Server 2003 中的奥秘。</P>
<P><STRONG>Matt Pietrek</STRONG> 是一位软件架构师兼作家。 他在 Compuware/NuMega 实验室担任 BoundsChecker 和 Distributed Analyzer 产品的首席 架构师。 他已经出版了三本有关 Windows 系统编程的书,并且是 <I>MSDN Magazine</I> 的特约编辑。 您可以访问他的个人 Web 站点 (<A href="http://www.wheaty.net/" target=_blank>[url]http://www.wheaty.net[/url]</A>),了解有关以前文章和专栏的常见问题解答和其他信息</SPAN></P>

页: [1]
© 1999-2008 EvilOctal Security Team