基于堆栈的错误注入

注意启用此功能的说明仅适用于Windows 8的 WDK。 对于Windows 8.1,此功能已集成到驱动程序验证程序中。 在运行 Windows 8.1 的计算机上,使用系统资源不足模拟选项。

“基于堆栈的故障注入”选项在内核模式驱动程序中注入资源故障。 此选项结合使用了特殊驱动程序 KmAutoFail.sys 和驱动程序验证程序来侵入驱动程序错误处理路径。 测试这些路径历来非常困难。 “基于堆栈的故障注入”选项以可预测的方式注入资源故障,使发现的问题可重现。 由于错误路径易于重现,因此也可以轻松地验证这些问题的修复。

为了帮助你确定错误的根本原因,我们提供了一个调试器扩展,可以确切地告诉你注入了哪些故障以及按什么顺序注入了故障。

在特定驱动程序上启用基于堆栈的故障注入选项时,它会截获该驱动程序对内核的一些调用,并Ndis.sys。 基于堆栈的故障注入将查看调用堆栈,具体来说,是调用堆栈的一部分,该部分来自启用它的驱动程序。 如果这是它第一次看到该堆栈,它将根据该调用的语义使调用失败。 否则,如果之前已看到该调用,它将通过未更改的传递。 基于堆栈的故障注入包含用于处理驱动程序可以多次加载和卸载这一事实的逻辑。 它将识别到调用堆栈是相同的,即使驱动程序重新加载到不同的内存位置。

激活此选项

将驱动程序 部署到测试计算机时,可以为一个或多个驱动程序激活基于堆栈的故障注入功能。 配置 驱动程序包项目的驱动程序验证程序属性时,可以选择“基于堆栈的故障注入”选项。 必须重新启动计算机才能激活或停用“基于堆栈的故障注入”选项。 还可以运行测试实用工具,以在测试计算机上启用驱动程序验证程序和此功能。

重要 在测试计算机上激活基于堆栈的故障注入时,请确保不要同时选择 “资源不足模拟”。

  • “使用驱动程序验证程序属性”页

    1. 打开驱动程序包的属性页。 右键单击解决方案资源管理器中的驱动程序包项目,然后选择“属性”。
    2. 在驱动程序包的属性页中,依次单击“配置属性”、“驱动程序安装”和“驱动程序验证程序”。
    3. 选择“ 启用驱动程序验证程序”。 在测试计算机上启用驱动程序验证程序时,可以选择为计算机上的所有驱动程序、仅驱动程序项目或指定驱动程序的列表启用驱动程序验证程序。
    4. “基于堆栈的故障注入器”下,选择“ (检查) 基于堆栈的故障注入”。
    5. 单击“应用”“确定”
    6. 有关详细信息 ,请参阅将驱动程序部署到测试计算机 。 测试计算机必须重新启动才能激活此选项。
  • 使用启用和禁用驱动程序验证程序测试

    1. 还可以通过运行实用工具测试来启用驱动程序验证程序。 按照 如何使用 Visual Studio 在运行时测试驱动程序中所述的说明进行操作。 在“ 所有测试\驱动程序验证程序 ”测试类别下,选择“ 启用驱动程序验证程序 (可能需要重新启动) 禁用驱动程序验证程序 (可能需要重新启动) 测试。

    2. 在“驱动程序测试组”窗口中单击 “启用驱动程序验证程序 (可能需要重新启动) 测试”的名称,选择 驱动程序验证程序 选项。

    3. 选择“ (检查) 基于堆栈的故障注入”。

    4. 将这些测试添加到测试组后,可以保存测试组。 若要启用基于堆栈的故障注入,请在配置为进行测试的计算机上运行 启用驱动程序验证程序 (可能需要重新启动) 测试。

      若要停用驱动程序验证程序,请运行 禁用驱动程序验证程序, (可能需要重新启动) 测试。

使用“基于堆栈的故障注入”选项

使用基于堆栈的故障注入进行测试时的一个重要注意事项是,它发现的大多数 bug 都会导致 bug 检查。 如果你的驱动程序是启动加载的驱动程序,这可能有点痛苦。 因此,如果禁用了驱动程序验证程序,将自动禁用基于堆栈的故障注入。 这意味着,可以通过使用 命令 !verifier –disable 禁用驱动程序验证程序,在启动时从调试器禁用基于堆栈的故障注入。

如果可能,对于使用基于堆栈的故障注入进行的初始测试,请设置驱动程序,使其不在启动时加载。 然后,可以运行一些简单的加载和卸载测试。 基于堆栈的故障注入发现的许多 bug 都发生在初始化或清理过程中。 重复加载和卸载驱动程序是查找这些驱动程序的好方法。

完成使负载卸载测试成功所需的任何修复后,可以继续执行基于 IOCTL 的测试、完整功能测试和最终压力测试。 通常,如果遵循此测试进度,则不会在压力测试期间发现许多新问题,因为在此之前已执行大多数代码路径。

使用基于堆栈的故障注入 (SBFI) 调试器扩展

在基于堆栈的故障注入中发现的大多数问题都会导致 bug 检查。 为了帮助确定这些代码 bug 的原因,WDK 提供了基于堆栈的故障注入调试器扩展和必要的符号。 安装过程将在调试器系统上安装这两个程序。 默认位置为 C:\Program Files (x86) \Windows Kits\8.0\Debuggers\<arch>

运行调试器扩展

  • 在调试器命令提示符下,键入以下命令: <path>\kmautofaildbg.dll.autofail。 例如,假设调试器扩展安装在 c:\dbgext,并且 kmautofail.pdb 位于符号路径中,则输入以下命令:

    !c:\dbgext\kmautofaildbg.dll.autofail
    

这会将信息转储到调试器,显示最近注入的故障的调用堆栈。 每个条目如下所示,取自实际测试运行。 在以下示例中,基于堆栈的故障注入在 Mydriver.sys

Sequence: 2, Test Number: 0, Process ID: 0, Thread ID: 0
                 IRQ Level: 2, HASH: 0xea98a56083aae93c
 0xfffff8800129ed83 kmautofail!ShimHookExAllocatePoolWithTag+0x37
 0xfffff88003c77566 mydriver!AddDestination+0x66
 0xfffff88003c5eeb2 mydriver!ProcessPacketDestination+0x82
 0xfffff88003c7db82 mydriver!ProcessPacketSource+0x8b2
 0xfffff88003c5d0d8 mydriver!ForwardPackets+0xb8
 0xfffff88003c81102 mydriver!RoutePackets+0x142
 0xfffff88003c83826 mydriver!RouteNetBufferLists+0x306
 0xfffff88003c59a76 mydriver!DeviceSendPackets+0x156
 0xfffff88003c59754 mydriver!ProcessingComplete+0x4a4
 0xfffff88001b69b81 systemdriver2!ProcessEvent+0x1a1
 0xfffff88001b3edc4 systemdriver1!CallChildDriver+0x20
 0xfffff88001b3fc0a systemdriver1!ProcessEvent+0x3e
 0xfffff800c3ea6eb9 nt!KiRetireDpcList+0x209
 0xfffff800c3ea869a nt!KiIdleLoop+0x5a

在输出顶部,序列号对注入的错误数进行计数。 此示例显示了在此测试运行期间注入的第二个错误。 进程 ID 为 0,因此这是系统进程。 IRQL 为 2,因此在调度级别调用。

在堆栈中,KmAutoFail 是基于堆栈的故障注入驱动程序。 KmAutoFail 函数名称指示截获并注入了来自 Mydriver.sys 的函数调用。 此处,失败的函数是 ExAllocatePoolWithTag。 KmAutoFail 中截获Ntoskrnl.sys或Ndis.sys调用的所有函数都使用此命名约定。 接下来,我们看到调用堆栈,其中驱动程序正在测试 (Mydriver.sys) 。 这是调用堆栈中用于确定堆栈的唯一性的部分。 因此,调试器扩展转储的每个条目在调用堆栈的这一部分中都是唯一的。 调用堆栈的其余部分指示调用驱动程序的人员。 此main重要性在于,驱动程序是通过 IOCTL) 从用户模式 (调用的,还是从内核模式驱动程序调用的。

请注意,如果驱动程序已从其 DriverEntry 例程返回失败,则重新加载尝试通常发生在不同的内存位置。 在这种情况下,来自早期位置的调用堆栈可能包含“垃圾”,而不是驱动程序的堆栈信息。 但这不是问题:它告知驱动程序已正确处理注入的错误。

下一个条目显示通过 IOCTL 从用户模式调用驱动程序。 请注意进程 ID 和 IRQ 级别。 由于 Mydriver.sys 是 NDIS 筛选器驱动程序,因此 IOCTL 通过Ndis.sys。 请注意,nt!NtDeviceIoControlFile 位于堆栈上。 在驱动程序上运行的任何使用 IOCTL 的测试都将通过此函数。

Sequence: 5, Test Number: 0, Process ID: 2052, Thread ID: 4588
                 IRQ Level: 0, HASH: 0xecd4650e9c25ee4
 0xfffff8800129ed83 kmautofail!ShimHookExAllocatePoolWithTag+0x37
 0xfffff88003c6fb39 mydriver!SendMultipleOids+0x41
 0xfffff88003c7157b mydriver!PvtDisconnect+0x437
 0xfffff88003c71069 mydriver!NicDisconnect+0xd9
 0xfffff88003ca3538 mydriver!NicControl+0x10c
 0xfffff88003c99625 mydriver!DeviceControl+0x4c5
 0xfffff88001559d93 NDIS!ndisDummyIrpHandler+0x73
 0xfffff88001559339 NDIS!ndisDeviceControlIrpHandler+0xc9
 0xfffff800c445cc96 nt!IovCallDriver+0x3e6
 0xfffff800c42735ae nt!IopXxxControlFile+0x7cc
 0xfffff800c4274836 nt!NtDeviceIoControlFile+0x56
 0xfffff800c3e74753 nt!KiSystemServiceCopyEnd+0x13

分析基于堆栈的故障注入的结果

你正在驱动程序上运行测试,但突然遇到了问题。 这很可能是检查的 bug,但也可能是因为计算机变得无响应。 如何找到原因? 假设它是检查的 bug,请先使用上述扩展查找注入故障的列表,然后使用调试器命令:!analyze –v

最常见的 bug 检查是由未检查分配是否成功导致的。 在这种情况下,bug 检查分析中的堆栈可能与上次注入的故障几乎相同。 在分配失败后 (通常) 的下一行之后的某个时间点,驱动程序将访问 null 指针。 这种类型的 bug 很容易修复。 有时,失败的分配是列表上的一两个,但此类型仍然很容易找到和修复。

第二个最常见的 bug 检查发生在清理过程中。 在这种情况下,驱动程序可能检测到分配失败,并跳转到清理;但在清理过程中,驱动程序未检查指针,并再次访问 null 指针。 一个密切相关的情况是,清理可以调用两次。 如果清理在释放结构后未将指向结构的指针设置为 null,则第二次调用清理函数时,它将尝试第二次释放结构,从而导致检查 bug。

导致计算机无响应的错误更难诊断,但调试这些错误的过程类似。 这些错误通常是由引用计数或旋转锁问题引起的。 幸运的是, 驱动程序验证程序 会在导致问题之前捕获许多旋转锁问题。 在这些情况下,请中断调试器并使用调试器扩展来转储已由基于堆栈的故障注入注入的错误列表。 快速查看有关最新故障的代码可能会显示一个引用计数,该计数是在失败之前获取的,但在失败之后不会释放。 如果没有,请在驱动程序中查找正在等待旋转锁的线程,或者查找任何明显错误的引用计数。