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

Как уже отмечалось ранее, виртуальные драйверы служат прежде всего для виртуализации аппаратуры, то есть для предоставления одновременно выполняемым задачам возможности совместного использования устройств компьютера. Измерительная или управляющая аппаратура, подключаемая к компьютеру с целью создания автоматизированной установки, вряд ли будет эксплуатироваться в многозадачном режиме, однако использование для ее управления виртуального драйвера может заметно сократить программные издержки и уменьшить время отклика. Рассмотрим пример виртуального драйвера, обслуживающего прерывания от описанной в предыдущей статье интерфейсной платы таймера-счетчика. Очевидно, что в состав такого драйвера должен входить обработчик прерываний от платы. Функции этого обработчика определяет программист; в простейшем случае обработчик может просто прочитать данные из выходного регистра счетчика и замаскировать прерывания. Однако приложение при этом не узнает о завершении измерений; более естественно организовать вызов из обработчика прерываний драйвера некоторой функции приложения, в которую можно передать прочитанные данные. Фактически эта функция будет играть роль обработчика прерывания приложения, однако вызываться она будет не самим прерыванием, а обработчиком драйвера.

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

Поскольку обработчик прерываний приложения вызывается из VMM и после своего завершения опять передает управление в систему, его возможности ограниченны. В нем, в частности, недопустим вызов функции вывода информации - MessageBox() - в окно сообщения. Поэтому здесь, как и в предыдущем примере, возникает проблема оповещения главной функции WinMain() о приходе прерывания. Мы решим эту проблему следующим образом: обработчик прерывания приложения, получив управление, устанавливает флаг, который опрашивается главной функцией в каждом шаге цикла обработки сообщений. Главная функция, обнаружив установленное состояние флага, посылает в приложение сообщение пользователя, которое наравне с сообщениями Windows обслуживается циклом обработки сообщения. В функции обработки этого сообщения уже можно выполнять любые действия безо всяких ограничений.

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

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

#define STRICT  
#include <windows.h>
#include <windowsx.h>
#include <string.h>
//Определения констант
#define MI_START 100 //Константы, используемые
#define MI_EXIT 101 //для идентификации
#define MI_READ 102 //пунктов меню
/* void Cls_OnUser(HWND hwnd) */
#define HANDLE_WM_USER(hwnd, wParam, lParam, fn) \//Макрос для обработки
((fn)(hwnd), 0L) // сообщения WM_USER
//Прототипы функций
void Register(HINSTANCE); //Вспомогательные
void Create(HINSTANCE); //функции
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);//Оконная функция
void OnCommand(HWND,int,HWND,UINT);//Функции
void OnDestroy(HWND); //обработки сообщений Windows
void OnUser(HWND); //и пользователя
void InitCard(); //Функция инициализации платы (через драйвер)
void GetAPIEntry(); //Функция получения точки входа в драйвер
void isr(int,int); //Прототип обработчика прерывания приложения
//Глобальные переменные
char szClassName[]="MainWindow";//Имя класса главного окна
char szTitle[]="Управление";//Заголовок окна
HWND hMainWnd; //Дескриптор главного окна
FARPROC VxDEntry; //Адрес точки входа в драйвер
int unsigned data; //Данное из драйвера
char request; //Флаг окончания измерений
//Главная функция WinMain
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int){
MSG msg; //Структура для приема сообщений
Register(hInstance); //Регистрация класса главного окна
Create(hInstance); //Создание и показ главного окна
/*Более сложный цикл обработки сообщений, в который включены
анализ флага request завершения измерений и посылка приложению
сообщения WM_USER при установке этого флага*/
do{
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)){
if (msg.message == WM_QUIT)return msg.wParam;
DispatchMessage(&msg);
}//Конец if(PeekMessage())
if(request){
request=0; //Сброс флага
PostMessage(hMainWnd,WM_USER,0,0);/*Поставить в очередь
сообщение WM_USER без параметров*/
}//Конец if(request)
}while(1);//Конец do
}//Конец WinMain
//Функция Register регистрации класса окна
void Register(HINSTANCE hInst){
WNDCLASS wc;
memset(&wc,0,sizeof(wc));
wc.lpszClassName=szClassName;
wc.hInstance=hInst;
wc.lpfnWndProc=WndProc;
wc.lpszMenuName="Main";
wc.hCursor=LoadCursor(NULL,IDC_ARROW);
wc.hIcon=LoadIcon(NULL,IDI_APPLICATION);
wc.hbrBackground=GetStockBrush(WHITE_BRUSH);
RegisterClass(&wc);
}
//Функция Create создания и показа окна
void Create(HINSTANCE hInst){
hMainWnd=CreateWindow(szClassName,szTitle,WS_OVERLAPPEDWINDOW,
10,10,200,100,HWND_DESKTOP,NULL,hInst,NULL);
ShowWindow(hMainWnd,SW_SHOWNORMAL);
}
//Оконная функция WndProc главного окна
LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam){
switch(msg){
HANDLE_MSG(hwnd,WM_COMMAND,OnCommand);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
HANDLE_MSG(hwnd,WM_USER,OnUser);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
//Функция OnCommand обработки сообщений WM_COMMAND от пунктов меню
void OnCommand(HWND hwnd,int id,HWND,UINT){
switch(id){
case MI_START://Инициализация платы
InitCard();
break;
case MI_READ:
char txt [80];
wsprintf(txt,"Накоплено %d событий",data);
MessageBox(hMainWnd,txt,"Данные",MB_ICONINFORMATION);
break;
case MI_EXIT://Завершить приложение
DestroyWindow(hwnd);
}
}
//Функция OnUser обработки сообщения WM_USER
void OnUser(HWND){
MessageBox(hMainWnd,"Измерения окончены","Контроль",MB_ICONINFORMATION);
}
//Функция OnDestroy обработки сообщения WM_DESTROY
void OnDestroy(HWND){
PostQuitMessage(0);
}
//Функция InitCard инициализации (через драйвер) платы
void InitCard (){
GetAPIEntry(); //Получение адреса точки входа в драйвер
_AX=_DS; //Передача в драйвер DS
_BX=55; //Канал 0
_CX=20000; //Канал 1; BX*CX=Длительность интервала
_DX=1000; //Канал 2, внутренний генератор
_SI=OFFSETOF(isr); //Смещение обработчика прерываний
_DI=SELECTOROF(isr); //Селектор обработчика прерываний
VxDEntry(); //Вызов драйвера
}
//Функция GetAPIEntry получения адреса точки входа в драйвер
void GetAPIEntry(){
asm{
mov AX,0x1684
mov BX,0x8000
int 0x2F
mov word ptr VxDEntry,DI
mov word ptr VxDEntry+2,ES
}
}
//Обработчик прерываний приложения. Вызывается VMM и возвращает управление в VMM
void isr(int segment,int dt){
_DS=segment; //Инициализируем DS селектором, полученным из драйвера
request++; //Поставим запрос на сообщение
data=dt; //Получим из драйвера аппаратные данные
}
{mospagebreak} 

Главная функция WinMain() выполняет три характерные для нее процедуры: регистрацию класса главного окна, создание и показ главного окна, цикл обработки сообщений. В цикле обработки сообщений имеется два принципиальных отличия от примера предыдущей части статьи: в каждом шаге цикла проверяется состояние флага завершения измерений request, и если флаг оказывается установленным, то вызывается функция Windows PostMessage(), которая ставит в очередь сообщений приведенного выше приложения наше сообщение с кодом WM_USER. Для того чтобы в цикл обработки сообщений включить проверку флага, пришлось заменить в нем функцию GetMessage() на функцию PeekMessage(), которая, в отличие от GetMessage(), при отсутствии сообщений в очереди возвращает управление в программу, что и дает возможность включить в цикл дополнительные действия. Однако PeekMessage() не анализирует сообщение WM_QUIT о завершении программы, поэтому <вылавливание> этого сообщения (и завершение программы оператором return 0 в случае его прихода) приходится выполнять вручную. Конструкция:

do{  
...
}while(1)

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

В оконной функции WndProc() фиксируется приход трех сообщений: WM_COMMAND от пунктов меню, WM_DESTROY от команд завершения приложения и WM_USER, свидетельствующего об окончании измерений. Поскольку для сообщения WM_USER в файле windowsx.h отсутствует макрос HANDLE_WM_USER, его пришлось определить в начале программы с помощью оператора #define, построив макрорасширение по аналогии с каким-либо из макросов вида HANDLE_сообщение из файла windowsx.h, хотя бы с макросом HANDLE_WM_DESTROY.

Фрагмент программы, выполняемый при выборе пользователем пункта меню <Пуск>, содержит лишь вызов функции InitCard(). В ней вызовом вложенной функции GetAPIEntry определяется адрес API-процедуры драйвера, а затем, после заполнения ряда регистров параметрами, передаваемыми в драйвер, вызывается эта процедура. В драйвер передаются следующие параметры: селектор сегмента данных приложения, три константы для инициализации платы, а также селектор и смещение обработчика прерываний приложения isr(). Передача в драйвер содержимого сегментного регистра DS (селектора сегмента данных) необходима потому, что при вызове драйвером (точнее, VMM) нашей функции isr() не восстанавливается операционная среда приложения, в частности регистр DS не указывает на поля данных приложения, которые в результате оказываются недоступными. Передав в драйвер содержимое DS, мы сможем вернуть его назад вместе с другими данными, передаваемыми из драйвера в приложение, восстановив тем самым адресуемость данных.

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

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

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

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

Текст виртуального драйвера, обрабатывающего аппаратные прерывания

;При вызове AX=DS приложения, BX=C0, CX=C1, DX=C2, DI=селектор isr ,SI=смещение isr  
.386p
.XLIST
include vmm.inc
include vpicd.inc
.LIST
Declare_Virtual_Device VMyD,1,0,VMyD_Control,8000h, \
Undefined_Init_Order,,API_Handler
;=======================
VxD_REAL_INIT_SEG
BeginProc VMyD_Real_Init
;Текст процедуры инициализации реального режима (см. часть 2 этого цикла)
EndProc VMyD_Real_Init
VxD_REAL_INIT_ENDS
;======================
VxD_DATA_SEG
Data dw 0 ;Ячейка для результата измерений
DSseg dw 0 ;Ячейка для хранения селектора приложения
Segment_Callback dw 0 ;Селектор функции isr приложения
Offset_Callback dd 0 ;Смещение функции isr приложения
IRQ_Handle dd 0 ;Дескриптор виртуального прерывания
VMyD_Int13_Desc label dword;32-битовый адрес следующей далее структуры
VPICD_IRQ_Descriptor <5,,OFFSET32 VMyD_Int_13>;Структура с информацией
;о виртуализованном прерывании
VxD_DATA_ENDS
;======================
VxD_CODE_SEG
BeginProc VMyD_Control
;Включим в состав драйвера процедуру обработки системного сообщения
;Device_Init об инициализации драйвера
Control_Dispatch Device_Init, VMyD_Device_Init
clc
ret
EndProc VMyD_Control
;----------------------
;Процедура, вызываемая при инициализации драйвера системой
BeginProc VMyD_Device_Init
mov EDI,OFFSET32 VMyD_Int13_Desc;Адрес структуры VPICD_IRQ_Descriptor
VxDCall VPICD_Virtualize_IRQ;Виртуализация устройства
mov IRQ_Handle,EAX;Сохраним дескриптор виртуального IRQ
clc
ret
EndProc VMyD_Device_Init
;-------------------------
;API-процедура, вызываемая из приложения
BeginProc API_Handler
;Получим параметры из приложения
push [EBP.Client_DI]
pop Segment_Callback
push [EBP.Client_AX];DS
pop DSseg
movzx ESI,[EBP.Client_SI]
mov Offset_Callback,ESI
;Общий сброс
mov DX,30Ch
in AL,DX
;Размаскируем уровень 5 в физическом контроллере прерываний
mov EAX,IRQ_Handle
VxDCall VPICD_Physically_Unmask
;Засылаем управляющие слова по каналам
mov DX,303h
mov AL,36h ;Канал 0
out DX,AL
mov AL,70h ;Канал 1
out DX,AL
mov AL,0B6h ;Канал 2
out DX,AL
;Засылаем константы в каналы
mov DX,300h ;Канал 0
mov AX,[EBP.Client_BX];Константа С0
out DX,AL ;Младший байт частоты
xchg AL,AH
out DX,AL ;Старший байт частоты
mov DX,301h ;Канал 1
mov AX,[EBP.Client_CX];Константа С1
out DX,AL ;Младший байт интервала
xchg AL,AH
out DX,AL ;Старший байт интервала
mov DX,302h ;Канал 2
mov AX,[EBP.Client_DX];Константа С2
out DX,AL ;Младший байт частоты
xchg AH,AL
out DX,AL ;Старший байт частоты
;Установим флаг S2 разрешения счета
mov DX,30Bh
in AL,DX
ret
EndProc API_Handler
;-------------------------
;Процедура обработки аппаратного прерывания IRQ5 (вектор 13)
BeginProc VMyD_Int_13, High_Freq
;Получим результат измерений из выходного регистра счетчика
mov DX,309h ;Порт старшего байта
in AL,DX ;Получим старший байт
mov AH,AL ;Отправим его в AH
dec DX ;DX=308h
in AL,DX ;Получим младший байт
mov Data,AX ;Весь результат в Data
;Выполним завершающие действия в PIC и вызовем функцию приложения
mov EAX,IRQ_Handle
VxDCall VPICD_Phys_EOI;EOI в физический контроллер прерываний
VxDCall VPICD_Physically_Mask;Маскируем наш уровень
;Перейдем на синхронный уровень
mov EDX,0 ;Данные отсутствуют
mov ESI,OFFSET32 Reflect_Int;Адрес синхронной процедуры
VMMCall Call_VM_Event;Установим запрос на ее вызов из VMM
clc
ret
EndProc VMyD_Int_13
;-------------------------
;Процедура уровня отложенных прерываний
BeginProc Reflect_Int
Push_Client_State ;Выделим место на стеке для регистров клиента
VMMCall Begin_Nest_Exec;Начнем вложенный блок выполнения
mov AX,Data ;Отправим данное
VMMCall Simulate_Push;в стек клиента
mov AX,DSseg ;Отправим полученный ранее DS
VMMCall Simulate_Push;в стек клиента
mov CX,Segment_Callback;Зашлем полученный ранее адрес функции isr
mov EDX,Offset_Callback;в CS:IP клиента, чтобы после возврата из VMM
VMMCall Simulate_Far_Call;в виртуальную машину вызвалась эта функция
VMMCall Resume_Exec;Возврат из VMM в текущую виртуальную машину
VMMCall End_Nest_Exec;Завершим вложенный блок выполнения
Pop_Client_State ;Освободим место на стеке для регистров клиента
clc
ret
EndProc Reflect_Int
VxD_CODE_ENDS
end VMyD_Real_Init
{mospagebreak} 

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

Макрос VPICD_IRQ_Descriptor позволяет описать в полях данных структуру с информацией о виртуализованном прерывании. Обязательными элементами этой структуры являются номер уровня виртуализуемого прерывания и адрес обработчика аппаратного прерывания (VMyD_Int_13 в нашем случае), включаемый в состав драйвера. Для того чтобы макросы виртуального контроллера прерываний были доступны ассемблеру, к программе необходимо подключить (оператором include) файл VPICD.INC.

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

Процедура VMyD_Device_Init содержит вызов функции виртуального контроллера прерываний (VPICD) VPICD_Virtualize_IRQ. Эта функция осуществляет виртуализацию указанного уровня прерываний и возвращает дескриптор виртуального прерывания, который сохраняется нами в ячейке IRQ_Handle с целью дальнейшего использования. Функция VPICD_Virtualize_IRQ фактически устанавливает в системе наш обработчик прерываний, имя которого включено нами в структуру VPICD_IRQ_Descriptor. Начиная с этого момента аппаратные прерывания IRQ5 будут вызывать по умолчанию, не обработчик этого уровня, находящийся в VPICD, а наш обработчик. Правда, для этого надо размаскировать уровень 5 в контроллере прерываний, чего мы еще не сделали.

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

Выполнив команду общего сброса программируемой платы, следует размаскировать прерывания в физическом (не виртуальном) контроллере прерываний. Эта операция осуществляется вызовом функции виртуального контроллера VPICD_Physically_Unmask с указанием ей в качестве параметра в регистре EAX дескриптора виртуального прерывания. Далее выполняется уже рассмотренная в предыдущей части статьи процедура инициализации платы (причем значения констант С0, С1 и С2 извлекаются из структуры клиента). После завершения API-процедуры управление возвращается в приложение до поступления аппаратного прерывания.

Аппаратное прерывание виртуализованного нами уровня через дескриптор таблицы прерываний IDT с номером 55h активизирует обработчик прерываний, входящий в состав VPICD, который, выполнив некоторые подготовительные действия (в частности, сформировав на стеке уровня 0 структуру клиента), передает управление непосредственно нашему драйверу, а именно процедуре обработки аппаратного прерывания VMyD_Int_13. Системные издержки этого перехода составляют около 40 команд процессора, то есть время от момента поступления прерывания до выполнения первой команды нашего обработчика составит на компьютере среднего быстродействия 10...15 мкс.

В процедуре VMyD_Int_13 после выполнения содержательной части (в нашем случае - чтения и запоминания результата измерений) необходимо послать в контроллер прерываний команду EOI, как это полагается делать в конце любого обработчика аппаратного прерывания. Для виртуализованного прерывания это действие выполняется с помощью функции VPICD_Phys_EOI, единственным параметром которой является дескриптор прерывания, сохраненный нами в ячейке IRQ_Handle. Последней операцией является вызов функции VPICD_Physically_Mask, с помощью которой маскируется уровень 5 в физическом контроллере прерываний.

Следует отметить, что названия функций VPICD могут быть обманчивыми. Функция VPICD_Phys_EOI в действительности не разблокирует контроллер прерываний, а размаскирует наш уровень в регистре маски физического контроллера (чего мы, между прочим, не заказывали!). Что же касается команды EOI, то она была послана в контроллер по ходу выполнения фрагмента VPICD еще до перехода на наш обработчик (упомянутые выше 40 команд). Тем не менее вызов функции VPICD_Phys_EOI в конце обработчика прерываний обязателен. Если ею пренебречь, то операционная система будет вести себя точно так же, как если бы в контроллер не была послана команда EOI: первое прерывание обрабатывается нормально, но все последующие - блокируются. Так происходит потому, что при отсутствии вызова функции VPICD_Phys_EOI нарушается работа функции VPICD_Physically_Unmask, которая выполняется у нас на этапе инициализации. Эта функция, выполнив анализ системных полей и обнаружив, что предыдущее прерывание не завершилось вызовом VPICD_Phys_EOI, обходит те свои строки, в которых в порте 21h устанавливается 0 бит нашего уровня прерываний. В результате этот уровень остается замаскированным и прерывания не проходят.

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

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

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

В понятие <событие> входит не только callback-функция, но и набор условий, при которых она может быть вызвана или которыми должен сопровождаться ее вызов. Так, например, можно указать, что callback-функцию можно вызвать только вне критической секции или что вызов callback-функции должен сопровождаться повышением приоритета ее выполнения. Кроме того, при установке события можно определить данное (двойное слово), которое будет передано в callback-функцию при ее вызове. В составе VMM имеется целый ряд функций установки событий, различающихся условиями их обработки, например Call_When_Idle, Call_When_Not_Critical, Call_Restricted_Event, Schedule_Global_Event, Schedule_Thread_Event и др. Необходимо подчеркнуть, что момент фактического вызова callback-функции заранее определить невозможно. Она может быть вызвана немедленно либо спустя некоторое время, когда будут удовлетворены поставленные условия.

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

Команда ret, которой заканчивается обработчик прерываний виртуального драйвера, передает управление VMM, который в удобный для него момент времени вызывает функцию Reflect_Int (реально до вызова может пройти 50...200 мкс). В этой функции вызовом Push_Client_State исходная структура клиента еще раз сохраняется на стеке уровня 0, после чего функцией Begin_Nest_Exec открывается блок вложенного выполнения. Внутри этого блока можно, во-первых, организовать переход на определенную функцию приложения, а во-вторых, создать условия для передачи ей требуемых параметров. Передача параметров осуществляется в соответствии с установленным интерфейсом используемого языка программирования. Поскольку наше приложение написано на языке Си, для его функций действуют правила этого языка: параметры передаются функции через стек, причем расположение параметров в стеке должно соответствовать их перечислению в прототипе и заголовке функции, то есть в глубине стека должен находиться последний параметр, а на вершине стека - первый (в функциях типа <Паскаль>, в частности во всех системных функциях Windows, действует обратный порядок передачи параметров).

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

Следующая операция - подготовка вызова требуемой функции приложения. Эта операция осуществляется с помощью функции VMM Simulate_Far_Call, которая помещает передаваемые ей в качестве параметров селектор и смещение требуемой функции приложения в поля структуры клиента Client_CS и Client_IP. В результате, когда VMM, передавая управление приложению, снимет со стека структуру клиента и выполнит переход по оставшимся в стеке значениям Client_CS и Client_IP, то в регистрах CS:IP окажется адрес интересующей нас функции, которая и начнет немедленно выполняться. Для того чтобы не потерять то место в приложении, на котором произошла его приостановка из-за прихода прерывания, текущее содержимое полей Client_CS и Client_IP сохраняется в созданной перед этим копии структуры клиента.

Наконец, вызовом Resume_Exec управление передается в приложение. Еще раз подчеркнем, что этот вызов функции приложения является вложенным в VMM и что возможности вызываемой функции весьма ограниченны. Фактически она работает в чуждой для приложения операционной среде. В частности, как уже отмечалось, содержимое сегментных регистров (кроме CS) не соответствует сегментам приложения. Для того чтобы функция isr() могла обратиться к глобальным переменным приложения (адресуемым через регистр DS), мы передаем ей селектор сегмента данных приложения.

{mospagebreak} 

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

void isr(int segment,int dt){  
...
}

Поскольку мы в драйвере протолкнули в стек сначала данные Data, а затем селектор DSseg, они расположились в стеке приложения в правильном с точки зрения этой функции порядке, поэтому она может обращаться к своим локальным переменным segment и dt, как если бы была вызвана обычным образом оператором:

isr(DSseg,Data);

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

Отложенная процедура драйвера завершается очевидными вызовами End_Nest_Exec - окончания вложенного блока выполнения и Pop_Client_State - восстановления структуры клиента.

Описанная методика организации взаимодействия обработчика аппаратных прерываний, включенного в состав драйвера, и самого приложения относительно сложна и тем не менее не обеспечивает необходимые функциональные возможности обработчику прерываний приложения, в котором запрещается вызов функций Windows. Для того чтобы по сигналу прерывания вывести на экран результаты измерений, нам пришлось создавать специальный цикл обработки сообщений с постоянным опросом флага request. Установка флага обработчиком прерываний приложения приводила к выполнению функции PostMessage() и посылке в приложение сообщения WM_USER, в ответ на которое мы уже могли выполнять любые программные действия безо всяких ограничений.

Заметного упрощения программы можно добиться, организовав посылку в приложение сообщения WM_USER непосредственно из обработчика прерываний драйвера, точнее с уровня отложенных прерываний. В этом случае отпадает необходимость в передаче драйверу каких-либо данных, кроме дескриптора того окна приложения, в которое посылается сообщение WM_USER, в данном случае - дескриптора главного окна. Сокращается и текст процедуры отложенных прерываний Reflect_Int. Приложение Windows также упрощается: отпадает необходимость в разделении функций обработки прерываний между функцией Isr(), работающей, по существу, на уровне отложенных прерываний, и функцией OnUser(), выполняемой уже на обычном уровне задачи. Поскольку результат измерений легко передать из драйвера в приложение в качестве параметра сообщения WM_USER, исчезает необходимость в пункте <Чтение> в меню приложения.

Рассмотрим изменения, вносимые при использовании такого метода.

Приложение Windows, обрабатывающее аппаратные прерывания

//Операторы препроцессора #define и #include  
...
#define HANDLE_WM_USER(hwnd, wParam, lParam, fn) \//Макрос для обработки
((fn)(hwnd,wParam), 0L) // сообщения WM_USER
//Прототипы функций
...
void OnUser(HWND,WPARAM); //Сигнатура функции изменилась
//Глобальные переменные
HWND hMainWnd; //Дескриптор главного окна
...
//Главная функция WinMain
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int){
...
while(GetMessage(&msg,NULL,0,0))//Обычный цикл
DispatchMessage(&msg);//обработки сообщений
return 0;
}//Конец WinMain
//Функция Register регистрации класса окна
...
//Функция Create создания и показа окна
void Create(HINSTANCE hInst){
hMainWnd=CreateWindow(szClassName,szTitle,WS_OVERLAPPEDWINDOW,
10,10,200,100,HWND_DESKTOP,NULL,hInst,NULL);
ShowWindow(hMainWnd,SW_SHOWNORMAL);
}
//Оконная функция WndProc главного окна
LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam){
switch(msg){
HANDLE_MSG(hwnd,WM_COMMAND,OnCommand);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
HANDLE_MSG(hwnd,WM_USER,OnUser);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
//Функция OnCommand обработки сообщений WM_COMMAND от команд меню
void OnCommand(HWND hwnd,int id,HWND,UINT){
switch(id){
case MI_START://Инициализация платы
InitCard();
break;
case MI_EXIT://Завершить приложение
DestroyWindow(hwnd);
}
}
//Функция OnUser обработки сообщения WM_USER
void OnUser(HWND,WPARAM wParam){
char txt [80];
wsprintf(txt,"Измерения окончены\nНакоплено %d событий",(UINT)wParam);
MessageBox(hMainWnd,txt,"Контроль",MB_ICONINFORMATION);
}
//Функция OnDestroy обработки сообщения WM_DESTROY
...
//Функция InitCard инициализации (через драйвер) платы
void InitCard (){
GetAPIEntry(); //Получение адреса точки входа в драйвер
_BX=55; //Канал 0
_CX=20000; //Канал 1; BX*CX=Длительность интервала
_DX=1000; //Канал 2, внутренний генератор
_DI=(UINT)hMainWnd; //Дескриптор главного окна
VxDEntry(); //Вызов драйвера
}
//Функция GetAPIEntry получения адреса точки входа в драйвер
void GetAPIEntry()
...

В приведенном выше тексте файла .CPP детально показаны только измененные участки программы.

Изменилось определение макроса HANDLE_WM_USER для обработки сообщения WM_USER, которое мы пошлем в приложение из драйвера: функция обработки этого сообщения fn (в приложении она носит название OnUser()) принимает два параметра - hwnd и wParam. Через параметр сообщения wParam в приложение будет передан результат измерений. При необходимости передавать в приложение больший объем данных можно было расширить состав параметров функции третьим параметром lParam.

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

В функции OnCommand() удален фрагмент, связанный с пунктом меню <Чтение> (идентификатор MI_READ), поскольку в этом пункте уже нет необходимости.

В функции OnUser() параметр wParam приводится к типу целого без знака, преобразуется в символьную форму и выводится на экран в окно сообщения с соответствующим текстом.

Как будет видно из программы драйвера (и как, впрочем, должно быть очевидно для читателя), при посылке из драйвера сообщения WM_USER необходимо указать системе, какому окну адресовано это сообщение. Однако драйверу, естественно, ничего не известно об окнах приложений; дескриптор нашего главного окна следует передать драйверу на этапе инициализации платы. Эта операция выполняется в функции InitCard(), где перед вызовом драйвера в регистры BX, CX и DX засылаются константы настройки таймера, а регистр DI является приведенным к целому типу дескриптором главного окна hMainWnd.

{mospagebreak} 

Посмотрим теперь, как изменится программа драйвера.

Программа драйвера для обслуживания аппаратных прерываний

...  
WM_USER=SPM_UM_AlwaysSchedule+400h;Код сообщения WM_USER
include shell.inc ;Дополнительный включаемый файл
...
;======================
VxD_DATA_SEG
Data dw 0,0 ;32-битовая ячейка с данным для передачи в приложение
hwnd dd 0 ;32-битовая ячейка для получения дескриптора окна
IRQ_Handle dd 0 ;Дескриптор виртуального прерывания
VMyD_Int13_Desc label dword;32-битовый адрес следующей далее структуры
VPICD_IRQ_Descriptor <5,,OFFSET32 VMyD_Int_13>;Структура с данными о прерывании
VxD_DATA_ENDS
;======================
VxD_CODE_SEG
BeginProc VMyD_Control
...
EndProc VMyD_Control
;----------------------
BeginProc VMyD_Device_Init
...
EndProc VMyD_Device_Init
;-------------------------
;API-процедура, вызываемая из приложения
;При вызове: BX=C0, CX=C1, DX=C2, DI=дескриптор главного окна
BeginProc API_Handler
;Получим параметры из приложения
movzx EAX,[EBP.Client_DI]
mov hwnd,EAX
;Общий сброс
...
;Размаскируем уровень 5 в физическом контроллере прерываний
...
;Засылаем управляющие слова по каналам
...
;Засылаем константы в каналы
...
;Установим флаг S2 разрешения счета
...
ret
EndProc API_Handler
;-------------------------
;Процедура обработки аппаратного прерывания IRQ5 (вектор 13)
BeginProc VMyD_Int_13, High_Freq
;Получим результат измерений из выходного регистра счетчика
...
mov Data,AX ;Результат в младшей половине Data
;Выполним завершающие действия в PIC и вызовем функцию приложения
mov EAX,IRQ_Handle
VxDCall VPICD_Phys_EOI;EOI в физический контроллер прерываний
VxDCall VPICD_Physically_Mask;Маскируем наш уровень
;Перейдем на синхронный уровень. Это все иначе
push 0 ;Таймаут
push CAAFL_RING0 ;Событие кольца 0
push 0 ;Данные для передачи в процедуру отложенных прерываний
push OFFSET32 Reflect_Int;Вызываемая процедура отложенных прерываний
VxDCall _SHELL_CallAtAppyTime;Вызвать во "время приложения"
add ESP,4*4 ;Компенсация стека
clc
ret
EndProc VMyD_Int_13
;-------------------------
;Процедура уровня отложенных прерываний. Это тоже иначе
BeginProc Reflect_Int
push 0 ;Данные для функции обратного вызова
push 0 ;Адрес функции обратного вызова
push 0 ;lParam
push Data ;wParam
push WM_USER ;Код сообщения
push hwnd ;Окно-адресат
VxDCall _SHELL_PostMessage;Поставить сообщение в очередь
add ESP,4*6 ;Компенсация стека
clc
ret
EndProc Reflect_Int
VxD_CODE_ENDS
end VMyD_Real_Init

В начале текста драйвера необходимо подключить еще один заголовочный файл SHELL.INC и определить значение константы WM_USER. В Windows эта константа имеет длину 16 бит и равна 400h, однако функции _SHELL_PostMessage необходимо передать 32-битовое слово, причем сам код сообщения WM_USER должен находиться в младшей половине этого слова, а в старшую половину следует поместить информацию о диспетчеризации. В нашем случае эта информация выглядит как константа: SPM_UM_AlwaysSchedule.

В сегменте данных удалены ячейки для адреса функции обратного вызова isr и селектора DS. Ячейка для результата измерений объявлена как два слова, поскольку все параметры функции Shell_PostMessage имеют размер 32 бит. Добавлена ячейка hwnd для получения в нее из приложения дескриптора главного окна. Сам дескриптор имеет размер 16 бит, однако передавать его той же функции Shell_PostMessage надо в виде длинного слова.

В начале API-процедуры из структуры клиента (конкретно - из регистра DI) извлекается дескриптор окна и после расширения до длинного слова помещается в ячейку hwnd.

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

Для перехода на синхронный уровень в данном случае используется системный вызов _SHELL_CallAtAppyTime, осуществляющий передачу управления указанной в вызове процедуре ReflectInt во <время приложения>, то есть когда управление будет возвращено из VMM в приложение. В этой процедуре уже можно будет поставить сообщение WM_USER в очередь сообщений главного окна нашего приложения.

В процедуре уровня отложенных прерываний ReflectInt после помещения в стек необходимых параметров вызывается системная функция _Shell_PostMessage, которая и посылает в приложение сообщение WM_USER. Легко увидеть, что программист должен в этом случае полностью сформировать весь состав структуры сообщения msg - дескриптор окна-адресата, код сообщения, а также оба параметра, входящие во все сообщения Windows, - wParam и lParam. Параметром wParam мы в данном примере пользуемся для передачи в приложение результата измерения из ячейки Data. При необходимости можно было использовать и lParam.

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

Для задач управления аппаратурой, работающей в режиме прерываний, важной характеристикой является время отклика на прерывание, то есть временная задержка от момента поступления прерывания до выполнения первой команды обработчика. Как мы увидели, при использовании виртуального драйвера системные издержки перехода на прикладной обработчик, включенный в состав драйвера, составляют около 40 команд, на выполнение которых на машине средней производительности может понадобиться 10...15 мкс. При использовании системы MS-DOS этих издержек не было бы вовсе, так как в реальном режиме переход на обработчик прерываний процессор осуществляет практически мгновенно. Если же реализовать обработку прерываний без помощи виртуального драйвера, как это было сделано в предыдущей части статьи, то переход на прикладной обработчик прерываний потребовал бы 200...300 команд, а время задержки увеличилось бы (на таком же компьютере) до 120...180 мкс, то есть более чем на порядок.

К.Г.Финогенов
КомпьютерПресс 8'2001