[Previous] [Next]

Драйвер отказывается работать?

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

Аппаратные проблемы

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

Причина упомянутых отклонений может быть и просто в недостаточной документированности поведения устройства. Разработчик аппаратуры после некоторых размышлений изменил конструкцию, но не исправил документацию и не сообщил об изменениях разработчику драйвера, возможно, посчитав их незначительными. Могут существовать малоизвестные или неисследованные ограничения на порядок следования команд — как в смысле последовательности, так и в смысле их временных диаграмм. Аппаратные прошивки (программы, загружаемые в обслуживаемые драйвером устройства) также могут содержать ошибки. Могут возникать сбои в шинных протоколах, причем из-за непериодических сбоев других устройств, подключенных к данной шине.

Программные проблемы

Поскольку драйвер работает в режиме ядра, для него весьма несложной задачей является "обрушение" всей операционной системы. Наиболее сложными для трассирования сценариями являются операции DMA, в которых некорректно установлены регистры отображения (mapping registers). Данные записываются устройством в случайные области памяти, и происходит сбой, виноватыми в котором кажутся совершенно другие подсистемы.

Утечка ресурсов

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

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

Торможение программных потоков

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

Самая простая ошибка состоит в том, что драйвер по какой-то причине не выполняет вызов IoCompleteRequest. В результате, присланный IRP пакет никогда не возвращается Диспетчеру ввода/вывода. Иногда не столь очевидна необходимость сделать вызов IoStartNextPacket (при использовании очередей IRP пакетов). Однако даже если не существует запросов, ожидающих обработки, драйвер должен выполнить этот вызов, поскольку только таким образом объект устройства будет "помечен" как простаивающий. Без этого вызова новые IRP пакеты будут помещаться в очередь ожидания обработки, так и не поступая в процедуру StartIo драйвера.

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

Аналогично, DMA драйверы могут остановиться в точке, где они сделали запрос на владение объектом адаптера. Драйверы, которые управляют сложными (составными) контроллерами, могут создать сходные проблемы в случае, если они не освобождают объект контроллера.

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

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

Проблема приоритетов времени выполнения

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

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

Показательным случаем неправильного использования приоритетов является вызов другого драйвера при помощи IoCallDriver, который формально может быть выполнен на уровнях IRQL APC_LEVEL и DISPATCH_LEVEL. Повысив текущий приоритет потока перед вызовом IoCallDriver до значения, например, DISPATCH_LEVEL, драйвер, скорее всего, организует блокировку: если вызываемый драйвер должен работать на уровне PASSIVE_LEVEL, то он не может сделать нужные ему вызовы, пока работает вызвавший его поток, который ждет возвращения управления из IoCallDriver. Система приходит в неработоспособное состояние ("замерзает").

Отслеживание ошибок

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

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