Марк Русинович доктор философии в области вычислительной техники Университета Карнеги-Меллон и один из соавторов многих популярных утилит для Windows NT, включая NTFSDOS, Filemon, Regmon и Recover. С ним можно связаться по электронной почте по адресу: mar
Как избежать ошибок при написании драйверов устройств в процессе их создания

В первой из трех статей, посвященных вопросам повышения надежности Windows 2000, я представил обзор новых механизмов восстановления операционной системы; во второй части основное внимание было уделено описанию новой политики наблюдения за системными файлами, в соответствии с которой любая модификация таких файлов требовала бы особого «разрешения». В последней части цикла статей я расскажу о двух новых технологиях, Write-Protected System Code и Driver Verifier, следуя которым разработчики драйверов устройств для Windows 2000 смогут избежать наиболее распространенных ошибок в процессе написания программ.

Write-Protected System Code

Приложения, ядро операционной системы, а также драйверы устройств работают с указателями (pointers). Указатель — это ссылка на область памяти, выделенную системой программе или драйверу для работы. Ошибки определенного типа при написании программ (драйверов) могут вызвать искажение указателя, которому программа (драйвер) присвоит бессмысленное с точки зрения адресации памяти значение. Это, в свою очередь, может привести к попытке передачи управления по адресу, на самом деле не содержащему программного кода. Существует также два тесно связанных друг с другом типа ошибок, которые служат причиной нарушения адресации при работе с буферами памяти. Эти ошибки возникают следующим образом: программа (драйвер) исходит из «предположения», что значение указателя находится в пределах области памяти, выделенной под буфер данных, тогда как на самом деле указатель только что вышел за пределы буфера (ошибка overrun) или же, напротив, еще не достиг его начального адреса (ошибка underrun).

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

В предыдущих версиях Windows NT, в том числе NT 4.0 и NT 3.51, ошибку в работе драйвера устройства, которая бы провоцировала операцию записи в область ядра, кода или данных чужой программы, обнаружить было непросто. В конечном счете, система почти всегда выявляла ошибку, когда поврежденный драйвер или ядро системы пытались выполнить непредусмотренную операцию, но провести обратную трассировку для установления причины сбоя фактически оказывалось невозможно. В Windows 2000 предусмотрена защита системного кода от записи (write-protected system code), что позволяет системным администраторам и разработчикам драйверов своевременно обнаруживать недопустимые изменения указателя. Если искаженный указатель ссылается в область программного кода драйвера или ядра системы, попытка несанкционированной записи фиксируется Windows 2000 Memory Manager.

В этом случае сразу же становится известен «виновник» события. Тем самым уменьшается вероятность возникновения ситуаций, когда система не в состоянии немедленно установить факт искажения указателя, что в дальнейшем может привести к записи в области памяти, занятые ядром или чужими программами. После того как система идентифицирует неисправный драйвер устройства, администратор может либо удалить драйвер, либо заменить его.

Компонент защиты системного кода от записи в Windows 2000 выполнен в виде набора подпрограмм, которые загружают образ ядра системы и файлы драйверов устройств в память. Функция MiLoadSystemImage идентифицирует исполняемый код на основе загруженного файла и, используя аппаратные особенности системы управления памятью, назначает выделенной секции памяти атрибут защиты от записи. В ряде случаев разработчикам программ может потребоваться отключить компонент защиты системного кода от записи. Для этого значение параметра реестра HKEY_ LOCAL_MACHINESYSTEMCurrentControlSet ControlMemoryManagerEnforceWriteProtection устанавливается равным 0.

Driver Verifier

Вероятно, лучшим средством повышения надежности Windows 2000 следует считать Driver Verifier (или просто Verifier). Как и компонент защиты системного кода от записи, Verifier является неотъемлемой частью ядра Windows 2000 и на случай возникновения ошибок в работе драйверов устройств имеет свои механизмы их обнаружения. Но если с помощью технологии Write-Protected System Memory можно обезопасить область размещения программного кода драйверов и ядра от модификаций, вызванных искаженными указателями, то технология Driver Verifier распространяет защиту памяти и на области данных. Если драйвер устройства с искаженным указателем ссылается за пределы (ошибка overrun) своего буфера данных и, как следствие, вызывает разрушение чужих данных, то для Windows NT 4.0 это может означать непредсказуемо долгое наличие скрытой ошибки, что не позволяет локализовать проблемный драйвер. Механизм защиты Verifier, как правило, обнаруживает подобные ситуации немедленно. Кроме того, Verifier реагирует и на другие общие ошибки, связанные с программированием драйверов устройств.

Verifier имеет и находится в \%systemroot%system32. Через графический интерфейс можно сконфигурировать параметры и просмотреть статистику работы в ядре системы. Настройка свойств Verifier производится в различных закладках. Чтобы указать, какой драйвер устройства верифицировать и какой тип верификации следует выполнить, нужно воспользоваться закладкой Modify Settings. При этом сделанные в закладке Modify Settings установки отражаются в разделе реестра HKEY_LOCAL_MACHINESYSTEMCurrentControlSet ControlMemoryManagement и содержат параметры VerifyDriverLevel (тип данных REG_ DWORD) и VerifyDrivers (тип данных — REG_SZ). Ядро Windows 2000 интерпретирует VerifyDriverLevel как маску, каждая позиция которой представляет собой один из типов верификации, перечисленных в правой части закладки Modify Settings. Когда указывается название драйвера, подлежащего верификации, его имя сохраняется в VerifyDrivers. Исключение составляет ситуация, когда проводится верификация всех драйверов. В этом случае параметру VerifyDrivers будет присвоен групповой символ звездочка (*).

После внесения изменений в установки Verifier систему требуется перезагрузить. На раннем этапе загрузки Windows 2000 Memory Manager считывает значения реестра, относящиеся к Verifier, чтобы определить, какие драйверы должны верифицироваться в процессе работы и какие параметры верификации следует активизировать. В дальнейшем, если выбран хотя бы один драйвер для проведения верификации, ядро системы в процессе загрузки будет сопоставлять имя загружаемого в данный момент драйвера и список имен верифицируемых драйверов. Для каждого драйвера из списка ядро вызовет функцию MiApplyDriverVerifier. Ее задача заключается в том, чтобы заменить любое обращение к ядру и подсистеме Win32 (Win32K.sys) ссылкой на соответствующий эквивалент Verifier (общее количество функций ядра системы и подсистемы Win32 около 40).

Функция ядра, к которой обращается драйвер устройства, и ссылка, формируемая в качестве переназначения вызовом MiApplyDriverVerifier, определяют вид проверок, проводимых Verifier. Например, Verifier перехватывает все функции, работающие на выделение и освобождение буферов памяти. Если, скажем, драйвер устройства, находящийся «под опекой» Verifier, использует для выделения памяти функцию ядра ExAllocatePool, то вместо нее будет вызываться функция VerifierAllocatePool.

Одна из наиболее распространенных ошибок, которые встречаются в драйверах устройств, проявляется в тот момент, когда драйвер обращается к данным, расположенным в страничной (pageable) памяти. Другой случай проявления той же самой ошибки — обращение драйвера к коду программы, находящейся в выгружаемой памяти, в тот момент, когда процессор занят обработкой запросов на прерывание (IRQL).

Когда код программы или данные расположены в виртуальной памяти, Windows 2000 может переместить страницу памяти, в которой они находятся, на диск, в файл подкачки (paging file). Вспомним, что Windows 2000 использует механизм IRQL для маскирования программных или аппаратных прерываний. Пока Windows 2000 обрабатывает аппаратное прерывание, вызванное необходимостью подкачки страниц с диска (page fault), т. е. когда менеджер памяти Windows 2000 обязательно должен иметь доступ к файлу подкачки для чтения страниц в память, операционная система не может обслужить такое же прерывание, вызванное выставленным IRQL. Поэтому драйверы устройств должны хранить свои данные в физической, не виртуальной памяти, чтобы они гарантированно находились в памяти компьютера, по крайней мере, в процессе обслуживания прерываний. Подобная память в Windows 2000 называется non-paged — «не страничной».

Когда процессор обрабатывает прерывание с высоким приоритетом, система не всегда в состоянии обнаружить обращение драйвера устройства к листаемой памяти, поскольку находящиеся в ней данные хранятся в оперативной памяти компьютера, а не в файле подкачки. Обнаружить такие ошибки в драйверах устройств при тестировании трудно, но в процессе работы они приводят к появлению «голубого экрана смерти» с кодом останова IRQL_NOT_LESS_OR_ EQUAL (т. е. при неудавшейся попытке обращения в страничную память уровень прерывания был не меньше или равен необходимому).

Если в закладке Modify Settings для описателя Verification Type установлен параметр Force IRQL Checking (обязательный контроль IRQL), то компонент Verifier пытается «спровоцировать» нарушение драйвером устройства правил выставления IRQL таким образом, чтобы отклонение от нормы было обнаружено Windows 2000. Всякий раз, когда драйвер устройства выставляет IRQL, компонент Verifier принудительно выгружает все листаемые данные ядра на диск в файл подкачки. Функция компонента Verifier, реализующая обязательный контроль IRQL, носит название MmTrimAllSystemPageableMemory. Получается, что, когда бы драйвер верифицируемого устройства ни обращался к листаемой памяти в момент поднятого IRQL, система незамедлительно обнаружит противоречия в работе драйвера, а «голубой экран смерти» однозначно идентифицирует источник ошибки.

Если для описателя Verification Type установлено значение I/O Verification, то всякий раз, когда проблемный драйвер вызывает ту или иную функцию I/O Manager, Verifier перенаправляет вызов к Verifier-версии этой функции. Verifier-версия гарантирует, что если верифицируемый драйвер устройства является инициатором запроса I/O Request Packets (IRPs) в другой драйвер, то это происходит не вследствие ошибки. С другой стороны, Verifier-версия гарантирует, что любой драйвер устройства, который является получателем запроса IRPs, выставленного верифицируемым драйвером, не нарушит правила обработки IRQL (еще одна общая ошибка, которая встречается при написании драйверов устройств).

Если для описателя Verification Type устанавливается параметр Allocation Fault Injection, то Verifier время от времени возвращает ошибку при выполнении запроса (со стороны драйвера) на выделение памяти. В недавнем прошлом разработчики создавали множество драйверов устройств, исходя из следующего предположения: память, в которой расположено ядро системы, всегда доступна. Если вся имеющаяся память уже распределена, очередной запрос на выделение памяти не ухудшит положение, так как система в любом случае аварийно завершит работу. В Windows 2000 специалисты Microsoft планируют реализовать возможность продолжения работы в условиях временной нехватки памяти. Поэтому необходимо, чтобы драйверы устройств надлежащим образом обрабатывали аварийные коды возврата функций выделения памяти, что, как правило, связано с нехваткой последней. Так, в течение первых наиболее критических семи минут после первоначальной загрузки системы, когда нехватка памяти может сказаться на успешной загрузке драйверов устройств, Verifier случайным образом выдает ошибки при вызове функций выделения памяти, который производится по запросу со стороны верифицируемых драйверов. Если разработчики драйвера не запрограммировали обработку ошибок при выделении памяти, то в этом случае рано или поздно будет сформирована недопустимая ссылка на ячейку памяти, и Windows 2000 сможет идентифицировать дефектный драйвер.

Параметр Pool Tracking, выбранный в качестве значения Verification Type, унаследован от предыдущих версий NT. И NT 4.0, и NT 3.51 также предоставляли возможность выполнять трассировку пула драйвера. Для обеих операционных систем включить трассировку пула можно было единственным способом: воспользоваться командой gflags.exe, включенной в набор системных утилит Windows NT Server 3.51 Resource Kit. В случае с Windows 2000 драйверы могут в качестве дополнительных параметров в запросе на выделение памяти указать специальный тег, состоящий из 4-х символов. При отключении трассировки пула, Windows 2000 игнорирует этот тег, однако, если трассировка выполняется, система ассоциирует значение тега с областью памяти, которую запросил драйвер устройства. С помощью другого инструмента разработчика, PoolMon, поставляемого в комплекте Device Driver Kit (DDK), программист может определить, сколько байт памяти Windows 2000 выделяет для каждого тега. Мониторинг областей памяти, которые используются драйвером устройства в процессе работы, дает разработчику возможность выявить так называемую «утечку памяти» (одну из ошибок в программировании драйвера, при которой не происходит освобождения уже ненужной приложению памяти). Verifier также показывает общую статистику использования пула на закладке Pool Tracking в окне Driver Verifier Manager.

Параметр Special Pool — последний в ряду установок, задаваемых для Verification Type. Если он указан, система выделяет специальную область памяти ядра для работы компонента Verifier. Запросы верифицируемого драйвера, связанные с распределением памяти, перенаправляются в Verifier, и память выделяется в специально отведенной области, а не в стандартном пуле памяти. Установка Special Pool активизирует дополнительные возможности системы для обнаружения ошибок типа overrun и underrun сразу после их возникновения. Кроме того, в режиме Special Pool выполняются дополнительные проверки при отработке запросов выделения ресурсов. Обычно когда драйвер устройства запрашивает или освобождает память, Memory Manager таких проверок не производит.

Когда установлена опция Special Pool и драйвер устройства запрашивает память, Verifier возвращает драйверу ссылку на целую страницу памяти. Verifier размещает буфер используемых драйвером данных или в самом конце, или в самом начале страницы, а оставшуюся часть страницы заполняет

РИСУНОК 1. Буфер памяти при выполнении проверки overrun.
случайным образом. Более того, страницу памяти непосредственно до и непосредственно после страницы, выделенной для нужд драйвера, Verifier маркирует как недействительную (invalid). На Рисунке 1 показан буфер памяти, выделенный для драйвера в режиме Special Pool, в момент выполнения проверки overrun. Если драйвер устройства попытается читать или писать за границей старших адресов буфера, то произойдет обращение к недействительной странице, и Windows 2000 Memory Manager сгенерирует «голубой экран смерти». В логике обнаружения ситуации overrun содержатся элементы, позволяющие выявить и ошибку underrun. Когда драйвер освобождает буфер данных, и выделенная область памяти возвращается в пул Verifier, есть гарантия того, что сигнатура, предшествующая ссылке на буфер, не изменилась. (Когда Windows 2000 в принудительном порядке выполняет процедуру проверки underrun, компонент Verifier выделяет буфер в самом начале страницы памяти, а не в конце.) Если сигнатура изменилась, это означает, что в драйвере, вероятно, произошла ошибка типа underrun, и была произведена запись за пределами буфера. В рамках интерфейса пользователя Verifier не предоставляет возможности контроля над ошибкой типа underrun, поэтому нужно вручную внести изменения в параметр реестра HKEY_ LOCAL_MACHINESYSTEMCurrentControlSet ControlMemoryManagementPoolTagOverruns и установить это значение равным 1, если требуется контролировать ситуацию underrun. Опция Special Pool при распределении памяти также предоставляет гарантии того, что правила обработки IRQL в процессе выделения и освобождения памяти не будут нарушены (т. е. Verifier обнаруживает ошибку в драйвере устройства, когда выдается запрос на выделение листаемой памяти, а уровень IRQL не позволяет это сделать).

Настройку Special Pool можно выполнить, не применяя интерфейс пользователя. Windows 2000 интерпретирует параметр реестра HKEY_ LOCAL_MACHINESYSTEMCurrentControlSetControlMemory ManagementPoolTag, тип данных REG_DWORD, как вышеупомянутый тег, который система использует в операциях распределения памяти. Следовательно, даже если верификация драйвера прямо не указана, но значение реестра PoolTag активизирует трассировку функций распределения памяти, запрашиваемых драйвером, ядро системы выделяет требуемую память в специальном пуле. Если для PoolTag установлено значение 0x0000002A или групповой символ (*), то вся распределяемая по запросам драйверов системы память выделяется в специальном пуле.

Microsoft использует компонент Verifier для проверки всех драйверов устройств, которые независимые поставщики предлагают внести в список Hardware Compatibility List (HCL). Таким образом, Microsoft гарантирует, что драйверы, перечисленные в HCL, совместимы с Windows 2000 и что в них отсутствуют наиболее типичные ошибки. Поскольку ошибка в любом драйвере устройства способна вызвать «голубой экран смерти», разработчики Microsoft обеспечили механизм автоматической активизации компонента Verifier. В случае появления «голубого экрана смерти» ядро системы выполнит маркировку нового драйвера, и при следующей перезагрузке системы для этого драйвера будет выполняться верификация. Если подозреваемый драйвер выполнит в процессе верификации недопустимую операцию, а Verifier это обнаружит, то администратор системы сможет отослать дамп аварийного останова с точной локализацией ошибки либо независимому поставщику, либо в Microsoft.

Page Heap

Компания Microsoft также распространила функциональность Special Pool на пользовательские приложения. Начиная с пакета исправлений NT 4.0 Service Pack 4 (SP4) Microsoft предоставляет в распоряжение разработчиков инструмент Page Heap, правда, при этом подчеркивается, что Page Heap — новая опция Windows 2000. Возможности Page Heap аналогичны функциям утилиты Gflags из Windows 2000 Resource Kit. Разработчики программного обеспечения и администраторы системы могут сконфигурировать работу приложений с учетом возможностей Page Heap. Память, запрашиваемая приложениями пользователя, выделяется в так называемой «куче» (heap), в то время как для драйверов устройств или самого ядра она распределяется в пуле. Каждое приложение имеет свою область для динамического размещения данных (свой heap). Как и Special Pool, механизм Page Heap распределяет память для приложений таким образом, чтобы в случае возникновения ошибок типа overrun или underrun система незамедлительно их обнаруживала. Схема распределения памяти с помощью Page Heap в точности совпадает с тем, как распределяется память в ядре при включении параметра Special Pool. Страницы памяти до и после той, которая выделена для нужд приложения, объявляются недействительными, и в дальнейшем этот способ используется для обнаружения ошибок приложения, связанных с переходом границ выделенного буфера.

Более надежная Windows NT

Наличие новых технологий, которые Microsoft предоставила разработчикам программного обеспечения и администраторам для предупреждения сбоев в работе, локализации источников ошибок и восстановления системы, является, на мой взгляд, главным стимулом для перехода от NT 4.0 к Windows 2000. Следует особо подчеркнуть наличие такого программного инструмента, как Driver Verifier, так как его использование способно обеспечить необходимый уровень надежности Windows 2000 и гарантирует высокое качество исполнения программ, работающих в режиме ядра.