分析 Windows 崩溃转储文件





4.00/5 (8投票s)
一篇关于如何分析崩溃转储文件的文章。
引言
本文将重点介绍如何使用 Windows 调试工具来分析崩溃转储文件。其目的是鼓励读者在使用这些技术来处理系统崩溃时,能够自己动手。对于那些经常遇到系统崩溃的人来说,掌握这项技能是完全有可能的。一旦理解了一些必要的原理以及执行分析所需的工具,分析操作系统生成的崩溃转储文件就可以成为一项轻松的任务。分析崩溃转储文件需要工具。分析崩溃转储文件所需的工具是 Windows 调试工具的调试器。安装这些工具后,您需要下载符号文件以在本地缓存。在调试过程中,也可以通过设置环境变量的路径从 Microsoft 符号服务器下载这些符号文件。
set PATH=srv*c:\symbols*http://msdl.microsoft.com/download/symbols
运行 Vista 时,您应该在行尾使用 /M 开关。请注意符号是如何在名为 c:\symbols 的目录中本地缓存的。但是,什么是符号?当程序被构建时,编译过程会将人类可读的源代码翻译成机器的汇编语言,从而生成符号。这些代码通常用于构建目标文件,其中包含一个符号表,描述文件中所有具有外部链接的对象。符号通过程序员在源代码中给出的名称来引用正在运行的程序中的变量和函数。为了显示和解释这些名称,调试器需要有关程序中变量和函数类型的信息,以及可执行文件中的哪些指令对应于源文件中的哪些行。此类信息以符号表的形式存在,编译器和链接器在链接过程中将其包含在可执行文件中以构建该可执行文件。因此,下载的符号仅限于 Microsoft 代码。正如我们将看到的,第三方驱动程序将没有符号,并且还会使用省略堆栈帧指针的调用约定。此第三方驱动程序可能会调用操作系统函数,从而导致崩溃,但很可能是第三方驱动程序向该函数传递了一些错误数据。话虽如此,另一个强大的调试器是由 Mark Russinovich 编写的 livekd.exe。正如我们将看到的,他也是导致崩溃的工具的作者,目的是为了教育目的,讲解如何分析崩溃转储文件并将其知识付诸实践。
在我们讨论这些工具及其用法之前,我们必须首先了解,当系统崩溃时,通常是内核模式出了问题。在内核模式下运行的设备驱动程序或操作系统函数会遇到未处理的异常,例如内存访问违例,其示例可能是尝试写入只读页面,或尝试读取当前未映射因此无效的内存地址。简单来说,正在执行的线程尝试或确实写入了一个它不拥有的内存块,并破坏了该内存块的状态。
崩溃转储分析属于内存分析的范畴。内存分析的一个基本方面是,操作系统使用的数据的位置与其在内存转储中定位数据所需的物理位置不同。由于物理内存通常不足以同时容纳所有正在运行的进程,因此 Windows 操作系统必须模拟更大的内存空间。这就是为什么配置完整的内存转储不是很有用,因为用户模式代码和数据通常不用于崩溃转储分析。如果内核模式出了问题,那么配置内核转储文件将是分析系统崩溃的最佳选择。这些设置可以在控制面板中包含设备管理器和远程设置的 Applet 的“高级设置”选项卡中找到。
简要了解线程和进程
线程是执行上下文的单位。线程是调度的单位,包含执行状态:寄存器值、指令指针和堆栈指针。进程是一个容器,至少包含一个线程、一个句柄表、一个安全令牌和一个地址空间。线程共享私有地址空间,因此程序员负责同步这些线程之间地址空间中共享数据的访问。事实上,Windows 内存保护机制的一部分基于这样一个事实:当一个进程(其中的线程)执行时,该进程的地址空间会映射到微处理器的内存管理硬件。因此,一个进程无法看到另一个进程的地址空间,因为它不存在——它当前未加载到微处理器的内存管理硬件中。这并不意味着它无法访问另一个进程的地址空间。为了做到这一点,它必须遵循 Windows 安全原则,打开该进程,并使用特殊的 API 来获得访问该远程进程地址空间的权限。
Windows 内存管理器会创建一种平坦的虚拟地址空间的假象,而实际上,微处理器的硬件会将虚拟地址空间映射到物理地址。这种更大的内存空间模拟是通过为每个进程创建虚拟地址空间来实现的,该地址空间通过一系列数据结构转换为物理存储位置。主要的数据结构是页目录和页表。将虚拟地址空间映射到物理地址以页面(4KB 物理内存)的粒度完成。当用户模式应用程序需要将其代码和数据映射到虚拟地址空间时,该进程可能会向系统表示一个正在运行的程序的实例。但是,当应用程序需要将其代码和数据映射到虚拟地址空间时,实际的操作系统也需要映射自身,以及配置的设备驱动程序,以及存储在内核模式堆栈上的设备驱动程序使用的数据。进程使用的虚拟地址并不表示对象在内存中的实际物理位置。相反,系统为每个进程维护一个页映射,这是一个用于将虚拟地址转换为相应物理地址的内部数据结构。
内存保护的另一件事是,地址空间包含用户的地址空间以及一部分专门用于映射内核、驱动程序以及它们两者使用的数据的地址空间。如果像记事本这样的用户模式组件能够访问内核模式并从中读取数据甚至修改它,那将构成安全风险。因此,Windows 依赖于内存管理硬件的帮助,将代表内核地址空间的页面标记为系统页面。用户进程内存地址是分开的,因为所有内核模式组件都共享一个地址空间:用户线程无法访问内核内存。
Windows 运行的处理器上的内存管理硬件阻止用户模式下运行的任何东西访问标记为系统页面的页面。因此,为了让线程进行系统调用从而进入操作系统代码并访问内核内存,必须发生一个转换。当线程需要进行系统调用时,该线程会调用 DLL 中的一个函数,该函数执行一个特殊指令,该指令会安全地转换为这种提升的处理器访问模式。在 x86 架构上,这种提升的处理器访问模式称为 Ring 0。所以,内核模式代码在 Ring 0 中运行,用户模式代码在 Ring 3 中运行。线程每次进行系统调用时,都会在用户模式和内核模式之间不断地来回切换。当进行这种切换时,线程现在正在内核模式下执行,现在操作系统和驱动程序可以访问这种内核模式保护的内存了。
中断请求级别:IRQL
x86 中断控制器执行中断优先级级别,但 Windows 强制执行自己的中断优先级方案,称为中断请求级别(IRQL)。该方案实际上是 Windows 用于对其自身工作进行优先级排序的软件概念。它基本上是当时处理器上发生的事件的优先级。有几个 IRQL 通常与崩溃相关。一个是最低级别,称为 PASSIVE_LEVEL
,在此期间没有任何中断被屏蔽:没有软件或硬件中断被屏蔽。根据定义,当系统运行时,IRQL 处于 PASSIVE_LEVEL
。IRQL 只能在响应软件生成的中断或硬件生成的中断触发中断服务例程或延迟过程调用的执行而执行内核模式代码时才会被提升到更高级别。即使在运行内核模式代码时,系统也会尝试将 IRQL 保持在 PASSIVE_LEVEL
,因为它对中断系统的设备更具响应性,以便使其中断保持未屏蔽状态。下一个与系统崩溃相关的 IRQL 是 DISPATCH_LEVEL
。DISPATCH_LEVEL
是最高的软件中断级别,调度器操作被映射到此级别。当调度器在系统上运行时,它会将 IRQL 提高到 DISPATCH_LEVEL
。其他操作也可以将 IRQL 提高到 DISPATCH_LEVEL
,但当另一个操作将 IRQL 提高到 DISPATCH_LEVEL
时,调度器将被禁用。内核模式下运行的线程确保自己不会被该处理器上的另一个线程抢占的一种方法是提高中断级别到 DISPATCH_LEVEL
。这会关闭调度器,现在该线程可以一直运行直到完成其执行的操作。完成后,它会将中断级别降低到 PASSIVE_LEVEL
并重新启用调度器。在 DISPATCH_LEVEL
下关闭调度器的一个副作用是,在 DISPATCH_LEVEL
或更高级别上执行的驱动程序无法发生页面错误。它无法引用一个未存在的、标记为可分页的内存块,因为这样做会触发页面进入处理程序中的内存管理器,后者将被迫发出磁盘 I/O(硬故障)。它会将该线程挂起,直到 I/O 完成,直到引用的数据已从磁盘(映射文件或页面文件)加载。在将线程置于等待状态的过程中,它基本上是在调用调度器,并告知调度器必须在该 CPU 上找到另一个线程来运行。但是,在 DISPATCH_LEVEL
下,调度器是关闭的。这是 Windows 内部同步架构的违规行为,因此被系统视为非法操作。
堆栈与堆的对比
堆栈是一种抽象数据结构,从底部到顶部递归读取。堆是动态分配的内存量,用于构建程序,当其数据结构的大小无法静态确定时。也就是说,数据结构会根据程序对堆分配的需求而增长和收缩。堆从较低的内存地址增长到较高的地址,这与堆栈的增长方式相反。堆和堆栈不可能相互冲突。应用程序程序的“数据”部分在堆上存储全局变量和静态变量。“BSS”部分在堆上存储全局已初始化变量。堆栈代表硬件记录的数据以及调用操作系统函数的设备驱动程序记录的数据,这些数据允许嵌套函数调用。因此,当设备驱动程序调用操作系统时,存储在堆栈中的信息用于将参数传递给操作系统并返回调用它的函数。因此,堆栈存储传递的参数、返回地址和局部变量(处理请求的函数特有的信息)。在 Windows 中,每个线程有两个堆栈:一个用于线程的用户模式执行,位于用户地址空间中,因此任何进程中的线程都可以访问。当线程调用系统调用进入内核模式时,该线程现在从其内核模式堆栈运行。内核模式堆栈位于内核地址,因此用户模式下运行的线程无法访问。
当函数 1 调用函数 2 时,返回地址被保存在硬件中。这就是硬件保存的内容,以便当函数 2 返回时,硬件知道在哪里恢复函数 1 中的执行。函数 2 被调用后,开始设置其帧指针。它将其保存在堆栈上;它可能使用一些局部缓冲区,这些缓冲区可以在执行期间临时使用。这些局部缓冲区分配在堆栈上,被视为局部变量 1 和局部变量 2。函数 2 调用函数 3 时,以与接收参数相同的方式将参数传递给堆栈。堆栈帧指针清晰地标出了对应于嵌套中每个函数的区域。
上述场景说明了一个非常简单的调用约定,供调试分析引擎分析。然而,其他调用约定是不同的。一旦调用了内核本身的调用约定,帧指针就会被省略:没有帧指针被推送到堆栈上,这使得调试器中的分析引擎难以分析,除非分析有符号。分析引擎拥有所有 Microsoft 代码的符号,但如果您在堆栈上有第三方驱动程序并且它们使用的是不使用帧指针的调用约定,那么分析引擎就很难弄清楚堆栈帧的位置。
当您在 Windows 调试器中打开崩溃转储文件时,它会执行基本分析,并大致猜测出罪魁祸首。当您打开调试器时,它会在内部调用一个您可以显式使用的命令,称为 !analyze (!analyze -v load)。!Analyze 显示停止代码和参数,并猜测出错的驱动程序。!Analyze 主要查看堆栈。有时,Bug 检查参数会指向导致错误的指令的指令指针 (cs:ip)。使用已加载模块列表(!lm 命令),它可以确定该指令属于哪个驱动程序。在其他情况下,!analyze 使用启发式方法遍历堆栈,确定崩溃时正在发生什么,然后执行一种性能分析。如果崩溃发生在操作系统内部,但触发崩溃的操作系统函数的调用者是第三方驱动程序,调试器可能会猜测并声明崩溃可能是由...引起的,然后指向第三方驱动程序,即使崩溃本身可能由 Windows 操作系统函数引起。但是,被调用的函数极有可能传递了一些错误数据(指向损坏数据结构的指针,或某个无效参数)。如果调试器声明崩溃是由,例如,ntokrnl.exe 或某个文件系统驱动程序引起的,请不要相信它。Microsoft 收集了大量与崩溃相关的数据,并有数据证明至少 80% 的崩溃是由第三方设备驱动程序引起的。这意味着您需要进行更深入的挖掘。
Mark Russinovich,现受雇于 Microsoft,编写了一个名为“Notmyfault.exe”的应用程序,该应用程序是免费下载的 zip 文件。该应用程序的目的是纯粹用于教育目的,以帮助用户和类似的人学习如何分析和解释崩溃转储文件。当操作系统检测到超出任何合法范围的错误时,它会调用一个名为 KernelBugEx
的操作系统函数(在 Windows DDK 中有记录)。该函数接收停止代码和四个参数,这些参数根据停止代码进行解释。KernelBugEx
会屏蔽显示器上所有处理器的所有中断,然后将显示模式切换到低分辨率 VGA 图形模式,然后让系统绘制蓝屏。下面将显示的示例描述了蓝屏的一些内容,同时开始将物理内存转储到磁盘。您可以使用 Notmyfault 工具来生成崩溃。您只需选择会导致系统崩溃的操作系统场景类型
- 高 IRQL(内核模式)
- 缓冲区溢出
- 代码覆盖
- 堆栈损坏
- 高 IRQL(用户模式)
- 死锁
- 挂起
选择“高 IRQL(内核模式)”崩溃并点击“do bug”将导致驱动程序分配一个可分页池页面,将 IRQL 提高到 DPC(延迟过程调用)DISPATCH_LEVEL
以上,然后触摸该已释放页面。如果崩溃没有立即发生,进程将通过读取超出页面末尾的内存继续,直到通过访问无效页面导致崩溃。更准确地说,驱动程序分配一个可分页池缓冲区,释放缓冲区,将 IRQL 提高到等于或大于 DISPATCH_LEVEL
,然后触摸该缓冲区及其后续页面。在 IRQL DISPATCH_LEVEL
或以上级别访问的内存页面必须物理存在。崩溃发生,蓝屏显示 DRIVER_IRQL_NOT_LESS_THAT_OR_EQUAL
并开始将内存转储到页面文件。简单来说,当系统重启时,smss.exe(会话管理器)会查看页面文件以确定是否存在崩溃转储文件。它会依次调用一个函数,让另一个进程复制崩溃文件并将其写入 %systemroot%。因此,在 WinDbg 工具中配置了我们的符号后,我们使用 Notmyfault 工具,但理想情况下是在虚拟环境中。本文作者强烈建议研究崩溃的学生下载 VMWare Workstation for Windows 的试用版,在那里可以安装另一个 Windows 安装来创建一个人工计算环境。这个虚拟环境将是一个软件层,充当抽象,提供一个与您的操作系统完全分离的计算环境。即使在虚拟环境中的间谍软件也无法访问外部计算机。
蓝屏出现后,系统会重启,我们打开应该位于 Windows 目录中的崩溃文件:c:\Windows\MEMORY.dmp。在 WinDbg 工具栏的“文件”菜单中选择“打开崩溃文件”选项时,打开此文件
Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Windows\MEMORY.DMP]
Kernel Summary Dump File: Only kernel address space is available
Symbol search path is: c:\symbols;srv*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows Kernel Version 6001 (Service Pack 1) MP (2 procs) Free x86 compatible
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 6001.18145.x86fre.vistasp1_gdr.080917-1612
Kernel base = 0x81a4c000 PsLoadedModuleList = 0x81b63c70
Debug session time: Sat Nov 1 01:08:53.731 2008 (GMT-4)
System Uptime: 0 days 4:07:49.287
Loading Kernel Symbols
.......................................................................................
Loading User Symbols
PEB is paged out (Peb.Ldr = 7ffd500c). Type ".hh dbgerr001" for details
Loading unloaded module list
BugCheck D1, {bec0c5e8, 1c, 0, b80493dd}
使用 !analyze -v 获取详细的调试信息。
请注意,错误消息显示无法加载符号信息:Microsoft 没有编写此驱动程序,因为 myfault.sys 是第三方驱动程序。
*** ERROR: Module load completed but symbols could not be loaded for myfault.sys
Page 52f17 not present in the dump file. Type ".hh dbgerr004" for details
PEB is paged out (Peb.Ldr = 7ffd500c). Type ".hh dbgerr001" for details
PEB is paged out (Peb.Ldr = 7ffd500c). Type ".hh dbgerr001" for details
Probably caused by : myfault.sys ( myfault+3dd )
Followup: MachineOwner
现在,我们显式地以详细模式发出 !analyze 调试器命令
0: kd> !analyze –v
IRQL_NOT_LESS_THAN_OR_EQUAL
是一个常见的停止代码,表示尝试引用一个不存在的内存页面
DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: bec0c5e8, memory referenced
Arg2: 0000001c, IRQL
Arg3: 00000000, value 0 = read operation, 1 = write operation
Arg4: b80493dd, address which referenced memory
Debugging Details:
------------------
Page 52f17 not present in the dump file. Type ".hh dbgerr004" for details
PEB is paged out (Peb.Ldr = 7ffd500c). Type ".hh dbgerr001" for details
PEB is paged out (Peb.Ldr = 7ffd500c). Type ".hh dbgerr001" for details
READ_ADDRESS: bec0c5e8 Paged pool
CURRENT_IRQL: 1c
FAULTING_IP:
myfault+3dd
b80493dd 8b06 mov eax,dword ptr [esi]
DEFAULT_BUCKET_ID: VISTA_DRIVER_FAULT
BUGCHECK_STR: 0xD1
PROCESS_NAME: NotMyfault.exe
TRAP_FRAME: aaf75b78 -- (.trap 0xffffffffaaf75b78)
ErrCode = 00000000
eax=bec0b5e8 ebx=83cf7138 ecx=b74e421c edx=83625088 esi=bec0c5e8 edi=00000000
eip=b80493dd esp=aaf75bec ebp=aaf75c44 iopl=0 nv up ei ng nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010286
myfault+0x3dd:
b80493dd 8b06 mov eax,dword ptr [esi] ds:0023:bec0c5e8=????????
Resetting default scope
请注意,问号表示无法访问的内存,并且出错的 IP(指令指针)指向与导致崩溃的模块相对应的地址。
LAST_CONTROL_TRANSFER: from b80493dd to 81aa6d24
STACK_TEXT:
aaf75b78 b80493dd badb0d00 83625088 00000003 nt!KiTrap0E+0x2ac
WARNING: Stack unwind information not available. Following frames may be wrong.
aaf75c44 81c98615 840bebd8 83cf7120 83cf7190 myfault+0x3dd
aaf75c64 81c98dba 83decf08 840bebd8 00000000 nt!IopSynchronousServiceTail+0x1d9
aaf75d00 81c82a8d 83decf08 83cf7120 00000000 nt!IopXxxControlFile+0x6b7
aaf75d34 81aa3a1a 00000080 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
aaf75d34 77bf9a94 00000080 00000000 00000000 nt!KiFastCallEntry+0x12a
0012f9f4 00000000 00000000 00000000 00000000 0x77bf9a94
请注意,堆栈底部包含指令 0x77bf9a94,该指令转换为内核模式。上面的指令 nt!KiFastCallEntry+0x12a 调用 nt!DeviceIoControlFile+0x2a(与系统中的 IOCTL 相关),该指令调用 nt!IopSynchrounousServiceTail+0x6b7,最后最终到达设备驱动程序 myfault.sys 引用无效内存。堆栈信息显示,在用户模式下运行可执行文件会导致线程进行系统函数调用后进入内核模式的底部指令。这会调用 kernel32.dll 中的 DeviceIoControl
函数,依此类推。“nt”模块(出现在系统函数之前)代表 Ntsokrnl.exe(内核)。因此,当调试器执行分析时,它会看到转换为内核模式的指令,然后是 nt,一直到 myfault。它识别“nt”代表内核映像,并对自己说,那是我们的,所以继续递归跟踪。
STACK_COMMAND: kb
FOLLOWUP_IP:
myfault+3dd
b80493dd 8b06 mov eax,dword ptr [esi]
SYMBOL_STACK_INDEX: 1
SYMBOL_NAME: myfault+3dd
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: myfault
IMAGE_NAME: myfault.sys
DEBUG_FLR_IMAGE_TIMESTAMP: 453143ee
FAILURE_BUCKET_ID: 0xD1_myfault+3dd
BUCKET_ID: 0xD1_myfault+3dd
Followup: MachineOwner
如果驱动程序不像 myfualt.sys 那样熟悉(或明显),则使用 lm(列表模块)命令查看驱动程序的版本信息。添加 k(内核模块)和 v(详细)选项,以及 m(匹配)选项,后面跟驱动程序的名称和一个通配符
0: kd> lm kv m myfault*
start end module name
b8049000 b8049ec0 myfault (no symbols)
Loaded symbol image file: myfault.sys
Image path: \??\C:\Windows\system32\drivers\myfault.sys
Image name: myfault.sys
Timestamp: Sat Oct 14 16:09:18 2006 (453143EE)
CheckSum: 0000295E
ImageSize: 00000EC0
File version: 2.0.0.0
Product version: 2.0.0.0
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 3.7 Driver
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Sysinternals
ProductName: Sysinternals Myfault
InternalName: myfault.sys
OriginalFilename: myfault.sys
ProductVersion: 2.0
FileVersion: 2.0
FileDescription: Crash Test Driver
LegalCopyright: Copyright (C) M. Russinovich 2002-2004
尽管上面的示例是一个使用测试崩溃驱动程序来生成崩溃和崩溃文件的基本示例,但涉及的技术通常可以在其他情况下使用。如果您的系统崩溃,并且您看到一个设备驱动程序是罪魁祸首,并且崩溃是规律性的,请访问该设备驱动程序的网站。您当前的版本可能已过时,驱动程序已更新多次。下载该驱动程序,看看系统是否仍然崩溃。也许,一些第三方设备驱动程序公司也可以提供用于调试信息的符号文件。
参考文献
- Mark Russinovich 和 David A. Solomon 著,《Windows Internals 4th Edition》。
- Mark Russinovich 和 David A. Solomon 著,《Sysinternals Video Library》。