div.main {margin-left: 20pt; margin-right: 20pt}
Повышение надежности Windows 2000. Часть
3
|
Марк Русинович доктор философии в области
вычислительной техники Университета Карнеги-Меллон и один из
соавторов многих популярных утилит для 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 и гарантирует высокое качество исполнения
программ, работающих в режиме ядра.
|