Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых

Операционная система Microsoft Windows 3.1 для программиста. Дополнительные главы

© Александр Фролов, Григорий Фролов
Том 17, М.: Диалог-МИФИ, 1994, 287 стр.

[Назад] [Содеожание] [Дальше]

5.2. Виртуальные драйверы

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

Сейчас самое время вспомнить систему команд процессора Intel 80386, сегментную и страничную организацию памяти, семафоры и другие аспекты, связанные со спецификой программирования для мультизадачной среды.

Мы рекомендуем вам обратиться к 6 тому "Библиотеки системного программиста", в котором описаны особенности работы процессора в защищенном режиме, а также рассмотрены основы мультизадачных операционных систем. Минимальные сведения об адресации памяти в защищенном режиме вы также можете получить из 13 тома "Библиотеки системного программиста" (глава "Управление памятью").

Виртуальные машины в Windows

Стандартный режим работы Windows позволял пользователю запустить несколько сеансов DOS. Когда пользователь переключался на такой сеанс, процессор переходил в реальный режим работы и занимался выполнением соответствующей программы MS-DOS. Пока работала программа MS-DOS, все приложения Windows находились в "замороженном" состоянии.

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

Когда Windows запускается в расширенном режиме, создается одна системная виртуальная машина, в рамках которой работают все приложения Windows. Системная виртуальная машина работает в защищенном режиме. Для каждого запускаемого вновь сеанса DOS создается отдельная виртуальная машина, работающая в режиме виртуального процессора V86.

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

Системная виртуальная машина (в рамках которой работают все приложения Windows), так же имеет свою таблицу LDT, причем только одну. Последнее означает, что все приложения Windows работают в одном адресном пространстве. Это обстоятельство облегчает обмен данными между приложениями, так как можно создать глобальный блок памяти, доступный любому приложению, однако сильно ухудшает защищенность приложений Windows друг от друга. И в самом деле, большинство ошибок в приложениях при своем проявлении приводят к необходимости перезапуска всей системы.

Виртуальные машины MS-DOS лучше защищены друг от друга, так их адресные пространства разделены. Создавая новую виртуальную машину MS-DOS, Windows сначала копирует в ее адресное пространство все драйверы и резидентные программы, загруженные до запуска Windows, а затем копирует туда запускаемую программу (рис. 5.1).

Рис. 5.1. Структура памяти в виртуальной машине MS-DOS

Если запускается резидентная программа, то она удаляется из памяти при завершении работы данной виртуальной машины. Этот факт удобно использовать для отладки резидентных программ MS-DOS.

Для виртуальной машины MS-DOS эмулируются все необходимые ей прерывания DOS и BIOS, которые доступны в "чистом" MS-DOS. Дополнительно программа, запущенная в виртуальной машине MS-DOS, может использовать функции DPMI (DOS Protected Mode Interface). С помощью функций интерфейса DPMI программа может переключить процессор из режима виртуального процессора V86 в защищенный режим. При этом ей будет доступна вся виртуальная память, имеющаяся в системе.

Интерфейс DPMI был нами описан в 6 томе "Библиотеки системного программиста". Там же был приведен пример программы, переключающей процессор в защищенный режим из режима виртуального процессора V86.

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

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

Существует малоизвестная возможность загрузить резидентную программу MS-DOS только в системную виртуальную машину, не копируя ее в адресные пространства виртуальных машин MS-DOS. Для этого имя такой программы необходимо указать в файле с именем winstart.bat, который запускается перед стартом системной виртуальной машины.

Загруженная таким образом резидентная программа MS-DOS может впоследствии взаимодействовать косвенно или непосредственно с приложениями Windows или виртуальными драйверами.

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

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

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

Что касается обработки прерываний, то она выполняется всегда в защищенном режиме. Соответствующая таблица прерываний IDT (Interrupt Descriptor Table) создается ядром Windows и никогда не изменяется непосредственно ни программами DOS, ни приложениями Windows.

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

Часть пресловутого "ядра операционной системы Windows" в расширенном режиме работы загружается из файла win386.exe, который представляет из себя ни что иное, как набор виртуальных драйверов, работающих в нулевом кольце защиты в модели памяти FLAT (которой мы скоро займемся). Одной из важнейших функций win386.exe является запуск виртуальных машин и планирование распределения времени, выделяемого для их работы.

Система планирования состоит из двух компонент - первичного планировщика (primary scheduler) и планировщика времени (time slicer). С помощью приложения Control Panel пользователь может назначить приоритет от 1 до 10000, который учитывается планировщиком времени. Можно установить как фоновый, так и основной приоритеты. Приоритеты можно изменять динамически для любой запущенной виртуальной машины MS-DOS или определить в соответствующем pif-файле. С помощью пиктограммы "Enchanced" приложения Control Panel вы можете также установить фоновый и основной приоритеты для системной виртуальной машины (т. е. одновременно для всех запущенных приложений Windows).

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

Модель памяти FLAT

Для того чтобы использовать модель памяти FLAT, недостаточно отметить крестиком слово "Flat" при создании проекта в Borland C++ или Microsoft Visual C++. Единственная возможность, которая существует для разработчиков виртуальных драйверов, - пакетный ассемблер masm5.exe и редактор link386.exe, входящий в состав DDK for Windows 3.1. Отложив на время описание этого скудного набора средств, рассмотрим особенности модели памяти FLAT.

Новый формат дескриптора для процессора Intel 80386 и процессоров Intel более старших моделей позволяет создавать сегменты кода и данных, имеющих размер 4 Гбайт - больше, чем может потребоваться в настоящее время. Виртуальные драйверы всегда работают в модели памяти FLAT, предполагающей использование таких, с вашего позволения, сегментов.

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

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

В качестве базового адреса в дескрипторе может находиться нулевое значение, и его едва ли нужно изменять - любая область памяти доступна так же легко, как в модели памяти TINY (если вы это еще помните, такая модель памяти с единственным сегментом используется com-программами MS-DOS).

Удобство FLAT-модели памяти для виртуальных драйверов заключается в том, что драйвер имеет прямой доступ к адресным пространствам всех одновременно запущенных виртуальных машин - как виртуальных машин MS-DOS, так и системной виртуальной машины, исполняющей приложения Windows. Так как виртуальный драйвер получает управление и выполняется в нулевом кольце защиты, он также имеет непосредственный доступ к системным областям памяти. Все это облегчает задачу организации взаимодействия между виртуальными машинами, доступ к аппаратуре и виртуализацию аппаратуры для совместного использования в многозадачной среде Windows.

Создает ли модель памяти FLAT какие-либо трудности для программиста по сравнению с обычными, 16-разрядными моделями памяти?

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

Единственное, о чем необходимо помнить - это о том, что все регистры процессора являются 32-разрядными. В вашем распоряжении есть 8-разрядный регистры AL, AH, BL, BH, ..., 16-разрядные регистры AX, BX, CX, ..., а также 32-разрядные регистры EAX, EBX, ECX и т. д.

Практически все регистры (кроме ESP) могут содержать 32-разрядное смещение. Следовательно, в них можно записывать адрес переменной. Под адресом здесь понимается короткий (NEAR) адрес, состоящий из одного смещения. В модели памяти FLAT нужно забыть про селекторы и сегменты.

В командах условных и безусловных переходов, а также при вызове подпрограмм командой call используйте ключевое слово short, так как используя короткое 32-разрядное смещение, можно "уйти" очень далеко.

Для загрузки 32-разрядного смещения в регистр указывайте ключевое слово offset32:

mov  esi, offset32 V86_Int21_Handler

Ну и конечно, не оставляйте без внимания новые команды, доступные для процессоров 80386. Очень удобна, например, команда MOVZX, позволяющая загрузить в 32-разрядный регистр 8- или 16-разрядное значение, расширив его слева нулями:

movzx ebx, byte ptr [ecx]

Аналогичная команда MOVSX выполняет знаковое расширение.

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

Структура виртуального драйвера

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

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

.386p
include vmm.inc

Так как виртуальный драйвер пользуется обычными и привилегированными командами процессора 80386, необходима директива ".386p", позволяющая вызывать такие команды.

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

Определение виртуального драйвера

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

Параметры разделяются запятыми, причем можно указывать не все параметры:

Declare_Virtual_Device VXDSRV, HiVers, LoVers, \
  VXDSRV_Control, VXDSRV_Id, Undefined_Init_Order, \
  VXDSRV_V86API_Handler, VXDSRV_PMAPI_Handler,

Макрокоманда Declare_Virtual_Device создает блок описания устройства DDB.

Первый параметр макрокоманды определяет имя виртуального устройства (т. е. имя виртуального драйвера). В приведенном выше примере виртуальный драйвер имеет имя VXDSRV.

Второй и третий параметры указывают старший и младший номер версии драйвера, соответственно. Здесь вы можете использовать целые числа или символические константы, определенные оператором equ. В нашем случае мы использовали символические константы HiVers и LoVers, которые соответствуют версии 1.1:

HiVers    equ 1    ; верхний номер версии драйвера
LoVers    equ 1    ; нижний номер версии драйвера

Четвертый параметр представляет собой метку управляющей точки входа виртуального драйвера, т. е. FLAT-адрес управляющей процедуры. Управляющая процедура вызывается при возникновении ряда событий в системе. В частности, эта точка получает управление несколько раз на различных стадиях инициализации драйвера. В нашем примере указана процедура VXDSRV_Control, которая будет описана позже:

BeginProc VXDSRV_Control
  Control_Dispatch Sys_Critical_Init, VXDSRV_Sys_Crit_Init
  clc
  ret
EndProc   VXDSRV_Control

Пятый параметр - идентификатор виртуального драйвера. Это число вы должны получить от фирмы Microsoft, послав запрос в произвольной форме через электронную почту по адресу vxdid@microsoft.com. В ответ на этот запрос вам придет бланк, который нужно заполнить и отправить обратно по тому же адресу. После этого через пару недель вы получите идентификатор.

Каждый виртуальный драйвер, предоставляющий сервис приложениям Windows и программам, работающим в среде виртуальных машин MS-DOS, должен иметь свой собственный идентификатор, отличный от идентификаторов других виртуальных драйверов. В противном случае возможен конфликт идентификаторов, который приведет к ошибке при загрузке виртуального драйвера. Использование идентификатора, полученного у Microsoft, гарантирует отсутствие конфликта с другими зарегистрированными виртуальными драйверами.

Если ваш драйвер не предоставляет никакого сервиса, ему не нужен идентификатор. В этом случае в качестве пятого параметра можно указать значение Undefined_Device_ID, определенное в файле vmm.inc.

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

В нашем примере порядок инициализации не имеет значения, поэтому мы указали константу Undefined_Init_Order, определенную в файле vmm.inc.

В том случае, когда ваш драйвер предоставляет сервис приложениям Windows или программам, запущенным в среде виртуальной машины MS-DOS, необходимо указывать седьмой и восьмой параметры. Это точки входа процедур, получающих управление при вызове драйвера для получения сервиса.

Седьмой параметр определяет адрес процедуры, которая вызывается при обращении к драйверу из виртуальной машины MS-DOS, восьмой - из приложения Windows. В нашем примере указаны адреса процедур VXDSRV_V86API_Handler и VXDSRV_PMAPI_Handler (седьмой и восьмой параметры, соответственно).

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

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

Сегменты инициализации

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

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

Сегмент инициализации реального режима VXD_REAL_INIT объявляется с помощью макрокоманды VXD_REAL_INIT_SEG. Этот сегмент имеет имя _RTEXT и содержит процедуру инициализации виртуального драйвера в реальном режиме работы процессора, а также, возможно, данные, необходимые для инициализации.

Конец сегмента отмечается макрокомандой VXD_REAL_INIT_ENDS:

VXD_REAL_INIT_SEG
RealInit proc near
    mov ax, Device_Load_Ok  
    xor bx, bx
    xor si, si
    xor edx, edx
    ret
RealInit endp
VXD_REAL_INIT_ENDS

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

Если инициализация в реальном режиме выполнена успешно, начинается второй этап инициализации виртуального драйвера, который выполняется в защищенном режиме. На этом этапе вызываются процедуры инициализации защищенного режима, расположенные в сегменте VXD_ICODE с именем _ITEXT. Этот сегмент объявляется макрокомандой VXD_ICODE_SEG. Конец сегмента отмечается макрокомандой VXD_ICODE_ENDS:

VXD_ICODE_SEG
BeginProc VXDSRV_Sys_Crit_Init
  clc
  ret
EndProc   VXDSRV_Sys_Crit_Init
VXD_ICODE_ENDS

На этапе инициализации в защищенном режиме может также использоваться сегмент данных VXD_IDATA (называется также _IDATA). Начало этого сегмента отмечается макрокомандой VXD_IDATA_SEG, конец - макрокомандой VXD_IDATA_ENDS.

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

Постоянные сегменты

Все процедуры и данные, не имеющие отношения к инициализации, располагаются, соответственно, в сегментах VXD_CODE и VXD_DATA.

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

Начало постоянного сегмента кода отмечается макрокомандой VXD_CODE_SEG или VXD_LOCKED_CODE_SEG. Обе эти макрокоманды в текущей версии DDK для Windows 3.1 полностью эквивалентны, в чем можно убедиться, посмотрев на их определения в файле vmm.inc.

Конец постоянного сегмента кода отмечается макрокомандой VXD_CODE_ENDS или VXD_LOCKED_CODE_ENDS.

Начало постоянного сегмента данных отмечается макрокомандой VXD_DATA_SEG или полностью эквивалентной ей макрокомандой VXD_LOCKED_DATA_SEG.

Для обозначения конца постоянного сегмента данных используются макрокоманды VXD_DATA_ENDS или VXD_LOCKED_DATA_ENDS.

Процесс инициализации

Рассмотрим процесс инициализации виртуального драйвера более подробно.

Вначале в память загружается сегмент инициализации реального режима VXD_REAL_INIT. После загрузки система управления виртуальными машинами VMM (Virtual Machine Manager) вызывает процедуру инициализации реального режима. Она выполняет все необходимые действия перед переключением Windows в защищенный режим, например, сбрасывает обслуживаемое драйвером устройство в исходное состояние или блокирует его. На этапе инициализации в реальном режиме можно выполнить проверку файлов инициализации system.ini и win.ini, резервирование физических страниц памяти для использования устройством. Можно также зарезервировать блок памяти, который имеет отношение к устройству и создается для каждой вновь запускаемой виртуальной машины.

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

Перед вызовом процедуры регистры процессора устанавливаются следующим образом.

Регистр Описание содержимого регистра
CS, DS, ES Адрес сегмента инициализации реального режима
AH Старший номер версии системы управления виртуальными машинами VMM
AL Младший номер версии системы управления виртуальными машинами VMM
BX Флаги загрузки:Duplicate_Device_ID повторная загрузка драйвера с тем же идентификатором;Duplicate_From_INT2F повторная загрузка драйвера с тем же идентификатором из списка драйверов прерывания INT 2Fh;Loading_From_INT2F драйвер был определен в списке драйверов прерывания INT 2Fh
ECX Дальний адрес в формате <сегмент:смещение> точки входа сервиса инициализации реального режима
EDX Указатель на данные, полученные из прерывания INT 2Fh. Если данные не передаются, регистр содержит нулевое значение
SI Сегментный адрес блока памяти, содержащего среду (environment) операционной системы MS-DOS
SS:SP Адрес стека

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

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

Регистр Описание содержимого регистра
AX Код возврата:Abort_Device_Load не загружать виртуальный драйвер;Abort_Win386_Load не загружать Windows;Device_Load_Ok можно продолжать процедуру инициализации и загрузки драйвера в память;No_Fail_Message это значение можно комбинировать со значениями Abort_Device_Load и Abort_Win386_Load, в этом случае на экран не выдается сообщение об аварийном завершении загрузки драйвера или операционной системы Windows
BX Указатель на массив, содержащий номера физических страниц памяти, зарезервированных для исключительного использования виртуальным драйвером. Последний элемент массива должен содержать нулевое значение.Под указателем здесь понимается смещение в сегменте инициализации реального режимаЕсли страницы физической памяти не резервируются, регистр BX должен содержать нулевое значение
EDX 32-разрядное значение, которое будет передано через регистр EDX процедуре инициализации защищенного режима, вызываемой по сообщению Sys_Critical_Init (инициализация в защищенном режиме будет рассмотрена позже)
SI Указатель на массив структур данных, состоящих из двойного слова и слова. Двойное слово содержит указатель на блок памяти, слово - размер этого блока памяти. Последний элемент массива должен содержать нулевое значение.Описанные таким образом блоки памяти создаются для каждой запускаемой виртуальной машины.Если блоки памяти не резервируются, в регистр SI следует записать нулевое значение

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

Вызов сервиса выполняется следующим образом:

mov ax, Service
call cs:[ecx]

где Service - код сервиса.

Для кода сервиса возможны следующие значения:

Код сервиса Описание
0000hGet_Profile_String Получение строки из файла system.ini.В DS:SI должен находиться указатель на строку имени секции или 0 для секции [386Enh].В DS:DI необходимо записать адрес имени, а в ES:DX - адрес строки, которая будет использована по умолчанию.На выходе ES:DX будет содержать адрес строки, прочитанной из файла конфигурации или адрес строки по умолчанию, если указанные раздел или имя не найдены
0001hGet_Next_Profile_String Получение следующего значения, используется в том случае, если для одного имени указано несколько разных значений
0003hGet_Profile_Boolean Получение значения типа BOOL из файла system.ini.В ECX нужно записать значение по умолчанию (0 или 0FFFFFFFFh). В DS:SI должен находиться указатель на строку имени секции или 0 для секции [386Enh].В DS:DI необходимо записать адрес имени.Прочитанное из файла конфигурации значение записывается в регистр ECX
0004hGet_Profile_Decimal_Int Аналогично предыдущему, но возвращается целое значение, преобразованное из десятичного формата
0005hGet_Profile_Hex_Int Аналогично предыдущему, но возвращается целое значение, преобразованное из шестнадцатиричного формата

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

Теперь мы займемся вторым этапом инициализации, который выполняется в защищенном режиме.

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

При вызове управляющей процедуре передается системное управляющее сообщение (system control message). В драйвере необходимо предусмотреть обработку некоторых или всех таких сообщений (в зависимости от назначения драйвера).

На втором этапе инициализации виртуальному драйверу по очереди передаются три инициализирующих системных управляющих сообщения: Sys_Critical_Init, Device_Init и Init_Complete.

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

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

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

Как организовать обработку этих и других сообщений?

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

Вот пример управляющей процедуры нашего драйвера VXDSRV, который мы рассмотрим позже:

BeginProc VXDSRV_Control
; Процедура системной критической инициализации  
  Control_Dispatch Sys_Critical_Init, VXDSRV_Sys_Crit_Init
  clc           ; признак успешного завершения
  ret
EndProc VXDSRV_Control

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

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

Приведем исходный текст процедуры системной критической инициализации драйвера VXDSRV:

VXD_ICODE_SEG
BeginProc VXDSRV_Sys_Crit_Init
  
; Устанавливаем фильтр для прерывания int 21h  
  mov  eax, 21h
  mov  esi, offset32 V86_Int21_Handler
  VMMcall Hook_V86_Int_Chain
  
  clc     ; признак успешного завершения
  ret
EndProc   VXDSRV_Sys_Crit_Init
VXD_ICODE_ENDS

Эта процедура расположена в сегменте инициализации защищенного режима, который будет удален из памяти после завершения второго этапа инициализации. Она добавляет обработчик в цепочку обработчиков прерывания INT 21h с помощью сервиса Hook_V86_Int_Chain. При этом указывается адрес добавляемого обработчика - смещение функции V86_Int21_Handler.

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

Итак, подведем небольшой итог.

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

Необходимо объявить один сегмент инициализации реального режима, один сегмент кода для инициализации защищенного режима и при необходимости один сегмент данных для инициализации защищенного режима.

Инициализация выполняется в два этапа.

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

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

Список системных управляющих сообщений

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

Инициализация драйвера

Sys_Critical_Init

Системная критическая инициализация в состоянии с запрещенными прерываниями

Device_Init

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

Init_Complete

Завершение инициализации

Завершение работы драйвера

System_Exit

Первое сообщение, которое получает драйвер при завершении работы системы. Следом за этим посылается сообщение Sys_VM_Terminate

Sys_Critical_Exit

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

Инициализация виртуальной машины

Create_VM

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

VM_Critical_Init

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

VM_Init

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

Sys_VM_Init

Это сообщение приходит после сообщения Init_Complete. Используется для инициализации программного и аппаратного обеспечения в системной виртуальной машине. Если перед выходом из процедуры установить флаг переноса в единицу, создание системной виртуальной машины будет отменено, что приведет к завершению работы Windows

Завершение работы виртуальной машины

Query_Destroy

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

VM_Terminate

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

Sys_VM_Terminate

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

VM_Not_Executable

Второе сообщение, которое посылается при уничтожении виртуальной машины

Destroy_VM

Третье сообщение, которое посылается при уничтожении виртуальной машины

Изменение состояния виртуальной машины

VM_Suspend

Работа виртуальной машины приостановлена

VM_Resume

Работа виртуальной машины возобновлена

Set_Device_Focus

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

Begin_Message_Mode

Это сообщение поступает, когда виртуальный драйвер Shell отображает диалоговую панель с сообщением для пользователя и не может использовать для этого системную виртуальную машину

End_Message_Mode

Сообщение приходит, когда больше не нужно отображать указанную выше диалоговую панель

Reboot_Processor

Попытка пользователя перезапустить процессор. В ответ на это сообщение виртуальный драйвер должен сбросить процессор (если он может это сделать). Функция сброса процессора возлагается на виртуальный драйвер клавиатуры

Сервис, предоставляемый виртуальным драйвером

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

Однако, прежде всего, необходимо получить адрес точки входа для вызова драйвера. Способ получения этого адреса одинаков как для приложений Windows, работающих в защищенном режиме, так и для программ MS-DOS, работающих на виртуальных машинах MS-DOS в виртуальном режиме процессора V86.

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

VXDAPI vxdGetDeviceAPI(unsigned short vxd_id)
{
  unsigned axreg, dxreg;
  asm push  di
  asm push  es
  asm mov   ax, 0x1684
  asm mov   bx, vxd_id
  asm xor   di, di
  asm mov   es, di
  asm int   0x2f
  asm mov   ax, di
  asm mov   dx, es
  asm pop   es
  asm pop   di
  asm mov   axreg, ax
  asm mov   dxreg, dx
  return((VXDAPI)MAKELP(dxreg, axreg));
}

Как видно из примера, получение адреса точки входа сводится к вызову функции 1684h прерывания INT 2Fh. Искомый адрес возвращается в регистрах ES:DI. Если драйвер с указанным идентификатором отсутствует в системе, возвращается нулевой адрес.

Учтите, что если адрес точки входа определяется из приложения Windows, когда процессор находится в защищенном режиме, адрес возвращается в формате <селектор:смещение>. Если же этот адрес определяется в виртуальной машине MS-DOS, он будет получен в формате <сегмент:смещение>.

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

asm mov dx, sel
asm mov cx, off
asm mov si, bsel
asm mov di, boff
asm mov ax, vxdapiRegisterWnd
(*vxdApi)();

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

Что происходит в виртуальном драйвере, когда его вызывают подобным образом?

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

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

Расскажем об этом подробнее.

Контекст виртуальной машины

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

Форматы соответствующих структур определены в файле vmm.inc (структуры Client_Reg_Struc, Client_Word_Reg_Struc, Client_Byte_Reg_Struc). В них определены следующие поля:

Client_EDI Client_SS
Client_ESI Client_ES
Client_EBP Client_DS
Client_EBX Client_FS
Client_EDX Client_GS
Client_ECX Client_Alt_EIP
Client_EAX Client_Alt_CS
Client_Error Client_Alt_ESP
Client_EIP Client_Alt_SS
Client_CS Client_Alt_ES
Client_EFlags Client_Alt_DS
Client_ESP Client_Alt_FS
Client_Alt_EFlags Client_Alt_GS
Client_DI Client_IP
Client_SI Client_Flags
Client_BP Client_SP
Client_BX Client_Alt_IP
Client_DX Client_Alt_Flags
Client_CX Client_Alt_SP
Client_AX  
Client_BL Client_CL
Client_BH Client_CH
Client_DL Client_AL
Client_DH Client_AH

Обращение к этим полям не вызывает никаких проблем:

movzx   eax, [ebp.Client_AX]

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

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

Сервис для виртуального драйвера

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

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

Система управления виртуальными машинами VMM предоставляет сервис, вызываемый с помощью макрокоманды VMMcall:

mov  ax, (Client_SI shl 8) + Client_DI
VMMcall Map_Flat 
mov [CallbackBuf], eax

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

Если после имени сервиса указать параметры, они будут занесены макрокомандой в стек.

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

Сервис, доступный виртуальным драйверам, слишком обширный, для того чтобы описать его в нашей книге достаточно подробно. За более детальным описанием вы можете обратиться к документации, поставляемой вместе с DDK, или к книге Д. Нортона "Writing Windows Device Drivers". Мы же ограничимся кратким перечислением основных возможностей сервиса. Впоследствии, при описании исходного текста драйвера VXDSRV, мы рассмотрим более подробно некоторые функции сервиса, использованные в этом драйвере.

Сервис системы управления виртуальными машинами

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

Функции управления памятью

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

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

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

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

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

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

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

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

Управление прерываниями и вызовом функций обратного вызова

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

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

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

Функции первичного планировщика

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

Пользуясь этим сервисом, виртуальный драйвер может планировать вызов функции обратного вызова для конкретной виртуальной машины, указанной своим идентификатором. Благодаря этому возможно, например, инициировать из виртуальной машины MS-DOS вызов функции, расположенной в системной виртуальной машине.

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

Функции вторичного планировщика

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

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

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

Функции таймера

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

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

Обработка сбоев и прерываний

Виртуальный драйвер может взять на себя обработку немаскируемого прерывания, нарушения защиты, сбоев во время работы виртуальной машины MS-DOS, системной виртуальной машины, а также системы управления виртуальными машинами VMM.

Получение справочной информации

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

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

Работа со связанными списками

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

Обработка ошибок

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

Сервис виртуального драйвера SHELL

Виртуальный драйвер Shell экспортирует две очень полезные функции, с помощью которых можно вывести на экран компьютера обычную и системную модальную диалоговую панель с сообщением. Эти функции напоминают функцию MessageBox из программного интерфейса Windows и называются SHELL_Message и SHELL_SYSMODAL_Message.

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

Сервис виртуального драйвера контроллера прерываний

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

Сервис драйвера VPICD позволяет выполнять операции с контроллером прерываний, такие как маскирование и размаскирование прерываний, обработка конца прерывания, обработка аппаратного прерывания и т. п.

Если ваш виртуальный драйвер должен обрабатывать прерывания от нестандартной аппаратуры, вам следует воспользоваться сервисом драйвера VPICD.

Сервис виртуального драйвера контроллера прямого доступа к памяти

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

Никакая виртуальная машина не может программировать регистры контроллера прямого доступа к памяти (ПДП), так как соответствующие порты ввода/вывода зарезервированы виртуальным драйвером VDMAD.

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

Сервис виртуального драйвера жесткого диска

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

Драйвер VXDSRV

После такого краткого обзора сервиса, доступного виртуальным драйверам, перейдем к практике. Мы предлагаем вам познакомиться с разработанным нами виртуальным драйвером VXDSRV, предназначенным для запуска приложений Windows из командной строки виртуальной машины MS-DOS или из среды таких программ, как Norton Commander и Xtree, работающих в виртуальной машине MS-DOS.

Поясним подробнее, для чего же предназначен наш драйвер.

Запустите любое приложение Windows из командной строки виртуальной машины MS-DOS. Вы увидите беспомощное сообщение о том, что требуется наличие Microsoft Windows:

This program requires Microsoft Windows.

Но Microsoft Windows уже работает! Не лучше ли было бы, вместо того чтобы выдавать подобное сообщение, попытаться запустить приложение в системной виртуальной машине?

Именно это и делает наш драйвер. После его активизации вы никогда не увидите сообщения о невозможности запуска приложения из виртуальной машины MS-DOS. Драйвер, работающий вместе с DLL-библиотекой d2w.dll и обычным приложением Windows dos2win.exe (исходные тексты также будут описаны), передает приложению dos2win.exe параметры запускаемого приложения. Получив эти параметры, dos2win.exe обеспечивает запуск приложения с помощью функции LoadModule из программного интерфейса Windows.

Для того чтобы обнаружить попытку запуска программы MS-DOS или приложения Windows, мы перехватываем функцию 4B00h прерывания INT 21h и анализируем передаваемый ей файл. Перехват выполняется при помощи сервиса VMM, поэтому свободное адресное пространство виртуальных машин MS-DOS не уменьшается (это было бы не так при использовании обычной резидентной программы MS-DOS).

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

Если запускается программа MS-DOS, виртуальный драйвер не вмешивается в процесс запуска, позволяя функции 4B00h прерывания INT 21h спокойно делать свое дело.

Если же пользователь сделал попытку запустить приложение Windows, виртуальный драйвер отменяет выполнение указанной выше функции прерывания INT 21h. Затем он сохраняет путь к запускаемому файлу, параметры и путь к текущему на момент запуска каталогу в буфере. Этот буфер расположен в фиксированном сегменте данных DLL-библиотеки d2w.dll.

Сохранив параметры в буфере, виртуальный драйвер вызывает функцию обратного вызова, которая находится в фиксированном сегменте кода той же DLL-библиотеки d2w.dll.

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

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

Описанная схема не лишена недостатков. В частности, в составе системы разработки программного обеспечения Microsoft Visual C++ for Windows поставляются достаточно странные программы, для которых наш алгоритм не подходит. Эти программы являются, строго говоря, приложениями Windows, так как содержащие их exe-файлы имеют два заголовка - старый и новый. Однако их необходимо запускать из среды MS-DOS или из среды виртуальной машины MS-DOS - иначе они не работают.

Наш драйвер совершенно справедливо относит такие программы к приложениям Windows, так как он принимает решение на основе анализа заголовка exe-файла. Попытка запуска такого "приложения" функцией WinExec приводит к зацикливанию Windows. Другая функция, с помощью которой можно запустить приложение Windows, это LoadModule. Эта функция в данном случае просто ничего не делает.

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

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

Обратимся к исходному тексту виртуального драйвера (листинг 5.1).


Листинг 5.1. Файл vxdsrv\vxdsrv.asm


; ---------------------------------------------------
; Виртуальный драйвер VXDSRV
; Version 1.1
; ---------------------------------------------------
; Copyright (C) Александр Фролов 1995
; ---------------------------------------------------
; Работает совместно с приложением dos2win.exe
; и DLL-библиотекой d2w.dll
;
; Позволяет запускать приложения Windows из виртуальной
; машины MS-DOS, из командной строки Norton Commander или
; аналогичной оболочки, работающей на виртуальной
; машине MS-DOS
;
; Выполняет перехват функции 4B00h прерывания int 21h
; и сохраняет полученные этой функцией командную строку 
; и строку параметров, а также определенные отдельно
; текущий диск и текущий каталог в области памяти, 
; зарезервированной DLL-библиотекой d2w.dll. 
; Затем драйвер вызывает функцию, определенную
; в этой библиотеке и посылающую сообщение приложению
; dos2win.exe. Приняв сообщение, приложение dos2win.exe
; запускает программу, пользуясь данными, полученными
; от виртуального драйвера
; ---------------------------------------------------

.386p
include vmm.inc

; Идентификатор драйвера VXDSRV. 
VXDSRV_Id equ 8000h 

HiVers    equ 1    ; верхний номер версии драйвера
LoVers    equ 1    ; нижний номер версии драйвера
Vers      equ ((HiVers shl 8) or LoVers)
CFlag     equ 1

; ===================================================
; Заголовок виртуального драйвера
; ===================================================
Declare_Virtual_Device VXDSRV, HiVers, LoVers, \
  VXDSRV_Control, VXDSRV_Id, Undefined_Init_Order, \
  VXDSRV_V86API_Handler, VXDSRV_PMAPI_Handler,

; ===================================================
; Инициализация в реальном режиме
; ===================================================
VXD_REAL_INIT_SEG

RealInit proc near

; Вывод "рекламной" текстовой строки 
    mov ah, 9
    mov dx, offset VxD_Hello
    int 21h

; Признак успешной инициализации
    mov ax, Device_Load_Ok  

; Страницы физической памяти не резервируются   
    xor bx, bx
    
; Данные для каждого экземпляра виртуальной машины    
; не резервируются
    xor si, si

; Значение, передаваемое процедуре Sys_Critical_Init
    xor edx, edx
    ret
RealInit endp

VxD_Hello db '*VXDSRV* Copyright (C) Alexandr Frolov 1995'
          db 0dh, 0ah, '$'
VXD_REAL_INIT_ENDS

; ===================================================
; Системная критическая инициализация
; ===================================================
VXD_ICODE_SEG
BeginProc VXDSRV_Sys_Crit_Init
  
; Устанавливаем фильтр для прерывания int 21h  
  mov  eax, 21h
  mov  esi, offset32 V86_Int21_Handler
  VMMcall Hook_V86_Int_Chain
  
  clc     ; признак успешного завершения
  ret
EndProc   VXDSRV_Sys_Crit_Init
VXD_ICODE_ENDS

; ===================================================
; Зафиксированный сегмент кода
; ===================================================
VxD_LOCKED_CODE_SEG

; ---------------------------------------------------
; Определение управляющих процедур
; ---------------------------------------------------
BeginProc VXDSRV_Control

; Процедура системной критической инициализации  
  Control_Dispatch Sys_Critical_Init, VXDSRV_Sys_Crit_Init
  
  clc
  ret
EndProc VXDSRV_Control

VxD_LOCKED_CODE_ENDS

; ===================================================
; Сегмент данных
; ===================================================
VxD_DATA_SEG

; ---------------------------------------------------
; Таблица адресов функций API драйвера
; ---------------------------------------------------
VXDSRV_API_Call label dword
  
  dd  offset32 vxdapiGetVersion         ; AX=0
  dd  offset32 vxdapiRegisterWnd        ; AX=1
  dd  offset32 vxdapiUnregisterWnd      ; AX=2

VXDSRV_API_MaxCall EQU ($ - VXDSRV_API_Call) / 4

CallbackSel     dw 0h ; селектор функции обратного вызова
CallbackOff     dd 0h ; смещение функции обратного вызова
V86CallFlag     dd 0  ; вызов из виртуальной машины V86
CallbackBuf     dd 0  ; адрес буфера для передачи строки

flatpCmdLine  dd 0      ; адрес командной строки
flatpParmLine dd 0      ; адрес строки параметров 
hSem   dd 0             ; идентификатор семафора

nCurDisk  db 0          ; текущий диск
szCurPath db 64 dup(0)  ; текущий каталог

; Сегмент буфера для получения текущего пути
wPathSeg     dw 0

; Соответствующий этому буферу FLAT-адрес
flatpPathBuf dd 0

VxD_DATA_ENDS

; ===================================================
; Перемещаемый сегмент кода
; ===================================================
VxD_CODE_SEG

; ---------------------------------------------------
; Вход API для виртуальных машин V86
; ---------------------------------------------------
BeginProc   VXDSRV_V86API_Handler
  mov eax, 1            ; признак машины V86
  mov V86CallFlag, eax
  
; Вызываем универсальное API драйвера
  call VXDSRV_API_Handler
  ret
EndProc     VXDSRV_V86API_Handler

; ---------------------------------------------------
; Вход API для виртуальных машин защищенного режима
; ---------------------------------------------------
BeginProc   VXDSRV_PMAPI_Handler
  mov eax, 0 ; признак защищенного режима
  mov V86CallFlag, eax
  
  call VXDSRV_API_Handler
  ret
EndProc     VXDSRV_PMAPI_Handler

; ---------------------------------------------------
; Универсальное API драйвера, вызывается как для
; режима VM86, так и для защищенного режима
; ---------------------------------------------------
BeginProc   VXDSRV_API_Handler
    
; Загружаем номер функции API    
    movzx   eax, [ebp.Client_AX]

; Проверяем этот номер на допустимость
    cmp     eax, VXDSRV_API_MaxCall
    jae     short InvalidNumber

; В случае успеха сбрасываем флаг переноса и    
; вызываем функцию по таблице адресов
    and     [ebp.Client_EFlags], NOT CFlag
    call    VXDSRV_API_Call[eax * 4]
    ret
InvalidNumber:
    or      [ebp.Client_EFlags], CFlag
    ret
EndProc     VXDSRV_API_Handler

; ---------------------------------------------------
; vxdapiGetVersion, номер = 0
; Возвращает в AX номер версии:
;    AH - старший номер, AL - младший номер
; ---------------------------------------------------
BeginProc   vxdapiGetVersion
    mov     [ebp.Client_AX], Vers
    clc      ; успешное завершение
    ret
EndProc     vxdapiGetVersion

; ---------------------------------------------------
; vxdapiRegisterWnd, номер = 1
; ---------------------------------------------------
BeginProc vxdapiRegisterWnd

; Можно вызывать только из защищенного режима  
  mov eax, V86CallFlag
  cmp eax, 0
  jnz short RW_CallFromRealMode
  
; Сохраняем смещение и селектор процедуры обратного
; вызова, расположенной в фиксированном сегменте
; кода DLL-библиотеки d2w.dll
  
  movzx eax, [ebp.Client_CX] ; смещение
  mov [CallbackOff], eax
  mov ax, [ebp.Client_DX]    ; селектор
  mov [CallbackSel], ax
  
; Преобразуем адрес буфера во flat-адрес 
  
  mov  ax, (Client_SI shl 8) + Client_DI
  VMMcall Map_Flat 
  mov [CallbackBuf], eax
  
; Создаем семафор с начальным значением 1  
  mov  ecx, 1
  VMMcall Create_Semaphore
  mov  hSem, eax  ; сохраняем идентификатор семафора
  
  clc
  ret

RW_CallFromRealMode:
  stc
  ret
EndProc   vxdapiRegisterWnd

; ---------------------------------------------------
; vxdapiUnregister, номер = 2
; ---------------------------------------------------
BeginProc vxdapiUnregisterWnd

; Можно вызывать только из защищенного режима  
  mov eax, V86CallFlag
  cmp eax, 0
  jnz short UW_CallFromRealMode
  
  mov eax, 0
  mov [CallbackOff],   eax
  mov [CallbackSel],   ax
  mov [CallbackBuf],   eax
  
; Уничтожаем семафор  
  mov  eax, hSem
  VMMcall Destroy_Semaphore
  
  clc
  ret

UW_CallFromRealMode:
  stc
  ret
EndProc   vxdapiUnregisterWnd

; ---------------------------------------------------
; StrCpy, копирование строки ASCIIZ
; [ecx] - исходный адрес
; [edx] - адрес буфера
; ---------------------------------------------------
BeginProc StrCpy
  push  eax
  push  ecx
  push  edx

StrCpyLoop:
  mov   al, [ecx]
  mov   [edx], al
  cmp   al, 0
  jz    short StrCpyEnd
  inc   ecx
  inc   edx
  jmp short StrCpyLoop
StrCpyEnd:
  
  pop edx
  pop ecx
  pop eax
  ret
EndProc   StrCpy

; ---------------------------------------------------
; CopyParm, копирование строки параметров
; ---------------------------------------------------
BeginProc CopyParm
  push  eax
  push  ebx
  push  ecx
  push  edx
  
; Вычисляем адрес строки параметров в буфере  
  mov   edx, CallbackBuf
  add   edx, 65 + 128

; Определяем размер строки параметров
  mov   ecx, flatpParmLine
  movzx ebx, byte ptr [ecx]
  inc   ecx
  
; Если параметров нет, закрываем строку нулем
  cmp   ebx, 0
  jz short ParmCopyEnd

; Цикл копирования строки параметров
ParmCopyLoop:
  cmp   ebx, 0
  jz    short ParmCopyEnd
  
  mov   al, [ecx]
  mov   [edx], al
  
  dec   ebx
  inc   ecx
  inc   edx
  jmp short ParmCopyLoop

ParmCopyEnd:

; Закрываем строку нулем 
  mov   al, 0
  mov   [edx], al
  
  pop edx
  pop ecx
  pop ebx
  pop eax
  ret
EndProc   CopyParm

; ---------------------------------------------------
; V86_Int21_Handler
; Фильтр для функции 4B00h прерывания INT 21h,
; вызываемого из виртуальной машины MS-DOS
; (эта функция выполняет запуск программы MS-DOS)
; ---------------------------------------------------
BeginProc V86_Int21_Handler
  
  pushad
  
; Проверяем номер функции. Нас интересует только  
; запуск программ
  mov ax, word ptr [ebp.Client_AX]
  cmp ax, 4B00h
  jnz HandlerExit
  
; Если окно приложения dos2win не зарегистрировано,
; ничего не делаем
  mov eax, CallbackBuf
  cmp eax, 0
  jz  HandlerExit

; Если запускается программа MS-DOS, ничего 
; не делаем
  call short IsWindowsApp
  jz HandlerExit

; Для исключения реентерабельных вызовов выполняем
; ожидание семафора
  mov  eax, hSem
  mov  ecx,(Block_Enable_Ints OR Block_Svc_If_Ints_Locked)
  VMMcall Wait_Semaphore
  
; Получаем текущий диск и каталог  
  call short GetCurDir
  
; Сохраняем номер текущего диска  
  mov   edx, CallbackBuf
  mov   al, nCurDisk
  mov   byte ptr [edx], al
  
; Определяем FLAT-адрес командной строки
  mov ax, (Client_DS shl 8) + Client_DX
  VMMcall Map_Flat
  mov flatpCmdLine, eax

; Определяем FLAT-адрес блока EPB
  mov ax, (Client_ES shl 8) + Client_BX 
  VMMcall Map_Flat
  
; Загружаем в DX:BX адрес строки параметров  
  mov bx, [eax + 2]
  mov dx, [eax + 4]

; Определяем FLAT-адрес строки параметров
  
  push dword ptr [ebp.Client_ES]
  push [ebp.Client_EBX]
  
  mov [ebp.Client_ES], dx
  mov [ebp.Client_BX], bx
  mov ax, (Client_ES shl 8) + Client_BX
  VMMcall Map_Flat
  mov flatpParmLine, eax

  pop [ebp.Client_EBX]
  pop dword ptr [ebp.Client_ES]
  
; Копируем командную строку в буфер, который
; находится в DLL-библиотеке d2w.dll
  mov   ecx, flatpCmdLine
  mov   edx, CallbackBuf
  add   edx, 65
  call  short StrCpy
 
; Выполняем копирование строки параметров
  call  short CopyParm
  
; Определяем идентификатор текущей VM
  VMMCall Get_Cur_VM_Handle
  mov edx, ebx
  
; Определяем идентификатор системной VM
  VMMcall Get_Sys_VM_Handle
  
; Планируем вызов функции обратного вызова
  mov esi, offset32 CallbackProc
  VMMcall Schedule_VM_Event
  
  popad
  
; Если запускается приложение Windows, блокируем  
; выполнение прерывания INT 21h в виртуальной
; машине MS-DOS
  clc
  
  ret
  
HandlerExit:  
  popad

; Если запускается программа MS-DOS, наш драйвер
; не мешает этому процессу
  stc
  
  ret
EndProc   V86_Int21_Handler

; ---------------------------------------------------
; CallbackProc
; Функция обратного вызова
; Вызывается в системной VM по запросу фильтра
; прерывания INT 21h, установленного нашим драйвером
; ---------------------------------------------------
BeginProc CallbackProc
  
; Сохраняем состояние системной VM
  Push_Client_State
  
; Начинаем вложенное выполнение  
  VMMcall Begin_Nest_Exec

; Записываем в стек системной VM параметр -
; версию нашего VxD-драйвера
  mov ax, Vers
  VMMcall Simulate_Push
  
; Вызов функции обратного вызова, определенной
; в DLL-библиотеке d2w.dll
  mov edx, [CallbackOff]
  mov cx,  [CallbackSel]
  VMMcall Simulate_Far_Call

; Выполняем вызов и восстановление состояния 
; системной VM
  VMMcall Resume_Exec
  VMMcall End_Nest_Exec
  Pop_Client_State
  
; Сбрасываем семафор, разрешая обработку 
; следующего прерывания INT 21h
  mov  eax, hSem
  VMMcall Signal_Semaphore
  
  ret  
EndProc   CallbackProc
  
; ---------------------------------------------------
; GetCurDir
; Определение текущего диска и текущего каталога
; в виртуальной машине MS-DOS, выполняющей запуск
; программы с помощью прерывания INT 21h
; ---------------------------------------------------
BeginProc GetCurDir  
  
  pushad
  
; Сохраняем состояние VM MS-DOS
  Push_Client_State
  
  VMMcall Begin_Nest_Exec
  
; Определяем номер текущего диска в VM MS-DOS
; Используем для этого функцию 1900h прерывания
; INT 21h. Номер диска возвращается в регистре AL
  mov ax, 1900h
  mov word ptr [ebp.Client_AX], ax
  mov eax, 21h
  VMMcall Exec_Int

; Сохраняем номер текущего диска в VM MS-DOS
  mov ax, word ptr [ebp.Client_AX]
  mov nCurDisk, al
  
; Для определения текущего пути VM MS-DOS
; заказываем буфер размером 64 байта в адресном
; пространстве VM MS-DOS, пользуясь функцией 4800h
; прерывания INT 21h
  mov ax, 4800h
  mov word ptr [ebp.Client_AX], ax
  
; Размер буфера задается в параграфах (по 16 байт)  
  mov ax, 0004h   
  mov word ptr [ebp.Client_BX], ax
  mov eax, 21h
  VMMcall Exec_Int

; Сохраняем сегментную компоненту адреса  
; (смещение полученного буфера всегда равно 0)
  mov ax, word ptr [ebp.Client_AX]
  mov wPathSeg, ax

; В маловероятном случае, когда в VM MS-DOS  
; совсем нет свободной памяти (даже 64 байт),
; мы не заполняем строку параметров: 
; если памяти нет, все равно нельзя запустить
; ни программу MS-DOS, ни приложение Windows
  mov ax, wPathSeg
  cmp ax, 0
  jz DoNotGetPath
  
; Определяем FLAT-адрес полученного буфера
  mov [ebp.Client_ES], ax
  mov [ebp.Client_BX], 0
  mov ax, (Client_ES shl 8) + Client_BX
  VMMcall Map_Flat
  mov flatpPathBuf, eax

; Получаем строку текущего каталога VM MS-DOS,
; для чего вызываем функцию 4700h прерывания 
; INT 21h. Перед вызовом этой функции нужно
; предоставить в DS:SI адрес буфера размером
; 64 байта, в который и будет записана строка
  mov ax, wPathSeg
  mov word ptr [ebp.Client_DS], ax
  mov ax, 0
  mov word ptr [ebp.Client_SI], ax
  mov ax, 0
  mov word ptr [ebp.Client_DX], ax
  mov ax, 4700h
  mov word ptr [ebp.Client_AX], ax
  mov eax, 21h
  VMMcall Exec_Int

; Копируем строку текущего каталога в буфер  
; CallbackBuf со смещением 1 байт
  mov   ecx, flatpPathBuf
  mov   edx, CallbackBuf
  add   edx, 1
  call  short StrCpy

; Освобождаем буфер, полученный в адресном
; пространстве VM MS-DOS
  mov ax, wPathSeg
  mov word ptr [ebp.Client_ES], ax
  mov ax, 4900h
  mov word ptr [ebp.Client_AX], ax
  mov eax, 21h
  VMMcall Exec_Int
  
  VMMcall End_Nest_Exec
  
DoNotGetPath:
  
  Pop_Client_State
  popad
  ret
EndProc GetCurDir  

; ---------------------------------------------------
; IsWindowsApp
; Проверка файла запускаемой программы
;
; Функция возвращает флаг Z = 0, если запускается
; приложение Windows в формате NE или PE, и Z != 0, 
; если запускается программа MS-DOS
;
; Входные параметры:
; [ebp.Client_DS] - сегмент буфера, в котором находится
;   путь к запускаемой программе
; [ebp.Client_DX] - смещение буфера, в котором находится
;   путь к запускаемой программе
; ---------------------------------------------------
; Copyright (C) Сергей Ноженко, 1995
; ---------------------------------------------------
BeginProc IsWindowsApp

  Push_Client_State
  VMMcall Begin_Nest_Exec

; Индикатор, который будет равен нулю на выходе  
; из функции, если запускаемый файл не содержит
; приложение Windows
  sub esi, esi

; Открываем файл с программой  
  mov word ptr [ebp.Client_AX], 3D00h
  mov eax, 21h
  VMMcall Exec_Int

; Проверка ошибки  
  test word ptr [ebp.Client_Flags], 1
  jnz EndNest

; Сохраняем идентификатор файла  
  mov ax, word ptr [ebp.Client_AX]
  push eax

; Заказываем 4 параграфа памяти  
  mov word ptr [ebp.Client_AX], 4800h
  mov word ptr [ebp.Client_BX], 4
  mov eax, 21h
  VMMcall Exec_Int

; Проверяем, выделена ли нам память  
  mov ax, word ptr [ebp.Client_AX]
  or  ax, ax
  jz  CloseFile

; Читаем старый заголовок исполнимого файла 
; (заголовок программы MS-DOS)
; Готовим ES для вызова функции освобождения
; памяти
  mov word ptr [ebp.Client_DS], ax
  mov word ptr [ebp.Client_ES], ax
  mov word ptr [ebp.Client_AX], 3F00h
  pop eax
  mov word ptr [ebp.Client_BX], ax
  mov word ptr [ebp.Client_CX], 40h ; читаем 40h байт
  mov word ptr [ebp.Client_DX], 0
  mov eax, 21h
  VMMcall Exec_Int

; Проверка ошибки
  test word ptr [ebp.Client_Flags], 1
  jnz FreeMem

; Если размер файла меньше 40h байт, это
; не приложение Windows
  cmp word ptr [ebp.Client_AX], 40h
  jl  short FreeMem

; Получаем FLAT-адрес
  mov ax, (Client_DS shl 8) + Client_DX
  VMMcall Map_Flat

; Проверяем сигнатуру EXE-файла. Если это не
; EXE-файл, то мы имеем дело не с приложением
; Windows
  cmp word ptr [eax], "ZM"
  jne short FreeMem

; Проверяем смещение таблицы relocation table.  
; Если оно меньше чем 40h, это программа MS-DOS
  cmp word ptr [eax + 18h], 40h
  jl  short FreeMem

  mov edi, eax

; Ищем заголовок NewEXE
  mov word ptr [ebp.Client_AX], 4200h
  mov bx, word ptr [eax + 3Ch]
  mov word ptr [ebp.Client_DX], bx
  mov bx, word ptr [eax + 3Eh]
  mov word ptr [ebp.Client_CX], bx
  mov eax, 21h
  VMMcall Exec_Int

  test word ptr [ebp.Client_Flags], 1
  jnz short FreeMem

; Читаем первое слово заголовка NewEXE  
  mov word ptr [ebp.Client_AX], 3F00h
  mov word ptr [ebp.Client_CX], 2
  mov word ptr [ebp.Client_DX], 0
  mov eax, 21h
  VMMcall Exec_Int

  test word ptr [ebp.Client_Flags], 1
  jnz short FreeMem
  cmp word ptr [ebp.Client_AX], 2
  jne short FreeMem

; Проверяем сигнатуру исполнимого сегмента.
; Допустимы сигнатуры "NE" (segmented executable)
; и "PE" (portable executable)
  cmp word ptr [edi], "EN"
  je  short WinApp

  cmp word ptr [edi], "EP"
  jne short FreeMem

WinApp:
  
; Устанавливаем индикатор - запускается
; приложение Windows
  inc esi

; Освобождаем память
FreeMem:
  mov word ptr [ebp.Client_AX], 4900h
  mov eax, 21h
  VMMcall Exec_Int

; Закрываем файл
CloseFile:
  mov word ptr [ebp.Client_AX], 3E00h
  mov eax, 21h
  VMMcall Exec_Int

EndNest:
  VMMcall End_Nest_Exec
  Pop_Client_State

  or esi, esi
  ret
EndProc   IsWindowsApp

  VxD_CODE_ENDS
END

Заголовок виртуального драйвера VXDSRV мы описали раньше, поэтому не будем на нем останавливаться.

Инициализация реального режима

Процедура инициализации реального режима выводит текстовую строку, пользуясь функцией 09h прерывания INT 21h. Как правило, виртуальные драйверы не отображают никаких сообщений, если инициализация выполняется без ошибок.

Системная критическая инициализация

На этапе системной критической инициализации процедура VXDSRV_Sys_Crit_Init (которая вызывается для обработки системного сообщения Sys_Crit_Init) устанавливает фильтр для прерывания INT 21h. Для этого используется функция Hook_V86_Int_Chain, входящая в сервис VMM.

Перед вызовом функции Hook_V86_Int_Chain следует записать в регистр EAX номер прерывания, для которого устанавливается фильтр, а в регистр ESI - FLAT-адрес процедуры фильтра.

В нашем драйвере определена процедура V86_Int21_Handler, которая встраивается в начало цепочки других фильтров прерывания INT 21h. Мы рассмотрим эту процедуру немного позже.

Программный интерфейс драйвера

Далее, вслед за управляющей процедурой VXDSRV_Control, обеспечивающей обработку системного сообщения Sys_Crit_Init, в исходном тексте драйвера находится таблица адресов функций программного интерфейса драйвера VXDSRV_API_Call.

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

Функция vxdapiGetVersion позволяет определить версию нашего виртуального драйвера. Абсолютно любой виртуальный драйвер должен иметь в составе своего интерфейса эту функцию, причем она должна иметь номер, равный 0 (т. е. при ее вызове в регистр AX необходимо загрузить нулевое значение).

Функции vxdapiRegisterWnd и vxdapiUnregisterWnd предназначены, соответственно, для регистрации и отмены регистрации главного окна приложения dos2win.exe, а также буфера и функции обратного вызова DLL-библиотеки d2w.dll в виртуальном драйвере. Регистрация заключается в том, что драйвер сохраняет адрес функции обратного вызова и буфера в своих глобальных переменных.

Имена процедур VXDSRV_V86API_Handler и VXDSRV_PMAPI_Handler описаны в макрокоманде определения драйвера. Эти процедуры получают управление при вызове драйвера, соответственно, из виртуальной машины MS-DOS и системной виртуальной машины. Их задачей является установка флага V86CallFlag, который может быть проанализирован при необходимости определения типа виртуальной машины, из которой сделан вызов - системной или виртуальной машины MS-DOS.

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

Обратите внимание, что анализируется не текущее содержимое регистра AX, а содержимое регистра AX виртуальной машины, взятое из структуры, адрес которой передается через регистр EBP. Напомним, что в этой структуре сохраняется текущий контекст виртуальной машины на момент вызова виртуального драйвера.

Займемся теперь программным интерфейсом драйвера VXDSRV.

Процедура vxdapiGetVersion - самая простая. Она возвращает версию драйвера в регистре AX вызывающей виртуальной машины. Заметьте, что процедура не просто записывает номер версии в регистр AX, а изменяет соответствующее поле структуры контекста вызывающей виртуальной машины.

Процедура vxdapiRegisterWnd регистрирует окно приложения dos2win.exe. Эту процедуру можно вызывать только в защищенном режиме, поэтому анализируется флаг V86CallFlag.

Селектор и смещение функции обратного вызова, расположенной в DLL-библиотеке, передается этой процедуре через регистры DX и CX. Виртуальный драйвер сохраняет эти значения в глобальных переменных CallbackSel и CallbackOff.

Помимо адреса функции обратного вызова, виртуальный драйвер сохраняет адрес буфера, предназначенного для записи параметров запускаемого приложения Windows. Этот адрес передается в регистрах SI:DI.

Но драйвер работает в модели памяти FLAT, поэтому для обеспечения доступа к буферу со стороны драйвера необходимо преобразовать адрес из формата <селектор:смещение> во FLAT-адрес. Проще всего сделать это с помощью сервиса Map_Flat.

Перед вызовом этого сервиса в регистр AH необходимо загрузить смещение поля структуры контекста виртуальной машины, содержащего селекторную (или сегментную) компоненту адреса, а в регистр AL - смещение поля, содержащего компоненту смещения. Сервис Map_Flat выполнит все необходимые преобразования, причем будет приниматься во внимание тип виртуальной машины. Для виртуальной машины MS-DOS будет выполнено преобразование из формата <сегмент:смещение>, а для системной виртуальной машины - из формата <селектор:смещение>.

Результат преобразования будет записан в регистр EAX.

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

Семафоры создаются сервисом Create_Semaphore. Начальное значение семафора задается содержимым регистра ECX перед вызовом сервиса. Идентификатор созданного семафора возвращается в регистре EAX. Его необходимо сохранить для выполнения операций над семафором.

Процедура vxdapiUnregisterWnd отменяет регистрацию. Она записывает нулевые значения в глобальные переменные, содержащие адреса функции обратного вызова и буфера, которые находятся в DLL-библиотеке d2w.dll, а также уничтожает созданный при регистрации семафор, вызывая сервис Destroy_Semaphore. Идентификатор уничтожаемого семафора передается через регистр EAX.

Копирование строк

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

Процедура CopyParm предназначена для копирования строки параметров запускаемого приложения Windows.

Строка параметров копируется в буфер, физически расположенный в сегменте данных DLL-библиотеки d2w.dll. Этот буфер состоит из четырех полей. Через первый байт буфера передается буква текущего диска. Следующие 64 байта предназначены для передачи текущего каталога (на момент запуска приложения Windows). Третье поле имеет размер 128 байт и содержит путь к запускаемому приложению Windows. Последнее поле также имеет размер 128 байт и содержит параметры запускаемого приложения. Именно в это поле и записывает параметры процедура CopyParm.

Фильтр прерывания INT 21h

Следующая процедура - фильтр прерывания INT 21h. Имя этой процедуры - V86_Int21_Handler, она вставляется в цепочку фильтров на этапе критической системной инициализации.

Эта процедура, прежде всего, проверяет номер функции. Если он не равен 4B00h, процедура ничего не делает, передавая управление дальше по цепочке фильтров.

После проверки факта регистрации вызывается процедура IsWindowsApp, выполняющая проверку заголовка exe-файла. Если запускается программа MS-DOS, фильтр передает управление дальше по цепочке.

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

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

Когда фильтр вызывается в первый раз, начальное значение семафора равно 1. При этом никакого ожидания не будет. Если же до завершения обработки прерывания INT 21h произойдет новое прерывание, сервис Wait_Semaphore переведет драйвер в состояние ожидания до тех пор, пока не будет вызван сервис Signal_Semaphore, восстанавливающий состояние семафора. Этот сервис будет вызван после окончания обработки прерывания INT 21h.

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

Исходя из содержимого регистров DS:DX виртуальной машины MS-DOS при вызове функции 4B00h прерывания INT 21h, драйвер определяет FLAT-адрес командной строки, сохраняя его в глобальной переменной flatpCmdLine. Аналогично определяется адрес блока параметров, в котором находится нужный нам адрес строки параметров, указанных пользователем при запуске приложения.

Вызов функции обратного вызова

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

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

В то же время на момент вызова процедуры фильтра прерывания INT 21h активна виртуальная машина MS-DOS. Поэтому нам нужно обеспечить переключение на системную виртуальную машину и вызвать функцию обратного вызова из среды системной виртуальной машины.

Эта задача решается сервисом Schedule_VM_Event. Перед вызовом сервиса в регистр EBX необходимо записать идентификатор виртуальной машины, для которой нужно сделать вызов функции обратного вызова, в регистр ESI - адрес процедуры обратного вызова, расположенной внутри виртуального драйвера, а в регистр EDX - произвольное значение, которое будет передано функции обратного вызова.

Заметьте, что для данного сервиса в регистре ESI необходимо указать адрес процедуры обратного вызова, расположенной в нулевом кольце защиты, т. е. в виртуальном драйвере, но не в DLL-библиотеке d2w.dll. В этом нет ничего страшного, нужная нам функция обратного вызова будет вызвана той процедурой, адрес которой передается сервису Schedule_VM_Event в регистре ESI.

Для определения идентификатора системной виртуальной машины мы использовали сервис Get_Sys_VM_Handle, возвращающий искомый идентификатор в регистре EBX.

Через регистр EDX мы передаем идентификатор текущей виртуальной машины (т. е. виртуальной машины MS-DOS, запустившей приложение Windows). Этот идентификатор возвращается в регистре EBX сервисом Get_Cur_VM_Handle.

Что происходит после вызова сервиса Schedule_VM_Event?

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

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

Затем она вызывает функцию обратного вызова, расположенную в d2w.dll, используя специально предназначенный для этого сервис. Опять это нельзя сделать прямым вызовом, так как функция обратного вызова находится в памяти системной виртуальной машины в третьем кольце защиты, а процедура CallbackProc - в памяти виртуального драйвера в нулевом кольце защиты. При этом в частности, используются разные стеки.

Вызов процедуры в среде виртуальной машины

Для вызова процедуры в среде виртуальной машины виртуальный драйвер должен использовать сервис Simulate_Far_Call, загрузив в регистры CX:EDX адрес функции в формате <селектор:смещение> (или <сегмент:смещение для режима V86>).

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

Подготовив параметры в стеке при помощи сервиса Simulate_Push и вызвав сервис Simulate_Far_Call, драйвер может передать управление виртуальной машине для выполнения вызова при помощи сервиса Resume_Exec.

Описанная выше методика вызова процедуры в виртуальной машине предполагает также использование функций сервиса VMM с именами Begin_Nest_Exec и End_Nest_Exec. Первая из них открывает блок вложенного выполнения процедур в виртуальной машине, а вторая - закрывает.

После выполнения всех действий в системной виртуальной машине процедура CallbackProc восстанавливает контекст этой виртуальной машины, вызывая сервис Pop_Client_State.

На данном этапе обработку прерывания INT 21h можно считать законченной, поэтому перед возвращением управления процедура CallbackProc открывает семафор, вызывая сервис Signal_Semaphore.

Определение текущего диска и каталога

Функция GetCurDir определяет текущий диск и каталог в момент вызова фильтра прерывания INT 21h.

Для выполнения этой задачи требуется вызвать функцию 1900h прерывания INT 21h (определение текущего диска) и функцию 4700h того же прерывания (определение текущего каталога).

В данном случае текущей является виртуальная машина MS-DOS, выполняющая запуск приложения Windows. Для вызова прерываний из этой виртуальной машины мы используем только что описанную методику, но вместо сервиса Simulate_Far_Call вызываем сервис Exec_Int, предназначенный для вызова программного прерывания. Перед вызовом Exec_Int драйвер должен записать в регистр EAX номер вектора прерывания.

Если номер текущего диска определить относительно просто, так как он возвращается соответствующей функцией прерывания INT 21h в регистре AL, то для получения текущего пути придется заняться эквилибристикой.

Перед вызовом функции 4700h прерывания INT 21h, определяющей текущий каталог, в регистры DS:SI необходимо записать адрес блока памяти, размером 64 байта, в который и будет записан этот путь. Сложность заключается в том, что мы не можем зарезервировать этот блок памяти в сегменте данных виртуального драйвера, а вынуждены заказывать его в адресном пространстве виртуальной машины MS-DOS.

Наш драйвер заказывает этот блок памяти при помощи функции 4800h прерывания INT 21h, преобразует адрес блока во FLAT-адрес, вызывает функцию 4700h, заполняя полученный буфер строкой пути к текущему каталогу. Затем, пользуясь FLAT-адресом буфера, драйвер копирует эту строку в буфер, расположенный в DLL-библиотеке d2w.dll. После этого буфер, заказанный в адресном пространстве виртуальной машины MS-DOS, освобождается функцией 4900h прерывания INT 21h.

Аналогичная техника используется и в процедуре IsWindowsApp, анализирующей заголовок exe-файла.

Эта процедура заставляет виртуальную машину MS-DOS, осмелившуюся запустить приложение Windows, открыть файл, считать его заголовок в память для анализа. Разумеется, что для чтения используется буфер, заказанный динамически в адресном пространстве виртуальной машины MS-DOS.

Файл определения модуля виртуального драйвера

Файл определения модуля виртуального драйвера напоминает аналогичный файл для DLL-библиотек (листинг 5.2). В этом нет ничего удивительного, так как виртуальный драйвер и есть DLL-библиотека, только 32-разрядная.


Листинг 5.2. Файл vxdsrv\vxdsrv.def


library VXDSRV
description 'VXDSRV Virtual Device, (C) A.V. Frolov, 1995'
exetype DEV386
segments
  _LTEXT PRELOAD NONDISCARDABLE
  _LDATA PRELOAD NONDISCARDABLE
  _ITEXT CLASS 'ICODE' DISCARDABLE
  _IDATA CLASS 'ICODE' DISCARDABLE
  _TEXT  CLASS 'PCODE' NONDISCARDABLE
  _DATA  CLASS 'PCODE' NONDISCARDABLE
exports
  VXDSRV_DDB @1

Обратите внимание, что тип загрузочного модуля указан в операторе exetype как DEV386. Далее, описаны все необходимые сегменты.

Виртуальный драйвер должен экспортировать только одну точку входа. Имя этой точки входа образуется автоматически макрокомандой определения драйвера добавлением к имени драйвера суффикса "_DDB". Номер точки входа должен быть равен 1.

Трансляция и сборка виртуального драйвера

Для трансляции и сборки виртуального драйвера вам не нужно переписывать на свой диск все файлы, входящие в комплект поставки DDK. Вам потребуется только содержимое каталога ddk\386\include и несколько утилит из каталога ddk\386\tools, а именно:

специальная версия MASM masm5.exe;

редактор связей link386.exe;

утилита добавления заголовка addhdr.exe

Далее вам нужно подготовить пакетный файл, предназначенный для запуска из среды MS-DOS. Мы использовали файл m.bat, представленный в листинге 5.3.


Листинг 5.3. Файл vxdsrv\m.bat


set include=g:\inc
masm5 -p -w2 vxdsrv;
link386 vxdsrv,vxdsrv.386,,,vxdsrv.def
addhdr vxdsrv.386

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

Подключение виртуального драйвера

Для подключения драйвера VXDSRV необходимо добавить строку в раздел [386Enh] файла system.ini с указанием пути к файлу виртуального драйвера:

[386Enh]
device=f:\wadvance\src\vxdsrv\vxdsrv.386

Если скопировать драйвер в системный каталог Windows, путь можно не указывать, ограничившись только именем файла vxdsrv.386.

После перезапуска Windows драйвер будет активен.

Приложение DOS2WIN

Приложение DOS2WIN (листинг 5.4) работает в кооперации с DLL-библиотекой d2w.dll и виртуальным драйвером vxdsrv.386, описанном в предыдущем разделе. Его основная задача - обработка сообщения WM_STARTWINAPP, которое посылается из библиотеки d2w.dll, когда любая виртуальная машина MS-DOS пытается запустить приложение Windows.


Листинг 5.4. Файл dos2win\dos2win.cpp


// ======================================================
// Приложение DOS2WIN
//
// Запуск приложений Windows из
// коммандной строки MS-DOS
//
// Используется совместно с DLL-библиотекой
// d2w.dll и VxD-драйвером VXDSRV.386 версии 1.1  
// ------------------------------------------------------
// Copyright (C) 1995 Alexandr Frolov
// ======================================================
#define STRICT
#include <windows.h>
#include <mem.h>
#include <string.h>
#include <dir.h>

#include "dos2win.hpp"
#include "vxdcall.hpp"

BOOL InitApp(HINSTANCE);

LRESULT CALLBACK _export
WndProc(HWND, UINT, WPARAM, LPARAM);

extern "C" void FAR PASCAL _export
WinAppStart(HWND hwnd, LPSTR szPath);

extern "C" void FAR PASCAL _export
RegisterWnd(HWND hwnd);

BOOL CALLBACK _export
DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam);

char const szClassName[]   = "DOS2WINClass";
char const szWindowTitle[] = "dos2win";
HINSTANCE  hInst;
HMENU hmenuSystemMenu;

struct LOADPARMS
{
  WORD   segEnv;      // среда
  LPSTR  lpszCmdLine; // коммандная строка
  LPWORD lpwShow;     // режим отображения
  LPWORD lpwReserved; // зарезервировано
};
struct LOADPARMS parms;

// Точка входа API VxD-драйвера
VXDAPI vxd;

WORD  awShow[2] = { 2, SW_SHOW };
LPSTR lpszCmd, lpszParm;
char  szBuf[256];
char  szCurPath[128];

DLGPROC lpfnDlgProc;

// ======================================================
// Функция WinMain
// ======================================================
#pragma argsused
int PASCAL
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  LPSTR lpszCmdLine, int nCmdShow)
{
  MSG  msg;
  HWND hwnd;

  if(hPrevInstance) // только одна копия приложения
    return FALSE;

  if(!InitApp(hInstance))
    return FALSE;

  hInst = hInstance;

  hwnd = CreateWindow(
    szClassName, szWindowTitle,
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT,  CW_USEDEFAULT,
    0, 0, hInstance, NULL);

  if(!hwnd)
    return FALSE;

  // Получаем точку входа API VxD-драйвера
  vxd = vxdGetDeviceAPI(VXD_ID);

  // Если VxD-драйвер не загружен, выводим
  // сообщение об ошибке
  if(vxd == NULL)
  {
    MessageBox(hwnd, "Error Loading DOS2WIN\n"
      "VxD Driver VXDSRV.386 not found",
      "DOS2WIN", MB_OK | MB_ICONHAND);
    return FALSE;
  }

  // Отображаем окно в виде пиктограммы
  ShowWindow(hwnd, SW_SHOWMINIMIZED);
  UpdateWindow(hwnd);

  while(GetMessage(&msg, 0, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// ======================================================
// Функция InitApp
// Выполняет регистрацию класса окна
// ======================================================
BOOL InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass;
  WNDCLASS wc;

  memset(&wc, 0, sizeof(wc));
  wc.lpszMenuName  = "APP_MENU";
  wc.style         = 0;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(hInstance, "AppIcon");
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszClassName = (LPSTR)szClassName;

  aWndClass = RegisterClass(&wc);
  return (aWndClass != 0);
}

// ======================================================
// Функция WndProc
// ======================================================
LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
    case WM_CREATE:
    {
      // Регистрируем окно приложения в DLL-библиотеке d2w.dll
      RegisterWnd(hwnd);

      // Изменяем системное меню приложения
      hmenuSystemMenu = GetSystemMenu(hwnd, FALSE);

      AppendMenu(hmenuSystemMenu, MF_SEPARATOR, 0, 0);
      AppendMenu(hmenuSystemMenu, MF_BYCOMMAND | MF_ENABLED,
        CM_SYSABOUT, "&About...");

      return 0;
    }

    // Удаляем лишние строки из системного меню приложения
    case WM_INITMENUPOPUP:
    {
      RemoveMenu(hmenuSystemMenu, SC_RESTORE, MF_BYCOMMAND);
      RemoveMenu(hmenuSystemMenu, SC_MAXIMIZE, MF_BYCOMMAND);
      RemoveMenu(hmenuSystemMenu, SC_MINIMIZE, MF_BYCOMMAND);
      RemoveMenu(hmenuSystemMenu, SC_SIZE, MF_BYCOMMAND);
      break;
    }

    case WM_SYSCOMMAND:
    {
      switch(wParam & 0xfff0)
      {
        // Блокируем изменение размеров окна
        case SC_RESTORE: case SC_MAXIMIZE: case SC_SIZE:
          return 0;

        // Выводим диалоговую панель "About"
        case CM_SYSABOUT:
        {
          lpfnDlgProc =
            (DLGPROC)MakeProcInstance((FARPROC)DlgProc, hInst);
          DialogBox(hInst, "ABOUT", hwnd, lpfnDlgProc);
          FreeProcInstance((FARPROC)lpfnDlgProc);
          return 0;
        }

        default:
          return DefWindowProc(hwnd, msg, wParam, lParam);
      }
    }

    // Это сообщение посылается DLL-библиотекой d2w.dll,
    // когда VxD-драйвер фиксирует запуск программы любой
    // виртуальной машиной MS-DOS.
    // Параметр lParam содержит указатель на строку,
    // имеющую следующий формат:
    //
    //  Смещение Размер Описание
    //  0        1      Номер дискового устройства, которое
    //                  было текущим при запуске программы 
    //  1        64     Каталог, который был текущим
    //                  при запуске программы
    //  65       128    Путь к запускаемой программе MS-DOS
    //                  или к запускаемому приложению Windows
    //  193      128    параметры запускаемой программы
    //                  или приложения
    //

    case WM_STARTWINAPP:
    {
      // Проверка указателя
      if(lParam == NULL) return 0;

      // Путь к запускаемой программе
      lpszCmd  = (LPSTR)lParam + 65;

      // Указатель на строку параметров
      lpszParm  = (LPSTR)lParam + 65 + 128;

      // Формируем буфер параметров для функции LoadModule
      // Первый байт резервируем для размера строки
      lstrcpy(szBuf, (LPCSTR)" ");
      lstrcat(szBuf, (LPCSTR)lpszParm);

      // Записываем размер строки
      *szBuf = (BYTE)lstrlen(lpszParm);

      // Заполняем структуру LOADPARMS
      if(lstrlen(lpszParm) != 0)
        parms.lpszCmdLine = (LPSTR)szBuf;
      else
        parms.lpszCmdLine = (LPSTR)"";

      parms.segEnv = 0;
      parms.lpwShow = (LPWORD) awShow;
      parms.lpwReserved = (LPWORD) NULL;

      // Устанавливаем такой же текущий диск,
      // какой был текущим при запуске программы
      // из виртуальной машины MS-DOS
      setdisk(*(LPSTR)lParam);

      // Устанавливаем такой же текущий каталог,
      // какой был текущим при запуске программы
      // из виртуальной машины MS-DOS
      szCurPath[0] = (char)((*(LPSTR)lParam) + 'A');
      lstrcpyn((LPSTR)szCurPath + 1, (LPSTR)":\\", 3);
      lstrcat((LPSTR)szCurPath, (LPSTR)lParam + 1);
      chdir(szCurPath);

      // Выполняем попытку запуска программы.
      // Если запускается приложение Windows, оно
      // будет запущено. Если же был выполнен запуск
      // программы MS-DOS, функция LoadModule вернет
      // код ошибки, который мы игнорируем
      LoadModule(lpszCmd, &parms);
      return 0;
    }

    case WM_DESTROY:
    {
      PostQuitMessage(0);
      return 0;
    }
    default:
      break;
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

// ======================================================
// Функция vxdGetDeviceAPI
// Получение адреса точки входа API для
// VxD-драйвера, идентификатор которого
// задан параметром vxd_id
// ======================================================
VXDAPI vxdGetDeviceAPI(unsigned short vxd_id)
{
  unsigned axreg, dxreg;

  asm push  di
  asm push  es
  asm mov   ax, 0x1684
  asm mov   bx, vxd_id
  asm xor   di, di
  asm mov   es, di
  asm int   0x2f
  asm mov   ax, di
  asm mov   dx, es
  asm pop   es
  asm pop   di

  asm mov   axreg, ax
  asm mov   dxreg, dx

  return((VXDAPI)MAKELP(dxreg, axreg));
}

// ======================================================
// Функция DldProc
// Обработка сообщений диалоговой панели "About"
// ======================================================
#pragma argsused

BOOL CALLBACK _export
DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch(msg)
  {
    case WM_INITDIALOG:
      return TRUE;

    case WM_COMMAND:
    {
      switch(wParam)
      {
        case IDOK: case IDCANCEL:
        {
          EndDialog(hdlg, 0);
          return TRUE;
        }
      }
    }
  }
  return FALSE;
}

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

На этапе инициализации приложение определяет точку входа виртуального драйвера VXDSRV и отказывается работать, если этот драйвер не загружен.

Во время обработки сообщения WM_CREATE приложение вызывает функцию RegisterWnd, которая находится в DLL-библиотеке d2w.dll. В качестве единственного параметра функции передается идентификатор главного окна приложения, необходимый для передачи приложению сообщения WM_STARTWINAPP.

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

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

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

Файл dos2win.hpp содержит определение сообщения WM_STARTWINAPP, констант CM_SYSABOUT и VXD_ID (листинг 5.5).


Листинг 5.5. Файл dos2win\dos2win.hpp


#define WM_STARTWINAPP (WM_USER + 100)
#define CM_SYSABOUT 0x8880
#define VXD_ID 0x8000

В файле vxdcall.hpp (листинг 5.6) находятся символические константы для вызова программного интерфейса виртуального драйвера VXDSRV, определение типа VXDAPI (точка входа программного интерфейса виртуального драйвера) и прототип функции vxdGetDeviceAPI.


Листинг 5.6. Файл dos2win\vxdcall.hpp


#define vxdapiGetVersion    0
#define vxdapiRegisterWnd   1
#define vxdapiUnregisterWnd 2

typedef unsigned long (far *VXDAPI)(void);
VXDAPI vxdGetDeviceAPI(unsigned short vxd_id);

Файл ресурсов приложения DOS2WIN приведен в листинге 5.7. В нем определены пиктограмма и диалоговая панель "About DOS2WIN".


Листинг 5.7. Файл dos2win\dos2win.rc


#include "dos2win.hpp"

AppIcon ICON "dos2win.ico"

ABOUT DIALOG 24, 40, 151, 134
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About DOS2WIN"
BEGIN
  ICON "APPICON", -1, 4, 7, 16, 16, WS_CHILD | WS_VISIBLE
  DEFPUSHBUTTON "OK", IDOK, 59, 112, 33, 14,
     WS_CHILD | WS_VISIBLE | WS_TABSTOP
  LTEXT "Starting Windows Application"
     " from MS-DOS Command Prompt\n\nVersion 1.1",
     -1, 30, 20, 120, 36, WS_CHILD | WS_VISIBLE | WS_GROUP
  LTEXT "DOS2WIN", -1, 30, 7, 97, 7,
     WS_CHILD | WS_VISIBLE | WS_GROUP
  CONTROL "", -1, "static",
     SS_BLACKRECT | WS_CHILD | WS_VISIBLE, 31, 58, 114, 1
  LTEXT "Copyright © 1995 Alexandr Frolov"
     "\n\nfrolov@glas.apc.org",
     -1, 30, 65, 114, 28, WS_CHILD | WS_VISIBLE | WS_GROUP
  CONTROL "", -1, "static",
     SS_BLACKRECT | WS_CHILD | WS_VISIBLE, 31, 96, 114, 1
END

Файл определения модуля приложения представлен в листинге 5.8.


Листинг 5.8. Файл dos2win\dos2win.def


NAME        DOS2WIN
DESCRIPTION 'Приложение DOS2WIN, (C) 1995, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

DLL-библиотека D2W.DLL

DLL-библиотека d2w.dll (листинг 5.9) взаимодействует, с одной стороны, с приложением dos2win.exe, с другой - с виртуальным драйвером vxdsrv.386.


Листинг 5.9. Файл dos2win\d2w.cpp


// ======================================================
// DLL-библиотека d2w.dll
//
// Используется совместно с приложением dos2win.exe
// и VxD-драйвером VXDSRV.386 версии 1.1
// ------------------------------------------------------
// Copyright (C) 1995 Alexandr Frolov
// ======================================================
#define  STRICT
#include <windows.h>
#include <mem.h>
#include "d2w.h"
#include "vxdcall.hpp"

extern "C" void FAR PASCAL _export
WinAppStart(WORD wVxDVersion);

extern "C" void FAR PASCAL _export
RegisterWnd(HWND hwnd);

void UnregisterWnd(VXDAPI vxdEntry);

// Точка входа API VxD-драйвера
VXDAPI vxdApi = NULL;

// Идентификатор окна приложения dos2win.exe
HWND hwndDos2Win;

// В этот буфер VXD-драйвер VXDSRV.386 будет записывать
// текущий диск, текущий каталог, путь к запускаемой
// программе и параметры, использованные виртуальной
// машиной MS-DOS в процессе запуска программы
BYTE szCallbackBuf[350];

// Очередь запросов на запуск программ из VM MS-DOS
// Организована в виде массива
BYTE szCmd[6][350];

// Номер строки параметров в массиве szCmd
int nCmdLine = 0;

// ========================================================
// Функция LibMain
// Получает управление только один раз при
// загрузке DLL-библиотеки в память
// ========================================================
#pragma argsused
int FAR PASCAL
LibMain(HINSTANCE hModule, WORD wDataSegment,
  WORD wHeapSize, LPSTR lpszCmdLine)
{
  if(wHeapSize != 0)
    UnlockData(0);

  // Получаем точку входа API VxD-драйвера
  vxdApi = vxdGetDeviceAPI(VXD_ID);

  return TRUE;
}

// ========================================================
// Функция WEP
// ========================================================
#pragma argsused
int FAR PASCAL WEP(int bSystemExit)
{
  // Если DLL-библиотека выгружается из памяти,
  // выполняем отключение VxD-драйвера
  if(vxdApi != NULL)
    UnregisterWnd(vxdApi);
  return 1;
}

// ========================================================
// Функция обратного вызова WinAppStart
// Вызывается из VxD-драйвера VXDSRV.386
// Копирует строку, содержащую все характеристики
// запускаемой программы в очередь запросов, затем
// посылает сообщение WM_STARTWINAPP в приложение
// dos2win.exe, которое выполняет запуск.
// ========================================================
extern "C" void FAR PASCAL _export
WinAppStart(WORD wVxDVersion)
{
  // Проверяем версию VxD-драйвера, передаваемую
  // через параметр wVxDVersion
  if(wVxDVersion != 0x0101) return;

  // Копируем текущий каталог, путь к запускаемой
  // программе и ее параметры
  _fmemcpy((LPVOID)szCmd[nCmdLine], 
           (LPVOID)szCallbackBuf, 65 + 256);

  // Посылаем сообщение приложению dos2win
  PostMessage(hwndDos2Win, WM_STARTWINAPP,
    0, (LPARAM)szCmd[nCmdLine]);

  // Очередь запросов организована в виде кольца
  nCmdLine++;
  if(nCmdLine > 5) nCmdLine = 0;
}

// ========================================================
// Функция RegisterWnd
// Регистрация окна приложения dos2win.exe
// ========================================================
extern "C" void FAR PASCAL _export
RegisterWnd(HWND hwnd)
{
  if(vxdApi == NULL) return;

  // Сохраняем идентификатор окна
  hwndDos2Win = hwnd;
  if(vxdApi == NULL) return;

  // Вычисляем компоненты адреса функции обратного вызова
  unsigned sel  = SELECTOROF((LPVOID)WinAppStart);
  unsigned off  = OFFSETOF((LPVOID)WinAppStart);

  // Вычисляем компоненты адреса буфера szCallbackBuf
  unsigned bsel  = SELECTOROF(szCallbackBuf);
  unsigned boff  = OFFSETOF(szCallbackBuf);

  // Регистрируем функцию обратного вызова и
  // буфер szCallbackBuf в VxD-драйвере
  asm mov dx, sel
  asm mov cx, off
  asm mov si, bsel
  asm mov di, boff
  asm mov ax, vxdapiRegisterWnd
  (*vxdApi)();
}

// ========================================================
// Функция UnregisterWnd
// Отключение VxD-драйвера
// ========================================================
void UnregisterWnd(VXDAPI vxdEntry)
{
  if(vxdApi == NULL) return;

  asm mov ax, vxdapiUnregisterWnd
  (*vxdEntry)();
}

// ========================================================
// Функция vxdGetDeviceAPI
// Получение адреса точки входа API для
// VxD-драйвера, идентификатор которого
// задан параметром vxd_id
// ========================================================
VXDAPI vxdGetDeviceAPI(unsigned short vxd_id)
{
  unsigned axreg, dxreg;

  asm push  di
  asm push  es
  asm mov   ax, 0x1684
  asm mov   bx, vxd_id
  asm xor   di, di
  asm mov   es, di
  asm int   0x2f
  asm mov   ax, di
  asm mov   dx, es
  asm pop   es
  asm pop   di

  asm mov   axreg, ax
  asm mov   dxreg, dx

  return((VXDAPI)MAKELP(dxreg, axreg));
}

В фиксированном сегменте данных библиотеки d2w.dll хранится идентификатор главного окна приложения dos2win.exe (переменная hwndDos2Win), который используется для посылки этому приложению сообщения WM_STARTWINAPP.

Кроме этого, в этом сегменте располагается буфер szCallbackBuf, адрес которого передается виртуальному драйверу при регистрации. Именно в этот буфер виртуальный драйвер записывает строку параметров, "подсмотренную" у функции прерывания INT 21h, запускающей программы MS-DOS.

При начальной загрузке DLL-библиотеки в память функция LibMain получает адрес точки входа программного интерфейса виртуального драйвера VXDSRV, вызывая для этого функцию vxdGetDeviceAPI.

Напомним, что в процессе создания своего главного окна приложение dos2win.exe вызывает функцию RegisterWnd, определенную в библиотеке d2w.dll. Эта функция не только сохраняет полученный ей через параметр идентификатор главного окна, но и вызывает функцию регистрации vxdapiRegisterWnd из программного интерфейса драйвера VXDSRV.

В регистрах DX:CX функции регистрации драйвера передается адрес функции обратного вызова WinAppStart, определенной в DLL-библиотеке, а через регистры SI:DI - адрес буфера szCallbackBuf, в который драйвер будет записывать параметры запускаемого приложения Windows.

Функция WEP, которая вызывается при удалении DLL-библиотеки из памяти, вызывает функцию отмены регистрации UnregisterWnd, определенную в библиотеке d2w.dll. Задача функции UnregisterWnd заключается в простом вызове функции vxdapiUnregisterWnd из программного интерфейса виртуального драйвера.

Так как библиотека d2w.dll будет выгружена из памяти при завершении работы загрузившего ее приложения dos2win.exe, произойдет автоматическая отмена регистрации. Это означает, что после завершения dos2win.exe виртуальный драйвер VXDSRV будет отключен и, следовательно, запуск приложений Windows из командной строки виртуальной машины MS-DOS будет невозможен. Разумеется, до следующего запуска dos2win.exe!

Функция обратного вызова WinAppStart вызывается виртуальным драйвером.

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

На момент вызова функции WinAppStart буфер szCallbackBuf содержит строку параметров запускаемого приложения. Эта строка записывается в очередь, организованную в виде кольца на базе массива szCmd.

Вслед за этим в очередь сообщений главного окна приложения dos2win.exe посылается сообщение WM_STARTWINAPP. Это можно сделать только функцией PostMessage (или PostAppMessage), так как только эти функции допускают реентерабельный вызов.

Почему используется очередь?

Дело в том, что пользователь может запустить пакетный bat-файл, содержащий вызовы приложений Windows (да, теперь возможен и такой симбиоз между старой технологией запуска программ MS-DOS и приложениями новейшей операционной системы Windows). В этом случае функция WinAppStart будет вызываться очень часто, поэтому буфер szCallbackBuf будет перезаписываться новой информацией еще до завершения обработки старой.

Сообщение WM_STARTWINAPP и идентификатор виртуального драйвера определены в файле d2w.h (листинг 5.10).


Листинг 5.10. Файл dos2win\d2w.h


#define WM_STARTWINAPP (WM_USER + 100)
#define VXD_ID 0x8000

Файл определения модуля DLL-библиотеки d2w.dll представлен в листинге 5.11.


Листинг 5.11. Файл dos2win\d2w.def


LIBRARY        D2W
DESCRIPTION    'DLL-библиотека D2W, (C) 1995, Frolov A.V.'
EXETYPE        windows
CODE           preload fixed
DATA           preload fixed single
HEAPSIZE       1024

[Назад] [Содеожание] [Дальше]