翻译:Kendiv(fcczj@263.net)
更新: Friday, January 14, 2005
本书的主页:
http://www.orgon.com/w2k_internals/
本书电子版的下载地址:
http://www.volynkin.com/references.htm
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
希望这些翻译能给大家有所帮助,我的E文不是很强,所以翻译的速度不是很快,我尽量保证最少每周增加一篇新的译文,已贴出来的译文我还会经常回来做些修改,另外,在翻译时,有些非技术的内容,我会省略或者不译(水平低啊,译的费劲,呵呵)。希望大家多多指教。
To all the people in the world who never stopped asking “Why”
尽管本书中的很多内容都称之为“Undocumented”,但其中的一些内容只能通过挖掘操作系统的代码才能获取。Windows 2000 DDK(Device Driver Kit)提供了一个强大的调试器可以出色的完成这方面的工作。本章将从建立一个完善的调试环境开始介绍。在阅读随后的章节时,你会经常的使用内核调试器来挖掘操作系统内部的各种特性。如果你对内核调试器很是厌烦,或许你需要制作一个自己的调试工具了。因此,本章还将介绍有关Windows 2000调试接口的文档化和未文档化的资料,包括微软符号文件(Symbol File)的详细信息。本章的特色是提供了两个示例库和用于列出进程、进程和系统模块,以及隐藏在Windows 2000符号文件中的各种符号信息的示例程序。做为一个特殊的收获,在本章结束时,你将得到首份有关PDB(Microsoft Program Database)的公开文档。
建立一个调试环境
“嗨,我不想调试Windows 2000程序。在此之前,我想自己写一个先!”当你读到这个标题时你可能会这样大喊出来。“很对!”我说“这就是你该去做的!”但是为什么你要以建立一个调试环境开始这次旅行呢?答案很简单:调试器是进入系统的后门。当然,这并不是调试器开发人员的主要目的。然而,当你跟踪代码的执行过程或者你的程序意外的玩完时,任何优秀的调试器都须能够告诉你一些有用的系统信息。仅仅报告一个指向4GB地址空间某处的8位崩溃地址,然后让你独自一人去寻找到底发生了什么,真是无法让人接受。调试器至少应该告诉你最后执行的引发错误的代码是哪个模块中的代码,而且,在理想情况下,它还应该告诉你让你的程序玩完的那个函数的名称。因此,调试器通常必须知道比编程手册还要多的系统信息,你可以利用这些信息来研究系统的内部情况。
Windows 2000提供了两个调试器:WinDbg.exe(发音很像“WindBag”,译注:WindBag在俚语中指空话连篇的人)一个Win32 GUI程序和i386kd.exe一个提供与之等价功能的命令行模式程序。我曾经同时使用过这两个程序,最后确定i386kd.exe是最好的一个,因为它有一组非常强大的选项。不过,最近看来WinDbg.exe似乎有所改进。不过,本书中的所有例子都是与i386kd.exe相关的。就像你猜想的那样,i386前缀表示目标平台(Intel 386处理器家族,也包括Pentium)kd是Kernel Debugger(内核调试器)的缩写。Windows 2000内核调试器是一个非常强大的工具。比如,他知道如何使用Windows 2000安装光盘中的符号文件(Symbol files),因此,可以给出系统内存中几乎任何地址的相关符号信息(这非常有价值)。而且,它还可以反编译二进制代码、将内存信息的16进制转储数据以多种格式显示,甚至还能显示一些内核关键结构的布局。在调试器的在线帮助中有其命令行接口的详细文档。
准备一次崩溃转储(Crash Dump)
这些都是好消息。坏消息是你在内核调试器顺从你之前,必须做一些准备工作。第一个障碍是调试通常涉及两台独立的计算机(通过线路连接在一起),其中一台运行调试器,另一台用于被调试。然而,如果并不需要实时调试,那么有一个简单的方法,可以不需要第二台机器。例如,如果一个有错误的程序抛出了一个未处理的异常而引发了声名狼藉的NT蓝屏死机(Blue Screen Of Death, BSOD),你可以选择保存崩溃前的内存映像到一个文件中,在重新启动后,检查这个崩溃转储(Crash Dump)文件。这项技术通常被叫做post mortem(事后检查)在拉丁文中,post mortem意思是“after death”。这种方式是本书首选方法之一。在这里,我们的主要任务是研究系统内存,在大多数情况下,内存数据是来自还在工作的系统或者来自系统崩溃前内存的一个快照(snapshot)都并不重要。然而,一些有趣的信息则需要通过内核模式的驱动程序深入正在工作的系统的内部才能观察到,这一主题被保留在后面的章节中。
一个崩溃转储(Crash Dump)只是简单将当前内存数据写入一个磁盘文件而已。因此,一个完整的崩溃转储(crash dump)文件的大小通常与系统物理内存一样大(事实上,会略微小些)。崩溃转储(Crash dump)是由内核中的一个特殊程序在处理致命错误过程中生成的。然而,这个例程(handler)并不是立即将内存数据写入目标文件中。这是个不错的处理方式,因为在系统崩溃后,磁盘文件系统可能也不能正常工作。因此,内存映像首先被复制到页面文件存储器(page file storage),这是系统内存管理器的一部分。因此,你应该将你的页面文件大小增加到至少两倍于物理内存。两倍?一样大还不够吗?当然—那只够存放崩溃转储(crash dump)。要知道,在启动时,系统会尝试将崩溃转储(crash dump)映像复制到实际的磁盘文件,这意味着,如果系统不能及时的释放映像数据占用的页面文件,它就可能用尽所有的虚拟内存。通常,系统会处理这种情况,它会疯狂的读写磁盘并向你抛出一个惹人厌的“虚拟内存不足”的警告。只要你预料蓝屏的概率会增大时,将页面文件设置的足够大,这将会为你节省很多时间。
到这儿,你应该打开Windows 2000的控制面板,改变如下的设置:
l 增加页面文件到至少两倍物理内存的大小。
l 接下来,配置系统以便当蓝屏发生时生成一个崩溃转储(crash dump)文件。在系统属性对话框里,选择高级页,然后单击启动和恢复按钮,检查写入调试信息选项。你应该在下拉列表中选择完成内存转储选项。在转储文件对话框中输入一个文件名和路径,转储文件将会从页面文件中复制到你指定的这个文件中。%SystemRoot%/MEMORY.DMP是默认设置。
让系统崩溃
当设置好系统准备一次crash dump后,是时候做在Windows 2000系统程序员一生中最厌恶的事了:开始让系统崩溃!通常,只要达摩克利斯的剑挂在了你的头顶上(通常是在离产品截止时间还有几个小时的时候)你就会看到恐怖的蓝屏。现在,是你自愿让系统崩溃,但你可能无法找到有问题的软件来完成这项“工作”。来试试David Solomon在他的《Inside Windows NT 第二版》中提到的那个优雅的诀窍:
“如何能可靠的产生一个崩溃转储(crash dump)文件?只需要使用Windows NT资源工具中的kill.exe工具,kill掉Win32子系统进程(csrss.exe)或者Windows NT登陆进程(winlogon.exe),你必须有管理员权限”(Solomon [1998],p.23.)
神奇,太神奇了!这个窍门不能在Windows 2000上工作!第一感觉,很不走运,但是从另一个角度看,这是个好消息。当你知道使用微软自己正式发布的一个小工具就能如此轻松的破坏系统,你会怎样想?事实上,微软关闭这个安全漏洞非常对。可是,我们现在需要一种方法来使系统崩溃啊。在这一点上,想想那个古老而简单的NT规则:“If anything seems to be impossible in the Win32 world just write a kernel-mode driver, and it will work out all right!”Windows 2000非常谨慎的管理Win32程序。它在应用程序和内核之间构建了一堵墙,任何企图跨越此边界者都会被毫不留情的解决掉。这对于系统的稳定性是个好消息,但是对于编写需要直接与硬件打交道的程序的人来说并不是个好消息。想想DOS,在那儿任何程序都可以直接触及硬件,在这方面Windows 2000有些过分讲究。但这并不意味着在Windows 2000中访问硬件是不可能的。不同的是,这种访问被限制到一个特殊类型的模块—内核模式的驱动程序(Kernel-mode driver)。
我可以告诉你,现在我将简要的介绍一下Kernel-mode driver编程技术(这本是第三章的内容)。眼下,这已经足够说明kernel-mode driver让系统崩溃是非常容易的事。当驱动程序出错时,Windows 2000没有提供一种错误恢复机制,这就导致即使无意中试图执行一个不合法的操作也会招来蓝屏。当然,最简单而且危险最小的违规动作就是读取一个无效的内存地址。由于系统显示捕获所有通过空指针进行的内存访问,这是C编程中一个常见的错误,读取一个空指针是让系统崩溃的理想动作。示例代码中的w2k_kill.sys driver就是这么做的。这是一个非常简单的程序,同时也是出现在本书中的第一个kernel-mode driver。
列表1-1是w2k_kill.c的部分引用,展示了引发蓝屏的错误代码。当编写这样无意义的代码时,需要注意内建于Visual C/C++的优化器可能会抵消你的努力,它会跟踪所有的代码并试图消除其中任何有副作用的部分。在下面的例子中,优化器并未起作用,因为DriverEntry()坚持将在0地址发现的东西作为其返回值。这意味着这个数值将会被存放到CPU的EAX寄存器中,最简单的方法就是 MOV EAX, [0]指令,这个指令将会抛出我们期待的异常。
NTSTATUS DriverEntry ( PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING puRegistryPath )
{
return *((NTSTATUS*)0); // read through NULL pointer
}
列表 1-1 A NULL Pointer Read Operation in Kernel-Mode Crashes the System
w2k_load.exe程序出现在第三章,用来载入并启动w2k_kill.sys驱动程序。如果你在精神上做好了kill掉你的Windows 2000系统,请按照下面的步骤来做:
l 关闭所有应用程序
l 插入本书的附带光盘
l 在开始菜单中选择运行
l 输入d:/bin/w2k_load w2k_kill.sys,用你CD-ROM的盘符替换d:,然后单击确定
当单击后,w2k_load.exe将试图加载w2k_kill.sys文件(位于光盘的/bin目录下)。随后DriverEntry()开始执行,蓝屏出现了,如图1-3所示,你会看到当内存数据被转储到页面文件存储器时,屏幕上会有一个计数器从0逐渐增加到100。如果你在启动和恢复对话框中选中了自动重起,当崩溃转储完成后,系统会立即重新启动。当系统进入等待登陆状态后,稍等一会直到硬盘灯不再闪烁。这是因为将崩溃转储数据从页面文件复制到磁盘文件需要一定的时间,特别是你的物理内存很大时。在此时干扰系统,例如,将系统过早的关闭,可能会产生一个无效的崩溃转储文件。
在图1-3中,可以看出系统会显示包含出错代码的模块的名称(w2k_kill.sys),以及引发异常的指令地址(0xBECC3000)。这个地址或许和你的系统的不同,因为它是随硬件配置而变化的。驱动程序的加载地址通常都不确定,和DLL的加载地址类似。请记下显示的地址----稍后在安装和配置内核调试器是你还需要它。
一点小的提示:故意让系统崩溃不该是你每天都作的事。尽管有问题的w2k_kill.sys本身是无害的,但在它执行的那一刻可能并不那么走运。如果读取空指针时另一个线程正在做某些重要的事情,系统可能会在该线程有机会做清理工作之前关闭。比如,在重起之后,活动桌面往往会抱怨发生了一些可恶的事情,它需要进行恢复。因此,在你使系统崩溃之前,应该仔细的检察系统是否影响了重要数据并且保证所有cache中的数据都被写入了磁盘。注意,作者和出版商不会对w2k_kill.sys驱动程序造成的破坏负责。
译注:达摩克利斯希腊传说中的叙拉古国王狄奥尼西奥斯的朝臣,据传说其被迫坐在上悬宝剑的餐桌旁,宝剑由一根头发系住,以此来暗示君王命运的多危
安装符号文件
重新启动后,你就有了一个Windows 2000系统的快照(snapshot),包括一个有问题的Kernel-mode Driver(读取空指针时被捕获)。观察此快照文件和察看实际系统内存是一样的。当然,这个快照文件和动物死尸一样---不再对外界刺激有所反应,但是你现在不需要担心这些。接下来你需要安装内核调试器需要的符号文件(symbol files),当你分析崩溃文件时,你就会用到了。
MSDN用户可以在Windows 2000 Customer Suuport – Diagnostic Tools光盘上找到这些符号文件。插入光盘,用IE打开光盘上的DBG.HTM文件,你会看到很多安装选项。如果你运行的是free build的Windows 2000,你最好安装retail symbols。对于checked build版,可以选择安装debug symbols。安装程序会从SYMBOLS.CAB中复制一些.dbg和.pdb文件到系统符号文件目录中。默认的系统符号文件目录为:%SystemRoot%/Symbols。%SystemRoot%环境变量代表Windows 2000的安装目录。
在起动时,Windows 2000内核调试器会尝试通过环境变量_NT_SYMBOL_PATH指示的路径来寻找符号文件,所以最好正确的定义该变量。
译注:
现在可以通过Symchk.exe工具来检查和下载最新的符号文件,该工具随Debugging Tools for Windows 软件包安装。
微软的文档中对于_NT_SYMBOL_PATH应该指向哪里的说明有些模糊不清。在DDK的内核调试一节里提到必须包含符号子目录,即C:/WINNT/Symbols或等价目录。而在SDK关于dbghelp.dll库的文档中,有关符号路径的描述又稍微有些区别:
“该库需要使用符号搜索路径来定位.dll、.exe或.sys对应的调试符号(.dbg文件)。它会在路径后添加/dll、/exe或/sys。例如,.dll符号文件位于:C:/WINNT/Symbols/dll,.exe文件的路径则为:C:/WINNT/Symbols/exe”
。。。。。。
“如果你设置了_NT_SYMBOL_PATH环境变量,符号管理器按照如下顺序搜索符号文件:
1. 应用程序的当前工作目录
2. _NT_SYMBOL_PATH指示的目录
3. _NT_ALT_SYMBOL_PATH指示的目录
4. SYSTMEROOT指示的目录
”
这样看来把_NT_SYMBOL_PATH设定为C:/WINNT似乎要好于C:/WINNT/Symbols,为了确定哪种说法是正确的。我试验了这两种方法,很高兴它们都能正常的工作。
配置内核调试器
构建调试环境的最后一步就是安装和配置内核调试器。如果你已经安装了Windows 2000 DDK,那你可以在/NTDDK/bin目录中找到调试器。内核调试器的可执行文件名为i386kd.exe。另一种方法是从Windows 2000 Customer Support---Diagnostic Tools光盘中安装。
为了使用前面我们得到的崩溃转储文件,你需要使用i368kd的 –Z 选项。示例如下:
i386kd –z C:/WINNT/MEMORY.DMF
成功打开我们的crash dump后,你会看到类似图1-7所示的东东,kd>提示符会出现,这表示内核调试器已经准备接受命令了。在开始之前,请检查符号搜索路径是否正确。列出的启动信息,表示调试器已经加载了三个扩展DLL。i386kd.exe一个强大的特性就是其扩展机制,这允许第三方采用独立的DLL来扩展其基本功能。对于这些扩展的命令,要在其前面加上!号以区分内建的命令。
如图1-7所示,我输入了一个内建命令:u becc3000,u的含义是“反编译(unassembel)”,becc3000是开始反编译的16进制地址。默认情况下,均采用16进制,但你也可也通过命令来改变此默认值。命令为:n 10,此后默认所有数字都是10进制表示。你可以使用0x前缀来表示这是一个16进制数。地址becc3000就是w2k_kill.sys引起系统崩溃的地方。请使用你在蓝屏时看到的地址。如果一切正确的话,你会看到mov eax, [00000000]指令,如上图所示。如果没有看到的话,你可能没有使用正确的崩溃转储文件。mov eax, [00000000]指令表示从虚拟地址(也可称作线性地址)0x00000000读取一个32位的数值到CPU寄存器EAX中,这明显是列表1-1中*(NTSTATUS*)0)表达式的实现,等同于读取空指针的操作。没有针对此类错误的异常处理例程,因此,系统在蓝屏上会显示KMODE_EXCEPTION_NOT_HANDLED,如图1-3所示。如果你想知道有关此错误信息更多的东西请参考《The NT Insider》(Open Systems Resources 1999b)。