Основы разработки прикладных виртуальных драйверов
Часть 9. Аппаратные прерывания и передача данных в 32-разрядных приложениях Windows
Введем в рассмотренную в предыдущей статье программу средства передачи данных из обработчика аппаратных прерываний в приложение. Казалось бы, если заранее с помощью вызова DeviceIoControl() передать драйверу линейный адрес буфера вывода в программу, в обработчике прерываний можно записать в этот буфер любые данные, в частности результаты измерений. Однако обработчик аппаратных прерываний активизируется асинхронно, что накладывает серьезные ограничения на его возможности. Вообще, любая асинхронная программа выполняется, как говорят, в другом контексте, нежели прерванная ею синхронная. Линейные адреса полей данных или процедур приложения, полученные драйвером на синхронном уровне, недействительны в то время, когда драйвер выполняет асинхронную процедуру. Поэтому пересылка данных из обработчика прерываний в приложение, хотя и возможна, но требует выполнения некоторых дополнительных действий как в приложении, так и в драйвере. Рассмотрим сначала изменения, вносимые в приложение.
Предусмотрим среди глобальных данных приложения переменную Data, в которую драйвер сможет переслать результаты измерения:
short unsigned int Data=1234h;//Начальное значение для отладки
Для того чтобы драйвер мог обращаться к этой ячейке, необходимо переслать в драйвер ее адрес. Это удобно выполнить в функции инициализации InitCard(), включив указатель на беззнаковое короткое целое в передаваемую в драйвер структуру InBuffer и инициализировав этот дополнительный член структуры адресом переменной Data. Функция InitCard() примет теперь следующий вид:
void InitCard(){ struct { short unsigned int C0; short unsigned int C1; short unsigned int C2; short unsigned int* dptr; }InBuffer; InBuffer.C0=20; //Канал 0 InBuffer.C1=50000; //Канал 1 InBuffer.C2=10000; //Канал 2 InBuffer.dptr=&Data; //Адрес переменной Data //Вызовем процедуру драйвера DIOC_INIT DeviceIoControl(hVMyD,DIOC_INIT,&InBuffer,8,NULL,0,&cbRet,0); }
Обратите внимание на изменение параметра, определяющего размер входного буфера: добавление в структуру InBuffer адреса переменной Data увеличило его длину до 8 байт.
Для чтения значения переменной Data после передачи в нее из драйвера результата измерений предусмотрим в меню пункт «Чтение данных». Для этого в файл заголовков следует включить определение идентификатора этого пункта меню:
#define MI_DATA 103
а в файл ресурсов – предложение
MENUITEM "Чтение данных", MI_DATA
Функция обработки сообщений от меню дополнится анализом на выбор пункта «Чтение данных» с идентификатором MI_DATA и примет следующий вид:
void OnCommand(HWND hwnd,int id,HWND,UINT){ switch(id) { case MI_START: InitCard(); break; case MI_ADDR: GetAddr(); break; case MI_DATA: ReadData(); break; case MI_EXIT: DestroyWindow(hwnd); } }
Новая функция ReadData() преобразует значение переменной Data в символьную форму и выводит его в окно сообщения в виде шестнадцатеричного и десятичного числа (такой вывод может пригодиться в процессе отладки драйвера, когда контролировать результат удобнее в шестнадцатеричной форме):
void ReadData(){ char txt[80]; wsprintf(txt,"Результат = %Xh = %u",Data,Data); MessageBox(NULL,txt,"Info",MB_ICONINFORMATION); }
Как мы увидим позже, драйвер для получения доступа к переменной приложения должен будет выполнить блокирование памяти, принадлежащей этой переменной. Перед завершением программы эту память необходимо разблокировать, для чего следует вызвать соответствующую прикладную функцию драйвера. Этот вызов можно включить в процедуру обработки сообщения WM_DESTROY:
void OnDestroy(HWND){ //Вызовем драйвер для разблокирования заблокированной ранее памяти DeviceIoControl(hVMyD,DIOC_EXIT,NULL,0,NULL,0,&cbRet,0); PostQuitMessage(0); }
Вызов не требует передачи каких-либо параметров, поэтому и адреса, и размеры обоих буферов обмена информацией с драйвером имеют нулевые значения. Определение кода действия DIOC_EXIT должно быть включено в заголовочный файл (а также в программу драйвера):
#define DIOC_EXIT 3
Таким образом, изменения в приложении носят скорее организационный, чем содержательный характер. Фактически вся работа по получению доступа к переменной приложения ложится на драйвер.
Виртуальный драйвер с доступом к данным приложения из обработчика аппаратных прерываний
;Функции драйвера: ;0 Проверка версии ;1 VMyD_Init — Инициализация и пуск платы; блокирование физической памяти ;2 GetAddr — Передача в приложение базового адреса драйвера ;3 VMyD_Exit — Разблокирование физической памяти DIOC_INIT=1 DIOC_ADDR=2 DIOC_EXIT=3 .386p .XLIST include vmm.inc include vwin32.inc include vpicd.inc .LIST Declare_Virtual_Device VMyD,1,0,VMyD_Control,8000h,Undefined_Init_Order ;======================= VxD_REAL_INIT_SEG ... ;Инициализация реального режима VxD_REAL_INIT_ENDS ;====================== VxD_DATA_SEG result dw 0 ;Результат измерений IRQ_Handle dd 0 ;Дескриптор виртуального прерывания VMyD_Int13_Desc label dword;32-битовый адрес следующей далее структуры VPICD_IRQ_Descriptor <5,,OFFSET32 VMyD_Int_13> lpdata dd 0 ;Адрес переменной Data приложения ndata dd 0 ;Число блокируемых страниц VxD_DATA_ENDS ;====================== VxD_CODE_SEG BeginProc VMyD_Control Control_Dispatch W32_DeviceIOControl,IOControl Control_Dispatch Device_Init, VMyD_Device_Init clc ret EndProc VMyD_Control ;----------------------- BeginProc VMyD_Device_Init mov EDI,OFFSET32 VMyD_Int13_Desc VxDCall VPICD_Virtualize_IRQ mov IRQ_Handle,EAX clc ret EndProc VMyD_Device_Init ;----------------------- BeginProc IOControl cmp dword ptr [ESI.dwIOControlCode],DIOC_GETVERSION je GetVer cmp dword ptr [ESI.dwIOControlCode],DIOC_INIT je VMyD_Init cmp dword ptr [ESI.dwIOControlCode],DIOC_ADDR je GetAddr cmp dword ptr [ESI.dwIOControlCode],DIOC_EXIT je VMyD_Exit clc ret EndProc IOControl ;-------------------------- BeginProc GetVer xor EAX,EAX clc ret EndProc GetVer ;-------------------------- BeginProc GetAddr mov EDI,dword ptr [ESI.lpvOutBuffer] mov [EDI],offset32 VMyD_Control clc ret EndProc GetAddr ;----------------------------- BeginProc VMyD_Init ;Общий сброс платы mov DX,30Ch ;(1) in AL,DX ;(2) ;Получим адрес буфера приложения и заблокируем физическую память переменной Data mov EDI,dword ptr [ESI.lpvInBuffer];(3)Адрес буфера приложения mov EAX,[EDI+6] ;(4)Получим адрес переменной Data приложения and EAX,0FFFh ;(5)Выделим младшие 12 бит — смещение на странице mov EBX,EAX ;(6)Сохраним его в EBX mov EAX,[EDI+6] ;(7)Получим адрес переменной Data приложения mov ECX,EAX ;(8)Занесем его и в ECX shr ECX,12 ;(9)Полный номер начальной физической страницы add EAX,1 ;(10)Прибавим длину данных — 1 к адресу переменной shr EAX,12 ;(11)Полный номер конечной физической страницы с данными sub EAX,ECX ;(12)Число страниц, требуемых для данных, — 1 inc EAX ;(13)Число страниц, требуемых для данных mov ndata,EAX ;(14)Сохраним его push PAGEMAPGLOBAL;(15)Вид блокирования push EAX ;(16)Число блокируемых страниц push ECX ;(17)Полный номер начальной блокируемой страницы VMMCall _LinPageLock;(18)EAX=новый линейный адрес страницы add EAX,EBX ;(19)Новый линейный адрес данного mov lpdata,EAX ;(20)Сохраним его add ESP,12 ;(21)Восстановление стека ;Засылаем управляющие слова по каналам, сбрасываем счетчик, ;устанавливаем флаг S2 разрешения счета ;... Эта часть программы драйвера полностью повторяет ; соответствующий фрагмент предыдущей статьи ;Размаскируем прерывания mov EAX,IRQ_Handle VxDCall VPICD_Physically_Unmask clc ret EndProc VMyD_Init ;-------------------------- BeginProc VMyD_Exit ;Разблокируем данные приложения mov EAX,lpdata ;Возьмем новый линейный адрес начальной страницы shr EAX,12 ;Полный номер начальной страницы push PAGEMAPGLOBAL;Тип разблокирования push ndata ;Число разблокируемых страниц push EAX ;Полный номер начальной страницы VMMCall _LinPageUnLock add ESP,12 ;Восстановим стек clc ret EndProc VMyD_Exit ;--------------------------------- BeginProc VMyD_Int_13, High_Freq pushad ;Получим результат измерений mov DX,309h in AL,DX mov AH,AL dec DX in AL,DX mov result,AX ;Сохраним результат в памяти ;Сброс флагов готовности и разрешения счета mov DX,30Ah out DX,AL ;Выполним завершающие действия в PIC и выведем результат mov EAX,IRQ_Handle VxDCall VPICD_Phys_EOI VxDCall VPICD_Physically_Mask mov AX,result ;Результат измерений mov EDI,lpdata ;Передадим данное mov [EDI],AX ;в буфер приложения popad clc ret EndProc VMyD_Int_13 VxD_CODE_ENDS end VMyD_Real_Init
В начале текста драйвера определяется значение добавленного кода действия DIOC_EXIT. Соответственно в процедуру IOControl включены предложения проверки пришедшего из приложения кода действия на значение DIOC_EXIT и перехода в случае равенства на процедуру VMyD_Exit.
Сущность изменений, внесенных в программу драйвера, заключается в том, что в процедуре VMyD_Init помимо инициализации платы осуществляется блокирование физической памяти с теми данными, к которым нужно получить доступ, а в новой процедуре VMyD_Exit — разблокирование этой памяти. Блокирование осуществляется целыми страницами (4 Кбайт) с помощью функции VMM _LinPageLock; функция в случае своего успешного выполнения возвращает новый линейный адрес первой заблокированной страницы, посредством которого драйвер получает доступ к полям данных приложения.
Рассмотрим подготовку параметров для функции _LinPageLock в процедуре VMyD_Init нашего драйвера (для удобства ссылок предложения этой процедуры пронумерованы). В предложении 3 из структуры DIOCParams извлекается и помещается в регистр EDI адрес входного буфера. В предложении 4 линейный адрес интересующей нас переменной приложения Data забирается из входного буфера (где он располагается в байтах 6-7 от начала буфера) и помещается в регистр — EAX. Как известно, в линейном адресе биты 22-31 определяют номер элемента в каталоге страниц, то есть в сущности, номер таблицы страниц, биты 12-21 — номер страницы, а биты 0-11 — смещение адресуемого данного на странице (рис. 1).
Функция _LinPageLock требует в качестве параметра часть линейного адреса, расположенную в битах 12-31, которую можно условно рассматривать как полный номер физической страницы (не ее адрес!). В предложении 5 из линейного адреса выделяются его младшие 12 бит — смещение на логической и физической страницах, а в предложении 6 оно сохраняется для дальнейшего использования в регистре EBX. Далее формируются полные номера физических страниц с данными — начальной (в регистре ECX) и конечной (в регистре EAX). В предложении 7 в регистре EAX восстанавливается линейный адрес Data, а в следующем предложении он помещается еще и в ECX. Сдвигом вправо на 12 бит содержимое ECX преобразуется в полный номер (начальной) страницы. В предложении 9 к линейному адресу данного в регистре EAX прибавляется величина на 1 меньшая, чем полная длина данных (в нашем случае всего 1 байт), что дает линейный адрес последнего байта блокируемых данных. Эта величина также сдвигается вправо на 12 бит с образованием полного номера (конечной) страницы. Разность двух номеров страниц (предложение 12) после прибавления 1 (предложение 13) дает число страниц, подлежащих блокированию. Необходимость в столь сложной процедуре определения числа блокируемых страниц объясняется тем, что блокируемые данные (даже при небольшой длине) могут начинаться на одной физической странице, а заканчиваться на другой. Полученное число страниц, подлежащих блокированию, сохраняется в ячейке ndata с целью использования в процедуре разблокирования.
В предложениях 15-17 в стек проталкиваются параметры функции VMM _LinPageLock: константа PAGEMAPGLOBAL, используемая в случае блокирования страниц ради доступа к ним из другого контекста, то есть из асинхронной операции, число блокируемых страниц и полный номер начальной блокируемой страницы. Функция _LinPageLock в случае своего успешного выполнения возвращает в регистре EAX новый линейный адрес заблокированного участка памяти. Однако этот адрес характеризует начало страницы; наши данные располагались на странице с некоторым смещением, которое мы выделили и сохранили в регистре EBX. Теперь это смещение можно прибавить к линейному адресу начальной страницы, чтобы получить точный адрес начала данных (предложение 19). В двух последующих предложениях полученный адрес сохраняется в предусмотренной для этого ячейке lpdat и восстанавливается состояние указателя стека, каким оно было перед помещением в стек трех параметров функции _LinPageLock.
Далее в процедуре VMyD_Init, как и в предыдущем примере, таймер-счетчик инициализируется константами, полученными из приложения через входной буфер, и размаскируется наш уровень прерываний.
В обработчике прерываний VMyD_Int_13 выполняются те же действия, что и в предыдущем примере (получение из счетчика результата измерений, сброс флагов готовности и разрешения счета, посылка в контроллер прерываний команды EOI), после чего результат измерений передается в приложение по новому «адресу-псевдониму», сохраненному ранее в ячейке lpdata.
Как уже отмечалось ранее, заблокированную память необходимо разблокировать. Это действие выполняется при завершении приложения, когда в драйвер посылается запрос на операцию ввода-вывода с кодом действия DIOC_EXIT. В этом случае в драйвере вызывается процедура VMyD_Exit, в которой еще раз выполняется процедура определения полного номера начальной страницы и числа заблокированных страниц. Поскольку блокирование осуществляется целыми страницами, смещение данных нас в этой процедуре уже не интересует. При разблокировании номера страниц определяются исходя из полученного в операции блокирования адреса-псевдонима.
КомпьютерПресс 11'2001