Создание VxD на Visual C++ без ассемблерных модулей

ОГЛАВЛЕНИЕ

Виртуальные драйверы устройств (VxD) в Windows во многих случаях являются единственным «честным» способом обхода ограничений, установленных системой для приложений Win32: невозможности прямого доступа к портам ввода-вывода и служебной памяти, эффективной обработки аппаратных прерываний, использования сервисных функций существующих VxD и т.п. Кроме того, без VxD не обходится практически ни один полноценный драйвер физического или виртуального устройства. Документация Microsoft и различные руководства по созданию VxD требуют, чтобы разработка велась на языке ассемблера. В то же время единственной официально поддерживаемой системой разработки является Microsoft Macro Assembler (MASM), синтаксис языка которого, а также надежность и удобство транслятора с начала 80-х годов оставляют желать лучшего. Гораздо более удобным средством разработки является Borland Turbo Assembler (TASM), особенно его режим Ideal, однако поставляемые Microsoft включаемые файлы, содержащие необходимые для драйвера определения, имеют множество специфических для MASM (и, честно говоря, чертовски уродливых) конструкций, что делает эти файлы непригодными для трансляции с помощью TASM.

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

Microsoft не поддерживает средств разработки VxD на «чистых» C/C++; набор для разработки драйверов (DDK) для Windows 95 содержит некоторые файлы для создания отдельных модулей на этих языках, однако «костяк» VxD, по замыслу Microsoft, должен строиться на языке ассемблера. Сторонние фирмы выпускают библиотеки, облегчающие разработку VxD на C и C++ — VtoolsD/DriverStudio (NuMega), VxDWriter (TechSoft Pvt.), DriverX (Tetradyne Software) и т.п., однако каждая из этих библиотек построена по определенному принципу и налагает на программиста ряд не всегда удобных для него правил.

В то же время разработка VxD на «чистых» C/C++ не представляет абсолютно никакой сложности, если немного разобраться в структуре VxD и того кода, который создается компиляторами. Более того, 32-разрядные среды Microsoft Visual C++ изначально имеют возможность построения модулей VxD средствами самой среды, не прибегая к внешним трансляторам или утилитам.

Компилятор MS VC++ версии 4.1 содержал ошибку, не позволявшую строить правильные VxD, отчего распространилось мнение, будто это невозможно в принципе. Однако версии 4.0, 4.2, 5.x и 6.x могут быть успешно использованы для создания VxD без выхода из среды разработки.

Единственное, что невозможно сделать полностью на Visual C++ — это построить VxD, использующий 16-разрядный код, который 32-разрядный компилятор Visual C++ создавать не способен. 16-разрядный код необходим в процессе инициализации системы в фазе реального режима (real mode), а также при размещении фрагментов кода внутри виртуальных машин V86. В этом случае требуется подключение внешних ассемблерных модулей, транслируемых при помощи MASM или TASM, однако основная часть драйвера все равно может быть сделана в системе Visual C++.

В данной статье рассматриваются вопросы разработки VxD на «чистом» C++, в среде MS VC++ 4.2, свободной от упомянутой ошибки компилятора.

Статья ни в коей мере не претендует на полное описание вопросов разработки VxD. Здесь даны лишь основные сведения, позволяющие сделать работоспособный VxD «с нуля».

Документацию из Windows DDK можно найти в онлайновой библиотеке Microsoft.


 

Основные свойства и особенности драйвера VxD

Смысл и назначение драйвера

VxD расшифровывается как Virtual x Driver — драйвер виртуального устройства x. Поскольку Windows построена на концепции виртуальных машин, каждой виртуальной машине нужно предоставить собственный «образ» каждого из имеющихся в системе устройств. Например, виртуальный драйвер клавиатуры VKD единолично управляет работой физического устройства — клавиатуры, получая все прерывания при нажатии клавиш, включая/выключая индикаторы и т.п. Каждая из виртуальных машин — системная, в которой работают программы Windows, и окна DOS «видят» только независимые копии физического устройства; каждая из виртуальных машин может считать, что имеет в своем пользовании полноценное физическое устройство.

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

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

Наиболее сложной и удобной является полная виртуализация, которую можно наблюдать на примере работы приложений DOS в окнах Windows. При этом каждая виртуальная машина DOS «видит» практически полноценный аппаратный видеоадаптер, может обращаться к его регистрам, напрямую работать с видеопамятью и т.п.; VxD, создающий этот образ, корректно обрабатывает все стандартные виды обращений, а состояние видеопамяти отображает в окне Windows заданного размера.

Помимо своего основного назначения — виртуализации устройств для виртуальных машин — VxD выполняют в Windows множество других функций. Можно сказать, что VxD в Windows 9x реализует понятие «служебный привилегированный процесс» — с его помощью реализуются практически все задачи, которые невозможно корректно выполнить посредством обычного приложения или DLL. При отсутствии для какого-либо аппаратного устройства стандартного системного представления (например, измерительного адаптера узкого применения) для него разрабатывается VxD, посредством которого приложения могут получить доступ к функциям устройства, не мешая при этом друг другу.

Все VxD в Windows управляются главным системным VxD — диспетчером виртуальных машин (VMM — Virtual Machine Manager). VMM предоставляет основной набор сервисных функций, при помощи которых остальные VxD выполняют необходимые им операции.

Имя и идентификатор драйвера

Каждый VxD в системе должен иметь имя (Name) и идентификатор (Id). Имя драйвера (устройства) состоит из восьми или менее символов; оно часто совпадает с именем файла драйвера, однако это не обязательно. Идентификатор драйвера представляет собой 16-разрядное число, присвоенное Microsoft данному драйверу (собственному или созданному сторонними разработчиками).

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

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

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


 

Статические и динамические драйверы

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

Порядок загрузки статических драйверов

Статические драйверы загружаются системой в определенном порядке при этом основные драйверы должны загружаться первыми, а после этого — зависящие от них драйверы более высокого уровня. Для этой процедуры каждый драйвер имеет параметр Init Order — числовую константу, определяющую место драйвера в списке загрузки, которая происходит в порядке возрастания значений параметра. Системным драйверам назначены определенные значения, отражающие их зависимость друг от друга. Если порядок загрузки не имеет смысла — используется нулевое значение параметра; такие драйверы загружаются после завершения инициализации «номерных» VxD.

Системные сообщения драйверу

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

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

Сервисные функции драйвера

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

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

Обязательна для поддержки только функция с нулевым номером — Get Version (запрос версии). Поддержка и назначение остальных функций оставлена на усмотрение разработчика драйвера.

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

Интерфейс с прикладными программами

Для взаимодействия с прикладными программами VxD может предоставлять три вида API (Application Program Interface — интерфейс прикладных программ):

  • V86 API — для 16-разрядных приложений DOS (программ режима V86).
  • PM API — для 16-разрядных приложений Windows, которые до появления Windows 9x назывались Protected Mode Applications (приложения защищенного режима).
  • Win32 API — для приложений Win32.

Для обработки запросов от 16-разрядных программ в драйвере предусматриваются две различные функции — для запросов от виртуальных машин DOS и для запросов от приложений Win16. Запросы от приложений Win32 передаются в виде системных сообщений их общему диспетчеру.

16-разрядные приложения получают доступ к своим API посредством функции 0x1684 программного прерывания 2F, которая возвращает адрес шлюза (gate) для вызова VxD. Поиск нужного драйвера возможен как по идентификатору, так и по имени; поиск по имени был введен позднее, поэтому документирован не во всех описаниях функции int 2F.

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

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

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


 

Структура и функционирование драйвера

VxD представляет собой 32-разрядный исполняемый файл формата LE (Linear Executable), который является частным случаем DLL. Система может вызывать VxD тремя различными способами:

  1. Через диспетчер системных сообщений.
  2. Через таблицу обработчиков сервисных функций.
  3. Через точки входа интерфейсов прикладных программ.

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

Секции файла драйвера

Загружаемый файл драйвера стандартным образом может делиться на секции (сегменты) с различными атрибутами (резидентный, изначально загруженный, уничтожаемый и т.п.). Документация Microsoft утверждает, что секции должны иметь определенные имена, однако это сделано лишь для удобства пользования стандартным макросами и фактически система распознает лишь сами атрибуты секций.

Рекомендованы следующие имена и классы секций для модуля VxD:

  • _LTEXT, _LDATA (класс LCODE) — 32-разрядные секции резидентных кода и данных, которые должны находиться в памяти постоянно. В эти секции включаются процедуры и данные диспетчера системных сообщений, сервисных функций и API, обработчиков прерываний, а также те фрагменты драйвера, для которых нужна предельная скорость выполнения;
  • _PTEXT, _PDATA (класс PCODE) — 32-разрядные секции выгружаемых, или откачиваемых, (pageable) кода и данных, которые в процессе работы системы могут быть автоматически выгружены (paged, swapped) на диск для освобождения системной памяти. Подсистема подкачки VMM автоматически возвращает выгруженные страницы в память в момент запроса к ним, однако такие запросы допускаются только в обычном режиме работы и недопустимы при обработке прерываний или критических системных функций;
  • _ITEXT, _IDATA (класс ICODE) — 32-разрядные секции кода и данных, используемых только при инициализации драйвера. После отработки драйвером функции инициализации VMM автоматически удаляет эти секции из памяти;
  • _RTEXT (класс RCODE) — 16-разрядная секция, в которой размещаются код и данные, используемые при инициализации в реальном режиме (real mode) на этапе начальной загрузки системы. Имеет смысл только для статических драйверов.

Блок описателя драйвера

Ключевым элементом драйвера является структура данных DDB (Device Descriptor Block — блок описателя устройства), которая описывает параметры драйвера. DDB содержит следующие важнейшие поля:

  • Идентификация устройства (драйвера) — имя модуля драйвера (максимум восемь символов) и его идентификатор.
  • Версия драйвера — старший (major) и младший (minor) номера версий, определяющих функциональность драйвера (устройства).
  • Адрес процедуры диспетчера системных сообщений — адрес функции диспетчера, которая будет вызываться для обработки системных сообщений, посылаемых драйверу.
  • Адрес и размер таблицы обработчиков сервисных функций — адрес таблицы указателей (адресов) индивидуальных процедур, которые будут вызываться для выполнения сервисных функций, и количество сервисных функций, выполняемых драйвером.
  • Адреса обработчиков функций API — адреса индивидуальных процедур, которые вызываются для обработки запросов API от 16-разрядных программ (виртуальных машин DOS и программ Win16).

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


 

Контексты

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

Поскольку VxD не является полноценным системным процессом и не имеет собственного контекста, его вызов всегда происходит в контексте какой-либо виртуальной машины (системной или DOS). Если вызов происходит в контексте системной виртуальной машины, то имеет место также контекст текущего приложения, а также задачи (thread), если текущим является приложение Win32.

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

Вызов драйвера, относящегося к определенной виртуальной машине или задаче, всегда происходит в момент работы этой виртуальной машины/задачи; в таких случаях контекст называется определенным, или неслучайным (non-arbitrary context). Вызов, инициированный внешней причиной — прерыванием или другим событием, может случиться в момент работы произвольной виртуальной машины/задачи; в этом случае контекст называется неопределенным, или произвольным (arbitrary context).


 

Доступ к памяти

В контексте приложения Win32 или виртуальной машины DOS драйвер имеет прямой доступ к их адресному пространству. Для виртуальных машин требуется лишь приведение 16-разрядных адресов типа «сегмент:смещение» к линейным, расположенным в пределах первого мегабайта 32-разрядного адресного пространства.

В контексте 16-разрядного приложения Windows подобной «прямой видимости» нет, и для прямого доступа к данным приложения необходимо выполнить отображение (mapping) фрагментов 16-разрядного адресного пространства приложения в «плоское» (flat) 32-разрядное адресное пространство драйвера. В результате этой операции в области системных адресов создаются страницы, отображенные на те же адреса физической памяти, что и заданные адреса 16-разрядного приложения.

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

Поскольку системная область (system arena) доступна для чтения всем приложениям Win32, они могут считывать локальные данные VxD по возвращенным им указателям. Однако доступ приложений к системой области памяти в общем случае не рекомендуется.

Повторная входимость

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

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

Загрузка, работа и выгрузка драйвера

Все VxD загружаются в системную область памяти (system memory arena), начинающуюся с адреса 0xC0000000. Сразу после загрузки статическому драйверу — сообщение DEVICE_INIT; динамическому драйверу передается сообщение SYS_DYNAMIC_DEVICE_INIT.

Последовательность передачи сообщений при инициализации статического драйвера на самом деле немного сложнее; точное описание процесса можно найти в документации DDK.

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

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

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

Ответственность за корректную «чистку» перед выгрузкой динамического драйвера возложена на его разработчика. Система не в состоянии проверить, действительно ли удалены все ссылки на объекты драйвера; если, например, драйвер сделал запрос на таймерное или иное событие, после чего был выгружен и не аннулировал этого запроса — при наступлении события VMM попытается вызвать заданную процедуру обработки, которая к этому времени уже не существует, что приведет к непредсказуемым последствиям, вплоть до полного зависания системы.


 

Особенности разработки VxD на C++

Среда выполнения

Среда выполнения (working environment) драйвера VxD сильно отличается от среды, в которой выполняются приложения Win32. VxD работают в режиме ядра операционной системы на нулевом уровне привилегий (ring 0), в отличие от приложений, работающих на уровне 3. Для VxD в общем случае недоступны функции Windows API; вместо них необходимо пользоваться специальными сервисными функциями VMM и других VxD.

Стандартные библиотеки

Наличие специализированной среды выполнения означает, что VxD, написанный на C или C++, в общем случае не может пользоваться функциями стандартной библиотеки исполнения (RTL). Возможно использование лишь тех функций, которые заведомо не содержат ссылок к Windows API. Например, в VxD возможно использование функций strlen, strcpy, strset или strcmp, однако функция strcoll в Visual C++ содержит обращения к API, чтобы определить параметры языка, и потому для использования в VxD не годится. То же относится к функциям sprintf, time и многим другим. Среди сервисных функций VMM содержится немало аналогичных по смыслу, но отличных по формату вызова и схеме работы операций.

Вспомогательные функции (wrappers)

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

Обертки для ряда часто используемых сервисных функций VMM и других стандартных VxD определены в соответствующих включаемых файлах из DDK — VMM.H, VTD.H, SHELL.H и пр., а также в файле VXDWRAPS.H. Написание остальных предоставляется разработчику VxD. В конце статьи приведен пример написания обертки SelectorMapFlat.

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

Функции, вызываемые извне

Некоторые внутренние процедуры VxD, оформленные в виде функций C++, должны вызываться системой (callback). К ним относятся, например, диспетчер системных сообщений, обработчики сервисных функций, прерываний, событий и т.п. Соглашения о связях в таких вызовах часто рассчитаны на использование языка ассемблера, когда параметры передаются в регистрах, а результат возвращается в регистрах и/или флагах процессора. В таком случае стандартное оформление функции, принятое в C++, может стать существенной помехой.

Расширение Visual C++ содержит квалификатор __declspec (naked), помещение которого в заголовке определяемой функции запрещает генерацию пролога/эпилога функции — начальной (сохранение регистров, установка указателя кадра) и завершающей (восстановление регистров, команда возврата) последовательностей команд. Результат компиляции будет содержать только код, присутствующий в теле функции в явном виде. Это позволяет программисту выполнить нужные действия в необходимой последовательности, однако налагает требования по соблюдению внутренних правил языка. В частности, должны быть сохранены регистры EBX, ESI, EDI, EBP; при использовании параметров в такой функции должен быть явно установлен указатель кадра в регистре EBP, а при использовании локальных переменных — зарезервировано достаточное количество байтов в стеке.

Для возврата из naked — функции в ее тело должна быть явно помещена команда _asm ret, если только функция не использует средств вроде VxDJmp, которые сами выполняют возврат в функцию, из которой был сделан вызов.

Неявные обращения к функциям поддержки

Visual C++ может генерировать неявные обращения к функциям из стандартной библиотеки при использовании некоторых конструкций языка, например исключений (exceptions) и RTTI, поэтому для применения этих возможностей необходимо написание собственной подсистемы их поддержки в VxD.

При использовании виртуальных функций компилятор назначает каждой чисто виртуальной (pure virtual) функции ссылку на библиотечную функцию _purecall. Это делается для того, чтобы отловить все ошибочные обращения к чисто виртуальной функции, для которой не определена реальная функция-адресат. Стандартная функция _purecall выводит диагностическое сообщение и завершает работу программы, используя Windows API; чтобы сделать возможным применение виртуальных функций в VxD, необходимо в одном из его модулей определить свой вариант:

int _purecall (void) {
return 0;
}

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

Экспорт ссылки на DDB

Экспорт ссылки на DDB по номеру обычно принято делать в DEF-файле, однако в Visual C++ для этого есть удобный квалификатор __declspec (dllexport), который достаточно поместить в заголовке определения структуры DDB.

Структура DEF-файла для построения VxD

DEF-файл для построения VxD может содержать опции VXD, DESCRIPTION и SECTIONS.

Опция VXD имеет вид:

VXD имя тип

  • имя — имя модуля драйвера, а типDEV386 для статического или DYNAMIC — для динамического драйвера.

Опция DESCRIPTION:

DESCRIPTION строка_описания

  • строка_описания — произвольная строка, описывающая драйвер, заключена в апострофы (') или двойные кавычки (").

Опция SECTIONS:

SECTIONS

имя [CLASS 'класс'] список_атрибутов

  • имя — имя секции (сегмента).
  • класс — класс секции (имя набора секций, внутри которой они компонуются подряд).
  • список_атрибутов — атрибуты секции: EXECUTE — исполняемая, READWRITE — доступная для записи, PRELOAD — загружаемая без явного запроса, DISCARDABLE — автоматически выгружаемая при отсутствии обращения. Названия атрибутов разделяются пробелами.

Установки компилятора и компоновщика

В установках компилятора в среде Visual C++ необходимо запретить обработку исключений и RTTI, установить однозадачную (single-threaded) стандартную библиотеку (RTL). Для корректной компиляции включаемых файлов из DDK, которые предназначены не только для VxD (например, MMSYSTEM.H) необходимо определить (в установках препроцессора или в тексте программы) символическое имя Win32_VxD.

В установках компоновщика (linker) необходимо убрать все библиотеки Windows API, ибо они предназначены только для стандартной среды выполнения. При использовании стандартных функций-оберток из DDK необходимо подключить библиотеку VXDWRAPS.CLB из DDK.

В командной строке компоновщика, отображаемой внизу окна настроек, необходимо вручную добавить опцию /VXD. Также нужно внести в список файлов проекта DEF-файл, управляющий построением модуля и содержащий название драйвера и описание используемых секций (сегментов). В среде Visual C++ имеется возможность обойтись без использования DEF-файла, внося все необходимые опции в командную строку компоновщика, однако это менее удобно, так как строка быстро загромождается и становится трудной для восприятия и редактирования.

Параметры секций

Компилятор Visual C++ по умолчанию создает секции четырех типов:

  • .text — код программы;
  • .data — данные программы;
  • .rdata — константы программы (данные только для чтения);
  • .bss — неинициализированные данные.

В DEF-файле этим секциям должны быть приписаны атрибуты EXECUTE, PRELOAD (как для класса резидентного кода LCODE).

При необходимости можно помещать код и данные в другие секции при помощи директив #pragma code_seg, #pragma data_seg и #pragma alloc_text, приписав им необходимые атрибуты. Это может понадобиться, например, для выделения части кода/данных, используемых только при инициализации или разрешенных для откачки (атрибут DISCARDABLE).

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

Функции, импортируемые из библиотеки VXDWRAPS.CLB, используют в основном секции _LTEXT и _LDATA.


 

Отладка

По причине работы в специализированной среде отладка VxD при помощи встроенного отладчика среды Visual C++ невозможна. Для полной отладки драйвера удобнее всего использовать отладчик SoftICE фирмы NuMega. SoftICE воспринимает всю отладочную информацию, сгенерированную компилятором, так что при разработке VxD, как и обычных приложений, можно использовать отладочный (debug) и оконечный (release) виды проектов. Для извлечения отладочной информации из модуля драйвера SoftICE имеет в комплекте утилиту nmsym, вызов которой удобно включать в список специальных действий (custom build) среды Visual C++.

Для поверхностной отладки посредством трассировочных сообщений (Out_Debug_String и т.п.) можно использовать различные системные мониторы — например Monitor (Vireo Software), DbgTerm (TechSoft Pvt.) и любые другие, которые перехватывают системные отладочные сообщения и отображают их в окне. Например, драйвер KbdFlip я отлаживал исключительно при помощи утилиты Monitor, ни разу не прибегнув к SoftICE.

Monitor и DbgTerm также позволяют загружать и выгружать динамические VxD, причем Monitor делает это более надежно. При разработке динамических VxD можно использовать Monitor и SoftICE вместе: первый — для загрузки/выгрузки драйвера, второй — для отладки. SoftICE перехватывает весь отладочный поток Windows, так что выводимые сообщения будут видны только в нем.


 

Общая схема драйвера VxD

Минимальный виртуальный драйвер должен содержать секцию резидентного кода, в которой расположены блок описателя устройства, диспетчер системных сообщений и обработчики сервисных функций.Обработчик системных сообщений анализирует код сообщения, переданный в EAX, выделяет интересующие его сообщения, обрабатывает их и возвращает сброшенный флаг CF в случае успеха, и установленный — в случае неудачи. Для всех необрабатываемых сообщений должен возвращаться сброшенный флаг CF.Обработчики сервисных функций вызываются по таблице, так что каждой функции соответствует собственный обработчик. Способ передачи параметров и возврата результатов определяется разработчиком.При необходимости драйвер может содержать обработчики запросов V86 и PM API. Для доступа к данным виртуальных машин DOS, указатели на которые могут передаваться в регистрах при запросе, достаточно преобразовать их в линейные 32-разрядные адреса, ибо первый мегабайт адресного пространства текущей виртуальной машины непосредственно «виден» из VxD. Для доступа к данным приложений Win16 потребуется выполнить отображение адресов посредством функции VMM _SelectorMapFlat.

Программирование VxD

Средства разработки, включаемые файлы и библиотекиМинимально необходимый набор включаемых файлов и библиотек содержится в Windows 95 DDK (подкаталоги Inc32 и Lib). Обычно требуется включение хотя бы файлов BASEDEF.H и VMM.H.Файлы VXDWRAPS.H, CONFIGMG.H и некоторые другие оформлены в стиле обычного языка C, поэтому при включении их в файлы типа CPP директивы #include необходимо помещать внутрь квалификатора extern "C":

extern "C" {
#include <vxdwraps.h>
}

 

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

Структуры, обычно используемые в VxD

VxD_Desc_Block - блок описателя устройстваОписывает структуру DDB. Заполняется статически, чтобы к моменту загрузки драйвера все поля имели нужные значения.

ULONG DDB_Next;
USHORT DDB_SDK_Version;
USHORT DDB_Req_Device_Number;
UCHAR DDB_Dev_Major_Version;
UCHAR DDB_Dev_Minor_Version;
USHORT DDB_Flags;
UCHAR DDB_Name [8];
ULONG DDB_Init_Order;
ULONG DDB_Control_Proc;
ULONG DDB_V86_API_Proc;
ULONG DDB_PM_API_Proc;
ULONG DDB_V86_API_CSIP;
ULONG DDB_PM_API_CSIP;
ULONG DDB_Reference_Data;
ULONG DDB_Service_Table_Ptr;
ULONG DDB_Service_Table_Size;
ULONG DDB_Win32_Service_Table;
ULONG DDB_Prev;
ULONG DDB_Size;
ULONG DDB_Reserved1;
ULONG DDB_Reserved2;
ULONG DDB_Reserved3;

  • DDB_Next — поле для адреса следующего DDB в списке VMM. Инициализируется нулем.
  • DDB_SDK_Version — версия DDK, с которой построен драйвер. Инициализируется константой DDK_VERSION.
  • DDB_Req_Device_Number — идентификатор устройства. При отсутствии назначенного идентификатора задается нулевое значение.
  • DDB_Dev_Major_Version — старшая часть номера версии драйвера.
  • DDB_Dev_Minor_Version — младшая часть номера версии драйвера.
  • DDB_Flags — для служебных флагов VMM. Инициализируется нулем.
  • DDB_Name — имя устройства, дополненное пробелами до восьми символов.
  • DDB_Init_Order — позиция драйвера в списке загрузки. Если порядок загрузки не важен, используется константа UNDEFINED_INIT_ORDER (нуль).
  • DDB_Control_Proc — адрес функции диспетчера системных сообщений.
  • DDB_V86_API_Proc — адрес функции диспетчера V86 API.
  • DDB_PM_API_Proc — адрес функции диспетчера PM API.
  • DDB_V86_API_CSIP — служебное поле, инициализируется нулем.
  • DDB_PM_API_CSIP — служебное поле, инициализируется нулем.
  • DDB_Reference_Data — служебное поле, инициализируется нулем.
  • DDB_Service_Table_Ptr — указатель таблицы адресов процедур—обработчиков сервисных функций.
  • DDB_Service_Table_Size — количество сервисных функций, реализованных в драйвере.
  • DDB_Win32_Service_Table — служебный указатель таблицы функций Win32, инициализируется нулем.
  • DDB_Prev — поле для адреса предыдущего DDB в списке VMM, инициализируется константой 'Prev'.
  • DDB_Size — размер структуры описателя.
  • DDB_Reserved1 — служебное поле. Инициализируется константой 'Rsv1'.
  • DDB_Reserved2 — служебное поле. Инициализируется константой 'Rsv2'.
  • DDB_Reserved3 — служебное поле. Инициализируется константой 'Rsv3'.

Client_Reg_Struc - структура пакета регистров клиента

Описывает состояние регистров процессора в вызвавшей виртуальной машине/приложении (клиенте).

ULONG Client_EDI;
ULONG Client_ESI;
ULONG Client_EBP;
ULONG Client_res0;
ULONG Client_EBX;
ULONG Client_EDX;
ULONG Client_ECX;
ULONG Client_EAX;
ULONG Client_Error;
ULONG Client_EIP;
USHORT Client_CS;
USHORT Client_res1;
ULONG Client_EFlags;
ULONG Client_ESP;
USHORT Client_SS;
USHORT Client_res2;
USHORT Client_ES;
USHORT Client_res3;
USHORT Client_DS;
USHORT Client_res4;
USHORT Client_FS;
USHORT Client_res5;
USHORT Client_GS;
USHORT Client_res6;
ULONG Client_Alt_EIP;
USHORT Client_Alt_CS;
USHORT Client_res7;
ULONG Client_Alt_EFlags;
ULONG Client_Alt_ESP;
USHORT Client_Alt_SS;
USHORT Client_res8;
USHORT Client_Alt_ES;
USHORT Client_res9;
USHORT Client_Alt_DS;
USHORT Client_res10;
USHORT Client_Alt_FS;
USHORT Client_res11;
USHORT Client_Alt_GS;
USHORT Client_res12;

Поля с именами Client_xxx содержат значения соответствующих регистров на момент обращения виртуальной машины или приложения к системной функции. В поле Client_Error может быть занесен код ошибки.Для структуры введен синоним типа (typedef) с именем CRS. В файле VMM.H определены также вспомогательные структуры Client_Word_Reg_Struc и Client_Byte_Reg_Struc и объединение всех трех структур CLIENT_STRUCT.

DIOCParams - параметры запроса DeviceIoControl

DWORD Internal1;
DWORD VMHandle;
DWORD Internal2;
DWORD dwIoControlCode;
DWORD lpvInBuffer;
DWORD cbInBuffer;
DWORD lpvOutBuffer;
DWORD cbOutBuffer;
DWORD lpcbBytesReturned;
DWORD lpoOverlapped;
DWORD hDevice;
DWORD tagProcess;

  • VMHandle — идентификатор виртуальной машины, сделавшей запрос.
  • dwIoControlCode — код функции. Константы для определенных в системе кодов функций имеют префикс DIOC_, остальные функции определяются разработчиком.
GETVERSION (0) Открывание и опрос интерфейса. Если драйвер не поддерживает Win32 API, он должен вернуть в EAX ненулевое значение. В противном случае в EAX возвращается нуль, а если задан буфер результата, то в него заносится номер версии драйвера.
CLOSEHANDLE (-1) Закрывание интерфейса. Драйвер должен прервать обработку всех асинхронных запросов по этому устройству и освободить относящиеся к нему ресурсы.
  • lpvInBuffer — указатель исходного буфера.
  • cbInBuffer — размер исходного буфера в байтах.
  • lpvOutBuffer — указатель буфера результата.
  • cbOutBuffer — размер буфера результата в байтах.
  • lpcbBytesReturned — поле для объема в байтах данных, занесенных драйвером в буфер результата.
  • lpoOverlapped — указатель структуры типа OVERLAPPED (описатель адреса внутри файла и/или данных для асинхронной операции).
  • hDevice — идентификатор устройства.
  • tagProcess — идентификатор запроса. Вместе с полем hDevice образует уникальный внутри системы идентификатор запроса, по которому запрос может быть найден и аварийно прерван при получении запроса CLOSEHANDLE.

 


Функции VxD, вызываемые из системы

Диспетчер системных сообщенийДиспетчер системных сообщений драйвера представляет собой функцию, получающую параметры в регистрах и возвращающую результат во флаге процессора CF (carry flag).Код сообщения передается в регистре EAX. При возврате флаг CF должен быть сброшен, если сообщение обработано успешно, и установлен, если произошли ошибка или отказ в обслуживании. Для всех сообщений, которые не обрабатываются данным VxD, должен возвращаться сброшенный флаг CF.Все регистры, не участвующие в возврате результата, должны быть сохранены. Рекомендуется оформлять диспетчер в виде naked–функции, чтобы гарантировать сохранение состояния флага CF после возврата, либо следить за кодом, который порождается компилятором.

Обработчики сервисных функций

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

Обработчики вызовов API

Обработчики вызовов API получают в регистре EBX идентификатор (handle) текущей виртуальной машины (VM) клиента, а в регистре EBP — адрес структуры регистров клиента.По стандартному соглашению, при общении к API драйвера в регистре AH передается номер функции, а в AL — номер подфункции. Остальные регистры могут передавать другие параметры запроса.

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


 

Системные средства поддержки VxD

Некоторые системные сообщения

DEVICE_INIT - инициализация статического драйвера

  • EBX — идентификатор системной виртуальной машины.
  • ESI — адрес командной строки VMM в его PSP. Первый байт строки определяет ее длину.

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

SYS_DYNAMIC_DEVICE_INIT - инициализация динамического драйвераСообщение посылается после загрузки динамического драйвера для его инициализации.

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

  • EBX — идентификатор создаваемой виртуальной машины.

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

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

  • EBX — идентификатор виртуальной машины.

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

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

  • EBX — идентификатор завершаемой машины.

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

DESTROY_VM - уничтожение виртуальной машины

  • EBX — идентификатор уничтожаемой машины.

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

CREATE_THREAD - создание новой задачи

  • EDI — идентификатор создаваемой задачи.
Посылается при создании в системе новой задачи. Создаваемая задача в этот момент еще не является текущей.Возврат установленного флага CF предотвращает создание задачи.THREAD_INIT - инициализация новой задачи EDI — идентификатор задачи.Посылается при инициализации задачи, в начале ее работы, в контексте задачи (новая задача является текущей).Флаг CF всегда должен возвращаться сброшенным.TERMINATE_THREAD - завершение задачи
  • EDI — идентификатор завершаемой задачи.

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

DESTROY_THREAD - уничтожение задачи

  • EDI — идентификатор уничтожаемой задачи.

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

SYSTEM_EXIT - завершение работы системы

  • EBX — идентификатор системной виртуальной машины.

Посылается в начале процесса завершения работы системы, при запросе закрытия системы (shutdown), перезагрузки (reboot) или при аварийном завершении.

W32_DEVICEIOCONTROL - запрос от приложения Win32

  • EBX — идентификатор текущей виртуальной машины.
  • ESI — адрес блока параметров DIOCParams.
  • Сообщение всегда посылается в контексте вызвавшей задачи Win32, так что драйверу напрямую доступно адресное пространство приложения. Драйвер обрабатывает запрос, извлекая из блока параметров и исходного буфера данные запроса, и возвращает в EAX код завершения:

0 — обработка завершена успешно;-1 — начата асинхронная операция. Возвращается только в том случае, если параметру был задан ненулевой параметр lpoOverlapped.код ошибки — если операция завершена неудачно.Вместе с возвратом результата в EAX драйвер может заносить необходимую информацию в буфер результата, если он указан в блоке параметров.


 

Некоторые сервисные функции VMM

VMMCall, VMMJmp, VxDCall, VxDJmp - вызов сервисных функций VxD void xxxCall (DWORD Service);void xxxJmp (DWORD Service);Service — код сервисной функции. Старшие 16 разрядов представляют собой идентификатор VxD, младшие 15 разрядов — номер сервисной функции.

Служит для обращения к сервисным функциям VMM и других VxD. Код функции одновременно определяет и VxD, к которому происходит обращение, и саму функцию этого VxD, так что конструкции VMMxxx и VxDxxx равнозначны. Константы для кодов функций определены во включаемых файлах соответствующих VxD; имена констант функций VxD, отличных от VMM, имеют уточняющие префиксы, например VPICD_Get_Version — запрос номера версии VPICD, драйвера виртуального контроллера прерываний.Сервисные функции доступны только в драйверах, имеющих идентификаторы. К драйверам без идентификаторов «честный» доступ возможен только со стороны приложений. Со стороны других VxD он возможен лишь путем непосредственного поиска данного VxD в системном списке с последующим прямым доступом к таблице его сервисных функций.

Параметры для сервисной функции, как правило, передаются в регистрах; результаты возвращаются в регистрах и флагах процессора. Некоторые функции рассчитаны на вызов в стиле языков C, с помещением параметров в стек и возвратом результата в EAX. Имена функций, оформленных в стиле C, начинаются со знака подчеркивания (_).

Оба вызова оформляются в виде команды прерывания int 0x20, следом за которой размещается код функции. При первой отработке вызова VMM заменяет эту конструкцию на команду far call/jmp в соответствующий шлюз, что дает экономию времени при последующих вызовах. По этой причине конструкцию Int 20, если она не опознается отладчиком как VMMxxx/VxDxxx, нельзя проходить командой типа Step Over, так как стоп-точка будет установлена отладчиком сразу за командой Int и при этом будет испорчен расположенный за нею код функции. Отладчик SoftICE корректно опознает эти конструкции.

Различие вызовов Call и Jmp состоит в том, что вызов Call запоминает адрес возврата в стеке, а Jmp — нет. Методом Jmp вызываются «фатальные» функции, не требующие возврата, а также функции, после которых нужен возврат сразу к вызвавшей функции, без восстановления регистров и других завершающих действий. Без хорошего понимания механизма работы вызова Jmp лучше ограничиться использованием вызова Call.

_SelectorMapFlat - отображение сегментного адреса в линейный

_SelectorMapFlat (
DWORD VMHandle,
DWORD Sel,
DWORD Reserved
);

  • VMHandle — идентификатор виртуальной машины, которой принадлежит селектор. Если селектор относится к GDT, идентификатор игнорируется.
  • Sel — селектор для отображения.
  • Flags — резервный параметр, должен быть нулевым.

Функция получает параметры в стеке, в стиле языков C, и возвращает в EAX линейный адрес, соответствующий началу сегмента, селектор которого задан параметром Sel, либо — 1 в случае ошибки. При помощи этой функции возможен доступ из VxD к данным приложений Win16.

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


 

Некоторые функции-обертки, определенные в VXDWRAPS

Out_Debug_String - вывод отладочного сообщения void Out_Debug_String (char *String);

  • String — указатель строки, выводимой в отладочный поток. Для перехода на новую строку используется символ '\n".

Системный отладочный поток можно просматривать отладчиками WDEB386, SoftICE, а также любым отладочным монитором.

_Sprintf - форматирование строки ULONG _Sprintf (char *Buffer, char *Format, ...);Функция аналогична стандартной функции sprintf языка C.

К сожалению, VMM не предоставляет функции, аналогичной vsprintf, поэтому для реализации функций целевого назначения, в основе которых лежит спецификация формата и список аргументов переменной длины (например, функций отладочного вывода или формирования строк специального вида), приходится использовать _Sprintf, копируя переменную часть списка аргументов (например, 10-20 двойных слов) из стекового кадра целевой функции.

Пример построения функции-обертки

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

#pragma warning (disable: 4035) // Блокировка предупреждения о невозврате

_declspec (naked)
void *SelectorMapFlat (DWORD VM, DWORD Sel, DWORD Rsv = 0) {

VMMJmp (_SelectorMapFlat) // Передача управления VMM

(void)(VM, Sel, Rsv); // Имитация использования параметров

}

#pragma warning (default: 4035) // Восстановление предупреждений о невозврате

Квалификатор _declspec (naked) подавляет генерацию пролога/эпилога, так что от функции остается лишь конструкция VMMJmp. При вызове этой функции-обертки параметры VM, Sel и Rsv помещаются в стек, затем туда же помещается адрес возврата, управление передается в функцию-обертку, при этом VMMJmp передает управление сервисной функции VMM _SelectorMapFlat, не запоминая нового адреса возврата в стеке. VMM использует значения параметров из стека, затем выполняет возврат по адресу, находящемуся на верхушке стека, при этом управление сразу возвращается в точку за вызовом функции-обертки, где находится код, удаляющий из стека параметры и использующий значение, возвращенное VMM в EAX.

Такая схема типична для построения функций-оберток, так как в полной мере использует особенности создания стекового кадра в языках C, а также возможности компилятора Visual C++ по оформлению функций. Без использования VMMJmp пришлось бы делать возврат дважды, а без использования квалификатора naked — дважды помещать в стек список параметров.