Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
MS Windows. Элементы архитектуры и системное программирование..pdf
Скачиваний:
273
Добавлен:
01.05.2014
Размер:
1.98 Mб
Скачать

Control_Dispatch Sys_Critical_Exit, VSAMPLED_Crit_Exit

End_Control_Dispatch VSAMPLED

Begin_Control_Dispatch и End_Control_Dispatch имеют единственный аргумент - имя

VxD. Первое макроопределение задаёт начало «оператора switch» а второе его окончание. Control_Dispatch используется для связи кода сообщения и процедуры его обработки. В приведённом примере процедура обработки команд вызывает процедуру

VSAMPLED_Crit_Init по сообщению с кодом Sys_Critical_Init. Константа Sys_Critical_Init

как и все другие коды сообщений объявлена в заголовочном файле DDK vmm.inc(h). Этот фрагмент кода также передаёт на обработку сообщения Device_Init и Sys_Critical_Exit. Первый аргумент макроопределения Control_Dispatch задаёт код сообщения, а второй адрес процедуры обработчика сообщения. Без использования макроопределений код выглядел бы следующим образом:

cmp

Eax, Sys_Critical_Init

jne

case_1

call

VSAMPLED_Crit_Init

jmp

case_end

case_1:

 

cmp Eax, Device_Init

jne

case_2

call

VSAMPLED_Device_Init

jmp

case_end

case_2:

 

cmp Eax, Sys_Critical_Exit

jne

case_end

call

Sys_Critical_Exit

case_end:

 

ret

 

Системные сообщения.

Как уже было отмечено, операционная система использует процедуру обработки команд для передачи устройству различных системных сообщений. Этот метод напоминает способ организации доступа к динамическим библиотекам. В данном случае процедура обработки команд играет роль функции DllMain. Но в отличие от DLL, которая получает только четыре системных сообщения через функцию DLLMain виртуальное устройство может получать и обрабатывать гораздо больше системных сообщений. Некоторые наиболее часто используемые системные сообщения приводятся в следующей таблице:

Create_VM

Сообщение посылается при создании новой виртуальной

 

машины

Destroy_VM

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

Sys_Critical_Init

Первое сообщение, которое получает VxD при загрузке

 

системы. Прерывания запрещены, т.о. VxD должно обработать

 

это сообщение как можно быстрее.

Device_Init

Второе сообщение, которое получает VxD при загрузке

 

системы. Прерывания разрешены. VxD может установить

 

обработчики прерываний и выполнить другие операции по

 

инициализации.

Init_Complete

Третье (последнее) сообщение которое VxD получает при

 

загрузке системы. Сообщение указывает, что инициализация

 

системы завершена.

System_Exit

Первое сообщение, которое VxD получает при завершении

 

работы системы. Прерывания разрешены.

Sys_Critical_Exit

Последнее сообщение, которое посылается виртуальному

 

устройству при завершении работы системы. Прерывания

 

запрещены.

Reboot_Processor

Сообщение о том, что пользователь выполнил команду

 

перезапуск компьютера.

Системные сообщения (примеры)

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

108

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

Интерфейсы виртуальных устройств.

Каждое виртуальное устройство может взаимодействовать с другими виртуальными устройствами, приложениями Win32, Win16 и MS-DOS. Макроопределение Declare_Virtual_Device позволяет задать отдельные точки входа для приложений, однако интерфейс между виртуальными устройствами организован по другой схеме с использованием так называемых сервисных функций (services). На рисунке показаны различные типы интерфейсов VxD.

Приложения

 

Приложения

 

Приложения

Win32

 

Win16

 

MS-DOS

 

 

 

 

 

Обработчик

Процедура PM API Обработчик V86 API Device control

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Другие VxD

 

 

 

 

 

 

 

Виртуальное

 

 

Сервисы

 

 

 

 

 

 

 

Устройство

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рисунок 4. Интерфейсы VxD.

Интерфейс с приложениями Win32.

Приложение Win32 может обращаться к виртуальному устройству. Обратная операция, когда инициатором обмена является VxD более сложна и будет рассмотрена позднее. Приложения Win32 могут вызывать API функции устройства через процедуру обработки команд VxD. Для организации обмена данными с VxD приложение Win32 использует несколько функций API.

Интерфейс приложений Win32. Пользовательский уровень.

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

HANDLE hVxD;

hVxD = CreateFile(«\\\\.\\MYVXD.VXD», 0, 0, 0, 0,

FILE_FLAG_DELETE_ON_CLOSE, 0);

где MYVXD.VXD это имя файла виртуального устройства. Специальный формат строки с именем устройства включает комбинацию символов \\.\. Это означает что функция CreateFile используется для получения ссылки на VxD а не для открытия файла MYVXD.VXD. Число символов «\» в строке удваивается из за особенностей синтаксиса языка Си. В Cи символ «\» в строке используется для вставки специальных символов, поэтому для того чтобы вставить в строку один такой символ его необходимо прописать дважды. Вариант открытия VxD на языке Паскаль (Delphi) приводится ниже:

hVxD := CreateFile(«\\.\MYVXD.VXD», 0, 0, Nil, 0

109

FILE_FLAG_DELETE_ON_CLOSE, 0);

Если ссылка которую возвращает функция CreateFile не равна нулю операция прошла успешно. Эта ссылка используется в дальнейшем для обращения к виртуальному устройству. Для передачи команд виртуальному устройству используется функция API DeviceIoControl.

BOOL DeviceIoControl(

//ссылка на

устройство

HANDLE hDevice,

DWORD dwIoControlCode, //код команды

LPVOID lpInBuffer,

//указатель

на буфер входных данных команды

DWORD nInBufferSize,

//размер буфера входных данных

LPVOID lpOutBuffer,

//указатель

на выходной буфер данных команды

DWORD nOutBufferSize,

//размер выходного буфера

LPDWORD lpBytesReturned, //указатель на число байтов заисанных в //выходной буфер драйвером

LPOVERLAPPED lpOverlapped //указатель на структуру для организации //асинхронного обмена

);

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

Буферы входных и выходных данных размещаются приложением. Наличие и содержимое этих буферов зависит от кода команды. Если например приложение только передаёт данные виртуальному устройству параметр lpOutBuffer должен быть равен Null и nOutBufferSize равен нулю. Функция возвращает количество байтов переданных приложению в выходном буфере (в случае если он используется) по указателю LpBytesReturned. Это значение может отличаться от заказанного в параметре nOutBufferSize. LpOverlapped указывает на структуру типа OVERLAPPED. Эта структура используется только в асинхронных операциях ввода-вывода. Если DeviceIoControl вызывается синхронно, этот параметр должен быть равен NULL. Различие между синхронными и асинхронными вызовами заключается в том, что в первом случае функция возвращает управление только после окончания операции, а в случае асинхронного обмена функция возвращает управление немедленно, при этом операция продолжает исполняться в фоновом режиме. Приложение может проверить результат операции с помощью функций ожидания. Функция DeviceIoControl возвращает TRUE, если операция успешно завершена. Пример синхронного вызова функции

DeviceIoControl:

#define IOCTL_ID_1 100

BOOL Res;

DWORD dwInParam; DWORD cbReturned;

char cBufOut[20];

Res = DeviceIoControl(hVxD, IOCTL_ID_1, &dwInParam, sizeof(dwInParam), cBufOut, 20, &cbReturned, NULL);

В этом примере виртуальному устройству передаётся команда с кодом IOCTL_ID_1 = 100 и одновременно 32-х битовый параметр команды, значение которого записано в переменную dwInParam. VxD выполняет команду и возвращает результат в массиве cBufOut вместе с количеством байт записанных в массив в переменной cbReturned.

Ссылка на VxD должна быть закрыта по окончании работы с виртуальным устройством. Для этого используется стандартная функция Windows API CloseHandle.

CloseHandle(hVxD);

110

Интерфейс приложений Win32. Системный уровень.

Операционная система транслирует вызов API функции DeviceIoControlCall в вызов функции обработки команд виртуального устройства с кодом сообщения W32_DeviceIoControl. Это сообщение должно быть обработано вместе с системными сообщениями по стандартной схеме:

Control_Dispatch W32_DeviceIoControl, MYVXD_DeviceIoControl

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

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

Ebx содержит указатель на блок описания устройства. Edx содержит ссылку на виртуальное устройство.

Esi содержит указатель на структуру DIOCParams. Эта структура используется передачи данных в обе стороны между виртуальным устройством и вызывающим приложением. Описание этой структуры приводится ниже:

DIOCParams STRUC

 

Internal1

DD ?

VMHandle

DD ?

Internal2

DD ?

dwIoControlCode

DD ?

lpvInBuffer

DD ?

cbInBuffer

DD ?

lpvOutBuffer

DD ?

cbOutBuffer

DD ?

lpcbBytesReturned DD ?

lpoOverlapped

DD ?

hDevice

DD ?

tagProcess

DD ?

DIOCParams ENDS

 

К наиболее важным полям этой структуры относятся:

VMHandle - ссылка на виртуальную машину из которой был произведён вызов(ссылка на системную виртуальную машину).

DwIoControlCode код команды который дублируется в регистре Ecx.

LpvInBufer указатель на входной буфер данных переданный из приложения Win32. CbInBuffer размер входного буфера в байтах.

LpvOutBuffer указатель на выходной буфер в котором виртуальное устройство возвращает данные приложению Win32.

CbOutBuffer размер выходного буфера в байтах.

LpcbBytesReturned указатель на двойное слово в которое виртуальное устройство должно записать действительный размер данных которые VxD возвращает приложению в выходном буфере.

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

Обработчик команд DeviceIoControl работает по принципу процедуры обработки сообщений. В его задачу входит диспетчеризация команд.

111

Процедура Device control

switch Номер сообщения

...

Обработчики других

сообщений

case W32_DeviceIoControl

DeviceIoControl handler

...

switch Управляющий Код

...

Обработчик IOCTL_ID_1

case IOCTL_ID_N

Обработчик IOCTL_ID_N

...

Рисунок 5. Диспетчеризация Управляющих кодов.

Обработчик DeviceIoControl должен перед возвратом управления записать код ошибки в регистр Eax. Если операция завершена успешно, регистр Eax должен содержать нулевое значение.

Пример процедуры DeviceIoControl:

cmp Ecx, IOCTL_GET_VERSION jne case_1

call GetVersion jmp case_end_ok case_1:

cmp Ecx, IOCTL_DO_ANYTHING_ELSE xor Eax, Eax

jne case_not_supported call DoAnythingElse

xor Eax, Eax jmp case_end_ok

case_not_supported: mov Eax, 1

ret case_end_ok: Xor Eax, Eax ret

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

IOCTL_GET_VERSION в параметре dwIoControlCode функции DeviceIoControl. Номер версии возвращается в переменной, на которую указывает поле lpvOutBuffer структуры DIOCParams (указатель на структуру находится в регистре Esi). Функция GetVersion может выглядеть так:

IOCTL_GET_VERSION

EQU 200h

BeginProc

GetVersion

 

Mov

Edi ,

[Esi].DIOCParams.lpvOutBuffer

Mov

Dword

Ptr

[Edi], 10h

Mov

Edi, [Esi].DIOCParams.lpcbBytesReturned

Mov

Dword Ptr[Edi], 4

Ret

 

EndProc GetVersion

112

Заметим, что начало, и окончание процедуры задаётся по правилам макроассемблера

MASM директивами BeginProc и EndProc.

Приведём в завершение код приложения вызывающий функцию GetVersion:

#define IOCTL_GET_VERSION 0x200 DWORD dwVersion;

DWORD cbRet;

DeviceIoControl(hVxD, IOCTL_GET_VERSION, NULL, 0, &dwVersion, 4, &cbRet, NULL);

VxD возвращает номер версии (0x10) в переменной dwVersion и число записанных байт

(4) в переменной cbRet.

Интерфейсы приложений Win16 и MS-DOS.

Эти интерфейсы используются для обеспечения связи приложений Win16 и MS-DOS с виртуальными устройствами. Интерфейс Win16 исторически называется интерфейсом защищённого режима, так как в операционных системах Windows 3.x под определение программам защищённого режима подпадали только 16-ти разрядные приложения Windows. Приложения MS-DOS и Win16 должны использовать для доступа к виртуальным устройствам программное прерывание 2Fh. Функция 1684h возвращает адрес точки входа виртуального устройства. Приложения Win16 получают с помощью этой функции адрес обработчика API защищённого режима, а приложения MS-DOS адрес обработчика API V86. Адреса этих точек входа должны быть указаны в макроопределении Declare_Virtual_Device виртуального устройства. В приведённом ниже примере показана процедура получения адреса точки входа виртуального устройства. Эта процедура одинакова для программ Win16 и MS-DOS. Различие заключается в формате адреса точки входа, который возвращает функция прерывания.

mov

ax, 1684h

mov

bx, MYVXD_ID

int

2Fh

mov

ax, es

or

ax, di

mov

Word Ptr API, di

mov

Word Ptr API+2, ax

Jz

error

Функция возвращает адрес точки входа в паре регистров Es:Di в формате СЕЛЕКТОР:СМЕЩЕНИЕ для приложений Win16 или в формате СЕГМЕНТ:СМЕЩЕНИЕ для приложений MS-DOS. Нулевой адрес возвращается в случае ошибки. MYVXD_ID это уникальный идентификатор виртуального устройства заданный в макроопределении Declare_Virtual_Device. В этом примере адрес сохраняется в переменной API типа dword. Точка входа вызывается как обычная дальняя функция:

mov ax, GET_VERSION call Dword Ptr [API] mov Version, bx

Параметры, которые приложение передаёт и получает от виртуального устройства, должны размещаться в регистрах. В примере перед вызовом функции приложение записывает в регистр ax константу GET_VERSION. Эта константа передаётся обработчику API (V86 или защищённого режима). Обработчик по значению регистра ax определяет, какую из функций устройства вызывает приложение. В данном случае по соглашению это функция GetVersion. Функция возвращает номер версии в регистре bx. Однако вызов точки входа и передача значений через регистры между приложением и виртуальным устройством происходит не на прямую. Система должна транслировать вызовы из виртуальных машин V86 и 16-ти разрядных приложений в 32-х битовую оболочку, в которой работает VxD. При этом содержимое регистров приложения на момент вызова обработчика API VxD сохраняется в специальной структуре регистров клиента Client_Reg_Struc. Перед возвратом управления вызывающему приложению система восстанавливает значения регистров из этой структуры. Виртуальное устройство использует структуру регистров клиента для чтения и изменения значений регистров вызывающего приложения. Как правило один из регистров ( например ax) используется для передачи кода команды (номера функции) обработчику API

113

виртуального устройства. Код команды применяется для той же цели что и в интерфейсе с приложениями Win32. Точка входа представляет собой диспетчер, в задачу которого входит вызов обработчиков конкретных команд. Указатель на структуру Client_Reg_Struct передаётся виртуальному устройству в регистре ebp. В ряде случаев обработчик API защищённого режима и обработчик API V86 это одна и та же процедура. Разные обработчики для программ Win16 и MS-DOS приходится писать только в некоторых специфических ситуациях.

Declare_Virtual_Device MYVXD, 1, 0, ControlDispatch, \

MYVXD_ID, MYVXD_INIT_ORDER, \

PM_and_V86_EntryPoint, PM_and_V86_EntryPoint

VXD_CODE_SEG

 

BeginProc PM_and_V86_EntryPoint

 

cmp

[ebp.Client_Ax], GET_VERSION

 

mov

[ebp.Client_Bx], 10h

//сбросить флаг переноса

and

[ebp.Client_Flags], NOT CF_Mask

jmp

case_end

 

ret

 

 

case_1:

 

 

...

 

 

case_not_supported:

//установить флаг переноса

or [ebp.Client_Flags], CF_Mask

ret

 

 

EndProc

PM_and_V86_EntryPoint

 

VXD_CODE_ENDS

Этот фрагмент обрабатывает команду GET_VERSION, возвращая в регистре bx номер версии (10h) и сбрасывая флаг переноса, или устанавливает флаг переноса если команда не поддерживается виртуальным устройством.

Существует другой способ организации интерфейса приложений Win 16 и MS_DOS. В этой технологии так же используется прерывание 2Fh. VxD устанавливает свой собственный обработчик функции прерывания 2F. Приложения MS-DOS и Win16 вызывают функции не через точку входа и через программное прерывание 2Fh. Прерывание 2F может быть перехвачено виртуальным устройством в обработчике системного сообщения Device_Init:

BeginProc DeviceInitHandler

Mov Eax, 2Fh

Mov Esi, OFFSET32 V86_Interface_Handler

VMMCall Hook_V86_Int_Chain

EndProc DeviceInitHandler

Функция Менеджера виртуальных машин Hook_V86_Int_Chain предназначена для установки обработчика программного прерывания. Регистр Eax перед вызовом функции должен содержать номер прерывания а регистр Esi 32-х разрядный виртуальный адрес процедуры обработчика прерывания. Ниже приводится пример обработчика прерывания 2F для использования в приложениях MS-DOS:

GET_MYVXD_VERSION

EQU 0E2h

BeginProc

V86_Interface_Handler

mov

Ax,

[Ebp.Client_ax]

cmp

Ah,

GET_MYVXD_VERSION

jnz case_not_supported mov [ebp.Client_bx], 10h clc

ret case_not_supported:

stc ret

EndProc V86_Interface_Handler

114

Приложение MS-DOS может получить номер версии виртуального устройства MYVXD, как показано в следующем примере:

mov ax, 0E2h Int 2Fh

jc error

mov Version, bx

GET_MYVXD_VERSION номер функции прерывания 2F которая обрабатывается виртуальным устройством MYVXD. При использовании этого способа доступа к виртуальному устройству отсутствует необходимость в указании обработчика API V86 в

макроопределении Declare_Virtual_Device.

Сервисы VxD (Services).

Для организации взаимодействия виртуальных устройств друг с другом используются так называемые сервисы. Сервисы это функции, которые экспортируются виртуальным устройством. Каждое виртуальное устройство может вызывать сервисы других VxD и Менеджера виртуальных машин(VMM). Функция Hook_V86_Int_Chain которая использовалась ранее для установки обработчика программного прерывания это один из сервисов VMM. В тексте виртуального устройства сервис как и обычная процедура объявляется директивами BeginProc и EndProc. Дополнительно к этому указываются директивы Service или AsyncService. Например:

BeginProc MYVXD_GetVersion, Service mov ax, 10h

clc ret

EndProc MYVXD_GetVersion

С помощью директивы AsyncService указывается, что сервис может быть вызван асинхронно, например из обработчика аппаратного прерывания. Асинхронные сервисы должны быть реентерабельны и не должны вызывать синхронные сервисы. Процедуры сервисов должны располагаться в одном из сегментов кода защищённого режима VxD. Все сервисы описываются в таблице сервисов VxD (Service Table). Эта таблица создаётся с помощью макроопределений

Begin_Service_Table, End_Service_Table и xxxx_Service. где xxxx имя VxD указанноё в блоке описания устройства (Declare_Virtual_Device). Например:

Begin_Service_Table MYVXD

MYVXD_Service

GetVersion

MYVXD_Service

Service1

MYVXD_Service

Service2, VXD_ICODE

End_Service_Table MYVXD

 

Для каждого сервиса создаётся отдельная запись в таблице. Первый параметр макроопределения _Service задаёт имя сервиса. По умолчанию, считается что сервис располагается в сегменте VXD_CODE_SEG. Если в действительности процедура сервиса находится в другом сегменте это должно быть указано в аргументах макроопределения (например, VXD_ICODE). Описание таблицы сервисов должно присутствовать в тексте VxD которое экспортирует сервисы и в текстах виртуальных устройств, которые импортируют сервисы этого VxD. Кроме того, текст VxD, которое экспортирует сервисы, должен содержать следующую строку

Create_xxxx_Service_Table EQU 1

перед описанием таблицы сервисов. В данном случае

Create_MYVXD_Service_Table EQU 1

Как правило вместе с VxD поставляется .inc файл с описанием таблицы сервисов которые экспортирует устройство. Этот файл включается в проекты VxD, которые используют его сервисы:

include MYVXD.INC

Для вызова сервиса вместо инструкции call используется макроопределение VxDCall:

VxDCall MYVXD_GetVersion

115

mov

Version, Eax

Для

вызова сервиса VMM следует использовать макроопределение VMMCall,

например:

VMMCall Hook_V86_Int_Chain

Сервис GetVersion виртуального устройства играет особую роль. Этот сервис должен возвращать в регистре eax ненулевое значение номера версии VxD. С помощью этого сервиса можно определить факт загрузки VxD. Если вызывается несуществующий сервис VxD или вызывается сервис VxD, которое не загружено, система отображает «голубой экран» с сообщением об ошибке. Для сервисов с именами xxx_GetVersion, где xxx имя VxD делается исключение. Например, если имя VxD MYVXD его сервис, возвращающий версию должен иметь имя MYVXD_GetVersion. Если VMM не в состоянии обнаружить VxD с именем MYVXD или виртуальное устройство MYVXD не имеет такого сервиса, система не отображает сообщение об ошибке и возвращает в регистре eax ноль. Этот механизм позволяет вызывающим устройствам произвести проверку наличия VxD перед вызовом других его сервисов.

Базовая структура исходного текста VxD.

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

Динамически загружаемые виртуальные устройства.

В Windows 3.x предоставляется возможность загрузки виртуальных устройств только в процессе загрузки операционной системы (Статически загружаемые или статические VxD) . Windows 95 имеет дополнительно механизм загрузки и выгрузки VxDs во время работы ОС (Динамически загружаемые или динамические VxD). Приложения Win32 могут обращаться к статически и динамически загружаемым VxD. Функция CreateFile загружает VxD или создаёт ссылку на VxD если виртуальное устройство загружено при запуске системы или предыдущим вызовом функции CreateFile. Если CreateFile вызывается для открытия динамического устройства повторно, система не создаёт несколько копий VxD в памяти, но вместо этого инкрементирует внутренний счётчик загрузки. Функция CloseHandle уменьшает этот счётчик на единицу. Когда счётчик достигнет нулевого значения, VxD выгружается из памяти. Динамически загружаемое VxD разрабатывается в том случае если нет необходимости в использовании функций виртуального устройства на протяжении всей работы системы. Такое VxD временно захватывает системные ресурсы и освобождает их когда работа с устройством завершается. Механизм доступа к функциям динамически и статически загружаемых устройств из приложений Win32 одинаков. Для этой цели используется функция DeviceIoControl. Исходный текст динамически загружаемого VxD практически не отличается от текста статически загружаемого VxD. Принципиальная разница между этими двумя типами устройств заключается том, что динамически загружаемые VxD могут получать и обрабатывать некоторые дополнительные системные сообщения и не получают некоторые сообщения предназначенные только для статических VxD. Действительно, если VxD загружается после загрузки операционной системы оно не может получить например сообщение Sys_Critical_Init. Вместо этого при динамической загрузке система посылает устройству сообщение Sys_Dynamic_Device_Init а при выгрузке сообщение Sys_Dynamic_Device_Exit. Эти сообщения не посылаются статически загружаемым виртуальным устройствам. Эти два сообщения, как и все другие, должны быть переадресованы обработчикам в процедуре обработки команд. Например:

BeginProc MYVXD_ControlProc

 

Begin_Control_Dispatch MYVXD

 

Control_Dispatch

Sys_Dynamic_Device_Init,

MYVXD_DeviceInit

Control_Dispatch

Sys_Dynamic_Device_Exit,

MYVXD_DeviceExit

Control_Dispatch

W32_DeviceIoControl,

MYVXD_DeviceIoControl

End_Control_Dispatch MYVXD clc

ret

EndProc MYVXD_ControlProc

116

Дополнительно система посылает динамически загружаемому устройству сообщение W32_DeviceIoControl с кодом команды DIOC_OPEN при загрузке VxD или создании новой ссылки на VxD и с кодом DIOC_CLOSE_HANDLE при выгрузке VxD или уничтожении ссылки на него (CloseHandle). Эти коды должны быть обработаны в данном случае в процедуре MYVXD_DeviceIoControl. Обработчик кода DIOC_OPEN возвращает ноль в регистре Eax, сообщая системе, что устройство поддерживает интерфейс с приложениями Win32.

BeginProc MYVXD_DeviceIoControl cmp ecx, DIOC_OPEN

jne case_1 mov eax, 0 ret

case_1:

...

ret

EndProc MYVXD_DeviceIoControl

Обработчик MYVXD_DeviceInit должен сбросить флаг переноса и записать ноль в регистр Eax в случае успешной инициализации устройства. В противном случае устройство не будет загружено.

BeginProc MYVXD_DeviceInit ;any initialization code

xor eax, eax clc

ret

EndProc MYVXD_DeviceInit

Кроме этого в тексте динамического виртуального устройства должна быть объявлена константа xxxx_Dynamic (где xxxx имя VxD):

MYVXD_DYNAMIC

EQU 1

В .DEF файле проекта VxD следует указать, что устройство динамически загружаемое:

VXD MYVXD DYNAMIC

Содержимое .DEF файла будет рассмотрено более подробно позднее.

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

Разработка VxD на языке Си.

Виртуальные устройства могут быть частично написаны на языках ассемблер и Си. Следует отметить, что для разработки драйверов для NT использование ассемблера вовсе необязательно. Однако проект VxD должен содержать, по крайней мере, небольшой фрагмент ассемблерного текста. Этот фрагмент может быть откомпилирован в объектный модуль один раз, и в дальнейшем, если вы пишете на языке Си возвращаться к нему нет необходимости. Как правило, этот ассемблерный текст содержит инициализацию блока описания устройства, и процедуру обработки команд:

.386p

.xlist

include vmm.inc

.list

MYVXD_DYNAMIC EQU 1 MYVXD_DEVICE_ID EQU 19ABH

DECLARE_VIRTUAL_DEVICE MYVXD, 1, 0, ASYNCW32_Control, \ MYVXD_DEVICE_ID, UNDEFINED_INIT_ORDER

VxD_LOCKED_CODE_SEG BeginProc MYVXD_Control

Control_Dispatch SYS_DYNAMIC_DEVICE_INIT, MYVXD_Dynamic_Init, sCall

117

Control_Dispatch SYS_DYNAMIC_DEVICE_EXIT, MYVXD_Dynamic_Exit, sCall

Control_Dispatch W32_DEVICEIOCONTROL, MYVXD_DeviceIOControl, \

sCall, <ecx, ebx, edx, esi>

clc

ret

EndProc MYVXD_Control

VxD_LOCKED_CODE_ENDS

END

В этом примере обработчик MYVXD_DeviceIoControl объявляется с 4 параметрами типа dword, , которые размещаются на стеке перед его вызовом. Эта функция вместе с другими указанными в диспетчере написана на Си и размещается в другом текстовом файле. Директивы sCall указывают на то, что функции имеют стандартную конвенцию вызова: первый параметр помещается в стек первым.

Функции обработчики сообщений написанные на Си:

DWORD _stdcall MYVXD_Dynamic_Init(void) { return(VXD_SUCCESS); }

DWORD _stdcall MYVXD_Dynamic_Exit(void) { return(VXD_SUCCESS); }

По соглашению принятому в Си функции возвращают результат типа dword в регистре eax.

DWORD _stdcall MYVXD_DeviceIOControl(DWORD dwService, DWORD dwDDB,

DWORD hDevice, LPDIOC lpDIOCParams)

{

DWORD dwRetVal = 0;

if ( dwService == DIOC_OPEN ) { dwRetVal = 0; }

else if ( dwService == DIOC_CLOSEHANDLE ) { dwRetVal = 0; }

else if ( dwService == IOCTL_GET_VERSION )

{

*(DWORD *)lpDIOCParams->lpvOutBuffer = 0x10; *(DWORD *)lpDIOCParams->lpcbBytesReturned = 4; dwRetVal = 0;

}

else dwRetVal = ERROR_NOT_SUPPORTED;

}

return dwRetVal;

}

Вызов сервисов виртуального устройства на языке Си.

Большинство сервисов VMM и виртуальных устройств для передачи параметров используют регистры. Для языков высокого уровня включая Си это представляет определённую трудность, так как параметры функций передаются через стек.

Интерфейсные файлы Си (.h) Windows DDK содержат объявления функций которые называются рапперы (wrappers). Раппер это функция Си которая вызывает какой либо определённый сервис. Раппер получает входные параметры по правилам Си через стек, переписывает их в регистры и вызывает сервис. Когда сервис возвращает управление, раппер перемещает выходные данные из регистров в стек и заканчивается как обычная функция Си. Программа использует рапперы вместо того, чтобы делать ассемблерные вставки на ассемблере для вызова сервисов. К сожалению рапперы имеются далеко не для всех сервисов. Однако они могут быть легко созданы. Следующий пример содержит текст раппера для вызова сервиса VWIN32_DIOCCompletionRoutine. Этот сервис устанавливает указанный объект синхронизации в состояние signaled. Отметим, что рапперы используются исключительно в целях удобства написания программы.

void VXDINLINE

118

VWIN32_DIOCCompletionRoutine(DWORD KernelEvent)

{

__asm mov Ebx, [KernelEvent]

__asm VxDCall (VWIN32_DIOCCompletionRoutine) return;

}

VxDCall () и VMMCall() это макросы объявленные в интерфейсном файле DDK vmm.h. Они используются соответственно для вызова сервисов VxD и VMM.

Реализация функций вызываемых извне (callback functions) на языке Си.

Функции Callback это особый часто использующийся при проектировании виртуальных устройств класс функций. Эти функции виртуального устройства вызываются системой в случае возникновения определённых событий. Например, к этому классу относится процедура обработки команд VxD. Другой пример это обработчик аппаратного прерывания. Система передаёт параметры при вызове таких функций через регистры. Функции языка Си имеют так называемые пролог и эпилог - фрагменты кода, которые автоматически вставляются компилятором в начало и конец функции. Этот код может изменить значения регистров заданные системой при вызове функции и возвращаемые функцией по завершении её работы. К счастью при использовании Microsoft Visual C 5.0 эта проблема может быть легко решена. Эта версия компилятора имеет директиву __declspec(naked) которая может быть использована при объявлении функции. Если функция объявлена с такой директивой, компилятор не вставляет в тело функции фрагменты пролога и эпилога. В этом случае функция содержит только код прямо заданный в тексте функции. Например, текст:

DWORD Myfunction (ulong a); { ulong b = 0;

a = 1;

...

return 0;

}

без директивы __declspec(naked) будет откомпилирован в следующую последовательность инструкций:

push

ebp

 

mov

ebp,

esp

push

ecx

 

mov

Dword Ptr [ebp-4], 0

mov

Dword Ptr [ebp+8], 1

...

 

 

mov

esp,

ebp

pop

ebp

 

mov

eax,

0

ret

4

 

Локальные переменные и параметры функции по соглашению принятому в Си адресуются через регистр ebp. Содержимое стека показано на следующем рисунке:

 

Stack

 

ebp-8

 

push a

a

ebp-4

eip

call

ebp = esp

ebp

push ebp

ebp+4

b

push b

 

 

 

 

 

 

 

 

 

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

119

переменным и параметрам функции. Эти вставки запрещаются директивой __declspec(naked). Например следующий сервис VMM используется для организации таймаута.

Set_Global_Time_Out(CallBackFunction, Milliseconds, RefData);

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

Когда время таймаута истекает, система вызывает функцию, адрес которой указан в аргументе CallBackFunction сервиса и передаёт ей через регистры:

ecx - длительность таймаута в миллисекундах edx - заданное пользователем значение RefData. и некоторые другие параметры.

Функция CallbackFunction объявляется с директивой __declspec(naked) для того чтобы параметры переданные ей через регистр не были испорчены кодом пролога:

void __declspec(naked) TimeOutCallBack(void) {

__asm ...

...

__asm ret

}

Примеры задач решаемых с помощью виртуальных устройств.

Пример 1. Обработка аппаратных прерываний.

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

include VPICD.inc

Для установки обработчика аппаратного прерывания используется сервис VPICD_Virtualize_IRQ. Пере вызовом сервиса Virtualize_IRQ необходимо разместить и инициализировать поля структуры описателя линии прерывания VPICD_IRQ_Descriptor. Эта структура так же объявлена в файле vpicd.inc. В полях структуры указывается номер аппаратного прерывания, указатель на процедуру обработчик прерывания и некоторые дополнительные параметры. Формат структуры приводится ниже:

VPICD_IRQ_Descriptor STRUC

 

VID_IRQ_Number

dw

?

VID_Options

dw

0

VID_Hw_Int_Proc

dd

?

VID_Virt_Int_Proc

dd

0

VID_EOI_Proc

dd

0

VID_Mask_Change_Proc dd

0

VID_IRET_Proc

dd

0

VID_IRET_Time_Out

dd

500

VID_Hw_Int_Ref

dd

?

VPICD_IRQ_Descriptor ENDS

VID_IRQ_number это номер аппаратного прерывания.

В поле VID_Options указываются свойства обработчика прерывания. Например, флаг VPICD_OPT_CAN_SHARE указывает на то что в системе могут быть несколько обработчиков этого прерывания, которые вызываются последовательно при возникновении прерывания. Если флаг не установлен, виртуальное устройство использует линию прерывания монопольно.

VID_Hw_Int_Proc задаёт адрес процедуры обработчика аппаратного прерывания. Этот параметр как и номер прерывания обязателен. Остальные поля можно не использовать. В этом случае их значения приравнивают нулю. Эти поля позволяют установить обработчики определённых событий связанных с контроллером прерываний. Например, поле VID_Mask_Change_Proc задаёт адрес процедуры,

120

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

Пример инициализации структуры для установки обработчика IntHandler аппаратного прерывания 11:

V_IRQ_Dsc VPICD_IRQ_Descriptor \

 

<11, 0, OFFSET32 IntHandler, 0, 0, 0, 0, 0, 0>

Вызов сервиса Virtualize_IRQ:

mov

edi, OFFSET32 V_IRQ_Dsc

VxDcall

VPICD_Virtualize_IRQ

jc

errorhandler

mov

[IRQHand], eax

В регистр Edi помещается адрес структуры VPICD_IRQ_Descriptor. Сервис устанавливает флаг переноса в случае ошибки и сбрасывает его в случае успешной установки обработчика. Сервис возвращает ссылку на виртуализированное прерывание в регистре eax. Эта ссылка используется в дальнейшем для управления линией прерывания: деинсталляции обработчика, маскирования линии прерывания и т.п. Например, линия прерывания маскируется сервисом VPICD_Physically_Mask и

размаскируется сервисом VPICD_Physically_UnMask.

mov eax, IRQHandle

VxDcall VPICD_Physically_Mask

mov eax, IRQHandle

VxDcall VPICD_Physically_Unmask

Эти сервисы не возвращают каких либо параметров.

Для деинсталляции обработчика используется сервис:

mov eax, IRQHandle

VxDcall VPICD_Force_Default_Behavior

Обработчик аппаратного прерывания на ассемблере, как и любая другая процедура объявляется директивами BeginProc и EndProc и заканчивается инструкцией ret. Дополнительно в директиве BeginProc может быть указан параметр High_Freq (High frequency). Обработчик должен располагаться в фиксированном сегменте кода защищённого режима. Обработчик аппаратного прерывания должен разрешить дальнейшую генерацию прерываний контроллером прерываний (Сбросить КП). Эта стандартная процедура в программах MS-DOS выполняется инструкцией out 20, 20 (для первого контроллера). VPICD имеет сервис VPICD_Phys_EOI который используется для сброса контроллера.

mov eax, IRQHandle VxDcall VPICD_Phys_EOI

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

Пример обработки аппаратного прерывания:

VXD_LOCKED_DATA_SEG

V_IRQ_Dsc VPICD_IRQ_Descriptor \

<11, 0, OFFSET32 IntHandler, 0, 0, 0, 0, 0, 0>

IRQHandler DD 0

VXD_LOCKED_DATA_ENDS

VXD_LOCKED_CODE_SEG

BeginProc SetInterrupt

mov edi, OFFSET32 V_IRQ_Dsc

VxDCall VPICD_Virtualize_IRQ

121

mov [IRQHandler], eax EndProc SetInterrupt

BeginProc ResetInterrupt

mov eax, [IRQHandler]

VxDCall VPICD_Force_Default_Behaviour

EndProc ResetInterrupt

BeginProc IntHandler, High_Freq ;any code

mov eax, IRQHandler VxDCall VPICD_Phys_EOI ret

EndProc IntHandler, High_Freq

VXD_LOCKED_CODE_ENDS

Пример 2. Управление контроллером ПДП.

Сервисы для управления контроллером ПДП объявлены в файле vdmad.inc. VDMAD это стандартное виртуальное устройство предназначенное для виртуализации и управления каналами ПДП. Сервис VDMAD_Virtualize_Channel используется для захвата и инициализации канала ПДП.

mov eax, Channel

mov esi, OFFSET32 CallbackProc VxDcall VDMAD_Virtualize_Channel jc ErrorHandler

mov [hChannel], eax

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

Сервис VDMAD_Unvirtualize_Channel используется для освобождения захваченного канала ПДП.

mov eax, hChannel

VxDcall VDMAD_Unvirtualize_Channel jc error

Контроллер ПДП это аппаратное устройство, которое работает с физическими адресами. Данные для передачи с помощью ПДП должны размещаться в линейном физическом адресном пространстве. Если размер буфера данных больше 4КБ он размещается в нескольких страницах. Эти страницы должны располагаться в физической памяти друг за другом. Физическая память буфера ПДП должна быть непрерывна. VMM предоставляет сервис _Page_Allocate который может используется для резервирования страниц виртуальной памяти. Этот сервис так же может быть использован для размещения непрерывной памяти для буфера ПДП.

VMMCall _PageAllocate, <nPages, pType, VM, AlignMask, minPhys, \

maxPhys, <OFFSET32 PhysAddr>, flags> test eax, eax ;возвращает 0 в случае ошибки

jz error

mov [Address], eax ; виртуальный адрес блока памяти

При размещении буфера для канала системного контроллера ПДП дополнительно необходимо руководствоваться следующими соображениями. В машинах AT буфер ПДП должен размещаться в нижних 16-ти мегабайтах физической памяти и не пересекать адреса, делящиеся без остатка на 64K и 128K. В примере, который приводится ниже, размещается буфер ПДП размером в три страницы (12K). Тип памяти буфера PG_SYS указывает на то, что буфер должен располагаться в системной памяти

122

(в нулевом кольце защиты). Параметр AlignMask задаёт выравнивание начала буфера по границе 16К. Этот параметр равен 011b. С таким выравниванием блок размером 12K не может пересекать адреса, делящиеся без остатка на 64K или 128K. Параметр MinPhys определяет минимальный номер страницы памяти, в которой системе разрешается разместить буфер. Этот параметр равен нулю. MaxPhys задаёт максимальный номер страницы в которой может размещаться буфер. В машинах AT этот параметр должен быть равен FFFh так как это максимальный номер страницы памяти, которая расположена ниже 16МБ:

1). Размер страницы равен 4K,

2). FFF+1 = 4096 = 4K,

3). 4K*4K = 16MB.

При размещении памяти указываются три флага:

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

PageContig - буфер размером 12K должен быть непрерывным в физичесой памяти. PageFixed - буфер фиксирован в памяти (всегда присутствует в физической памяти и не может быть выгружен на диск).

VMMcall _PageAllocate <3, PG_SYS, 0, 11b, 0, FFFh, ebx, \ <PageUseAlign+PageContig+PageFixed>>

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

mov eax, [Address]

VMMCall

_PageFree <eax, 0>

;virtual address and flags

or

eax, eax ; nonzero

if freed, zero if error

jz failed

После размещения буфер должен быть связан с виртуализированным каналом ПДП. Эта операция выполняется сервисом VDMAD_Set_Region_Info.

mov eax, hChannel mov bl, Buffer

mov bh, LockStatus

mov esi, OFFSET32 Region mov ecx, RegionSize

mov edx, OFFSET32 PhysAddress VxDcall VDMAD_Set_Region_Info

hChannel это ссылка на канал ПДП которую возвращает сервис VDMAD_Virtualize_Channel. Buffer - идентификатор канала ПДП, который назначается пользователем и может равняться нулю. LockStatus определяет статус буфера фиксированный (не ноль) или нет (ноль). В данном случае буфер фиксирован в памяти, так как при его размещении был указан флаг PageFixed. Region это виртуальный а PhysAddress физический адрес начала буфера. Эти значения возвращаются при размещении буфера сервисом _PageAllocate. RegionSize размер буфера в байтах. В данном случае этот параметр равен 12K. Сервис не возвращает каких либо значений.

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

mov eax,

hChannel

mov ebx,

VMHandle

mov

dl,

Mode

mov

dh,

Ext_Mode

VxDcall

VDMAD_Set_Phys_State

VM handle это ссылка на виртуальную машину (Ссылка на системную ВМ может быть получена с помощью сервиса VMMCall Get_Sys_VM_Handle ,который возвращает ссылку в регистре Ebx).

123

Mode и Ext_Mode задают режим работы контроллера ПДП 8237. Например для установки режима 16-ти разрядной записи в single mode следует занести в регистр dl

комбинацию констант DMA_single_mode + DMA_type_write ( = 0x44), а в регистр dh константу _16_bit_xfer (=0x40).

Заметим, что эта функция не предназначена для работы с контроллерами ПДП PS/2 и EISA. Для инициализации таких контроллеров имеются другие сервисы виртуального устройства VDMAD.

Последняя операция заключается в размаскировании канала ПДП.

mov edx, hChannel mov ebx, VMHandle

VxDcall VDMAD_Phys_Unmask_Channel

Функция не возвращает значений. Канал ПДП может быть снова замаскирован:

mov eax, [hChannel]

VxDCall VDMAD_Phys_Mask_Channel

Каждая последующая операция ПДП должна сопровождаться указанием буфера ПДП ( VDMAD_Set_Region_Info) и установкой режима работы канала (VDMAD_Set_Phys_State). Обычно аппаратура, которая применяет для передачи данных механизм ПДП, использует аппаратное прерывания для того, чтобы сообщить программному обеспечению о завершении операции ПДП. В таком случае виртуальное устройство должно виртуализировать линию аппаратного прерывания и канал ПДП, разместить в памяти буфер ПДП и запустить операцию ПДП. Операция ПДП может быть инициирована, например пользовательским приложением, которое получает данные от устройства. В этом случае приложение должно разместить буфер для приёма данных. Обработчик аппаратного прерывания который вызывается по завершении операции ПДП копирует данные из системного буфера ПДП (размещённого по правилам изложенным выше /_PageAllocate/) в буфер подготовленный приложением и сообщает приложению о поступлении новых данных. Системный буфер удовлетворяет всем условиям работы с контроллером ПДП, в то время как буфер приложения может быть не непрерывным, пересекать запрещённые адреса, и располагаться в любом месте адресного пространства. Структура такой системы сбора данных приведена на рисунке:

Приложение Win 32.

Пользовательский буфер данных

1.Call GetData

DeviceIoControl(IOCTL_GET_DATA, ...); 6.Сообщение Данные готовы

5.Копирование данных из системного буфера в пользовательский

Виртуальное устройство

Системный непрерывный буфер ПДП

3.Копирование данных в системный буфер

2.Пуск чтения ПДП

 

4.Данные готовы.

 

 

 

 

Прерывание.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Устройство сбора данных

Рисунок 6. Чтение данных с использованием ПДП. Пример проекта.

124

Пример 3. Асинхронные сообщения.

Виртуальное устройство получает асинхронные сообщения от аппаратуры через механизм прерываний. Передача сообщений от виртуальных устройств пользовательским программам включает ряд специальных программных приёмов. Операционные системы Windows 95/98/NT предлагают стандартное решение этой задачи основанное на использовании асинхронных операций ввода-вывода. Это единственный метод, который позволяет асинхронно предавать информацию от драйверов приложениям Win32 в Windows NT. Однако в операционных системах Windows 95/98 имеется альтернативный способ асинхронной передачи сообщений от драйверов пользовательским приложениям. VxD Shell предоставляет сервисы с помощью которых виртуальные устройства могут выступать в качестве инициаторов обмена данными с приложениями.

Сервисы Shell.

Сервисы виртуального устройства Shell объявлены в файле shell.inc, который входит в состав Windows 95/98 DDK. Сервис _SHELL_PostMessage используется для передачи сообщения Windows из VxD оконной функции заданного окна пользовательского приложения. Сообщения пересылается через очередь сообщений приложения. Этот сервис аналогичен функции API PostMessage, которую используют пользовательские приложения. Сервис немедленно возвращает управление, не дожидаясь, пока сообщение будет помещено в очередь, извлечено из очереди, доставлено адресату и обработано. Сервис позволяет установить callback функцию, (pfnCallback) которая вызывается системой после того, как сообщение будет помещено в очередь сообщений приложения. Если параметр pfnCallback

при вызове сервиса равен NULL, функция не вызывается.

VxDcall _SHELL_PostMessage, <hwnd, uMsg, wParam, lParam, \ <OFFSET32 pfnCallback>, dwRefData>

or eax, eax jz not_posted

Сервис возвращает ноль в регистре eax если сообщение не может быть отправлено. Параметр HWnd задаёт ссылку (handle) на окно которому отправляется сообщение. Эта ссылку приложение должно заранее передать виртуальному устройству. UMsg это код сообщения - как правило константа в диапазоне который выделен пользовательским сообщениям (WM_USER + n). wParam и lParam параметры сообщения смысл которых определяется разработчиком. DwRefData это необязательный параметр значение которого будет передано callback функции при вызове. Callback функция должна иметь следующий интерфейс:

cCall [pfnCallback], <dwRc, dwRefData>

dwRc - флаг ошибки, который не равен нулю если сообщение отправлено, и равен нулю в случае ошибки.

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

VxD Shell экспортирует несколько сервисов для работы с 16-ти разрядными динамическими библиотеками. Сервис _SHELL_CallDll например используется для вызова экспортируемых функций 16-ти разрядных DLL. Этот механизм нельзя использовать для доступа к функциям 32-х разрядных динамических библиотек.

Асинхронный ввод/вывод.

Асинхронный (используется так же термин overlapped) ввод/вывод используется для организации передачи асинхронных сообщений от драйверов расположенных в системной области пользовательским приложениям Win32. 32-х разрядные приложения Windows могут обращаться к виртуальным устройствам с помощью API функции DeviceIoContol. Последний параметр в интерфейсе этой функции указывает на структуру типа OVERLAPPED. Напомню, что в случае синхронного вызова функции (когда она возвращает управление только после того, как заданная операция будет выполнена) структура не используется и этот параметр равен NULL. Структура OVERLAPPED должна содержать ссылку на объект синхронизации Event. Структура

125

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

При открытии VxD (создании ссылки на VxD) был указан флаг

FILE_FLAG_OVERLAPPED.

HANDLE hVxD;

hVxD = CreateFile(«\\\\.\\MYVXD.VXD», 0, 0, 0, 0, FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_OVERLAPPED, 0);

Параметр lpOverlapped при вызове функции DeviceIoControl не равен NULL.

OVERLAPPED ovr;

//create event

ovr.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

Res = DeviceIoControl(hVxD, IOCTL_ID_1, &dwInParam, sizeof(dwInParam), cBufOut, 20, &cbReturned, &Ovr);

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

Защёлкнуть в физической памяти структуру OVERLAPPED, входной и выходной буферы. Указатели на эти параметры передаются в структуре DIOCParams. Память должна быть защёлкнута с флагом PAGEMAPGLOBAL. При этом память отображается в глобальное адресное пространство и становится доступной из любого контекста памяти. Переключение потоков (включая переключение контекстов памяти) производится системой независимо. При этом указатели на буферы и структуру OVERLAPPED должны быть достоверны всегда, пока в них есть необходимость, так как драйвер может обратиться к ним в любой момент времени и при любом контексте. Минимальный элемент памяти который может быть зафиксирован это страница(=4096 байт). Нужное количество страниц защёлкивается сервисом VMM _LinPageLock (и освобождается сервисом

_LinPageUnlock).

Запустить заданную операцию. Например ,запустить операцию чтения ПДП.

Вернуть управление с -1 в регистре Eax. В этом случае функция DeviceIoControl вернёт False и вызванная затем функция GetLastError вернёт константу

ERROR_IO_PENDING.

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

Переписать полученные данные в защёлкнутый выходной буфер.

Установить объект event переданный драйверу в состояние signaled. Пользовательская программа помещает ссылку на объект в поле hEvent структуры OVERLAPPED. Система транслируя вызов функции DeviceIoControl перед тем как передать управление VxD помещает ссылку на объект event в поле O_Internal этой структуры. Виртуальное устройство должно брать ссылку на объект из поля O_Internal а не из поля h_Event. Эти ссылки имеют разные форматы. Пользовательские программы не могут работать с ссылками на объекты которые использует системное ПО. VxD получает ссылку в системном формате и именно с таким форматом ссылок работают сервисы управления объектами синхронизации. В данном случае для установки объекта event в состояние signaled следует использовать сервис VWIN32_DIOCCompletionRoutine. Единственным параметром при вызове является значение поля O_Internal структуры OVERLAPPED - системная ссылка на объект event. Этот параметр передаётся через регистр ebx.

3.Освободить защёлкнутую память буферов данных и структуры OVERLAPPED (_LinPageUnlock).

126

Пользовательское приложение может инициировать асинхронную операцию, вызвав API функцию DeviceIoControl. Затем так как операция выполняется асинхронно приложение может произвести какие либо действия и перейти в режим ожидания окончания операции. Для этого используется одна из функций ожидания. Поток приложения, который вызвал функцию ожидания исключается из цикла переключения потоков и не занимает процессорное время до тех пор пока виртуальное устройство не установит объект синхронизации event в состояние signaled. Обычно в асинхронных операциях ввода/вывода используется функция ожидания GetOverlappedResult.

Со стороны приложений интерфейс асинхронного ввода/вывода стандартизирован и одинаков для всех 32-х разрядных ОС Windows. Реализации интерфейса на системном уровне для ОС Windows95/N отличаются.

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

LPOVERLAPPED sioLpo;

PVOID sioOutBuf;

DWORD sioOutBufSize;

DWORD _stdcall MYVXD_DeviceIOControl(DWORD dwService,

DWORD

dwDDB,

DWORD

hDevice,

LPDIOC

lpDIOCParams)

{

 

switch (dwService) {

case IOCTL_GET_DATA:

//защёлкиваем структуру overlapped и сохраняем адрес в sioLpo

sioLpo

= (LPOVERLAPPED)lpDIOCParams->lpoOverlapped;

sioLpo

= (LPOVERLAPPED)PageLock((DWORD)sioLpo,

 

sizeof(OVERLAPPED));

//защёлкиваем выходной буфер

sioOutBufSize = lpDIOCParams->cbOutBuffer;

sioOutBuf

= lpDIOCParams->lpvOutBuffer;

sioOutBuf

= (PVOID)PageLock((DWORD)sioOutBuf, sioOutBufSize);

//запускаем асинхронную операцию

StartDMATransfer();

//возвращаем ERROR_IO_PENDING

return -1;

case ...

 

}}

 

//эта процедура отображает страницы содержащие заданную область //памяти lpMem - lpMem+cbSize в диапазон глобальных адресов, //защелкивает их и возвращает адрес защёлкнутого буфера

DWORD _stdcall PageLock(DWORD lpMem, DWORD cbSize)

{

DWORD LinPageNum, LinOffset, nPages;

LinOffset = lpMem & 0xfff;

//

смещение от начала страницы

LinPageNum = lpMem

>> 12;

//

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

nPages

= ((lpMem +

cbSize) >> 12) - LinPageNum + 1;

return

(_LinPageLock(LinPageNum,

nPages, PAGEMAPGLOBAL) +

LinOffset);

}

//освобождает защёлкнутые страницы содержащие заданную область

//памяти lpMem - lpMem+cbSize

//lpMem это адрес который вернула функция PageLock void _stdcall PageUnlock(DWORD lpMem, DWORD cbSize)

{

DWORD LinPageNum, nPages; LinPageNum = lpMem >> 12;

nPages = ((lpMem + cbSize) >> 12) - LinPageNum + 1; _LinPageUnlock(LinPageNum, nPages, PAGEMAPGLOBAL);

127

}

//аппаратура вырабатывает прерывание по завершении операции ПДП //в обработчике данные копируются в выходной буфер и устанавливается //специальная callback функция которая будет вызвана системой после

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

//потому, что сервис VWIN32_DIOCCompletionRoutine не может быть

//вызван из обработчика прерывания. Подробнее см. документацию DDK.

//обработчик прерывания

void __declspec(naked) IntHandler(void) {

...

//копируем данные из буфера ПДП в выходной буфер memcpy(sioOutBuf, dmaBuf, sioOutBufSize);

...

//устанавливаем callback функцию

(один из

вариантов).

__asm

mov

ebx,

SYS_VM_HANDLE

//ссылка

на системную BM

__asm

mov

esi,

OFFSET EventCallback

 

__VMMcall

Schedule_VM_Event

 

 

...

}

//callback функция

void __declspec(naked) EventCallback(void)

{

...

__asm mov edi, sioLpo

__asm mov ebx, [edi].OVERLAPPED.O_Internal __asm VxDCall (VWIN32_DIOCCompletionRoutine) PageUnlock((DWORD)sioOutBuf, sioOutBufSize); PageUnlock((DWORD)sioLpo, sizeof(OVERLAPPED));

...

}

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

 

Приложение Win32

 

 

 

 

 

Поток ожидающий ввод.

 

 

 

 

 

 

 

WaitForSingleObject();

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Создаёт объект event

 

 

 

 

 

 

 

 

 

 

1. Передача ссылки на

 

 

 

 

 

 

 

Объект

 

 

объект event

 

 

 

 

 

 

 

 

 

 

Event

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2. Установка объекта в состояние signaled

 

 

 

 

 

 

 

 

 

 

 

Виртуальное устройство

Рисунок 7. Перехват асинхронных событий.

128

Основы проектирования драйверов WDM для Windows 98/2K.

Автор: Сидякин И.М.

Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (02.2000)

Email:sidiakin@iu3.bmstu.ru

В операционных системах MS Windows 98 и Windows 2K, используется новая модель драйверов нулевого кольца. Эта модель получила название Модель Драйверов

Windows (Windows Driver Model), сокращённо WDM. Драйверы WDM являются развитием структуры драйверов kernel, которая используется в операционных системах Windows NT. Модель WDM включает несколько новых концепций, которые позволяют использовать более гибкий подход при проектировании ПО для управления аппаратными ресурсами вычислительной системы. Драйверы WDM более универсальны. Они обладают свойством переносимости. Один и тот же драйвер может работать на различных аппаратных платформах. Базовые структуры драйверов kernel и WDM очень похожи. Это позволяет во многих случаях без особых проблем переделать драйвер kernel в драйвер WDM. Как правило, переделка заключается в замене функций системного программного интерфейса (SPI) которые использует драйвер. Функции, специфические для какой либо платформы удаляются или заменяются на универсальные. Можно сделать вывод, что спецификация WDM является подмножеством спецификации драйверов kernel. В тоже время модель WDM имеет ряд существенных дополнений, например в области организации интерфейса драйверов и поддержки технологии автоматической идентификации устройств Plug and Play. Если эти дополнения не используются в явном виде, исходный текст драйвера WDM может ничем не отличаться от текста драйвера kernel. В Модели Драйверов Windows широко используется сочетание мини-драйверов (minidriver) и так называемых драйверов классов (class driver). Для некоторых классов устройств, например, устройств подключаемых к шине USB или шине IEEE 1394 разработаны драйверы классов. Драйвер класса выполняет операции общие для всех устройств данного класса. Каждое отдельное устройство, входящее в класс обслуживается мини-драйвером, который разрабатывается производителем устройства. Мини-драйвер реализует функции специфические для устройства. Процедура разработки мини-драйвера упрощается за счёт того, что ряд функций по управлению устройством переносится в драйвер класса. Мини-драйверы устройств относящихся к различным классам пишутся согласно спецификациям, разработанным для этих классов. В этой главе не рассматриваются особенности проектирования мини-драйверов для существующих классов устройств. Здесь будет изложена общая структура драйвера WDM устройства, которое не относится к какому либо конкретному классу.

129

Средства проектирования драйверов WDM.

Драйверы WDM пишутся, как правило, на языке Си. Автор допускает возможность разработки драйверов на других языках, однако сам не проводил подобных экспериментов. Поэтому далее будут обсуждаться приёмы программирования драйверов исключительно на языке Си. По той же причине для создания исполняемых модулей драйверов примеров следует использовать компилятор Си и компоновщик, входящие в состав Microsoft Visual C++ 5.0 (6.0). Драйверы можно компилировать из командной строки или из оболочки MSVC. Далее будут рассмотрены оба способа. Дополнительно следует установить пакет проектирования драйверов, в зависимости от операционной системы, Windows 98 DKK или Windows 2000 DDK. Эти пакеты включают набор заголовочных файлов и библиотек, необходимых для создания драйверов WDM. DDK содержат так же документацию и примеры. В настоящее время (1.2000) пакеты DDK как для Windows 98, так и для Windows 2000 можно бесплатно переписать с web узла компании Microsoft. Однако эти копии запрещено использовать в коммерческих целях. DDK следует устанавливать после установки MSVC.

Программа инсталлятор DDK добавляет в меню «Пуск->Программы» подменю

«Windows 98 DDK» (или «Windows 2000 DDK») . Это подменю содержит три пункта: «Checked Build Environment», «Free Build Environment» и «DDK Documentation».

Последний пункт предназначен для вызова справки по DDK, а первые два используются для организации компиляции драйверов из командной строки в сессии MS-DOS. В сессии «Checked» драйверы компилируются с отладочной информацией. В сессии «Free» драйверы компилируются без отладочной информации. Через пункты «Checked» и «Free» запускается командный файл setenv.bat, который расположен в каталоге <DDK>\bin. Этот файл устанавливает переменные среды окружения необходимые для компиляции драйверов. В частности пути к заголовочным файлам, и библиотекам DDK. Кроме этого настраиваются переменные среды окружения Visuаl С++. В параметрах командного файла указан корневой каталог DDK и способ компиляции драйвера, соответственно checked или free. Полная строка команды пункта

«Checked Build Environment» Windows 98 DDK выглядит так:

C:\WIN98\COMMAND.COM /e:4096 /k c:\98ddk\bin\setenv c:\98ddk checked

Копия процессора COMMAND.COM с ключом /e:4096 вызывается для того, чтобы увеличить до 4К установленный по умолчанию размер блока переменных среды окружения.

Драйвер WDM создаётся с помощью утилиты Build. Buld вызывается из командной строки. Эта утилита последовательно запускает компилятор для всех исходных файлов проекта, и затем компоновщик. Ошибки компиляции помещаются в текстовый файл Build.err. Предупреждения помещаются в текстовый файл Build.wrn. Если процесс создания драйвера завершается без ошибок, в сессии «Checked» по умолчанию исполняемые модули драйверов (с расширением sys) помещаются в каталог

<DDK>\lib\i386\checked, а в сессии «Free» в каталог <DDK>\lib\i386\free.

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

Пример файла Dirs:

DIRS =

mainsrc

\

 

headers

\

 

addons

 

Символ «\» используется для переноса строки. Утилита Build использует файл Dirs для поиска исходных компонентов проекта. Кроме этого в каталоге проекта, который содержит файлы исходных текстов располагается командный файлы для утилиты Build. Это текстовый файл (так же без расширения) с именем Sources. Sources содержит список файлов исходных текстов проекта и некоторую другую информацию.

130

Пример файла Sources:

TARGETNAME=TESTDRV

TARGETTYPE=DRIVER

TARGETPATH=$(BASEDIR)\lib

INCLUDES=$(BASEDIR)\inc

C_DEFINES=-DDRIVER

SOURCES=

\

drvshell.c

\

vector.c

\

testdrv.c

\

testdrv.rc

\

debugwdm.c

 

Директива TARGETNAME задаёт имя драйвера. TARGETTYPE определяет тип создаваемого файла. Для драйверов указывается тип DRIVER. Директива INCLUDES задаёт пути к заголовочным файлам. Директива C_DEFINES задаёт дополнительные директивы командной строки компилятора Си. В разделе SOURCES указываются все исходные файлы проекта. Файл может содержать ряд дополнительных директив, полное описание которых приводится в файле Sources.tpl DDK.

Использование оболочки Microsoft Visual C++ для создания драйверов.

Для создания проекта MSVC драйвера WDM можно воспользоваться протокольным файлом, который утилита Build создаёт в процессе компиляции драйвера. Этот файл с именем Build.log содержит настройки компилятора и компоновщика, которые использовались при создании исполняемого модуля драйвера. Например, строка запуска компилятора Си в протокольном файле может быть такой:

cl -nologo -Ii386\ -I. -Ic:\98ddk\inc -Ic:\98ddk\inc\win98 -

 

Ic:\98ddk\inc\win98 -Ic:\98ddk\inc\win98 -D_X86_=1 -Di386=1

-

DSTD_CALL -DCONDITION_HANDLING=1 -DNT_UP=1 -DNT_INST=0 -DWIN32=100 -

D_NT1X_=100 -DWINNT=1 -D_WIN32_WINNT=0x0400

-

 

DWIN32_LEAN_AND_MEAN=1 -DDBG=1 -DDEVL=1 -DFPO=0

-DNDEBUG -D_DLL=1

-DDRIVER

/c /Zel

/Zp8 /Gy -cbstring

/W3 /Gz

/QIfdiv- /QIf

/Gi-

/Gm- /GX- /GR- /GF

-Z7 /Od /Oi

/Oy-

-

 

 

FIc:\98ddk\inc\win98\warning.h

.\wldh000.c

 

 

wldh000.c

 

 

 

 

 

 

Для того, чтобы создать проект MSVC, сначала следует запустить компиляцию драйвера из командной строки с помощью утилиты Build. После этого надо загрузить оболочку MSVC и создать новый проект «Win32 application» или «Win32 dynamic link library». Какой тип проекта выбрать не имеет значения, так как настройки проекта будут изменены. Далее следует вызвать диалоговое окно настроек проекта. Настройки компилятора в закладке C/C++ заменяются на настройки из строки запуска компилятора cl.ехе в Log файле. Если проект включает несколько С файлов в протокольном файле содержатся строки запуска компилятора для каждого файла. Все эти настройки одинаковы, поэтому следует взять их из первой строки. Настройки компилятора ресурсов в закладке Resources заменяются на настройки из строки запуска компилятора ресурсов rc.ехе в Log файле. Настройки компоновщика в закладке Link так же замещаются настройками компоновщика link.ехе из Log файла. Далее необходимо аккуратно удалить из настроек компиляторов и компоновщика все записи содержащие имена исходных файлов. Например, в приведённой выше строке компилятора сl.ехе следует удалить записи ‘.\wldh000.c’ и wldh000. После этого окно настроек следует закрыть и в закладке FileView с помощью всплывающего меню добавить к проекту все исходные файлы. Не забудьте добавить файл ресурсов. Voila! Теперь проект можно компилировать (F7). Настройки следует сохранить на будущее для использования в других проектах. Можно пойти дальше и разработать Wizard для генерации шаблона проекта драйвера.

Регистрация драйверов WDM.

Для того чтобы операционная система обнаружила драйвер в реестр необходимо внести учётную запись этого драйвера. Учётные записи драйверов kernel и WDM

131

похожи. Поэтому с некоторыми оговорками нижеизложенная процедура установки применима так же к драйверам Windows NT.

Драйвер WDM следует поместить в каталог <WINDOWS>\SYSTEM32\DRIVERS. Затем в реестре в ключ HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services необходимо добавить подключ с именем драйвера. Имя драйвера задаёт разработчик. Например:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MYDRV

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

Строковое значение ImagePath задаёт путь к файлу драйвера. Например:

ImagePath = ‘\SystemRoot\system32\drivers\mydrv.sys’

Двойное слово Type задаёт тип драйвера. Для драйверов WDM это значение равно 1. Двойное слово Start определяет вариант запуска драйвера. Возможны следующие значения:

0Драйвер запускается загрузчиком ОС на начальном этапе загрузки системы. Этот тип запуска применяется редко, в тех случаях когда есть необходимость в том, чтобы драйвер стартовал в самом начале загрузки операционной системы.

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

2Драйвер запускается вместе с ОС после загрузки графического интерфейса, но перед операцией log in.

3Драйвер не запускается во время загрузки ОС. Он может быть загружен впоследствии. Например, из командной строки командой net start <имя драйвера>. В Windows 98 драйвер WDM должны запускаться в процессе загрузки операционной системы.

Двойное слово ErrorControl определяет варианты действия операционной системы в случае ошибки инициализации драйвера. Допускаются следующие значения:

0Ошибка игнорируется, сообщение об ошибке помещается в протокольный файл загрузки (log файл).

1Система выдаёт пользователю текстовое сообщение об ошибке.

2Если идёт загрузка последней успешной конфигурации загрузка продолжается, иначе загрузка прерывается и производится загрузка последней успешной конфигурации.

3Если идёт загрузка последней успешной конфигурации загрузка прекращается с ошибкой, иначе загрузка прерывается и производится загрузка последней успешной конфигурации.

Строковое значение Group задаёт имя группы, к которой относится драйвер. Например, Base, SCSI, Port. Имя группы можно использовать, в случае если необходимо загружать группы драйверов в заданной последовательности. Для определения последовательности загрузки драйверов используется несколько дополнительных значений реестра.

Строковое значение DependOnGroup задаёт зависимость драйвера, от какой либо группы. Например, в случае если драйвер может быть загружен только после загрузки группы драйверов с именем ‘MYGROUP1’, подключ драйвера должен содержать значение DependOnGroup = ‘MYGROUP1’.

Строковое значение DependOnService определяет зависимость драйвера от другого драйвера. Например: DependOnService = ‘MYDRV2’

Полное описание значений реестра можно найти в документации Windows NT DDK во второй главе Programmer’s Guide.

132

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

REGEDIT4 [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MYDRV] "ImagePath"="\\SystemRoot\\system32\\drivers\\mydrv.sys" "Type"=dword:00000001

"Start"=dword:00000001

"Group"="Base"

Организация обмена данными. Пакеты запросов ввода-вывода.

Прежде чем перейти непосредственно к вопросу проектирования драйверов WDM, рассмотрим несколько важных концепций общих для драйверов kernel и WDM. Операционная система применяет единый механизм передачи команд и данных между драйверами. Драйверы kernel и WDM в отличие от виртуальных устройств Windows 95 пользуются унифицированным интерфейсом для организации связи друг с другом и с 32-х разрядными приложениями. Процессом передачи информации управляет Менеджер ввода-вывода (I/O Мanager). Менеджер ввода-вывода формирует так называемые Пакеты запросов ввода-вывода (I/O Request Packets), сокращенно IRP. Пакет запроса содержит служебную информацию, которая используется в процедуре доставки пакета получателю и возврата результата обработки Пакета вызывающей программе. Кроме этого в IRP содержатся собственно данные для получателя. Драйвер содержит процедуры, предназначенные для обработки Пакетов запросов различного типа. Операционная система для передачи пакета драйверу вызывает один из обработчиков и передаёт ему в параметрах указатель на IRP. Адреса обработчиков IRP записываются в специальную служебную таблицу доступную операционной системе. Адреса обработчиков указываются программистом при проектировании драйвера.

Пакет запроса может быть передан по цепочке драйверов. Т.е. один IRP может последовательно обрабатываться несколькими драйверами. Такой механизм используется для многоступенчатой развязки низкоуровневого и высокоуровневого программного обеспечения. Эта технология используется, например, для организации файлового ввода-вывода(Рисунок 1).

Менеджер

ввода/вывода

Менеджер кэш памяти

Файловая система

Сетевые драйверы

Драйверы аппаратуры

Рисунок 1. Многоуровневая структура драйверов.

Для того, чтобы прочитать блок данных из файла приложение вызывает функцию API ReadFile. Менеджер ввода-вывода формирует Пакет запроса и передаёт его расположенному первым в цепочке Менеджеру кэш памяти. Если указанный фрагмент файла находится в кэш памяти, он передаётся приложению. Иначе IRP передаётся на более низкий уровень драйверу файловой системы (или сетевому драйверу, если файл расположен на удалённой машине). Драйвер файловой системы по таблице размещения файлов может определить какие именно сектора диска содержат нужную информацию. Эти сведения он помещает в Пакет запроса и передаёт его драйверу физического носителя. Например, драйверу жёсткого диска. Драйвер диска выполняет операции низкоуровневого чтения секторов и помещает результат чтения в IRP. Далее заполненный данными IRP возвращается вызывающей программе. Такая структура

133

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

Пакет запроса ввода-вывода включает элементы необходимые для поддержки многоуровневой обработки IRP. Вместе с IRP Менеджер ввода-вывода создаёт массив структур типа IO_STACK_LOCATION. Эти структуры используются для передачи параметров драйверам, включённым в цепочку обработки IRP. Каждый драйвер в цепочке имеет свою собственную структуру IO_STACK_LOCATION и следовательно свой собственный набор параметров. Параметры содержаться в поле Parameters структуры IO_STACK_LOCATION. Это поле имеет тип объединения (UNION) нескольких структур. Кроме параметров IO_STACK_LOCATION содержит информацию о типе запроса. Формат параметров, т.е. какая из структур входящих в объединение Parameters должна использоваться, определяется типом запроса. Например, запрос чтения данных имеет тип IRP_MJ_READ. Параметры этого типа запроса описываются структурой Read:

struct { ULONG Length; ULONG Key; LARGE_INTEGER ByteOffset; } Read;

Длина буфера данных в запросе чтения может быть получена следующим образом:

ReadBufLength = CurStackLocation->Parameters.Read.Length;

,где CurStackLocation это указатель на структуру IO_STACK_LOCATION драйвера.

Перед тем, как передать IRP обработчику драйвера Менеджер ввода-вывода записывает в него ссылку на структуру IO_STACK_LOCATION этого драйвера. Для получения адреса IO_STACK_LOCATION следует использовать функцию

IoGetCurrentIrpStackLocation.

PIO_STACK_LOCATION CurStackLocation =

IoGetCurrentIrpStackLocation(Irp);

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

Точка входа драйвера WDM.

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

link /entry:DriverEntry@8

Функция вызывается по соглашению __stdcall. Первый параметр помещается в стек первым. Функция имеет следующий интерфейс:

NTSTATUS

DriverEntry(IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath)

Параметр DriverObject указывает на объект драйвера. Объект драйвера это структура данных, которая содержит служебную информацию о драйвере. Объект драйвера создаётся и частично инициализируется операционной системой перед вызовом точки входа драйвера. Объект драйвера должен содержать в частности адреса обработчиков Пакетов запросов ввода-вывода, адресуемых драйверу. Второй параметр RegistryPath указывает на строку в формате UNICODE содержащую ключ реестра, в котором расположены учётные записи драйвера:

\HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\<Имя драйвера>

134

Функция возвращает код ошибки инициализации. В случае если инициализация драйвера закончилась успешно, функция должна вернуть константу

STATUS_SUCCESS.

В функции DriverEntry необходимо проинициализировать таблицы адресов обработчиков IRP. Таблица адресов размещается в объекте драйвера. Ссылка на таблицу даётся в поле MajorFunction структуры объекта драйвера DRIVER_OBJECT. Указатель эту на структуру содержится в параметре DriverObject. В процессе работы драйверу передаются Пакеты запросов различных типов. Адреса обработчиков IRP каждого типа размещаются в таблице в заданном порядке. В заголовочном файле DDK wdm.h? определены константы IRP_nnn для различных типов IRP. Эти константы задают номера обработчиков в таблице объекта драйвера. Например, константа IRP_MJ_DEVICE_CONTROL задаёт номер обработчика пакетов IRP использующихся для организации передачи драйверу команд из 32-х разрядных приложений Windows.

Для того, чтобы записать в таблицу адрес обработчика IRP такого типа следует поместить в тело функции DeviceEntry следующую строку:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =

IoControlHandler;

,где IoControlHandler это имя функции обработчика.

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

Кроме обработчиков IRP и точки входа DriverEntry, в драйвере могут быть реализованы некоторые другие функции вызываемые извне. Например, функция DriverUnload, которая вызывается при выгрузке драйвера. Если эта функция реализована, её адрес так же заносится в процессе инициализации в структуру DriverObject.

DriverObject->DriverUnload = UnloadHandler;

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

NTSTATUS

DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING

RegistryPath)

{

 

DriverObject->DriverExtension.AddDevice

= AddDeviceHandler;

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]= IoControlHandler;

DriverObject->MajorFunction[IRP_MJ_CREATE]

= CreateHandler;

DriverObject->MajorFunction[IRP_MJ_CLOSE]

= CloseHandler;

DriverObject->DriverUnload

= UnloadHandler;

return STATUS_SUCCESS;

 

}

 

Устройства. Расширения и cимволические ссылки.

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

135

структурой данных называется устройством. Взаимодействие с устройством происходит через объект устройства, а не через объект драйвера. Драйвер может обслуживать несколько устройств. В этом случае для каждого устройства создаётся отдельный объект DEVICE_OBJECT. Устройства могут совместно использовать код и глобальные переменные драйвера. Каждое устройство может иметь символическую ссылку (Symbolic Link). Символическая ссылка это имя, по которому можно получить доступ к устройству. При создании объекта устройства, как правило, в памяти динамически размещается буфер для хранения данных устройства. Этот буфер называется расширение (Extension). Устройство использует расширение для хранения своих внутренних данных. Каждое устройство может хранить свои данные в собственном расширении. Такая технология позволяет избавиться от зависимостей между устройствами, созданными внутри одного драйвера. Ситуации, в которой один и тот же драйвер обслуживает несколько устройств встречаются достаточно часто. Например, в системе может быть установлено несколько одинаковых сетевых контроллеров. Все эти контроллеры обсуживаются одним драйвером. Только одна копия исполняемого модуля драйвера загружается в память. Однако для каждого контроллера создаётся отдельное устройство (объект DEVICE_OBJECT). Устройствам назначается уникальные символические ссылки. Например ‘NETCARD1’, ‘NETCARD2’ и т.д. С помощью этих ссылок внешнее программное обеспечение может указать, к какому именно контроллеру производится обращение. Кроме этого для каждого устройства создаётся расширение. В расширении устройства могут храниться текущие настройки одного из контроллеров и другая частная информация об устройстве. При этом все устройства используют один и тот же код обработчиков Пакетов запросов реализованных в драйвере. Возникает вопрос - как обработчик драйвера определяет контроллер, которому предназначен Пакет запроса? Операционная система по символической ссылке может определить адрес объекта устройства которому направляется Пакет запроса. Этот адрес вместе с указателем на IRP передаётся обработчику. Структура DEVICE_OBJECT содержит поле DeviceExtension. Это поле указывает на блок расширения устройства, т.е. на внутренние данные устройства. Таким образом для всех устройств, обработчики IPR могут быть абсолютно одинаковыми. Устройства отличаются друг от друга только данными. Это справедливо, конечно лишь в том случае, если драйвер обсуживает несколько одинаковых устройств.

В драйверах kernel Windows NT объекты устройств, как правило, создаются непосредственно при запуске драйвера в процедуре DriverEntry. В этом случае разработчик должен включить в процедуру DriverEntry операции поиска физических устройств, которыми управляет драйвер и создания объекта для каждого обнаруженного устройства. В спецификации WDM предусмотрена возможность автоматизации процесса создания объектов устройств, выполненных по технологии Plug and Play. Менеджер конфигурации при обнаружении физического устройства вызывает обработчик драйвера устройства, адрес которого указан поле AddDevice расширения объекта драйвера. Объект драйвера, как и объект устройства, имеет расширение. Объект драйвера и его расширение создаются автоматически операционной системой при загрузке драйвера перед вызовом точки входа DriverEntry. Адрес обработчика AddDevice задаётся вместе с другими в процедуре DriverEntry.

DriverObject->DriverExtension.AddDevice = AddDeviceHandler;

Обработчик AddDevice имеет следующий интерфейс:

NTSTATUS AddDeviceHandler(IN PDRIVER_OBJECT DriverObject,

IN PDEVICE_OBJECT pdo);

В первом параметре передаётся указатель на объект драйвера DRIVER_OBJECT. Второй параметр содержит указатель на так называемый Физический Объект Устройства (Physical Device Object). Системные драйверы, обслуживающие шины, к которым подключаются устройства, самостоятельно создают для каждого обнаруженного устройства Физический Объект. С помощью этого объекта драйвер шины может контролировать работу устройства. Этот дополнительный объект создаётся драйвером шины для собственных целей.

Таким образом, для устройств, выполненных по технологии Plug and Play, процедура поиска выполняется операционной системой, а объекты обнаруженных устройств создаются в обработчике AddDevice драйвера. В драйверах WDM можно так же

136

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

DriverEntry.

Процедура создания объекта устройства.

Объект устройства создаётся с помощью функции IoCreateDevice.

NTSTATUS ntStatus;

UNICODE_STING deviceNameUnicodeString; PDEVICE_OBJECT DeviceObject;

ntStatus = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &deviceNameUnicodeString, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);

Первый параметр функции это указатель на объект драйвера устройства. Указатель PDRIVER_OBJECT передаётся в параметрах точки входа (или в параметрах обработчика AddDevice). Далее в параметрах указывается размер в байтах блока расширения. Переменная deviceNameUnicodeString содержит указатель на имя устройства. Имя устройства определяется разработчиком. Имя и символическая ссылка это разные понятия. Имя может использоваться для идентификации объекта устройства в системе. Символическая ссылка используется для организации доступа к устройству, например из 32-х разрядных приложений Windows. Имя задаётся строкой в формате UNICODE. Строка UNICODE имеет заголовок, в котором задаётся текущая длина строки, максимально возможная длина строки и указатель на буфер строки. Под каждый символ в буфере строки резервируется слово (16-бит). Для работы с такими строками используются несколько специальных функций. Кроме строк UNICODE используются так же строки в формате ANSI. Эти строки имеют такой же заголовок, однако, в буфере строки под каждый символ резервируется один байт. Для инициализации строки UNICODE с именем устройства используют следующую последовательность операций:

ANSI_STRING

devName;

NTSTATUS

ntStatus;

UNICODE_STRING

deviceNameUnicodeString;

CHAR

DeviceNameBuffer[NAME_MAX];

// инициализация буфера строки

strcpy(DeviceNameBuffer, "\\Device\\MYDEVICE");

// инициализация ANSI строки с именем устройства

RtlInitAnsiString(&devName, DeviceNameBuffer);

// преобразование ANSI строки в строку UNICODE

ntStatus = RtlAnsiStringToUnicodeString(&deviceNameUnicodeString, &devName, TRUE);

Переменную deviceNameUnicodeString теперь можно подставлять в параметры функции IoCreateDevice. В этом примере устройству присваивается имя ‘MYDEVICE’. Четвёртый и пятый параметры функции IoCreateDevice задают соответственно тип и дополнительные характеристики устройства. Возможные значения этих параметров приводятся в документации DDK. Шестой параметр равен TRUE, в случае если одновременное обращение к устройству из нескольких потоков запрещено. Наконец, в последнем параметре передаётся адрес переменной, в которую функция возвращает указатель на созданный объект устройства.

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

typedef struct _DEVICE_EXTENSION

{

DWORD IrqNumber; //

номер

линии прерывания

DWORD DmaLine;

//

номер

канала ПДП

CHAR

LinkName[NAME_MAX];

137

PDEVICE_OBJECT

PhysDeviceObject;

} DEVICE_EXTENSION,

*PDEVICE_EXTENSION;

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

Символическая ссылка используется для организации внешнего интерфейса устройства. Приложения Win32 должны обращаться к устройству по его символической ссылке, а не по имени. Для того, чтобы получить доступ к устройству приложения Win32 используют функцию API CreateFile (см. так же главу 9). В первом параметре этой функции указывается символическая ссылка на устройство. Символическая ссылка доступная из приложений Win32 создаётся функцией IoCreateUnprotectedSymbolicLink.

ntStatus = IoCreateUnprotectedSymbolicLink(&linkNameUnicodeString, &deviceNameUnicodeString);

В первом параметре функции указывается имя символической ссылки, а во втором имя устройства. Символическая ссылка, так же как имя устройства, задаётся строкой в формате UNICODE.

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

#define DeviceName «MYDEVICE»

NTSTATUS

ASL_CreateDeviceObject(IN PDRIVER_OBJECT DriverObject,

 

IN OUT PDEVICE_OBJECT *DeviceObject, IN

PCHAR DeviceName)

 

{

 

ANSI_STRING

devName;

ANSI_STRING

linkName;

NTSTATUS

ntStatus = STATUS_SUCCESS;

UNICODE_STRING

deviceNameUnicodeString;

UNICODE_STRING

linkNameUnicodeString;

PDEVICE_EXTENSION deviceExtension;

CHAR

DeviceNameBuffer[NAME_MAX];

CHAR

DeviceLinkBuffer[NAME_MAX];

// инициализация переменных с именами устройства и символической ссылки

strcpy(DeviceNameBuffer, "\\Device\\");

strcpy(DeviceLinkBuffer, "\\DosDevices\\");

// окончательное формирование переменных с именами устройства и

ссылки

strcat(DeviceNameBuffer, DeviceName);

strcat(DeviceLinkBuffer, DeviceName);

// инициализация ANSI строки с именами устройства и ссылки

RtlInitAnsiString(&devName, DeviceNameBuffer);

RtlInitAnsiString(&linkName, DeviceLinkBuffer);

// преобразование строк ANSI в строки UNICODE ntStatus =

RtlAnsiStringToUnicodeString(&deviceNameUnicodeString, &devName, TRUE);

ntStatus = RtlAnsiStringToUnicodeString(&linkNameUnicodeString,

&linkName, TRUE);

// создание объекта устройства ntStatus = IoCreateDevice(DriverObject,

sizeof(DEVICE_EXTENSION),

&deviceNameUnicodeString, FILE_DEVICE_UNKNOWN, 0, FALSE, DeviceObject);

if (NT_SUCCESS(ntStatus)) {

// создание символической ссылки ntStatus =

IoCreateUnprotectedSymbolicLink(&linkNameUnicodeString,

&deviceNameUnicodeString);

138

// получение указателя на Device Extension deviceExtension=

(PDEVICE_EXTENSION)(*DeviceObject)->DeviceExtension;

// обнуление Device Extension

RtlZeroMemory(deviceExtension, sizeof(DEVICE_EXTENSION));

//запись в расширение символической ссылки

strcpy(deviceExtension->LinkName, DeviceLinkBuffer);

//и указателя на устройство deviceExtension->PhysDeviceObject = *DeviceObject;

}

// освобождение памяти от Unicode строк

RtlFreeUnicodeString(&deviceNameUnicodeString);

RtlFreeUnicodeString(&linkNameUnicodeString); return ntStatus;

}

Объект устройства динамически размещается в памяти. Если устройство удаляется из системы, объект устройства должен быть, так же удалён. Символическая ссылка на объект так же должна быть удалена. Расширение объекта удаляется автоматически при удалении объекта DEVICE_OBJECT. Ниже приводится пример удаления объекта устройства и символической ссылки.

NTSTATUS RemoveDevice(IN PDEVICE_OBJECT pDeviceObject)

{

NTSTATUS

ntStatus = STATUS_SUCCESS;

UNICODE_STRING

DeviceLinkUnicodeString;

ANSI_STRING

DeviceLinkAnsiString;

PDEVICE_EXTENSION DeviceExtension =

pDeviceObject->DeviceExtension;

//преобразование строки с именем символической ссылки в строку

//UNICODE

RtlInitAnsiString(&DeviceLinkAnsiString, DeviceExtension->LinkName);

ntStatus = RtlAnsiStringToUnicodeString( &DeviceLinkUnicodeString,

&DeviceLinkAnsiString, TRUE);

//удаление символической ссылки

IoDeleteSymbolicLink(&DeviceLinkUnicodeString);

//удаление устройства

IoDeleteDevice(pDeviceObject);

RtlFreeUnicodeString(&DeviceLinkUnicodeString); return ntStatus;

}

Типы объектов устройств.

Спецификация WDM разделяет объекты устройств на несколько типов. (В Windows NT такого разделения нет). Для управления одним физическим устройством может быть создано несколько объектов. Все они описываются структурой DEVICE_OBJECT, однако имеют различное функциональное назначение. Физический объект устройства (Physical Device Object) создаётся драйвером шины, к которой подключается устройство. Например, системный драйвер, обслуживающий шину PCI после обнаружения устройства на шине создаёт свой собственный физический объект устройства, который он в дальнейшем использует для обслуживания устройства. Существует набор операций по управлению устройством, которые не входят в компетенцию драйвера устройства. Эти операции драйвер шины выполняет через физический объект устройства. В свою очередь драйвер устройства создаёт и использует для работы с устройством функциональный объект устройства (Functional Device Object). Процедура создания именно такого объекта была описана выше. Приложения и другие системные модули получают доступ к устройству через функциональный объект. Кроме этих двух объектов с устройством могут быть связаны один или несколько фильтров (Filter Object). Фильтры используются для расширения функциональных возможностей драйверов. Фильтры вставляются в цепочку обработки пакетов запросов, которые направляются устройству. Каждый объект в цепочке может выполнять свою часть процедуры обработки IRP. Например, в цепь объектов

139

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

Объект Фильтр

(создаётся драйвером этого фильтра)

Функциональный объект устройства

(создаётся драйвером устройства)

Объект Фильтр

(создаётся драйвером этого фильтра)

Физический объект устройства

(создаётся драйвером шины)

Рисунок 2. Последовательность обработки IRP различными объектами устройства.

Создание объектов фильтров необязательно. В простейшем случае, если устройство не относится ни к одному из классов устройств, для которых имеются драйверы шины, для управления устройством достаточно создать только функциональный объект. Так как система не в состоянии обнаружить такое устройство и, следовательно, вызвать обработчик AddDevice, функциональный объект устройства следует создавать в точке входа DriverEntry. Драйвер WDM такого устройства может практически не отличаться от kernel драйвера Windows NT.

Интерфейсы устройств.

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

В спецификации WDM эти проблемы решаются с помощью введения понятия интерфейса устройства. Интерфейс открывает доступ к определённому набору функций реализованных в драйвере. Драйвер WDM может одновременно поддерживать несколько различных интерфейсов. Каждый интерфейс идентифицируется в системе уникальным «в пространстве и времени» кодом GUID (Global User Identifier). Этот код фактически используется как символическая ссылка, с помощью которой приложения и другие системные компоненты могут получить доступ к конкретному набору функций. Коды GUID задаются при проектировании драйвера. Код может быть получен, например, с помощью программы UUIDGEN, которая входит в состав Windows SDK. Код формируется на основе системного времени и IP адреса, поэтому он уникален. Код GUID декларируется в заголовочном файле проекта драйвера. Этот файл может быть использован в дальнейшем приложениями, которые

140

работают с драйвером. Для объявления кода используется макроопределение

DEFINE_GUID . Например:

#define INITGUID

DEFINE_GUID(MYDEVICE_GUID, 0x673b9cc0, 0x320d, 0x11d3, 0x93, 0xeb, 0x9a, 0x8c, 0x47, 0x70, 0x31, 0x18);

Далее в тексте программы для указания кода GUID используется имя MYDEVICE_GUID.

Если перед DEFINE_GUID записать #define INITGUID под структуру GUID

автоматически резервируется память.

Перед использованием интерфейс устройства должен быть зарегистрирован. Как правило, интерфейс устройства регистрируется сразу после создания объекта устройства DEVICE_OBJECT в процедуре AddDevice.

NTSTATUS AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo)

{

NTSTATUS status; PDEVICE_OBJECT DeviceObject;

PDEVICE_EXTENSION DeviceExtension;

status = IoCreateDevice(..., &DeviceObject); dext = DeviceObject->DeviceExtension;

if (!NT_SUCCESS(status)) return status;

status = IoRegisterDeviceInterface(pdo, &MYDEVICE_GUID, NULL, &dext->InterfaceName);

if (!NT_SUCCESS(status)) { IoDeleteDevice(DeviceObject); return status;}

IoSetDeviceInterfaceState(DeviceExtension->InterfaceName,

TRUE);

...

return status;

}

Интерфейс регистрируется с помощью функции IoRegisterDeviceInterface. Первый параметр функции это указатель на физический объект устройства (не функциональный!). Во втором параметре задаётся код GUID интерфейса. Третий параметр, указатель на строку UNICODE, используется для организации пространства имён внутри интерфейса. Эта технология используется некоторыми драйверами шин, однако она не документирована, и не рекомендуется её использовать. Последний параметр это адрес строки UNICODE, в которую функция возвращает символическую ссылку на интерфейс. Память под заголовок строки UNICODE размещает вызывающая программа. В приведённом примере заголовок строки располагается в расширении функционального объекта DeviceObject.

...

PUNICODE_STRING InterfaceName;

...

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

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

141

генерирует символическую ссылку на устройство - InterfaceName. Для каждого устройства поддерживающего данный интерфейс создаётся уникальная символическая ссылка. Таким образом, устройство идентифицируется в два этапа, сначала по коду GUID интерфейса, а затем по символической ссылке созданной внутри этого интерфейса.

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

Получить список устройств, которые экспортируют заданный интерфейс.

HANDLE hDevList = SetupDiGetClassDevs(&MYDEVICE_GUID, NULL, NULL,

DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);

В параметрах функции указывается код GUID интерфейса. Функция возвращает ссылку на список устройств (device information set), которые поддерживают интерфейс. По окончании работы со списком он должен быть удалён.

SetupDiDestroyDeviceInfoList(hDevList);

Получить информацию о интерфейсе каждого устройства в списке.

SP_INTERFACE_DEVICE_DATA InterfaceData;

for (i = 0; ; i++) { SP_INTERFACE_DEVICE InterfaceData;

InterfaceData.cbSize = sizeof(InterfaceData);

if (!SetupDiEnumDeviceInterfaces(hDevList, NULL, &MYDEVICE_GUID, i, &InterfaceData)) {

if (GetLastError() == ERROR_NO_MORE_ITEMS) break; //выход (пройден весь список)

}

//доступ к i-ому устройству. (пп. 3 и 4)

}

Функция SetupDiEnumDeviceInterfaces вызывается в цикле и возвращает в структуре InterfaceData информацию об интерфейсе i-ого устройства в списке hDevList.

Получить расширенную информацию о интерфейсе для каждого (i-ого) устройства в списке.

SetupDiGetInterfaceDeviceDetail(hDevList, &InterfaceData, NULL, 0, &size, NULL);

PSP_INTERFACE_DEVICE_DETAIL_DATA idd = (PSP_INTERFACE_DEVICE_DETAIL_DATA)malloc(size);

idd->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);

SetupDiGetInterfaceDeviceDetail(hDevList, &InterfaceData, idd, size, NULL, NULL);

char linkname[MAX_PATH];

strncpy(linkname, idd->DevicePath, sizeof(linkname)); free((PVOID)idd);

Для каждого из обнаруженных устройств вызывается функция SetupDiGetInterfaceDeviceDetail. Эта функция возвращает в структуре idd расширенную информацию о интерфейсе устройства. В первом параметре функции указывается ссылка на список устройств, во втором указатель на информацию об интерфейсе полученную с помощью функции SetupDiEnumDeviceInterface. Третий параметр указывает на буфер в который записывается структура с расширенной информацией о интерфейсе устройства. В четвёртом параметре указывается размер этой структуры. В пятом возвращается её действительный размер. Функцию приходится вызывать дважды, так как размер данных которые функция возвращает по указателю idd заранее

142

неизвестен. Первый вызов функции используется для определения размера возвращаемой структуры (&cbsize). Структура SP_INTERFACE_DEVICE_DETAIL_DATA

содержит поле cbSize, которое следует проинициализировать перед вызовом функции. Поле cbSize используется для указания версии структуры SP_INTERFACE_DEVICE_DETAIL_DATA. Его значение определяется константой sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA), а не реальным размером структуры который возвращается функцией в переменную size.

Открыть устройство с помощью функции CreateFile.

В поле DevicePath структуры SP_INTERFACE_DEVICE_DETAIL_DATA содержится символическая ссылка устройства (InterfaceName). Эта ссылка создаётся автоматически когда драйвер регистрирует интерфейс устройства вызовом функции IoRegisterDeviceInterface. Приложение использует символическую ссылку для открытия устройства.

HANDLE hDevice = CreateFile(linkname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

После того как устройство открыто ссылка (handle) hDevice указывается в операциях записи (WriteFile), чтения (ReadFile) и передачи команд (DeviceIoControl) адресованных этому устройству. По завершении работы с устройством ссылка hDevice должна быть закрыта: CloseHandle(hDevice).

К устройству можно получить доступ через интерфейс, или через символическую ссылку, типа «\\DosDevices\\MYDEVICE», как это принято в драйверах kernel Windows NT. В последнем случае символическая ссылка создаётся, например, функцией CreatеUnprotectedSymbolicLink. Это два различных метода, и вопрос о том какой из них использовать в драйвере WDM решается разработчиком.

Обработка пакетов запросов ввода-вывода (IRP).

Пакеты запросов IRP передаются устройству через вызовы обработчиков, адреса которых записываются в структуру DEVICE_OBJECT при загрузке драйвера в процедуре DriverEntry. В драйвере может быть реализовано несколько обработчиков различных пакетов или групп пакетов запросов. Один обработчик может обслуживать несколько различных типов пакетов запросов. В этом случае обработчик выполняет функции диспетчеризации пактов запросов внутри драйвера. Интерфейс обработчика IRP устройства содержит два параметра: указатель на объект устройства и указатель на IRP. По указателю на объект устройства обработчик может получить доступ к блоку расширения устройства. В структуре IRP содержится служебная информация, в том числе ссылка на предназначенную драйверу структуру параметров IO_STACK_LOCATION и данные. Тип IRP определяется двумя значениями из структуры IO_STACK_LOCATION, которые называются старшим кодом функции MajorFunction и младшим кодом функции MinorFunction. Для некоторых типов пакетов IRP декодируется только старший код функции. Значение старшего кода функции совпадает с номером в таблице адресов обработчиков расположенной в объекте драйвера: DriverObject- >MajorFunction. Если в этой таблице для каждого кода MajorFunction задан отдельный обработчик диспетчеризация выполняется автоматически. Допускается также указывать адрес одного и того же обработчика в нескольких записях этой таблицы. Например:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =

IoControlHandler;

DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] =

IoControlHandler;

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

IoGetCurrentIrpStackLocation:

PIO_STACK_LOCATION irpStack;

irpStack = IoGetCurrentIrpStackLocation(Irp);

143

MjFunc = irpStack->MajorFunction;

MnFunc = irpStack->MinorFunction;

Переданная через параметры обработчика переменная Irp указывает на структуру пакета запроса. Обработчик производит диспетчеризацию вызова в зависимости от значений MjFunc и MnFunc.

IRP содержит структуру IoStatus в которой возвращается результат обработки IRP. В поле Irp-> IoStatus.Status обработчик, перед возвратом управления должен поместить код ошибки. В случае если Irp обработано без ошибок в поле Status записывается константа STATUS_SUCCESS. Обработка асинхронных сообщений это особый случай. Если устройство не может завершить операцию синхронно, т.е. до возврата управления вызывающей программе в поле Status записывается константа STATUS_PENDING. При этом IRP сохраняется до тех пор пока асинхронная операция не будет закончена.

Содержимое поля Irp-> IoStatus.Information зависит от типа Пакета запроса. В пакетах запросов которые используются для передачи (чтения/записи) данных в поле Information следует указывать число реально переданных байтов.

После обработки пакета запроса следует вызвать функцию IoCompleteRequest которая информирует систему о завершении обработки IRP. Следует отметить, что если Irp передаётся по цепочке драйверов, эту функцию вызывает драйвер расположенный последним в цепочке. Драйверы, через которые проходит Irp, могут передать его следующим в очереди c помощью функции IoCallDriver. Каждый драйвер может установить свою собственную callback функцию IoCompletionRoutine. Эти функции вызываются, когда последний в цепочке драйвер заканчивает обработку Irp и вызывает функцию IoCompleteRequest.

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

Обработчик IRP может заканчиваться следующими строками:

Irp-> IoStatus.Status = ntStatus;

Irp-> IoStatus.Information = sizeof(DataBuffer); if (ntStatus == STATUS_PENDING)

IoMarkIrpPending(Irp);

else

IoCompleteRequest(Irp, IO_NO_INCREMENT);

Функция IoCompleteRequest, кроме указателя на IRP содержит так же параметр, задающий значение, на которое следует увеличить базовый приоритет потока инициировавшего передачу IRP. Базовый приоритет потока увеличивается временно (см. главу 5). Если этот параметр равен IO_NO_INCRENENT приоритет потока не изменяется.

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

Синхронная обработка IRP.

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

Irp->IoStatus.Status = ntStatus;

Irp->IoStatus.Information = Information;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return ntStatus;

144

Асинхронная обработка IRP.

Драйверы WDM используют специальную технологию конвееризации асинхронной обработки пакетов запросов. Если пакет запроса не может быть обработан синхронно, он передаётся на конвеер обработки пакетов запросов. Для этого используется функция IoStartPacket. Для использования этой технологии в драйвере должна быть реализована функция DriverStartIo. Адрес этой функции заносится в структуру объекта драйвера устройства, как правило в процедуре DriverEntry.

DriverObject->DriverStartIo = StartIoHandler;

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

void StartIoHandler(IN_PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

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

IoMarkIrpPending(Irp); IoStartPacket(DeviceObject, Irp, NULL, OnCancel); return STATUS_PENDING;

В параметрах функции IoStartPacket передаются указатели на объект устройства DeviceObject, пакет запроса Irp и необязательный указатель на процедуру OnCancel которая будет вызвана в случае если обработка Irp будет отменена до её завершения. Отменить обработку Irp может программа, которая изначально инициировала запрос. Третий параметр функции задаёт место в очереди, в которое помешается пакет запроса. Если этот параметр равен NULL, пакет запроса помещается в конец очереди.

По завершении обработки IRP драйвер должен вызвать функцию IoStartNextPacket. Эта функция извлекает следующий в очереди пакет запроса и вызывает функцию DriverStartIo для его обработки. Если в очереди нет запросов, функция просто возвращает управление.

Как правило, в функции DriverStartIo производится инициализация аппаратуры и запуск асинхронной операции. Типичным, является сценарий, в котором аппаратное устройство запускается в функции DriverStartIo, выполняет заданную операцию и сообщает о её окончании выставляя аппаратное прерывание. Драйвер должен установить обработчик, который используется для перехвата сообщения об окончании операции. Асинхронная обработка IRP с привлечением механизма прерываний имеет важную особенность. Обработчик прерывания запускается на повышенном уровне приоритета IRQL. На этом уровне запрещен вызов некоторых важных системных функций, в частности функции IoStartNextPacket и функции IoCompleteRequest, с

помощью которой драйвер должен сообщить Менеджеру ввода-вывода о завершении обработки IRP. Поэтому драйвер использует специальный механизм отложенных вызовов процедур (Deferred Procedure Call, сокращённо DPC). Процедура DPC автоматически получает управление после того, как уровень приоритета будет снижен. Это происходит уже после того, как обработчик прерывания завершится. Подробнее эта технология будет рассмотрена в дальнейшем. Сейчас для нас важным является то, что аппаратура информирует драйвер о завершении асинхронной операции, используя механизм прерываний. Обработчик прерывания формирует отложенный вызов процедуры. Наконец в процедуре DPC обработка IRP завершается и запускается обработка очередного пакета запроса (рисунок 3).

IoStartNextPacket(DeviceObject, FALSE);

Irp->IoStatus.Status = ntStatus;

Irp->IoStatus.Information = Information;

145

IoCompleteRequest(Irp, IO_NO_INCREMENT);

1

 

 

 

 

 

 

 

 

Обработчик IRP

2

 

 

 

MarkIrpPending()

 

 

 

 

IoStartPacket

 

 

 

 

3

 

 

 

return STATUS_PENDING

 

 

 

 

 

Менеджер

DriverStartIo

 

ввода-вывода

//запуск аппаратуры

4

 

 

 

 

 

 

 

 

 

Обработчик прерывания (ISR)

//запрос вызова отложенной //процедуры

5IoRequestDpc

6

Отложенная процедура (DPC)

7 IoStartNextPacket

IoCompleteRequest

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

Следует отметить, что использование стандартной очереди пакетов запросов и функции DriverStartIo для асинхронной обработки IRP необязательно. Можно создавать и использовать собственные очереди пакетов запросов. Необходимость в этом возникает в случае если устройство должно одновременно поддерживать несколько независимых очередей IRP. Для создания дополнительных очередей и управления ими используются функции KeInitializeDeviceQueue, KeInsertDeviceQueue, KeRemoveDeviceQueue и др.

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

Передача IRP объекту устройства расположенного следующим в стеке.

Стек объектов создаётся на этапе загрузки и инициализации драйверов устройства. Наиболее типичным является случай, в котором стек устройств содержит физический объект созданный драйвером шины и функциональный объект созданный драйвером устройства. Функциональный объект расположен первым в стеке. В ряде случаев пакет запроса переданный функциональному объекту устройства должен быть передан для обработки физическому объекту. Физический объект устройства создаётся первым. Драйвер устройства получает адрес физического объекта и как правило сохраняет этот адрес в расширении функционального объекта. Эта процедура более подробно описывается в параграфе «Пакеты запросов PnP и стек объектов устройств».

Для вызова обработчика IRP ниже расположенного в стеке объеста используется функция IoCallDriver.

NTSTATUS status = IoCallDriver(dext->NextDevice, Irp);

В параметрах функции указывается адрес объекта, которому передаётся IRP и указатель на структуру IRP.

146

При создании пакета запроса учитывается количество объектов расположенных в стеке. Для каждого объекта создаётся отдельный блок IO_STACK_LOCATION. (рисунок

4)

IRP

Функциональный объект устройства

Физический объект устройства

IO_STACK_LOCATION (1)

IO_STACK_LOCATION (2)

Рисунок 4 Объекты и элементы стека.

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

IoGetCurrentIrpStackLocation.

Выше расположенный объект устройства должен инициализировать структуру IO_STACK_LOCATION следующего устройства, перед тем как передать ему управление. Как правило текущий элемент копируется в следующий. Для этого используется функция IoCopyCurrentIrpStackLocationToNext. Объекты устройств в стеке могут совместно использовать один и тот же элемент IO_STACK_LOCATION. Для этого вместо копирования элемента, необходимо вызвать функцию IoSkipCurrentIrpStackLocation. Эта функция сдвигает указатель в массиве элементов на одну позицию назад. При вызове обработчика ниже расположенного в стеке устройства 3указатель автоматически сдвигается на одну позицию вперёд и снова указывает на элемент массива выше расположенного объекта. Какой из этих двух способов следует использовать, зависит от решаемой задачи. Однако следует отметить, что применение функции IoCopyCurrentIrpStackLocationToNext имеет дополнительное преимущество. Пакет запроса передаётся вниз по стеку до тех пор, пока его обработку не закончит устройство, расположенное на дне стека. Это устройство вызывает функцию IoCompleteRequest. Устройство, которое передаёт пакет запроса следующему объекту, может продолжить обработку IRP после того, как её закончат устройства расположенные ниже в стеке. Для того чтобы объект устройства мог получить управление после того, как обработка Irp на более низком уровне завершена, в

структуре IO_STACK_LOCATION предусмотрено поле CompletionRoutine. Перед вызовом функции IoCallDriver объект устройства может занести в это поле адрес процедуры, которая будет вызвана, после того как нижерасположенный драйвер закончит обработку IRP и вызовет функцию IoCompleteRequest. При вызове функции IoCompleteRequest Менеджер ввода-вывода последовательно сканирует элементы массива IO_STACK_LOCATION начиная с самого нижнего. Если поле CompletionRoutine очередного элемента не равно NULL Менеджер передаёт управление по указанному в поле адресу. Функция CompletionRoutine имеет следующий интерфейс:

NTSTATUS OnRequestComplete(IN PDEVICE_OBJECT DeviceObject,

IN PIRP Irp, IN PVOID Context);

Функции при вызове предаются указатели на объект устройства и Irp, а так же определяемая пользователем переменная Context.

147

Для того чтобы установить функцию CompletionRoutine следует использовать функцию IoSetCompletionRoutine. Каждый объект в стеке может установить собственную функцию CompletionRoutine, однако если два драйвера совместно используют один элемент массива IO_STACK_LOCATION (драйвер расположенный выше в стеке вызывает функцию IoSkipCurrentIrpStackLocation), только один из объектов может установить функцию CompletionRoutine.

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

IO_STACK_LOCATION за исключением поля CompletionRoutine.

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

PDEVICE_EXTENSION dext = DeviceObject->DeviceExtension; IoSkipCurrentStackLocation(Irp);

return IoCallDriver(dext->NextDevice, Irp);

В следующем примере устанавливается функция CompletionRoutine.

IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp,(PIO_COMPLETION_ROUTINE)OnRequestComplete,

&Context, TRUE, TRUE, TRUE); return IoCallDriver(dext->NextDevice, Irp);

Последний расположенный в стеке объект устройства может выполнить операцию синхронно, т.е. до возврата управления обработчику объекта расположенного над ним. При этом он возвращает код отличный от STATUS_PENDING. Например, в случае успешного завершения операции возвращается код STATUS_SUCCESS. Если операция должна быть выполнена асинхронно, обработчик объекта устройства только инициирует исполнение операции. Например, IRP помещается в стандартную очередь пакетов запросов для последующей передачи процедуре DriverStartIo. Обработчик вызывает функцию IoMarkIrpPending, запускает асинхронную операцию и немедленно возвращает управление с кодом STATUS_PENDING обработчику вышерасположенного объекта устройства. Функция IoMarkIrpPending устанавливает в текущем элементе

IO_STACK_LOCATION флаг SL_PENDING_RETURNED. Этот флаг указывает, что обработка IRP производится асинхронно. Флаг в дальнейшем используется в процедурах завершающих обработку IRP. Код возврата STATUS_PENDING последовательно передаётся обратно, вверх по стеку до программы, которая инициировала запрос. Когда вызывающая программа получает управление, пакет запроса может быть ещё не выполнен аппаратурой. Вызывающая программа получив код возврата STATUS_PENDING, как правило, переходит в режим ожидания завершения асинхронной операции. Например, пользовательское приложение может применить следующую стратегию ожидания завершения операции:

OVERLAPPED ovr = {0,0,0,0,0}; char buffer[1024]; ULONG cbReaden;

HANDLE hDevice = CreateFile(DeviceSLinkName, ...);

...

ovr.hEvent = CreateEvent(0, TRUE, 0, NULL);

if (!ReadFile(hDevice, buffer, 1024, &cbReaden, &ovr) {

if (GetLastError() == ERROR_IO_PENDING)

//ждём завершения асинхронной операции

WaitForSingleObject(ovr.hEvent, INFINITE);

...

else { //ошибка ... return}

}

else { //операция выполнена синхронно}

148

Вызывающая программа передаёт в данном случае в параметрах функции ReadFile ссылку на объект синхронизации ovr.hEvent. Системная ссылка на этот объект помещается в поле UserEvent структуры IRP. Если операция выполняется асинхронно, ReadFile возвращает значение FALSE, а последующий вызов GetLastError возвращает код ERROR_IO_PENDING. Объект устройства, выполняющий асинхронную операцию, сообщает о завершении операции, устанавливая объект синхронизации ovr.hEvent (UserEvent) в состояние signaled. После этого функция WaitForSingleObject возвращает управление, и исполнение вызывающей программы продолжается..

Функция IoCompleteRequest используется для завершения как синхронных, так и асинхронных операций. Функцию вызывает устройство, расположенное на дне стека. Если операция выполняется асинхронно, IoCompleteRequest вызывается по завершении операции, например из процедуры DPC. Функция последовательно сканирует элементы IO_STACK_LOCATION IRP, начиная с самого нижнего, и вызывает установленные функции CompletionRoutine. Кроме этого функция устанавливает в состояние signaled объект UserEvent, если операция выполнялась асинхронно. Функция принимает решения о том асинхронно или синхронно выполнялась операция по значению поля PendingReturned структуры IRP. Если значение этого поля равно TRUE (операция исполнялась асинхронно) функция устанавливает в состояние signaled объект UserEvent. Если PendingReturned = FALSE (операция исполнялась синхронно) функция не изменяет состояние объекта UserEvent.

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

IO_STACK_LOCATION. Если флаг SL_PENDING_RETURNED в текущем элементе установлен полю PendingReturned присваивается значение TRUE. Если флаг сброшен полю PendingReturned присваивается значение FALSE. Далее если в элементе не установлена функция CompletionRoutine в выше расположенном элементе стека флаг SL_PENDING_RETURNED устанавливается в том случае, если текущее значение

PendingReturned = TRUE. Если текущее значение PendingReturned = FALSE значение флага SL_PENDING_RETURNED в вышерасположенном элементе стека не изменяется.

Таким образом, если устройство расположенное на дне стека установило в своём элементе IO_STACK_LOCATION флаг асинхронной операции

SL_PENDING_RETURNED (IoMarkIrpPending) и при этом ни один драйвер в цепочке не установил свою функцию CompletionRoutine, флаг SL_PENDING_RETURNED и вместе с ним значение TRUE поля PendingReturned последовательно всплывает по стеку до самого верхнего элемента. По окончании операции сканирования стека значение поля

PendingReturned равно TRUE. На основании этого функция IoCompleteRequest

принимает решение о том, что операция производилась асинхронно и устанавливает объект синхронизации UserEvent в состояние signaled.

Однако, если в каком либо элементе стека установлена функция CompletionRoutine значение флага SL_PENDING_RETURNED автоматически не переносится в следующий элемент стека. Вместо этого просто вызывается функция CompletionRoutine. Программист должен самостоятельно выполнить операцию переноса флага в верхний элемент IO_STACK_LOCATION, иначе «всплытие» флага по стеку прервётся и результирующее значение PendingReturned может быть равно FALSE, несмотря на то, что объект устройства расположенный на дне стека выполнял операцию асинхронно и указал на это установив флаг SL_PENDING_RETURNED в своём элементе IO_STACK_LOCATION. Это происходит по той причине, что объекты устройств расположенные выше в стеке передают IPR вниз по стеку, не вызывая функции

IoMarkIrpPending, т.е. не устанавливая в своих элементах IO_STACK_LOCATION флага

SL_PENDING_RETURNED. Эти объекты при передаче IRP не могут определить, синхронно или асинхронно будет обрабатываться этот запрос в объектах расположенных ниже в стеке. В случае если флаг асинхронной операции будет потерян при сканировании стека функция IoСompleteRequest не установит объект синхронизации UserEvent в состояние signaled и вызывающая программа зависнет на вызове WaitForSingleObject(ovr.hEvent ...);

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

149

NTSTATUS OnRequestComplete(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)

{

if(Irp->PendingReturned) IoMarkIrpPending(Irp);

...

}

Этот код устанавливает флаг SL_PENDING_RETURNED в текущем элементе IO_STACK_LOCATION, если значение поля PendingReturned, полученное в результате сканирования ниже расположенных элементов IO_STACK_LOCATION равно TRUE. Если функция CompletionRoutine возвращает код

STATUS_MORE_PROCESSING_REQUIRED, вставка указанного фрагмента кода необязательна. Это особый случай. Следует ещё раз отметить, что установленные в обработчиках объектов устройств функции CompletionRoutine (типа

OnRequestComplete) вызываются из функции IoCompleteRequest. Если какая либо из функций CompletionRoutine возвращает код STATUS_MORE_PROCESSING_REQUIRED

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

Отмена асинхронно исполняющихся запросов.

Асинхронная обработка запроса может быть отменена, до её завершения. Эту операцию может выполнить программа, которая инициировала запрос или Менеджер ввода-вывода, например в случае если поток (thread) вызывающей программы завершает исполнение в то время, как в очереди драйвера ещё остаются необработанные пакеты запросов переданные из этого потока. Пользовательское приложение, для отмены обработки пакетов запросов вызывает функцию CancelIo. Драйверы используют функцию IoCancelIrp. К сожалению, инициатор пакета запроса не может самостоятельно выполнить всю процедуру отмены обработки пакета запроса. Простое освобождение памяти выделенной под структуру IRP может привести к ошибке. IRP передаётся по стеку объектов устройств. Объект устройства, который выполняет асинхронную обработку пакета запроса, использует свои собственные указатели на структуру IRP. Например, IRP может находится в стандартной очереди объекта устройства, ожидая передачи на конвеер DriverStartIo. Инициатор передачи пакета запроса не обладает достаточной информацией для того, чтобы корректно удалить пакет запроса. Часть процедуры удаления IRP должен выполнить драйвер, который обрабатывает IRP. Для того чтобы драйвер устройства мог перехватить команду отмены обработки запроса и выполнить свою часть операции отмены, в структуре IRP предусмотрено поле CancelRoutine. Перед тем как начать асинхронную обработку пакета запроса драйвер записывает в это поле адрес процедуры, которая будет вызвана в случае отмены обработки IRP.

void CancelHandler (PDEVICE_OBJECT DeviceObject, PIRP Irp);

Структура Irp также содержит поле Cancel, которое используется, как флаг отмены обработки Irp. Функция IoCancelIrp устанавливает флаг Cancel и вызывает

CancelRountine если её адрес не равен NULL.

Если драйвер использует стандартный конвеер обработки IRP, адрес CancelRoutine, указывается в последнем параметре функции IoStartPacket.

IoMarkIrpPending(Irp);

IoStartPacket(fdo, Irp, NULL, CancelHandler);

Если стандартный конвеер не используется, CancelRoutine можно установить функцией

IoSetCancelRoutine.

IoSetCancelRoutine(Irp, CancelHandler);

150

Операция отмены запроса должна быть синхронизирована с операциями обработки IRP, которые выполняет драйвер. Это обусловлено тем, что MS Windows многозадачная операционная система. Функция IoCancelIrp может, например, исполнятся на одном из процессоров, в то время как на другом процессоре выполняется операция извлечения IRP из стандартной очереди пакетов запросов для последующей передачи IRP обработчику DriverStartIo. Одновременное выполнение этих операций приводит к ошибке. Поэтому на время исполнения таких операций IRP следует защитить от доступа из других потоков. Для этой цели используются системные структуры spin lock. Spin lock работает подобно критической секции. Одновременно только один поток может захватить ресурс с которым ассоциирован spin lock. В данном случае этим ресурсом является IRP. Поток захватывает ресурс, выполняет необходимые операции и затем освобождает его. Если другой поток пытается захватить уже занятый ресурс, его исполнение блокируется до тех пор, пока текущий владелец ресурса не освободит его. В операциях связанных с процедурой отмены обработки IRP пакет запроса захватывается функцией IoAcquireCancelSpinLock и освобождается функцией IoReleaseCancelSpinLock. Если для обработки IRP используется стандартный конвеер, большинство операций синхронизации выполняется автоматически функциями IoStartPacket, IoStartNextPacket и IoCancelIrp.

Однако часть работы возлагается на разработчика драйвера.

Рассмотрим распространённый случай, когда CancelRoutine устанавливается только на время, пока IRP находится в стандартной очереди пакетов запросов. В ниже приведённом примере, когда Irp передаётся для исполнения обработчику DriverStartIo в поле CancelRoutine заносится значение NULL, поэтому запрос может быть отменён только пока он находится в очереди.

void StartIoHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp)

{

KIRQL oldirql; IoAcquireCancelSpinLock(&oldirql);

if (Irp != DeviceObject->CurrentIrp || Irp->Cancel) { IoReleaseCancelSpinLock(oldirql);

return;

}

else {

IoSetCancelRoutine(Irp, NULL); IoReleaseCancelSpinLock(oldirql);

}

...

}

Операции проверки состояния Irp и сброса CancelRoutine защищены структурой spin lock, поэтому они не могут быть прерваны вызовом IoCancelIrp.

Функция IoCancelIrp перед тем как передать управление обработчику CancelRoutine

захватывает Irp вызывая функцию IoAcquireCancelSpinLock. В функции CancelRoutine

необходимо освободить ресурс вызвав функцию IoReleaseCancelSpinLock.

void CancelHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp)

{

if (DeviceObject->CurrentIrp == Irp) { IoReleaseCancelSpinLock(Irp->CancelIrql); IoStartNextPacket(DeviceObject, TRUE);

}

else {

KeRemoveEntryDeviceQueue(&DeviceObject->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry);

IoReleaseCancelSpinLock(Irp->CancelIrql);

}

Irp->IoStatus.Status = STATUS_CANCELLED;

IoCompleteRequest (Irp, IO_NO_INCREMENT);

}

151

Если пакет запроса находится в очереди, он из неё извлекается

(KeRemoveEntryDeviceQueue). Функция CancelHandler завершает обработку Irp с кодом STATUS_CANCELLED.

Основные типы IRP.

IRP_MJ_CREATE. Этот пакет запроса посылается устройству, когда пользовательское приложение или другой драйвер запрашивает ссылку (handle) на устройство. Для того чтобы получить доступ к устройству драйверы должны использовать функцию ZwCreateFile, а пользовательские приложения функцию CreateFile. В параметре FileName этой функции указывается символическая ссылка устройства. Например:

HANDLE hDevice;

hDevice = CreateFile("\\\\.\\MYDEVICE", 0,0,0,CREATE_NEW, FILE_FLAG_DELETE_ON_CLOSE, 0);

В процессе исполнения функции CreateFile система формирует пакет запроса IRP_MJ_CREATE и передаёт его обработчику драйвера устройства. Функция CreateFile (ZwCreateFile) создаёт специальный объект файла (file оbject) который в дальнейшем используется для организации обмена данными между приложением и устройством. Функция возвращает ссылку на объект файла в переменную hDevice. Эта ссылка в дальнейшем используется для организации обмена данными с устройством. Указатель на объект файла передаётся драйверу в поле FileObject структуры IO_STACK_LOCATION. Этот указатель присутствует во всех пакетах запросов связанных со ссылкой hDevice (В том числе и в пакете запроса IPR_MJ_CREATE). По этому указателю устройство может различать источники запросов. Несколько приложений могут одновременно получить несколько ссылок на устройство. Для каждой из ссылок создаётся отдельный объект файла. Приложение, обращаясь к устройству, указывает ссылку по которой происходит обращение. Например, для передачи данных драйверу может использоваться функция WriteFile(hDevice, ...) в первом параметре которой указывается ссылка на устройство. WriteFile формирует IRP и передаёт его обработчику драйвера устройства. При этом в поле FileObject структуры IO_STACK_LOCATION заносится указатель на объект файла ранее созданный для ссылки hDevice.

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

CloseHandle(hDevice);

Драйверы используют для этой цели функцию ZwClose. При уничтожении ссылки на устройство удаляется так же связанный с ней объект файла.

IRP_MJ_CLEANUP. Этот пакет запроса устройство получает непосредственно перед пакетом запроса IRP_MJ_CLOSE. В обработчике этого IRP устройство должно отменить асинхронное исполнение всех пакетов запросов, которые относятся к объекту файла указанному в структуре IO_STACK_LOCATION. Т.е. перед тем как ссылка на устройство будет закрыта, следует удалить все необработанные пакеты запросов переданные по этой ссылке. Если в драйвере применяется технология буферизации пакетов запросов (например, используется стандартный конвеер обработки Irp) в обработчике IRP_MJ_CLEANUP, как правило, выполняется процедура просмотра очереди запросов. При этом из очереди извлекаются пакеты запросов, в которых указатель FileObject совпадает с указателем FileObject переданным в пакете запроса IRP_MJ_CLEANUP. Здесь же в обработчике эти пакеты запросов завершаются с кодом

STATUS_CANCELLED.

DequeuedIrp->IoStatus.Status = STATUS_CANCELLED;

IoCompleteRequest(DequeuedIrp, IO_NO_INCREMENT);

Процедура извлечения Irp из очереди должна быть защищена spin lock’ом.

152

IRP_MJ_READ и IRP_MJ_WRITE. Это запросы чтения и записи данных. Инициатором таких запросов может быть пользовательское приложение. Например, вызов функции ReadFile приводит к формированию и пересылке драйверу устройства запроса

IRP_MJ_READ.

ReadFile(hDevice, &DataBuffer, DataSize, &cbRet, NULL);

IRP_MJ_DEVICE_CONTROL и IRP_MJ_INTERNAL_DEVICE_CONTROL. Эти IRP

используется для передачи команд устройству соответственно от пользовательских приложений и других устройств . Можно сказать, что запросы чтения и записи данных являются частными случаями этих типов запросов. С помощью этих IRP можно передать блок данных драйверу и возвратить блок данных вызывающей программе. IRP DEVICE_CONTROL и INTERNAL_DEVICE_CONTROL имеют дополнительный параметр IoControlCode, определяющий код команды. На пользовательском уровне для передачи команд устройству используется функция DeviceIoControl. В параметрах функции задаётся код команды, адреса и размеры входного и выходного буфера.

DeviceIoControl(hDevice, CTRL_ID1, &InputBuffer, sizeof(InputBuffer), &OutputBuffer, sizeof(OutputBuffer), &cbRet, NULL);

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

определяется разработчиком. Процедура обработки IRP должна включать диспетчер кодов команд.

NTSTATUS

IoControlHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

{

NTSTATUS

ntStatus = STATUS_SUCCESS;

PIO_STACK_LOCATION

irpStack;

ULONG

ioControlCode;

//инициализация полей в структуре Irp для возвращаемых значений

Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0;

//указатель на структуру StackLocation в Irp, где находятся

//коды функций и код команды

irpStack = IoGetCurrentIrpStackLocation(Irp); ioControlCode =

irpStack->Parameters.DeviceIoControl.IoControlCode; switch (ioControlCode) {

case CTRL_ID1:

...

break;

case CTRL_ID2:

...

break;

default:

ntStatus = STATUS_INVALID_PARAMETER;

break;

}

Irp->IoStatus.Status = ntStatus; IoCompleteRequest(Irp, IO_NO_INCREMENT); return ntStatus;

}

Формат кода команды DEVICE_CONTROL.

Универсальный ( во обе стороны) обмен данными с устройством организуется с помощью пакетов запросов IRP_MJ_DEVICE_CONTROL или

IRP_MJ_INTERNAL_DEVICE_CONTROL. Пакеты этих типов могут передавать различные команды и сопутствующие им параметры. Код команды помещается в структуру IO_STACK_LOCATION указатель на которую можно получить по указателю на IRP. Код команды это 32-х битовое значение, которое задаётся разработчиком. Код кроме номера команды, который так же называется номером функции, должен содержать дополнительную служебную информацию. Формат кода команды приводится на рисунке 5.

153

31

15

13

2

1

0

16

14

 

 

 

 

DeviceType

Access

Function

 

Method

Рисунок 5. Формат кода команды.

В поле DeviceType указывается тип устройства. Тип устройства задаётся при его создании функцией IoCreateDevice. Поле типа устройства содержит 16 разрядов. Значения от 0 до 32767 зарезервированы. Некоторые основные типы устройств описываются константами FILE_DEVICE_xxx, значения которых лежат в этом диапазоне. Например, FILE_DEVICE_CDROM, FILE_DEVICE_MOUSE и т.п.

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

Поле Access определяет допустимое направление передачи данных. Это поле может содержать один из флагов:

FILE_READ_ACCESS - чтение.

FILE_WRITE_ACCESS - запись.

FILE_ANY_ACCESS - чтение или запись.

В поле Function указывается уникальный номер команды (функции). По этому номеру обработчик IRP производит диспетчеризацию команды. Номера функций в диапазоне от 0 до 2047 зарезервированы для использования операционной системой.

Разработчики могут использовать оставшиеся значения от 2048 до 4095.

Поле Method определяет метод передачи данных. Подробнее различные методы передачи данных устройству будут рассмотрены в следующем параграфе.

Для того чтобы задать код команды можно использовать макроопределение CTL_CODE. Это макроопределение содержится в заголовочном файле DDK wdm.h

#define CTL_CODE( DeviceType, Function, Method, Access ) ( \

((DeviceType) << 16)|((Access) << 14)|((Function) <<

2)|(Method)\

)

 

 

Например:

 

 

#define CTRL_ID1

CTL_CODE(FILE_DEVICE_UNKNOWN,

1, \

METHOD_BUFFERED,

FILE_ANY_ACCESS)

 

Директива #define CTRL_ID1 помещается в заголовочный файл, содержащий описание интерфейса драйвера WDM. Этот файл, может быть например включён в проект пользовательского приложения, которое взаимодействует с устройством. Приложение использует функцию DeiceIoControl для передачи драйверу команды с кодом CTRL_ID1.

DeviceIoControl(hDevice, CTRL_ID1, ...);

Методы передачи данных.

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

IRP_MJ_DEVICE_CONTROL и IRP_MJ_INTERNAL_DEVICE_CONTROL. Менеджер ввода-вывода может организовать передачу буферов данных связанных с этими IRP тремя различными способами. Эти способы называются buffered (метод буферизации), direct (прямой метод) и neither(простой метод). Метод передачи данных в пакетах запросов чтения и записи, как правило, определяется сразу после создания объекта устройства. Структура объект устройства DEVICE_OBJECT содержит поле Flags в котором указывается метод передачи данных. Метод задаётся одной из констант

DO_BUFFERED_IO, DO_DIRECT_IO или их комбинацией. Например:

ntStatus = IoCreateDevice(... &DeviceObject);

if (NT_SUCCESS(ntStatus)) DeviceObject->Flags |= DO_DIRECT_IO;

154

Если ни одна из констант не указана, по умолчанию принимается метод neither.

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

METHOD_BUFFERED - метод буферизации

METHOD_IN_DIRECT - прямой метод при передаче данных драйверу METHOD_OUT_DIRECT - прямой метод при передаче данных от драйвера

METHOD_NEITHER - простой метод

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

Метод буферизации (BUFFERED).

В процессе исполнения операции чтения, Менеджер ввода-вывода создаёт системный буфер, размер которого совпадает с размером буфера, который вызывающая программа размещает для приёма данных. Этот последний буфер называется так же пользовательским буфером. Указатель на пользовательский буфер и его размер передаётся в параметрах функции чтения. Например, приложение Win32 может инициировать операцию чтения с помощью функции ReadFile.

Char DataBuffer[100];

ReadFile(hDevice, DataBuffer, 100, &cbRet, NULL);

В данном случае указатель на пользовательский буфер это виртуальный адрес массива DataBuffer. Размер пользовательского буфера равен размеру массива (100 байт). Менеджер ввода-вывода записывает адрес системного буфера в поле SystemBuffer структуры AssociatedIrp. Обработчик IRP в драйвере получает указатель на эту структуру через указатель на IRP.

Buffer = Irp->AssociatedIrp.SystemBuffer.

Адрес пользовательского буфера записывается в поле AssociatedIrp.UserBuffer. Обработчик IRP записывает данные? возвращаемые приложению в системный буфер. После завершения обработки IRP Менеджер ввода-вывода копирует содержимое системного буфера в пользовательский буфер. Затем управление возвращается вызывающей программе.

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

Операция передачи команды методом буферизации имеет ряд особенностей. В отличие от операций чтения и записи при передаче команды имеется возможность записать и прочитать данные устройства в одной операции. В следующем примере приложение Win32 передаёт устройству структуру MyStruct. Устройство возвращает данные приложению в буфер OutBuf.

struct {ULONG Param1, ULONG Param2} MyStruct; char OutBuf[10];

ULONG cbRet;

DeviceIoControl(hDevice, CTRL_ID1, &MyStruct, sizeof(MyStruct), OutBuf, 10, &cbRet, NULL);

Менеджер ввода-вывода создаёт системный буфер, размер которого равен размеру наибольшего из двух пользовательских буферов. В данном случае размер входного пользовательского буфера равен sizeof(MyStruct) = 8 байт. Выходной буфер имеет размер 10 байт. Поэтому размер системного буфера определяется размером

155

выходного буфера. Содержимое входного буфера (структура MyStruct) копируется в системный буфер. Указатель на системный буфер записывается в поле AssociatedIrp- >SystemBuffer. Обработчик IRP драйвера устройства должен записать возвращаемые приложению данные в системный буфер. Следует отметить, что в системном буфере входные и выходные данные пересекаются. Обработчик IRP должен считать из буфера входные данные, прежде чем записывать в него выходные.

Размеры входных и выходных буферов заносятся в блок параметров структуры IO_STACK_LOCATION. Блок параметров имеет разный формат для разных типов IRP. Ниже приводятся варианты определения размеров выходного буфера в операциях различных типов:

Операция чтения:

BufferSize = irpStack->Parameters.Read.Length;

Операция записи:

BufferSize = irpStack->Parameters.Write.Length;

Операция передачи команды:

- выходной буфер:

ОutputBufferSize = irpStack->Parameters.DeviceIoControl.OutputBufferLength;

- входной буфер:

InputBufferSize = irpStack->Parameters.DeviceIoControl.InputBufferLength;

Прямой метод передачи данных (DIRECT).

В операциях чтения и записи пользовательский буфер защёлкивается в памяти. Менеджер ввода вывода создаёт структуру, которая называется Список описателей памяти (Memory Descriptor List, сокращённо MDL). Адрес этой структуры помещается в поле MdlAddress IRP. Структура MDL содержит список номеров физических страниц памяти, в которых располагается буфер. Для того чтобы получить системный виртуальный адрес буфера обработчик IRP должен использовать функцию MmGetSystemAddressForMdl. Размер буфера определяется с помощью функции

MmGetMdlByteCount.

BufferAddr = MmGetSystemAddressForMdl(Irp->MdlAddress);

BufferSize = MmGetMdlByteCount(Irp->MdlAddress);

Поля SystemBuffer и UserBuffer в операциях прямого чтения и записи не используются.

При передаче команды, в случае если код команды содержит флаги

METHOD_IN_DIRECT или METHOD_OUT_DIRECT, Менеджер ввода-вывода создаёт системный буфер защёлкивает его в памяти, копирует содержимое входного пользовательского буфера в системный буфер и записывает адрес системного буфера в поле SystemBuffer. Выходной пользовательский буфер защёлкивается в памяти. Для этого буфера создаётся структура MDL, указатель на которую заносится в поле MdlAddress. Таким образом, адресация к входным данным производится через системный буфер, а выходные данные доступны напрямую.

Простой метод передачи данных (NEITHER).

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

IO_STACK_LOCATION.

InBuffer = Parameters.DeviceIoControl.Type3InputBuffer;

156

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

Уровни приоритетов.

Отдельные фрагменты кода драйвера могут иметь различный приоритет исполнения. Система поддерживает несколько уровней приоритетов, которые называются уровнями запросов прерываний (Interrupt Request Levels, сокращённо IRQLs). Обработчики пакетов запросов и другие процедуры драйвера, асинхронно вызываемые системой, имеют жёстко определённые приоритеты. Самый низкий приоритет имеет код расположенный на уровне PASSIVE_LEVEL. На этом уровне обрабатывается большинство пакетов запросов, например, пакеты запросов чтения, записи и команд. Для всех стандартных обработчиков IRP в документации DDK указывается уровень приоритета, с которым они вызываются. Уровень приоритета обработчиков аппаратных прерываний определяется номером прерывания. Эти уровни приоритета поддерживаются аппаратно контроллером прерываний. Уровень приоритета ограничивает набор функций системного программного интерфейса, которые могут быть вызваны из различных фрагментов кода драйвера. Некоторые функции разрешено вызывать на уровне приоритета не выше или не ниже заданного. Некоторые функции допускается вызывать, только на определённом уровне приоритета или в заданном диапазоне уровней приоритетов. Эта информация приводится в документации DDK в описании каждой функции. Уровень приоритета кода можно изменить в процессе исполнения. Для повышения уровня приоритета используется функции KeRaiseIrql, а для возврата к исходному уровню функция KeLowerIrql. Эта пара функций используется для временного повышения IRQL, например, для выполнения процедур, критичных ко времени исполнения.

KIRQL OldIrql;

//уровень приоритета PASSIVE_LEVEL

KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);

//уровень приоритета DISPATCH_LEVEL

KeLowerIrql(DISPATCH_LEVEL, &OldIrql);

//уровень приоритета PASSIVE_LEVEL

Текущий уровень приоритета возвращает функция KeGetCurrentIrql.

Поддержка механизма Plug and Play в драйверах WDM.

В предыдущих параграфах уже обсуждались некоторые элементы архитектуры драйвера WDM предназначенные для упрощения обслуживания устройств разработанных по технологии Plug and Play. Рассмотрим этот важный вопрос более детально. Аппаратные устройства размещаются на нескольких системных шинах. Каждая шина имеет свой собственный драйвер шины который выполняет операции общие для всех устройств подключённых к шине. В том числе драйвер шины отвечает за обнаружение устройств на шине. Кроме этого каждое устройство так же имеет свой собственный драйвер который выполняет специфические для данного устройства функции. Схема подключения аппаратуры имеет древовидную структуру фрагмент которой приводится на рисунке 6.

157

Контроллер PCI

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Контроллер USB

 

 

 

Контроллер SCSI

 

 

Контроллер ISA

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Камера

Джойстик

Диск 1

Звуковая карта

Рисунок 6. Пример схемы подключения аппаратных устройств.

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

 

 

 

 

 

Функциональный объект

Драйвер устройства

 

устройства

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Физический объект

Драйвер шины

устройства

 

Рисунок 7. Схема подключения объектов устройств

Дополнительно к этим типам объектов могут быть созданы объекты фильтров, которые участвуют в процессе обработки запросов ввода-вывода направляемых устройству. Все объекты относящиеся к одному устройству выстраиваются в цепочку при обработке IRP (рисунок 2). Физический объект располагается на самом нижнем уровне. Он получает Irp в последнюю очередь. Объекты фильтров могут располагаются на разных уровнях в зависимости от их назначения. Далее речь пойдет о том, как элементы технологии Plug and Play поддерживаются в драйверах устройств.

Операциями автоматической идентификации устройств управляет Менеджер PnP. Процедура создания функционального объекта устройства переносится в обработчик AddDevice. Этот обработчик вызывается Менеджером PnP при обнаружении аппаратного устройства. Менеджер PnP получает информацию о том, какой именно драйвер обслуживает устройство из реестра. Собственно процедура поиска аппаратного устройства подключённого к шине производится драйвером шины. Драйвер шины создаёт физический объект для обнаруженного устройства. Указатель на этот объект передаётся в параметрах функции AddDevice драйверу устройства. В обработчике AddDevice драйвер устройства создаёт функциональный объект и помещает его на вершину так называемого стека объектов устройств. До этой операции в стеке верхним элементом является физический объект устройства созданный драйвером шины к которой это устройство подключено. В общем случае шина к которой подключается устройство может в свою очередь подключаться к другой шине. Таким образом в стеке может располагаться несколько физических объектов. Однако в простейшем случае стек состоит всего из двух объектов - физического и функционального. Причём функциональный объект располагается на вершине стека. Пакет запроса ввода-вывода переданный функциональному объекту устройства может

158

быть после обработки в драйвере устройства передан физическому объекту драйвера шины. Каждый драйвер в стеке выполняет свою часть обработки пакета запроса. Устройство может завершить обработку запроса или передать запрос устройству расположенному ниже в стеке.

Функциональный объект устройства выполненного по технологии Plug and Play должен обрабатывать пакеты запросов ввода вывода IRP_MJ_PNP (MajorFunction). Обработчик пакетов PNP устанавливается в точке входа драйвера DriverEntry.

DriverObject->MajorFunction[IRP_MJ_PNP] = PnpHandler;

Обработчик имеет стандартный интерфейс:

NTSTATUS PnpHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

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

Пакеты запросов PNP формирует и передаёт драйверу устройства Менеджер PnP. Существует несколько типов запросов PNP. Тип запроса PNP определяется младшим кодом функции MinorFunction.

Пакет запроса IRP_MN_START_DEVICE передаётся драйверу при начальной установке или изменении аппаратной конфигурации устройства. В пакете запроса передаются сведения о конфигурации. Например, номер прерывания или диапазон адресов памяти выделенный устройству. Как правило, используется следующий сценарий:

Драйвер шины обнаруживает устройство, считывает его аппаратные установки и создаёт физический объект устройства.

Вызывается функция AddDevice драйвера устройства, в которой создаётся функциональный объект устройства.

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

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

Менеджер конфигурации формирует пакет запроса IRP_MJ_PNP:IRP_MN_START_DEVICE. В пакет запроса заносится информация о аппаратных настройках. Этот пакет запроса передаётся драйверу устройства.

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

Менеджер PnP передаёт драйверу пакет запроса IRP_MN_STOP_DEVICE, в случае если требуется перенастройка устройства. В обработчике этого Irp освобождаются ресурсы захваченные в обработчике IRP_MN_START_DEVICE. Например, снимается обработчик прерывания.

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

Перед передачей пакетов запросов IRP_MN_STOP_DEVICE и IRP_MN_REMOVE_DEVICE Менеджер PnP посылает драйверу запросы о возможности остановки/удаления устройства, соответственно IRP_MN_QUERY_STOP_DEVICE и IRP_MN_QUERY_REMOVE_DEVICE. Если обработчики этих запросов возвращают Status = STATUS_SUCCESS процедура остановки/удаления продолжается, иначе Менеджер PnP прекращает операцию и посылает драйверу сообщения

IRP_CANCEL_STOP_DEVICE или IPR_CANCEL_REMOVE_DEVICE.

Схема механизма PnP и основные типы запросов PnP приведены на рисунке 8.

159

Устройство физически установлено

Устройство обнаружено драйвером шины

Драйвер устройства загружен Менеджером конфигурации

 

 

 

 

Устройство удалено

 

 

 

Устройство остановлено

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IRP_MN_REMOVE

 

IRP_MN_REMOVE

 

IRP_MN_START_D

 

IRP_MN_STOP_DE

 

_DEVICE

 

_DEVICE

 

EVICE

 

 

VICE

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Устройство готово к удалению

 

 

 

Устройство готово к остановке

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IRP_MN_QUERY_

IRP_MN_CANCE

 

 

IRP_MN_QUERY_

 

IRP_MN_CANCE

 

 

 

 

REMOVE_DEVICE

L_REMOVE_DE

 

 

STOP_DEVICE

 

L_STOP_DEVICE

 

 

 

 

 

 

 

VICE

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Устройство работает

Менеджер PnP вызывает

Менеджер PnP выделяет устройству

DriverEntry драйвера устройства

системные ресурсы и посылает

 

IRP_MN_START_DEVICE

Драйвер устройства

 

 

 

 

 

 

выполнил операции

 

 

Создан функциональный объект

 

 

инициализации

Менеджер PnP вызывает

устройства

 

AddDevice

 

 

 

 

 

Рисунок 6. Схема механизма PnP.

Пакеты запросов PnP и стек объектов устройств.

Менеджер PnP посылает пакеты запросов PnP функциональному объекту устройства, который реализован в драйвере устройства. Обработчик драйвера устройства должен передать эти пакеты запросов физическому объекту устройства расположенному ниже в стеке объектов. Указатель на ниже расположенный объект устройства, как правило, сохраняется в расширении функционального объекта устройства в процедуре AddDevice. Указатель на физический объект передаётся в параметрах этой функции. Функциональный объект помешается на вершину стека объектов устройств. Для этого используется функция IoAttachDeviceToDeviceStack. Функция помещает объект на вершину стека и возвращает указатель на объект который был на вершине стека до вызова. Как правило им является физический объект устройства (pdo == IoAttachDeviceToDeviceStack(DeviceObject, pdo));

NTSTATUS AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo)

{

NTSTATUS status; PDEVICE_OBJECT DeviceObject;

PDEVICE_EXTENSION DeviceExtension;

status = IoCreateDevice(..., &DeviceObject);

dext = DeviceObject->DeviceExtension;

//сохраняем в расширении указатель на объект устройства расположенный //ниже в стеке

dext->NextDevice = IoAttachDeviceToDeviceStack(DeviceObject, pdo);

...

return status;

}

160