다음을 통해 공유


버그 검사 이유 콜백 루틴 작성

드라이버는 필요에 따라 크래시 덤프 파일이 작성된 후 시스템이 호출하는 KBUGCHECK_REASON_CALLBACK_ROUTINE 콜백 함수를 제공할 수 있습니다.

참고

이 문서에서는 KBUGCHECK_CALLBACK_ROUTINE 콜백 함수가 아닌 버그 검사이유 콜백 루틴에 대해 설명합니다.

이 콜백에서 드라이버는 다음을 수행할 수 있습니다.

  • 크래시 덤프 파일에 드라이버별 데이터 추가

  • 디바이스를 알려진 상태로 다시 설정

다음 루틴을 사용하여 콜백을 등록하고 제거합니다.

이 콜백 형식은 등록 시 제공되는 KBUGCHECK_CALLBACK_REASON 상수 값에 따라 동작이 변경되어 오버로드됩니다. 이 문서에서는 다양한 사용 시나리오에 대해 설명합니다.

버그 검사 데이터에 대한 일반적인 내용은 버그 검사 콜백 데이터 읽기를 참조하세요.

버그 검사 콜백 루틴 제한

버그 검사 콜백 루틴은 IRQL = HIGH_LEVEL 실행되어 수행할 수 있는 작업을 강력하게 제한합니다.

버그 검사 콜백 루틴은 다음을 수행할 수 없습니다.

  • 메모리 할당

  • 페이지 사용 가능한 메모리 액세스

  • 동기화 메커니즘 사용

  • IRQL = DISPATCH_LEVEL 이하에서 실행해야 하는 루틴을 호출합니다.

버그 검사 콜백 루틴은 중단 없이 실행되도록 보장되므로 동기화가 필요하지 않습니다. 버그 검사 루틴이 동기화 메커니즘을 사용하여 잠금을 획득하려고 하면 시스템이 교착 상태에 빠지게 됩니다. 버그 검사 시 데이터 구조 또는 목록이 일관되지 않은 상태일 수 있으므로 잠금으로 보호되는 데이터 구조에 액세스할 때는 주의해야 합니다. 예를 들어 목록을 볼 때 상한 검사를 추가하고, 순환 목록이 있거나 링크가 잘못된 주소를 가리키는 경우 링크가 유효한 메모리를 가리키는지 확인해야 합니다.

MmIsAddressValid는 버그 검사 콜백 루틴에서 주소 액세스로 인해 페이지 오류가 발생하는지 여부를 검사 수 있습니다. 루틴은 중단 없이 실행되고 다른 코어는 고정되므로 해당 함수의 동기화 요구 사항을 충족합니다. 페이지 오류로 인해 이중 오류가 발생하고 덤프가 기록되지 않을 수 있으므로 버그 검사 콜백에서 연기하기 전에 항상 MmIsAddressValid를 사용하여 페이징되거나 유효하지 않을 수 있는 커널 주소를 확인해야 합니다.

드라이버의 버그 검사 콜백 루틴은 READ_PORT_XXX, READ_REGISTER_XXX, WRITE_PORT_XXXWRITE_REGISTER_XXX 루틴을 안전하게 사용하여 드라이버의 디바이스와 통신할 수 있습니다. (이러한 루틴에 대한 자세한 내용은 하드웨어 추상화 계층 루틴을 참조하세요.)

KbCallbackAddPages 콜백 루틴 구현

커널 모드 드라이버는 KbCallbackAddPages 형식의 KBUGCHECK_REASON_CALLBACK_ROUTINE 콜백 함수를 구현하여 버그 검사 발생할 때 하나 이상의 데이터 페이지를 크래시 덤프 파일에 추가할 수 있습니다. 이 루틴을 운영 체제에 등록하기 위해 드라이버는 KeRegisterBugCheckReasonCallback 루틴을 호출합니다. 드라이버가 언로드하기 전에 등록을 제거하려면 KeDeregisterBugCheckReasonCallback 루틴을 호출해야 합니다.

Windows 8 시작하여 커널 메모리 덤프 또는 전체 메모리 덤프 중에 등록된 KbCallbackAddPages 루틴이 호출됩니다. 이전 버전의 Windows에서는 커널 메모리 덤프 중에는 등록된 KbCallbackAddPages 루틴이 호출되지만 전체 메모리 덤프 중에는 호출되지 않습니다. 기본적으로 커널 메모리 덤프에는 버그 검사 발생 당시 Windows 커널에서 사용 중인 실제 페이지만 포함되는 반면 전체 메모리 덤프에는 Windows에서 사용하는 모든 실제 메모리가 포함됩니다. 전체 메모리 덤프에는 기본적으로 플랫폼 펌웨어에서 사용하는 실제 메모리가 포함되지 않습니다.

KbCallbackAddPages 루틴은 덤프 파일에 추가할 드라이버별 데이터를 제공할 수 있습니다. 예를 들어 커널 메모리 덤프의 경우 이 추가 데이터에는 가상 메모리의 시스템 주소 범위에 매핑되지 않지만 드라이버를 디버그하는 데 도움이 되는 정보가 포함된 실제 페이지가 포함될 수 있습니다. KbCallbackAddPages 루틴은 매핑되지 않거나 가상 메모리의 사용자 모드 주소에 매핑된 드라이버 소유 물리적 페이지를 덤프 파일에 추가할 수 있습니다.

버그 검사 발생하면 운영 체제는 등록된 모든 KbCallbackAddPages 루틴을 호출하여 드라이버에서 크래시 덤프 파일에 추가할 데이터를 폴링합니다. 각 호출은 하나 이상의 연속 데이터 페이지를 크래시 덤프 파일에 추가합니다. KbCallbackAddPages 루틴은 시작 페이지에 대한 가상 주소 또는 실제 주소를 제공할 수 있습니다. 호출 중에 둘 이상의 페이지를 제공하는 경우 시작 주소가 가상인지 물리적인지에 따라 가상 또는 물리적 메모리에서 페이지가 연속됩니다. 연속되지 않은 페이지를 제공하기 위해 KbCallbackAddPages 루틴은 KBUGCHECK_ADD_PAGES 구조에서 플래그를 설정하여 추가 데이터가 있고 다시 호출되어야 함을 나타낼 수 있습니다.

보조 크래시 덤프 지역에 데이터를 추가하는 KbCallbackSecondaryDumpData 루틴과 달리 KbCallbackAddPages 루틴은 기본 크래시 덤프 지역에 데이터 페이지를 추가합니다. 디버깅하는 동안 기본 크래시 덤프 데이터는 보조 크래시 덤프 데이터보다 액세스하기 쉽습니다.

운영 체제는 ReasonSpecificData가 가리키는 KBUGCHECK_ADD_PAGES 구조체의 BugCheckCode 멤버를 채웁니다. KbCallbackAddPages 루틴은 이 구조체의 Flags, AddressCount 멤버 값을 설정해야 합니다.

KbCallbackAddPages에 대한 첫 번째 호출 전에 운영 체제는 컨텍스트NULL로 초기화합니다. KbCallbackAddPages 루틴이 두 번 이상 호출되면 운영 체제는 콜백 루틴이 이전 호출에서 Context 멤버에게 쓴 값을 유지합니다.

KbCallbackAddPages 루틴은 수행할 수 있는 작업에서 매우 제한됩니다. 자세한 내용은 버그 검사 콜백 루틴 제한을 참조하세요.

KbCallbackDumpIo 콜백 루틴 구현

커널 모드 드라이버는 KbCallbackDumpIo 형식의 KBUGCHECK_REASON_CALLBACK_ROUTINE 콜백 함수를 구현하여 크래시 덤프 파일에 데이터를 쓸 때마다 작업을 수행할 수 있습니다. 시스템은 ReasonSpecificData 매개 변수에서 KBUGCHECK_DUMP_IO 구조체에 대한 포인터를 전달합니다. Buffer 멤버는 현재 데이터를 가리키고 BufferLength 멤버는 길이를 지정합니다. Type 멤버는 덤프 파일 헤더 정보, 메모리 상태 또는 드라이버에서 제공하는 데이터와 같이 현재 작성 중인 데이터의 형식을 나타냅니다. 가능한 정보 유형에 대한 설명은 KBUGCHECK_DUMP_IO_TYPE 열거형을 참조하세요.

시스템에서 크래시 덤프 파일을 순차적으로 작성하거나 순서를 초과할 수 있습니다. 시스템에서 크래시 덤프 파일을 순차적으로 작성하는 경우 ReasonSpecificDataOffset 멤버는 -1입니다. 그렇지 않으면 오프셋이 크래시 덤프 파일의 현재 오프셋(바이트)으로 설정됩니다.

시스템에서 파일을 순차적으로 작성할 때 헤더 정보(KbDumpIoHeader유형 = )를 작성할 때 각 KbCallbackDumpIo 루틴을 한 번 이상 호출하고, 크래시 덤프 파일의 기본 본문(KbDumpIoBody형식 = )을 작성할 때 한 번 이상, 보조 덤프 데이터(KbDumpIoSecondaryDumpData형식 = )를 작성할 때 한 번 이상 호출합니다. 시스템이 크래시 덤프 파일 작성을 완료하면 버퍼 = NULL, BufferLength = 0 및 Type = KbDumpIoComplete를 사용하여 콜백을 호출합니다.

KbCallbackDumpIo 루틴의 기본 목적은 시스템 크래시 덤프 데이터를 디스크 이외의 디바이스에 쓸 수 있도록 하는 것입니다. 예를 들어 시스템 상태를 모니터링하는 디바이스는 콜백을 사용하여 시스템에서 버그 검사 발급했음을 보고하고 분석을 위해 크래시 덤프를 제공할 수 있습니다.

KeRegisterBugCheckReasonCallback을 사용하여 KbCallbackDumpIo 루틴을 등록합니다. 이후에 드라이버는 KeDeregisterBugCheckReasonCallback 루틴을 사용하여 콜백을 제거할 수 있습니다. 드라이버를 언로드할 수 있는 경우 DRIVER_UNLOAD 콜백 함수에서 등록된 콜백을 제거해야 합니다.

KbCallbackDumpIo 루틴은 수행할 수 있는 작업에서 강력하게 제한됩니다. 자세한 내용은 버그 검사 콜백 루틴 제한을 참조하세요.

KbCallbackSecondaryDumpData 콜백 루틴 구현

커널 모드 드라이버는 KbCallbackSecondaryDumpData 형식의 KBUGCHECK_REASON_CALLBACK_ROUTINE 콜백 함수를 구현하여 크래시 덤프 파일에 추가할 데이터를 제공할 수 있습니다.

시스템은 ReasonSpecificData가 가리키는 KBUGCHECK_SECONDARY_DUMP_DATA 구조체의 InBuffer, InBufferLength, OutBufferMaximumAllowed 멤버를 설정합니다. MaximumAllowed 멤버는 루틴에서 제공할 수 있는 최대 덤프 데이터 양을 지정합니다.

OutBuffer 멤버의 값은 다음과 같이 시스템에서 드라이버의 덤프 데이터 크기를 요청하는지 또는 데이터 자체를 요청하는지 여부를 결정합니다.

  • KBUGCHECK_SECONDARY_DUMP_DATA OutBuffer 멤버가 NULL인 경우 시스템은 크기 정보만 요청합니다. KbCallbackSecondaryDumpData 루틴은 OutBuffer 및 OutBufferLength 멤버를 채웁니다.

  • KBUGCHECK_SECONDARY_DUMP_DATA OutBuffer 멤버가 InBuffer 멤버와 같으면 시스템에서 드라이버의 보조 덤프 데이터를 요청합니다. KbCallbackSecondaryDumpData 루틴은 OutBuffer 및 OutBufferLength 멤버를 채우고 OutBuffer에서 지정한 버퍼에 데이터를 씁니다.

KBUGCHECK_SECONDARY_DUMP_DATA InBuffer 멤버는 루틴을 사용하기 위해 작은 버퍼를 가리킵니다. InBufferLength 멤버는 버퍼의 크기를 지정합니다. 쓸 데이터의 양이 InBufferLength보다 작은 경우 콜백 루틴은 이 버퍼를 사용하여 크래시 덤프 데이터를 시스템에 제공할 수 있습니다. 그런 다음 콜백 루틴은 OutBufferInBuffer 로 설정하고 OutBufferLength 를 버퍼에 기록된 실제 데이터 양으로 설정합니다.

InBufferLength보다 큰 양의 데이터를 작성해야 하는 드라이버는 자체 버퍼를 사용하여 데이터를 제공할 수 있습니다. 이 버퍼는 콜백 루틴이 실행되기 전에 할당되어야 하며, 상주 메모리(예: 비페이지 풀)에 있어야 합니다. 그런 다음 콜백 루틴은 드라이버의 버퍼를 가리키도록 OutBuffer 를 설정하고 OutBufferLength 는 크래시 덤프 파일에 쓸 버퍼의 데이터 양을 설정합니다.

크래시 덤프 파일에 쓸 각 데이터 블록은 KBUGCHECK_SECONDARY_DUMP_DATA 구조체의 Guid 멤버 값으로 태그가 지정됩니다. 사용되는 GUID는 드라이버에 고유해야 합니다. 이 GUID에 해당하는 보조 덤프 데이터를 표시하려면 디버거 확장에서 .enumtag 명령 또는 IDebugDataSpaces3::ReadTagged 메서드를 사용할 수 있습니다. 디버거 및 디버거 확장에 대한 자세한 내용은 Windows 디버깅을 참조하세요.

드라이버는 동일한 GUID를 사용하여 여러 블록을 크래시 덤프 파일에 쓸 수 있지만 첫 번째 블록만 디버거에 액세스할 수 있기 때문에 이는 매우 좋지 않습니다. 여러 KbCallbackSecondaryDumpData 루틴을 등록하는 드라이버는 각 콜백에 대해 고유한 GUID를 할당해야 합니다.

KeRegisterBugCheckReasonCallback을 사용하여 KbCallbackSecondaryDumpData 루틴을 등록합니다. 이후에 드라이버는 KeDeregisterBugCheckReasonCallback 루틴을 사용하여 콜백 루틴을 제거할 수 있습니다. 드라이버를 언로드할 수 있는 경우 DRIVER_UNLOAD 콜백 함수에서 등록된 콜백 루틴을 제거해야 합니다.

KbCallbackSecondaryDumpData 루틴은 수행할 수 있는 작업에서 매우 제한됩니다. 자세한 내용은 버그 검사 콜백 루틴 제한을 참조하세요.

KbCallbackTriageDumpData 콜백 루틴 구현

Windows 10, 버전 1809 및 Windows Server 2019부터 커널 모드 드라이버는 KbCallbackTriageDumpData 형식의 KBUGCHECK_REASON_CALLBACK_ROUTINE 콜백 함수를 구현하여 조각된 커널 미니덤프에 포함할 가상 메모리 범위를 표시할 수 있습니다. 이렇게 하면 미니덤프에 지정된 범위가 포함되므로 커널 덤프에서 작동하는 동일한 디버거 명령을 사용하여 액세스할 수 있습니다. 이는 현재 '조각된' 미니덤프에 대해 구현되어 커널 또는 더 큰 덤프가 캡처된 다음 더 큰 덤프에서 미니덤프가 생성되었음을 의미합니다. 대부분의 시스템은 기본적으로 자동/커널 덤프에 대해 구성되며, 시스템은 충돌 후 다음 부팅에 미니덤프를 자동으로 만듭니다.

시스템은 ReasonSpecificData 매개 변수에서 버그 검사에 대한 정보가 포함된 KBUGCHECK_TRIAGE_DUMP_DATA 구조에 대한 포인터와 드라이버가 초기화되고 채워진 데이터 배열을 반환하는 데 사용하는 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(pBuffer, 'Xmpl');
        gTriageDumpDataArray = NULL;
        gBugcheckTriageCallbackRecord = 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 루틴은 수행할 수 있는 작업에서 매우 제한됩니다. 자세한 내용은 버그 검사 콜백 루틴 제한을 참조하세요.

MmIsAddressValid 함수는 KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE 플래그가 설정되어 있는지 유효성을 검사한 후에만 KbCallbackTriageDumpData 루틴에서 사용해야 합니다. 이 플래그는 현재 항상 설정되어야 하지만 추가 동기화 없이 설정되지 않은 경우 루틴을 호출하는 것은 안전하지 않습니다.