По роду своей деятельности (Windows Kernel) мне регулярно приходится разбирать дампы BSOD'ов. Не единичны случаи, когда у конечного пользователя успешно пишутся только Mini-дампы, в которых сохраняется только значение регистров процессора и стек падения. А другого средства отладки клиентской машины просто нет. Но что делать, если в стеке нет нашего драйвера, а заказчик настаивает, что падения начались после установки продукта и закончились после отключения драйвера этого продукта? В моем случае хорошим решением оказалось ведение небольшого журнала последних событий в циклическом буфере. Осталось только сохранить этот циклический буфер в дампе.
Под катом я расскажу, как из своего драйвера добавить в дамп данные. А затем извлечь их, используя pykd.
Начиная с 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:
//
// 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);
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
Собственно, весь скрипт чтения и форматирования данных:
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 написан так, что бы продемонстрировать работу в автономном режиме (вне отладчика), если ему указан аргумент командной строки. Каждый переданный аргумент скрипт трактует как путь в файлу дампа, загружает этот дамп и извлекает из него целевые данные. В частности, скриптом можно в пакетном режиме обработать файлы дампов:
~> 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)
kITerE Автор
10.03.2018 09:52+1Все эти моменты связаны с синтаксисом отладчика:
Почему после каждого имени драйвера написано 7
Это биты флагов команды отладчика !drvobj
Что такое dx в данном контексте?
Команда отладчика отображения C++ выражения — Display Debugger Object Model Expression
Что значит !Wdf01000?
Восклицательный знак используется как разделитель между именем модуля и именем символа из отладочной информации этого модуля: Symbol Syntax and Symbol Matching
MooNDeaR
Я не силен в драйверах винды, но хотелось бы стать умнее. Мне не очень понятны следующие моменты:
1) Почему после каждого имени драйвера написано 7
2) Что такое dx в данном контексте?
3) Что значит !Wdf01000?
kITerE Автор
Промахнулся, ответил ниже.