BSOD有多可靠?

Lat*_*aji 2 bsod

我在超级用户网站上搜索了这个问题,但没有人发布它,所以这是我的问题:BSOD是否给了我们 100% 准确的错误?

Jam*_*han 7

BSOD 代码正是传递给KeBugCheckEx的参数。第一个这样的参数称为“错误检查代码”。它被转换为您在 BSOD 上看到的消息。例如,错误检查代码 0x50 是 PAGE_FAULT_IN_NONPAGED_AREA,0x44 是 MULTIPLE_IRP_COMPLETE_REQUESTS。

其他四个参数的含义特定于特定的错误检查代码。例如,在 PAGE_FAULT_IN_NONPAGED_AREA 的情况下,其他参数之一将指示出现故障的虚拟地址。对于 MULTIPLE_IRP_COMPLETE_REQUESTS,其中一个参数指示 IRP(I/O 请求数据包)的地址。

然而:我们在内核模式调试中经常使用的一句话是“受害者并不总是罪魁祸首”。即导致崩溃的代码并不总是罪魁祸首(创建导致崩溃的情况的代码)。BSOD 只能识别受害者。即使是小型转储通常也没有足够的信息来超越它。

有两大类 BSOD 代码:表示“断言失败”的代码,以及由内核模式中引发的未处理或无法处理的异常引起的代码。(调试器文档没有明确区分这些,尽管您通常可以从每个错误检查代码的描述中弄清楚。)

“断言失败”类似于 C 编程中常用的“断言”宏(尽管 Windows 在内核模式下不使用“断言”宏)。这是对“不应该发生”条件的“内联”测试。例如,NO_MORE_IRP_STACK_LOCATIONS 表示某人创建的 IRP 的“堆栈位置”太少(注意:这与用于返回地址、局部变量等的“堆栈”不同)对于存在的分层驱动程序的数量对于给定设备(或“DevNode”)。

“异常”是作为执行指令的副作用而发生的事情。可以“处理”一些异常。例如,页面错误就是一个异常。在大多数情况下(当您处于用户模式或 IRQL < 2 的内核模式,并且被引用的虚拟地址在当前访问模式下被正确定义和访问时)操作系统的分页器可以处理页面错误。

但是,如果不满足这些条件中的任何一个,则无法解决页面错误。在用户模式下,这通常会导致进程崩溃。在内核模式下,它将导致蓝屏死机,根据确切的循环,有几个错误检查代码中的任何一个。常见的有:

  • IRQL_NOT_LESS_OR_EQUAL(页面错误发生在 IRQL 2 或以上)
  • DRIVER_IRQL_NOT_LESS_OR_EQUAL(类似,但 KeBugCheckEx 发现页面错误是在驱动程序内部引发的,并更改了错误检查代码以表明这一事实)
  • KMODE_EXCEPTION_NOT_HANDLED(它处于 IRQL 0 或 1,但由于其他原因无法解决)
  • SYSTEM_SERVICE_EXCEPTION(也在 IRQL 0 或 1 但在从用户模式调用的内核模式例程中(与“服务”进程无关))
  • SYSTEM_THREAD_EXCEPTION_NOT_HANDLED(也在 IRQL 0 或 1 但问题发生在“系统”进程中的线程中)...等。

现在,问题来了:

错误检查代码和其他信息始终准确地指示检测到问题时的情况。但这并不一定——事实上它通常不会——表明问题的真正原因

例如,内核模式中无法处理的页面错误的最常见原因很简单,就是被引用的地址不正确。例如,假设我调用 ExAllocatePool(k-mode 等价物,大致相当于 malloc)并且它不能分配我想要的。在这种情况下,它将返回给我的不是已分配块的地址,而是零——“空指针”。现在假设我将零存储在指针应该所在的位置。后来,操作系统中而不是我的代码中的其他一些代码尝试使用该指针。蓝屏!BSOD 和 minidump 上的明显信息将指向试图使用的代码指针。但真正的罪魁祸首是我的代码,它未能检查 ExAllocatePool 的零返回并将其存储为“指针”。但到那时我的代码可能早就不复存在了,即不再执行。

另一个例子:假设我成功分配了我需要的池(堆),但是当我分配了 120 个字节时,我的代码错误地写入了超出返回给我的地址的 140 个字节。我刚刚破坏了下一个池块的池元数据,如果该块正在使用中,我也破坏了属于该块所有者的数据。这在一段时间内可能不会导致问题。它不会立即对我造成问题!但最终,当拥有该块的人尝试使用他们的数据时,他们会遇到问题(可能是页面错误,可能是很多事情)。或者,如果释放或分配池的请求碰巧遇到损坏的元数据,则可能会引发某种异常,从而导致 BSOD。而且,我,罪魁祸首,很可能不会出现在任何地方。

在调试这些时,您必须弄清楚坏数据(通常是指针)的来源,而不仅仅是试图使用它的人。

类似地,NO_MORE_IRP_STACK_LOCATIONS 错误检查从来都不是 IoCompleteRequest 中检测它的代码的错误。可能是某些驱动程序的错误设置了驱动程序分层不正确。简单地看一下小型转储(当然,人们一直在发布输出的“谁崩溃”的东西)会很快得出结论“问题出在 ntoskrnl 中”,因为在此路径中调用 KeBugCheckEx 的是 IoCompleteRequest,而 IoCompleteRequest 在 ntoskrnl 中。但在这种情况下真正的问题总是一些驱动程序不正确地设置设备对象的分层(或者它设置正确,但后来损坏了)。在小型转储文件中,哪个驱动程序代码做了不法行为可能不会很明显。

有时可以从内核转储中找出哪个驱动程序是真正的罪魁祸首,但 BSOD 几乎永远不会告诉您,并且小型转储通常没有足够的信息来告诉您。

在少数情况下,BSOD 上的错误检查代码和其他信息似乎与实际问题相差甚远。例如,UNEXPECTED_KERNEL_MODE_TRAP 曾经是我们经常看到的东西(随着 BSOD 的发展......特别是如果你使用的是我不会在这里识别的特定声卡;产品甚至它使用的芯片组已经过时了,所以它现在无关紧要),参数表示“双重故障”。其中许多实际上是由使用过多内核堆栈空间的代码引起的。“UNEXPECTED_KERNEL_MODE_TRAP”甚至“双重错误”都无法告诉您这一点。(一段时间后,调试器文档已更新,其中包含一个建议,即内核堆栈溢出可能导致“双重故障”。)

有关所有可能的错误检查代码的详细信息,请参阅 WinDbg 附带的帮助,“错误检查代码参考”部分。如果您还不熟悉由 Solomon、Russinovich 和 Ionescu 编写的Windows Internals 中的材料,您可能需要参考该材料以了解许多描述。有关“为什么操作系统不能修复并继续运行”而不是崩溃的更多解释,请参阅我的答案here