编写 Bug 检查原因回调例程

驱动程序可以选择性地提供 KBUGCHECK_REASON_CALLBACK_ROUTINE 回调函数,系统在写入故障转储文件后调用该函数。

注意

本文介绍 bug 检查原因回调例程,而不是KBUGCHECK_CALLBACK_ROUTINE回调函数。

在此回调中,驱动程序可以:

  • 将特定于驱动程序的数据添加到故障转储文件

  • 将设备重置为已知状态

使用以下例程注册和删除回调:

此回调类型是重载的,其行为会根据注册时提供的 KBUGCHECK_CALLBACK_REASON 常量值而更改。 本文介绍不同的使用方案。

有关 bug 检查数据的常规信息,请参阅读取 Bug 检查回调数据

Bug 检查回调例程限制

bug 检查回调例程在 IRQL = HIGH_LEVEL 处执行,这对其可执行的操作施加了严格的限制。

bug 检查回调例程无法:

  • 分配内存

  • 访问可分页内存

  • 使用任何同步机制

  • 调用必须在 IRQL = DISPATCH_LEVEL 或更低位置执行的任何例程

Bug 检查回调例程保证运行而不会中断,因此无需同步。 (如果 bug 检查例程尝试使用任何同步机制获取锁,系统会死锁。) 请记住,在检查 bug 时,数据结构或列表可能处于不一致状态,因此在访问受锁保护的数据结构时应小心。 例如,在浏览列表时,应添加上限检查,并验证链接是否指向有效内存,以防有循环列表或链接指向无效地址。

Bug 检查回调例程可以使用 MmIsAddressValid 来检查访问地址是否会导致页面错误。 由于例程在没有中断的情况下运行,并且其他核心被冻结,这满足了该函数的同步要求。 在 Bug 检查回调中延迟它们之前,应始终使用 MmIsAddressValid 检查可能分页或无效的内核地址进行检查,因为页面错误将导致双重错误,并可能阻止写入转储。

驱动程序的 bug 检查回调例程可以安全地使用 READ_PORT_XXXREAD_REGISTER_XXXWRITE_PORT_XXXWRITE_REGISTER_XXX 例程与驱动程序的设备通信。 (有关这些例程的信息,请参阅 硬件抽象层例程。)

实现 KbCallbackAddPages 回调例程

内核模式驱动程序可以实现 KbCallbackAddPages 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以在 bug 检查发生时向故障转储文件添加一页或多页数据。 若要向操作系统注册此例程,驱动程序会调用 KeRegisterBugCheckReasonCallback 例程。 在驱动程序卸载之前,它必须调用 KeDeregisterBugCheckReasonCallback 例程来删除注册。

从 Windows 8 开始,在内核内存转储完整内存转储期间调用已注册的 KbCallbackAddPages 例程。 在早期版本的 Windows 中,注册的 KbCallbackAddPages 例程在内核内存转储期间调用,但不在完整内存转储期间调用。 默认情况下,内核内存转储仅包含发生 bug 检查时 Windows 内核正在使用的物理页,而完整的内存转储则包含 Windows 使用的所有物理内存。 默认情况下,完整内存转储不包括平台固件使用的物理内存。

KbCallbackAddPages 例程可以提供特定于驱动程序的数据,以添加到转储文件。 例如,对于内核内存转储,此附加数据可以包括未映射到虚拟内存中的系统地址范围但包含有助于调试驱动程序的信息的物理页。 KbCallbackAddPages 例程可能会将未映射或映射到虚拟内存中用户模式地址的任何驱动程序拥有的物理页面添加到转储文件。

当出现 bug 检查时,操作系统会调用所有已注册的 KbCallbackAddPages 例程,以轮询驱动程序以查找要添加到故障转储文件中的数据。 每次调用都会将一页或多页连续数据添加到故障转储文件。 KbCallbackAddPages 例程可以为起始页提供虚拟地址或物理地址。 如果在调用期间提供了多个页面,则这些页面在虚拟内存或物理内存中是连续的,具体取决于起始地址是虚拟地址还是物理地址。 若要提供不连续的页面, KbCallbackAddPages 例程可以在 KBUGCHECK_ADD_PAGES 结构中设置一个标志,以指示它具有其他数据,并且必须再次调用。

与向次要故障转储区域追加数据的 KbCallbackSecondaryDumpData 例程不同, KbCallbackAddPages 例程会将数据页添加到主要故障转储区域。 在调试期间,主要故障转储数据比辅助故障转储数据更易于访问。

操作系统填充 ReasonSpecificData 指向的 KBUGCHECK_ADD_PAGES 结构的 BugCheckCode 成员。 KbCallbackAddPages 例程必须设置此结构的标志地址计数成员的值。

在首次调用 KbCallbackAddPages 之前,操作系统会将 Context 初始化为 NULL。 如果 多次调用 KbCallbackAddPages 例程,操作系统将保留回调例程在上一次调用中写入 上下文 成员的值。

KbCallbackAddPages 例程在可以执行的操作方面非常有限。 有关详细信息,请参阅 Bug 检查回调例程限制

实现 KbCallbackDumpIo 回调例程

内核模式驱动程序可以实现 KbCallbackDumpIo 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以在每次将数据写入故障转储文件时执行工作。 系统会在 ReasonSpecificData 参数中传递指向 KBUGCHECK_DUMP_IO 结构的指针。 Buffer 成员指向当前数据,BufferLength 成员指定其长度。 Type 成员指示当前正在写入的数据类型,例如转储文件头信息、内存状态或驱动程序提供的数据。 有关可能的信息类型的说明,请参阅 KBUGCHECK_DUMP_IO_TYPE 枚举。

系统可以按顺序或无序写入故障转储文件。 如果系统按顺序写入故障转储文件,则 ReasonSpecificDataOffset 成员为 -1;否则,Offset 设置为故障转储文件中的当前偏移量(以字节为单位)。

当系统按顺序写入文件时, 在编写类型 = KbDumpIoHeader () 的标头信息时,它会调用每个 KbCallbackDumpIo 例程一次或多次,在编写故障转储文件的main正文 (Type = KbDumpIoBody) 时调用一次或多次, (Type = KbDumpIoSecondaryDumpData) 编写辅助转储数据时调用一次或多次。 系统完成编写故障转储文件后,会调用 Buffer = NULLBufferLength = 0 和 Type = KbDumpIoComplete 的回调。

KbCallbackDumpIo 例程main目的是允许将系统故障转储数据写入磁盘以外的设备。 例如,监视系统状态的设备可以使用回调来报告系统检查发出 bug,并提供故障转储以供分析。

使用 KeRegisterBugCheckReasonCallback 注册 KbCallbackDumpIo 例程。 驱动程序随后可以使用 KeDeregisterBugCheckReasonCallback 例程删除回调。 如果驱动程序可以卸载,则必须删除其 DRIVER_UNLOAD 回调函数中的任何已注册回调。

KbCallbackDumpIo 例程在可以执行的操作方面受到严格的限制。 有关详细信息,请参阅 Bug 检查回调例程限制

实现 KbCallbackSecondaryDumpData 回调例程

内核模式驱动程序可以实现 KbCallbackSecondaryDumpData 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以提供要追加到故障转储文件的数据。

系统设置 ReasonSpecificData 指向的KBUGCHECK_SECONDARY_DUMP_DATA结构的 InBuffer、InBufferLengthOutBufferMaximumAllowed 成员。 MaximumAllowed 成员指定例程可以提供的最大转储数据量。

OutBuffer 成员的值确定系统是请求驱动程序转储数据的大小还是数据本身的大小,如下所示:

  • 如果 KBUGCHECK_SECONDARY_DUMP_DATA 的 OutBuffer 成员为 NULL,则系统仅请求大小信息。 KbCallbackSecondaryDumpData 例程填充 OutBufferOutBufferLength 成员。

  • 如果 KBUGCHECK_SECONDARY_DUMP_DATA 的 OutBuffer 成员等于 InBuffer 成员,则系统正在请求驱动程序的辅助转储数据。 KbCallbackSecondaryDumpData 例程填充 OutBufferOutBufferLength 成员,并将数据写入 OutBuffer 指定的缓冲区。

KBUGCHECK_SECONDARY_DUMP_DATA 的 InBuffer 成员指向一个小缓冲区供例程使用。 InBufferLength 成员指定缓冲区的大小。 如果要写入的数据量小于 InBufferLength,则回调例程可以使用此缓冲区向系统提供故障转储数据。 然后,回调例程将 OutBuffer 设置为 InBuffer将 OutBufferLength 设置为写入缓冲区的实际数据量。

必须写入大于 InBufferLength 的数据量的驱动程序可以使用其自己的缓冲区来提供数据。 必须在执行回调例程之前分配此缓冲区,并且必须驻留在常驻内存 (,例如非分页池) 。 然后,回调例程将 OutBuffer 设置为指向驱动程序的缓冲区,将 OutBufferLength 设置为缓冲区中要写入故障转储文件的数据量。

要写入故障转储文件的每个数据块都使用 KBUGCHECK_SECONDARY_DUMP_DATA 结构的 Guid 成员的值进行标记。 使用的 GUID 对于驱动程序必须是唯一的。 若要显示与此 GUID 对应的辅助转储数据,可以在调试器扩展中使用 .enumtag 命令或 IDebugDataSpaces3::ReadTagged 方法。 有关调试器和调试器扩展的信息,请参阅 Windows 调试

驱动程序可以将具有相同 GUID 的多个块写入故障转储文件,但这种做法非常糟糕,因为调试器只能访问第一个块。 注册多个 KbCallbackSecondaryDumpData 例程的驱动程序应为每个回调分配唯一的 GUID。

使用 KeRegisterBugCheckReasonCallback 注册 KbCallbackSecondaryDumpData 例程。 驱动程序随后可以使用 KeDeregisterBugCheckReasonCallback 例程删除回调例程。 如果驱动程序可以卸载,则必须删除其 DRIVER_UNLOAD 回调函数中的任何已注册回调例程。

KbCallbackSecondaryDumpData 例程在可以执行的操作方面受到非常限制。 有关详细信息,请参阅 Bug 检查回调例程限制

实现 KbCallbackTriageDumpData 回调例程

从 Windows 10 版本 1809 和 Windows Server 2019 开始,内核模式驱动程序可以实现 KbCallbackTriageDumpData 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以标记虚拟内存范围以包含在雕刻的内核微型转储中。 这可确保小型转储将包含指定的范围,以便可以使用在内核转储中工作的相同调试器命令来访问它们。 目前,这是针对“雕刻”的小型转储实现的,这意味着捕获了内核或更大的转储,然后从较大的转储创建了一个小型转储。 默认情况下,大多数系统都针对自动/内核转储进行配置,并且系统会在崩溃后下次启动时自动创建一个小型转储。

系统在 ReasonSpecificData 参数中传递指向 KBUGCHECK_TRIAGE_DUMP_DATA 结构的指针,该结构包含有关 Bug 检查的信息,以及驱动程序用来返回其初始化和填充的数据数组的 OUT 参数。

在以下示例中,驱动程序配置会审转储数组,然后注册回调的最小实现。 驱动程序将使用 数组将两个全局变量添加到小型转储。

#include <ntosp.h>

// Header definitions


    //
    // The maximum count of ranges the driver will add to the array.
    // This example is only adding max 3 ranges with some extra.
    //

#define MAX_RANGES 10

    //
    // This should be large enough to hold the maximum number of KADDRESS_RANGE
    // which the driver expects to add to the array.
    //

#define ARRAY_SIZE ((FIELD_OFFSET(KTRIAGE_DUMP_DATA_ARRAY, Blocks)) + (sizeof(KADDRESS_RANGE) * MAX_RANGES))

// Globals 
 
static PKBUGCHECK_REASON_CALLBACK_RECORD gBugcheckTriageCallbackRecord; 
static PKTRIAGE_DUMP_DATA_ARRAY gTriageDumpDataArray;

    //
    // This is a global variable which the driver wants to be available in
    // the kernel minidump. A real driver may add more address ranges.
    //

ULONG64 gDriverData1 = 0xAAAAAAAA;
PULONG64 gpDriverData2;
 
// Functions
 
VOID 
ExampleBugCheckCallbackRoutine( 
    KBUGCHECK_CALLBACK_REASON Reason, 
    PKBUGCHECK_REASON_CALLBACK_RECORD Record, 
    PVOID Data, 
    ULONG Length 
    ) 
{ 
    PKBUGCHECK_TRIAGE_DUMP_DATA DumpData; 
 
    UNREFERENCED_PARAMETER(Reason);
    UNREFERENCED_PARAMETER(Record);
    UNREFERENCED_PARAMETER(Length);

    DumpData = (PKBUGCHECK_TRIAGE_DUMP_DATA) Data;

    if ((DumpData->Flags & KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE) == 0) {
        return;
    }

    if (gTriageDumpDataArray == NULL)
    {
        return;
    }
 
    //
    // Add the dynamically allocated global pointer and buffer once validated.
    //

    if ((gpDriverData2 != NULL) && (MmIsAddressValid(gpDriverData2))) {

        //
        // Add the address of the global itself a well as the pointed data
        // so you can use the global to access the data in the debugger
        // by running a command like "dt example!gpDriverData2"
        //

        KeAddTriageDumpDataBlock(gTriageDumpDataArray, &gpDriverData2, sizeof(PULONG64));
        KeAddTriageDumpDataBlock(gTriageDumpDataArray, gpDriverData2, sizeof(ULONG64));
    }

    //
    // Pass the array back for processing.
    //
 
    DumpData->DataArray = gTriageDumpDataArray; 
 
    return; 
}

// Setup Function

NTSTATUS
SetupTriageDataCallback(VOID) 
{ 
    PVOID pBuffer;
    NTSTATUS Status;
    BOOLEAN bSuccess;
 
    //
    // Call this function from DriverEntry.
    // 
    // Allocate a buffer to hold a callback record and triage dump data array
    // in the non-paged pool. 
    //
 
    pBuffer = ExAllocatePoolWithTag(NonPagedPoolNx,
                                    sizeof(KBUGCHECK_REASON_CALLBACK_RECORD) + ARRAY_SIZE,
                                    'Xmpl');

    if (pBuffer == NULL) {
        return STATUS_NO_MEMORY;
    }

    RtlZeroMemory(pBuffer, sizeof(KBUGCHECK_REASON_CALLBACK_RECORD));
    gBugcheckTriageCallbackRecord = (PKBUGCHECK_REASON_CALLBACK_RECORD) pBuffer;
    KeInitializeCallbackRecord(gBugcheckTriageCallbackRecord); 

    gTriageDumpDataArray =
        (PKTRIAGE_DUMP_DATA_ARRAY) ((PUCHAR) pBuffer + sizeof(KBUGCHECK_REASON_CALLBACK_RECORD));

    // 
    // Initialize the dump data block array. 
    // 
 
    Status = KeInitializeTriageDumpDataArray(gTriageDumpDataArray, ARRAY_SIZE);
    if (!NT_SUCCESS(Status)) {
        ExFreePoolWithTag(pBuffer, 'Xmpl');
        gTriageDumpDataArray = NULL;
        gBugcheckTriageCallbackRecord = NULL;
        return Status;
    }

    //
    // Set up a callback record
    //    

    bSuccess = KeRegisterBugCheckReasonCallback(gBugcheckTriageCallbackRecord, 
                                                ExampleBugCheckCallbackRoutine, 
                                                KbCallbackTriageDumpData, 
                                                (PUCHAR)"Example"); 

    if ( !bSuccess ) {
         ExFreePoolWithTag(gTriageDumpDataArray, 'Xmpl');
         gTriageDumpDataArray = NULL;
         return STATUS_UNSUCCESSFUL;
    }

    //
    // It is possible to add a range to the array before bugcheck if it is
    // guaranteed to remain valid for the lifetime of the driver.
    // The value could change before bug check, but the address and size
    // must remain valid.
    //

    KeAddTriageDumpDataBlock(gTriageDumpDataArray, &gDriverData1, sizeof(gDriverData1));

    //
    // For an example, allocate another buffer here for later addition tp the array.
    //

    gpDriverData2 = ExAllocatePoolWithTag(NonPagedPoolNx, sizeof(ULONG64), 'Xmpl');
    if (gpDriverData2 != NULL) {
        *gpDriverData2 = 0xBBBBBBBB;
    }

    return STATUS_SUCCESS;
} 



// Deregister function

VOID CleanupTriageDataCallbacks() 
{ 

    //
    // Call this routine from DriverUnload
    //

    if (gBugcheckTriageCallbackRecord != NULL) {
        KeDeregisterBugCheckReasonCallback( gBugcheckTriageCallbackRecord );
        ExFreePoolWithTag( gBugcheckTriageCallbackRecord, 'Xmpl' );
        gTriageDumpDataArray = NULL;
    }

}

只有非分页内核模式地址应与此回调方法一起使用。

KbCallbackTriageDumpData 例程在可以执行的操作方面受到很大限制。 有关详细信息,请参阅 Bug 检查回调例程限制

验证是否设置了KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE标志后,只能从 KbCallbackTriageDumpData 例程使用 MmIsAddressValid 函数。 当前始终需要设置此标志,但如果未设置其他同步,则调用例程是不安全的。