Программирование драйверов Windows

         

Используйте интеллект нового устройства


Некоторые периферийные устройства содержат собственные процессоры, которые выполняют операции диагностики и управления. Эти процессоры могут работать под управлением встроенного программного кода (прошитого в ПЗУ), но возможно, что драйвер самостоятельно выполняет загрузку управляющего кода в оперативную память устройства в момент его инициализации (как это имеет место при работе с весьма интересными микросхемами USB контроллеров фирмы Cypress).

Использование встроенных возможностей интеллектуальных устройств может дать существенно лучшие результаты в работе и в диагностике такой аппаратуры.



ISR, Interrupt Service Routine


Процедура обслуживания прерываний. Функция, которую драйвер регистрирует для того, чтобы она получала управление в момент, когда аппаратура, обслуживаемая драйвером, передала сигнал прерывания. Задача этой функции выполнить некоторую самую минимальную работу и зарегистрировать callback функцию, называемую процедурой отложенного вызова для обслуживания прерывания (часто обозначается именем DpcForISR, однако автор драйвера может дать ей любое имя). Если учесть, что в операционной системе Windows на типовом компьютере ежесекундно "происходит" от 100 до 600 прерываний, то станет понятно, почему так вредно задерживаться на высоких приоритетных уровнях, которые имеют ISR функции.



Источники информации


Разумеется, главным источником информации и истиной в последней инстанции относительно драйверов Windows следует считать документацию Microsoft, поставляемую в составе пакетов разработки драйверов DDK (Device Driver Kit). Однако сначала о других источниках.



Издания, которые не были переведены на русский язык


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

Karen Hazzah, Writing Windows VxDs & Device Drivers; Programming secrets for Virtual Device Drivers, 479 pages 2nd Bk&Dk edition (March 1, 1997) CMP Books; ISBN: 0879304383. Книга целиком посвящена разработке VxD драйверов под Windows 95, что позволяет говорить о ней, как о морально устаревшей.

Alessandro Rubini, Jonatan Corbet, Linux Device Drivers, 2nd Edition, 562 pages 2nd edition (June 2001) O'Reilly & Associates; ISBN: 0596000081. Рассматриваются вопросы внутренней организации ядра Linux и создания драйверов как модулей ядра. Программисту в Windows книга может быть интересна для проведения сравнительного анализа программирования драйверов в Windows и Linux (Unix).

Walter Oney, Programming the Microsoft Windows Driver Model, 624 pages Bk&Cd-Rom edition (September 1999), Microsoft Press; ISBN: 0735605882. Основательное издание, последовательно вводящее в программирование драйверов. На сопроводительном диске имеется отличный мастер инициации драйверных проектов на базе MS Visual Studio. Используются приемы программирования С++, особенно во втором (декабрь 2002 года) издании.

Art Baker, Jerry Lozano, Windows 2000 Device Driver Book, The: A Guide for Programmers. 500 pages Bk&Cd-Rom edition (December 15, 2000) Prentice Hall PTR; ISBN: 0130204315. Хорошо соответствует подзаголовку - руководство для программистов. Наиболее подробно из всех зарубежных изданий рассмотрено программирование DMA операций, однако совершенно не рассматривается программирование USB.

Edward N. Dekker, Jozeph M. Newcomer, Developing Windows NT Device Drivers. A Programmer's Handbook, 1227 pages (March, 1999) Addison Wesley Longman, Inc.; ISBN: 0201695901. Замечательная и чрезвычайно объемная книга, посвященная программированию драйверов Windows 2000 (хотя была завершена в момент выпуска ее бета-версии).
Затронуто много вопросов, которые можно считать общесистемными. Дублирует много сведений из DDK, однако делает это с большим количеством комментариев. И хотя это та книга, которую должен иметь под рукой разработчик драйверов, ее нельзя считать книгой, которую новичку следует читать первой. Поскольку книга выпушена без CD-ROM, это компенсируется размещением большого количества кода на интернет-сайте одного из авторов.

Chris Cant, Writing Windows Wdm Device Drivers: Covers Nt 4, Win 98, and Win 2000, 540 pages Bk&Cd Rom edition (July 1999) CMP Books; ISBN: 0879305657. Книга касается только драйверной модели WDM и сосредоточена, в основном, на программировании USB (хотя есть пример, связанный и с LPT портом). Из-за своеобразного стиля изложения читатель-новичок, доверившийся этой книге, получит поверхностные знания, особенно если не уделит должного внимания примерам. Следует отметить, что некоторые примеры, связанные с HID USB устройствами плохо работают под Windows XP. Тем не менее, несомненным плюсом данного издания является наличие на прилагаемом CD-ROM прекрасного отладочного средства, известного под названием DebugPrint.

Peter G. Vascarola, W. Anthony Mason, Windows NT Device Driver Development, 684 pages, (November 1998) MacMillan Publishing Company; ISBN: 1578700582. Книга написана ветеранами разработки кода под Windows. И хотя авторы аннотировали ее как "not a cookbook" (не книга рецептов), что подтверждается малым количеством примеров кода и отсутствием CD-ROM, эту книгу ни в коем случае нельзя сбрасывать со счетов, даже учитывая почти полное отсутствие в ней материала по WDM. Отдельные тонкости работы в режиме ядра NT предельно тщательно освещены только в этом издании. Наиболее подробно здесь (из всех упомянутых непереведенных изданий) рассмотрены NDIS, SCSI miniport и видео драйверы.

David A. Solomon, Mark Russinovich, Inside Microsoft Windows 2000 (Microsoft Programming Series) Microsoft Press; 3rd edition (September 2000) ISBN: 0735610215.Далеко небесполезная книга для разработчика драйверов, хотя имеет общеобразовательную направленность для программистов Windows NT, 2000, XP.

Gary Nebett, Windows NT/2000 Native API Reference, 528 pages; MTP; ISBN 1578701996. Справочное руководство по набору Windows Native API функций. Небольшое количество комментариев, немного примеров.


Изменение приоритетов как средство синхронизации


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



Ячейки стека ввода/вывода


Основное назначение ячеек стека ввода/вывода (I/O stack location) состоит в том, чтобы хранить функциональный код и параметры запроса на ввод/вывод (последние могут претерпевать изменения при путешествии пакета по стеку драйверов). Ниже приводятся поля ячеек стека ввода/вывода, к которым драйвер может обращаться непосредственно по указателю (чего не рекомендуется делать для остальных полей).

Таблица 8.6. Некоторые элементы ячейки стека ввода/вывода

IO_STACK_LOCATION, *PIO_STACK_LOCATION
Поля Описание
UCHAR MajorFunction Код IRP_MJ_XXX, описывающий назначение операции
UCHAR MinorFunction Суб-код операции
PDEVICE_OBJECT

DeviceObject

Указатель на объект устройства, которому был адресован данный запрос IRP
PFILE_OBJECT FileObject Файловый объект для данного запроса, если он задан
union Parameters (трактовка определяется значением MajorFunction):
struct Read Параметры для IRP типа IRP_MJ_READ:

• ULONG Length

• ULONG Key

• LARGE_INTEGER ByteOffset

struct Write Параметры для IRP типа IRP_MJ_WRITE:

• ULONG Length

• ULONG Key

• LARGE_INTEGER ByteOffset

struct DeviceIoControl Параметры для IRP типа IRP_MJ_DEVICE_CONTROL:

• ULONG OutputBufferLenght

• ULONG InputBufferLenght

• ULONG IoControlCode

• PVOID Type3InputBuffer

Для запроса, который адресован драйверу самого нижнего уровня, соответствующий IRP пакет имеет только одну ячейку стека. Для запроса, который послан драйверу верхнего уровня, Диспетчер ввода/вывода создает пакет IRP с несколькими стековыми ячейками &#8212 по одной на каждый драйверный слой. Другими словами, размер стека в пакете IRP численно равен количеству драйверных слоев, участвующих в обработке запроса на данную операцию. Любому драйверу в иерархии разрешен доступ только к его собственной &#8212 "числящейся" за ним &#8212 ячейке стека пакета IRP (хотя никто не контролирует и иные "противоправные" действия).

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

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



Kernel mode


Режим ядра. Привилегированный режим, в котором разрешено выполнять ответственные инструкции (команды процессора). Если приложение пользовательского режима в Windows 98 попытается выполнить инструкции

mov dx,0378h out dx,ах

то не случится ничего страшного. Но после такого поступка в Windows XP на экране монитора непременно появится сообщение, подобное следующему:

Рис. 1.1

Исключение при выполнении привилегированных инструкций пользовательским приложением..

Что произошло? Просто Windows NT позволяет выполнять ответственные операции только модулям режима ядра (к которым относятся драйверы режима ядра). В режиме ядра можно достоверно определять реально присутствующие в системе аппаратные ресурсы, непосредственно обращаться к ним, вызывать "могущественные" системные функции, влиять на прохождение данных (подсчитывать, кодировать/декодировать) и т.д.

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



Коды ошибочных ситуаций


При выводе системных сообщений о прекращении работы (известные как bug-checks), выводятся также коды, по которым можно определить, что побудило систему запаниковать. В зависимости от ошибки, система сообщает до 4-х дополнительных параметров, которые дают дополнительную информацию о возникшей проблеме. Хотя полный перечень кодов можно найти в заголовочном файле bug-codes.h, входящий в пакет DDK, расшифровки значений там не приводится. По этой причине ниже приводятся наиболее часто встречающиеся коды, основные причины данных ситуаций и расшифровка дополнительных параметров. Данному вопросу посвящена статья Microsoft Knowledge Base Q103059.

0x08 IRQL_NOT_DISPATCH_LEVEL
Был выполнен функцией (предназначенной для вызова с уровня IRQL, равного DISРATCH_LEVEL) из кода, работающего на более высоких уровнях, например DIRQL
Параметры Описание
1-4 Зарезервировано

0x0A IRQL_NOT_LESS_OR_EQUAL
Драйвер пытается получить доступ к странично организованной памяти при работе на уровне привилегий IRQL равном DISPATCH_LEVEL или выше, причем эта страница в момент обращения отсутствует в оперативной памяти (ситуация не может быть перехвачена обработчиком PAGE FAULT)
Параметры Описание
1 Адрес в страничной памяти, обращение по которому вызвало сбой
2 Значение уровня IRQL в момент обращения
3 Тип доступа при этом обращении 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, которая выполняла некорректное обращение

0x10 SPIN_LOCK_NOT_OWNED
Программный поток пытается освободить объект спин-блокировки, которым не владеет
Параметры Описание
1-4 Зарезервировано

0x19 MEMORY_MANAGEMENT
Структура "кучи" (heap &#8212 область, из которой программному коду динамически выделяется память) нарушена
Параметры Описание
1-4 Зарезервировано

0x1E KMODE_ESCEPTION_NOT_HANDLED
Программный код режима ядра вызвал исключение, для которого нет обработчика (в частности, его не предоставил сам программный код)
Параметры Описание
1 Код исключения (как правило, это значения, которые можно найти в файле winerror.h), в частности:

• 0xC0000005 &#8212 нарушение прав доступа (access violation): ошибочный указатель или запись в область памяти "только для чтения"

• 0xC000001D &#8212 ошибочная инструкция. Возможно, при выполнении вызова по указателю произошло неправильное декодирование инструкции, поскольку адрес был ошибочен. Возможно, что стек был испорчен, в результате чего не сохранился адрес возврата из текущей функции

• 0xC0000094 &#8212 целочисленное деление на ноль

• 0xC00000FD &#8212 переполнение стека, например, в результате многочисленных рекурсивных вызовов

2 Адрес инструкции, которая вызвала исключение
3 Первый параметр исключения
4 Второй параметр исключения
<
0x24 NTFS_FILE_SYSTEM
Выявлена проблема в ntfs.sys
Параметры Описание
1 Исходный файл и номер строки
2 Адрес записи исключения (необязательно)
3 Адрес записи контекста (необязательно)
4 Адрес инструкции, где было вызвано исключение (необязательно)
0x2A INCONSISTENT_IRP
Проблема в структуре IRP пакета: параметры конфликтуют. Возможно, в результате того, что указатель на IRP пакет ошибочно использовался в качестве указателя на элемент данных другого типа
Параметры Описание
1 Адрес IRP пакета
2-4 Зарезервировано
0x2E DATA_BUS_ERROR
Данная ошибка обычно бывает обусловлена сбоем в контроле четности в системной памяти &#8212 аппаратная проблема. (Данная ошибка может быть также вызвана обращением драйвера по несуществующему виртуальному адресу 0x8xxxxxxx в системном адресном пространстве.)
Параметры Описание
1 Виртуальный адрес, который вызвал сбой
2 Физический адрес, который вызвал сбой
3 Содержимое PSR (Process Status Register, регистра состояния процессора)
4 Faulting instruction register (FIR)
0x35 NO_MORE_IRP_STACK_LOCATIONS
Был создан IRP пакет, в котором оказалось недостаточно ячеек стека (stack locations) для того, чтобы передать его в IoCallDriver &#8212 для обработки нижними слоями драйверов
Параметры Описание
1 Адрес IRP пакета
2-4 Зарезервировано
0x36 DEVICE_REFERENCE_COUNT_NOT_ZERO
Был выполнен системный вызов IoDeleteDevice, в процессе которого обнаружилось, что число ссылок на объект устройства все еще не равно нулю, то есть операция удаления объекта некорректна
Параметры Описание
1 Адрес объекта устройства (Device Object)
2-4 Зарезервировано
0x3F NO_MORE_SYSTEM_PTES
Системная таблица страниц заполнена. Наиболее вероятная причина состоит в том, что драйвер не выполняет освобождение страниц памяти. Возможно, слишком мала таблица страниц (следует обратиться к методам ее увеличения)
Параметры Описание
1-4 Зарезервировано
<


0x44 MULTIPLE_IRP_COMPLETE_REQUESTS
Был выполнен системный вызов IoCompleteRequest, в процессе которого обнаружилось, что относительно данного IRP пакета это действие уже выполнялось ранее
Параметры Описание
1 Адрес IRP пакета
2-4 Зарезервировано
0x50 PAGE_FAULT_IN_NONPAGED_AREA
Ошибочное обращение к области памяти в системном адресном пространстве
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 Зарезервировано
0x51 REGISTRY_ERROR
Структура Системного Реестра нарушена, возможно, в процессе предыдущего системного сбоя. К сожалению, переустановка Windows самый вероятный способ преодоления этой проблемы. (Разумеется, если не выполнялось резервное копирование системных файлов.)
Параметры Описание
1-4 Зарезервировано
0x58 FT_DISK_INTERNAL_ERROR
Система была загружена из восстановленного основного раздела (primary partition). (Поддерево реестра говорит &#8212 все хорошо, но на самом деле это не так.)
Параметры Описание
1-4 Зарезервировано
0x76 PROCESS_HAS_LOCKED_PAGES
Работа завершена, но остались зафиксированные в памяти (locked) страницы. Возможно, драйвер пытался освободить заблокированные страницы после операции ввода/вывода, в частности, в процедуре Unload или обработчике запроса Shutdown, но это ему не удалось
Параметры Описание
1 0
2 Адрес процесса
3 Число заблокированных страниц
4 0 или указатель на стек драйвера
0x77 KERNEL_STACK_INPAGE_ERROR
Запрошенная страница системного адресного пространства, размещенная в страничном файле, не может быть считана в оперативную память
Параметры Описание
1 Код состояния или 0
2 Код состояния ввода/вывода или значение, найденное в стеке, где должна размещаться сигнатура
3 0 или номер страничного файла
4 Адрес сигнатуры в стеке или смещение в страничном файле
<


0x79 MISMATCHED_HAL
Версии ( revision level) программного кода ядра и программного кода HAL не согласуются. Это может происходить из-за неправильного использования ключей /kernel и /hal в файле BOOT.INI. (См. Microsoft Knowledge Base, Q103059.)
Параметры Описание
1 1: Рассогласование редакций PRCB

2: Рассогласование версий сборки

3: Рассогласование Micro Channel
2 1: Редакция (release level) ntoskrnl.exe

2: Тип сборки (build type) ntoskrnl.exe

3: Тип компьютера, обнаруженный во время загрузки
3 1: Редакция hal.dll

2: Тип сборки hal.dll

3: Тип компьютера, поддерживаемый HAL
4 Зарезервировано
0x7A KERNEL_DATA_INPAGE_ERROR
Запрошенная страница системного адресного пространства, размещенная в страничном файле, не может быть считана в оперативную память
Параметры Описание
1 Тип блокировки (значения 1,2,3 или адрес записи в таблице страниц)
2 Код статуса операции ввода/вывода (во время которой произошла ошибка)
3 Если тип блокировки 1 или 2 &#8212 текущий процесс

Если тип блокировки 3 &#8212 виртуальный адрес
4 Виртуальный адрес, соответствующая которому страница не может быть считана в оперативную память
0x7B INACCESSIBLE_BOOT_DEVICE
Операционная система Window NT потеряла доступ к системному разделу (system partition) во время запуска. Это может происходить из-за ошибок в драйвере, обслуживающем устройство, с которого должна осуществляться загрузка, из-за наличия программного обеспечения, несовместимого с NT, вирусов, неверного конфигурирования CD-ROM, ошибок в разбиении или незавершенного разбиения жесткого диска (partition table). (См. также Microsoft Knowledge Base Q103069, Q105026, Q115339, Q120744, Q124307, Q126423, Q131337, Q131712, Q136074, Q137860, Q153296, Q156168, Q161960.)
Параметры Описание
1 Адрес объекта устройства, которое не может быть смонтировано
2-4 0
0x7F UNEXPECTED_KERNEL_MODE_TRAP
Только для платформы INTEL. Процессор сгенерировал системное прерывание (trap), которое не может быть обработано ядром
Параметры Описание
1 Код прерывания
2-4 Зарезервировано
<


0x81 SPIN_LOCK_INIT_FAILURE
Ошибка при инициализации спин-блокировки. Условия возникновения данной ошибки четко не определены, однако ее природа всегда &#8212 программная
Параметры Описание
1-4 Зарезервировано
0x9F DRIVER_POWER_STATE_FAILURE
Драйвер находится в ошибочном состоянии с точки зрения участия в схеме энергопотребления
Параметры Описание
1 1: Освобождаемый драйверный объект имеет незавершенный запрос по схеме энергопотребления, который ожидает обработки

2: Драйверный объект завершил обработку системного IRP запроса по энергопотреблению, но не выполнил вызов PoStartNextPowerIrp

3: Драйвер не оформил должным образом IRP запрос как ожидающий обработки или как обработанный

100: Объекты устройств в узле (devnode) несостоятельны в своем использовании DO_POWER_PAGABLE

101: Родительский объект устройства обнаружил, что дочерний объект устройства не установил бит DO_POWER_PAGABLE
2 1: Указатель на объект устройства

2: Указатель на целевой объект устройства

3: Указатель на целевой объект устройства

100: Указатель на объект устройства в нестраничной памяти

101: Дочерний объект устройства (FDO)
3 1: Зарезервировано

2: Указатель на объект устройства

3: Указатель на объект устройства

100: Указатель на целевой объект устройства

101: Дочерний объект устройства (FDO)
4 1: Зарезервировано

2: Зарезервировано

3: IRP пакет

100: Указатель на уведомляемый объект устройства

101: Родительский объект устройства
0xBE ATTEMPTED_WRITE_TO_READONLY_MEMORY
Драйвер пытается выполнить запись в область памяти, предназначенную только для чтения
Параметры Описание
1 Виртуальный адрес, по которому производится попытка записи
2 Содержимое записи в таблице страниц
3-4 Зарезервировано
0xC1 SPECIAL_POOL_DETECTED_MEMORY_CORRUPTION
Драйвер произвел запись в ошибочную секцию специального пула памяти
Параметры Описание
Попытка освободить область памяти, которая не была ранее получена (выделена, allocated)
1 Адрес, к которому предпринята попытка "освободить" его
2 Зарезервировано
3 0
4 0x20
Попытка освободить область по ошибочному адресу
1 Адрес, к которому предпринята попытка "освободить"
2 Запрошенное число байт
3 Рассчитанное число байт
4 0x21, 0x22
Попытка освободить область по адресу, в то время как близлежащие байты на странице испорчены
1 Адрес, к которому предпринята попытка "освободить"
2 Адрес, где размещается испорченная область
3 Зарезервировано
4 0x23
Попытка освободить область по адресу, в то время как байты после данной выделенной области перезаписаны
1 Адрес, к которому предпринята попытка "освободить"
2 Адрес, где размещается испорченная область
3 Зарезервировано
4 0x24
Попытка получить область в пуле памяти при ненадлежащем уровне IRQL
1 Текущий уровень IRQL
2 Тип пула памяти
3 Число байт
4 0x30
Попытка освободить область в пуле памяти при ненадлежащем уровне IRQL
1 Текущий уровень IRQL
2 Тип пула памяти
3 Адрес, к которому предпринята попытка "освободить"
4 0x31
<


0xC2 BAD_POOL_CALLER
Текущий программный поток сделал ошибочный запрос к области памяти
Параметры Описание
Заголовок пула памяти испорчен
1 0x01, 0x02 или 0x04
2 Указатель на заголовок пула
3 Первая часть содержимого заголовка пула
4 0
Попытка освободить область в пуле памяти, которая уже освобождена
1 0x06
2 Зарезервировано
3 Указатель на заголовок пула
4 Содержимое заголовка пула
Попытка освободить область в пуле памяти, которая уже освобождена
1 0x07
2 Зарезервировано
3 Указатель на заголовок пула
4 0
Попытка получить область в пуле памяти при ненадлежащем уровне IRQL
1 0x08
2 Текущий уровень IRQL
3 Тип пула памяти
4 Размер запрошенной области
Попытка освободить область в пуле памяти при ненадлежащем уровне IRQL
1 0x09
2 Текущий уровень IRQL
3 Тип пула памяти
4 Адрес пула
Попытка освободить область в пуле памяти уровня ядра по указателю, который имеет вид адреса пользовательского режима
1 0x40
2 Начальный адрес области
3 Начало системного адресного пространства
4 0
Попытка освободить область в пуле нестраничной памяти, которая не была ранее получена (выделена, allocated)
1 0x41
2 Начальный адрес области
3 Страничный блок физической памяти
4 Максимальный страничный блок физической памяти
Попытка освободить область в нуле страничной памяти, которая не была выделена
1 0x50
2 Начальный адрес области
3 Смещение (в страницах) от начала страничного пула
4 Размер области страничного пула в байтах
Попытка освободить область в пуле памяти по ошибочному адресу либо с разрушенным заголовком
1 0x99
2 Адрес, к которому предпринята попытка "освободить"
3 0
4 0
0xC5 DRIVER_CORRUPTED_EXPOOL
Драйвер, возможно, разрушил пул в системном адресном пространстве
Параметры Описание
1 Указатель на область памяти, обращение к которой вызвало сбой
2 Уровень IRQL в момент обращения
3 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, обратившейся к области памяти
<


0xC6 DRIVER_CAUGHT_MODIFYING_FREED_POOL
Драйвер обращается к области пула памяти, которая освобождена
Параметры Описание
1 Указатель на область памяти, обращение к которой вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 0: в режиме ядра

1: в пользовательском режиме
4 4
0xC7 TIMER_OR_DPC_INVALID
Области памяти под объектами уровня ядра &#8212 таймером или DPC &#8212 были освобождены, хотя они еще находятся в очереди, ожидая активации
Параметры Описание
1 0: объект таймера

1: объект DPC

2: DPC процедура
2 Адрес объекта
3 Начало области, обращение к которой вызвало ошибку
4 Конец области, обращение к которой вызвало ошибку
0xCA PNP_FATAL_ERROR
РnР Менеджер обнаружил критическую ошибку, вероятно, в результате ошибки в функционировании РnР драйвера
Параметры Описание
Обнаружены двойники-PDO (объекты физического устройства) &#8212 отдельные фрагменты драйвера создали несколько PDO объектов с одинаковыми идентификатором устройства
1 0x01
2 Адрес вновь созданного PDO объекта
3 Адрес ранее созданного PDO объекта, который теперь "повторен"
4 Зарезервировано
Ошибка в объекте физического устройства. Программный поток, которых запросил объект PDO, не выполнил инициализацию объектов PDO или FDO
1 0x02, 0x03
2 Адрес подразумеваемого PDO объекта
3 Зарезервировано
4 Зарезервировано
Ошибка в идентификаторе (ID). Модуль ядра, который выполнял перечисление (enumeration) возвратил идентификатор, который содержит ошибочные символы или неверно завершен. Идентификаторы должны содержать только символы ASCII 0x20..0x2B и 0x2D..0x7F
1 0x04
2 Адрес PDO объекта с установленным DO_DELETE_PENDING
3 Адрес буфера, содержащего ID
4 1: DeviceID

2: UniqueID

3: Аппаратные идентификаторы

4: Совместимые идентификаторы
Объект PDO был освобожден (то есть Менеджер объектов уменьшил число ссылок на него до нуля), но сам объект еще участвует в дереве объектов устройств (devnode tree). Как правило, это означает, что драйвер не добавил ссылку (соответствующим системным вызовом) в тот момент, когда возвратил PDO объект в ответ на поступивший соответствующий IRP запрос
1 0x05
2 Адрес PDO объекта
3 Зарезервировано
4 Зарезервировано
<


0xCB DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS
Драйверу не удалось выполнить освобождение зафиксированных в оперативной памяти (locked) страниц после операции ввода/вывода
Параметры Описание
1 Адрес инициатора (в драйвере) фиксации данных страниц
2 Инициатор вызова инициатора фиксации
3 Указатель на MDL список, содержащий зафиксированные в оперативной памяти страницы
4 Указатель на имя "виновного" драйвера (в формате Unicode)
0xCC PAGE_FAULT_IN_FREED_SPECIAL_POOL
Система обратилась к ранее освобожденной области памяти
Параметры Описание
1 Адрес, обращение к которому, вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 Зарезервировано
0xCD PAGE_FAULT_BEYOND_END_OF_ALLOCATION
Система обратилась к памяти за пределами выделенной драйвером области (в некотором пуле)
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 Зарезервировано
0xCE DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATION
Драйвер не отменил незавершенные (pending) операции перед своей выгрузкой
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 Зарезервировано
0xD0 DRIVER_CORRUPTED_MMPOOL
Драйвер испортил системный пул памяти
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Уровень IRQL в момент обращения
3 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, из которой был осуществлен ошибочный доступ
0xD1 DRIVER_IRQL_NOT_LESS_OR_EQUAL
Драйвер пытался получить доступ к страничной памяти при работе на высоком уровне IRQL
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Уровень IRQL в момент обращения
3 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, из которой был осуществлен ошибочный доступ
<


0xD3 DRIVER_PORTION_MUST_BE_NONPAGED
Драйвер поступил некорректно, пометив сегмент своего кода или своих данных как размещаемый в страничной памяти
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Уровень IRQL в момент обращения
3 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, из которой был осуществлен ошибочный доступ
0xD4 SYSTEM_SCAN_AT_RAISED_IRQL_CAUGHT_IMPROPER_DRIVER_UNLOAD
Драйвер не отменил незавершенные (pending) операции перед своей выгрузкой
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Уровень IRQL в момент обращения
3 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, из которой был осуществлен ошибочный доступ
0xD5 DRIVER_PAGE_FAULT_IN_FREED_SPECIAL_POOL
Драйвер обратился к ранее освобожденной области памяти
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 Зарезервировано
0xD6 DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION
Драйвер обратился к памяти за пределами выделенного пула памяти
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
3 Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 Зарезервировано
0xD7 DRIVER_UNMAPPING_INVALID_VIEW
Драйвер пытается получить физический адрес для виртуального, который не существует
Параметры Описание
1 Виртуальный адрес
2 0: Система не является терминальным сервером

1: Система является терминальным сервером
3 0
4 0
0xD8 DRIVER_USED_EXCESSIVE_PTES
Не осталось свободных записей в системной таблице страниц виртуальной памяти
Параметры Описание
1 Указатель на имя "виновного" драйвера (Unicode) либо 0
2 Число записей в системной таблице страниц, используемых "виновным" драйвером (если первый параметр не равен 0)
3 Общее количество свободных записей в системной таблице страниц
4 Общее количество записей в системной таблице страниц
<


0xDB DRIVER_CORRUPTED_SYSPTES
Была сделана попытка обращения к памяти при ненадлежащем уровне IQRL, вероятно, из-за разрушения записей в системных страничных таблицах виртуальной памяти
Параметры Описание
1 Адрес, обращение по которому вызвало сбой
2 Уровень IRQL в момент обращения
3 Код доступа при возникновении ошибки 0 &#8212 чтение, 1 &#8212 запись
4 Адрес инструкции, из которой был осуществлен ошибочный доступ
0xDC DRIVER_INVALID_STACK_ACCESS
Драйвер пытался получить доступ к пространству стека, которое находится ниже указателя границы стека (stack pointer) текущего рабочего потока
Параметры Описание
1-4 Зарезервировано
0xDE POOL_CORRUPTED_IN_FILE_AREA
Драйвер разрушил пул памяти, используемый для хранения страниц, предназначенных для работы с диском
Параметры Описание
1-4 Зарезервировано
0xE1 WORKER_THREAD_RETURNED_AT_BAD_IRQL
Рабочий поток, созданный драйвером, завершился и вернул управление на IRQL уровне равном DISPATCH_LEVEL или выше
Параметры Описание
1 Адрес рабочей процедуры
2 Уровень IRQL (должен быть 0)
3 Параметры рабочей единицы
4 Адрес рабочей единицы
0xE2 MANUALLY_INITIATED_CRASH
Пользователь сознательно вызвал сбой из отладчика либо с клавиатуры
Параметры Описание
1-4 Зарезервировано
0xE3 RESOURCE_NOT_OWNED
Программный поток пытается освободить ресурс, который ему не принадлежит
Параметры Описание
1 Адрес объекта ресурса
2 Адрес объекта потока
3 Адрес таблицы владельца (если существует)
4 Зарезервировано
0xE4 WORKER_INVALID
Запись рабочего процесса подмножества Executive была обнаружена в области памяти, которая не должна была содержать такую запись
Параметры Описание
1 Индикатор положения кода
2 Адрес записи рабочего процесса
3 Начало блока памяти
4 Конец блока памяти

Когда следует применять многослойную архитектуру?


Один из самых первых и самых важных вопросов конструирования драйвера состоит в том, следует ли реализовать драйвер в виде набора слоев или он должен быть монолитным?



Компиляция и сборка драйвера Example.sys


Компиляция и сборка отладочной версии драйвера в среде Visual Studio 7 Net требует выбора пункта меню Rebuild Solution, после чего будет выполнена компиляция и сборка драйвера, а результат (в соответствии с настройками Example.sln и Example.vcproj) будет размещен в поддиректории .\checked.

Для компиляции и сборки драйвера утилитой Build пакета DDK потребуется создать два файла описания проекта — Makefile и Sources.



Компиляция и сборка драйвера утилитой Build пакета DDK


В том варианте, как поставляется пакет DDK, весьма просто использовать компилятор и редактор связей этого пакета. Для этого следует выбрать в меню запуска программ Пуск — Программы — ... запуск соответствующей среды (по ряду причин наиболее предпочтителен выбор среды Window 2000, checked или free), в результате чего появится консольное окно, для которого уже (автоматически) будут должным образом установлены переменные окружения. В том случае, если у разработчика имеются файлы makefile, sources (описывающие процесс сборки данного конкретного драйвера), а пакет DDK установлен корректно, то необходимо лишь перейти в рабочую директорию проекта командой cd (для драйвера, рассматриваемого в следующей главе, это — директория C:\Example) и ввести команду build (см. рисунок 2.1). Разумеется, что в случае ошибок компиляции или сборки вывод будет содержать и их диагностику.

Рис. 2.1

Рабочее окно сборки драйвера под Windows 2000 DDK версии checked



Компиляция и сборка при помощи утилиты Build


Разместим для определенности все файлы (нам понадобятся файлы init.cpp, Driver.h, Makefile и sources) в директорию C:\Example. После этого процесс компиляции и сборки checked (отладочной) версии драйвера при помощи утилиты Build пакета DDK полностью описывается во 2 главе ("Компиляция и сборка драйвера утилитой Build пакета DDK").

Результат сборки можно будет найти в поддиректории .\objchk_w2k\i386 (поскольку используются настройки переменных среды сборки под Windows 2000).



Контекст исключения или внутреннего прерывания (trap)


Запрос программного кода пользовательского режима на обслуживание фрагментом кода режима ядра обслуживается через использование внутренних прерываний ('trap', что переводится как 'ловушка', 'внутреннее прерывание'). Переход к выполнению процедуры режима ядра оформлен как вызванное этим приложением пользовательского режима программное исключение (exception, или внутреннее прерывание, trap). В данном случае, контекст следует отнести, скорее, к коду peжима ядра, нежели к коду пользовательского режима, вызвавшего исключение. Неуверенность толкования этой ситуации проистекает из того, что действие после генерации исключения разворачивается все-таки в режиме ядра. Однако память по адресам пользовательского приложения (каким либо образом попавшим в код драйвера) видится коду режима ядра точно так же, как она представлялась вызвавшему его потоку пользовательского режима.

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



Контекст прерывания


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

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

Код обслуживания прерывания может использовать области памяти, указатели на которые он может получить через внутренние переменные и ссылки собственно в драйвере. Однако следует внимательно следить за тем, чтобы используемые указатели или области памяти, на которые они указывают, не оказались в страничной памяти, поскольку это потенциально опасно и рано или поздно приведет к краху системы. В частности, следует аккуратно использовать директивы указаний компилятору типа #pragma code_seg("PAGE") и #pragma alloc_text("PAGE", MyFunctionName). При сомнениях, относительно корректности размещения кода в страничной памяти можно применять макроопределение PAGED_CODE(), которое выявит случаи входа в данный код с недопустимо высокими приоритетами (разумеется, только в отладочной версии драйвера).



Контекст программного потока режима ядра


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

Некоторые драйверы предпочитают создавать дополнительные программные потоки для выполнения специфических задач, связанных, например, с обслуживаем устройств, которые требуют последовательного опроса (polling), или при работе в условиях, которые продиктованы специфическими схемами организации ожидания. Эти потоки, выполняющиеся в режиме ядра, не так сильно отличаются от привычных потоков пользовательского режима, подробно описанных в книгах по Win32 программированию. Они выполняются в соответствии с решениями планировщика ядра и в соответствии с присвоенными им приоритетами. Объясняется это "простое", несмотря на режим ядра, поведение тем, что создаются такие потоки вызовом PsCreateSystemThread, который может быть сделан только из кода, работающего на уровне IRQL_PASSIVE_LEVEL. Соответственно, начинают работу они на этом же уровне IRQL среди приоритетов пользовательского режима.

Такой программный поток не имеет ни собственного TEB (Thread environment block), ни контекста пользовательского режима, хотя вполне возможно, что "ответвился" от потока, у которого такой контекст имелся. Новый программный поток работает только в режиме ядра. Он не может интерпретировать виртуальные адреса, полученные из приложений пользовательского режима &#8212 даже если он их как-то получит (например, через внутренние переменные драйвера).

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



Контекст выполнения программного кода


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

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

Практическим следствием контекста является то, как процессор будет интерпретировать виртуальные адреса, предлагаемые ему исполняемым кодом, иными словами, какой набор страничных таблиц будет принят для трансляции этих адресов.

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



Координаты


С сообщениями об опечатках и ошибках в книге, а также с предложениями и информацией можно обращаться по этим координатам:

mail: korvin@nxt.ru

icq: 5953073

С уважением, [Korvin]



Квалификаторы IN, OUT, OPTIONAL


Еще одним небесполезным элементом украшения программного кода, активно используемого в DDK, являются макроопределения IN, OUT и другие. Как можно увидеть в файле ntdef.h, они не обозначают ровным счетом ничего, но зато повышают наглядность программного кода при его чтении, поскольку сообщают о назначении переменных, рядом с которыми используются. (Поскольку они ничего не значат, что является нетипичным для программирования, то позволим себе применить к ним нетипичный термин "квалификатор", который тоже ничего особенного не обозначает.)

#define IN .......... #define OUT .......... #define OPTIONAL .......... #define CRITICAL

Если обратиться к использованию этих элементов, то можно рассмотреть определение функции RtlZeroMemory, которая может быть использована для обнуления некоторой области виртуальной памяти (обнуляет страничную память на уровнях IRQL только ниже DISPATCH_LEVEL, нестраничную &#8212 на любых), а именно:

VOID RtlZeroMemory(IN VOID UNALIGNED *Destination, IN ULONG Length);

В этом описании квалификатор IN сообщает, что указатель Destination и длина буфера Length являются параметрами, передаваемыми внутрь вызова RtlZeroMemory.

В иных случаях бывают даже сочетания IN OUT, что сигнализирует: через этот параметр вызова информация передается внутрь данного вызова и возвращается обратно. Квалификатор OPTIONAL обозначает необязательные параметры.



LastKnownGood


Последняя работающая конфигурация. Подраздел Системного Реестра HKLM\System\CurrentControlSet00X, где X - число от 1 до 3, описывающий состояние системы, когда была выполнена полностью удачная загрузка. Какой же конкретно фрагмент реестра используется в качестве LastKnownGood (то есть значение X), можно определить по значению параметра LastKnownGood в разделе HKLM\System\Select.



Layering


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

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



Legacy Driver, NT Style Driver


Унаследованный драйвер (устаревший драйвер), драйвер в-стиле-NT. Драйвер, который не является WDM драйвером, работает не с PnP устройством (если вообще работает с реальными устройствами) и не участвует в обмене данными по поводу изменений в энергоснабжении. При компиляции использует только определения из файла ntddk.h, отчего может в полной мере пользоваться устаревшими функциями HalGetBusData, HalGetInterruptVector и т.п., но зато должен надеяться только на свои способности, поскольку у него нет могущественных прародителей в лице шинных драйверов, способных предоставить поддержку.

"Правильный" драйвер в-стиле-NT может быть запущен и остановлен при помощи программы Monitor (из состава пакета Numega Kernel Driver) или процедурами SCM Менеджера. При наличии PnP вкраплений (в частности, из-за наличия зарегистрированной процедуры для обработки IRP_MJ_PNP запросов) эта возможность исчезает.

Следует обратить внимание, что при компиляции кода с использованием ntddk.h, не только становятся недоступными отдельные функции, открытые раннее (для модели WDM), но и изменяется назначение некоторых все еще доступных функций и полей внутри доступных структур. Тем не менее, подключение такого типа драйверов к другим драйверам по-прежнему возможно (при помощи вызовов IoAttachDeviceToDeviceStack

или IoAttachDevice).

Драйверы "в-стиле-NT" совершенно не обязаны работать под Win98 (из-за использования специфических функций), и это иногда может стать существенной проблемой разработчика.



Major IRP Code


Основной код IRP пакета. Число, которое обозначает назначение пакета IRP, а значит и основной смысл данного обращения к драйверу. В пакете Microsoft DDK каждое такое число имеет еще и символьное обозначение (установленное через #define директиву). Для наглядности в литературе всегда используются присвоенные таким образом имена. Например, код IRP_MJ_DEVICE_CONTROL имеет IRP пакет, который поступил в драйвер (разумеется, из кода Диспетчера ввода/вывода) в результате того, что пользовательское приложение вызвало функцию DeviceIoControl

(см. пример драйвера в главе 3). Пакеты IRP, соответствующие вызовам функций чтения или записи (read, write) имеют коды IRP_MJ_READ, IRP_MJ_WRITE.



Мастер Установки/удаления новой аппаратуры


В главе 3 рассматривался вариант инсталляции Example.sys &#8212 драйвера "в-стиле-NT" (legacy driver) при помощи Мастера Установки нового оборудования и inf-файла.

Процесс установки драйвера для PnP устройства, которое предъявляет системе идентификационные коды, отличается тем, что Мастер Установки нового оборудования самостоятельно находит драйвер (если для данного типа оборудования драйвер уже устанавливался ранее хотя бы раз) либо предлагает пользователю выбрать более подходящий драйвер, который заявляет соответствующие коды в соответствующей установочной секции inf-файла.

Если установка успешно завершена Мастером Установки, процедуры драйвера DriverEntry и AddDevice должны, кроме того, подтвердить, что аппаратное обеспечение, которым их "пригласили" управлять, удовлетворяет требованиям выбранного драйвера, подтверждая правильность выбора именно этого варианта установки. Другими словами, не исключена ситуация, когда интерактивный выбор может довести установку до конца, но инициализация устройства все же завершится неудачей (потому что собственно программный код драйвера не "согласился" работать с предложенной аппаратурой в предложенных условиях).



Материалы из пакетов разработки драйверов третьих фирм


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

Фирма Jungo Ltd. (бывшая Wretch Ltd.), информацию о продуктах которой (WinDriver и KernelDriver) можно найти на интернет-сайте jungo.com.

Фирма Compuware Corporation, распространяющая пакет разработки драйверов DriverStudio. По интернет-адресу compuware.com/products/driverstudio

можно обнаружить много технической информации, касающейся программирования драйверов для Windows и Linux.

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



Механизмы передачи данных


При всем многообразии компьютерной техники, существует три основных механизма, используя которые устройство может обмениваться с центральным процессором или, в широком смысле, с компьютером:

Программируемый ввод/вывод.

Прямой доступ к памяти (Direct Memory Access, DMA).

Совместно используемые области памяти.

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


Драйверы для работы с устройствами, поддерживающими программируемый ввод/вывод, сильно отличаются от драйверов, реализующих методы DMA передачи данных. Некоторые устройства поддерживают оба механизма ввода/вывода. В случае DMA устройств, следует выяснить, требуется реализация DMA механизма, в котором устройство выступает в роли 'bus master', или механизма, когда устройство выполняет действия по сценарию 'bus slave'. Следует определить, имеются ли ограничения на интервал адресов физического буфера памяти, который может быть задействован при этих операциях.



Механизмы прерываний


Шина PCI использует четыре равно-приоритетных линии запросов на прерывание INTA-INTD. Эти линии являются активными по низкому уровню (сигналом прерывания является низкий уровень в линии), переключаемые по уровню (информативным значением является логический уровень &#8212 в отличие от того случая, когда информативным является фронт сигнала, то есть переход от одного уровня к другому). Данные линии допускают возможность совместного использования (это, возможно, станет технически более понятным, если сказать, что такая линия прерываний реализуется по электронной схеме "open drain", N-МОП аналог "открытого коллектора" для ТТЛ логики). Устройство, подключаемое к PCI и представляющее одну функцию, должно использовать только линию INTA.

Многофункциональные устройства могут использовать комбинации из четырех линий, начиная с INTA. Единственное ограничение состоит в том, что каждая из восьми функций (возможных в одном устройстве) может использовать только одну линию прерывания. Соответственно, устройство с внутренними восемью функциями может задействовать имеющиеся линии INTA, INTB, INTC, INTD следующим образом:

Все восемь функций подключены к INTA.

Семь подключены к INTA, одна к INTB.

Две подключены к INTA, две к INTB, две к INTC и две к INTD.

Четыре подключены к INTA, четыре к INTB.

И т.п.

Спецификация PCI относительно безразлична к приоритетам прерываний. Приоритеты, в данном случае, зависят от внешнего контроллера, который переадресует запрос на прерывание PCI устройства в соответствующую линию системных прерываний. Рекомендуемые схемы представления прерываний PCI устройств с использованием программируемых редиректоров (redirector или router) прерываний разной сложности можно найти в книге Тома Шанли и Дона Андерсона, PCI System Architecture, 4 издание, стр. 227-230, содержащей изложение спецификации PCI с полезными комментариями.

Например, на персональных компьютерах редиректор может преобразовать запрос функциональной единицы PCI по линиям INTA-INTD в запрос по одной из линий IRQ0-IRQ15, схема 14-4 в указанном издании.

Шина 1394 симулирует прерывания устройств (впрочем, как и шина USB). Устройство должно послать пакет данных для того, чтобы сообщить хост-контроллеру о своем состоянии, когда требуется вмешательство операционной системы. Драйвер, отвечающий за данное устройство, должен отреагировать на такой пакет данных, который размещается IEEE 1394 интерфейсом в системном адресном пространстве.

Семейство стандартов 1394 включает Open Host Controller Interface. Спецификация OHCI является наиболее значимым стандартом для разработчиков драйверов, работающих с устройствами шины 1394. Этот интерфейс обеспечивает общий механизм работы с прерываниями и DMA передачей данных. Ассоциация 1394 Trade Association предоставляет информацию по спецификации OHCI и другие связанные с ним данные на интернет сайте 1394.ta.org.




Для шины USB настоящего механизма прерываний, строго говоря, не определено. Вместо этого, USB интерфейс хост-компьютера опрашивает подключенные устройства на предмет наличия данных о прерывании. Опрос происходит в фиксированные интервалы времени, обычно каждые 1-32 миллисекунды. Устройству разрешается посылать в момент опроса до 64 байт данных.

С точки зрения драйвера, возможности работы с прерываниями и DMA передачей данных фактически определяются USB хост-контроллером, который и обеспечивает поддержку физической реализации USB интерфейса. Достаточно много усилий было предпринято для стандартизации интерфейса хост-адаптера, в результате чего появились два типа интерфейсов: Open Host Controller Interface (OpenHCI) и Universal Host Controller Interface (UHCI). Собственно хост-контроллер и поддерживает общепринятые механизмы прерываний и DMA передачи данных. Внешние пользовательские USB устройства должны лишь пассивно и в соответствии с протоколом участвовать в обмене данными.




В стандартах PC Card и CardBus определен один проводник (линия) для прерываний &#8212 IREQ, или CINT. Эта линия прерываний управляется уровнем (level sensitive), следовательно, может быть использована совместно несколькими картами на одной и той же шине. Однако при использовании многофункциональных PCMCIA карт (если линия прерываний используется совместно несколькими функциями карты) должно быть обеспечено арбитрирование средствами программного обеспечения.



Менеджер (диспетчер) объектов


Object Manager. Практически все услуги, предоставляемые операционной системой, оперируют с такой распространенной абстракцией, как объекты, хотя это и не стопроцентные объекты, по некоторым признакам, известным из объектно-ориентированного программирования. Например, программа, выполняемая в пользовательском режиме, которой необходимо синхронизировать несколько собственных потоков, может запросить у операционной системы объект синхронизации Event (событие), а на самом деле — просто особым образом обслуживаемую структуру данных. Система предоставляет Event в форме системного объекта, на который из программы пользовательского режима можно ссылаться только по дескриптору (handle). Файлы, процессы, потоки, события (Events), секции памяти (Memory Sections) и даже подразделы Системного Реестра (Registry Keys) поддерживаются операционной системой как системные объекты. Все объекты создаются и уничтожаются централизованно &#8212 Менеджером Объектов. Это позволяет получить унификацию (единообразие) доступа к объектам, контроля над их временем жизни и обеспечивать безопасность и права доступа к ним.

Всем исполнительным компонентам дозволено вступать в работу только на определенном уровне приоритета IRQL &#8212 для обеспечения слаженности в их совместной работе. В результате, функции, предоставляемые этими компонентами, так же могут быть вызваны не с любого произвольного уровня IRQL. В частности, функции Менеджера объектов (например, ObDereferenceObject) следует вызывать с уровня IRQL не выше DISPATCH_LEVEL. Нарушение этого правила ставит систему в двусмысленное положение. В самом деле, поток с более высоким IRQL может ожидать окончания работы кода, который должен работать на более низком IRQL (в силу своей медлительности или других внутренних вызовов). Чтобы не приводить систему к деградации, соблюдение такого типа правил жестко контролируется операционной системой.



Менеджер конфигурирования


Configuration Manager. Менеджер конфигурирования Windows NT 5 конструирует модель всей доступной аппаратуры и всего инсталлированного программного обеспечения, которое имеется на компьютере. Для хранения образа этой модели используется база данных, хорошо известная под названием Системный Реестр. Драйверы устройств используют информацию Реестра для уточнения множества характеристик окружения, в котором им придется работать. С введением спецификации Plug and Play роль Системного Реестра для драйверов, реализованных по WDM модели, существенно снизилась.



Менеджер процессов


Process Manager. Предоставляет функции, начинающиеся с префикса Ps (например, PsGetVersion &#8212 получить версию операционной системы). Функции PsXxx могут выполняться на разных уровнях IQRL, что следует уточнять в документации DDK.

В чем заключается работа Менеджера процессов?

Процесс является средой, в которой существует (выполняется) поток. Каждый процесс определяет собственное адресное пространство и содержит элементы идентификации для определения прав доступа (security identity). Важно отметить, что в Windows

процесс не выполняется; выполняется поток, который является единицей выполнения, в то время, как процесс является "фигурой" собственности. Процесс владеет одним или несколькими потоками.

Менеджер процессов Windows 2000/XP/2003 является исполнительным компонентом, который управляет созданием процесса и предоставляет ему окружение, в котором работают программные потоки. Менеджер Процессов в своей работе опирается, главным образом, на другие исполнительные компоненты (например, Менеджера Объектов и Менеджера виртуальной памяти), так что можно сказать, что он представляет верхний уровень абстрагирования над другими системными сервисами более низкого уровня.

Драйверы редко контактируют непосредственно с Менеджером процессов. Как правило, они опираются на другие системные сервисы для доступа к среде процесса (process environment). Например, драйвер хочет быть уверен, что буферная область памяти в области адресов, находящихся во владении процесса, удерживается в течение всего времени передачи данных. Системные процедуры с префиксом Mm (относящиеся к Менеджеру памяти) предоставляют драйверу средства для такого удержания.



Менеджер виртуальной памяти


Virtual Memory Manager, VMM. В операционной системе Windows 2000/XP/2003 (впрочем, как и в Windows 9x) адресное пространство процесса является непрерывным и непрерывно адресуемым (flat). Размер адресуемого таким образом пространства в 32 разрядной версии Windows составляет 4 ГБ (2 в степени 32), что соответствует использованию 32 разрядного указателя. При этом только нижние 2 ГБ доступны для использования кодом пользовательского режима. Программный код и данные пользовательских программ должны размещаться в этой нижней половине адресного пространства. В случае, если пользовательские программы используют совместно динамически подключаемые библиотеки (DLL), то этот библиотечный код также должен размещаться в первых двух гигабайтах адресного пространства (эта схема претерпевает некоторые изменения — в сторону увеличения пользовательского адресного пространства до 3 ГБ — лишь в Enterprise Server при его соответствующей настройке).

Верхние 2 ГБ адресного пространства каждого процесса содержат код и данные, доступ к которым возможен только из программного кода, выполняющегося на уровне ядра. Верхние 2 ГБ используются кодом уровня ядра совместно от процесса к процессу. Если вы выполните распечатку адресов каких-нибудь объектов, переменных или процедур в окне DebugView или DebugPrint, то сразу же увидите, что код драйвера отображается на адресное пространство выше 2 ГБ (адреса превышают 0x80000000).

Менеджер памяти (VMM) осуществляет управление памятью от имени всей операционной системы. Для обычной программы пользовательского режима это означает выделение памяти и управление адресным пространством и физической памятью ниже границы 2 ГБ. В том, достаточно обычном, случае, когда процессу пользовательского режима не хватает физической памяти, VMM создает иллюзию наличия памяти путем виртуализации

запроса. Необходимая память выделяется страницами (то есть блоками соответствующего размера, для Intel платформы — 4 КБ) на жестком диске (что называется — paging). По мере необходимости доступа к ней со стороны потоков процесса выделенные страницы перемещаются в физическую память. Таким образом, физическая память становится совместно используемым ресурсом всех процессов.

Менеджер Виртуальной Памяти выступает также и в роли ответственного за распределение памяти в том смысле, что управляет "кучей" (heap area) для программного кода уровня ядра. Для своих нужд драйверы через соответствующие вызовы (например, вызов ExAllocatePool, в конечном счете, обрабатываемый VMM) могут запросить выделение областей памяти в страничной (то есть организованной странично и допускающей сброс на жесткий диск) или нестраничной памяти.



Minidriver


Мини-драйвер. Представляет нечто меньшее, чем "полный" драйвер (являющийся для него оболочкой), и отражает аппаратную специфику обслуживаемого устройства. Реализуется, как правило, в виде динамически загружаемой библиотеки (DLL). Вероятно, самым известным примером мини-драйвера является SCSI мини-порт-драйвер (мини-драйвер для SCSI порт-драйвера). В данном термине в глоссарии MS Windows DDK наблюдается откровенный сбой, поскольку он трактуется как DLL, которая использует (?!) классовый драйвер для завершения своих запросов.

(1) Это совершенно другое понятие, вовсе не стек ввода/вывода IRP пакета!



Minor IRP Code


Младший код IRP пакета. Часто значение основного кода IRP пакета обозначает слишком большой диапазон проблем. Для уточнения были придуманы младшие коды IRP запросов, которым даны имена IRP_MN_Xxx (бывают случаи применения и третьего уровня детализации). Например, запрос типа IRP_MJ_PNP, который обязаны обрабатывать все драйверы PnP устройств, слишком обширен, и его конкретизирует, например, младший код IRP_MN_STOP_DEVICE.

Далее в тексте основной код IRP запроса будем называть просто код IRP, а младший код — суб-код IRP.



Мьютексы


Мьютекс является синхронизационным примитивом (объектом), которым может владеть только один поток в данный конкретный момент времени. Термин mutex является сокращением от 'mutual exclusion', совместное исключение. Объект этого типа имеет несигнальное состояние, когда поток им владеет, и сигнальное &#8212 когда объект свободен. Мьютексы обеспечивают несложный механизм координации исключительного доступа к совместно используемым ресурсам, обычно &#8212 областям памяти.

Предположим, потоки t1 и t2 ожидают освобождения мьютекса, которым владеет поток t0. В момент, когда поток t0 освободит мьютекс, один из ожидающих потоков "пробудится" и станет его владельцем.

Для использования мьютекса необходимо получить блок памяти размером sizeof(KMUTEX) в области нестраничной памяти (например, вызовом ExAllocatePool), после чего следует выполнить его инициализацию вызовом KeInitializeMutex

(таблица 10.34). Следует помнить, что когда мьютекс инициализируется, то он сразу же устанавливается в сигнальное состояние.

В случае, если некий поток выполняет вызов KeWaitForXxx относительно того мьютекса, которым он уже владеет, никакого ожидания не случится. Вместо этого, происходит увеличение на единицу внутреннего счетчика объекта мьютекса, что всего лишь отражает факт повторного запроса на владение данным мьютексом со стороны потока. Когда поток пожелает освободить мьютекс, то ему придется сделать столько вызовов KeReleaseMutex (таблица 10.35), сколько ранее было сделано запросов на владение им. Только после этого объект мьютекса перейдет в сигнальное состояние. Точно такое же поведение демонстрируют мьютексы и в программировании приложений пользовательского режима. Мьютексы похожи на семафоры с максимальным значение счетчика 1. Правда, с одним существенным отличием: сам программный поток, получивший владение мьютексом, может сделать это еще много раз (столько же раз он должен и освободить мьютекс).

Таблица 10.34. Прототип вызова KeInitializeMutex

VOID KeInitializeMutex IRQL == PASSIVE_LEVEL
Параметры Инициализирует объект мьютекса и устанавливает его начальное состояние &#8212 сигнальное.
IN PKMUTEX pMutex Указатель на область, подготовленную для объекта мьютекса
IN LONG Level Уровень, присвоенный мьютексу разработчиком
Возвращаемое значение void
<
Интересен параметр Level, который мало где описан, включая DDK, но может улучшить защищенность кода от ситуаций взаимоблокировок, если драйвер использует несколько мьютексов сразу из нескольких программных потоков. При инициализации объекта мьютекса устанавливается номер уровня (параметр Level). Позднее, когда поток пытается получить очередной мьютекс, ядро не разрешает владение этим мьютексом в случае, если во владении данного потока уже находится любой другой мьютекс с более низким значением уровня. При умелом использовании этого механизма, ядро автоматически предотвращает взаимоблокировки, возникающие в результате использования в драйвере нескольких объектов мьютексов.

Программный поток не должен пытаться освобождать мьютексы, которые он не получал, поскольку это вынудит систему прекратить работу (bugcheck). Попытка освободить мьютекс, который имеет сигнальное состояние (то есть уже никому не принадлежит) приведет к аналогичным последствиям.

Драйвер должен освобождать все мьютексы, которые находятся в его владении, перед тем, как передаст управление в пользовательский режим (то есть завершит рабочую процедуру и вернет управление Диспетчеру ввода/вывода). Ядро воспримет это как ошибку. Процедура AddDevice, DriverEntry или какая-либо рабочая процедура драйвера, получая для себя мьютекс, не должны планировать его освобождение в другой рабочей процедуре или в другом программном потоке данного драйвера.

Таблица 10.35. Прототип вызова KeReleaseMutex

LONG KeReleaseMutex IRQL == PASSIVE_LEVEL
Параметры Уменьшает на единицу "счетчик занятости" объекта мьютекса, обозначая намерение инициатора вызова тут же вызвать (или не вызывать) KeWaitXxx.
IN PKMUTEX pMutex Указатель на объект мьютекса
IN BOOLEAN doCallOfKeWaitXxx • TRUE &#8212 следом за данным вызовом текущий программный поток собирается сделать вызов KeWaitForXxx

(используется редко)

• FALSE &#8212 применяемое на практике значение (см. документацию DDK)
Возвращаемое значение 0, если объект мьютекса перешел в сигнальное состояние
<


Запрос на владение мьютексом выполняется вызовом KeWaitForSingleObject

либо вызовом KeWaitForMultipleObject из кода, работающего на уровне IRQL равном PASSIVE_LEVEL. Специально для мьютексов придумано также макроопределение KeWaitForMutexObject, которое есть текстуальная подстановка все того же системного вызова KeWaitForSingleObject.

Единственная функция, которую можно вызывать из кода уровня IRQL выше, чем PASSIVE_LEVEL, &#8212 это вызов KeReadStateMutex (таблица 10.36).

Таблица 10.36. Прототип вызова KeReadStateMutex

LONG KeReadStateMutex IRQL &#60= DISPATCH_LEVEL
Параметры Возвращает состояние объекта мьютекса
IN PKMUTEX pMutex Указатель на объект мьютекса
Возвращаемое значение 1, если объект мьютекса находится в сигнальном состоянии
Описанные выше простые мьютексы применяются теперь не так широко, как быстрые мьютексы, которые рассмотрены ниже.

Быстрый мьютекс (fast mutex) &#8212 это синхронизационный объект, который работает практически так же, как и описанный выше обычный мьютекс режима ядра, однако не допускает повторных (рекурсивных) запросов на владение из одного и того же программного потока. Такой мьютекс выполняет меньше работы и функционирует быстрее.

Таблица 10.37. Функции для работы с объектами быстрых мьютексов

Что необходимо сделать Используемый вызов
Создать быстрый мьютекс ExInitializeFastMutex
Сделать запрос на владение ExAcquireFastMutex

ExAcquireFastMutexUnsafe

ExTryToAcquireFastMutex
Освободить объект ExReleaseFastMutex

ExReleaseFastMutexUnsafe
Объект быстрого мьютекса описывается типом FAST_MUTEX (см. например, заголовочный файл DDK wdm.h) и используется для синхронизации доступа к одному или нескольким элементам данных. Любой код, задумавший воспользоваться этими данными, должен сначала сделать запрос на владение соответствующим объектом FAST_MUTEX.

Следует обратить внимание на то, что эти объекты имеют собственные вызовы для выполнения запроса на владение. Функция KeWaitForXxx в данном случае не может быть использована.



Перед использованием функций ExAcquireFastMutex и ExReleaseFastMutex

следует выполнить инициализацию объекта быстрого мьютекса вызовом ExInitializeFastMutex, см. таблицу 10.38. И хотя память под структуру объекта выделяет инициатор этого вызова, как и в ранее описанных случаях для других объектов синхронизации, непосредственно обращаться к полям этого объекта не следует &#8212 необходимо пользоваться только вызовами, предлагаемыми в DDK.

Таблица 10.38. Прототип вызова ExInitializeFastMutex

VOID ExInitializeFastMutex IRQL &#60= DISPATCH_LEVEL
Параметры Инициализирует объект быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта быстрого мьютекса
Возвращаемое значение void
В случае если запрос на владение вызовом ExAcquireFastMutex

удовлетворен быть не может (у объекта быстрого мьютекса уже есть владельцы), поток блокируется до наступления сигнального состояния. Блокируется также и процедура APC, адресованная данному программному потоку. При успешном завершении вызова поток инициатора вызова выполняется на уровне IRQL равном APC_LEVEL, а прежнее значение сохраняется в объекте быстрого мьютекса (оно будет восстановлено при освобождении объекта быстрого мьютекса вызовом ExReleaseFastMutex).

Таблица 10.39. Прототип вызова ExAcquireFastMutex

VOID ExAcquireFastMutex IRQL &#60 DISPATCH_LEVEL
Параметры Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void
Таблица 10.40. Прототип вызова ExAcquireFastMutexUnsafe

VOID ExAcquireFastMutexUnsafe IRQL == APC_LEVEL
Параметры Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void
В случае если запрос на владение вызовом ExAcquireFastMutexUnsafe

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


Инициатор вызова ExAcquireFastMutexUnsafe должен обеспечить условия, чтобы во время вызова не мог быть выполнен APC вызов для данного потока. Для этого есть два способа. Первый состоит в том, чтобы увеличить уровень IRQL равный APC_LEVEL. Второй способ состоит в том, чтобы непосредственно перед вызовом ExAcquireFastMutexUnsafe выполнить KeEnterCriticalRegion, что временно блокирует простые APC вызовы (в отличие от специальных APC вызовов режима ядра). В последнем случае не следует забывать делать отменяющий вызов KeLeaveCriticalRegion.

Освобождение быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe, следует выполнять только при помощи специально для того предназначенного вызова ExReleaseFastMutexUnsafe.

Можно пытаться получить владение объектом быстрого мьютекса без блокировки вызывающего потока (в случае занятости нужного объекта быстрого мьютекса) при помощи вызова ExTryToAcquireFastMutex. B случае неудачи этот вызов возвратит значение FALSE. При удовлетворении запроса возвращается, соответственно, значение TRUE, см. таблицу 10.41.

Владение объектом быстрого мьютекса отменяется его текущим владельцем по вызову ExReleaseFastMutex. Редакция пакета DDK для XP настаивает на том, чтобы этот вызов выполнялся из кода, работающего на уровне IRQL равном DISPATCH_LEVEL, вплоть до того, что инициатор вызова должен установить явно этот уровень перед вызовом ExReleaseFastMutex. Обычно не следует об этом беспокоиться, если уровень IRQL не менялся со времени последнего вызова ExAcquireFastMutex, поскольку он автоматически устанавливает именно это значение.

Таблица 10.41. Прототип вызова ExTryToAcquireFastMutex

BOOLEAN ExTryToAcquireFastMutex IRQL &#60 DISPATCH_LEVEL
Параметры Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение TRUE &#8212 при успешном завершении, иначе &#8212 FALSE
Таблица 10.42. Прототип вызова ExReleaseFastMutex



VOID ExReleaseFastMutex IRQL == APC_LEVEL
Параметры Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void
Таблица 10.43. Прототип вызова ExReleaseFastMutexUnsafe

VOID ExReleaseFastMutexUnsafe IRQL &#60= APC_LEVEL
Параметры Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void

Многослойные драйверы


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

В рамках многослойного подхода можно определить три типа драйверов:

Шинные драйверы &#8212 обеспечивают интерфейс аппаратных шин в базисе "один слот &#8212 одна единица" и создают один или более физических объектов устройств (PDO, Physical Device Object), соответствующих каждому обнаруженному устройству, подключенному к шине. Шинный драйвер конструирует PDO и управляет им, вследствие чего часто его называют физическим драйвером.

Функциональные драйверы &#8212 обеспечивают чтение, запись и прочую логику функционирования отдельного устройства. Они создают и управляют одним или более функциональными объектами устройств (FDO, Functional Device Object).

Фильтр-драйверы &#8212 обеспечивают модификацию запроса на ввод/вывод перед предъявлением его драйверам более низких уровней. Фильтры могут быть размещены вокруг функционального драйвера либо над шинным драйвером.



Модификация приложения для тестирования драйвера


Тестирующее консольное приложение не так сильно изменилось, за исключением переписывания кода на использование вызовов DeviceIoControl

и организации ожидания вызовом WaitForSingleObject.

//======================================================================= // Файл тестовой программы test.cpp //=======================================================================

#include &#60windows.h&#62 #include &#60stdio.h&#62 #include "winioctl.h"

#define IOCTL_SEND_TO_PORT CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_SEND_TO_USER CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_TAKE_EVENT CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_CLOSE_EVENT CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS) int ReadWrite(HANDLE devHandle); #define BUFFSIZE (17) static unsigned char outBuffer[BUFFSIZE], inBuffer[BUFFSIZE*2]; static HANDLE devHandle, hEvent;

int __cdecl main() { printf("\n\n\n\n\nParallel Port CheckIt Loopback Device Test Program.\n" ); devHandle = CreateFile( "\\\\.\\LPTPORT0", GENERIC_READ | GENERIC_WRITE, 0, // share mode none NULL, // no security OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); // no template

if ( devHandle == INVALID_HANDLE_VALUE ) { printf("Error: can not open device PLPTPORT0. Win32 errno %d\n", GetLastError() ); return -1; } printf("Congratulation. LPTPORT0 device is open.\n\n"); //========================================== // Получение доступа к объекту события, // создаваемому в драйвере DWORD bytesRead; if ( !DeviceIoControl(devHandle, IOCTL_TAKE_EVENT, NULL, 0, // отправляем в драйвер &hEvent, sizeof(HANDLE), // получаем из драйвера &bytesRead, NULL ) ) { printf("Error during IOCTL_TAKE_EVENT: errno %d.\n", GetLastError() ); CloseHandle(devHandle); return -1; } printf("\nEvent handle = %04X(hex)\n", hEvent); //========================================== // Заполнение буфера данными: DWORD i=3,j=0; for ( ; j&#60sizeof(outBuffer); ) outBuffer[j++] = (unsigned char)i++; //========================================== ReadWrite(devHandle); //========================================== // Завершение работы if ( !DeviceIoControl(devHandle, IOCTL_CLOSE_EVENT, NULL, 0, // отправляем в драйвер NULL, 0, // получаем из драйвера &bytesRead, NULL ) ) { printf("\nError during IOCTL_CLOSE_EVENT: errno %d.\n", GetLastError() ); } else printf("\nEvent handle is normally closed.\n");


if ( ! CloseHandle(devHandle) ) { printf("\ n Error during CloseHandle: errno %d.\n", GetLastError() ); return -1; } printf("\n\n\n Device LPTPORT0 successfully closed. Normal exit.\n"); return 0; } //========================================================================== // передача и получение данных из CheckIt заглушки: int ReadWrite(HANDLE devHandle) { //========================================== // Передача данных драйверу printf("Writing to LPTPORT0 device...\n");

DWORD bytesReturned, outCount = sizeof(outBuffer); if ( !DeviceIoControl(devHandle, IOCTL_SEND_TO_PORT, outBuffer, outCount, // отправляем в драйвер NULL, 0, // получаем из драйвера &bytesReturned, NULL ) ) { printf("Error during IOCTL_SEND_TO_PORT: errno %d.\n", GetLastError() ); return 11; } printf( "Successfully transferred %d bytes.\n" "Buffer content was: \n", outCount); for (DWORD i=0; i&#60outCount; i++ ) printf("%02X ",outBuffer[i]); //========================================== // Использование созданного события DWORD result = WaitForSingleObject(hEvent,10);

switch(result) { case WAIT_TIMEOUT: printf("\nWait timeout.\n");break; case WAIT_ABANDONED: printf("\nWait WAIT_ABANDONED.\n"); break; default: printf("\nWait default case.\n"); } //========================================== // Получение данных из драйвера printf("\n\nReading from device LPTPORT0...\n"); DWORD bytesRead, inCount = sizeof(inBuffer); if ( !DeviceIoControl(devHandle, IOCTL_SEND_TO_USER, NULL, 0, // отправляем в драйвер inBuffer, inCount, // получаем из драйвера &bytesRead, NULL ) ) { printf("Error during OCTL_SEND_TO_USER: errno %d.\n", GetLastError() ); return 12; } if ( bytesRead != outCount ) { // размер записанных и прочитанных данных не совпадает printf("Error: is to read %d bytes,\n" "but IOCTL_SEND_TO_USER reported %d bytes.\n", outCount, inCount); return 13; } printf("Succesfully read %d bytes.\n Buffer content is: \n", bytesRead); for ( i=0; i&#60bytesRead; i++ ) printf( "%02X ", (UCHAR)inBuffer[i] );



return 0; // Нормальное завершение }

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

Рис. 11.5

Именованный объект события "LPTPORT_EVENT", созданный драйвером, в рабочем окне программы WinObj
Вывод на экран (при наличии в параллельном порту заглушки CheckIt) практически не изменился и при запуске тестирующего приложения, текст которого приведен выше, в консольном окне можно наблюдать следующие сообщения:

Parallel Port CheckIt Loopback Device Test Program. Congratulation. LPTPORT0 device is open.

Event handle = 07CC(hex) Writing to LPTPORT0 device...

Successfully transferred 17 bytes. Buffer content was: 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 Wait default case.

Reading from device LPTPORT0... Successfully read 17 bytes. Buffer content is: 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00 01 02 03 Event handle is normally closed.

Device LPTPORT0 successfully closed. Normal exit.

Ниже приводится информация из отладочного вывода, перехваченного программой DebugView (log-файл этой программы). Средняя часть этого файла (сообщения с 35 по 137) опущена, как и в первом случае, поскольку в этих строках содержится однообразная и малоинтересная информация.

00000000 0.00000000 LPTPORT: in DriverEntry, RegistryPath is: 00000001 0.00000223 \REGISTRY\MACHINE\SYSTEM\ControlSet001\ Services\LPTPort. 00000002 0.00003101 LPTPORT: Interrupt 7 converted to kIrql = 8, kAffinity = 1, kVector = 191(hex) 00000003 0.00004023 LPTPORT: Interrupt successfully connected. 00000004 0.00006761 LPTPORT: Symbolic Link is created: \DosDevices\LPTPORT0. 00000005 3.62195949 LPTPORT: in DispatchCreate now 00000006 3.62206817 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0 00000007 3.62210393 LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT, 00000008 3.62210616 event named \BaseNamedObjects\LPTPORT_EVENT successfully created. 00000009 3.62211231 LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT, 00000010 3.62211426 event handle = 07CC(hex) is sent to user. 00000011 3.62228496 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0 00000012 3.62229082 LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT, 00000013 3.62229250 xfer size is 17 Irp is pending. 00000014 3.62229864 LPTPORT: We are now in StartIo , currentIrql=2 00000015 3.62230256 LPTPORT: StartIo: IoRequestDpc will be called now. 00000016 3.62230982 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 0 00000017 3.62231485 LPTPORT: currentByteNo = 0, byteToBeOutToPort=03(hex) 00000018 3.62231848 LPTPORT: DoNextTransfer: 00000019 3.62232323 LPTPORT: Sending 0x03 to port 378 00000020 3.62232826 LPTPORT: generating next interrupt... 00000021 3.62243972 LPTPORT: In Isr procedure, ISR_Irql=8 00000022 3.62244671 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 0 00000023 3.62245341 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 1F ReadByte = 03 00000024 3.62245621 LPTPORT: 00000025 3.62246096 LPTPORT: currentByteNo = 1, byteToBeOutToPort=04(hex) 00000026 3.62246459 LPTPORT: DoNextTransfer: 00000027 3.62246906 LPTPORT: Sending 0x04 to port 378 00000028 3.62247409 LPTPORT: generating next interrupt... 00000029 3.62258471 LPTPORT: In Isr procedure, ISR_Irql=8 00000030 3.62259170 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 1 00000031 3.62259812 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus= 27 ReadByte= 04 00000032 3.62260120 LPTPORT: 00000033 3.62260595 LPTPORT: currentByteNo =2, byteToBeOutToPort=05(hex) 00000034 3.62260958 LPTPORT: DoNextTransfer: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 00000138 3.62448468 LPTPORT: DoNextTransfer: 00000139 3.62448915 LPTPORT: Sending 0x02 to port 378 00000140 3.62449417 LPTPORT: generating next interrupt... 00000141 3.62460480 LPTPORT: In Isr procedure, ISR_Irql=8 00000142 3.62461123 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 15 00000143 3.62461765 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus= 17 ReadByte= 02 00000144 3.62462073 LPTPORT: 00000145 3.62462548 LPTPORT: currentByteNo = 16, byteToBeOutToPort=13(hex) 00000146 3.62462883 LPTPORT: DoNextTransfer: 00000147 3.62463330 LPTPORT: Sending 0x03 to port 378 00000148 3.62463833 LPTPORT: generating next interrupt... 00000149 3.62474868 LPTPORT: In Isr procedure, ISR_Irql=8 00000150 3.62475566 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount= 16 00000151 3.62476209 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus= 1F ReadByte= 03 00000152 3.62476488 LPTPORT: 00000153 3.62476879 LPTPORT: We are now in DpcForIsr, all data transmitted. 00000154 3.62529372 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0 00000155 3.62529875 LPTPORT: TransferToUserSafely, currentIrql=8 00000156 3.62530322 requested 34 bytes, while ready 17 bytes. 00000157 3.62530769 Transferred 17, the rest 0 bytes. 00000158 3.62531271 LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_USER, 00000159 3.62531439 17 bytes transferred to user. 00000160 3.62566360 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0 00000161 3.62567449 LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT, 00000162 3.62567645 event handle closed with STATUS_SUCCESS. 00000163 3.62568231 LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT, 00000164 3.62568427 event (handle 07CChex) closing status = 0. 00000165 3.62577506 LPTPORT: in DispatchClose now 00000166 6.71423547 LPTPORT: in DriverUnload now 00000167 6.71426704 LPTPORT: SymLink \DosDevices\LPTPORT0 deleted


Как ни удивительно это будет


Как ни удивительно это будет узнать, но данный драйвер (собранный как версия checked для Windows 2000) устанавливается, запускается и работает под Windows 98 SE. Для запуска драйвера следует переписать его бинарный файл Example.sys в директорию C:\Windows\System32\Drivers и создать файл (назовем его Example98.reg) со следующими записями:

REGEDIT4 [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Example] "ErrorControl"=dword:00000001 "Type"=dword:00000001 "Start"=dword:00000002 "ImagePath"="\\SystemRoot\\System32\\Drivers\\Example.sys"

После этого следует войти в редактор Системного Реестра (Пуск — Выполнить — regedit) и произвести импорт созданного файла. Импорт данного файла в Реестр Windows 98 можно выполнить, если дважды кликнуть мышкой на этом файле в стандартной программе Проводник (после этого последует предложение импортировать файл в реестр Windows 98, на которое следует ответить утвердительно).

В результате импорта в Системном Реестре будет создан новый подраздел \Example

в ветви HKLM\System\CurrentControlSet\Services. В этот подраздел будут занесены параметры ErrorControl, ImagePath, Start и Туре, значение которых обсуждается ниже.

Параметр Туре определяет драйвер режима ядра (значение 1).

Параметр ImagePath определяет местонахождение файла загружаемого модуля (в нашем случае — C:\Windows\System32\Drivers\Example.sys).

Параметр Start определяет момент загрузки сервиса — автостарт после загрузки системы (значение 2).

Параметр ErrorControl определяет поведение системы при возникновении ошибок во время загрузки данного модуля. В данном случае (значение 1) означает следующее: в процессе загрузки ошибки игнорируются, но выводятся сообщения о них, при этом загрузка продолжается. (Другие значения использовать не рекомендуется.)

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

в Системный Реестр Windows NT


Файлы импорта в Системный Реестр Windows NT должны быть в формате UNICODE, поэтому их невозможно создать простым текстовым редактором (например, Notepad), как это можно было сделать в случае Windows 98.

Для модификации Системного Реестра Windows 2000, XP, Server 2003 необходимо выполнить модификацию Системного Реестра вручную непосредственно в Реестре. Для этого необходимо войти в редактор Системного Реестра (Пуск — Выполнить — regedit32 для Windows 2000 и Пуск — Выполнить — regedit для Windows XP, Server 2003) и найти там подраздел HKLM\System\CurrentControlSet\Services. В этом подразделе следует создать вложенный подраздел \Example

и внести в него следующие параметры и их значения:

dword параметр ErrorControl со значением 1

dword параметр Туре со значением 1

dword параметр Start co значением 2

строковый параметр ImagePath со значением "\\SystemRoot\\System32\\Drivers\\Example.sys"

После того как указанные данные будут внесены в Системный Реестр, можно выполнить экспорт подраздела HKLM\System\CurrentControlSet\Services\Example

во внешний файл (например, ExampleNT.reg) и в следующий раз (например, на другом компьютере) выполнять импорт этого файла в Системный Реестр вместо набора вручную.

Смысл вносимых параметров тот же, что и в случае Windows 98.


Monolithic Driver


Монолитный драйвер. Драйвер, который не участвует ни в одном стеке устройств, получая и завершая обработку всех поступающих IRP пакетов, самостоятельно обращаясь к своему обслуживаемому устройству. Несложно представить себе монолитный драйвер, который обслуживает старые устройства (не-PnP) или служит лишь в качестве средства доступа к функциям режима ядра (для исследовательских целей). Однако практически невозможно представить современный драйвер для устройств SCSI, USB и FireWire, который был бы реализован как монолитный в прежнем смысле этого слова. В некоторых источниках информации по драйверам предпринята попытка модификации этого понятия. В них монолитным WDM драйвером (!) называется драйвер, который сам получает свои IRP запросы и доводит их до шинного драйвера, на чем его работа по общению с устройством завершается (остается лишь перехватить обратный отклик и его интерпретировать).



Набор рабочих процедур


Все драйверы обязаны поддерживать функцию, которая отвечает за обработку запроса с кодом IRP_MJ_CREATE, так как этот код генерируется в ответ на вызов пользовательского режима CreateFile. Без поддержки этого запроса пользовательское приложение не будет иметь никакой возможности получить дескриптор (handle) для доступа к драйверу устройства, а значит, и к самому устройству. Разумеется, должна существовать функция, обрабатывающая запрос с кодом IRP_MJ_CLOSE, который генерируется по обращению приложения к драйверу при помощи функции API пользовательского режима CloseHandle. Кстати заметить, вызов CloseHandle

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

Набор остальных поддерживаемых функций зависит от особенностей устройства, которое находится "под покровительством" драйвера. Таблица 8.7б показывает взаимосвязь между кодами запросов ввода/вывода и вызовами API пользовательского режима, которые приводят к их генерации. При написании многослойных драйверов следует помнить, что вышестоящий драйвер обязан поддерживать подмножество запросов нижестоящего драйвера (драйверов), так как запрос пользовательского приложения проникает в нижние слои только через вышестоящий драйвер.

Таблица 8.7б. Коды запросов IRP и функции API пользовательского режима

IRP коды Вызовы API пользовательского режима
IRP_MJ_CREATE CreateFile
IRP_MJ_CLEANUP Очистка ожидающих обработки пакетов IRP при закрытии дескриптора при отработке вызова CloseHandle
IRP_MJ_CLOSE CloseHandle
IRP_MJ_READ ReadFile
IRP_MJ_WRITE WriteFile
IRP_MJ_DEVICE_CONTROL DeviceIoControl
IRP_MJ_INTERNAL_DEVICE_CONTROL Действия по управлению устройством, доступные только для клиентов, работающих в режиме ядра (недоступно для вызовов пользовательского режима)
IRP_MJ_QUERY_INFORMATION Передача длины файла в ответ на вызов GetFileSize
IRP_MJ_SET_INFORMATION Установка длины файла по вызову SetFileSize
IRP_MJ_FLUSH_BUFFERS Запись или очистка служебных буферов при отработке вызовов (например):

• FlushFileBuffers

• FlushConsolelnputBuffer

• PurgeComm

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

Рабочая процедура обычно может отслеживать состояние обработки запроса только с использованием пакета IRP. В том случае, если рабочая процедура использует какие-либо данные за пределами пакета IRP, драйвер должен обеспечить достаточно надежные меры по синхронизации доступа к этим данным. Это означает, что можно было бы использовать спин-блокировку для координации действий с другими процедурами, выполняющимися на уровне IRQL не выше DISPATCH_LEVEL. Для синхронизации доступа из процедур, выполняющихся на уровне кода обслуживания прерываний, следует использовать KeSyncronizeExecution.

Пакеты IRP являются данными коллективного пользования, хотя и с последовательным доступом. В частности, Диспетчер ввода/вывода использует поля объединения Parameters для того, чтобы завершить обработку запроса. Например, по окончании обработки буферизованного ввода/вывода, ему необходимо выполнить копирование данных из области памяти нестраничного пула в буфер, заданный пользовательским приложением.По завершении копирования Диспетчер ввода/вывода должен освободить буфер в нестраничном пуле. Одно из полей объединения (union) Parameters как раз указывает на этот буфер, и изменение кодом драйвера данного указателя приведет к нарушению работы системы.


Начальная стадия загрузки


Загрузчик NTLDR переключает процессор из реального режима в 32-разрядный защищенный, что необходимо для получения возможности выполнить любую дополнительную функцию. Файловая мини-система, встроенная в NTLDR позволяет загружать Windows NT 5 с носителей FAT, FAT32 и NTFS.



Настройка операционной системы


При работе с приводимыми тестовыми драйверами и заглушкой CheckIt использовалась операционная система Windows XP. Несмотря на ее способности к автоконфигурированию, для проведения тестов потребовались некоторые изменения в ее настройках и настройках BIOS.

Прежде всего, чтобы избежать разночтений и странных ошибок, следует выставить в BIOS компьютера настройки SPP параллельного порта по адресу 378 с использованием прерывания 7. Эти фиксированные настройки как раз и будет использовать драйвер.

Во-вторых, после загрузки операционной системы следует обратиться к настройкам системного драйвера параллельного порта, который будет выполнять начальное инициирование параллельного порта без участия испытываемых драйверов. Для этого следует выполнить Пуск &#8212 Настройка &#8212 Панель управления &#8212 Система &#8212 Свойства системы &#8212 Диспетчер устройств &#8212 Оборудование &#8212 Порты (СОМ и LPT) &#8212 Порт принтера (LPT). Запустив системный апплет "Свойства: Порт принтера (LPT1)" следует проверить, что порту выделены ресурсы портов ввода-вывода (0378) и прерывания 7. Затем в закладке "Параметры порта" указать, что стандартный системный драйвер должен использовать прерывание, см. рисунок 11.2.

Рис. 11.2

Настройки системного драйвера для использования прерываний

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



Настройки проекта в Visual Studio 7 Net


Настройка проекта для компиляции и сборки драйвера режима ядра существенно отличаются от настроек, которые используются для работы с приложениями и динамическими библиотеками пользовательского режима. Ниже приводится точный текст файл Example.sln ("sln" является сокращением от "solution"), который описывает проект драйвера Example, рассматриваемого в следующей главе.

Microsoft Visual Studio Solution File, Format Version 7.00 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Example", "Example.vcproj", "{E524BA09-7993-4528-91A9-7E27FAA3565F}" EndProject Global GlobalSection(SolutionConfiguration) = preSolution ConfigName.0 = Checked EndGlobalSection GlobalSection(ProjectDependencies) = postSolution EndGlobalSection GlobalSection(ProjectConfiguration) = postSolution {E524BA09-7993-4528-91A9-7E27FAA3565F}.Checked.ActiveCfg = Checked|Win32 {E524BA09-7993-4528-91A9-7E27FAA3565F}.Checked.Build.0 = Checked|Win32 EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution EndGlobalSection GlobalSection(ExtensibilityAddIns) = postSolution EndGlobalSection EndGlobal

Значительно более важным в проекте Example является файл Example.vcproj, который содержит конкретные значения настроек и описания используемых файлов. Точный текст Example.vcproj (файла настроек для компиляции и сборки простейшего не-WDM драйвера checked-версии в среде Visual Studio 7 Net) приводится ниже.

&#60?xml version="1.0" encoding = "windows-1251"?&#62 &#60VisualStudioProject ProjectType="Visual C++" Version="7.00" Name="Example" SccProjectName="" SccLocalPath=""&#62 &#60Platforms&#62&#60Platform Name="Win32"/&#62&#60/Platforms&#62 &#60Configurations&#62 &#60Configuration Name="Release|Win32" OutputDirectory=".\checked" IntermediateDirectory=".\checked" ConfigurationType="2" UseOfMFC="0" ATLMinimizesCRunTimeLibraryUsage="FALSE" CharacterSet="1"&#62 &#60Tool Name="VCCLCompilerTool" AdditionalOptions="/Zel -cbstring /QIfdiv- /QIf /Gi- /Gm- /GX" Optimization="0" EnableIntrinsicFunctions="FALSE" OmitFramePointers="TRUE" OptimizeForProcessor="2" AdditionalIncludeDirectories="C:\WinDDK\2600\inc\ddk\w2k; C:\WinDDK\2600\inc\w2k;C:\WinDDK\2 600\inc\crt" PreprocessorDefinitions="_X86_=l;i386=l; CONDITION_HANDLING=1;NT_UP=1; NT_INST=0; WIN32=100;_NT1X_=100;WINNT=l; _WIN32_WINNT=0x0400;WIN32_LEAN_AND_MEAN=1; DEVL=l;DBG=l;FPO=0" IgnoreStandardIncludePath="TRUE" StringPooling="TRUE" ExceptionHandling="TRUE" RuntimeLibrary="O" StructMemberAlignment="4" BufferSecurityCheck="FALSE" EnableFunctionLevelLinking="TRUE" PrecompiledHeaderFile=".\checked/Example.pch" AssemblerListingLocation=".\checked/" ObjectFile=".\checked/" ProgramDataBaseFileName=".\checked\Example.pdb" WarningLevel="3" SuppressStartupBanner="TRUE" DebugInformationFormat="l" CallingConvention="2" CompileAs="0" ForcedIncludeFiles="warning.h"/&#62 &#60Tool Name="VCCustomBuildTool"/&#62 &#60Tool Name="VCLinkerTool" AdditionalOptions="" AdditionalDependencies="hal.lib ntoskrnl.lib int64.1ib msvcrt.lib " OutputFile=".\checked\Example.sys" Version="5.0" LinkIncremental="l" SuppressStartupBanner="TRUE" AdditionalLibraryDirectories="C:\WinDDK\2 600\lib\w2k\i386" IgnoreAllDefaultLibraries="TRUE" ProgramDatabaseFile=".\checked/Example.pdb" GenerateMapFile="TRUE" MapFileName="Example.map" StackReserveSize="262144" StackCommitSize="4096" OptimizeReferences="2" EnableCOMDATFolding="2" EntryPointSymbol="DriverEntry" SetChecksum="TRUE" BaseAddress="0x10000" ImportLibrary="" MergeSections=".rdata=.text" TargetMachine="1"/&#62 &#60Tool Name="VCMIDLTool" MkTypLibCompatible="TRUE" SuppressStartupBanner="TRUE" TargetEnvironment="1" TypeLibraryName=".\checked/Example.tlb"/&#62 &#60Tool Name="VCPostBuildEventTool"/&#62 &#60Tool Name="VCPreBuildEventTool"&#62 &#60Tool Name="VCPreLinkEventTool"/&#62 &#60Tool Name="VCResourceCompilerTool"/&#62 &#60Tool Name="VCWebServiceProxyGeneratorTool"/&#62 &#60Tool Name="VCWebDeploymentTool"/&#62 &#60/Configuration&#62 &#60/Configurations&#62 &#60Files&#62 &#60Filter Name="Header Files" Filter=".h"&#62 &#60File RelativePath=".\Driver.h"&#62 &#60/File&#62 &#60/Filter&#62 &#60Filter Name="Source Files" Filter=".c;.cpp"&#62 &#60File RelativePath="Init.cpp"&#62 &#60/File&#62 &#60/Filter&#62 &#60/Files&#62 &#60Globals&#62&#60/Globals&#62 &#60/VisualStudioProject&#62


Приведенный выше текст переформатирован (для удобства чтения в формате книги), поэтому расположение слов несколько отличается от их размещения в оригинальных .vcproj файлах, генерируемых средой Visual Studio 7 Net. Значения строковых параметров AdditionalIncludeDirectories и PreprocessorDefinitions обязательно должны быть записаны в одну строку.

Следует отметить, что особую важность для компиляции имеют значения пара метров IgnoreStandardIncludePath (здесь он отменяет стандартные пути для обнаружения заголовочных файлов, которые явно заданы теперь в параметре AdditionalIncludeDirectories), AdditionalOptions и PreprocessOrDefinitions (значения которых рекомендуется повторить в точности), CallingConvention (здесь определяет __stdcall).

Из параметров сборки следует отметить параметры IgnoreAllDefaultLibraries (здесь он отменяет использование библиотек, назначаемых в Visual Studio по умолчанию), AdditionalLibraryDirectories и AdditionalDependencies (они определяют используемые библиотеки — в данном случае для сборки не-WDM драйвера под Windows 2000), BaseAddress (обязательно следует указать равным 0x10000) и "неприметный" коварный параметр SetChecksum (должен быть "TRUE").

Все эти параметры можно настроить интерактивно и в самой интегрированной среде Visual Studio, однако, затем рекомендуется сравнить содержимое файла .vcproj с приведенным текстом, стараясь получить полное совпадение.


Недостатки многослойной архитектуры


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

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

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



Некоторые стандартные параметры описания драйвера в Системном Реестре


В Системном Реестре в разделе HKLM\System\CurrentControlSet\Services

сосредоточены описания загружаемых драйверов, представленные набором параметров и/или вложенных подразделов.

После загрузки и запуска драйвера Example.sys, рассмотренного в главе 3, при помощи программы monitor, ранее описанной во 2 главе, можно наблюдать следующие изменения в Системном Реестре (рисунок B-1).

Программа monitor при загрузке драйвера создает в Системном Реестре соответствующий подраздел в HKLM\System\CurrentControlSet\Services с новым именем Example, в который помещает параметры DisplayName, ErrorControl, ImagePath, Start и Туре. Все перечисленные параметры достаточно типичны для описания драйверов в Системном Реестре, поэтому рассмотрим их подробнее.

Рис. В-1

Записи Системного Реестра для драйвера Example.sys



Nonpaged Memory, Nonpaged Pool


Нестранично организованная память, нестраничный пул, нестраничная память.

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

Недостатки нестраничной памяти (помимо того, что ее не очень много) проявляются практически в одном случае — при работе с устройствами, поддерживающими режим прямого доступа к памяти (DMA или, в русском варианте, ПДП). Будь то страничная виртуальная память, либо нестраничная — соответствующие области в физической памяти могут быть разрывными. Однако работа с DMA устройствами требует физической непрерывности областей, из которых берутся или куда передаются данные при DMA операциях. Выход, который могла бы предложить функция MmAllocateContiguousMemory

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

Объем памяти, которую система отводит под нестраничную память, ограничен. Даже при наличии достаточного объема физической оперативной памяти Windows 2000 позволяла иметь до 660 Мбайт нестраничной памяти, а 32-разрядная версия Windows XP до 1,3 Гбайт. Использование физической памяти свыше 4 Гбайт в 32-разрядных версиях Windows (даже если это позволяет аппаратура) производится через механизм физических адресов, что определяется параметром /РАЕ (разрешающим использование Physical Address Extension через загрузку другой под-версии ядра) в файле конфигурирования процесса загрузки boot.ini, см. главу 4.



Новые рабочие процедуры в WDM драйверах


Процедура AddDevice, вызываемая PnP Менеджером, только лишь производит инициализацию объекта устройства и, если необходимо, структуры данных расширения объекта устройства. В процедуре AddDevice, по правилам хорошего тона WDM модели, действия над собственно аппаратурой не должны совершаться. Но тогда остаются нерешенными две важные задачи:

резервирование и конфигурирование аппаратных ресурсов обслуживаемого физического устройства;

инициализация и подготовка аппаратной части к использованию.

Все это должен сделать драйвер по получении IRP пакета с кодом IRP_MJ_PNP. Такие IRP пакеты посылается PnP Менеджером, когда происходят события включения или выключения устройства, либо возникают вопросы по конфигурированию устройства.

Категория IRP_MJ_PNP пакетов включает запросы широкого спектра, которые детализируются суб-кодами IRP_MN_Xxx. Поскольку они пропускается через единственную рабочую процедуру, то ее обязанностью является вторичная диспетчеризация по этим суб-кодам, содержащимся в IRP пакете и описывающим специфические действия, осуществления которых ожидает PnP Менеджер.

Регистрация новой для WDM модели рабочей процедуры, которой будет поручено обрабатывать запросы IRP_MJ_PNP со всеми подтипами IRP_MN_Xxx, производится традиционным образом в процедуре DriverEntry:

pDriverObj-&#62MajorFunction[IRP_MJ_PNP] = MyPnP_Handler;

Пример программного кода для осуществления вторичной диспетчеризации на основе суб-кодов IRP_MN_Xxx приводится ниже.

NTSTATUS MyPnP_Handler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { // Получить указатель на текущую ячейку стека IRP пакета PIO_STACK_LOCATION pIrpStackLocation = IoGetCurrentIrpStackLocation( pIrp );

switch (pIrpStackLocation -&#62MinorFunction) { case IRP_MN_START_DEVICE: . . . // Внимание. Все ветви оператора switch должны возвратить // результаты обработки . . . default: // если не поддерживается здесь, то передать запрос вниз: IoSkipCurrentIrpStackLocation(pIrp); return IoCallDriver(. . ., pIrp); } }

Первым параметром вызова IoCallDriver (см.
таблицу 9.5), разумеется, является указатель на объект устройства, к которому было произведено подключение в процедуре AddDevice.

Вызов IoSkipCurrentIrpStackLocation (см. таблицу 9.6) сообщает Диспетчеру ввода/вывода, что драйвер отказывается от дальнейшего участия в судьбе данного IRP пакета. В том случае, если драйвер желает получить управление над IRP пакетом в момент, когда его обработка нижними слоями драйверов будет завершена, то он должен воспользоваться системным вызовом IoCopyCurrentIrpStackLocationToNext

(см. таблицу 9.7) и зарегистрировать процедуру CompletionRoutine. Она будет вызвана в соответствующий момент.

Таблица 9.5. Прототип функции IoCallDriver

NTSTATUS IoCallDriver IRQL &#60= DISPATCH_LEVEL
Параметры Обращается к другому драйверу с запросом, сформулированным в пакете IRP (запросы типа IRP_MJ_POWER следует выполнять при помощи вызова PoCallDriver)
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, которому адресован IRP запрос
IN PIRP pIrp Указатель на отправляемый IRP пакет
Возвращаемое значение • STATUS_SUCCESS

• STATUS_PENDING &#8212 в случае, если пакет требует дополнительной обработки

• STATUS_Xxx &#8212 в случае ошибки
Таблица 9.6. Прототип функции IoSkipCurrentIrpStackLocation

VOID IoSkipCurrentIrpStackLocation IRQL &#60= DISPATCH_LEVEL
Параметры Изменяет указатель стека IRP так, что нижестоящий драйвер будет считать текущую ячейку стека IRP своей
IN PIRP pIrp Указатель на модифицируемый IRP пакет
Возвращаемое значение void
Таблица 9.7. Прототип функции IoCopyCurrentIrpStackLocationToNext

VOID IoCopyCurrentIrpStackLocationToNext IRQL &#60= DISPATCH_LEVEL
Параметры Копирует содержимое ячейки стека IRP для текущего драйвера в ячейку стека для нижестоящего драйвера
IN PIRP pIrp Указатель на модифицируемый IRP пакет
Возвращаемое значение void
Процедура завершения ввода/вывода CompletionRoutine есть обратный вызов от Диспетчера ввода/вывода, который позволяет перехватить IRP пакет после того, как низкоуровневый драйвер завершит его обработку.


Процедура завершения ввода/вывода регистрируется вызовом IoSetCompletionRoutine (см. таблицу 9.8).

Таблица 9.8. Прототип макроопределения IoSetCompletionRoutine

VOID IoSetCompletionRoutine IRQL &#60= DISPATCH_LEVEL
Параметры Выполняет регистрацию callback-функции завершения обработки IRP пакета
IN PIRP pIrp Указатель на отслеживаемый IRP пакет
IN PIO_COMPLETE_ROUTINE CompletionRoutine Функция, которая должна получить управление, когда обработка IRP будет завершена
IN PVOID pContext Параметр, который получит регистрируемая callback функция CompletionRoutine
IN BOOLEAN doCallOnSuccess Вызывать CompletionRoutine в случае успешного завершения обработки данного IRP пакета
IN BOOLEAN doCallOnError Вызывать CompletionRoutine в случае завершения обработки данного IRP с ошибкой
IN BOOLEAN doCallOnCancel Вызывать CompletionRoutine в случае прерванной обработки данного IRP пакета
Возвращаемое значение void
Подключать собственные процедуры CompletionRoutine для детектирования окончания обработки IRP пакетов можно не только к пакетам с кодом IRP_MJ_PNP, но также ко всем остальным, отправляемым драйверам нижних слоев.
Вызов IoSetCompletionRoutine помещает регистрируемую функцию в ячейке стека IRP пакета, соответствующую нижнему драйверу. Поэтому, за исключением драйвера, находящегося в самом низу стека, каждый драйвер в иерархии может подключить свою собственную процедуру окончания ввода/вывода к обработке данного IRP пакета. Процедуры завершения выполняются в порядке помещения драйверных объектов в стек, то есть снизу к вершине стека.

Таблица 9.9. Прототип функции завершения ввода/вывода CompletionRoutine

NTSTATUS CompletionRoutine IRQL == см. текст ниже
Параметры Перехватывает пакет IRP после завершения работы нижнего драйверного слоя
IN PDEVICE_OBJECT pDevObj Объект устройства (в составе данного драйвера), которому был ранее адресован данный IRP пакет
IN PIRP pIrp Указатель на IRP пакет, обработка которого только что завершена
IN PVOID pContext Аргумент, указанный в IoSetCompleteRoutine
Возвращаемое значение STATUS_MORE_PROCESSING_REQUIRED

STATUS_SUCCESS
<


Однозначно предсказать, на каком уровне IRQL выполняется процедура завершения, невозможно. В том случае, если нижележащий драйвер, вызывает IoCompleteRequest

(подробности &#8212 ниже) с уровня IRQL равного PASSIVE_LEVEL, то процедура завершения находящегося выше драйвера выполняется на уровне PASSIVE_LEVEL. В случае, если лежащий ниже драйвер завершает обработку IRP пакета на уровне DIPATCH_LEVEL (например, из DPC процедуры), то и процедура завершения лежащего выше драйвера выполняется на уровне DISPATCH_LEVEL.

Выполнение программного кода на уровне DISPATCH_LEVEL ограничивается системными вызовами, которые работают на этом уровне IRQL. Разумеется, следует особо позаботиться, чтобы здесь не производилась работа со страничной памятью.

Таблица 9.10. Прототип функции IoCompleteRequest

VOID IoCompleteRequest IRQL &#60= DISPATCH_LEVEL
Параметры Вызывается, когда драйвер желает полностью завершить обработку данного IRP пакета. Обеспечивает вызов процедур завершения всех драйверов, имеющихся над данным (см. ниже)
IN PIRP pIrp Указатель на текущий IRP пакет, обработка которого только что завершена
IN CCHAR PriorBoost Величина, на которую следует изменить приоритет потока, выполняющего обработку данного IRP пакета. Величина IO_NO_INCREMENT используется, если никаких изменений делать не нужно.
Возвращаемое значение void
Чтобы устранить упомянутую выше неоднозначность уровня IRQL работы процедуры завершения, можно прибегнуть к следующей уловке. Предположим, что мы имеем программный код рабочей процедуры. Известно также, что PnP Менеджер (как, впрочем, и Диспетчер ввода/вывода) всегда выполняет вызов рабочей процедуры драйвера на уровне PASSIVE_LEVEL. Тогда, отправляя пакет IRP нижним слоям драйвера, организуем ожидание (средствами объекта события режима ядра) не выходя из кода данной рабочей процедуры, пока отосланный нижним слоям IRP пакет не возвратится в зарегистрированную функцию CompletionRoutine. Как только это произойдет, объект события кодом функции CompletionRoutine будет переведен в сигнальное состояние, и стадия ожидания в основном потоке завершится.


Таким образом, мы получим сигнал о завершении обработки пакета IRP на вполне определенном уровне IRQL, равном именно PASSIVE_LEVEL. Полностью данный метод описывается в примере ниже:

. . . . . . // код рабочей процедуры, выполняющийся на уровне PASSIVE_LEVEL . . . . . . IoCopyCurrentIrpStackLocationToNext(pIrp);

// Резервируем место под объект события: KEVENT myEvent; // Инициализируем его, состояние не сигнальное: KeInitializeEvent( &myEvent, NotificationEvent, FALSE ); // Регистрируем свою процедуру завершения обработки IRP пакета. // Указатель на объект myEvent передаем как дополнительный параметр. IoSetCompletionRoutine( pIrp, MyCompleteRoutine, (PVOID)&myEvent, TRUE, TRUE, TRUE);

// Предположим, что указатель на объект устройства, // к которому был подключен текущий объект устройства, был ранее // сохранен в структуре расширения текущего объекта устройства. PDEVICE_EXTENSION pDeviceExtension = (PDEVICE EXTENSION) pDeviceObject-&#62DeviceExtension; PDEVICE_OBJECT pUnderlyingDevObj = pDeviceExtension-&#62pLowerDevice;

// Отправляем IRP пакет на обработку нижними драйверными слоями IoCallDriver( pUnderlyingDevObj, pIrp );

// Организуем ожидание, пока не закончится работа на нижних уровнях KeWaitForSingleObject( &myEvent, Execute, KernelMode, FALSE, NULL);

// Теперь завершаем обработку IRP пакета. // Его адрес не изменился - pIrp. // По "возвращении" из "ожидания" уровень IRQL остался прежним для // данного потока. // Поскольку Диспетчер ввода/вывода и PnP Менеджер вызывают // рабочие процедуры драйвера на уровне PASSIVE_LEVEL, то таким // он в данном потоке и остался. . . . } NTSTATUS MyCompleteRoutine( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp, IN PVOID pContextArgument ) { // Вычисляем указатель на Объект События: PEVENT pEvent = (PEVENT) pContextArgument; // Устанавливаем его в сигнальное состояние KeSetEvent( pEvent, 0, FALSE ); // IRQL &#60=DISPATCH_LEVEL

// Пакет IRP получен. Завершение работы здесь. Но не окончательно.


return STATUS_MORE_PROCESSING_REQUIRED; }

Рассмотрим подробнее работу вызова IoCompleteRequest. Когда некий код некоего драйвера делает этот вызов, программный код IoCompleteRequest

обращается к ячейкам стека IRP пакета и анализирует, зарегистрировал ли верхний (над текущим) драйвер процедуру завершения CompleteRoutine &#8212 это как раз отмечено в стеке IRP пакета. В том случае, если таковой процедуры не обнаруживается, указатель стека поднимается и снова выполняется проверка. Если обнаружена зарегистрированная функция, то она выполняется. В том случае, если вызванная таким образом функция возвращает код завершения, отличный от STATUS_MORE_PROCESSING_REQUIRED, то указатель стека снова поднимается, и действия повторяются. Если в результате вызова получен код завершения STATUS_MORE_PROCESSING_REQUIRED, то управление возвращается инициатору вызова IoCompleteRequest.

Когда код IoCompleteRequest благополучно достигает в своем рассмотрении вершины стека, то Диспетчер ввода/вывода выполняет действия по освобождению данного IRP пакета (наряду с некоторыми другими операциями).

Отсюда несколько важных следствий.

Во-первых, если драйвер сам создал IRP пакет (подробно рассматривается ниже), то вызов IoCompleteRequest означает приказ Диспетчеру ввода/вывода заняться его освобождением &#8212 поскольку иных драйверов, процедуры завершения которых можно было бы рассматривать, просто нет.

Во-вторых, если текущий драйвер зарегистрировал свою процедуру завершения и, не вызывая нижних драйверов, сразу выполнил IoCompleteRequest, то такая процедура завершения вызвана не будет &#8212 код IoCompleteRequest

ee в рассмотрение просто не примет, переходя сразу к анализу ячеек стека IRP для вышестоящих драйверов.

В-третьих, возможна ситуация, когда после выполнения вызова IoCompleteRequest

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

В любом случае, после вызова IoCompleteRequest драйвер не имеет права прикасаться к IRP пакету, который передан этому вызову как завершаемый. Кроме того, возврат кода STATUS_MORE_PROCESSING_REQUIRED &#8212 это практика зарегистрированных процедур завершения, что является "просьбой" Диспетчеру ввода/вывода возвратиться к данной процедуре завершения позже.


Объекты события


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

Если присмотреться к определению объекта события в заголовочных файлах DDK, то становится очевидным, что объект события состоит только лишь из структуры DISPATCHER_HEADER, в то время как другие синхронизационные примитивы &#8212 таймеры, семафоры и мьютексы &#8212 содержат, помимо DISPATCHER_HEADER, некоторые дополнительные поля. То есть, события &#8212 самые простые из них.

Объекты события делятся на две категории: объекты для уведомления (Notification Events) и объекты для синхронизации (Synchronization Events). Тип выбирается в момент инициализации объекта. Эти два типа объекта событий проявляют различие в своем поведении в момент, когда объект переводится в сигнальное состояние.

Как только объект уведомляющего (Notification) события переходит в сигнальное состояние, все потоки, реагирующие на него, выходят из состояния ожидания. Однако объект такого типа необходимо перевести в несигнальное состояние явно (вызовом KeClearEvent), иначе он так и останется в активном состоянии. Поведение данного типа объектов аналогично поведению объектов события пользовательского режима, которые управляются ручной установкой.

Поведение синхронизационных (Synchronization) объектов события несколько отличается. Когда синхронизационный объект события переходит в сигнальное состояние, то он остается в этом состояние лишь столько времени, сколько это необходимо для выполнения одного вызова KeWaitForXxx. Затем объект переводит себя в несигнальное состояние автоматически. Другими словами, ворота остаются открытыми только до тех пор, пока кто-то первый не прошел через них, после чего они закрываются автоматически. Этот тип эквивалентен событиям с авто-сбросом (auto-reset) в пользовательском режиме.


Таблица 10.26. Функции для работы с объектами событий

Что необходимо сделать... Какой вызов нужно использовать...
Создать событие KeInitializeEvent
Создать именованное событие IoCreateSynchronizationEvent

IoCreateNotificationEvent
Изменить состояние события KeSetEvent

KeClearEvent

KeResetEvent
Запросить состояние KeReadStateEvent
Для использования объекта события сначала необходимо получить память для его хранения размером sizeof(KEVENT), и только после этого можно выполнять вызовы функций, перечисленные выше. Рассмотрим некоторые из них.

Несигнальное состояние объекта событий можно установить при помощи вызовов KeResetEvent и KeClearEvent. Разница между ними заключается в том, что функция KeResetEvent еще и возвращает состояние объекта, в котором тот пребывал перед данным вызовом. Функция KeClearEvent

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

Таблица 10.27. Прототип вызова KeInitializeEvent

VOID KeInitializeEvent IRQL == PASSIVE_LEVEL
Параметры Инициализация объекта события и установка его начального состояния
IN PKEVENT pEvent Указатель на область памяти для объекта события
IN EVENT_TYPE Type Одно из двух значений

• NotificationEvent

• SynchronizationEvent
IN BOOLEAN bInitalState Начальное состояние объекта

• TRUE &#8212 сигнальное состояние

• FALSE &#8212 несигнальное состояние
Возвращаемое значение void
Таблица 10.28. Прототип вызовов KeClearEvent

u KeResetEvent

VOID KeClearEvent

LONG KeResetEvent
IRQL &#60= DISPATCH_LEVEL
Параметры Установка объекта события в несигнальное состояния
IN PKEVENT pEvent Указатель на инициализированный объект события
Возвращаемое значение KeResetEvent возвращает предыдущее состояние объекта события
Таблица 10.29. Прототип вызова KeSetEvent

LONG KeSetEvent IRQL &#60= DISPATCH_LEVEL
Параметры Переводит объект события в сигнальное состояние
IN PKEVENT pEvent Указатель на инициализированный объект события
IN KPRIORITY Increment Обычно используется значение IO_NO_INCREMENT
IN BOOLEAN bWait Обычно используется значение FALSE
Возвращаемое значение Возвращает ненулевое значение, если предыдущее состояние объекта события было сигнальным
<


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

В основе объектов события пользовательского режима лежат объекты события режима ядра &#8212 именно те, которые обсуждались выше. Основное различие заключается в том, что типовой доступ в пользовательском режиме &#8212 по дескриптору, а в режиме ядра &#8212 по указателю. Иными словами, один и тот же объект события при некоторой сноровке можно использовать для синхронизации действий между разными драйверами и между драйверами и приложениями пользовательского режима.

Совместное использование двумя несвязанными драйверами одного объекта события, созданного вызовом KeInitializeEvent, есть весьма непростая задача. Более простого способа передать его, иначе как по специальному предварительному соглашению (например, с использованием специального внутреннего кода IOCTL), не существует. Имеется и такая проблема: как гарантировать, что драйвер, создавший объект события, и в момент получения указателя другим драйвером и во все время его использования все еще останется загруженным?

Функции IoCreateSynchronizationEvent и IoCreateNotificationEvent

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


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

При использовании именованного объекта события совместно драйвером и приложением пользовательского режима следует создавать такой объект сначала в пользовательском приложении. Причина кроется в том, что пользовательские объекты события должны размещаться в директории объектов \BaseNamedObjects, которая создается после инициализации подсистемы Win32 и к моменту запуска драйвера, возможно, еще не существует. После этого, в результате IOCTL запроса (выступающего в роли команды) к драйверу, последний должен получить доступ к объекту события по заранее определенному имени либо должен получить некоторую дополнительную информацию из IOCTL запроса &#8212 имя или дескриптор созданного объекта события.

Таблица 10.30. Прототип вызовов IoCreateSynchronization(Notification)Event

PKEVENT IoCreateSynchronizationEvent

PKEVENT IoCreateNotificationEvent
IRQL == PASSIVE_LEVEL
Параметры Создает новый или получает доступ к существующему объекту события по имени
IN PUNICODE_STRING EventName Имя объекта, заканчивающаяся нулем строка широких (UNICODE) символов
OUT PHANDLE EventHandle Указатель, по которому будет возвращен дескриптор объекта.
Возвращаемое значение Указатель на созданный или существующий объект события с данным именем либо NULL в случае ошибки.
Для работы драйверу требуется указатель на объект события. Его можно получить из дескриптора существующего объекта следующим способом.

Выполнить вызов ObReferenceObjectByHandle. Эта функция возвращает указатель на собственно объект и увеличивает на единицу число ссылок на данный объект.

Поскольку собственно дескриптор становится ненужным, необходимо вы полнить вызов ZwClose со значением дескриптора. Эта функция выполнит уменьшение на единицу счетчика ссылок на данный объект.

Когда объект события станет ненужным, необходимо выполнить вызов ObDereferenceObject

для того, чтобы уменьшить на единицу счетчик ссылок на объект, что, возможно, уничтожит его.

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

В том случае, если драйвер получает от приложения дескриптор через IOCTL запрос, то этот дескриптор имеет силу, поскольку код драйвера (обработчика IOCTL запросов) работает в контексте пользовательского потока, обратившегося к драйверу.

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


Object


Объект. В программировании драйверов объект всегда является структурой

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

В режиме ядра имеются трудности с реализацией трюков С++ (оператора new, позднего связывания, идентификации типов во время выполнения и виртуальных методов), следовательно, и основных приемов объектно-ориентированного программирования (ООП). Соответственно, и объекты здесь "ненастоящие". Объекты режима ядра роднит с "настоящими" объектами (в смысле ООП) практически только одно обстоятельство: к каждому объекту прилагается набор функций, и фирма Microsoft рекомендует работать с объектами ядра только при помощи этих специализированных функций. Этим Microsoft достигает решения трех задач. Во-первых, скрывается внутренняя структура объектов (которая в будущем может модифицироваться разработчиком операционной системы или иначе реализовываться на разных платформах). Во-вторых, становится возможным ограничить пределы вмешательства программиста в жизнь ядра, что разработчик ОС считает потенциально опасным. В третьих, программист действительно делает меньше ошибок.

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



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


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

Необходимо заменить вызовы ExAllocatePool на вызовы ExAllocatePoolWithTag. Дополнительный аргумент, представляющий собой четырехбайтную величину (4 символа), используется для того, чтобы пометить вновь выделенный блок этим значением (тегом).

Необходимо запустить драйвер под отладочной версией (checked build) Windows. Поддержка трассировки страниц пула является дорогостоящим "удовольствием", поэтому доступно оно только в отладочных версиях Windows.

Когда выполняется анализ 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. Эта программа непрерывно выводит на экран обновляемые данные о страничных тегах.



Обработчики запросов Open и Close


Все драйверы, если только они не тестового назначения, должны иметь в своем составе процедуру CreateDispatch (в примере Example.sys эта процедура называлась Create_File_IRPprocessing), которая производит обработку пользовательского запроса CreateFile. Помимо того, драйверы, которые должны выполнять действия по очистке в ответ на пользовательский запрос CloseHandle, обязаны иметь в своем распоряжении еще и процедуру CloseDispatch (в примере Example.sys &#8212 процедура Close_HandleIRPprocessing).



Обработка аппаратных прерываний


Обработка прерываний в драйверах уже неоднократно рассматривалась ранее, см. гл. 6, 8 и 10. Однако это происходило в теоретическом аспекте, так что пришло время применить полученные сведения на практике. Данная глава посвящена рассмотрению двух вариантов несложного драйвера LPTPORT.sys, работающего с реальными аппаратными прерываниями и позволяющего подробно и всесторонне рассмотреть их "живьем".



Обработка прерываний


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

Приостанавливает выполнение текущей последовательности процессорных команд.

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

Повышает значение IRQL процессора до значения IRQL поступившего запроса, предотвращая вмешательство в работу прерываний с более низкими уровнями IRQL.

Передает управление надлежащей сервисной процедуре обслуживания прерывания (ISR).

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

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