Écriture d’une routine de rappel de motif de vérification des bogues

Un pilote peut éventuellement fournir une fonction de rappel KBUGCHECK_REASON_CALLBACK_ROUTINE , que le système appelle après l’écriture d’un fichier de vidage sur incident.

Notes

Cet article décrit la routine de rappel du bogue case activée raison, et non la fonction de rappel KBUGCHECK_CALLBACK_ROUTINE.

Dans ce rappel, le pilote peut :

  • Ajouter des données spécifiques au pilote au fichier de vidage sur incident

  • Réinitialiser l’appareil à un état connu

Utilisez les routines suivantes pour inscrire et supprimer le rappel :

Ce type de rappel est surchargé, avec un comportement qui change en fonction de la valeur constante KBUGCHECK_CALLBACK_REASON fournie lors de l’inscription. Cet article décrit les différents scénarios d’utilisation.

Pour obtenir des informations générales sur les données de case activée de bogues, consultez Données de rappel de vérification des bogues.

Restrictions de routine de vérification des bogues

Un bogue case activée routine de rappel s’exécute à IRQL = HIGH_LEVEL, ce qui impose de fortes restrictions sur ce qu’elle peut faire.

Une routine de rappel case activée bogue ne peut pas :

  • Allouer de la mémoire

  • Accéder à la mémoire paginable

  • Utiliser tous les mécanismes de synchronisation

  • Appeler n’importe quelle routine qui doit s’exécuter à IRQL = DISPATCH_LEVEL ou en dessous

Les routines de rappel case activée bogues sont garanties pour s’exécuter sans interruption, donc aucune synchronisation n’est requise. (Si le bogue case activée routine tente d’acquérir des verrous à l’aide d’un mécanisme de synchronisation, le système s’interblocage.) N’oubliez pas que les structures ou listes de données peuvent être dans un état incohérent au moment de la vérification des bogues. Il faut donc être prudent lors de l’accès aux structures de données protégées par des verrous. Par exemple, vous devez ajouter la vérification des limites supérieures lors des listes de marche et vérifier que les liens pointent vers la mémoire valide, dans le cas où il existe une liste circulaire ou un lien pointe vers une adresse non valide.

Le MmIsAddressValid peut être utilisé par une routine de rappel de vérification des bogues pour case activée si l’accès à une adresse provoque une erreur de page. Étant donné que la routine s’exécute sans interruption et que d’autres cœurs sont gelés, cela répond aux exigences de synchronisation de cette fonction. Les adresses de noyau qui peuvent être paginées ou non valides doivent toujours être vérifiées avec MmIsAddressValid avant de les différer dans un rappel de vérification de bogue, car une erreur de page entraîne une double erreur et peut empêcher l’écriture du vidage.

La routine de rappel des bogues case activée d’un pilote peut utiliser en toute sécurité les routines READ_PORT_XXX, READ_REGISTER_XXX, WRITE_PORT_XXX et WRITE_REGISTER_XXX pour communiquer avec l’appareil du pilote. (Pour plus d’informations sur ces routines, consultez Routines de couche d’abstraction matérielle.)

Implémentation d’une routine de rappel KbCallbackAddPages

Un pilote en mode noyau peut implémenter une fonction de rappel KBUGCHECK_REASON_CALLBACK_ROUTINE de type KbCallbackAddPages pour ajouter une ou plusieurs pages de données à un fichier de vidage sur incident lorsqu’un bogue case activée se produit. Pour inscrire cette routine auprès du système d’exploitation, le pilote appelle la routine KeRegisterBugCheckReasonCallback . Avant le déchargement du pilote, il doit appeler la routine KeDeregisterBugCheckReasonCallback pour supprimer l’inscription.

À compter de Windows 8, une routine KbCallbackAddPages inscrite est appelée lors d’un vidage de la mémoire du noyau ou d’un vidage de mémoire complet. Dans les versions antérieures de Windows, une routine KbCallbackAddPages inscrite est appelée pendant un vidage de la mémoire du noyau, mais pas pendant un vidage de mémoire complet. Par défaut, un vidage de la mémoire du noyau inclut uniquement les pages physiques utilisées par le noyau Windows au moment où le bogue case activée se produit, tandis qu’un vidage de mémoire complet inclut toute la mémoire physique utilisée par Windows. Par défaut, une image mémoire complète n’inclut pas la mémoire physique utilisée par le microprogramme de la plateforme.

Votre routine KbCallbackAddPages peut fournir des données spécifiques au pilote à ajouter au fichier de vidage. Par exemple, pour un vidage de mémoire du noyau, ces données supplémentaires peuvent inclure des pages physiques qui ne sont pas mappées à la plage d’adresses système dans la mémoire virtuelle, mais qui contiennent des informations qui peuvent vous aider à déboguer votre pilote. La routine KbCallbackAddPages peut ajouter au fichier de vidage toutes les pages physiques appartenant au pilote qui ne sont pas maquées ou qui sont mappées à des adresses en mode utilisateur dans la mémoire virtuelle.

Lorsqu’un bogue case activée se produit, le système d’exploitation appelle toutes les routines KbCallbackAddPages inscrites pour interroger les pilotes pour obtenir les données à ajouter au fichier de vidage sur incident. Chaque appel ajoute une ou plusieurs pages de données contiguës au fichier de vidage sur incident. Une routine KbCallbackAddPages peut fournir une adresse virtuelle ou une adresse physique pour la page de démarrage. Si plusieurs pages sont fournies pendant un appel, les pages sont contiguës en mémoire virtuelle ou physique, selon que l’adresse de départ est virtuelle ou physique. Pour fournir des pages non incohérentes, la routine KbCallbackAddPages peut définir un indicateur dans la structure KBUGCHECK_ADD_PAGES pour indiquer qu’elle contient des données supplémentaires et qu’elle doit être appelée à nouveau.

Contrairement à une routine KbCallbackSecondaryDumpData , qui ajoute des données à la région de vidage sur incident secondaire, une routine KbCallbackAddPages ajoute des pages de données à la région de vidage sur incident principal. Pendant le débogage, les données de vidage sur incident principal sont plus faciles d’accès que les données de vidage sur incident secondaires.

Le système d’exploitation remplit le membre BugCheckCode de la structure KBUGCHECK_ADD_PAGES vers laquelle ReasonSpecificData pointe. La routine KbCallbackAddPages doit définir les valeurs des membres Flags, Address et Count de cette structure.

Avant le premier appel à KbCallbackAddPages, le système d’exploitation initialise Context sur NULL. Si la routine KbCallbackAddPages est appelée plusieurs fois, le système d’exploitation conserve la valeur que la routine de rappel a écrite au membre Context dans l’appel précédent.

Une routine KbCallbackAddPages est très restreinte dans les actions qu’elle peut effectuer. Pour plus d’informations, consultez Restrictions de routine de vérification des bogues.

Implémentation d’une routine de rappel KbCallbackDumpIo

Un pilote en mode noyau peut implémenter une fonction de rappel KBUGCHECK_REASON_CALLBACK_ROUTINE de type KbCallbackDumpIo pour effectuer un travail chaque fois que des données sont écrites dans le fichier de vidage sur incident. Le système passe, dans le paramètre ReasonSpecificData , un pointeur vers une structure KBUGCHECK_DUMP_IO . Le membre Buffer pointe vers les données actuelles et le membre BufferLength spécifie sa longueur. Le membre Type indique le type de données en cours d’écriture, comme les informations d’en-tête du fichier de vidage, l’état de la mémoire ou les données fournies par un pilote. Pour obtenir une description des types d’informations possibles, consultez l’énumération KBUGCHECK_DUMP_IO_TYPE .

Le système peut écrire le fichier de vidage sur incident de manière séquentielle ou dans le désordre. Si le système écrit le fichier de vidage sur incident de manière séquentielle, le membre Offset de ReasonSpecificData est -1 ; sinon, offset est défini sur le décalage actuel, en octets, dans le fichier de vidage sur incident.

Lorsque le système écrit le fichier de manière séquentielle, il appelle chaque routine KbCallbackDumpIo une ou plusieurs fois lors de l’écriture des informations d’en-tête (type = KbDumpIoHeader), une ou plusieurs fois lors de l’écriture du corps main du fichier de vidage sur incident (type = KbDumpIoBody) et une ou plusieurs fois lors de l’écriture des données de vidage secondaire (type = KbDumpIoSecondaryDumpData). Une fois que le système a terminé d’écrire le fichier de vidage sur incident, il appelle le rappel avec Buffer = NULL, BufferLength = 0 et tapez = KbDumpIoComplete.

L’objectif main d’une routine KbCallbackDumpIo est de permettre l’écriture de données de vidage sur incident système sur des appareils autres que le disque. Par exemple, un appareil qui surveille l’état du système peut utiliser le rappel pour signaler que le système a émis un bogue case activée et pour fournir un vidage sur incident à des fins d’analyse.

Utilisez KeRegisterBugCheckReasonCallback pour inscrire une routine KbCallbackDumpIo . Un pilote peut ensuite supprimer le rappel à l’aide de la routine KeDeregisterBugCheckReasonCallback . Si le pilote peut être déchargé, il doit supprimer tous les rappels inscrits dans sa fonction de rappel DRIVER_UNLOAD .

Une routine KbCallbackDumpIo est fortement restreinte dans les actions qu’elle peut effectuer. Pour plus d’informations, consultez Restrictions de routine de vérification des bogues.

Implémentation d’une routine de rappel KbCallbackSecondaryDumpData

Un pilote en mode noyau peut implémenter une fonction de rappel KBUGCHECK_REASON_CALLBACK_ROUTINE de type KbCallbackSecondaryDumpData pour fournir des données à ajouter au fichier de vidage sur incident.

Le système définit les membres InBuffer, InBufferLength, OutBuffer et MaximumAllowed de la structure KBUGCHECK_SECONDARY_DUMP_DATA vers laquelle ReasonSpecificData pointe. Le membre MaximumAllowed spécifie la quantité maximale de données de vidage que la routine peut fournir.

La valeur du membre OutBuffer détermine si le système demande la taille des données de vidage du pilote ou les données elles-mêmes, comme suit :

  • Si le membre OutBuffer de KBUGCHECK_SECONDARY_DUMP_DATA a la valeur NULL, le système demande uniquement des informations de taille. La routine KbCallbackSecondaryDumpData remplit les membres OutBuffer et OutBufferLength .

  • Si le membre OutBuffer de KBUGCHECK_SECONDARY_DUMP_DATA est égal au membre InBuffer , le système demande les données de vidage secondaire du pilote. La routine KbCallbackSecondaryDumpData remplit les membres OutBuffer et OutBufferLength et écrit les données dans la mémoire tampon spécifiée par OutBuffer.

Le membre InBuffer de KBUGCHECK_SECONDARY_DUMP_DATA pointe vers une petite mémoire tampon pour l’utilisation de la routine. Le membre InBufferLength spécifie la taille de la mémoire tampon. Si la quantité de données à écrire est inférieure à InBufferLength, la routine de rappel peut utiliser cette mémoire tampon pour fournir les données de vidage sur incident au système. La routine de rappel définit ensuite OutBuffersur InBuffer et OutBufferLength sur la quantité réelle de données écrites dans la mémoire tampon.

Un pilote qui doit écrire une quantité de données supérieure à InBufferLength peut utiliser sa propre mémoire tampon pour fournir les données. Cette mémoire tampon doit avoir été allouée avant l’exécution de la routine de rappel et doit résider dans la mémoire résidente (par exemple, un pool non paginé). La routine de rappel définit ensuite OutBuffer pour pointer vers la mémoire tampon du pilote et OutBufferLength sur la quantité de données dans la mémoire tampon à écrire dans le fichier de vidage sur incident.

Chaque bloc de données à écrire dans le fichier de vidage sur incident est marqué avec la valeur du membre GUID de la structure KBUGCHECK_SECONDARY_DUMP_DATA . Le GUID utilisé doit être propre au pilote. Pour afficher les données de vidage secondaires correspondant à ce GUID, vous pouvez utiliser la commande .enumtag ou la méthode IDebugDataSpaces3::ReadTagged dans une extension de débogueur. Pour plus d’informations sur les débogueurs et les extensions de débogueur, consultez Débogage Windows.

Un pilote peut écrire plusieurs blocs avec le même GUID dans le fichier de vidage sur incident, mais c’est une très mauvaise pratique, car seul le premier bloc sera accessible au débogueur. Les pilotes qui inscrivent plusieurs routines KbCallbackSecondaryDumpData doivent allouer un GUID unique pour chaque rappel.

Utilisez KeRegisterBugCheckReasonCallback pour inscrire une routine KbCallbackSecondaryDumpData . Un pilote peut ensuite supprimer la routine de rappel à l’aide de la routine KeDeregisterBugCheckReasonCallback . Si le pilote peut être déchargé, il doit supprimer toutes les routines de rappel inscrites dans sa fonction de rappel DRIVER_UNLOAD .

Une routine KbCallbackSecondaryDumpData est très limitée dans les actions qu’elle peut effectuer. Pour plus d’informations, consultez Restrictions de routine de rappel de vérification des bogues.

Implémentation d’une routine de rappel KbCallbackTriageDumpData

À compter de Windows 10, version 1809 et Windows Server 2019, un pilote en mode noyau peut implémenter une fonction de rappel KBUGCHECK_REASON_CALLBACK_ROUTINE de type KbCallbackTriageDumpData pour marquer les plages de mémoire virtuelle à inclure dans un minidump de noyau sculpté. Cela garantit qu’un minidump contient les plages spécifiées, afin qu’elles soient accessibles à l’aide des mêmes commandes de débogueur qui fonctionnent dans un vidage du noyau. Ceci est actuellement implémenté pour les minidumps « sculptés », ce qui signifie qu’un noyau ou un vidage plus grand a été capturé, puis un minidump a été créé à partir du vidage plus grand. La plupart des systèmes sont configurés pour les vidages automatiques/du noyau par défaut, et le système crée automatiquement un minidump au prochain démarrage après l’incident.

Le système transmet, dans le paramètre ReasonSpecificData , un pointeur vers une structure de KBUGCHECK_TRIAGE_DUMP_DATA qui contient des informations sur la vérification de bogue, ainsi qu’un paramètre OUT qui est utilisé par le pilote pour retourner son tableau de données initialisé et rempli.

Dans l’exemple suivant, le pilote configure un tableau de vidage de triage, puis inscrit une implémentation minimale du rappel. Le pilote utilise le tableau pour ajouter deux variables globales au minidump.

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

}

Seules les adresses non pagées en mode noyau doivent être utilisées avec cette méthode de rappel.

Une routine KbCallbackTriageDumpData est très restreinte dans les actions qu’elle peut effectuer. Pour plus d’informations, consultez Restrictions de routine de rappel de vérification des bogues.

La fonction MmIsAddressValid ne doit être utilisée qu’à partir d’une routine KbCallbackTriageDumpData après avoir vérifié que l’indicateur KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE est défini. Cet indicateur est actuellement toujours censé être défini, mais il est dangereux d’appeler la routine dans le cas où elle n’est pas définie sans synchronisation supplémentaire.