[Previous] [Next]

Общие приемы отладки

Зачастую проблемы исправления ошибок можно легко устранить при наличии достаточного объема диагностических сообщений. Ниже приводятся приемы, основанные как раз на этом.

Установка фиксированных точек прерывания

При использовании интерактивных отладчиков не найдется много причин устанавливать фиксированные точки прерывания внутри кода драйвера (hard breakpoints). Тем не менее, это можно сделать, если воспользоваться двумя функциями:

VOID DbgBreakPoint();
VOID KdBreakPoint(); 

Вызов KdBreakPoint представляет из себя макроопределение, которое определяет условную компиляцию с целью выполнить вызов DbgBreakPoint. Это макроопределение не выполняет данного вызова, если выполнена релизная (без отладочных инструкций) сборка драйвера (free build).

Будьте внимательны: Windows дает фатальный сбой с сообщением KMODE_EXCEPTION_NOT_HANDLED в том случае, если драйвер применил фиксированную точку прерывания (через упомянутые вызовы), но в этот момент клиент отладки (см. рисунок 13.1) был недоступен. Если драйвер добрался до такой точки прерывания, и не оказалось отладчика, подключенного к последовательному порту, то драйвер "виснет". В некоторых случаях, ситуация может быть выправлена запуском отладчика на хост-компьютере.

Промежуточный вывод на экран

Делать сообщения из недр отлаживаемого кода при помощи функции printf — это метод с давними и славными традициями. Его иногда называют "генерацией промежуточного вывода". Фактически, нет таких ошибок, которые нельзя было бы найти, если применить такой метод в достаточном (!) объеме.

И хотя данный метод не так ныне распространен, как отладка с использованием точен прерывания в интерактивных отладчиках, тем не менее, он может быть очень полезен при поисках сбоев, связанных с временными затруднениями драйвера (например, ошибках или сбоях в последовательности событий, связанных с устройством). Для генерации промежуточных сообщений используются две функции DbgPrint и KdPrint. Обе функции посылают форматированные строки, созданные на целевом компьютере, отладчику WinDbg, работающему на хост-компьютере.

Как было сказано ранее, собирать отладочные сообщения можно и при помощи DebugView.

Вызов KdPrint на самом деле является макроопределением и превращается в пустышку (невыполняемый участок) в релизной сборке драйвера (free build).

Сохранение отладочного кода в исходном тексте драйвера

Хорошим приемом в деле доводки драйвера является сохранение отладочных фрагментов кода на их месте, даже если для потребителя была собрана релизная версия (free версия без отладочных инструкций). Все это несложно сделать при помощи директив условной компиляции, и при возврате к выявлению более глубоких ошибок и повторному тестированию этот прием окажет отличную поддержку.

Утилита BUILD использует символ времени компиляции DBG, который может быть использован при составлении условно компилируемых фрагментов. В отладочной версии (checked build) этому символу присвоено значение 1, в версии free значение DBG равно 0. При внесении отладочного кода в драйвер следует ограничивать его рамками директив условной компиляции типа #if DGB и #endif.

Перехват некорректных условий

Разработчик драйвера, как и всякий разработчик, создает свой продукт, отталкиваясь от некоторых изначальных допущений. Важно лишь, чтобы они не были беспочвенными или чрезмерно оптимистическими. В том случае, если возникают некоторые сомнения в условиях работы некоторых важных участков кода драйвера, лучше подстраховаться. Например, предполагая, что некий фрагмент кода, будет вызван только на определенном уровне IRQL, автор закладывает потенциальную возможность будущих сбоев (если данные ожидания не будут оправданы).

Для того чтобы перехватить эти непредусмотренные отклонения, следует применять несложный прием: такие допущения должны проверяться во время выполнения, хотя бы в отладочной сборке драйвера. Макроопределения ASSERT и ASSERTMSG помогут решить эту задачу:

ASSERT( Expression );
ASSERTMSG( Message, Expression );

В случае если выражение Expression, рассмотренное как логическое, будет равно FALSE, макроопределение ASSERT произведет запись сообщения в командное окно WinDbg, подключенного на хост-компьютере для проведения отладки. Это сообщение содержит исходный код выражения, значение которого оказалось равным FALSE, имя файла (в котором был исходный текст данного фрагмента) и номер строки, где было вызвано макроопределение ASSERT. Затем предлагается сделать выбор, сделать ли прерывание в месте данного макроопределения ASSERT, игнорируя причину остановки, либо прервать процесс или поток, в котором "сработал" ASSERT (что, впрочем, совершенно аналогично применению ASSERT в Visual Studio).

Макроопределение ASSERTMSG демонстрирует точно такое же поведение, с той лишь разницей, что в сообщение включается содержимое Message (являющееся строкой). В отличие от debug print функций, описанных ранее, ASSERTMSG не допускает форматного вывода данных в стиле printf.

Следует также обратить внимание на то, что оба макроопределения используют условную компиляцию, вследствие чего в версии "free" оно совершенно исчезает. Следовательно, совершенно недопустимо помещать какой-либо исполняемый код внутрь этих макроопределений — в версии "free" они исчезнут вовсе.

Кроме того, в основе упомянутых выше макроопределений лежит использование функции RtlAssert, которая во "free" версиях Windows 2000/XP/2003 превращается в пустышку. Следовательно, чтобы наблюдать последствия ошибок в макроопределениях ASSERT и ASSERTMSG следует тестировать драйвер под отладочной (checked) версией Windows.

Использование диагностических callback-функций

Диагностические обратные вызовы являются процедурами драйвера, который тот предлагает для вызова в моменты начала фатальных сбоев системы. Эти процедуры предлагают удобный способ захвата отладочной информации во время сбоя. Кроме того, они могут быть использованы для того, чтобы перевести аппаратуру в определенное состояние перед тем, как система окончательно "рухнет". Для использования диагностических обратных вызовов нужно выполнить следующее.

  1. В процедуре DriverEntry необходимо выполнить вызов функции KeInitializeCallbackRecord для выполнения настройки структуры KBUGCHECK_CALLBACK_RECORD. Место для хранения этой закрытой структуры должно быть выделено в нестраничном пуле (причем должно содержаться в не прикосновенности до момента ее де-регистрации, выполняемой при помощи функции KeDeregisterBugCheckCallback в процедуре Unload данного драйвера).
  2. В DriverEntry необходимо выполнить вызов KeRegisterBugCheckCallback для подключения драйвера к механизму уведомления об ошибочных ситуациях. Аргументами этого вызова будет указатель на структуру типа KBUGCHECK_CALLBACK_RECORD, адрес предоставляемой драйвером callback-функции, адрес и размер предоставляемого драйвером буфера и строка, которая будет использована для идентификации буфера. Место для буфера должно быть выделено в нестраничном пуле (причем также должно содержаться в неприкосновенности до момента вызова функции KeDeregisterBugCheckCallback).
  3. В том случае, если была обнаружена ошибка, система выполняет вызов зарегистрированной в п. 2 callback-функции, которой будет передан адрес буфера и его размер. Работа вызванной функции состоит в том, что она должна заполнить предоставленный буфер информацией, которую сочтет необходимой, и которая не смогла бы иным образом найти свое отражение в crash dump файле, например, внутреннее содержимое регистров обслуживаемого устройства.
  4. При анализе crash dump файла при помощи WinDbg эту информацию можно вывести на экран, если воспользоваться командой !bugdump.

Существуют некоторые ограничения на действия, разрешенные регистрируемой callback-функции. Во время работы она не может получать какие-либо системные ресурсы (например, память). Она также не должна использовать объекты синхронизации. Однако разрешено делать вызовы процедур ядра, которые не нарушают указанных ограничений, и вызовы процедур HAL уровня, например, для того, чтобы получить доступ к регистрам устройства.

Обнаружение утечек памяти

Утечки памяти являются самой жестокой патологией программных кодов. Драйверы, которые получили для себя пространство в одном из пулов памяти, а затем забыли его освободить, могут привести к серьезной деградации системы и, через некоторое время, к ее фатальному сбою. Использование тегового механизма выделения памяти может оказать существенную помощь в обнаружении утечек. Как это работает?

  1. Необходимо заменить вызовы ExAllocatePool на вызовы ExAllocatePoolWithTag. Дополнительный аргумент, представляющий собой четырехбайтную величину (4 символа), используется для того, чтобы пометить вновь выделенный блок этим значением (тегом).
  2. Необходимо запустить драйвер под отладочной версией (checked build) Windows. Поддержка трассировки страниц пула является дорогостоящим "удовольствием", поэтому доступно оно только в отладочных версиях Windows.
  3. Когда выполняется анализ crash dump файла или в ситуации, когда достигну та точка прерывания при отладке "живого" драйвера, следует воспользоваться командами !poolused или !poolfind для того, чтобы ознакомиться с состоянием пулов памяти. Эти команды сортируют области пулов по значению тегов и высвечивают различные статистические данные об использовании памяти. Следует помнить, что в отсутствии файлов отладочных символов указанные команды WinDbg не работают.

Легкий способ повсеместной замены вызовов ExAllocatePool на вызовы ExAllocatePoolEx состоит в том, чтобы изначально использовать фрагменты условной компиляции, например:

#if DBG==1
	#define ALLOCATE_POOL(type,size) \
	        ExAllocatePoolWithTag((type),(size),'1234')
#else
	#define ALLOCATE_POOL(type,size) ExAllocatePool((type),(size))
#endif  

Аргумент тега в вызове ExAllocatePoolWithTag состоит из четырех букв (в верхнем регистре), которые на экране отладчика предстанут в обратном порядке, то есть '4321'.

В данном примере для всех операций выделения памяти при помощи ExAllocatePoolWithTag использован один и тот же тег. В некоторых ситуациях может оказаться целесообразным использование разных значений тега для выделения памяти под структуры данных разного типа, или для выделения памяти в разных фрагментах драйвера. Эти приемы помогут эффективнее идентифицировать утечку памяти.

Поставляемая в составе DDK утилита PoolTag позволяет наблюдать теговое выделение памяти и без привлечения отладчика WinDbg. Эта программа непрерывно выводит на экран обновляемые данные о страничных тегах.

Установка параметров загрузки в файле boot.ini

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

Его можно редактировать при помощи редактора notepad (если убрать атрибут "только для чтения", например, в программе WinCommander-TotalCommander), но более правильно это делать в окне системного апплета, показанного на рисунке 13.3.

В документации DDK несложно отыскать описание задаваемых параметров загрузки — по ключу "boot.ini" - "Parameters for boot.ini". Помимо параметра /sos, подробно рассмотренного в приложении Б, разработчику драйверов может быть также полезен параметр /maxmem=Xxx, который ограничивает размер используемой физической памяти, что позволит протестировать систему и драйвер в условиях недостатка физической памяти.