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

ОГЛАВЛЕНИЕ

Программа виртуального драйвера

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

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

;При вызове 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 

В полях данных драйвера зарезервирован ряд ячеек для временного хранения полученных из приложения параметров, а также результата измерений. Особняком стоит ячейка 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, которая может проталкивать в стек как одинарные, так и двойные слова. В нашем случае в стек помещаются два слова - результат измерений и селектор сегмента данных приложения.