Это копия моей статьи, опубликованной на Хабрахабр: Собственные данные в системном дампе падения Windows.
По роду своей деятельности (Windows Kernel) мне регулярно приходится разбирать дампы BSOD'ов. Не единичны случаи, когда у конечного пользователя успешно пишутся только Mini-дампы, в которых сохраняется только значение регистров процессора и стек падения. А другого средства отладки клиентской машины просто нет. Но что делать, если в стеке нет нашего драйвера, а заказчик настаивает, что падения начались после установки продукта и закончились после отключения драйвера этого продукта? В моем случае хорошим решением оказалось ведение небольшого журнала последних событий в циклическом буфере. Осталось только сохранить этот циклический буфер в дампе.
Начиная с Windows XP SP1 и 2003 Server система предоставляет возможность драйверам добавлять в дамп падения ядра собственные данные: Secondary Callback Data. Для того, что бы система запросила эти данные у драйвера, необходимо зарегистрировать свою callback-функцию вызовом KeRegisterBugCheckReasonCallback. При регистрации нужно указать адрес функции, которая будет вызваться при падении ядра и, в нашем случае ( BugCheckSecondaryDumpDataCallback ), предоставлять данные, которыми нужно дополнить системный дамп. Указанная callback-функция будет вызвана дважды:
- Первый раз система вызывает драйвер, что бы определить размер буфера. Уже на этом этапе во входных данных ОС указывает максимальный размер данных ( KBUGCHECK_SECONDARY_DUMP_DATA.MaximumAllowed ), который можно сохранить в дапме. Этот размер зависит от типа системного дампа, который будет сгенерирован. В Windows XP при установленной настройке записи Mini-дампа система предоставляет 4096 байт (одну страницу памяти).
- Второй раз система запрашивает сами данные.
Из-за того, что 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. В результате выполнения команды мы увидим сырой дамп памяти. Вот пример интересующих нас данных:
Работать с таким форматом можно, но не удобно. Microsoft предлагает написать свое расширение к отладчику:
Но я являюсь одним из разработчиков проекта pykd. Модуль pykd может выступать расширением отладчика, позволяющим использовать Python для автоматизации отладки. Поэтому я покажу как с его помощью извлечь и визуализировать данные. Сразу оговорюсь, что перечисление и извлечение Secondary Callback Data было добавлено в последнем (на момент написания статьи) релизе - 0.3.3.3. Поэтому, если у вас уже установлена более старая версия, нужно обновить pykd (Last Release).
В качестве тестового дампа я буду использовать файл, используемый для unit-тестов pykd - win8_x64_mem.cab.
Собственно, весь скрипт чтения и форматирования данных:
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:
Если посмотреть на содержимое скрипта после функции parse, то можно заметить, что скрипт может принимать аргумент. Скрипт kmdf_tagged.py написан так, что бы продемонстрировать работу в автономном режиме (вне отладчика), если ему указан аргумент командной строки. Каждый переданный аргумент скрипт трактует как путь в файлу дампа, загружает этот дамп и извлекает из него целевые данные. В частности, скриптом можно в пакетном режиме обработать файлы дампов:
Надеюсь, что мой опыт (и содержимое этой статьи) будет кому-то полезным. А количество BSOD'ов, причина которых остается загадкой, будет стремиться к 0.
ΞρεΤΙκ