5. Драйверы для Windows

; Lasciate ogni speranza, voi ch' entrata!
; (Leave behind every hope, you who enter!)
;
; -- Dante
Оставь надежду, всяк сюда входящий! Данте
ddk\286\keyboard\xlat.asm, строка 53

В этой главе мы затронем одну из сложнейших областей программирования для операционной системы Windows - создание драйверов.

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

В чем же сложность? Только ли в объеме листингов? Увы, нет.

Для начала сообщим, что в операционной системе Windows существуют драйверы трех типов. Причем два их них используются как в стандартном, так и в расширенном режиме работы Windows, а один - только в расширенном (к счастью, реальный режим Windows ушел в прошлое, вслед за ним идет и стандартный: Windows for Workgroups версии 3.11 работает только в расширенном режиме).

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

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

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

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

С возникновением версии 3.0 операционной системы Windows на свет появился новый тип драйверов - виртуальные драйверы.

Виртуальные драйверы не похожи ни на что другое в Windows. Это 32-разрядные DLL-библиотеки, работающие в так называемой FLAT-модели памяти. Они - чужестранцы в 16-разрядном мире Windows версий 3.0 и 3.1.

FLAT-модель является сплошной (несегментированной) моделью памяти. Однако название модели памяти не должно вводить вас в заблуждение. На самом деле сегменты памяти существуют всегда (так как даже в процессоре Pentium есть сегментные регистры!), но размер этих сегментов для FLAT-модели составляет 4 Гбайт.

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

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

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

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

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

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

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

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

Как правило, в расширенном режиме работы Windows загружаемые драйверы не обращаются к аппаратуре непосредственно. Они вызывают для этого виртуальные драйверы. Например, вместе с широко известным звуковым адаптером Sound Blaster поставляется не только загружаемый, но и виртуальный драйвер.

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

Что касается стандартных драйверов, то вы едва ли будете создавать их самостоятельно (если только вы не занимаетесь разработкой стандартной периферии компьютеров, такой как видеоконтроллеры или принтеры). В любом случае для разработки драйверов вам потребуется приобрести программный продукт Microsoft Driver Development Kit for Windows 3.1, содержащий полный комплект документации, примеры драйверов и все необходимые утилиты. Относительно подробное описание стандартных драйверов вы сможете также найти в книге "Writing Windows Device Drivers", написанной Д. Нортоном (имеется перевод этой книги на русский язык).

Лучший способ создания собственного стандартного драйвера заключается в модификации исходного текста наиболее подходящего драйвера из комплекта примеров DDK.

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

Когда у вас может возникнуть необходимость создания собственного виртуального драйвера?

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

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

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

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

Для чего вам может потребоваться загружаемый драйвер?

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

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

5.1. Стандартные драйверы

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

Драйвер видеоконтроллера

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

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

Как мы уже говорили в 14 томе "Библиотеки системного программиста", приложения Windows работают с видеоконтроллером и принтером через интерфейс графических устройств GDI, реализованный как DLL-библиотека gdi.exe. Этот интерфейс обеспечивает независимость приложений от особенностей физической аппаратуры.

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

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

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

Драйверы экспортируют многие функции, которые по выполняемым действиям можно отнести к интерфейсу GDI, например, BitBlt, ExtTextOut, SetPalette, StretchDIBits и многие другие. При этом достигается максимальная производительность, так как только разработчик видеоконтроллера знает все его аппаратные особенности.

Так как драйвер видеоконтроллера выполняет многие критические функции, возможные ошибки, допущенные при создании драйвера, могут сказаться на работе любых приложений Windows. Авторам известна ошибка в одной из версий драйвера Cirrus Logic, влияющая на режим предварительного просмотра документа текстового процессора Word for Windows. Поэтому если вы обнаружили, что ваше приложение работает неправильно (рисует не то, что нужно или даже завершается с сообщением о нарушении защиты памяти), проверьте его работу с другим драйвером или при другом разрешении.

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

Последнее обстоятельство приводит в замешательство некоторых пользователей, работающих с символами кириллицы. Установив Windows и драйвер видеоконтроллера VGA для разрешения 640х480 пикселов, такие пользователи выполняют русификацию Windows при помощи таких продуктов типа CyrWin, ParaWin и т. п. В ходе этой процедуры многие шрифты из системного каталога Windows заменяются на другие, сходные по начертанию, но имеющие в своем составе символы кириллицы.

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

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

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

Сохранение и восстановление содержимого экрана виртуальной машины MS-DOS выполняется модулем, который называется Display Grabber. Мы будем называть его модулем сохранения экрана.

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

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

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

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

Драйвер принтера

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

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

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

Так же, как и драйвер видеоконтроллера, драйвер принтера экспортирует функции BitBlt, ExtTextOut, SetPalette, StretchDIBits и другие.

Для облегчения задачи создания собственного драйвера принтера DLL-библиотека gdi.exe экспортирует "грубые" функции (Brute Functions), которые выполняют основные операции с монохромными битовыми изображениями. Имена этих функций начинаются с префикса dm, например, dmBitBlt.

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

Драйвер клавиатуры

В стандартном режиме работы Windows используется драйвер клавиатуры в виде обычной DLL-библиотеки. Основное назначение драйвера клавиатуры, как нетрудно догадаться, заключается в определении скан-кода нажатой или отпущенной клавиши, преобразования его в код ANSI и передаче этих кодов Windows для создания соответствующего сообщения WM_KEYDOWN или WM_KEYUP. Клавиатурное сообщение будет затем записано в очередь приложения, имеющего фокус ввода.

Процедура передачи кодов заключается в вызове процедуры, адрес которой Windows передает драйверу при инициализации драйвера. Эта процедура называется процедурой события (Event Procedure).

Менее очевидно второе назначение драйвера клавиатуры, которое состоит в преобразовании кодировки символов из стандарта OEM в стандарт ANSI и обратно.

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

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

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

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

Дополнительно виртуальный драйвер клавиатуры обеспечивает симулирование клавиатурного ввода при выполнении операции вставки содержимого Clipboard в окно виртуальной машины MS-DOS (эта возможность есть только в расширенном режиме работы Windows).

При этом виртуальный драйвер клавиатуры определяет метод, используемый программой MS-DOS для ввода с клавиатуры - вызов прерывания INT 16h или непосредственное сканирование клавиатурного буфера в области данных BIOS.

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

Виртуальный драйвер клавиатуры может также обеспечить доступ к клавиатуре другим виртуальным драйверам, войдя в так называемый режим сообщений (Message Mode).

Драйвер клавиатуры подключается в секции [boot] файла system.ini:

keyboard.drv=keyboard.drv

Кроме этого, используется секция [keyboard]:

[keyboard]
subtype=
type=4
keyboard.dll=
oemansi.bin=xlat866.bin

С помощью строки type задается тип клавиатуры. Значение 1 соответствует клавиатуре XT, значение 4 - клавиатуре AT.

Строка subtype нужна только для драйвера клавиатуры Olivetti и описывает разновидность клавиатуры внутри одного типа.

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

Строка oemansi.bin определяет таблицу перекодировки символов из стандарта OEM в стандарт ANSI и обратно. Для корректной перекодировки текстов, содержащих символы кириллицы, необходимо использовать специальную таблицу, которая поставляется в составе таких средств русификации Windows, как CyrWin или ParaWin.

Еще одно небольшое замечание относительно использования символов кириллицы.

Есть два подхода в решении этой проблемы. Первый заключается в использовании русифицированных версий MS-DOS, Windows и других продуктов. Русификация выполнена А.О. Microsoft, поэтому можно считать, что она сделана профессионально. При втором подходе вы отдельно приобретаете оригинальные версии MS-DOS, Windows и т. п., и отдельно средства русификации, такие, как CyrWin, ParaWin, WinOrfo, Hameleon и т. п.

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

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

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

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

Самый простой способ корректной русификации оригинальной версии MS-DOS заключается в замене файлов ega.cpi и country.sys на аналогичные файлы из русифицированной MS-DOS с внесением соответствующих изменений в файлы config.sys и autoexec.bat.

В файле config.sys следует указать код страны 7 и подключить драйвер дисплея display.sys, указав кодовую страницу 866:

COUNTRY=7,,C:\CYR\COUNTRY.SYS
DEVICE=C:\DOS\DISPLAY.SYS CON=(EGA,866,1)

В файле autoexec.bat нужно подготовить и загрузить шрифт, соответствующий 866-й кодовой странице:

C:\DOS\MODE CON CP PREP=((866) C:\CYR\EGA.CPI)
C:\DOS\MODE CON CP SEL=866

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

C:\KEYR\KEYRUS /KEYS=C:\KEYR\KBDMAIN /ROM /GRAPH=16

Далее следует русифицировать Windows, запустив CyrWin или аналогичное изделие.

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

До сих пор нам не встречалось ни одно комплексное средство русификации MS-DOS и Windows, которое бы с одной стороны, было бы удобно в работе, а с другой - выполняло бы корректную русификацию MS-DOS и Windows с заменой всех необходимых шрифтов (в частности, часто используемого шрифта MS Sans Serif). Возможно, что вы сможете решить эту проблему. Исходные тексты драйвера клавиатуры, библиотек перекодировки и таблиц перекодировки есть в DDK, так что можно попробовать!

Драйвер мыши

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

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

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

Драйвер последовательного адаптера

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

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

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

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, а именно:

Далее вам нужно подготовить пакетный файл, предназначенный для запуска из среды 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

5.3. Загружаемые драйверы

В этом разделе мы расскажем вам о последнем, третьем типе драйверов Windows. Это так называемые загружаемые или инсталлируемые драйверы (installable drivers).

Как мы уже говорили, загружаемые драйверы - это обычные DLL-библиотеки, которые экспортируют функцию с именем DriverProc.

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

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

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

Перед обращением к загружаемому драйверу его необходимо открыть функцией OpenDriver:

HDRVR OpenDriver(
  LPCSTR lpDriverName,  // имя драйвера
  LPCSTR lpSectionName, // имя секции ini-файла
  LPARAM lParam);       // адрес дополнительных данных

Займемся параметрами этой функции.

В процессе инсталляции устанавливаемого драйвера в секции [drivers] файла system.ini для каждого драйвера создается отдельная строка:

[drivers]
Wave=galaxy.drv
MIDI=galaxy.drv
AUX=sgaux.drv
WAST=wastdrv.dll

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

В качестве параметра lpDriverName для функции OpenDriver следует указывать имя драйвера, как оно определено в секции [drivers], или имя файла, содержащего драйвер.

Параметр lpSectionName позволяет загрузить драйвер, описанный в произвольной секции файла system.ini. Если же используется стандартная секция [drivers], этот параметр нужно указать как NULL.

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

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

После завершения работы с драйвером его необходимо закрыть функцией CloseDriver:

LRESULT CloseDriver(
  HDRVR hdrvr,     // идентификатор драйвера
  LPARAM lParam1,  // дополнительные данные 1
  LPARAM lParam2); // дополнительные данные 2

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

Для взаимодействия с загружаемым драйвером приложения Windows и DLL-библиотеки используют функцию SendDriverMessage, которая посылает сообщение функции DriverProc. Приведем прототип функции SendDriverMessage:

LRESULT SendDriverMessage(
  HDRVR hdrvr,     // идентификатор драйвера
  UINT msg,         // код сообщения
  LPARAM lParam1,  // первый параметр
  LPARAM lParam2); // второй параметр

Функция DriverProc

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

LRESULT CALLBACK _export DriverProc(
  DWORD  dwDiverId, // идентификатор драйвера 
  HDRVR  hDriver,   // идентификатор, который
                    // используется для обращения к драйверу 
  UINT   msg,       // код сообщения
  LPARAM lParam1,   // первый параметр сообщения
  LPARAM lParam2)   // второй параметр сообщения

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

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

Сообщения для драйвера

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

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

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

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

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

Когда пользователь выполняет установку загружаемого драйвера с помощью приложения Control Panel, функция DriverProc получает сообщение DRV_INSTALL. Обработчик этого сообщения может выполнить необходимые инициализирующие действия, например, создание разделов в файле system.ini или в других конфигурационных файлах.

Далее драйвер получает сообщение DRV_QUERYCONFIGURE. С помощью этого сообщения приложение Control Panel определяет, поддерживает ли данный драйвер функцию настройки конфигурации. Если такая функция поддерживается, обработчик сообщения DRV_QUERYCONFIGURE должен вернуть значение 1L, в противном случае - значение 0L.

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

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

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

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

Для организации взаимодействия между приложением и загружаемым драйвером можно использовать пользовательские сообщения, код которых должен быть больше DRV_RESERVED, но меньше DRV_USER. Эти две константы описаны в файле windows.h.

Драйвер WASTDRV.DLL

Загружаемый драйвер часто используется в качестве альтернативы DLL-библиотеки. Мы подготовили исходный текст загружаемого драйвера wastdrv.dll (листинг 5.12), который предназначен для совместной работы с описанным ранее виртуальным драйвером vxdsrv.386 и приложением wast.exe, которое будет описано позже.

Драйвер wastdrv.dll решает те же задачи, что и DLL-библиотека d2w.dll, однако теперь появилась возможность удобной установки всего комплекса и его включения/отключения с помощью стандартных средств Control Panel.

Итак, обратимся к исходному тексту драйвера.

Листинг 5.12. Файл wast\wastdrv.cpp

// ======================================================
// Загружаемый драйвер wastdrv.dll
// Версия 1.0
// Используется совместно с приложением wast.exe
// и VxD-драйвером VXDSRV.386 версии 1.1
// ------------------------------------------------------
// Copyright (C) 1995 Alexandr Frolov
// ======================================================
#define  STRICT
#include <windows.h>
#include <mem.h>
#include <string.h>
#include "wastdrv.hpp"
#include "wastvxd.hpp"

extern "C" LRESULT CALLBACK _export
DriverProc(DWORD  dwDiverId, HDRVR  hDriver,
  UINT msg, LPARAM lParam1, LPARAM lParam2);

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

BOOL RegisterTask(HTASK hTask);
void UnregisterTask(VXDAPI vxdEntry);

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

void AutoStartWAST(BOOL);

DWORD dwRc = 0L;

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

// Идентификатор задачи приложения wast.exe
HTASK hTaskWAST = NULL;

// Буфер для передачи информации о запукаемой
// программе
BYTE szCallbackBuf[350];

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

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

// Флаг регистрации приложения wast.exe
BOOL fTaskRegistered = FALSE;

HINSTANCE hMod;
char szDriverName[] = "wastdrv";
char szTemp[80];

// ========================================================
// Функция LibMain
// ========================================================
#pragma argsused
int FAR PASCAL
LibMain(HINSTANCE hModule, WORD wDataSegment,
  WORD wHeapSize, LPSTR lpszCmdLine)
{
  hMod = hModule;

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

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

// ========================================================
// Функция обратного вызова WinAppStart
// ========================================================
extern "C" void FAR PASCAL _export
WinAppStart(WORD wVxDVersion)
{
  if(wVxDVersion != 0x0101) return;

  _fmemcpy((LPVOID)szCmd[nCmdLine],
           (LPVOID)szCallbackBuf, 65 + 256);

  PostAppMessage(hTaskWAST, WM_STARTWINAPP,
    0, (LPARAM)szCmd[nCmdLine]);

  nCmdLine++;
  if(nCmdLine > 5) nCmdLine = 0;
}

// ========================================================
// Функция RegisterTask
// Регистрация приложения wast.exe
// ========================================================
BOOL RegisterTask(HTASK hTask)
{
  if(vxdApi == NULL)
    return FALSE;

  // Устанавливаем флаг регистрации
  fTaskRegistered = TRUE;

  // Сохраняем идентификатор окна
  hTaskWAST = hTask;

  // Вычисляем компоненты адреса функции обратного вызова
  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, vxdapiRegisterTask
  (*vxdApi)();

  return TRUE;
}

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

  // Сбрасываем флаг регистрации
  fTaskRegistered = FALSE;

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

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

  asm push  ax
  asm push  bx
  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 mov   axreg, ax
  asm mov   dxreg, dx

  asm pop   di
  asm pop   bx
  asm pop   ax

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

// ========================================================
// DriverProc
// Функция загружаемого драйвера
// ========================================================
extern "C" LRESULT CALLBACK _export
DriverProc(
  DWORD  dwDiverId, // идентификатор драйвера 
  HDRVR  hDriver,   // идентификатор, который
                    // используется для обращения к драйверу 
  UINT   msg,       // код сообщения
  LPARAM lParam1,   // первый параметр сообщения
  LPARAM lParam2)   // второй параметр сообщения
{
  switch(msg)
  {
    // Регистрация окна приложения wast.exe
    case UDRV_REGISTERTASK:
    {
      // Если виртуальный драйвер не загружен,
      // возвращаем признак ошибки FALSE
      if(vxdApi == NULL)
      {
        dwRc = (DWORD)FALSE;
        break;
      }

      // Выполняется только в том случае, если
      // разрешен запуск приложений Windows из
      // командной строки MS-DOS
      if(GetPrivateProfileInt(szDriverName,
        "Enable", 0, "system.ini"))
      {
        dwRc = (DWORD)RegisterTask((HTASK)lParam1);
      }
      break;
    }

    // Отмена регистрации приложения wast.exe
    case UDRV_UNREGISTERTASK:
    {
      if(fTaskRegistered)
         UnregisterTask(vxdApi);
      break;
    }

    // Загрузка драйвера. Это первое сообщение, которое
    // получает драйвер
    case DRV_LOAD:
    {
      dwRc = 1l;
      break;
    }

    // Это сообщение посылается драйверу перед тем,
    // как драйвер будет выгружен из памяти
    case DRV_FREE:
    {
      dwRc = 1l;
      break;
    }

    // Открытие драйвера
    case DRV_OPEN:
    {
      dwRc = 1l;
      break;
    }

    // Закрытие драйвера
    case DRV_CLOSE:
    {
      dwRc = 1l;
      break;
    }

    // Разблокирование драйвера
    case DRV_ENABLE:
    {
      dwRc = 1l;
      break;
    }

    // Блокирование драйвера
    case DRV_DISABLE:
    {
      dwRc = 1l;
      break;
    }

    // Это сообщение посылается при установке драйвера
    case DRV_INSTALL:
    {
      // Создаем раздел [wastdrv] в файле system.ini
      // В этом разделе создаем строку: "Enable=1"
      WritePrivateProfileString(szDriverName,
        "Enable", "1", "system.ini");

      // Дописываем имя приложения wast.exe в строке
      // "load=" раздела [windows] файла win.ini 
      AutoStartWAST(TRUE);

      // Запрашиваем перезапуск Windows
      dwRc = DRVCNF_RESTART;
      break;
    }

    // Удаление драйвера
    case DRV_REMOVE:
    {
      // Если окно приложения wast.exe было
      // зарегистрировано, отменяем регистрацию
      if(fTaskRegistered)
         UnregisterTask(vxdApi);

      // Посылаем приложению wast.exe сообщение
      // WM_UNLOADWAST, в ответ на которое оно
      // завершает свою работу
      if(hTaskWAST != NULL)
        PostAppMessage(hTaskWAST, WM_UNLOADWAST, 0, 0l);

      // Идентификатор задачи wast.exe теперь
      // недействителен
      hTaskWAST = NULL;

      // В разделе [wastdrv] файла system.ini
      // заменяем строку "Enable=1" на "Enable=0"
      WritePrivateProfileString(szDriverName,
        "Enable", "0", "system.ini");

      // Отменяем автоматический запуск wast.exe
      AutoStartWAST(FALSE);

      dwRc = 0l;
      break;
    }

    // Это сообщение присылается как запрос на
    // возможность конфигурирования драйвера.
    // Так как наш драйвер можно конфигурировать,
    // возвращаем значение 1l
    case DRV_QUERYCONFIGURE:
    {
      dwRc = 1l;
      break;
    }

    // Конфигурирование драйвера
    case DRV_CONFIGURE:
    {
      // Создаем диалоговую панель конфигурирования
      dwRc = DialogBox(hMod, "CONFIG", NULL, DlgProc);
      break;
    }

    // Все необработанные сообщения необходимо
    // передать функции DefDriverProc
    default:
    {
      return DefDriverProc(dwDiverId, hDriver,
         msg, lParam1, lParam2);
    }
  }
  return dwRc;
}

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

int CALLBACK _export
DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
  static BOOL fWasActive, fChanged;

  switch(msg)
  {
    case WM_INITDIALOG:
    {
      // Определяем текущее состояние строки "Enable="
      fWasActive = GetPrivateProfileInt(szDriverName,
        "Enable", 0, "system.ini");

      // В соответствии с этим состоянием отмечаем
      // переключатель, управляющий возможностью запуска
      // приложений Windows из командной строки MS-DOS
      CheckDlgButton(hdlg, ID_ENABLE, fWasActive);

      return TRUE;
    }

    case WM_COMMAND:
    {
      switch(wParam)
      {
        case IDOK:
        {
          // Флаг изменения режима работы драйвера
          // при его конфигурировании
          fChanged = TRUE;

          // Если переключатель находится во включенном
          // состоянии, разрешаем работу драйвера
          if(IsDlgButtonChecked(hdlg, ID_ENABLE))
          {
            if(fWasActive)
              fChanged = FALSE;

            // Заносим изменения в файл system.ini
            WritePrivateProfileString(szDriverName,
              "Enable", "1", "system.ini");

            // Разрешаем автоматический запуск wast.exe
            AutoStartWAST(TRUE);
          }

          // Если переключатель находился в выключенном
          // состоянии, запрещаем работу драйвера
          else
          {
            if(!fWasActive)
              fChanged = FALSE;
            WritePrivateProfileString(szDriverName,
              "Enable", "0", "system.ini");
            AutoStartWAST(FALSE);
          }

          // Если был изменен режим работы драйвера,
          // запрашиваем перезапуск Windows
          EndDialog(hdlg,
            fChanged ? DRVCNF_RESTART : DRVCNF_CANCEL);
          return TRUE;
        }

        // Отмена изменения конфигурации
        case IDCANCEL:
        {
          EndDialog(hdlg, DRVCNF_CANCEL);
          return TRUE;
        }
      }
    }
  }
  return FALSE;
}

// ======================================================
// Функция AutoStartWAST
// В зависимости от значения параметра эта функция
// дописывает имя приложения wast.exe в строке
// "load=" раздела [windows] файла win.ini
// или наоборот, удаляет имя wast.exe из этой строки 
// ======================================================
void AutoStartWAST(BOOL fStart)
{
  static LPSTR lpszSubst;

  // Получаем строку "load=..." во временный буфер
  GetProfileString("windows", "load",
    "", szTemp, 80);

  // Ищем в этой строке имя приложения wast.exe
  lpszSubst = _fstrstr(szTemp, (LPSTR)"wast");

  // Если параметр функции равен TRUE, добавляем
  // имя в строку
  if(fStart)
  {
    // Добавляем только в том случае, если этого
    // имени там нет
    if(lpszSubst == NULL)
    {
      lstrcat(szTemp, " ");
      lstrcat(szTemp, "wast");
    }
  }

  // Если параметр функции равен FALSE, удаляем
  // имя приложения wast.exe из строки
  else
  {
    // Удаляем только в том случае, если это имя
    // там действительно есть
    if(lpszSubst != NULL)
    {
      _fstrcpy(lpszSubst, lpszSubst + _fstrlen(lpszSubst));
    }
  }

  // Сохраняем новое значение строки "load=..."
  WriteProfileString("windows", "load", szTemp);
}

Функция LibMain загружаемого драйвера аналогична такой же функции DLL-библиотеки d2w.dll. Она получает и сохраняет точку входа виртуального драйвера и дополнительно запоминает в глобальной переменной идентификатор модуля загружаемого драйвера.

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

Функция WinAppStart выполняет ту же задачу, что и одноименная функция из библиотеки d2w.dll, однако делает она это немного по-другому.

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

В качестве первого параметра функции PostAppMessage необходимо передать идентификатор задачи wast.exe (а не идентификатор окна, которое, кстати, вообще не существует).

Идентификатор задачи сохраняется в глобальной переменной функцией RegisterTask, которая соответствует функции RegisterWnd библиотеки d2w.dll, но вызывается по-другому. Вызов этой функции выполняется из функции DriverProc при обработке пользовательского сообщения UDRV_REGISTERTASK, посылаемого загружаемому драйверу приложением wast.exe.

Аналогично, обработчик другого пользовательского сообщения UDRV_UNREGISTERTASK вызывает функцию UnregisterTask, действие которой полностью аналогично действию функции UnregisterWnd библиотеки d2w.dll.

Функция DriverProc обрабатывает все необходимые сообщения, реагируя на них соответствующим образом. Мы не будем повторять комментарии, приведенные в исходном тексте. Отметим только, что при установке загружаемого драйвера обработчик сообщения DRV_INSTALL дописывает имя приложения wast.exe в строке "load=" файла win.ini, обеспечивая автоматический запуск приложения wast.exe. Для этого он вызывает функцию AutoStartWAST с параметром TRUE.

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

Пользовательские сообщения для приложения wast.exe и для загружаемого драйвера, идентификатор виртуального драйвера, а также константа ID_ENABLE определены в файле wastdrv.hpp (листинг 5.13).

Листинг 5.13. Файл wast\wastdrv.hpp

#define WM_STARTWINAPP      (WM_USER + 100)
#define WM_UNLOADWAST       (WM_USER + 101)
#define UDRV_REGISTERTASK   (DRV_USER - 10)
#define UDRV_UNREGISTERTASK (DRV_USER - 11)
#define VXD_ID 0x8000
#define ID_ENABLE 101

Номера процедур программного интерфейса виртуального драйвера, точка входа программного интерфейса и прототип функции для получения точки входа определены в файле wastvxd.hpp (листинг 5.14).

Листинг 5.14. Файл wast\wastvxd.hpp

#define vxdapiGetVersion     0
#define vxdapiRegisterTask   1
#define vxdapiUnregisterTask 2
typedef unsigned long (far *VXDAPI)(void);
VXDAPI vxdGetDeviceAPI(unsigned short vxd_id);

Файл ресурсов загружаемого драйвера (листинг 5.15) содержит определение пиктограммы и диалоговой панели конфигурирования драйвера.

Листинг 5.15. Файл wast\wastdrv.rc

#include "wastdrv.hpp"

AppIcon ICON "wast.ico"

CONFIG DIALOG 11, 18, 167, 139
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Configure Driver WASTDRV"
BEGIN
  ICON "APPICON", -1, 4, 7, 16, 16, WS_CHILD | WS_VISIBLE
  DEFPUSHBUTTON "OK", IDOK, 43, 118, 33, 14,
    WS_CHILD | WS_VISIBLE | WS_TABSTOP
  LTEXT "Version 1.0", -1, 30, 18, 120, 9,
    WS_CHILD | WS_VISIBLE | WS_GROUP
  CONTROL "", -1, "static",
    SS_BLACKRECT | WS_CHILD | WS_VISIBLE, 31, 36, 129, 1
  LTEXT "Copyright © 1995 Alexandr Frolov"
    "\n\nfrolov@glas.apc.org", -1, 30, 41, 114, 28,
    WS_CHILD | WS_VISIBLE | WS_GROUP
  CONTROL "", -1, "static",
    SS_BLACKRECT | WS_CHILD | WS_VISIBLE, 31, 74, 129, 1
  LTEXT "Installable Driver WASTDRV", -1, 30, 6, 108, 12,
    WS_CHILD | WS_VISIBLE | WS_GROUP
  CONTROL "", ID_ENABLE, "BUTTON",
    BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    31, 87, 10, 15
  LTEXT "Enable Starting Windows Application from MS-DOS Command Prompt", -1, 43, 90, 124, 18,
    WS_CHILD | WS_VISIBLE | WS_GROUP
  PUSHBUTTON "Cancel", IDCANCEL, 91, 118, 32, 14,
    WS_CHILD | WS_VISIBLE | WS_TABSTOP
END

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

Листинг 5.16. Файл wast\wastdrv.def

LIBRARY        WASTDRV
DESCRIPTION    'Driver WASTDRV, (C) 1995, Frolov A.V.'
EXETYPE        windows
CODE           preload fixed
DATA           preload fixed single
HEAPSIZE       1024

Для обеспечения возможности автоматической установки комплекса, состоящего из загружаемого драйвера wastdrv.dll, виртуального драйвера vxdsrv.386 и приложения wast.exe дистрибутивная дискета должна содержать помимо перечисленных файлов файл с именем oemsetup.inf (листинг 5.17).

Листинг 5.17. Файл wast\oemsetup.inf

[disks]
1 = ., "WAST Installable Driver Disk 1"

[Installable.Drivers]
wastdrvr=1:wastdrv.dll,"WAST",
  "WAST Installable Driver","1:vxdsrv.386",

[wastdrvr]
1:wast.exe

Секция [disks] этого файла описывает дистрибутивные дискеты. В нашем случае дистрибутив системы WAST состоит из одной дискеты с номером 1.

Диск называется "WAST Installable Driver Disk 1". Именно эта строка появится в списке при установке драйвера приложением Control Panel.

Секция [Installable.Drivers] описывает устанавливаемые драйверы.

Мы устанавливаем драйвер wastdrvr, который расположен на первом диске дистрибутива. Имя соответствующего файла - wastdrv.dll.

Тип драйвера, который появится в секции [drivers] файла system.ini, указан как WAST, поэтому в секцию [drivers] будет добавлена следующая строка:

[drivers]
WAST=wastdrv.dll

В списке установленных драйверов, отображаемом приложением Control Panel, наш загружаемый драйвер будет показан с названием "WAST Installable Driver".

В добавок, на первом диске дистрибутива имеется виртуальный драйвер vxdsrv.386, который поставляется вместе с загружаемым драйвером и должен автоматически копироваться в системный каталог Windows, а также автоматически подключаться в секции [386Enh] файла system.ini.

В файле oemsetup.inf мы создали секцию [wastdrvr], в которой можно перечислить любые файлы, копируемые в системный каталог Windows в процессе установки. Мы копируем загрузочный модуль приложения wast.exe.

Приложение WAST

Приложение WAST (листинг 5.18) не создает главного окна и не знает ничего о виртуальном драйвере vxdsrv.386, хотя обращается к нему косвенно через загружаемый драйвер wastdrv.dll.

Листинг 5.18. Файл wast\wast.cpp

// ======================================================
// Приложение WAST
// ------------------------------------------------------
// Запуск приложений Windows из
// командной строки MS-DOS
//
// Используется совместно с загружаемым драйвером
// wastdrv.dll и VxD-драйвером VXDSRV.386 версии 1.1.
// Запускается автоматически при загрузке Windows.
// Автоматический запуск можно отменить, изменив
// конфигурацию драйвера WASTDRVR при помощи приложения
// Control Panel
// ------------------------------------------------------
// Copyright (C) 1995 Александр Фролов, Сергей Ноженко
// ======================================================
#define STRICT
#include <windows.h>
#include <dir.h>

#include "wast.hpp"

BOOL InitApp(HINSTANCE);

static char szDriverName[] = "wastdrv.dll";

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

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

// ======================================================
// Функция WinMain
// ======================================================
#pragma argsused
int PASCAL
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  LPSTR lpszCmdLine, int nCmdShow)
{
  if(hPrevInstance) // только одна копия приложения
    return FALSE;

  // Открываем драйвер
  HDRVR hDriver = OpenDriver(szDriverName, NULL, NULL);

  HTASK hTask;

  if(hDriver)
  {
    // Получаем идентификатор текущей задачи 
    hTask = GetCurrentTask();

    // Регистрируем в драйвере текущую задачу
    BOOL rc = (BOOL)SendDriverMessage(
          hDriver, UDRV_REGISTERTASK, (LPARAM)hTask, NULL);

    // Если не удалось зарегистрировать задачу,
    // виртуальный драйвер vxdsrv.386 не загружен
    if(!rc)
    {
      MessageBox(NULL, "Error Loading WAST\n"
       "Driver vxdsrv.386 not found",
       "Windows Application Starter", MB_OK | MB_ICONHAND);
      return FALSE;
    }
  }

  // Если не удалось открыть загружаемый драйвер,
  // выводим сообщение об ошибке
  else
  {
    MessageBox(NULL, "Error Loading WAST\n"
     "Driver wastdrv.dll not found",
     "Windows Application Starter", MB_OK | MB_ICONHAND);
    return FALSE;
  }

  MSG  msg;

  while(GetMessage(&msg, NULL, 0, 0))
  {
    // Это сообщение посылается загружаемым драйвером 
    // wastdrv.dll, когда VxD-драйвер фиксирует запуск
    // программы любой виртуальной машиной MS-DOS
    if (msg.message == WM_STARTWINAPP)
    {
      // Создаем и активируем временное невидимое окно, 
      // переключая системную виртуальную машину из фонового
      // режима в основной режим
      HWND hDummyWnd = CreateWindow("static", "", WS_POPUP,
            0, 0, 0, 0, HWND_DESKTOP, NULL, hInstance, NULL);

      ShowWindow(hDummyWnd, SW_SHOW);
      DestroyWindow(hDummyWnd);

      // Адрес командной строки
      lpszCmd  = (LPSTR)msg.lParam + 65;

      // Адрес строки параметров
      lpszParm  = (LPSTR)msg.lParam + 65 + 128;

      // Устанавливаем текущий каталог
      setdisk(*LPSTR(msg.lParam));
      szBuf[0] = *LPSTR(msg.lParam) + 'A';
      szBuf[1] = ':';
      szBuf[2] = '\\';
      lstrcpy(szBuf + 3, LPSTR(msg.lParam) + 1);
      chdir(szBuf);

      // Готовим параметры для LoadModule
      lstrcpy(szBuf, (LPCSTR)" ");
      lstrcat(szBuf, (LPCSTR)lpszParm);
      *szBuf = (BYTE)lstrlen(lpszParm);

      if(lstrlen(lpszParm) != 0)
        parms.lpszCmdLine = (LPSTR)szBuf;
      else
        parms.lpszCmdLine = (LPSTR)"";

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

      // Копируем командную строку и параметры
//      lstrcpy(szTempBuf, (LPSTR)msg.lParam + 65);
//      lstrcat(szTempBuf, (LPSTR)msg.lParam + 65 + 128);

      // Запускаем приложение Windows
      LoadModule(lpszCmd, &parms);

//      WinExec(szTempBuf, SW_SHOW);
    }

    // Это сообщение поступает от загружаемого драйвера
    // при его удалении, выполняемом из приложения
    // Control Panel
    else if (msg.message == WM_UNLOADWAST)
      PostQuitMessage(0);
    else
      DispatchMessage(&msg);
  }

  // Отменяем регистрацию текущей задачи
  SendDriverMessage(hDriver, UDRV_UNREGISTERTASK,
     (LPARAM)hTask, NULL);

  // Закрываем драйвер
  CloseDriver(hDriver, NULL, NULL);

  return msg.wParam;
}

Первое, что делает функция WinMain - открывает загружаемый драйвер, вызывая для этого функцию OpenDriver.

Затем приложение получает идентификатор текущей задачи (т. е. свой идентификатор задачи), и регистрирует его в загружаемом драйвере, посылая тому сообщение UDRV_REGISTERTASK.

Далее запускается цикл обработки сообщений.

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

После этого запускаем приложение функцией LoadModule.

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

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

Перед завершением своей работы приложение wast.exe отменяет регистрацию и закрывает загружаемый драйвер.

Все пользовательские сообщения определены в файле wast.hpp (листинг 5.19).

Листинг 5.19. Файл wast\wast.hpp

#define WM_STARTWINAPP      (WM_USER + 100)
#define WM_UNLOADWAST       (WM_USER + 101)
#define UDRV_REGISTERTASK   (DRV_USER - 10)
#define UDRV_UNREGISTERTASK (DRV_USER - 11)

Файл ресурсов приведен в листинге 5.20.

Листинг 5.20. Файл wast\wast.rc

#include "wast.hpp"
AppIcon ICON "wast.ico"

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

Листинг 5.21. Файл wast\wast.def

NAME        WAST
DESCRIPTION 'WAST: Windows App Starter, (C) 1995, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple