Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #133, ЯНВАРЬ 2010 г.

Борьба с синим змием. Краткий мануал по профилактике BSOD для начинающего драйверописателя

Александр Эккерт, aleksandr-ehkkert@rambler.ru


По статистике, 90% времени, которое программист тратит на разработку программы, составляют ее дебаг и отладка. И вправду, ведь накидать код несложно - сложно заставить его работать :). Вдвойне тяжело, если речь идет об отладке кода, который должен исполняться в ядре Windows. Поговорим о том, как облегчить жизнь начинающему драйверописателю и преодолеть трудности при отладке и дебаге кода ядра.

Отладка юзермодных программ, написанных на языках высокого уровня, не представляет особого затруднения для человека разумного. Существующие сегодня среды разработки значительно упрощают программисту жизнь - теперь отладка программного кода и выискивание ошибок стало делом вполне обычным и легко решаемым. А отладка исключений, скажем, в Visual Studio, вообще потрясает воображение. Правда, дебаггером еще надо научиться пользоваться, но это уже совсем другой вопрос.

Отладка — это часто тяжелая и утомительная задача. Способности кодера к отладке — по-видимому, важнейший фактор в обнаружении источника проблемы, но сложность отладки сильно зависит от используемого языка программирования и инструментов, в частности, отладчиков.

Но и эта сложность может стать неподъемной, если речь идет об отладке драйверов уровня ядра. Причина одна - если падение юзермодной программы приводит лишь к завершению работы самой программы, то за падением драйвера, как правило, следует BSOD.
Помню, когда мне только-только пришлось столкнуться с разработкой драйверов, BSOD'ы стали неотъемлемой частью моей кодерской жизни и к ним было очень тяжело привыкнуть. Но еще тяжелее было понять причину, вызвавшую падение драйвера – ведь даже когда я научился загружать крэшдамп в WinDBG, коды багчека на первых порах мне мало что говорили. Со временем кусочки разрозненной информации стали складываться в более-менее узнаваемую разумом мозаику и отладка стала для меня сродни детективному расследованию.

Blue Screen Of Death

Так радующий линуксоидов Blue Screen Of Death (BSOD) представляет собой всего лишь результат выполнения функции ядра KeBugCheckEx. Функция показывает пользователю, что ядро системы зарегистрировало внештатную ситуацию, которую она не в силах обработать.

При всем многообразии причин, ведущих к BSOD, для его отображения (и дополнительных действий, описание которых здесь опустим) в ядре разработан специальный механизм, который необходимо вызвать. Банальность ответа о причине BSOD заключается в том, что BSOD «наступает» всякий раз, как только вызывается функция ядра KeBugCheckEx, и, независимо от источника и кода ошибки, именно эта функция и вызывается драйверами режима ядра. При этом BSOD, вызванный UserMod'ными процессами - редкость, однако, если постараться, то, помурыжив функцию NtRaiseHardError(), можно сгенерировать «синий экран» и из пользовательского режима.

При вызове KeBugCheckEx ОС Windows создает и сбрасывает на диск (обычно в папку %SystemRoot%\Minidump) крэшдамп, созданный при аварийном выходе. Этот крэшдамп содержит в себе необходимую минимальную информацию, которая нужна, чтобы понять причину ошибки, вызвавшей BSOD. Обычно ее хватает.

Стандартный минидамп, который система создает по умолчанию, «весит» 64 Kb. При этом существует возможность заставить систему сбрасывать на диск «Дамп памяти ядра» и «Полный дамп памяти». В последнем случае на диск будет сброшено все содержимое физической памяти, занятое под юзермодные программы и компоненты ядра. Что именно выбирать - зависит от конкретных задач, иногда стандартного минидампа вполне хватает, чтобы выявить причины падения.

Вооружаемся по полной программе

WinDBG, продукт малоизвестной компании Microsoft - мощное оружие, предназначенное для отладки любого, как юзермодного, так и ядерного кода, а также анализа аварийных дампов. Последнюю версию WinDBG можно скачать здесь - http://msdl.microsoft.com/download. Ценность WinDBG в нашем случае проявляется именно в анализе крэшдампов. Об этой особенности WinDBG мы сейчас и поговорим.
Для полноценного анализа крэшдампов нужно скачать с сайта Microsoft пакет т.н. «символов» - набор файлов с расширением pdb, в которых содержится структурное описание системных функций, списков и констант (http://msdl.microsoft.com/download/symbols). Символы жизненно необходимы при отладке (почему? смотри картинку!).

На рисунке можно увидеть, что наличие символов делает отладку более читаемой. WinDBG, помимо отладки юзермодного кода, может также использоваться и в качестве ядерного отладчика реального времени (типа SoftIce или Syser). Как это сделать - ты легко можешь найти в Сети, скажу только, что нужно будет поднять виртуальную машину и пошаманить с настройками WinDBG. После можно начать отладку драйверов.

Итак, как бы нам ни хотелось, системный разработчик драйверов уровня ядра BSOD'ы будет видеть всегда. Однако, приложив небольшие усилия, на стадии кодинга и дебага можно свести число их появления к минимуму. Для этого оказалось достаточным в процессе кодинга соблюдать следующие три условия:

  1. использовать блоки __try{} - __except{};
  2. использовать при необходимости макрос ASSERT;
  3. пользоваться Driver Verifier.

Работа с исключениями в драйверах ничем не отличается от юзермода, поэтому я смело использую блоки __try{} __except{} в своем коде. Бывают, конечно, случаи, когда простой SEH-фрейм не сработает (например, исключения, генерируемые всякими функциями вроде MmProbeForRead обрабатываются только C-шным SEH-фреймом с его специфичными структурами). Есть множество типов исключений, которые вообще не обрабатываются SEH-фреймами, например, деление на 0, двойное исключение, исключения при обращении к неподкачиваемой памяти и т.д. Но это тебе для сведения, поскольку обычные SEH-фреймы часто выручают при обработке исключений.

Макрос ASSERT

Чтобы быть во всеоружии, позволь дать совет – очень полезно в коде драйвера проверять выполнение некоторых условий через макрос ASSERT. Также использование этого макроса - просто хороший тон в кодинге. Итак, этот макрос выполняется тогда, когда логическое выражение, которое он «охраняет», является ложным. Т.е. ASSERT(2+2 == 4) прервет выполнение программы в том случае, когда 2 + 2 не будет равным четырем. Мы пойдем еще дальше - попробуем сообразить свой макрос ASSERT, который поможет нам в отладке кода.

Пишем свой макрос ASSERT

#define MY_BUGCHECK_CODE 0xdeadbeaf
__inline void _myBugCheck(char * File, int Line)
{
KdPrint(("А-А-А-А! Help! Help! Execution
failed in file %s at line %d\n", File, Line));
KeBugCheckEx(MY_BUGCHECK_CODE , line, 0, 0, 0);
}

#define MyBugCheck() _myBugCheck(__FILE__, __LINE__)
#define MyAssertAlways(x) if (!(x)) { MyBugCheck(); }
#if DBG
#define MyAssert(x) MyAssertAlways(x)
#else
#define MyAssert(x)
#endif

Driver Verifier

В стандартный состав Windows DDK входит замечательная тулза, которую очень часто игнорируют начинающие разработчики драйверов уровня ядра - Driver Verifier. Она специально предназначена для того, чтобы выявлять ошибки, возможно, допущенные в драйвере. Если таковые будут обнаружены - Driver Verifier сгенерирует BSOD, в котором будут подробно показаны причины его возникновения. Driver Verifier поможет тебе выявить самые часто встречающиеся ошибки, возникающие при использовании памяти, обнаружении взаимоблокировок, слежении за уровнем IRQL и многое другое. Для проведения проверки достаточно выбрать драйвер из имеющегося списка или загрузить свой, после чего стартовать сам драйвер.

Как показывает практика...

Позволю себе в рамках статьи остановиться на двух багчеках, которые тебе будут встречаться в процессе разработки драйверов уровня ядра чаще всего. Они же являются самыми малоинформативными и раздражающими и без того измученную «Нарзаном» нервную систему программиста.

Первый багчек - это KERNEL_MODE_EXCEPTION_NOT_HANDLED. Даже его название ни о чем не говорит, намекая на «произошло что-то, что система не смогла обработать». Практика показывает, что если система чистая (на ней не установлены драйверы антивирей, файров и проактивок), то этот багчек в 99.99% процентах случаев прямо указывает на твои, прости за прямоту, кривые руки. Причиной, скорее всего, окажется неинициализированная переменная или же нулевой указатель.

Второй багчек - IRQL_NOT_LESS_OR_EQUAL. В стабильно работающей системе этот багчек практически не встречается. Его причина всегда одна - в твоем загруженном драйвере была осуществлена попытка обратиться к странице памяти на уровне DISPATCH_LEVEL, что и привело к падению.

Несмотря на явно видимые причины возникновения, баг часто встречается у начинающих драйверописателей из-за незнания одной вещи - все системные функции ядра выполняются на разных IRQL. Если внимательно почитать DDK, то можно увидеть, что в каждом описании системной функции в самом конце имеется объявление, которое прямо указывает, на каком уровне IRQL выполняется данная функция. Как устранить этот багчек? Грамотное решение: использовать WorkItem'ы - специально разработанный механизм, предназначенный именно для таких случаев. Он выполняет отложенное программистом действие, которое будет выполнено на приемлемом уровне IRQL. Кстати, если не ошибаюсь, в .NET Framework, начиная со второй версии, также появился класс WorkItem'ов, предназначенных для выполнения отложенных операций, хотя принципы этого класса работы в CLR, конечно же, отличаются от Win32 Native.

Используем WorkItem'ы в коде

if (KeGetCurrentIrql() != PASSIVE_LEVEL)
{
struct DelayedParameters *param =
(struct DelayedParameters *)malloc(sizeof(*param));
memset(param, 0, sizeof(*param));
ExInitializeWorkItem(&param->item,
DelayedFunction, param);
ExQueueWorkItem(&param->item, DelayedWorkQueue);
}

В данном коде, если текущий уровень IRQL будет выше, чем PASSIVE_LEVEL, выполнение функции DelayedFunction будет отложено на более поздний срок = т.е., когда ядро «поймает» для этого подходящий уровень IRQL. Развернутый пример использования WorkItems для работы в ядре ищи на диске.

Заключение

Приведенные здесь советы должны помочь тебе в успешном дебаггинге. Если ты всерьез увлекаешься разработкой драйверов уровня ядра, будь то системы защиты или же написание не совсем добропорядочных зверушек, хотя и редко, но на стадии тестирования будут встречаться ситуации, которые смогут поставить тебя в тупик. Это связано с одной единственной причиной: в работу ядра успешно вмешиваются разработчики всяческих проактивных защит, файрволов и антивирей. Логика их действий практически всегда скрыта от посторонних глаз (ну правильно, кто же будет палить алгоритм своих действий - ведь его «непубличность» и является гарантией успешности). Встраиваясь в эту систему, ты невольно ее нарушаешь, а ядро очень нервно реагирует на всякие попытки изменения существующего баланса.

Как результат - ты будешь лицезреть BSOD практически на ровном месте. Например, это касается разработки всяческих сетевых фильтров - существующие коммерческие файрволы, как правило, перехватывают системные вызовы как на NDIS уровне, так и уровне TDI-интерфейса. Однако, какова именно логика их перехвата - не всегда известно и значит заранее неизвестно, как будут вести себя твой драйвер и драйвер файрвола в одном стеке. К примеру, у меня была ситуация, что драйвер приложения «ESET Smart Security» epfwtdi.sys (фильтрующий TDI-трафик), вылетал с BSOD, когда мне приспичило ковыряться с содержимым сетевого IRP-пакета. Как результат - малоинформативный BSOD c кодом KERNEL_MODE_EXCEPTION_NOT_HANDLED_M.

В этой ситуации точно обозначить причину, послужившую причиной BSOD'а, с первого раза можно было лишь словами: «Она утонула». Если появились вопросы, пиши, обсудим. Удачного компилирования и да пребудет с тобой Сила!

DVD

На диске ты найдешь литературу, которая поможет тебе в отладке и дебаге твоего кода.

WWW

http://www.rsdn.ru/forum/asm - лучший форум для твоего времяпрепровождения.

INFO

Советую к обязательному прочтению «Приложение «А» к книге Криса Касперски «Записки исследователя компьютерных вирусов» под говорящим названием «Практические советы по восстановлению системы в боевых условиях».

Содержание