По роду своей деятельности (Windows Kernel) мне регулярно приходится разбирать дампы BSOD'ов. Не единичны случаи, когда у конечного пользователя успешно пишутся только Mini-дампы, в которых сохраняется только значение регистров процессора и стек падения. А другого средства отладки клиентской машины просто нет. Но что делать, если в стеке нет нашего драйвера, а заказчик настаивает, что падения начались после установки продукта и закончились после отключения драйвера этого продукта? В моем случае хорошим решением оказалось ведение небольшого журнала последних событий в циклическом буфере. Осталось только сохранить этот циклический буфер в дампе.


Под катом я расскажу, как из своего драйвера добавить в дамп данные. А затем извлечь их, используя pykd.
pykd


Начиная с Windows XP SP1 и 2003 Server система предоставляет возможность драйверам добавлять в дамп падения ядра собственные данные: Secondary Callback Data. Для того, что бы система запросила эти данные у драйвера, необходимо зарегистрировать свою callback-функцию вызовом KeRegisterBugCheckReasonCallback. При регистрации нужно указать адрес функции, которая будет вызваться при падении ядра и, в нашем случае (BugCheckSecondaryDumpDataCallback), предоставлять данные, которыми нужно дополнить системный дамп. Указанная callback-функция будет вызвана дважды:


  1. Первый раз система вызывает драйвер, что бы определить размер буфера. Уже на этом этапе во входных данных ОС указывает максимальный размер данных (KBUGCHECK_SECONDARY_DUMP_DATA.MaximumAllowed), который можно сохранить в дапме. Этот размер зависит от типа системного дампа, который будет сгенерирован. В Windows XP при установленной настройке записи Mini-дампа система предоставляет 4096 байт (одну страницу памяти).
  2. Второй раз система запрашивает сами данные.

Из-за того, что callback-функция вызывается в момент падения ядра операционной системы, на код этой функции накладываются серьезные ограничения: не использовать выделение памяти (все выделяется заранее), не обращаться к Paged-памяти (подкачка страниц невозможна), не использовать механизмы синхронизации (риск взаимоблокировок). Более подробные детали можно найти в статье MSDN Writing a Bug Check Callback Routine.


Достаточно странно, но примера использования функции KeRegisterBugCheckReasonCallback, нет в коллекции примеров к WDK. Зато пример обнаружился в открытых Microsoft'ом исходниках KMDF (Kernel-Mode Driver Framework) — fxbugcheckcallback.cpp:


Регистрация обработчика: куски функции FxInitializeBugCheckDriverInfo
    //
    // The KeRegisterBugCheckReasonCallback exists for xp sp1 and above. So
    // check whether this function is defined on the current OS and register
    // for the bugcheck callback only if this function is defined.
    //
    RtlInitUnicodeString(&funcName, L"KeRegisterBugCheckReasonCallback");
    funcPtr = (PFN_KE_REGISTER_BUGCHECK_REASON_CALLBACK)
        MmGetSystemRoutineAddress(&funcName);

    if (NULL == funcPtr) {
        goto Done;
    }

    //
    // Initialize the callback record.
    //
    KeInitializeCallbackRecord(callbackRecord);

    //
    // Register the bugcheck callback.
    //
    funcPtr(callbackRecord,
            FxpLibraryBugCheckCallback,
            KbCallbackSecondaryDumpData,
            (PUCHAR)WdfLdrType);

    ASSERT(callbackRecord->CallbackRoutine != NULL);

Реализация обработчика: функция FxpLibraryBugCheckCallback
VOID
FxpLibraryBugCheckCallback(
    __in    KBUGCHECK_CALLBACK_REASON Reason,
    __in    PKBUGCHECK_REASON_CALLBACK_RECORD /* Record */,
    __inout PVOID ReasonSpecificData,
    __in    ULONG ReasonSpecificLength
    )

/*++

Routine Description:

    Global (framework-library) BugCheck callback routine for WDF

Arguments:

    Reason               - Must be KbCallbackSecondaryData
    Record               - Supplies the bugcheck record previously registered
    ReasonSpecificData   - Pointer to KBUGCHECK_SECONDARY_DUMP_DATA
    ReasonSpecificLength - Sizeof(ReasonSpecificData)

Return Value:

    None

Notes:
    When a bugcheck happens the kernel bugcheck processor will make two passes
    of all registered BugCheckCallbackRecord routines.  The first pass, called
    the "sizing pass" essentially queries all the callbacks to collect the
    total size of the secondary dump data. In the second pass the actual data
    is captured to the dump.

--*/

{
    PKBUGCHECK_SECONDARY_DUMP_DATA  dumpData;
    ULONG                           dumpSize;

    UNREFERENCED_PARAMETER(Reason);
    UNREFERENCED_PARAMETER(ReasonSpecificLength);

    ASSERT(ReasonSpecificLength >= sizeof(KBUGCHECK_SECONDARY_DUMP_DATA));
    ASSERT(Reason == KbCallbackSecondaryDumpData);

    dumpData = (PKBUGCHECK_SECONDARY_DUMP_DATA) ReasonSpecificData;
    dumpSize = FxLibraryGlobals.BugCheckDriverInfoIndex * 
                sizeof(FX_DUMP_DRIVER_INFO_ENTRY);
    //
    // See if the bugcheck driver info is more than can fit in the dump
    //
    if (dumpData->MaximumAllowed < dumpSize) {
        dumpSize = EXP_ALIGN_DOWN_ON_BOUNDARY( 
                        dumpData->MaximumAllowed,
                        sizeof(FX_DUMP_DRIVER_INFO_ENTRY));
    }

    if (0 == dumpSize) {
        goto Done;
    }

    //
    // Ok, provide the info about the bugcheck data.
    //
    dumpData->OutBuffer = FxLibraryGlobals.BugCheckDriverInfo;
    dumpData->OutBufferLength  = dumpSize;
    dumpData->Guid = WdfDumpGuid2;

Done:;
}

В качестве демонстрации, именно эти данные и будем извлекать из дампа. Данными является массив структур FX_DUMP_DRIVER_INFO_ENTRY, каждая структура имеет в своих полях версию и имя драйвера. Ключом к данным в дампе выступает указанный при записи GUID, в нашем случае это {F87E4A4C-C5A1-4d2f-BFF0-D5DE63A5E4C3}.


Для просмотра сохраненных в дампе данных есть команда отладчика .enumtag. В результате выполнения команды мы увидим сырой дамп памяти. Вот пример интересующих нас данных:


1: kd> .enumtag
{65755A40-F146-43EA-8C9136B85728FD35} - 0x0 bytes
<...>
{F87E4A4C-C5A1-4D2F-BFF0D5DE63A5E4C3} - 0x508 bytes
  00 00 00 00 00 00 00 00 01 00 00 00 0D 00 00 00  ................
  00 00 00 00 57 64 66 30 31 30 30 30 00 00 00 00  ....Wdf01000....
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00 00 00 00 00 00 00 00 90 AC 55 00 00 E0 FF FF  ..........U.....
  01 00 00 00 0B 00 00 00 00 00 00 00 61 63 70 69  ............acpi
  65 78 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ex..............
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  30 81 F6 00 00 E0 FF FF 01 00 00 00 0B 00 00 00  0...............
  00 00 00 00 6D 73 69 73 61 64 72 76 00 00 00 00  ....msisadrv....
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00 00 00 00 00 00 00 00 A0 D3 EB 00 00 E0 FF FF  ................
  01 00 00 00 0B 00 00 00 00 00 00 00 76 64 72 76  ............vdrv
<...>

Работать с таким форматом можно, но не удобно. Microsoft предлагает написать свое расширение к отладчику:


To use this data in a more practical way, it is recommended that you write your own debugger extension.

Но я являюсь одним из разработчиков проекта pykd. Модуль pykd может выступать расширением отладчика, позволяющим использовать Python для автоматизации отладки. Поэтому я покажу как с его помощью извлечь и визуализировать данные. Сразу оговорюсь, что перечисление и извлечение Secondary Callback Data было добавлено в последнем (на момент написания статьи) релизе — 0.3.3.3. Поэтому, если у вас уже установлена более старая версия, нужно обновить pykd (Last Release).


В качестве тестового дампа я буду использовать файл, используемый для unit-тестов pykd — win8_x64_mem.cab


Собственно, весь скрипт чтения и форматирования данных:


kmdf_tagged.py
import os
import sys
import pykd
import struct

def print_command(command):
    if pykd.getDebugOptions() & pykd.debugOptions.PreferDml:
        pykd.dprint( '<exec cmd="{}">{}</exec>'.format(command, command),
                     dml = True )
    else:
        pykd.dprint( command )

def parse():
    buff = bytearray( pykd.loadTaggedBuffer("F87E4A4C-C5A1-4d2f-BFF0-D5DE63A5E4C3") )
    entry_type = pykd.typeInfo("Wdf01000!_FX_DUMP_DRIVER_INFO_ENTRY")

    _struct = struct.Struct( "<{}III".format("Q" if pykd.is64bitSystem() else "L") )

    name_offset = entry_type.fieldOffset("DriverName")
    name_size = entry_type.DriverName.size()

    entry_size = entry_type.size()

    if len(buff) % entry_size:
        raise RuntimeError( "The buffer size ({}) is not a multiple of entry size ({})".format(len(buff), entry_size) )

    print("[FxLibraryGlobals.BugCheckDriverInfo]")

    while len(buff):
        ptr, mj, mn, build = _struct.unpack_from(buff)

        name = str(buff[name_offset : name_offset + name_size]).strip("\0")

        command = "!drvobj {} 7".format(name)
        print_command( command )

        pykd.dprint( " " * (24 - len(name)) )

        pykd.dprint( " {:12} ".format("({}.{}.{})".format(mj, mn, build)) )
        if ptr:
            command = "dx ((Wdf01000!{})0x{:x})".format(entry_type.FxDriverGlobals.name(), ptr)
            print_command( command )

        pykd.dprintln( "" )

        buff = buff[entry_size:]

if __name__ == "__main__":
    if len(sys.argv) == 1:
        parse()
    else:
        for file_name in sys.argv[1:]:
            print(file_name)
            dump_id = pykd.loadDump(file_name)
            parse()
            pykd.closeDump(dump_id)

Содержимое скипта, на мой взгляд, достаточно простое (функция parse):


  • Вызовом pykd.loadTaggedBuffer считываем содержимое сохраненных данных, указывая в качестве ключа-аргумента GUID в виде строки.
  • Используя информацию из отладочных символов (создание экземпляра объекта pykd.typeInfo), получаем смещение до имени драйвера (name_offset), размер буфера имени драйвера (name_size) и размер одной структуры FX_DUMP_DRIVER_INFO_ENTRY (entry_size).
  • Для каждой структуры FX_DUMP_DRIVER_INFO_ENTRY в вычитанном буфере с помощью стандартного python-модуля struct распаковываем поля структуры, содержащие указатель на глобальный объект драйвера и версию. А затем получаем имя драйвера, преобразуя его в строку, отбрасывая 0-символы. И печатаем полученные данные, используя DML, если текущее окружение позволяет использовать этот язык разметки (функция print_command).

Исполняем скрипт в отладчике WinDbg:
windbg_output


Если посмотреть на содержимое скрипта после функции parse, то можно заметить, что скрипт может принимать аргумент. Скрипт kmdf_tagged.py написан так, что бы продемонстрировать работу в автономном режиме (вне отладчика), если ему указан аргумент командной строки. Каждый переданный аргумент скрипт трактует как путь в файлу дампа, загружает этот дамп и извлекает из него целевые данные. В частности, скриптом можно в пакетном режиме обработать файлы дампов:


~> for /R .\dumps %i in (*.*) do @python.exe kmdf_tagged.py %i
~\dumps\win8_x64_mem.cab
[FxLibraryGlobals.BugCheckDriverInfo]
!drvobj Wdf01000 7                 (1.13.0)
!drvobj acpiex 7                   (1.11.0)     dx ((Wdf01000!_FX_DRIVER_GLOBALS*)0xffffe0000055ac90)
<...>
!drvobj PEAUTH 7                   (1.7.6001)   dx ((Wdf01000!_FX_DRIVER_GLOBALS*)0xffffe000022081c0)
~\dumps\win8_x64_mem2.cab
[FxLibraryGlobals.BugCheckDriverInfo]
!drvobj Wdf01000 7                 (1.13.0)
!drvobj acpiex 7                   (1.11.0)     dx ((Wdf01000!_FX_DRIVER_GLOBALS*)0xffffe0000055ac90)
<...>
!drvobj PEAUTH 7                   (1.7.6001)   dx ((Wdf01000!_FX_DRIVER_GLOBALS*)0xffffe000022081c0)

Надеюсь, что мой опыт (и содержимое этой статьи) будет кому-то полезным. А количество BSOD'ов, причина которых остается загадкой, будет стремиться к 0.

Комментарии (3)


  1. MooNDeaR
    10.03.2018 09:33

    Я не силен в драйверах винды, но хотелось бы стать умнее. Мне не очень понятны следующие моменты:

    1) Почему после каждого имени драйвера написано 7
    2) Что такое dx в данном контексте?
    3) Что значит !Wdf01000?


    1. kITerE Автор
      10.03.2018 09:52

      Промахнулся, ответил ниже.


  1. kITerE Автор
    10.03.2018 09:52
    +1

    Все эти моменты связаны с синтаксисом отладчика:


    Почему после каждого имени драйвера написано 7

    Это биты флагов команды отладчика !drvobj


    Что такое dx в данном контексте?

    Команда отладчика отображения C++ выражения — Display Debugger Object Model Expression


    Что значит !Wdf01000?

    Восклицательный знак используется как разделитель между именем модуля и именем символа из отладочной информации этого модуля: Symbol Syntax and Symbol Matching