Gravando uma rotina de retorno de chamada do motivo da verificação de bugs

Opcionalmente, um driver pode fornecer uma função de retorno de chamada KBUGCHECK_REASON_CALLBACK_ROUTINE , que o sistema chama depois que um arquivo de despejo de falha é gravado.

Observação

Este artigo descreve o bug marcar rotina de retorno de chamada de motivo e não a função de retorno de chamada KBUGCHECK_CALLBACK_ROUTINE.

Nesse retorno de chamada, o driver pode:

  • Adicionar dados específicos do driver ao arquivo de despejo de memória

  • Redefinir o dispositivo para um estado conhecido

Use as seguintes rotinas para registrar e remover o retorno de chamada:

Esse tipo de retorno de chamada é sobrecarregado, com o comportamento mudando com base no valor constante KBUGCHECK_CALLBACK_REASON fornecido no registro. Este artigo descreve os diferentes cenários de uso.

Para obter informações gerais sobre dados de marcar de bugs, consulte Lendo dados de retorno de chamada de verificação de bugs.

Restrições de rotina de retorno de chamada de verificação de bugs

Um bug marcar rotina de retorno de chamada é executado em IRQL = HIGH_LEVEL, o que impõe fortes restrições ao que ele pode fazer.

Um bug marcar rotina de retorno de chamada não pode:

  • Alocar memória

  • Acessar memória paginável

  • Usar qualquer mecanismo de sincronização

  • Chamar qualquer rotina que deve ser executada em IRQL = DISPATCH_LEVEL ou abaixo

As rotinas de retorno de chamada marcar bug são garantidas para serem executadas sem interrupção, portanto, nenhuma sincronização é necessária. (Se o bug marcar rotina tentar adquirir bloqueios usando qualquer mecanismo de sincronização, o sistema fará deadlock.) Tenha em mente que as estruturas ou listas de dados podem estar em um estado inconsistente no momento da verificação de bugs, portanto, deve-se tomar cuidado ao acessar estruturas de dados protegidas por bloqueios. Por exemplo, você deve adicionar verificação de limites superiores ao percorrer listas e verificar se os links estão apontando para memória válida, caso haja uma lista circular ou um link esteja apontando para um endereço inválido.

O MmIsAddressValid pode ser usado por uma rotina de retorno de chamada de Verificação de Bugs para marcar se o acesso a um endereço causará uma falha de página. Como a rotina é executada sem interrupção e outros núcleos são congelados, isso atende aos requisitos de sincronização dessa função. Os endereços kernel que podem ser paginados ou inválidos sempre devem ser verificados com MmIsAddressValid antes de adiá-los em um retorno de chamada de Verificação de Bugs, pois uma falha de página causará uma falha dupla e poderá impedir que o despejo seja gravado.

A rotina de retorno de chamada marcar bug de um driver pode usar com segurança as rotinas READ_PORT_XXX, READ_REGISTER_XXX, WRITE_PORT_XXX e WRITE_REGISTER_XXX para se comunicar com o dispositivo do driver. (Para obter informações sobre essas rotinas, consulte Rotinas de camada de abstração de hardware.)

Implementando uma rotina de retorno de chamada KbCallbackAddPages

Um driver de modo kernel pode implementar uma função de retorno de chamada KBUGCHECK_REASON_CALLBACK_ROUTINE do tipo KbCallbackAddPages para adicionar uma ou mais páginas de dados a um arquivo de despejo de memória quando ocorre um bug marcar. Para registrar essa rotina no sistema operacional, o driver chama a rotina KeRegisterBugCheckReasonCallback . Antes que o driver descarregue, ele deve chamar a rotina KeDeregisterBugCheckReasonCallback para remover o registro.

Começando com Windows 8, uma rotina KbCallbackAddPages registrada é chamada durante um despejo de memória do kernel ou um despejo de memória completo. Em versões anteriores do Windows, uma rotina KbCallbackAddPages registrada é chamada durante um despejo de memória do kernel, mas não durante um despejo de memória completo. Por padrão, um despejo de memória do kernel inclui apenas as páginas físicas que estão sendo usadas pelo kernel do Windows no momento em que o bug marcar ocorrer, enquanto um despejo de memória completo inclui toda a memória física usada pelo Windows. Um despejo de memória completo não inclui, por padrão, a memória física usada pelo firmware da plataforma.

Sua rotina KbCallbackAddPages pode fornecer dados específicos do driver para adicionar ao arquivo de despejo. Por exemplo, para um despejo de memória do kernel, esses dados adicionais podem incluir páginas físicas que não são mapeadas para o intervalo de endereços do sistema na memória virtual, mas que contêm informações que podem ajudá-lo a depurar o driver. A rotina KbCallbackAddPages pode adicionar ao arquivo de despejo todas as páginas físicas de propriedade do driver que não são mapeadas ou mapeadas para endereços do modo de usuário na memória virtual.

Quando ocorre um bug marcar, o sistema operacional chama todas as rotinas KbCallbackAddPages registradas para sondar drivers de dados a serem adicionados ao arquivo de despejo de memória. Cada chamada adiciona uma ou mais páginas de dados contíguos ao arquivo de despejo de memória. Uma rotina KbCallbackAddPages pode fornecer um endereço virtual ou um endereço físico para a página inicial. Se mais de uma página for fornecida durante uma chamada, as páginas serão contíguas na memória virtual ou física, dependendo se o endereço inicial é virtual ou físico. Para fornecer páginas não contíguas, a rotina KbCallbackAddPages pode definir um sinalizador na estrutura KBUGCHECK_ADD_PAGES para indicar que ela tem dados adicionais e precisa ser chamada novamente.

Ao contrário de uma rotina KbCallbackSecondaryDumpData , que acrescenta dados à região de despejo de memória secundária, uma rotina KbCallbackAddPages adiciona páginas de dados à região de despejo de falhas primária. Durante a depuração, os dados de despejo de falhas primários são mais fáceis de acessar do que os dados secundários de despejo de memória.

O sistema operacional preenche o membro BugCheckCode da estrutura KBUGCHECK_ADD_PAGES para a qual ReasonSpecificData aponta. A rotina KbCallbackAddPages deve definir os valores dos membros Flags, Address e Count dessa estrutura.

Antes da primeira chamada para KbCallbackAddPages, o sistema operacional inicializa Context to NULL. Se a rotina KbCallbackAddPages for chamada mais de uma vez, o sistema operacional preservará o valor que a rotina de retorno de chamada escreveu para o membro Context na chamada anterior.

Uma rotina KbCallbackAddPages é muito restrita nas ações que ela pode executar. Para obter mais informações, consulte Bug Check Callback Routine Restrictions( Restrições de rotina de retorno de chamada de verificação de bugs).

Implementando uma rotina de retorno de chamada KbCallbackDumpIo

Um driver de modo kernel pode implementar uma função de retorno de chamada KBUGCHECK_REASON_CALLBACK_ROUTINE do tipo KbCallbackDumpIo para executar o trabalho sempre que os dados forem gravados no arquivo de despejo de memória. O sistema passa, no parâmetro ReasonSpecificData , um ponteiro para uma estrutura KBUGCHECK_DUMP_IO . O membro Buffer aponta para os dados atuais e o membro BufferLength especifica seu comprimento. O membro Type indica o tipo de dados que está sendo gravado no momento, como informações de cabeçalho de arquivo de despejo, estado de memória ou dados fornecidos por um driver. Para obter uma descrição dos possíveis tipos de informações, consulte a enumeração KBUGCHECK_DUMP_IO_TYPE .

O sistema pode gravar o arquivo de despejo de memória sequencialmente ou fora de ordem. Se o sistema estiver gravando o arquivo de despejo de memória sequencialmente, o membro Offset de ReasonSpecificData será -1; caso contrário, Offset é definido como o deslocamento atual, em bytes, no arquivo de despejo de falha.

Quando o sistema grava o arquivo sequencialmente, ele chama cada rotina KbCallbackDumpIo uma ou mais vezes ao gravar as informações de cabeçalho (Tipo = KbDumpIoHeader), uma ou mais vezes ao gravar o corpo main do arquivo de despejo de falha (Tipo = KbDumpIoBody) e uma ou mais vezes ao gravar os dados de despejo secundários (Type = KbDumpIoSecondaryDumpData). Depois que o sistema tiver concluído a gravação do arquivo de despejo de falha, ele chamará o retorno de chamada com Buffer = NULL, BufferLength = 0 e Tipo = KbDumpIoComplete.

A finalidade main de uma rotina KbCallbackDumpIo é permitir que os dados de despejo de falha do sistema sejam gravados em dispositivos diferentes do disco. Por exemplo, um dispositivo que monitora o estado do sistema pode usar o retorno de chamada para relatar que o sistema emitiu um bug marcar e para fornecer um despejo de memória para análise.

Use KeRegisterBugCheckReasonCallback para registrar uma rotina KbCallbackDumpIo . Um driver pode remover posteriormente o retorno de chamada usando a rotina KeDeregisterBugCheckReasonCallback . Se o driver puder ser descarregado, ele deverá remover quaisquer retornos de chamada registrados em sua função de retorno de chamada DRIVER_UNLOAD .

Uma rotina KbCallbackDumpIo é fortemente restrita nas ações que ela pode tomar. Para obter mais informações, consulte Bug Check Callback Routine Restrictions( Restrições de rotina de retorno de chamada de verificação de bugs).

Implementando uma rotina de retorno de chamada KbCallbackSecondaryDumpData

Um driver no modo kernel pode implementar uma função de retorno de chamada KBUGCHECK_REASON_CALLBACK_ROUTINE do tipo KbCallbackSecondaryDumpData para fornecer dados para acrescentar ao arquivo de despejo de falha.

O sistema define os membros InBuffer, InBufferLength, OutBuffer e MaximumAllowed da estrutura KBUGCHECK_SECONDARY_DUMP_DATA para a qual ReasonSpecificData aponta. O membro MaximumAllowed especifica a quantidade máxima de dados de despejo que a rotina pode fornecer.

O valor do membro OutBuffer determina se o sistema está solicitando o tamanho dos dados de despejo do driver ou os próprios dados, da seguinte maneira:

  • Se o membro do OutBuffer do KBUGCHECK_SECONDARY_DUMP_DATA for NULL, o sistema solicitará apenas informações de tamanho. A rotina KbCallbackSecondaryDumpData preenche os membros OutBuffer e OutBufferLength .

  • Se o membro OutBuffer do KBUGCHECK_SECONDARY_DUMP_DATA for igual ao membro do InBuffer , o sistema solicitará os dados de despejo secundários do driver. A rotina KbCallbackSecondaryDumpData preenche os membros OutBuffer e OutBufferLength e grava os dados no buffer especificado pelo OutBuffer.

O membro InBuffer do KBUGCHECK_SECONDARY_DUMP_DATA aponta para um pequeno buffer para uso da rotina. O membro InBufferLength especifica o tamanho do buffer. Se a quantidade de dados a serem gravados for menor que InBufferLength, a rotina de retorno de chamada poderá usar esse buffer para fornecer os dados de despejo de memória para o sistema. Em seguida, a rotina de retorno de chamada define OutBuffer como InBuffer e OutBufferLength como a quantidade real de dados gravados no buffer.

Um driver que deve gravar uma quantidade de dados maior que InBufferLength pode usar seu próprio buffer para fornecer os dados. Esse buffer deve ter sido alocado antes que a rotina de retorno de chamada seja executada e deve residir na memória residente (como pool nãopagado). Em seguida, a rotina de retorno de chamada define OutBuffer para apontar para o buffer do driver e OutBufferLength para a quantidade de dados no buffer a serem gravados no arquivo de despejo de memória.

Cada bloco de dados a ser gravado no arquivo de despejo de falha é marcado com o valor do membro Guid da estrutura KBUGCHECK_SECONDARY_DUMP_DATA . O GUID usado deve ser exclusivo para o driver. Para exibir os dados de despejo secundários correspondentes a esse GUID, você pode usar o comando .enumtag ou o método IDebugDataSpaces3::ReadTagged em uma extensão de depurador. Para obter informações sobre depuradores e extensões de depurador, consulte Depuração do Windows.

Um driver pode gravar vários blocos com o mesmo GUID no arquivo de despejo de memória, mas essa é uma prática muito ruim, pois somente o primeiro bloco estará acessível ao depurador. Os drivers que registram várias rotinas KbCallbackSecondaryDumpData devem alocar um GUID exclusivo para cada retorno de chamada.

Use KeRegisterBugCheckReasonCallback para registrar uma rotina KbCallbackSecondaryDumpData . Um driver pode remover posteriormente a rotina de retorno de chamada usando a rotina KeDeregisterBugCheckReasonCallback . Se o driver puder ser descarregado, ele deverá remover todas as rotinas de retorno de chamada registradas em seu DRIVER_UNLOAD função de retorno de chamada.

Uma rotina KbCallbackSecondaryDumpData é muito restrita nas ações que ela pode executar. Para obter mais informações, consulte Bug Check Callback Routine Restrictions( Restrições de rotina de retorno de chamada de verificação de bugs).

Implementando uma rotina de retorno de chamada KbCallbackTriageDumpData

A partir do Windows 10, versão 1809 e do Windows Server 2019, um driver de modo kernel pode implementar uma função de retorno de chamada KBUGCHECK_REASON_CALLBACK_ROUTINE do tipo KbCallbackTriageDumpData para marcar intervalos de memória virtual para inclusão em um minidespejo de kernel esculpido. Isso garante que um minidump contenha os intervalos especificados, para que eles possam ser acessados usando os mesmos comandos do depurador que funcionariam em um despejo de kernel. Atualmente, isso é implementado para minidespejos 'esculpidos', o que significa que um kernel ou um despejo maior foi capturado e, em seguida, um minidespejo foi criado a partir do despejo maior. A maioria dos sistemas é configurada para despejos automáticos/kernel por padrão e o sistema cria automaticamente um minidump na próxima inicialização após a falha.

O sistema passa, no parâmetro ReasonSpecificData , um ponteiro para uma estrutura KBUGCHECK_TRIAGE_DUMP_DATA que contém informações sobre a Verificação de Bugs, bem como um parâmetro OUT que é usado pelo driver para retornar sua matriz de dados inicializada e populada.

No exemplo a seguir, o driver configura uma matriz de despejo de triagem e registra uma implementação mínima do retorno de chamada. O driver usará a matriz para adicionar duas variáveis globais ao minidespejo.

#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;
    }

}

Somente endereços no modo kernel nãopaged devem ser usados com esse método de retorno de chamada.

Uma rotina KbCallbackTriageDumpData é muito restrita nas ações que ela pode executar. Para obter mais informações, consulte Bug Check Callback Routine Restrictions( Restrições de rotina de retorno de chamada de verificação de bugs).

A função MmIsAddressValid só deve ser usada de uma rotina KbCallbackTriageDumpData depois de validar se o sinalizador de KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE está definido. Atualmente, espera-se que esse sinalizador seja definido, mas não é seguro chamar a rotina caso ela não seja definida sem sincronização adicional.