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

         

Драйверы: крупный план. Unix-Linux, DOS-Windows


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

Концепция драйвера как отдельного сменного модуля оформилась не сразу. Некоторые версии UNIX и по сию пору практикуют полную перекомпиляцию ядра при замене какого-либо драйвера, что совершенно не похоже на обращение с драйверами в Linux, Windows и MS DOS. Кстати, именно MS DOS ввела в массовое обращение понятие драйвера, как легко сменяемой насадки, позволяющей моментально (сразу после очередной перезагрузки) улучшить качество жизни пользователя: шрифты на мониторе радуют глаз, емкость дискеты возросла вдвое, в принтере наконец-то поселилась кириллица и т.п.

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

Наблюдение 1. В операционных системах MS DOS, Windows, Unix и всех клонах Linux принят способ работы с драйверами как с файлами. То есть при доступе к драйверу используются функции либо совпадающие (лексически), либо весьма похожие на функции для работы с файлами (open, close, read, write, CreateFile...).

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



Драйверы. Общие понятия и термины


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

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



DriverEntry


Процедура DriverEntry. Функция драйвера, которая будет вызвана первой при его загрузке. Единственная функция драйвера, которую все разработчики предпочитают называть именно так (поскольку изменение стоит существенных и, в общем-то, бессмысленных усилий). Данная функция выполняется регистрацию основных процедур, в том числе рабочих (Dispatch Routines, см. выше). Для WDM и не-WDM драйверов задачи, которые должна решить эта функция, немного различаются. Функция DriverEntry выполняется на уровне IRQL равном PASSIVE_LEVEL и даже может быть размещена в странично организованной памяти (путем специальных директив для редактора связей, линкера).



Другие процедуры драйвера


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



Другие секции, определяющие копирование файлов




Как было сказано выше, дополнительную информацию о том, где взять исходные файлы и куда их скопировать предоставляют секции [SourceDisksNames], [SourceDisksFiles] и [DestinatonDirs] (имена данных секций не подлежат изменению разработчиком).



Другие существенные компоненты операционной системы


В дополнение к подсистемам, реализующим среду выполнения кодов DOS, Windows, POSIX и OS/2, имеется еще несколько ключевых системных компонентов, которые реализованы как процессы пользовательского режима. Среди них:

Security Subsystem, подсистема безопасности, которая управляет локальной и удаленной безопасностью (защитой от неправомерного доступа) с использованием ряда процессов и динамических библиотек. Часть работы Active Directory протекает как раз среди этой логической подсистемы.

Service Control Manager (SCM, функции которого использовались для запуска драйвера в Главе 3), Менеджер Управления Сервисами, управляет процессами-демонами (сервисами), и драйверами устройств.

Процессы поддержки RPC вызовов (Remote Procedure Call, вызов удаленной процедуры), которые оказывают поддержку приложениям, распространяемым по сети. Прибегая к использованию вызовов удаленных процедур, приложения могут выполнять свою работу с использованием множества сетевых компьютеров.



EISA: Extended Industry Standard Architecture


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

Разумеется, требование на обеспечение совместимости ограничивало модернизацию шины по многим направлениям. Например, в то время, как разрядность передаваемых данных (в литературе &#8212 data bus width) была увеличены до 32 бит, тактовая частота осталась равной 8 МГц. Соответственно, максимальная скорость передачи данных по шине стала составлять около 32 Мбайт в секунду. Кроме того, поскольку разъемы EISA на материнской плате должны были принимать еще и платы собственно ISA, то появились существенные сложности в устранении электрических шумов, обусловленных топологией разводки соединений ISA. Компьютерные пользователи постарше могут припомнить, что разъем EISA на материнской плате состоял из ISA разъема и присоединенного к нему в продолжение разъема покороче и поизящнее, что вместе и составляло EISA-слот.

Устройства EISA уже вполне обеспечивали возможности автоматической идентификации, хотя процедура эта была не слишком проста. Во всяком случае, спецификация EISA сдала свои позиции протоколу PCI достаточно легко, стоило лишь тому появиться. (А вот прощание с ISA проходило гораздо тяжелее, можно, даже сказать &#8212 еще продолжается и сейчас!)



Enumeration


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



Enumerator


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



Файл Makefile


Этот файл управляет работой программы Build и в нашем случае имеет стандартный вид (его можно найти практически в любой директории примеров DDK), а именно:

# Файл Makefile # # DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source # file to this component. This file merely indirects to the real make file # that is shared by all the driver components of the Windows NT DDK #

!INCLUDE $(NTMAKEENV)\makefile.def



Файл Sources


Файл sources отражает индивидуальные настройки процесса компиляции и сборки. В нашем случае файл Sources чрезвычайно прост и имеет вид:

# Файл Sources TARGETNAME=Example TARGETTYPE=DRIVER #DRIVERTYPE=WDM TARGETPATH=obj SOURCES=init.cpp

Данный файл задает имя выходного файла Example. Поскольку проект (TARGETTYPE) имеет тип DRIVER, то выходной файл будет иметь расширение .sys. Промежуточные файлы будут размещены во вложенной директории .\obj. Строка SOURCES задает единственный файл с исходным текстом — это файл init.cpp.

Если бы мы выполняли компиляцию и сборку WDM драйвера, то нужно было бы в тексте Driver.h использовать #include "wdm.h" (взять определения из заголовочного файла "wdm.h" вместо "ntddk.h"), а в данном файле Sources — удалить символ '#' (который вводит строку-комментарий) в первой позиции третьей строки. После этого строка

DRIVERTYPE=WDM

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



Фиксация страничных секций кода и данных в оперативной памяти


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

Фиксация секций страничной памяти данных и кода выполняется при помощи вызовов MmLockPagableDataSection и MmLockPagableCodeSection

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

Применение вызовов MmLockPagableDataSection/MmLockPagableCodeSection

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

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

#pragma data_seg("MY_DATA") &#60описание переменных&#62 #pragma data_seg()

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

Применять приведенные функции для фиксации в оперативной памяти буферных областей, поступивших в пакете IRP, нельзя &#8212 для этого следует использовать специально для того предназначенный вызов MmProbeAndLockPages.

Все упомянутые выше вызовы следует выполнять только в коде, работающем на уровне IRQL равном PASSIVE_LEVEL.



Filter Device Object


Объект устройства, создаваемый фильтр-драйвером. Имеет следующее формальное отличие — это устройство остается безымянным (драйвер не указывает его имени при создании объекта) и не имеет символьной ссылки. Этот объект устройства, как правило, не предназначен для непосредственного доступа к нему, но после должного подключения его под или над устройством основного драйвера, это фильтр-устройство пропускает через себя все IRP пакеты, на самом деле предназначенные основному драйверу.



Filter Driver


Фильтр-драйвер. Драйвер, предназначенный для выполнения дополнительных манипуляций над IRP пакетами основного драйвера (вплоть до того, что самостоятельно отвергает их), к которому он подключается в стеке либо сверху, Upper Filter, либо снизу, Lower Filter. У одного основного драйвера может быть несколько фильтр-драйверов, но они для основного драйвера как бы не существуют. Как правило, разбиение на основные и фильтр-драйвера делает сам разработчик основного драйвера, определяя порядок их установки и загрузки, обеспечивающий должную конфигурацию стека устройств в этом месте дерева устройств.



Функции библиотеки времени выполнения для работы с памятью


Операционная система Windows предоставляет набор Rtl (библиотека времени выполнения) функций для работы с памятью, которые в режиме ядра заменяют столь привычные программистам пользовательских приложений вызовы memcpy, memset и т.п. Некоторые наиболее употребительные вызовы описаны ниже.

Таблица 7.29. Прототип вызова RtlFillMemory

VOID RtlFillMemory IRQL &#8212 любой (если это допускает тип памяти заполняемого буфера)
Параметрам Заполняет область памяти значением Pattern
IN VOID UNALIGNED

*Destination

Указатель на буфер-приемник (область без выравнивания)
IN ULONG Length Размер заполняемой области в байтах
IN UCHAR Pattern Значение, которым будет заполнена указанная область (байт)
Возвращаемое значение void

Таблица 7.30. Прототип вызова RtlZeroMemory

VOID RtlZeroMemory IRQL &#8212 любой (если это допускает тип памяти обнуляемого буфера)
Параметры Обнуляет область памяти
IN VOID UNALIGNED

*Destination

Указатель на буфер-приемник (область без выравнивания)
IN ULONG Length Размер обнуляемой области в байтах
Возвращаемое значение void

Таблица 7.31. Прототип вызова RtlCopyMemory

VOID RtlCopyMemory IRQL &#8212 любой (если это допускают типы памяти копируемых буферов)
Параметры Копирует содержимое одного буфера в другой
IN VOID UNALIGNED

*Destination

Указатель на буфер-приемник (область без выравнивания)
IN CONST VOID UNALIGNED

*Source

Указатель на буфер-источник (область без выравнивания)
IN ULONG Length Размер копируемой области в байтах
Возвращаемое значение void

Замечание. Области источника и приемника не должны перекрываться.

Таблица 7.32. Прототип вызова RtlMoveMemory

VOID RtlMoveMemory IRQL &#8212 любой (если это допускают типы памяти копируемых буферов)
Параметры Копирует содержимое одного буфера в другой
IN VOID UNALIGNED

*Destination

Указатель на буфер-приемник (область без выравнивания)
IN CONST VOID UNALIGNED

*Source

Указатель на буфер-источник (область без выравнивания)
IN ULONG Length Размер копируемой области в байтах
Возвращаемое значение void

Замечание. Допускается перекрытие областей источника и приемника.

Существует также вызов RtlCopyBytes, совершенно идентичный приведенному выше вызову RtlMoveMemory.



Функции для работы с файлами


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

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

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

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

Таблица 7.39. Прототип вызова ZwCreateFile

NTSTATUS ZwCreateFile IRQL == PASSIVE_LEVEL
Параметры Предоставляет доступ к системным ресурсам (в том числе файлам) в режиме ядра
OUT PHANDLE pHandle Указатель на переменную, куда следует поместить дескриптор открытого объекта (файла, подраздела реестра и т.п.)
IN ACCESS_MASK DesiredAccess Характеристика доступа к объекту. Для файлов вполне приемлемы значения GENERIC_READ или GENERIC_WRITE, которые представляют сложные комбинации из более простых масок доступа (типа FILE_APPEND_DATA и т.п.)
IN POBJECT_ATTRIBUTES pObjAttributes Указатель на заполненную вызывающим кодом структуру данных, которая описывает имя, местоположение и некоторые другие характеристики открываемого объекта (см. ниже)
OUT PIO_STATUS_BLOCK

pIOStatus

Указатель на буфер, в котором будет размещена информация об открытом объекте в формате структуры IO_STATUS_BLOCK
IN PLARGE_INTEGER

AllocationSize OPTIONAL

Начальный размер файла в байтах. Ненулевое значение принимается во внимание только при создании и перезаписи файла
IN ULONG FileAttributes Атрибуты открываемого файла. Типовым является значение FILE_ATTRIBUTE_NORMAL
IN ULONG SharedAccessFlags Описывает, разрешен ли совместный доступ, например, FILE_SHARE_READ &#8212 для чтения
IN ULONG CreateDispositionFlags Способ открытия файла, например, FILE_OPEN_IF &#8212 если не существует, создать
IN ULONG CreateOptions Комбинация флагов создания, например, FILE_SYNCHRONOUS_IO_NONALERT &#8212 все операции над файлом выполняются как синхронные (DesiredAccess должен включать флаг SYNCHRONIZE)
IN PVOID EaBuffer OPTIONAL Для драйверов устройств и драйверов средних слоев следует указывать NULL
IN ULONG EaLength Для драйверов устройств и драйверов средних слоев следует указывать 0
Возвращаемое значение STATUS_SUCCESS или код ошибки (несколько более подробную информацию можно найти в структуре IO_STATUS_BLOCK)
<
Поле pIOStatus-&#62Information ( структуры IO_STATUS_BLOCK) может после вызова иметь одно из следующих значений: FILE_CREATED, FILE_OPENED, FILE_OVERWRITTEN, FILE_SUPERSEDED, FILE_EXISTS или FILE_DOES_NOT_EXIST.

Ниже приводится пример создания (или открытия &#8212 при повторных обращениях) файла C:\Example\testfile.txt и запись в него строки " string : test write!\"

NTSTATUS status; UNICODE_STRING fullFileName; HANDLE fileHandle; IO_STATUS_BLOCK iostatus; OBJECT_ATTRIBUTES oa;

RtlInitUnicodeString( &fullFileName, L"\\??\\C:\\Example\\testfile.txt");

InitializeObjectAttributes( &oa, &fullFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL );

status = ZwCreateFile ( &fileHandle, GENERIC_WRITE | SYNCHRONIZE, &oa, &iostatus, 0, // alloc size = none FILE_ATTRIBUTE_NORMAL, FILE_SHARE_WRITE, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); // Здесь: // GENERIC_WRITE равно STANDARD(0x40000000L) // // FILE_GENERIC_WRITE равно STANDARD_RIGHTS_WRITE|FILE_WRITE_DATA | // FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | // SYNCHRONIZE, что можно увидеть в заголовочном файле winnt.h

if( NT_SUCCESS(status)) { // Строка для записи в файл char myString[100]="string : test write!\r\n"; // Структура, которая поможет определить длину файла: FILE_STANDARD_INFORMATION fileInfo;

status = // Получаем информацию о файле ZwQueryInformationFile( fileHandle, &iostatus, &fileInfo, sizeof(FILE_STANDARD_INFORMATION), FileStandardInformation ); ULONG len = strlen(myString); if( NT_SUCCESS(status) ) { LARGE_INTEGER ByteOffset = fileInfo.EndOfFile; status = ZwWriteFile(fileHandle, NULL, NULL, NULL, &iostatus, myString, len, // Записываемая строка &ByteOffset, // a если NULL? см. ниже NULL); if( !NT_SUCCESS(status) || iostatus.Information != len ) { DbgPrint("Error on writing. Status = %x.", status); } } ZwClose(fileHandle); break;

}

Следует заметить, что без магической комбинации "\\??\\" в начале имени файла, вызов ZwCreateFile непременно возвратит ошибку.


Этот префикс является обязательным для файлов на диске.

Таблица 7.40. Прототип вызова ZwWriteFile

NTSTATUS ZwWriteFile IRQL == PASSIVE_LEVEL
Параметры Производит модификацию объекта (файла), указанного открытым дескриптором
IN HANDLE FileHandle Дескриптор открытого для модификации файлового объекта
IN HANDLE Event OPTIONAL Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
IN PIO_APC_ROUTINE

pApcRoutine OPTIONAL
Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
IN PVOID ApcContext OPTIONAL Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
OUT PIO_STATUS_BLOCK pIoStatusBlock В поле pIoStatusBlock-&#62Information по завершении вызова находится число реально записанных байт
IN PVOID Buffer Буфер с данными для записи
IN ULONG Length Размер записываемой порции данных
IN PLARGE_INTEGER pByteOffset OPTIONAL Указатель на переменную, где содержится смещение в файле (от начала), по которому следует производить запись данных
IN PULONG Key OPTIONAL Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
Возвращаемое значение STATUS_SUCCESS или код ошибки
При помощи ZwCreateFile можно получать и доступ к драйверам, как это делается из вызова CreateFile пользовательского режима. Таким образом, в частности, предлагается получать доступ к драйверу отладочной печати DebugPrint. Разумеется, вид имени для использования будет несколько другим. Для драйвера DebugPrint это будет имя L"\\Device\\PHDDebugPrint". По данному имени через ZwCreateFile можно получить доступ к драйверу из другого драйвера, несмотря на то, что символьная ссылка этим драйвером не создается. Аналогично, к нашему драйверу Example можно получить доступ из другого драйвера по имени L"\\Device\\Example", но ничего не получится, если пытаться использовать имя L"\\\\.\\Example", как в вызове CreateFile.



Рассмотрим подробнее некоторые функции, использованные в приведенном выше примере.

Макроопределение InitializeObjectAttributes используется для заполнения полей структуры OBJECT_ATTRIBUTES, что делает эту операцию компактнее. Это макроопределение вводится в заголовочном файле ntdef.h пакета DDK. Там же описана и внутренняя организация OBJECT_ATTRIBUTES.

Собственно запись в файл в приведенном выше примере выполняется системной функцией ZwWriteFile.

Способ вызова ZwReadFile во многом повторяет прототип для ZwWriteFile, приведенный в таблице 7.40.

Если параметр pByteOffset указан равным NULL, то в большинстве случаев это воспринимается как нулевое смещение, и запись была бы выполнена (скажем, в приведенном примере) с начала файла. Однако на это допущение лучше не полагаться. Например, если бы мы пытались произвести запись в упомянутое выше устройство PHDDebugPrint, то нас преследовали бы ошибки, пока значение *pByteOffset не было бы задано явно, то есть:

pByteOffset-&#62QuadPart = 0i64; // 0 для LARGE_INTEGER.

Упоминания в документации DDK XР, по поводу того, что при наличии в параметре DesiredAccess (при вызове ZwCreateFile для получения доступа к файлу) флага FILE_APPEND_DATA запись всегда производится в конец файла, мягко говоря, не совсем справедливы. При наличии этого флага (среди параметров вызова ZwCreateFile) значение ByteOffset играет по-прежнему ту же роль в вызове ZwWriteFile, что и 6eз использования этого флага при открытии файла.
В примере, представленном выше, для получения информации о размере файла был использован системный вызов ZwQueryInformationFile. Данный вызов достаточно универсален, и возвращаемая им информация зависит от его последнего параметра, который может принимать следующие значения:

FileBasicInformation &#8212 для получения общих параметров открываемого файла, времени создания, времени последнего обращения, аргументов.

FileStandardInformation &#8212 для получения размера файла и признака того, что данный объект может быть директорией и т.п.

FilePositionInformation &#8212 для получения текущей позиции в файле.

FileAlignmentInformation &#8212 для получения информации о способе выравнивания буфера для работы с данным объектом.

FileNameInformation &#8212 для получения системной информации об имени файла.

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

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


Функции для работы с системным представлением времени


Внутреннее время в Windows 2000/XP/Server 2003 хранится как число 100-наносекундных отсчетов с 1 января 1601 года. Это очень большое число, и для его хранения используется 64-разрядный тип данных в структуре, обозначенной как тип данных LARGE_INTEGER. В таблице 7.44 перечислены функции, предназначенные для работы с такими данными.

Таблица 7.44. Функции для работы с системным представлением времени

Функции Описание
KeQuerySystemTime Возвращает 64-разрядное значение абсолютного системного времени
KeQueryTickCount Возвращает число прерываний системного таймера (часов) с момента последней загрузки системы
KeQueryTimelncrement Возвращает число 100-нс интервалов, добавляемых к системному времени при каждом прерывании, поступающем от системного таймера (часов)
RtlTimeToTimeFields Разбивает 64-разрядное системное время на поля даты и времени
RtlTimeFieldsToTime Преобразует дату и время в 64-разрядное значение абсолютного системного времени
RtlConvertLongToLargeInteger Создает знаковое LARGE_INTEGER
RtlConvertULongToLargelnteger Создает положительное LARGE_INTEGER
RtlLargelntegerXxx Различные арифметические и логические операции над типом данных LARGE_INTEGER (вместо этих функций в пакете DDK рекомендовано использовать встроенную поддержку компилятора для 64-разрядных операций)

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

Таблица 7.45. Прототип вызова KeQuerySystemTime

VOID KeQuerySystemTime IRQL == любой
Параметры Возвращает текущее время
OUT PLARGE_INTEGER

CurrentTime

Время в 100 нc интервалах с 1 января 1601 года
Возвращаемое значение void



Функции для работы с Системным Реестром


Системный Реестр является системной базой данных для хранения разнообразных параметров, используемых приложениями пользовательского режима и программными модулями, работающими в режиме ядра. В операционных системах от Windows 98 до Windows Server 2003 существует набор системных вызовов, которыми можно воспользоваться в режиме ядра для доступа к существующим записям в Системном Реестре, для их чтения, редактирования, удаления, а также &#8212 для создания новых.

Перед тем, как перейти к рассмотрению собственно функций доступа к Системному Реестру из кода режима ядра, еще раз повторим, что Реестр разбит на основные разделы, среди которых основным является раздел HKEY_LOCAL_MACHINE (в литературе часто используется сокращение HKLM). Разделы могут быть разбиты на подразделы произвольной глубины вложения. В англоязычной литературе подразделы могут называться словом "path", однако, чаще употребляется "key" или "registry key". B подразделе (даже если он содержит вложенные подразделы) могут содержаться параметры (values), которые имеют имя (обычно, в литературе называемые ValueName) и значение.

Наиболее точно способ доступа к параметрам отражает следующее представление. Каждый подраздел можно считать файлом (в начале работы его следует открыть и получить соответствующий дескриптор), после чего можно работать с вложенной в него информацией &#8212 собственными параметрами, вложенными подразделами и их параметрами.



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


В некоторых ситуациях необходимо получать доступ к объектам режима ядра. В операционной системе Windows объектами представлены многие компоненты: программные потоки, объекты синхронизации, файловые объекты, драйверы, устройства, объекты DPC процедур, прерываний и т.п. Правильнее говорить, что эти явления представлены структурами данных для удобства ведения их учета. Структуры эти в полной мере (например, в смысле ООП) объектами не являются. У них нет конструкторов, деструкторов, виртуальных методов в привычном смысле С++. Тем не менее, понятие "объект" широко используется и, в некотором смысле, справедливо &#8212 оперировать этими структурами предпочтительнее специально для того предназначенными системными вызовами, которые и можно считать методами для данных объектов.

Рассмотрим пример, в котором объектом синхронизации является программный поток. Программные потоки могут своим окончанием сигнализировать другим программным потокам о том, что пора приступать к работе. В частности, программный поток может быть запущен вызовом PsCreateSystemThread (см. главу 10). Другой поток может дожидаться его окончания, организовав ожидание при помощи одного из вызовов KeWaitForXxx. Возникает небольшое затруднение. Системный вызов PsCreateSystemThread, создавая поток, возвращает его дескриптор (HANDLE), a функции KeWaitForXxx принимают в качестве входного аргумента указатель на объект, по состоянию которого организуется ожидание.

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

Таблица 7.41. Прототип вызова ObReferenceObjectByHandle

NTSTATUS ObReferenceObjectByHandle IRQL == PASSIVE_LEVEL
Параметры Предоставляет указатель на объект режима ядра по открытому дескриптору
IN HANDLE Handle Исходный дескриптор объекта
IN ACCESS_MASK

DesiredAccess

Маска доступа, интерпретация которой зависит от типа рассматриваемого объекта
IN POBJECT_TYPE ObjectType

OPTIONAL

Можно установить NULL (если параметр AccessMode равен KernelMode) или одно из значений IoFileObjectType

or ExEventObjectType

IN KPROCESSOR_MODE

AccessMode

• KernelMode &#8212 для работы в режиме ядра

• UserMode

OUT PVOID *ppObject Указатель на переменную типа PVOID, в которой будет возвращен указатель, соответствующий исходному дескриптору
OUT POBJECT_HANDLE_INFORMATION Handlelnfо OPTIONAL Указатель на структуру, в которой будет представлена дополнительная информация об объекте и правам доступа к нему
Возвращаемое значение • STATUS_SUCCESS

• STATUS_OBJECT_TYPE_MISMATCH

• STATUS_ACCESS_DENIED

• STATUS_INVALID_HANDLE

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

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

Таблица 7.42. Прототип вызова ObReferenceObjectByPointer

NTSTATUS ObReferenceObjectByPointer IRQL == PASSIVE_LEVEL
Параметры Предоставляет указатель на объект режима ядра по открытому дескриптору
IN PVOID pObject Указатель на объект
IN ACCESS_MASK DesiredAccess Маска доступа, интерпретация которой зависит от типа рассматриваемого объекта
IN POBJECT_TYPE ObjectType

OPTIONAL
Можно установить NULL (если параметр AccessMode равен KernelMode) или одно из значений IoFileObjectType

or ExEventObjectType
IN KPROCESSOR_MODE AccessMode • KernelMode &#8212 для работы в режиме ядра

• UserMode
Возвращаемое значение • STATUS_SUCCESS

• STATUS_OBJECT_TYPE_MISMATCH
Вернемся к случаю с ожиданием, организованным по объекту программного потока одним из вызовов KeWaitForXxx. Когда поток завершился, ожидание прекращено. При этом объект потока еще не уничтожен, поскольку в результате вызова ObReferenceObjectByHandle получилось так, что число ссылок на него не равно нулю. Это положение можно изменить, если уменьшить число ссылок при помощи вызова ObDereferenceObject, что, вероятно, приведет и к удалению объекта потока из системы.



Таблица 7.43. Прототип вызова ObDereferenceObject

VOID ObDereferenceObject IRQL == PASSIVE_LEVEL
Параметры Уменьшает счетчик ссылок на объект
IN PVOID pObject Указатель на объект
Возвращаемое значение void
Заметим, что при выполнении вызова ZwClose происходит автоматическое уменьшение счетчика ссылок на объект и проверка, не пора ли его удалить из системы.


Функции доступа к Системному Реестру, предоставляемые Диспетчером ввода/вывода


Функции доступа к Реестру, предоставляемые Диспетчером ввода/вывода, предоставляют доступ по указателю на объект устройства, либо по имени зарегистрированного интерфейса (аналог символьной ссылки). На практике, программисты выбирают в качестве имени интерфейса глобальный идентификатор GUID в строковом представлении (генерируется программой GuidGen).

Документация DDK рекомендует применять функции, предоставляемые для доступа к Реестру Диспетчером ввода/вывода, вместо функций прямого доступа &#8212 для облегчения переноса на другие процессорные платформы и в качестве защиты от изменений в структуре Реестра в будущем.

IoRegisterDeviceInterface регистрирует интерфейс устройства (аналог регистрации символьной ссылки), что делает возможным доступ к устройству из приложений пользовательского режима и других системных компонентов. Диспетчер ввода/вывода создает подраздел Реестра для зарегистрированного интерфейса. Драйвер может хранить в этом подразделе собственные параметры, получая доступ к нему вызовом IoOpenDeviceInterfaceRegistryKey.

IoGetDeviceProperty запрашивает из Системного Реестра установочную информацию об устройстве.

IoOpenDeviceInterfaceRegistryKey возвращает дескриптор доступа к подразделу реестра для зарегистрированного интерфейса устройства (способ регистрации устройства в системе при помощи вызова IoRegisterDeviceInterface, вместо символьной ссылки). Полученный таким образом дескриптор должен по окончании использования быть закрыт вызовом ZwClose.

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

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



Функции RtlXxx прямого доступа к Системному Реестру


RtlCheckRegistryKey возвращает STATUS_SUCCESS в том случае, если указанный вложенный подраздел существует внутри подраздела, описанного первым параметром вызова. Первый параметр указывается как одно из значений RTL_REGISTRY_Xxx. Например, раздел \Registry\Machine\System\CurrentControlSet\Services описывается значением RTL_REGISTRY_SERVICES, а раздел \Registry\User\CurrentUser описывается значением RTL_REGISTRY_USER (см. заголовочные файлы ntddk.h или wdm.h).

RtlCreateRegistryKey создает подраздел внутри подраздела Реестра, указанного вторым параметром, который работает так же, как и в описанной выше функции RtlCheckRegistryKey.

RtlQueryRegistryValues позволяет одним вызовом получить значения нескольких параметров из всего поддерева указанного подраздела Реестра. При обнаружении требуемого параметра происходит вызов callback процедуры QueryRoutine, предоставляемой драйвером. Тем не менее, во всем многообразии примеров DDK нет ни одного случая использования этого вызова с callback процедурой QueryRoutine. На практике, вызов RtlQueryRegistryValues выполняется с первым параметром, имеющим в своем составе флаг RTL_QUERY_REGISTRY_DIRECT, при котором callback процедура не используется.

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

UNICODE_STRING newParameterUnicodeTextValue; RtlInitUnicodeString( &newParameterUnicodeTextValue, L"Example text parameter 1");

NTSTATUS status = RtlWriteRegistryValue( RTL_REGISTRY_SERVICES, L"Example1\\InnerKey", L"NewParameter", // можно даже L"Вложенный Раздел" REG_SZ, newParameterUnicodeTextValue.Buffer, sizeof(WCHAR)*( wcslen(newParameterUnicodeTextValue.Buffer)+1) ); if ( !NT_SUCCESS( status ) ) { #if DBG DbgPrint("RtlWriteRegistryValue call is unsuccessful."); #endif }

Первый параметр RTL_REGISTRY_SERVICES указывает на то, что действия будут выполняться в разделе \Registry\Machine\System\CurrentControlSet\Services, или, в общепринятых терминах, HKLM\SYSTEM\CurrentControlSet\Services.
В нем должен существовать вложенный подраздел \Example1. Тогда возможны два варианта: в подраздел \Example1 вложен существующий подраздел \InnerKey, либо раздел \InnerKey не существует (и тогда он будет создан). В результате вызова получится подраздел HKLM\SYSTEM\CurrentControlSet\Services\Example1\InnerKey, в котором будет присутствовать строковый параметр NewParameter со значением "Example text parameter 1".

В том случае, если не существует не только подраздел \InnerKey, но и подраздел \Example1, вызов RtlWriteRegistryValue завершится неудачей.

В том случае, если все указанные подразделы присутствуют, и параметр NewParameter уже имеет какое-то значение, то в результате последовательности действий, как в описанном выше примере, в Системный Реестр будет записано новое значение, то есть "Example text parameter 1" .

RtlDeleteRegistryVaIue удаляет параметр из указанного подраздела, например:

status = RtlDeleteRegistryValue( RTL_REGISTRY_SERVICES, L"Example1\\InnerKey ", L"NewParameter" ); if( !NT_SUCCESS(x_status ) ) { #if DBG DbgPrint("RtlDeleteRegistryValue call is unsuccessful."); #endif }

В результате, в HKLM\SYSTEM\CurrentControlSet\Services\Example1\InnerKey исчезнет параметр NewParameter.


Функция CompleteIrp


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

// // (Файл init.cpp) // CompleteIrp: Устанавливает IoStatus и завершает обработку IRP // Первый аргумент - указатель на объект нашего FDO. // NTSTATUS CompleteIrp( PIRP Irp, NTSTATUS status, ULONG info) { Irp->IoStatus.Status = status; Irp->IoStatus.Information = info; IoCompleteRequest(Irp,IO_NO_INCREMENT); return status; }



Функционирование DPC


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

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

Для учета заявок на вызов DPC процедур операционная система поддерживает очередь объектов DPC.

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

Объект DPC для использования в процедурах обработки прерываний создается по вызову IoInitializeDpcRequest, выполняемому обычно в стартовых процедурах драйвера. Данный вызов регистрирует предлагаемую драйвером DpcForIsr процедуру и ассоциирует ее с создаваемым объектом &#8212 достаточно распространенная методика в Windows. Следует особо отметить, что DPC объект, созданный данным вызовом так и останется в недрах операционной системы, недоступным разработчику драйвера. (Отличие DpcForIsr от других DPC процедур состоит только в том, что работа с последними проходит при помощи вызовов Ke...Dpc, а создаваемые для них DPC объекты доступны разработчику драйвера.)

Если драйвер зарегистрировал свою процедуру DpcForIsr, то во время обработки прерывания ISR процедурой в системную очередь DPC может быть помещен соответствующий DPC объект (фактически, запрос на вызов этой DpcForIsr процедуры позже) &#8212 при помощи вызова IoRequestDpc. Процедура DpcForIsr и завершит позже обработку полученного ISR процедурой запроса, что будет выполнено в менее критичных условиях и при низком уровне IRQL.

В общих чертах, функционирование DPC процедур (в данном случае, DpcForIsr) складывается из следующих операций:

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

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



Голубой экран смерти (BSOD)


Системное сообщение об останове системы &#8212 это появляющееся на голубом фоне сообщение, которое носит закрепившееся за ним название "Blue Screen Of Death" (BSOD). В разных версиях NT вид BSOD сообщений сильно менялся. В русифицированных версиях зачастую на экране видна абракадабра вместо текста (из-за неверной кодировки), что, однако, не должно сильно обременять разработчика. Главной информацией являются две строки, содержащие код ошибки и некоторые параметры сбоя (шестнадцатеричные числа), например:

DRIVER_IRQL_NOT_LESS_OR_EQUAL *** STOP: 000000Dl (00000000 00000002 00000000 F8B57624) *** Example.sys - address F8B57624 base at F8B57000 Datestamp 3e6da099

Сообщение код "bugcheck code", указанный в текстовой форме (DRIVER_IRQL_NOT_LESS_OR_EQUAL), его шестнадцатеричное значение (здесь 000000D1, после слова STOP) и четыре аргумента, переданные в вызове KeBugCheckEx. B зависимости от кода интерпретируется значение остальных 4-х аргументов (см. Приложение А).

Коду 0xD1 соответствует DRIVER_IRQL_NOT_LESS_OR_EQUAL. Приложение А указывает, что код 0xD1 означает ошибку, связанную с обращением к отсутствующей в физической памяти странице (page fault) на уровне IRQL равном DISPATCH_LEVEL или выше. Кроме того, при этом коде ошибки можно сказать, что четыре аргумента KeBugCheckEx означают следующее:

Arg1: Ссылочный адрес (обращение к которому вызвало сбой) 00000000 Arg2: Текущий IRQL уровень 2 Arg3: Тип доступа 0 (что означает "чтение") Arg4: Адрес инструкции, которая вызвала сбой 0xF8B57624



Группа функций (Ex)InterlockedXxx


В том случае, если разработчика драйвера устраивает то, что размер охраняемых данных составит размер sizeof(LONG) или sizeof(PVOID), то тогда в его распоряжении оказывается набор вызовов InterlockedXxx, например, InterlockedExchange. Эти вызовы реализуют доступ к переменной типа LONG и некоторые операции над ней в эксклюзивном (атомарном) режиме, например, операции увеличения и уменьшения на единицу, сравнения и т.п., хотя многие из них не документированы в DDK. Операции безопасного доступа и сравнения имеются и для указателей.

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

Похожие действия выполняет набор функций ExInterlockedXxx, который позволяет также безопасно работать и со списками. Вызовы InterlockedXxx

являются более быстрыми, если сравнивать их с функционально соответствующими вызовами ExInterlockedXxx.



HAL, Hardware Abstraction Layer


Слой аппаратных абстракций. Слой программного обеспечения в Windows NT, который призван скрыть специфику аппаратной платформ (Intel32, Intel64, Alpha) от остальных компонентов операционной системы, обеспечивая малые затраты при переносе системы или элементов программного обеспечения. Уровень HAL предоставляет процедуры, которые позволяют абстрагироваться от аппаратных тонкостей, как, например, детали реализации шин ввода/вывода, прерываний, DMA операций и т.п.

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

Персонально драйверам WDM остается лишь скромный набор макроопределений для доступа к портам ввода/вывода.

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



Hardware branch


Подраздел Системного Реестра HKLM\Hardware. Расширенное множество, описывающее всю аппаратуру, когда-либо установленную на данном компьютере. Подмножество этого списка, называемое hardware tree, резидентно находящиеся в оперативной памяти, содержит только реально присутствующие в системе устройства.



Идентификаторы PnP устройств


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

Каждое устройство, спроектированное по спецификации PnP, должно иметь идентификатор, который однозначно определяет модель данного устройства. Этот идентификатор должен быть предоставлен шинному аппаратному обеспечению (а следовательно, и шинному драйверу) по поступлении запроса. Разумеется, шинный драйвер подает запрос сразу же, как только новое устройство подключено. Секция [Models] в inf-файле содержит значение hw_id, играющее роль идентификатора модели.

В примере инсталляционного inf-файла для драйвера Example.sys (глава 3) в роли такого идентификатора выступила строка "*svpBook\Example", что было приемлемо для не-PnP устройства. Значение, вводимое в поле hw_id для PnP устройств, должно придерживаться определенного формата, изменяющегося в зависимости от типа шины, к которой устройство подключается, но обычно идентификатор поступает в устанавливающий программный код в виде:

тип_шины\идентификатор_модели

например:

PGI\VEN_1011&DEV_002&SUBSYS_00000000&REV_02

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



IEEE 1394: Firewire Bus


Предложенная и реализованная Apple Computers, a впоследствии определенная институтом IEЕЕ как высокоскоростная одноранговая (peer-to-peer) последовательная шина, IEEE 1394 предназначена для приложений, когда USB шина версии 1.1 не могла использоваться из-за низкой скорости передачи данных. Спецификация IEEE 1394 (на текущий момент &#8212 IEEE 1394a-2000) описывает три стандартных скорости передачи данных 100, 200 и 400 Мбит/сек. (Спецификация IEEE 1394b поддерживает большие скорости.) Но даже при таких скоростях, шина IEEE 1394 уже в состоянии переносить более 10 Мбайт в секунду, что превышает показатели ISA.

Наименование Firewire представляет собой торговую марку Apple Computers. Термин 1394 обычно используется для того, чтобы обозначить принадлежность этой спецификации к аппаратному обеспечению персональных компьютеров. Фирма Sony и другие производители видеокамер используют для обозначения своей модификации 1394 термин i.Link(TM).

Каждое устройство IEEE 1394 может быть подключено к шине кабелем 4,5 м, содержащем 6 или 4 провода. К шине может быть подключено до 63 устройств шлейфовым образом (daisy-chained) при общей длин соединения 72 м. Можно использовать шинные мосты и устройства разветвления, что позволяет подключить дополнительно до 62 устройств. При использовании шинных мостов можно задействовать до 1024 шин. В результате, число устройств, которые теоретически можно подключить к компьютеру в конфигурации 1394, составляет 64K. При подключении устройства, ему присваивается 16-разрядный идентификатор (Node ID).

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



Инициализация ядра


По завершении загрузки, ядро инициализируется и ему передается управление от загрузчика NTLDR.

Создается раздел HKLM\Hardware по результатам распознавания аппаратуры, куда заносится информация о системной плате, устройствах и прерываниях.

Создается набор параметров Clone путем копирования управляющих параметров; информация о которых содержится в параметре Current в разделе HKLM\System\Select. Набор Clone никогда не модифицируется.

Загружаются драйверы, указанные в разделе системного реестра HKLM\System\CurrentControlSet\Services, в параметрах которых присутствует значение Start равное 0x01 , порядок загрузки которых так же, как и было указано выше, определяется в параметре Group. Драйверы инициализируются сразу же после их загрузки. Значения параметра ErrorControl в описании драйвера (то есть в его параметре, указанном в Системном Реестре) определяет реакцию системы в том случае, если при загрузке и инициализации данного драйвера произошла ошибка. Подробнее возможные значения параметра ErrorControl и соответствующие способы реакции операционной системы представлены в Приложении В.

Запускаются сервисы (например, Служба Журнала Событий) и драйверы.



Инсталляция драйверов при помощи INF файлов


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

Пример, приведенный в главе 3, предлагал два способа установки &#8212 при помощи SCM Менеджера и при помощи inf-файла.

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

Инсталляция при помощи inf-файла позволяет выполнить все действия по копированию файлов, относящихся к драйверу, и внесению изменений в Системный Реестр практически без участия пользователя. Текстовый формат inf-файла сходен со стилем старых inf-файлов Windows 3.x, но теперь этот формат много сложнее.

Еще одним, третьим, способом инсталляции можно считать создание специального приложения пользовательского режима, которое выполняет ту же работу (копирование файлов и внесение новой информации в Системный Реестр). Однако такая методика не может быть признана приемлемой при работе с самостоятельно идентифицирующимися (PnP) устройствами: в этом случае запустившийся в ответ на обнаружение нового устройства Мастер Установки работает с inf-файлом, который одной из своих записей должен подтверждать свою "ответственность" за обнаруженное устройство. Данная глава описывает правила построения INF файлов и работы с ними.



Инсталляция и запуск драйвера Example.sys


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



Инсталляция с использованием INF файла


Для такого способа инсталляции драйвера потребуется создать текстовый файл (назовем его Example.inf), в котором будет представлена информация для работы Мастера Установки нового оборудования. В данном файле имеет значение даже то, куда поставлена запятая. Поэтому его следует повторить в точности. (Более подробно составление inf-файлов обсуждается в документации DDK, файл справки install.chm, и в главе 12.)

; Example.Inf - install information file ; Created 2 feb 2003 by SVP [Version] Signature="$Chicago$" Class=Unknown Provider=%SVPBook% DriverVer=02/22/2003,1.0.0.2

[Manufacturer] %SVPBook%=SVP.Science

[SVP.Science] %Example%=Example.Install, *svpBook\Example

[DestinationDirs] Example.Files.Driver=10,System32\Drivers ; куда копировать для Win98 Example.Files.Driver.NTx86=10,System32\Drivers ; куда копировать для NT

[SourceDisksNames] 1="Example build directory",,, ; первая цифра -- единица

[SourceDisksFiles] Example.sys=1,drv\w98 ; где находится новый драйвер для Win98

[SourceDisksFiles.x86] Example.sys=1,drv\nt ; где находится новый драйвер для NT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Windows 98 [Example.Install] CopyFiles=Example.Files.Driver AddReg=Example.AddReg

[Example.AddReg] HKR,,DevLoader,,*ntkern HKR,,NTMPDriver,,Example.sys [Example.Files.Driver] Example.sys ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Windows 2000, XP, Server 2003 [Example.Install.NTx86] CopyFiles=Example.Files.Driver.NTx86

[Example.Files.Driver.NTx86] Example.sys,,,%COPYFLG_NOSKIP%

[Example.Install.NTx86.Services] AddService = Example, %SPSVCINST_ASSOCSERVICE%, Example.Service

[Example.Service] DisplayName = %Example.ServiceName% ServiceType = %SERVICE_KERNEL_DRIVER% StartType = %SERVICE_AUTO_START% ErrorControl = %SERVICE_ERROR_NORMAL% ServiceBinary = %10%\System32\Drivers\Example.sys ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Strings [Strings] SVPBook="Introduction to Driver Programming" Example="Example driver: checked build" Example.ServiceName="Example NTDDK driver (V.001)"


SPSVCINST_ASSOCSERVICE=0x00000002 COPYFLG_NOSKIP=2 ; Do not allow user to skip file SERVICE_KERNEL_DRIVER=1 SERVICE_AUTO_START=2 SERVICE_DEMAND_START=3 ; см. п. 11.1.10 SERVICE_ERROR_NORMAL=1

Для проведения инсталляции рекомендуется воспользоваться дискетой. По крайней мере, не следует проводить инсталляцию из директорий на жестком диске, имеющих в названии пробелы и символы кириллицы, например, "С:\Пример драйвера\".

В корневой каталог дискеты следует поместить данный файл, Example.inf, a также создать директорию a:\drv со вложенными поддиректориями a:\drv\w98 и a:\drv\nt, куда следует поместить по одной копии файла драйвера Example.sys. (В том случае, если решено устанавливать драйвер из директории на жестком диске, то указанная структура информации должна быть также соблюдена).

Теперь (когда уже создан inf-файл) можно приступать к установке драйвера при помощи Мастера Установки нового оборудования (Пуск — Настройка — ...). При его работе важно выполнить следующие действия:

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

В Windows 98 следует указать тип устройства "? Другие устройства".

Выбрать установку драйвера с диска, после чего следует указать диск 'a:' (либо директорию на жестком диске, где находится Example.inf, поддиректории \drv\nt и \drv\w98 и две копии Example.sys, как было указано выше).

После идентификации inf файла Мастер Установки нового оборудования самостоятельно скопирует файл Example.sys из соответствующей директории drv\w98 или drv\nt (в нашем случае эти файлы идентичны) в \System32\Drivers внутри системной директории. Мастер Установки произведет модификацию записей в Системном Реестре, в результате чего драйвер будет загружаться после загрузки системы (когда она произойдет в следующий раз).

Для запуска данного драйвера сразу после установки Мастером Установки не требуется перезагрузки системы. (Но для других драйверов под Windows 98 это может потребоваться.)



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

Результаты работы Мастера Установки с записями Системного Реестра следует искать в разделе HKLM\System\CurrentControlSet\Services\Class\Unknown

(для Win98) и в разделе HKLM\System\CurrentControlSet\Services\Unknown\Example

(для Windows 2000/XP/Server 2003).

Следует отметить, что информацию о драйвере Example.sys после установки можно увидеть в Настройках Системы (Система/Диспетчер Устройств в Windows NT, либо Система/Устройства в Windows 98), однако многие информационные поля там не будут определены (в случае Windows NT таких полей будет меньше). Это объясняется тем, что информация, для которой указано "неизвестна" должна поступать из файла драйвера, для чего в нем должны быть предусмотрены информационные ресурсы, обычно размещающиеся в .rc файле проекта. В данном проекте такого файла нет, поэтому не вся желаемая информация предоставляется системным службам.

Возникает вопрос, почему драйвер, предназначенный для NT, запускается и работает под Windows 98?! Ответ прост. В Windows 98 установлен модуль ntkern.vxd, который так выполняет свою работу, что драйверам NT кажется, что они имеют дело с NT-системой, К сожалению, возможности его не безграничны, иначе Windows 98 была бы Windows NT.
Другой вопрос, который может возникнуть после описанной процедуры: почему мы смогли установить драйвер, в сущности, "никакого" устройства?! Ответ также несложен. Поскольку к системе могут подключаться устройства, не поддерживающие PnP (legacy devices), которые не могут быть автоматически обнаружены и которые не могут быть подключены (загружены их драйверы) иначе, чем по указанию администратора системы, то фирма Microsoft обязана предоставить способ установки драйверов "по желанию". Что и произошло в нашем случае.

При установке драйвера под операционной системой Windows2000/XP/Server 2003 может появиться сообщение следующего вида (рисунок 3.1).
<


Данное сообщение, выдаваемое операционной системой, связано с тем, что фирма Microsoft для повышения ответственности разработчиков за качество своих драйверов ввела программу тестирования и подписания вновь присоединяемых к дистрибутиву Windows драйверов. Для получения цифровой подписи драйвер должен пройти тестирование в специальной лаборатории Microsoft (соответственно, она действует только на конкретный бинарный .sys файл, при перекомпиляции цифровую подпись следует получать заново). Разумеется, "потренировать" свой драйвер перед такой процедурой вполне можно — для этого Microsoft поставляет соответствующие программные средства. В данном случае для инсталляции драйвера Example.sys следует выбрать кнопку "Все равно продолжить".

Рис. 3.1

Предупреждение о том, что драйвер не подписан
По завершении инсталляции в окне Диспетчера Устройств (свойства устройства) можно увидеть сообщения об установленном драйвере, в частности, в форме, представленной на рисунке 3.2 (в графе "Цифровая подпись" для данного драйвера указано, что она отсутствует).

Рис. 3.2

Свойства драйвера в окне Диспетчера Устройств
Следует также удостовериться при помощи перечисленных в главе 2 программ, поступила ли информация и драйвере (и в достаточном ли объеме) в операционную систему. Программа DeviceTree предоставляет информацию, показанную на рисунке 3.3. На нем показаны коды IRP_MJ_Xxx, для которых драйвер зарегистрировал собственные процедуры обработки, а также более общая информация о драйвере, в частности, операционная система сама установила для него флаг LEGACY_DRIVER.

Рис. 3.3

Общие свойства драйвера в окне DeviceTree, поддерживаемые IRP_MJ_Xxx
Более подробно вопросы составления inf файлов для установки драйверов будут рассмотрены в главе 12.

В процессе установки драйвера при помощи Мастера установки система выполняет резервное копирование файлов, отражающих ее состояние перед установкой драйвера. Результаты этой работы сохраняются в директории \System Volume Information\RpNn

на одном из логических дисков (Nn — это номер резервной точки). В том случае, если инсталляция драйвера приведет к нестабильной работе системы, можно восстановить ее состояние на момент сохранения данной резервной точки (reserve point) через запуск системной утилиты Пуск — Программы — Стандартные — Служебные — Восстановление Системы. Эта же утилита позволит администратору выполнить принудительное создание резервной копии, если в том имеется необходимость. Более подробно эти вопросы освещены в обстоятельной книге Ольги Кокоревой "Реестр Windows XP", рассматривающей многие аспекты организации Системного Реестра Windows NT, весьма важные для разработчика драйверов.
<


br>

В большинстве случаев драйвер, установленный с помощью Мастера установки, можно отключить и включить (задействовать) снова, даже не выполняя при этом перезагрузку системы. Если между отключением и включением выполнить подмену бинарного файла драйвера (например, на новую версию), то в результате таких манипуляций в работу вступит новая версия драйвера. Разумеется, чтобы обеспечить такую возможность, драйвер должен иметь корректно написанные процедуры завершения работы и выгрузки. Подмену файла WDM драйвера РпР устройства вполне успешно можно выполнять в то время, когда отключены все PnP устройства, обслуживаемые таким драйвером. Однако выполнение установки новых версий драйвера только с помощью Мастера установки дает следующее небольшое преимущество: средствами системы можно восстанавливать предыдущую версию драйвера (если она существовала). Для этого необходимо в окне — см. рисунок 3.2 — выбрать пункт "Откатить".

Инсталляция с использованием программы Monitor


Как было сказано в главе 2, для запуска драйверов "в-стиле-NT" под управлением Windows NT предназначена программа Monitor, разработанная CompuWare Corporation и входящая в состав пакета Driver Studio (в том числе, в 30-дневную trial версию).

Эта программа прекрасно подходит для загрузки, запуска, остановки и удаления драйвера Example.sys. Интуитивно понятный графический интерфейс этой программы практически не требует дополнительных пояснений. Для запуска драйвера не следует перезагружать систему и самостоятельно модифицировать Системный Реестр, что удобно для проведения быстрых автономных тестов.

Если перед запуском драйвера из программы Monitor предварительно запустить программу DebugView, то все отладочные сообщения, которые были введены даже в функции DriverEntry, будут отображены в рабочем окне DebugView и могут быть сохранены в файле протокола (LOG-файле).

Более подробно режимы ознакомительного тестирования драйвера Example.sys при помощи собственного приложения пользовательского режима будут рассмотрены ниже.



Инсталляция с использованием сервисов SCM Менеджера


В операционной системе Windows NT имеется компонент, называемый Service Control Manager (SCM Менеджер). Удобство предоставляемых им услуг состоит в том, что, используя его функции в приложениях пользовательского режима, можно динамически запускать и выгружать драйверы, требующиеся только данному конкретному приложению, не прибегая к вызову Мастера Установки нового оборудования. Таким образом, приложение само определяет время присутствия драйвера в операционной системе.

Достаточно подробное описание программирования приложений с использованием функций SCM имеется в документации MSDN (практически, единственное место ее соприкосновения с потребностями собственно разработки драйверов), поставляемой отдельно или в составе пакетов Microsoft Visual Studio.

Работа с сервисами SCM менеджера начинается с вызова функции OpenSCManager

(это имя можно рассматривать как "точку входа" в документацию MSDN по программированию с применением SCM функций) и завершается вызовом функции CloseServiceHandle.

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

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



Инсталляция внесением записей в Системный Реестр


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



Интерфейс системных служб


System Service Interface. Данный компонент обеспечивает точки перехода из пользовательского режима в код режима ядра, что позволяет пользовательским приложениям (потокам этих приложений) безопасно осуществлять вызовы системных сервисов (процедур режима ядра). В зависимости от платформы, переход из пользовательского режима к коду режима ядра может быть и простой процессорной инструкцией, и достаточно сложным переключателем контекста Save или Restore.



Интервалы ожидания для отдельного потока


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

Поток, который желает приостановить свою работу на время до 50 мкс, может использовать вызов KeStallExecutionProcessor.

Таблица 10.7. Прототип вызова KeStallExecutionProcessor

VOID KeStallExecutionProcessor IRQL == любой
Параметры Останавливает работу на указанный интервал, независимо от производительности процессора
IN ULONG IntervalCount Время задержки в 1 мкс интервалах
Возвращаемое значение void

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

Более сложным является вызов KeDelayExecutionThread (таблица 10.8). Он удаляет программный поток из очереди "ready to run", следовательно, не мешает выполнению других потоков, готовых к работе. Минимальный временной интервал определяемой им задержки составляет 100 нс.

Рекомендуемые для драйверов значения WaitMode=KernelMode

и Alertable=FALSE ограничивают применимость вызова KeDelayExecutionThread кодом системных потоков, созданных самим драйвером, и кодом процедур инициализации и завершения работы драйвера (то есть работающего заведомо вне пользовательского контекста).

Таблица 10.8. Прототип вызова KeDelayExecutionThread

NTSTATUS KeDelayExecutionThread IRQL == PASSIVE_LEVEL
Параметры Останавливает работу на указанный интервал, независимо от производительности процессора
IN KPROCESSOR_MODE WaitMode Для драйверов: KernelMode
IN BOOLEAN Alertable Для драйверов: FALSE

IN PLARGE_INTEGER TimeInterval Время задержки в 100нс интервалах
Возвращаемое значение STATUS_SUCCESS &#8212 ожидание завершено
<
Значение TimeInterval может описывать как относительные, так и абсолютные временные интервалы. Для задания абсолютных интервалов следует использовать вызов KeQuerySystemTime

(см. таблицу 7.45), при помощи которого можно получить текущее системное время &#8212 как время начала ожидания. Функции, которые можно использовать для операции над типом данных LARGE_INTEGER, перечислены в таблице 7.44.

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

Таблица 10.9. Прототип вызова IoInitializeTimer

NTSTATUS IoInitializeTimer IRQL == PASSIVE_LEVEL
Параметры Выполняет регистрацию callback-функции IoTimerRoutine, предоставляемой драйвером
IN PDEVICE_OBJECT pDevObject Объект устройства инициатора вызова, за которым будет "закреплен" создаваемый данным вызовом объект таймера
IN PIO_TIMER_ROUTINE pIoTimerRoutine Указатель на регистрируемую callback-функцию IoTimerRoutine
IN PVOID pContext Аргумент, передаваемый впоследствии в callback-функцию IoTimerRoutine
Возвращаемое значение STATUS_SUCCESS при успешном завершении
В результате вызова IoInitializeTimer (таблица 10.9) операционная система создает таймерный объект режима ядра и связывает его с объектом устройства и callback-функцией IoTimerRoutine, предоставляемой драйвером. Регистрацию функции IoTimerRoutine лучше всего выполнять сразу после создания объекта устройства в процедуре AddDevice или DriverEntry (для не-WDM драйверов).


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

Таблица 10.10. Прототип функции обратного вызова IoTimerRoutine

VOID IoTimerRoutine IRQL == DISPATCH_LEVEL
Параметры Callback-функция, вызываемая через 1 сек. интервал
IN PDEVICE_OBJECT pDeviceObject Указатель на объект устройства, с которым соотнесена данная функция
IN PVOID pContext Контекстный аргумент
Возвращаемое значение void
Собственно создание функции IoTimerRoutine и ее регистрация при помощи вызова IoInitializeTimer еще не приводят к работе таймера и периодическим вызовам IoTimerRoutine.

Если внимательно присмотреться к структуре DEVICE_OBJECT, то несложно заметить, что поле "PIO_TIMER Timer" в этой структуре единственное. Это недвусмысленно подразумевает, что более одной функций IoTimerRoutine для данного устройства использовать просто невозможно, хотя ничто не запрещает использовать одну callback-функцию IoTimerRoutine c несколькими объектами устройств.

Для запуска таймера, ассоциированного с callback-функцией IoTimerRoutine, используется вызов IoStartTimer. Останавливается таймер вызовом IoStopTimer.

Таблица 10.11. Прототип функции обратного вызова IoStartTimer

VOID IoStartTimer IRQL&#60=DISPATCH_LEVEL
Параметры Запуск таймера, в результате чего callback-функция IoTimerRoutine, соотнесенная с данным объектом устройства будет вызываться каждую секунду
IN PDEVICE_OBJECT pDeviceObject Указатель на объект устройства, с которым соотнесен таймер, который следует запустить
Возвращаемое значение void
Таблица 10.12. Прототип функции обратного вызова IoStopTimer

VOID IoStopTimer IRQL&#60=DISPATCH_LEVEL
Параметры Остановка таймера
IN PDEVICE_OBJECT pDeviceObject Указатель на объект устройства, с которым соотнесен таймер, который следует остановить
Возвращаемое значение void
<


Выполнение вызова IoStopTimer из функции IoTimerRoutine не допускается.

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

Работа с использованием callback-функции IoTimerRoutine может протекать следующим образом.

В процедуре AddDevice (DriverEntry) выполняется вызов IoInitializeTimer

и связываются таймерная функция IoTimerRoutine и конкретный объект устройства.

В момент, когда вызывается драйверная рабочая процедура, предназначенная для обслуживания IRP_MJ_CREATE (то есть вызова Win API CreateFile), выполняется вызов IoStartTimer. Bce время, пока дескриптор доступа к драйверу (устройству) остается открытым, производятся вызовы функции IoTimerRoutine.

Каждый раз, когда производится вызов callback-процедуры IoTimerRoutine, значение счетчика таймерных "тиков" (если он задан) уменьшается (увеличивается) на единицу.

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

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

Рассмотрим несложный частный случай.

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

Дополняем структуру расширения объекта устройства счетчиком времени, оставшегося до наступления таймаута (превышения времени ожидания) с момента последнего прерывания, поступившего от обслуживаемого устройства:

typedef struct {

. . . LONG Remaining; // сколько еще осталось секунд . . . } MYDEVICE_EXTENSION, *PMYDEVICE_EXTENSION;



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

NTSTATUS AddDevice ( IN PDRIVER_OBJECT pDriverObject, IN PDEVICE_OBJECT pDeviceObject ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDeviceObject -&#62DeviceExtension; . . . IoInitializeTimer(pDeviceObject, MyIoTimerRoutine, pDevExt); . . . }

Рабочая процедура драйвера CreateRequestHandler вызывается, когда в пользовательском приложении была попытка доступа к устройству через Win API вызов CreateFile. В этот момент вполне можно запустить таймер. Он продолжает отсчеты до тех пор, пока дескриптор доступа к устройству из пользовательского приложения остается открытым. Поскольку таймер работает, а его отсчеты нам еще не нужны, то необходимо инициализировать таймер таким значением, которое показывало бы, что его можно игнорировать (это будет -1).

NTSTATUS CreateRequestHandler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj-&#62DeviceExtension; . . . // ближе к концу инициализируем и запускаем таймер pDevExt-&#62Remaining = -1; IoStartTimer(pDevObj); . . . }

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

Использование системного вызова InterlockedExchange обеспечивает безопасное обновление и считывание 32-разрядного счетчика срабатываний таймера, который был ранее размещен в полностью определяемой разработчиком структуре расширения объекта устройства.



#define MY_INTERRUPT_TIMEOUT (10)

VOID StartIo( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj-&#62DeviceExtension; . . . InterlockedExchange(&pDevExt-&#62Remaining, MY_INTERRUPT_TIMEOUT); // Старт устройства: MyTransmitDataRoutine(pDevObj, pIrp); . . . }

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

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

BOOLEAN OnInterrupt ( IN PKINTERRUPT pInterruptObject, IN PVOID pContext ) { PDEVICE_EXTENSION pDeviceExt = (PDEVICE_EXTENSION)pContext; . . . // В случае, если остались еще данные для передачи, то // обновить счетчик if( IHaveTransmitBytes( pDeviceExt ) ) InterlockedExchange( &pDeviceExt-&#62Remaining, MY_INTERRUPT_TIMEOUT ); else // иначе - очистить счетчик InterlockedExchange ( &pDevExt-&#62Remaining, -1 ); . . . }

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

VOID MyIoTimerRoutine( IN PDEVICE_OBJECT pDeviceObj, IN PVOID pContext ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pContext; // Проверить время ожидания if( (pDevExt-&#62Remaining,-1) &#60 0 ) return; // значение счетчика не важно (поскольку -1) if( InterlockedDecrement(&pDevExt-&#62Remaining) == 0 ) { // Время ожидания истекло InterlockedExchange( &pDevExt-&#62Remaining, (-1) ); PIRP pCurrentIrp = pDeviceObj-&#62CurrentIrp; pCurrentIrp-&#62IoStatus.Status = STATUS_IO_TIMEOUT; pCurrentIrp-&#62IoStatus.Information = 0; IoRequestDpc( pDeviceObj, pCurrentIrp, NULL ) // Некоторые делают совсем "просто": // MyDpcForIsr(NULL, pDeviceObj, pCurrentIrp, pDevExt); } return; }



Существует маленький временной зазор между тем, как функция MyIoTimerRoutine убедилась, что счетчик активен, и моментом, когда произошло его уменьшение на единицу. Если предположить, что в этот момент "вклинилась" процедура OnInterrupt и установила значение счетчика в -1, то функция MyIoTimerRoutine, получив управление, сделает значение счетчика равным -2. Код, приведенный выше, учитывает эту возможность, сравнивая Remaining c нулем.

Зарегистрированная соответствующим образом процедура DpcForIsr может выглядеть следующим образом:

VOID MyDpcForIsr( IN PKDPC pDpcObj, IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp, IN PVOID pContext ) { . . . // Инициируем поступление IRP из внутренней очереди в // процедуру StartIO (): TodoStartNextPacket(&pDevExtension-&#62dqReadWrite, pDevObject);

// Даем возможность отработать процедурам завершения всех // вышестоящих драйверов, если они есть: IoCompleteRequest(pIrp, IO_NO_INCREMENT); . . . }


IO stack location


Ячейка стека ввода/вывода в пакете IRP. Одна позиция в изменяемой части пакета IRP, называемой стеком ввода/вывода IRP пакета. Ячейка стека сама является составной структурой. Если драйверы объединены в цепочку (называемую стеком устройств, Device Stack(1)), то, как правило, число ячеек стека IRP равно числу устройств в Device Stack перед данным устройством (возможны варианты). Собственно, ячейки стека IRP и предназначены для хранения "переменной" информации при хождении пакета IRP по стеку драйверов (или стеку устройств - на некотором этапе разработчики драйверов перестают делать различия между этими словосочетаниями). Однако хотя информация и "переменная", но имеет вполне определенный формат. При путешествии по процедурам драйвера, те извлекают из "своей" ячейки стека ввода/вывода полезную информацию. В некоторых случаях, передавая IRP пакет вниз по стеку устройств (драйверов), драйверы могут и сохранять там, в пределах "своей" ячейки, некоторые текущие данные - если ожидают, что получат этот IRP пакет при его обратном движении по стеку устройств.



IOCTL


I/O ConTroL code. Код управления вводом/выводом. Позволяет обращаться к драйверу с запросами, отличающимися от операций чтения и записи в устройство (хотя и они легко реализуются через IOCTL запросы). Разработчик драйвера имеет возможность создавать свои собственные коды IOCTL. Данный код является одним из аргументов функции пользовательского режима DeviceIoControl (в приложениях пользовательского режима). Поступающий в драйвер в результате работы этой пользовательской функции и Диспетчера ввода/вывода пакет IRP будет иметь код IRP_MJ_DEVICE_CONTROL, а одним из внутренних параметров данного IRP пакета будет указанный в вызове функции DeviceIoControl код IOCTL.



IOManager


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

Помимо ДВВ в операционной системе выделяются и другие "сгустки" кода, которые обслуживают какую-нибудь часть ее ресурсов и называются Менеджер или Диспетчер, например, Менеджер Памяти, Диспетчер Объектов, Менеджер Энергопотребления (Power Manager), PnP Менеджер, Менеджер Безопасности (Security Manager) и т.п.



IRP, Input/output Request Packet, IRP request, IRP packet


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



IRQ, Interrupt Request Line


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



IRQL, Interrupt ReQuest Level


Уровень приоритета выполнения. Термин, исторически доставшийся от задач по обслуживанию прерываний (IRQ), но теперь связанный не только с ними. Приоритеты, принятые для программного кода, работающего в режиме ядра. Планирование потоков с приоритетами IRQL хотя бы на 1 выше минимального (PASSIVE_LEVEL) сильно отличается от планирования потоков в пользовательском режиме. В режиме ядра поток, работающий при некотором приоритете IRQL может быть прерван только для выполнения работы потоком с более высоким IRQL. Даже поток с равным приоритетом IRQL должен дожидаться естественного окончания работы своего "равноправного коллеги". Поток может самостоятельно повысить свой IRQL, однако, величина повышения в некоторых версиях Windows не произвольна. Например, работая на уровне PASSIVE_LEVEL (0), поток может получить от операционной системы Windows XP согласие только на уровень IRQL равный 10 (для сравнения, DISPATCH_LEVEL имеет численное значение 3, a INTERRUPT_LEVEL численно равен 13).

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

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



ISA: Industry Standard Architecture


Шина ISA была предложена фирмой IBM для PC/AT в начале 1980-x. Она поддерживала подключение и 8-разрядных, и 16-разрядных устройств. По той причине, что эта шина изобреталась два десятилетия назад, она не отличалась ни быстротой, ни простотой конструкции. Эта шина является динозавром компьютерных технологий, однако ее невозможно забыть по двум причинам.

Во-первых, столь популярные и до сих пор обязательные устройства настольного компьютера, какими являются параллельный порт LPT и последовательные СОМ порты, являются устройствами ISA шины. Эта ISA шина через мост подключается к шине PCI, и существует в компьютере даже тогда, когда на материнской плате нет ни одного ISA разъема. (Для фанатиков же ISA устройств некоторые фирмы, например PHILIPS, выпускает микросхемы для реализации подключения ISA плат к шине USB.)

Во-вторых, при усовершенствовании компьютеров, которые ISA шина поначалу вполне устраивала, выявилось столько неудобств и ограничений, что стало очевидным &#8212 к принятию шинных протоколов следует подходить более ответственно. Невысокая пропускная способность (максимум 8 МБ/сек), отсутствие стандарта на использование адресов ввода/вывода, невозможность совместного использования прерываний (их всего 16, большая часть которых уже занята системными устройствами, оставляя вновь подключаемым устройствам всего лишь 2 или 3) настойчиво указывали на необходимость новых решений. Например, старые ISA платы не могут использовать все 16 разрядов адресов ввода/вывода, а декодируют только первые 10 разрядов адреса, в результате чего отзываются по адресам через 0x400 адресов. Устройство с адресом 0x300 отвечает также и по адресу 0x700. Появление таких устройств в системе приводит к откровенной бесполезности большей части 64 Кбайтного адресного пространства ввода/вывода. И, наконец, шина ISA имеет возможность всего лишь 24 разрядной адресации, что определяет ограничение на выполнение переноса данных только в нижние 16 МБ физической памяти. Этот артефакт доставляет Windows особые проблемы при работе с ISA шиной, если возникает необходимость применить DMA способ передачи данных.

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

Современные ISA устройства пытаются скорректировать эту ситуацию, предлагая Plug and Play расширение к стандарту ISA. Такие устройства значительно расширили свою популярность с введением операционной системы Windows 95. Версии NT, существовавшие до Windows 2000, реально не поддерживали спецификацию PnP, отчего этим новым ISA устройствам приходилось полностью полагаться на специальные программы инсталляции, которые обеспечивали их должное функционирование под NT, и услуги Системного Реестра. Однако с выходом Windows 2000 поддержка таких устройств стала вполне корректной.



Исполнительные компоненты


Так как Исполнительные компоненты представляют базисные сервисы операционной системы Windows NT 5 (в дополнение к планированию потоков, осуществляемых ядром), их обязанности ясно очерчены.

В таблице 4.1 представлены наименования основных исполнительных компонентов операционной системы. В первом столбце указаны сокращения, обычно являющиеся первыми символами имен функций, которые данный компонент предоставляют разработчику для использования при программировании в режиме ядра. Например, функция RtlCopyMemory, являющаяся аналогом известной функции пользовательского режима memcpy, предоставляется библиотекой времени выполнения Rtl (Run Time Library).

Таблица 4.1. Исполнительные компоненты Windows NT5

Сс Диспетчер кэша
Dbg Поддержка отладки
Ex Поддержка исполняющей подсистемы Ex(ecutive)
FsRtl Библиотека времени выполнения для поддержки файловой системы (File System Run-Time Library)
Hal Диспетчер уровня аппаратных абстракций, the Hardware Abstraction Layer (HAL)
Inbv Драйвер инициализации системы/загрузки VGA
Init Инициализация системы
Interlocked Потокобезопасное оперирование переменными
Io Диспетчер ввода/вывода Io

(I/O Manager)

Kd Поддержка Kernel Debugger
Ке Подпрограммы ядра (Kernel)
Ki Обработка ядра
Ldr Загрузчик образа
Lpc Локальный вызов процедур (Local Procedure Call)
Lsa Local Security Authority
Mm Менеджер памяти (Менеджер Виртуальной памяти, VMM)
Nls Лингвистическая поддержка (National Language Support)
Nt NT Native API
Ob Менеджер объектов (Object Manager)
Pfx Обработка префиксов
Po Менеджер электропитания
Ps Поддержка процессов (Process Structure)

Rtl Библиотека времени выполнения (Run-Time Library)
Se Управление безопасностью и обеспечение привилегий
Zw Альтернативный интерфейс Native API
другие Вспомогательные функции и библиотека времени выполнения С

Наиболее важные для разработчика драйверов компоненты будут рассмотрены ниже подробнее.



Исполнительские ресурсы


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

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

Если открыть определение структуры ERESOURCE в файле, например, wdm.h, то несложно понять, что исключительный доступ к данным, охраняемым объектом типа ERESOURCE, реализуется через механизм спин-блокировок.

Для работы с исполнительскими ресурсами используются вызовы, описанные в таблице 10.47. Как и быстрые мьютексы, эти объекты имеют собственные вызовы для запроса на владение, а не вызовы KeWaitForXxx. Разумеется, перед получением доступа следует выделить память под структуру ERESOURCE в нестраничной памяти и инициализировать ее при помощи вызова ExInitializeResourceLite.

Таблица 10.47. Функции для работы с исполнительскими ресурсами

Действие Используемый вызов
Создание ExInitializeResourceLite
Запрос на владение ExAcquireResourceExclusiveLite

ExAcquireResourceSharedLite

ExTryToAcquireResourceExclusizeLite

ExConvertExclusizeToSharedLite

ExAcquireSharedStarveExclusive

ExAcquireSharedWaitForExclusive

Запрос состояния ExIsResourceAcquiredExclusiveLite

ExIsResourceAcquiredSharedLite

Освобождение ExReleaseResourceForThreadLite
Удаление ExDeleteResourceLite

Запросы на владение можно выполнять из кода, работающего на уровне IRQL ниже DISPATCH_LEVEL, все остальные вызовы можно делать и из кода работающего собственно на этом уровне.

Ре-инициализация исполнительского ресурса может быть выполнена вызовом ExReinitializeResourceLite, который заменяет сразу три вызова (по удалению ресурса, выделению памяти под новую структуру и инициализации) и экономит память.



Исполняемый код драйвера


В файле Driver.cpp размещен исходный текст всех функций драйвера.

В процедуре DriverEntry выполняется регистрация процедур DriverUnload (отвечает за завершающие операции при выгрузке драйвера), DispatchCreate (при получении клиентом дескриптора для доступа к драйверу), DispatchClose (при закрытии дескриптора, полученного для доступа к драйверу), DispatchWrite (обработка IRP пакета, поступившего вследствие вызова WriteFile в приложении-клиенте), DispatchRead (обработка IRP пакета, поступившего вследствие вызова ReadFile

в приложении-клиенте).

Действия по созданию объекта устройства, символьной ссылки и подключению драйвера к прерыванию в данном Legacy драйвере тоже выполняются в DriverEntry, только лишь оформлены они в виде автономной функции CreateDevice. (В WDM драйвере реального PnP устройства эти операции следовало бы выполнять в процедуре AddDevice и обработчике IRP_MJ_PNP + IRP_MN_START_DEVICE, поскольку загрузка драйвера является только частью старта PnP устройства).

//========================================================================= // Файл driver.c // Драйвер обслуживания заглушки CheckIt (параллельный порт 378h) // By SVP, 20 June 2004 //========================================================================= #include "driver.h"

// Предварительные объявления функций static NTSTATUS CreateDevice ( IN PDRIVER_OBJECT pDriverObject, IN ULONG portBase, IN ULONG Irq ); static NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ); static NTSTATUS DispatchClose ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ); static VOID DriverUnload ( IN PDRIVER_OBJECT pDriverObject ); static NTSTATUS DispatchWrite ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ); static NTSTATUS DispatchRead ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );

BOOLEAN Isr ( IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext );

BOOLEAN DoNextTransfer ( IN PVOID pContext );

VOID DpcForIsr( IN PKDPC pDpc, IN PVOID DeferredContext, IN PVOID pArg1, IN PVOID pArg2 ); //========================================================================= // Функция: DriverEntry // Назначение: Инициализирует драйвер, подключает объект устройства для // получения прерываний. // Аргументы: pDriverObject - поступает от Диспетчера ввода/вывода // pRegistryPath - указатель на Юникод-строку, // обозначающую раздел Системного Реестра, созданный // для данного драйвера. // Возвращаемое значение: // NTSTATUS - в случае нормального завершения STATUS_SUCCESS // или код ошибки STATUS_Xxx // extern "C" NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS status; #if DBG==1 DbgPrint("LPTPORT: in DriverEntry, RegistryPath is:\n %ws. \n", pRegistryPath-&#62Buffer); #endif // Регистрируем рабочие процедуры драйвера: pDriverObject-&#62DriverUnload = DriverUnload;


pDriverObject-&#62MajorFunction[IRP_MJ_CREATE] = DispatchCreate; pDriverObject-&#62MajorFunction[IRP_MJ_CLOSE] = DispatchClose; pDriverObject-&#62MajorFunction[IRP_MJ_WRITE] = DispatchWrite; pDriverObject-&#62MajorFunction[IRP_MJ_READ] = DispatchRead;

// Работа по созданию объекта устройства, подключению // ресурсов, прерывания, созданию символьной ссылки: status = CreateDevice(pDriverObject, 0x378, 0x7);

return status; } //========================================================================= // Функция: CreateDevice // Назначение: Создание устройства с точки зрения системы // Аргументы: pDriverObject - поступает от Диспетчера ввода/вывода // portBase - адрес базового регистра параллельного порта (378h) // Irq - прерывание (в терминах шины ISA) для обслуживания порта // Возвращаемое значение: // NTSTATUS - в случае нормального завершения STATUS_SUCCESS // или код ошибки STATUS_Xxx // NTSTATUS CreateDevice ( IN PDRIVER_OBJECT pDriverObject, IN ULONG portBase, IN ULONG Irq ) { NTSTATUS status; PDEVICE_OBJECT pDevObj; PDEVICE_EXTENSION pDevExt; // Создаем внутреннее имя устройства

UNICODE_STRING devName; RtlInitUnicodeString( &devName, L"\\Device\\LPTPORT" ); // Создаем объект устройства status= IoCreateDevice( pDriverObject, sizeof(DEVICE_EXTENSION), &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj ); if (!NT_SUCCESS(status)) return status;

// Будем использовать метод буферизации BUFFERED_IO pDevObj-&#62Flags |= DO_BUFFERED_IO; // Заполняем данными структуру Device Extension pDevExt = (PDEVICE_EXTENSION)pDevObj-&#62DeviceExtension; pDevExt-&#62pDevice = pDevObj; // сохраняем - это пригодится pDevExt-&#62ustrDeviceName = devName; pDevExt-&#62Irq = Irq; pDevExt-&#62portBase = (PUCHAR)portBase; pDevExt-&#62pIntObj = NULL; pDevExt-&#62xferRest = 0; // сейчас нет неотправленных данных pDevExt-&#62pIntObj = NULL; //================================================ // Инициализируем объект DPC для последующего использования // при обработки прерываний: KeInitializeDpc( &(pDevExt-&#62DpcForIsr_Object), DpcForIsr, pDevExt // &#60- pDeferredContext в функции DpcForIsr ); //================================================ // На всякий случай блокируем поступление прерываний: WriteControlRegister ( pDevExt, CR_DEFAULT ); //================================================ // Создаем и подключаем объект прерываний: KIRQL kIrql; KAFFINITY kAffinity;



ULONG kVector = HalGetInterruptVector(Isa, 0, pDevExt-&#62Irq, pDevExt-&#62Irq, &kIrql, &kAffinity); // Замечание. Для Isa шины второй параметр (номер шины) обычно // равен 0, а третий и четвертый параметры равны.

#if DBG==1 DbgPrint( "LPTPORT: Interrupt %d converted to kIrql = %d, " "kAffinity = %d, kVector = %X(hex)\n", pDevExt-&#62Irq, kIrql, kAffinity, kVector); #endif

status = IoConnectInterrupt ( &pDevExt-&#62pIntObj, // Здесь будет создан Interrupt Object Isr, // Наша функция ISR pDevExt, // Этот указатель ISR функция будет // получать при вызове (контекстный указатель) NULL, // Не будем использовать spin-блокировку для // безопасного доступа к совместно используемым // данным kVector, // транслированное значение прерывания kIrql, // DIRQL kIrql, // DIRQL Latched, // Прерывание по перепаду TRUE, // Совместно используемое (Shared) прерывание kAffinity, // Поцессоров в мультипроцессорной системе FALSE ); // Не сохранять значения регистров сопроцессора if (!NT_SUCCESS(status)) { // В случае неудачи удаляем объект устройства IoDeleteDevice( pDevObj ); return status; } #if DBG==1 DbgPrint("LPTPORT: Interrupt successfully connected.\n"); #endif //================================================ // Создаем символьную ссылку: UNICODE_STRING symLinkName; // Сформировать символьное имя: //#define SYM_LINK_NAME L"\\??\\LPTPORT0" // ^^ проходит только в NT // Для того, чтобы работало в Windows 98 & XP : #define SYM_LINK_NAME L"\\DosDevices\\LPTPORT0"

RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME );

// Создать символьную ссылку: status = IoCreateSymbolicLink( &symLinkName, &devName ); if (!NT_SUCCESS(status)) { // При неудаче - отключаемся от прерывания и // удаляем объект устройства: IoDisconnectInterrupt( pDevExt-&#62pIntObj ); IoDeleteDevice( pDevObj ); return status; }

pDevExt-&#62ustrSymLinkName = symLinkName; #if DBG==1 DbgPrint("LPTPORT: Symbolic Link is created: %ws. \n", pDevExt-&#62ustrSymLinkName.Buffer); #endif return STATUS_SUCCESS; }



Работа c DPC процедурами может проходить по двум существенно различающимся сценариям. В первом из них, который будет реализован в следующем варианте драйвера, DPC процедура соотносится с объектом устройства вызовом IoInitializeDpcRequest, и код драйвера может запланировать ее вызов путем применения IoRequestDpc

со ссылкой на объект устройства. Таким образом, за объектом устройства можно закрепить одну DPC функцию. А сам DPC объект "обитает" в объекте устройства и его не рекомендуется "касаться" непосредственно.

Другой сценарий, реализуемый ниже, предлагает связывание DPC объекта (неинициализированный DPC объект &#8212 это просто область памяти под структурой типа KDPC) с одной из функций драйвера вызовом KeInitializeDpc, см. таблицу 10.23. Такой инициализированный DPC объект может быть вставлен в очередь DPC объектов с помощью вызова KeInsertQueueDpc &#8212 так можно запланировать к вызову связанную с ним DPC функцию драйвера в любом месте кода драйвера, правда, работающем при уровне IRQL не ниже DISPATCH_LEVEL. При использовании данного сценария, драйвер (в том числе, его процедура обработки прерывания) может планировать для последующего вызова разные DPC функции. Заметим, что, временно повысив IRLQ при помощи вызова KeRaiseIrql, драйвер может планировать вызовы DPC функций при помощи KeInsertQueueDpc даже внутри кода, работающего при IRQL, равном PASSIVE_LEVEL.

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

//========================================================================= // Функция: DriverUnload // Назначение: Останавливает и удаляет объекты устройств, отключает // прерывания, подготавливает драйвер к выгрузке. // Аргументы: pDriverObject - поступает от Диспетчера ввода/вывода // Возвращаемое значение: нет // VOID DriverUnload ( IN PDRIVER_OBJECT pDriverObject ) { #if DBG==1 DbgPrint("LPTPORT: in DriverUnload now\n"); #endif PDEVICE_OBJECT pNextObj = pDriverObject-&#62DeviceObject;



// Проход по всем устройствам, контролирумым драйвером for( ; pNextObj!=NULL; ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pNextObj-&#62DeviceExtension; // Удаляем объект прерываний: if (pDevExt-&#62pIntObj) { // На всякий случай блокируем поступление прерываний // и очищаем DPC очередь от нашего DPC объекта WriteControlRegister( pDevExt, CR_DEFAULT); KeRemoveQueueDpc( &(pDevExt-&#62DpcForIsr_Object) ); IoDisconnectInterrupt( pDevExt-&#62pIntObj ); } // Удаляем символьную ссылку: IoDeleteSymbolicLink(&pDevExt-&#62ustrSymLinkName); #if DBG==1 DbgPrint("LPTPORT: SymLink %ws deleted\n", pDevExt-&#62ustrSymLinkName.Buffer); #endif // Сохраняем ссылку на следующее устройство и удаляем // текущий объект устройства: pNextObj = pNextObj-&#62NextDevice; IoDeleteDevice( pDevExt-&#62pDevice ); } // Замечание. Поскольку мы использовали ресурс (параллельный порт) // объявленные не нами, то освобождение этого ресурса можно опустить. } //========================================================================= // Функция: DispatchCreate // Назначение: Обрабатывает запрос по поводу Win32 вызова CreateFile // Аргументы: pDevObj - поступает от Диспетчера ввода/вывода // pIrp - поступает от Диспетчера ввода/вывода // Возвращаемое значение: STATUS_SUCCESS // NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { #if DBG==1 DbgPrint("LPTPORT: in DispatchCreate now\n"); #endif pIrp-&#62IoStatus.Status = STATUS_SUCCESS; pIrp-&#62IoStatus.Information = 0; // ни одного байта не передано IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_SUCCESS; } //========================================================================= // Функция: DispatchClose // Назначение: Обрабатывает запрос по поводу Win32 вызова CloseHandle // Аргументы: pDevObj - поступает от Диспетчера ввода/вывода // pIrp - поступает от Диспетчера ввода/вывода // Возвращаемое значение: STATUS_SUCCESS // NTSTATUS DispatchClose ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { #if DBG==1 DbgPrint("LPTPORT: in DispatchClose now\n"); #endif pIrp-&#62IoStatus.Status = STATUS_SUCCESS; pIrp-&#62IoStatus.Information = 0; // ни одного байта не передано IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_SUCCESS; } //========================================================================= // Функция: DispatchWrite // Назначение: Обрабатывает запрос по поводу Win32 вызова WriteFile // Аргументы: pDevObj - поступает от Диспетчера ввода/вывода // pIrp - поступает от Диспетчера ввода/вывода // Возвращаемое значение: // NTSTATUS - в случае нормального завершения STATUS_SUCCESS // или код ошибки STATUS_Xxx // NTSTATUS DispatchWrite ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { #if DBG==1 DbgPrint("LPTPORT: in DispatchWrite now\n"); #endif PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp ); PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj-&#62DeviceExtension; ULONG xferSize = pIrpStack-&#62Parameters.Write.Length;



if( xferSize == 0 ) // Нет данных для передачи : { #if DBG==1 DbgPrint("LPTPORT: DispatchWrite: no bytes to transfer.\n"); #endif pIrp-&#62IoStatus.Status = STATUS_SUCCESS; pIrp-&#62IoStatus.Information = 0; // Нет переноса IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_SUCCESS; } if( pDevExt-&#62xferRest&#62 0 ) { // Не начинаем обрабатывать новый запрос, если остались // непереданные данные (в буфере deviceOutBuffer драйвера) #if DBG==1 DbgPrint("LPTPORT: DispatchWrite: not all data transferred\n"); #endif pIrp-&#62IoStatus.Status = STATUS_DEVICE_BUSY; pIrp-&#62IoStatus.Information = 0; // Нет переноса IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_DEVICE_BUSY; } if( xferSize &#62 MAX_BUFFER_SIZE ) { // Слишком большой запрос. Завершаем обработку IRP пакета: #if DBG==1 DbgPrint( "PLPTPORT: DispatchWrite: xferSize &#62 MAX_BUFFER_SIZE\n" ); #endif pIrp-&#62IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; pIrp-&#62IoStatus.Information = 0; // Нет переноса IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_INSUFFICIENT_RESOURCES; } // Буфер с данными, поступивший от клиента, переносим в // рабочий буфер: PUCHAR userBuffer = (PUCHAR)pIrp-&#62AssociatedIrp.SystemBuffer; RtlCopyMemory( pDevExt-&#62deviceOutBuffer, userBuffer, xferSize );

pDevExt-&#62xferRest = xferSize; pDevExt-&#62xferCount = 0;

// Запускаем перенос данных в первый раз KeSynchronizeExecution( pDevExt-&#62pIntObj, DoNextTransfer, pDevExt ); // Формально -- передача завершена: pIrp-&#62IoStatus.Information = xferSize; IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_SUCCESS; }

Обработчик запросов от Win32 вызова WriteFile переносит данные во внутренний буфер deviceOutBuffer и инициирует процесс переноса вызовом DoNextTransfer при посредничестве KeSyncronizeExecution. Последний повышает текущий уровень IRQL работы до уровня, ассоциированного с объектом прерывания, указанного в качестве первого параметра pDevExt-&#62pIntObj.


В результате (это будет видно позже в распечатке log- файла из программы DebugView) код функции DoNextTransfer выполняется на уровне IRQL равном IRQL кода функции ReadDataSafely и кода функции Isr, которые равны 8 в данном тесте.

//========================================================================= // Функция: DispatchRead // Назначение: Обрабатывает запрос по поводу Win32 вызова ReadFile // Аргументы: pDevObj - поступает от Диспетчера ввода/вывода // pIrp - поступает от Диспетчера ввода/вывода // Возвращаемое значение: // NTSTATUS - в случае нормального завершения STATUS_SUCCESS // или код ошибки STATUS_Xxx // NTSTATUS DispatchRead ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj-&#62DeviceExtension; PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp ); ULONG xferSize, xferredSize; #if DBG==1 DbgPrint("LPTPORT: in DispatchRead now\n"); #endif

if( pDevExt-&#62xferRest&#62 0 ) { // Не начинаем обрабатывать новый запрос, если остались // непереданные данные #if DBG==1 DbgPrint("LPTPORT: DispatchRead: Exists nonprocessed request\n"); #endif pIrp-&#62IoStatus.Status = STATUS_DEVICE_BUSY; IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_DEVICE_BUSY; }

// Определяем размер запроса: xferSize = pIrpStack-&#62Parameters.Read.Length; if( xferSize &#62 MAX_BUFFER_SIZE ) xferSize = MAX_BUFFER_SIZE;

// Передаем не более данных, чем размер числа переданных байт xferredSize = pDevExt-&#62xferCount; xferSize = (xferSize &#60 xferredSize ? xferSize : xferredSize ); if(xferSize&#62 0) { // Копируем содержимое внутреннего входного буфера // в буфер клиента PVOID userBuffer = pIrp-&#62AssociatedIrp.SystemBuffer; RtlCopyMemory(userBuffer, pDevExt-&#62deviceInBuffer, xferSize); }

// Завершаем обработку IRP пакета: pIrp-&#62IoStatus.Status = STATUS_SUCCESS; pIrp-&#62IoStatus.Information = xferSize; // число переданных байт IoCompleteRequest( pIrp, IO_NO_INCREMENT ); #if DBG==1 DbgPrint("LPTPORT: DispatchRead: %d byted transferred.\n", xferSize); #endif return STATUS_SUCCESS; }



//========================================================================= // Процедура обслуживания прерывания: // BOOLEAN Isr ( IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pServiceContext; KIRQL currentIrql = KeGetCurrentIrql(); #if DBG==1 DbgPrint("LPTPORT: In Isr procedure, ISR_Irql=%d\n", currentIrql); #endif //=============================================================== // Строго говоря, следовало бы проверить, имело ли место // прерывание и наше ли это прерывание: // UCHAR status = ReadStatusRegister( pDevExt ); // if( status & 0x04 ) return FALSE; // прерывания не было // Однако в силу упомянутых накладок с использованием бита SR.2, // не проверяем это вовсе, делая допущение, что если Isr получила // управление, то прерывание vнаше. //=============================================================== // Общей практикой является блокирование поступления прерываний // в этом месте:

// WriteControlRegister( pDevExt, CR_DEFAULT);

// Однако, мы не будем этого делать, чтобы не испортить данные, // находящиеся сейчас в Status Register. Полагаем, что кроме // нашего драйвера такие прерывания никто не генерирует, а драйвер // защищен тем, что Write запросы отвергаются до полного переноса // данных. //================================================================ // Планируем вызов DPC процедуры для обработки прерывания позднее // KeInsertQueueDpc( &(pDevExt-&#62DpcForIsr_Object), (VOID *)NULL, // &#60- Arg1 in DpcForIsr (VOID *)NULL); // &#60- Arg2 in DpcForIsr return TRUE; // нормальное завершение обработки прерывания }

Процедура обработки прерывания Isr планирует вызов DpcForIsr DPC функции через размещение DPC объекта в системной очереди. Этим ее функции и ограничиваются в столь простом драйвере.

//========================================================================= // Код, который посылает в порт данные, вызывающие (при наличии // CheckIt заглушки) сигнал прерывания: // VOID ForceInterrupt( PDEVICE_EXTENSION pDevExt, UCHAR bits ) { // Генерируем сигнал прерывания WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT ); KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс // Удерживая информационные биты, снимаем импульс ACK# WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_NOT_RST | CR_DEFAULT ); KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс // Удерживая информационные биты, снимаем импульс ACK# WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT ); } //========================================================================= // Функция DoNextTransfer безопасно (от вмешательства кода прерывания, ISR // функции) записывает данные в параллельный порт: // BOOLEAN DoNextTransfer ( IN PVOID pContext ) {



PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pContext; UCHAR nextByte = 0x0F & ( pDevExt-&#62deviceOutBuffer[pDevExt-&#62xferCount] ); #if DBG==1 DbgPrint("LPTPORT: DoNextTransfer:\n"); DbgPrint("LPTPORT: Sending 0x%02X to port %X\n", nextByte, pDevExt-&#62portBase); #endif // Отправка полубайта данных. //= 1 =============================================================== // Заглушка CheckIt работает не самым простым образом. // Бит 0 отсылаемого полубайта нужно отправить как бит 0 // в Data Register WriteDataRegister ( pDevExt, nextByte & 0x01); // // Это бит будет считан как бит 3 из Status Register

//= 2 =============================================================== // Биты 1-3 отсылаемого полубайта нужно отправить как // биты 0, 1 и 3 в Control Register UCHAR bits = (nextByte & 0x8) + ((nextByte & 0x6)&#62&#62 1); // Таким образом бит 2 всегда равен 0

bits ^= 0x3; // Инвертируем биты (0 & 1) перед // записью в Control Register // Эти биты будут считаны в Status Register как биты 4,5 и 7 #if DBG==1 DbgPrint("LPTPORT: generating next interrupt...\n"); #endif // Собственно отправляем данные вместе с генерацией // сигнала прерывания: ForceInterrupt( pDevExt, bits ); return TRUE; }

//========================================================================= // Функция ReadDataSafely выполняет чтение данных из устройства // без опасения быть прерванной кодом ISR функции: // BOOLEAN ReadDataSafely ( IN PVOID pContext ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pContext; UCHAR status = ReadStatusRegister( pDevExt ); // Преобазуем полученные через Status register биты в понятную форму UCHAR readByte = ((status & 0x8)&#60&#60 1) | ((status & 0x30)&#60&#60 1) | (status & 0x80); readByte &#62&#62= 4; pDevExt-&#62deviceInBuffer[pDevExt-&#62xferCount++] = readByte;

#if DBG==1 KIRQL currentIrql = KeGetCurrentIrql(); DbgPrint( "LPTPORT: ReadDataSafely, currentIrql=%d ReadStatus = %02X" " ReadByte = %02X\n", currentIrql, status, readByte ); DbgPrint( "LPTPORT: \n"); #endif



pDevExt-&#62xferRest--; // Число непереданных байт уменьшилось return (pDevExt-&#62xferRest&#60 1 ? FALSE : TRUE ); // это значение возвратится // через вызов KeSynchronizeExecution }

//========================================================================= // Функция: DpcForIsr // Назначение: Данная функция начинает работу по "заказу" ISR функции // и выполняет ввод/вывод путем безопасного вызова функций // ReadDataSafely и DoNextTransfer, то есть реализует // низкоуровневый ввод/вывод // Аргументы: Указатель на текущий DPC объект (не используется) // pDeferredContext - контекстный указатель - так передается // указатель на структуру Device Extension // Возвращаемое значение: нет // VOID DpcForIsr( IN PKDPC pDpc, // не используется IN PVOID pDeferredContext, IN PVOID pArg1, // не используется IN PVOID pArg2 // не используется ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDeferredContext; #if DBG==1 KIRQL currentIrql = KeGetCurrentIrql(); DbgPrint("LPTPORT: We are now in DpcForIsr, currentIrql=%d xferCount = %d\n", currentIrql, pDevExt-&#62xferCount ); #endif // Безопасно читаем данные из устройства: BOOLEAN dataNotTransferrered = KeSynchronizeExecution( pDevExt-&#62pIntObj, ReadDataSafely, pDevExt ); if ( dataNotTransferrered ) // остались непереданные данные : { // Если остались данные, то записываем следующую порцию // данных в порт и запускаем прерывание: KeSynchronizeExecution( pDevExt-&#62pIntObj, DoNextTransfer, pDevExt ); } else { #if DBG==1 DbgPrint("LPTPORT: We are now in DpcForIsr, all data transmitted.\n"); #endif } } //=========================================================================



#if DBG==1 KIRQL currentIrql = KeGetCurrentIrql(); DbgPrint("LPTPORT: TransferToUserSafely, currentIrql=%d\n",currentIrql ); DbgPrint(" requested %d bytes, while ready %d bytes.\n", xferReq, pDevExt-&#62xferCount); #endif

if( pDevExt-&#62xferCount&#60 1 || xferReq&#60 1 ) { // Нет никаких полученных данных или нулевой запрос pDevExt-&#62xferSize = 0; return FALSE; } if( xferReq &#62 MAX_BUFFER_SIZE ) xferReq = MAX_BUFFER_SIZE; // Передаем не более данных, чем все, полученные из LPT порта, // оказавшиеся во внутреннем буфере драйвера: if( xferReq &#62 pDevExt-&#62xferCount ) xferReq = pDevExt-&#62xferCount;

// Собственно перенос запрошенных данных в буфер клиента: RtlCopyMemory( pDevExt-&#62pUserBuffer, inBuffer, xferReq );

if( xferReq &#60 pDevExt-&#62xferCount) { // Перемещаем оставшиеся данные к началу буфера: ULONG i=0,j=xferReq; for(; j&#60pDevExt-&#62xferCount; ) inBuffer[i++]=inBuffer[j++]; } pDevExt-&#62xferCount -= xferReq; pDevExt-&#62xferSize = xferReq; #if DBG==1 DbgPrint(" Transferred %d, the rest %d bytes.\n", xferReq, pDevExt-&#62xferCount); #endif return TRUE; } //========================================================================= // Функция: DpcForIsr // Назначение: Данная функция начинает работу по "заказу" ISR функции // и выполняет ввод/вывод путем безопасного вызова функций // ReadDataSafely и DoNextTransfer, то есть реализует // низкоуровневый ввод/вывод // Аргументы: Указатель на текущий DPC объект (не используется) // pDeferredContext - контекстный указатель - так // передается указатель на структуру объекта устройства // Возвращаемое значение: нет // VOID DpcForIsr( IN PKDPC pDpc, // не используется IN PVOID pDeferredContext, // Внимание! Теперь здесь // находится указатель pDevObj ! IN PVOID pArg1, // не используется IN PVOID pArg2 // не используется ) { PDEVICE_OBJECT pDevObj = (PDEVICE_OBJECT)pDeferredContext; PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj-&#62DeviceExtension; PIRP pIrp = pDevObj-&#62CurrentIrp; PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(pIrp); ULONG total = pIrpStack-&#62Parameters.DeviceIoControl.InputBufferLength, currentByteNumToWriteToPort = (ULONG)pIrpStack-&#62Parameters.DeviceIoControl.Type3InputBuffer;



#if DBG==1 KIRQL currentIrql = KeGetCurrentIrql(); DbgPrint("LPTPORT: We are now in DpcForIsr, currentIrql=%d xferCount = %d\n", currentIrql, pDevExt-&#62xferCount ); #endif

if( currentByteNumToWriteToPort &#62 0 ) // &#60- т.е. уже были передачи { // Безопасно читаем следующий байт из устройства: KeSynchronizeExecution( pDevExt-&#62pIntObj, ReadDataSafely, pDevExt ); } if ( currentByteNumToWriteToPort &#60 total ) { // Если остались данные, то записываем следующий байт // данных в порт и запускаем прерывание: PUCHAR userBuffer = (PUCHAR)pIrp-&#62AssociatedIrp.SystemBuffer; pDevExt-&#62byteToBeOutToPort = userBuffer[currentByteNumToWriteToPort]; #if DBG==1 DbgPrint("LPTPORT: currentByteNo = %d, byteToBeOutToPort=%02X(hex)\n", currentByteNumToWriteToPort, pDevExt-&#62byteToBeOutToPort ); #endif // Заранее корректируем номер следующего байта для записи в порт // и сохраняем его там же: pIrpStack-&#62Parameters.DeviceIoControl.Type3InputBuffer = (PVOID)( currentByteNumToWriteToPort+1 ); KeSynchronizeExecution( pDevExt-&#62pIntObj, DoNextTransfer, pDevExt ); } else { #if DBG==1 DbgPrint("LPTPORT: We are now in DpcForIsr, all data transmitted.\n"); #endif if(pDevExt-&#62pEvent!=NULL) { // Устанавливаем объект события в сигнальное состояние, // что является сигналом клиету о полностью завершенном // переносе данных в параллельный порт KeSetEvent(pDevExt-&#62pEvent, IO_NO_INCREMENT , FALSE ); //^^^^^^^^^^^^^^^ Замечание А. } // Завершаем обработку текущего IRP пакета. // Поскольку это была обработка пакета от Win32 вызова // DeviceIoControl, который передавал данные в драйвер, // а назад ничего не ожидал, // то указываем, что число байт, возвращаемых клиенту // (третий аргумент вызова CompleteIrp), равно 0: PIRP pIrp = pDevObj-&#62CurrentIrp; CompleteIrp( pIrp, STATUS_SUCCESS, 0 );

// Сообщаем Диспетчеру ввода/вывода, что готовы обработать // следующий пакет: IoStartNextPacket( pDevObj, FALSE ); } return; } //========================================================================= // Функция: DeviceControlRoutine // Назначение: Обрабатывает запросы от Win32 вызова DeviceIoControl. // Аргументы: pDevObj - поступает от Диспетчера ввода/вывода, // pIrp - поступает от Диспетчера ввода/вывода. // Возвращаемое значение: // NTSTATUS - в случае нормального завершения STATUS_SUCCESS // или код ошибки STATUS_Xxx // NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION pIrpStack=IoGetCurrentIrpStackLocation(pIrp); PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj-&#62DeviceExtension; //------------------------------- #if DBG==1 KIRQL currentIrql = KeGetCurrentIrql(); DbgPrint("LPTPORT: We are now in DeviceControlRoutine, currentIrql=%d\n", currentIrql ); #endif // Диспетчеризация по IOCTL кодам: switch( pIrpStack-&#62Parameters.DeviceIoControl.IoControlCode ) { case IOCTL_SEND_TO_PORT: { // Размер данных, поступивших от клиента драйвера // (см.


таблицу 8.9): ULONG xferSize = pIrpStack-&#62Parameters.DeviceIoControl.InputBufferLength; if( xferSize &#60 1 ) { // Нет данных для отправки в порт: #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,\n", " no bytes to transfer.\n"); #endif // Завершение без переноса данных клиенту: return CompleteIrp( pIrp, STATUS_SUCCESS, 0 ); } // if( xferSize &#62 MAX_BUFFER_SIZE ) ? Но размер входящих данных // уже не актуален, поскольку будет использоваться буфер, // поступивший от клиента (точнее, от Диспетчера ввода/вывода). // // Теперь мы ничего не копируем, а просто инициируем процесс // программируемого вывода, откладывая пакет в очередь // необработанных IRP пакетов (не забываем сообщить адрес // процедуры CancelRoutine, которая должна получить управление, // если клиент решит отозвать свой запрос): #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,\n" " xfer size is %d Irp is pending.\n", xferSize ); #endif IoMarkIrpPending( pIrp ); IoStartPacket( pDevObj, pIrp, 0, CancelRoutine); return STATUS_PENDING; } case IOCTL_SEND_TO_USER: { // Размер данных, ожидаемых пользователем pDevExt-&#62xferSize = pIrpStack-&#62Parameters.DeviceIoControl.OutputBufferLength; if( pDevExt-&#62xferSize&#62 0 ) { // Согласно таблице 8.9, адрес клиентского буфера при // методе METHOD_BUFFERED в IOCTL запросе находится там // же, что и при обработке обычного запроса Read (см. // первый вариант драйвера, функцию DispatchRead): pDevExt-&#62pUserBuffer = (PUCHAR)pIrp-&#62AssociatedIrp.SystemBuffer; // Пытаемся безопасно перенести данные: KeSynchronizeExecution( pDevExt-&#62pIntObj, TransferToUserSafely, pDevExt ); } // Завершаем обработку IRP пакета: #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_USER,\n" " %d bytes transferred to user.\n", pDevExt-&#62xferSize); #endif return CompleteIrp( pIrp, STATUS_SUCCESS, pDevExt-&#62xferSize); } case IOCTL_TAKE_EVENT: { // Размер данных, поступивших от клиента драйвера ULONG xferFromDriverSize = pIrpStack-&#62Parameters.DeviceIoControl.OutputBufferLength; if( xferFromDriverSize &#60 sizeof(HANDLE)) { #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n " "event info can not be transferred due to 0 buffer size.\n"); #endif status = STATUS_INVALID_PARAMETER; // Завершение без переноса данных клиенту: return CompleteIrp( pIrp, status, 0 ); // 0 - Нет переноса } if(pDevExt-&#62pEvent==NULL) // Объект события еще не был создан { // для данного клиента. // Создаем объект события типа // SynchronizationEvent, с автоматическим переходом в // несигнальное состояние: #define EVENT_NAME L"\\BaseNamedObjects\\LPTPORT_EVENT" UNICODE_STRING eventName; RtlInitUnicodeString( &eventName, EVENT_NAME );



HANDLE hEvent; // Объект события - без имени: PKEVENT pEvent = IoCreateSynchronizationEvent( &eventName, &hEvent ); if(pEvent==NULL) { // Объект события не был создан #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n" " error - event wasn't created.\n"); #endif // Завершение без переноса данных клиенту: return CompleteIrp( pIrp, STATUS_UNSUCCESSFUL, 0 ); } #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n" " event named %ws successfully created.\n", eventName.Buffer); #endif pDevExt-&#62pEvent = pEvent; pDevExt-&#62hEvent = hEvent; // Предустанавливаем объект события в несигнальное состояние: KeClearEvent(pDevExt-&#62pEvent); } // Cообщаем об объекте события клиенту - передаем дескриптор RtlCopyMemory( pIrp-&#62AssociatedIrp.SystemBuffer, &pDevExt-&#62hEvent, sizeof(HANDLE) ); #if DBG==1 DbgPrint( "LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n" " event handle = %04X(hex) is sent to user.\n", pDevExt-&#62hEvent); #endif return CompleteIrp( pIrp, STATUS_SUCCESS, sizeof(HANDLE) ); } case IOCTL_CLOSE_EVENT: { if(pDevExt-&#62pEvent!=NULL) // объект события был создан { NTSTATUS sts = ZwClose(pDevExt-&#62hEvent); #if DBG==1 if(sts==STATUS_SUCCESS) DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,\n" " event handle closed with STATUS_SUCCESS.\n"); DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,\n" " event (handle %04Xhex) closing status = %d.\n", pDevExt-&#62hEvent, sts ); #endif // Во всяком случае, эти событием пользоваться не будем: pDevExt-&#62pEvent = NULL; pDevExt-&#62hEvent = NULL; } return CompleteIrp( pIrp, STATUS_SUCCESS, 0 ); } default: { #if DBG==1 DbgPrint("LPTPORT: DeviceControlRoutine: bad IOCTL code."); #endif // Завершение без переноса данных клиенту: status = STATUS_INVALID_DEVICE_REQUEST; CompleteIrp( pIrp, status, 0 ); } } // &#60- конец оператора "switch" return status; } //=================================================================================



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

Драйвер обрабатывает четыре IOCTL кода, которые может задать клиент при своем обращении к драйверу (из приложения пользовательского режима это делается через Win32 вызов DeviceIoControl):

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

IOCTL_SEND_TO_USER обеспечивает передачу данных, накопившихся на текущий момент в рабочем буфере deviceInBuffer (см. описание структуры расширения объекта устройства для второго варианта драйвера в файле Driver.h), то есть полученных из параллельного порта через механизм прерываний.

IOCTL_TAKE_EVENT передает клиенту дескриптор объекта события, если оно было создано или создает его, сохраняя данные в полях pEvent и hEvent структуры расширения объекта устройства (см. описание структуры расширения объекта устройства для второго варианта драйвера в файле Driver.h).

IOCTL_CLOSE_EVENT закрывает дескриптор текущего используемого объекта события.

Если речь идет о взаимодействии приложения пользовательского режима с драйвером, то, как правило, событие создается именно в приложении пользовательского режима. Операционная система по Win32 вызову CreateEvent создает событие как объект режима ядра, возвращая приложению открытый дескриптор. Этот дескриптор передается в драйвер (через IOCTL запрос), а драйвер при помощи вызова ObReferenceObjectByHandle

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


Для этого используется практически не применяемый разработчиками вызов IoCreateSynchronizationEvent

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

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

Данный драйвер (и первый, и второй варианты) в том виде как они приведены, не позволяют одновременно получить доступ к нему из нескольких приложений, поскольку объект устройства создается для эксклюзивного доступа. По этой причине Win32 вызов CreateFile, который должен получить дескриптор доступа к драйверу, завершается с ошибкой 5 (отказано в доступе). Для того чтобы исправить ситуацию необходимо предпоследний параметр вызова IoCreateDevice в драйверной функции CreateDevice задать равным FALSE (вместо TRUE, как указано изначально).
Еще один вопрос-опасение, который может возникнуть у внимательного читателя: как обстоят дела с доступом ко входным буферам с данными и для данных в смысле корректности уровня IRQL, на которых к ним обращается драйвер? Например, копирование в пользовательский буфер полученных данных по IOCTL запросу IOCTL_SEND_TO_USER производится функцией TransferToUserSafely, защищенной KeSynchronizeExecution, то есть работающей на уровне IRQL, равном IRQL процедуры обслуживания прерывания Isr. Почему не происходит сбоя, поскольку достоверно известно, что пользовательские области находятся в странично организованной памяти?

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

в первом варианте драйвера было указано, что объект устройства имеет метод буферизации при запросах на запись/чтение pDevObj-&#62Flags |= DO_BUFFERED_IO;

во втором варианте аналогичное указание Диспетчер ввод/вывода получает в каждом IOCTL коде, когда при его определении явно указывается METHOD_BUFFERED.

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


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


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

В процедуре DriverEntry необходимо выполнить вызов функции KeInitializeCallbackRecord

для выполнения настройки структуры KBUGCHECK_CALLBACK_RECORD. Место для хранения этой закрытой структуры должно быть выделено в нестраничном пуле (причем должно содержаться в не прикосновенности до момента ее де-регистрации, выполняемой при помощи функции KeDeregisterBugCheckCallback в процедуре Unload данного драйвера).

В DriverEntry необходимо выполнить вызов KeRegisterBugCheckCallback

для подключения драйвера к механизму уведомления об ошибочных ситуациях. Аргументами этого вызова будет указатель на структуру типа KBUGCHECK_CALLBACK_RECORD, адрес предоставляемой драйвером callback-функции, адрес и размер предоставляемого драйвером буфера и строка, которая будет использована для идентификации буфера. Место для буфера должно быть выделено в нестраничном пуле (причем также должно содержаться в неприкосновенности до момента вызова функции KeDeregisterBugCheckCallback).

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

При анализе crash dump файла при помощи WinDbg эту информацию можно вывести на экран, если воспользоваться командой !bugdump.

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



Использование INF файлов


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

Рассмотрим два варианта запуска установки &#8212 при помощи явно вызываемого Мастера Установки (метод для не-PnP устройств и драйверов "в-стиле-NT") и автоматическую установку, запускаемую системой автоматически при обнаружении новых PnP устройств.



Используемые инструментальные программы


Запуск драйверов выполнялся при помощи программы Monitor (из пакета Numega Driver Studio) после полной загрузки операционной системы. Этот простой способ запуск драйверов позволяет проследить все диагностические сообщения всех рабочих процедур испытываемых драйверов, начиная от DriverEntry и заканчивая процедурой DriverUnload.

Сборка драйвера выполнялась в отладочной среде Win 2K Checked Build Environment, что уже стало традицией для примеров данной книги. Драйвер собирался как Legacy Driver при помощи определений ntddk.h и простейшего файла Sources:

TARGETNAME=LPTPort
TARGETTYPE=DRIVER TARGETPATH=.
INCLUDES= $(BASEDIR)\inc;. SOURCES=driver.cpp

Компиляция драйверов как Legacy Driver обеспечила отсутствие проблем при старте и остановке драйверов программой Monitor. Программный код драйверов размещен в двух файлах Driver.cpp (собственно исполняемые процедуры) и Driver.h (заголовочный файл). Для двух вариантов драйверов, работающих с прерываниями, которые описываются в данной главе, исходные тексты приводятся ниже полностью по причине необходимости большого количества дополнительных комментариев.

Поскольку драйверы компилировались в отладочной среде DDK, это позволило выводить отладочную диагностику при помощи условно компилируемых фрагментов вида:

#if DBG==1 DbgPrint( "LPTPORT: Interrupt %d converted to kIrql = %d, " "kAffinity = %d, kVector = %X(hex)\n", pDevExt-&#62Irq, kIrql, kAffinity, kVector); #endif

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

При работе с объектом события во втором варианте драйвера привлекалась программа WinObj, которая подтвердила создание именованного объекта события "BaseNamedObjects\LPTPORT_EVENT" в соответствующей ситуации.

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

Для интерпретации ошибок по их коду использовалась программа ErrLook, описанная ранее.