首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

14.8  高级的崩溃转储分析

上一节利用驱动程序检验器来创建可让调试器的自动化分析引擎能够解释的崩溃转储。

你可能仍然会碰到一些无法让系统产生很容易分析的崩溃转储的情形,如果是这样,你将需要执行以下的手工的分析来试着确定问题之所在。

n  使用“!process 0 0”调试器命令来查看哪些进程正在运行,并确保你了解每一个进程的用途。试着禁止或者卸载掉不必要的应用程序和Windows服务。

n  使用lm命令和kv选项,列出已加载的内核模式驱动程序。确保你理解任何第三方驱动程序的用途,以及你安装的是它们的最新版本。

n  使用!vm命令,看一看该系统是否耗尽了虚拟内存、换页内存池或非换页内存池。如果虚拟内存已被耗尽,则已提交的页面将接近于提交限制(commit limit),所以,你可以检查进程列表,看哪一个进程报告了很高的提交内存量,以期望找到一个可能的内存泄漏错误。如果非换页内存池或换页内存池已被耗尽(也就是说,使用量已接近于最大值),则看一看第7章中的“实验:诊断内存池泄漏”。

还有其他一些调试命令可以提供有用的、但是更加高级的知识,它们对于崩溃转储分析也是很有用的。!irp命令就是其中之一。下一小节叙述了通过该命令来找到一个可疑驱动程序的方法。

栈破坏

栈溢出(stack overrun)或栈破坏(Stack Trash)是由于缓冲区上溢或下溢错误造成的。然而,目标缓冲区并不像你在Notmyfault的缓冲区溢出错误中看到的那样位于内存池中,而是位于执行此错误(bug)的线程的栈中。这种类型的错误是另一种很难调试的错误,因为栈是任何崩溃转储分析的基础。

当你运行Notmyfault并选择了Stack Trash时,Myfault驱动程序溢出一个缓冲区,该缓冲区是它在当前执行线程的内核栈中分配的。当Myfault试图将控制权返回给发起此调用的Ntoskrnl函数时,它从栈中读取返回地址,这是该线程应该继续往下执行的地址。该地址已经被栈缓冲区溢出破坏了,所以,该线程在内存中另外某一个地址处继续执行——该地址处可能根本没有包含代码。该线程执行到一条非法CPU指令或者它引用了无效内存时,就会引发一个非法异常并崩溃。

栈溢出的崩溃转储分析把错误的职责归结到哪个驱动程序上,这将随着每次崩溃而有所不同,但是,停止代码几乎总是KMODE_EXCEPTION_NOT_HANDLED。如果你执行一次详细的分析,则栈痕迹看起来如下所示:

STACK_TEXT:  
b7b0ebd4 00000000 00000000 00000000 00000000 0x0

这就像是内核栈已经被用零来改写过一样。不幸的是,像特殊内存池和系统代码写保护这样的机制无法捕捉到这种类型的错误。相反地,你必须采取一些手工的分析步骤,来间接地确定在破坏发生的时候,哪个驱动程序正在执行。一种办法是,对于在栈破坏时刻正在执行的线程,检查它的正在进行之中的IRP。当一个线程发出一个I/O请求时,I/O管理器将一个指针(指向这一未完成的IRP)保存在该线程的ETHREAD结构的IRP列表上。调试器命令!thread可以转储出一个线程的IRP列表。(如果你没有指定一个线程对象地址,则!thread转储出该处理器的当前线程。)然后,你可以利用!irp命令来查看该IRP。

kd> !thread
THREAD ff740020  Cid 8f8.420  Teb: 7ffde000  Win32Thread: a20cdbe8 RUNNING
IRP List:
    bc5a7f68: (0006,0094) Flags: 00000000  Mdl: 00000000
Not impersonating
Owning Process ff75f120
...
 
kd> !irp bc5a7f68
Irp is active with 1 stacks 1 is current (= 0xbc5a7fd8)
 No Mdl Thread ff740020:  Irp stack trace.  
     cmd  flg cl Device   File     Completion-Context
>[  e, 0]   0  0 ff79e4c0 ff7ac028 00000000-00000000    
       \Driver\MYFAULT Args: 00000000 00000000 83360010 00000000

上面的输出显示了IRP的当前栈单元(由“>”前缀指明的栈单元,在这里也是惟一的栈单元)属于Myfault驱动程序。如果这是一个实际系统的崩溃,那么,接下来的步骤是,确保所安装的驱动程序版本是最新的版本,如果不是的话,则安装新的版本;如果是的话,则针对该驱动程序启用驱动程序检验器(打开除了低内存模拟以外的所有选项)。

挂起的系统或无响应的系统

如果一个系统变得不再响应了(也就是说,对于你的键盘和鼠标输入,你没有收到任何响应),鼠标被冻结了,或者你虽然可以移动鼠标,但是系统不再响应鼠标点击事件,那么,该系统称为已经挂起(hung)了。以下的一些事情可以导致一个系统被挂起。

n  一个设备驱动程序没有从它的中断服务(ISR)例程或者延迟过程调用(DPC)例程中返回。

n  一个高优先级的实时线程抢占了窗口系统驱动程序的输入线程。

n  在内核模式中发生了死锁(两个线程或处理器相互之间持有对方想要的资源,而且都不愿意放弃自己的资源所有权)。

如果你的系统是Windows XP或Windows Server 2003,那么,你可以利用驱动程序检验器中的一个称为“死锁检测(deadlock detection)”的选项,来检查死锁。这一死锁检测功能会监视自旋锁、快速互斥体和互斥体的使用情况,查找可能会导致死锁的模式(有关这些以及其他同步原语的更多信息,请参见第3章)。 如果找到了死锁的模式,则驱动程序检验器使当前系统崩溃,并且指明哪个驱动程序引起了死锁。当两个线程都持有对方想要的资源,并且都不愿意放弃自己已有的资源或放弃对期望资源的等待时,最简单形式的死锁就会发生。如果你正在运行Windows XP或Windows Server 2003,则诊断一个挂起的系统的第一个步骤是,对于可疑的驱动程序启用死锁检测特性,然后对于未签名的驱动程序,再对于所有的驱动程序都启用死锁检测特性,直至你得到一个崩溃转储,它指明了是哪个驱动程序引起了死锁。

如果你正在运行Windows 2000或者已经检验了所有的驱动程序,但系统仍然被挂起,那么,你必须要么手工使此挂起的系统崩溃,并分析所得到的崩溃转储,要么用内核调试器进入系统,采取另外的方法来检查挂起的原因。

有两种方法可允许你进入到一个挂起的系统中,从而你可以应用本章中讲述的手工崩溃诊断技术来确定是哪个驱动程序或组件导致了系统挂起:第一种方法是,让已挂起的系统崩溃,希望得到一个能够进行分析的崩溃转储;第二种方法是,用内核调试器进入该系统,并分析系统的行为。这两种方法都要求事先建立准备环境,再重新引导。你可以在这两种方法中,使用同样的系统状态分析手段,来试着确定挂起的原因。

为了用手工使一个已挂起的系统崩溃,你必须首先加入一个DWORD注册表值HKLM\System\CurrentControlSet\Services\i8042prt\Parameters\CrashOnCtrlScroll,并将其设置为1。在重新引导以后,i8042端口驱动程序(它是PS2键盘输入的端口驱动程序)在它的中断服务例程(ISR,第3章中有进一步的讨论)中监视键盘的键击,寻找两次ScrollLock键击,同时右Ctrl键也被压下。当该驱动程序看到这样的序列,它调用KeBugCheckEx,并且停止代码为MANUALLY_INITIATED_CRASH (0xE2),表明这是一个由用户手工触发的崩溃。当系统重新引导起来时,打开崩溃转储文件,并应用本章前面提到的技术,来试着确定系统为什么被挂起(例如,确定当系统挂起时哪个线程正在运行,其内核栈指明了当时正在发生什么事情,等等)。注意,这种做法对于绝大多数挂起的系统是行得通的,但如果i8042端口驱动程序的ISR没有执行,则这种做法就不能生效了(如果所有处理器的IRQL都高于此ISR的IRQL,从而导致它们全部被挂起,或者系统数据结构的破坏波及了与中断相关的代码或数据,那么,i8042端口驱动程序的ISR将不会执行)。

注     利用i8042端口驱动程序中提供的支持来用手工使一个已挂起的系统崩溃,这种做法对于USB键盘行不通。它只能跟PS2键盘一起工作。

另一种触发系统崩溃的方法是,如果你的硬件有一个内置的“崩溃”按钮(有些高端的服务器有这样的按钮),在这种情况下,通过给系统主板的不可屏蔽中断(NMI)针脚发送一个信号,则可以触发崩溃。为了启用这种能力,你可以将DWORD注册表值HKLM\System\ CurrentControlSet\Control\CrashControl\NMICrashDump设置为1。然后,当你按下转储开关时,系统就会接到一个NMI,内核的NMI中断处理器调用KeBugCheckEx。这种方法比i8042端口驱动程序的机制可以在更多的场合下使用,因为NMI IRQL总是高于i8042端口驱动程序中断的IRQL。有关更多的信息,请参见http://www.microsoft.com/hwdev/platform/proc/dmpsw.asp

如果你不能用手工生成一个崩溃转储,那么,你可以首先在系统引导的时候让系统进入调试模式,再试图进入已挂起的系统。有两种方法可以让系统进入调试模式:你可以在引导的时候按下F8键,并选择Debugging Mode;或者,你可以在Boot.ini中创建一个调试模式引导选项,做法很简单,你只需从系统的Boot.ini中复制一个已有的引导条目,然后加上/DEBUG开关。如果你使用F8键这种做法,那么,系统将使用默认的连接(串行端口COM2和19200波特率)。若使用/DEBUG方案,你必须也要配置在“运行内核调试器的主机系统”和“引导进入调试模式的目标系统”之间的连接机制,然后针对此连接类型配置适当的/Debugport和/Baudrate开关。两种连接类型为:使用串行端口的零调制解调器线缆;或者,对于Windows XP和Windows Server 2003系统,在每个系统上使用1394端口的IEEE 1394(Firewire)线缆。关于如何配置调试主机和目标系统来进行内核调试的详细信息,请参见Windows调试工具箱的帮助文件。

当引导进入调试模式时,系统在引导时刻加载内核调试器,并且准备好跟串行线缆或IEEE 1394线缆另一端的计算机上正在运行的内核调试器建立一个连接。注意,内核调试器的存在并不会影响性能。当系统挂起时,在被连接的系统上运行Windbg或Kd调试器,建立一个内核调试连接,并进入到已挂起的系统中。如果中断已被禁止,或者其内核调试器已经被破坏,那么,这种方法也不能奏效。

注     引导一个系统进入到调试模式下,如果它没有被连接到另一个系统,则并不会影响性能;然而,对于一个已被配置成一旦崩溃即自动重新引导的系统,如果它在引导时启用了内核调试特性,则它不会自动重新引导,因为在崩溃以后,其内核调试器将等待跟另一个系统建立一个连接。

当你执行分析时,你可以不让系统保留在停止的状态,而是使用调试器的“.dump”命令,在主调试器机器上创建一个崩溃转储文件。然后,你可以重新引导被挂起的系统,并且以离线的方式来分析这一崩溃转储(或者提交给Microsoft)。注意,如果你是使用串行的零调制解调器线缆来连接两台计算机的,则可能要花一点时间(相比较于高速的1394连接),所以,你可能会使用“.dump /m”命令,仅仅捕捉一个小转储。另外一种做法是,如果目标主机还有能力写一个崩溃转储,那么,你可以从调试器中发出“.crash”命令,以强迫它写一个崩溃转储。这将使得目标机器在它的本地硬盘驱动器中创建一个崩溃转储,当系统重新引导以后你可以检查此转储。

你可以运行Notmyfault并选择Hang选项,以产生一次挂起情形。这使得Myfault驱动程序在系统的每个处理器上排队一个DPC,并且该DPC执行一个无限循环。因为处理器在执行DPC函数时的IRQL是DPC/Dispatch级别,所以,键盘ISR将会响应特殊的崩溃键击序列。

如果你已经在调试器中进入了一个已挂起的系统,或者加载了一个来自于挂起系统中的、手工生成的转储,那么,你应该执行!analyze命令,并带上-hang选项。这使得调试器检查该系统中的锁,并试图确定是否存在一个死锁,如果有死锁的话,确定哪个(或哪些)驱动程序被牵连进去。然而,对于像Notmyfault生成的挂起情形,!analyze分析命令不会报告任何有用的信息。

如果!analyze命令没有查明问题之所在,那么,在该转储的每个CPU环境中执行!thread!process,看一看每个处理器正在做什么(利用~命令可切换CPU环境,例如,使用~1切换到1号处理器的环境中)。如果由于有一个线程在DPC/Dispatch级别或更高的IRQL上执行一个无限循环,从而挂起了系统,那么,你将会在!thread命令的栈痕迹中看到,该线程是在哪个驱动程序模块中被停住了。当你使一个正在经历Notmyfault挂起错误的系统崩溃时,你得到的崩溃转储的栈痕迹看起来如下所示:

STACK_TEXT:  
f9e66ed8 f9b0d681 000000e2 00000000 00000000 nt!KeBugCheckEx+0x19
f9e66ef4 f9b0cefb 0069b0d8 010000c6 00000000 i8042prt!I8xProcessCrashDump+0x235
f9e66f3c 804ebb04 81797d98 8169b020 00010009 i8042prt!I8042KeyboardInterruptService +0x21c
f9e66f3c fa12e34a 81797d98 8169b020 00010009 nt!KiInterruptDispatch+0x3d
WARNING: Stack unwind information not available. Following frames may be wrong.
ffdff980 8169b288 f9e67000 0000210f 00000004 myfault+0x34a
8054ace4 ffdff980 804ebf58 00000000 0000319c 0x8169b288
8054ace4 ffdff980 804ebf58 00000000 0000319c 0xffdff980
8169ae9c 8054ace4 f9b12b0f 8169ac88 00000000 0xffdff980

栈痕迹的最上面几行引用了你敲入i8042端口驱动程序的崩溃键序列时所执行的例程。这里出现了Myfault驱动程序,表示它可能要为这次系统挂起负责。

另一个可能有指示作用的命令是!locks,它转储出所有执行体资源锁的状态。在默认情况下,该命令只列出那些处于竞争条件下的资源。这里竞争的资源是指,它们已被某个线程所持有,而且至少有一个线程正在等着获得它们。用!thread命令来检查这些资源的所有者的线程栈,以便看清楚这些线程可能在哪个驱动程序中执行。

当没有崩溃转储时

在这一节中,我们将针对那些由于某种原因而未能记录一个崩溃转储的系统,讨论如何进行诊断。未能记录一个崩溃转储的可能的原因是:引导卷上的页面文件太小,以至于未能容得下此崩溃转储,或者,在重新引导以后,没有足够的空闲磁盘空间来提取出此崩溃转储。这两种情形很容易解决,或者增大页面文件的大小,或者配置系统的崩溃转储设置,让它将此转储保存到有足够磁盘空间的卷上。

未能记录崩溃转储的第三个原因是,在崩溃时刻,用于写崩溃转储的内核代码和数据结构已经被破坏了。

正如前面所描述的那样,当系统引导的时候,此数据是计算了校验和的,如果在崩溃时刻所做的校验和不匹配,则系统根本不考虑保存崩溃转储(以避免可能会破坏磁盘上的数据)。因此,在这种情况下,你需要在系统崩溃的时候,捕捉住系统,然后试着确定崩溃的原因。

最后一个可能的原因是,系统磁盘的磁盘子系统不能够处理磁盘写请求(可能正是这个条件本身触发了系统失败)。这样的条件可能是磁盘控制器中的一个硬件失败,或者也可能是硬盘附近的连接线缆问题。

一种简单的方案是,在“Startup And Recovery”设置中关闭“Automatically Restart”选项,这样,如果系统崩溃了,你可以检查控制台上的蓝屏。然而,只有对最简单的崩溃,才能够仅仅根据蓝屏上的文本信息来加以解决。

为了执行更为深入的分析,你需要使用内核调试器,以便在崩溃的时候查看系统的情况。你只需按照上一小节所讲述的那样在调试模式下引导系统,就可以做到这一点。当一个在调试模式下引导起来的系统崩溃时,它不再绘制蓝屏和试图记录崩溃转储,相反地,它会一直等待,直至有一个主内核调试器连接过来。这样,你可以看到崩溃的原因,可能还可以利用前面讲述的内核调试器命令来执行一些基本的分析。正如在上一小节中所提到的那样,你可以在调试器中使用.dump命令来保存该崩溃系统的内存空间的一份拷贝,以便于将来调试。因此,你可以重新引导此崩溃的系统,并且以离线方式来调试该问题。

实验:蓝屏屏幕保护程序

有一种很巧妙的方法可以提醒你自己,蓝屏应该长什么样,或者也可以用来捉弄你的办公室同事或朋友,那就是,运行Sysinternals的蓝屏屏幕保护程序(Sysinternals Blue Screen screen saver,你可以从www.sysinternals.com下载)。此屏幕保护程序模拟真实的蓝屏,它显示了当前你正在运行的Windows的版本,并且利用实际的系统信息(比如已加载的驱动程序的列表)来生成所有的蓝屏文本。它也会模仿一次自动的重新引导过程,直到出现Windows的启动屏幕(splash screen)。注意,在其他的屏幕保护程序中,一旦鼠标移动就会退出屏幕保护,而与此不同的是,蓝屏屏幕保护程序要求一次键击才能退出。

利用Sysinternals的Psexec工具的以下语法,你甚至可以在另一个系统上运行屏幕保护程序:

psexec \\computername –i –d “c:\sysinternals bluescreen.scr” –s

该命令要求你在远程系统上具有管理特权(你可以使用Psexec的-u和-p开关来指定其他的凭证)。 不过,你一定要确保你的同事懂得幽默!

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论