Создание VxD на Visual C++ без ассемблерных модулейИсточник: cyberguru форум 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 - интерфейс прикладных программ):
Для обработки запросов от 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 тремя различными способами:
Функции драйвера могут также вызываться в результате запроса самого драйвера - в ответ на прерывания от устройств, вызов перехваченных функций или программных прерываний, при наступлении различных событий и т.п. Секции файла драйвераЗагружаемый файл драйвера стандартным образом может делиться на секции (сегменты) с различными атрибутами (резидентный, изначально загруженный, уничтожаемый и т.п.). Документация Microsoft утверждает, что секции должны иметь определенные имена, однако это сделано лишь для удобства пользования стандартным макросами и фактически система распознает лишь сами атрибуты секций. Рекомендованы следующие имена и классы секций для модуля VxD:
Блок описателя драйвераКлючевым элементом драйвера является структура данных DDB (Device Descriptor Block - блок описателя устройства), которая описывает параметры драйвера. DDB содержит следующие важнейшие поля:
Символическое имя, назначенное 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) { При желании можно поместить внутрь тела функции вывод диагностического сообщения средствами, доступными для VxD. Экспорт ссылки на DDBЭкспорт ссылки на DDB по номеру обычно принято делать в DEF-файле, однако в Visual C++ для этого есть удобный квалификатор __declspec (dllexport), который достаточно поместить в заголовке определения структуры DDB. Структура DEF-файла для построения VxDDEF-файл для построения VxD может содержать опции VXD, DESCRIPTION и SECTIONS. Опция VXD имеет вид: VXD имя тип
Опция DESCRIPTION: DESCRIPTION строка_описания
Опция SECTIONS: SECTIONS имя [CLASS 'класс'] список_атрибутов
Установки компилятора и компоновщикаВ установках компилятора в среде 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++ по умолчанию создает секции четырех типов:
В 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" { Файлы из DDK можно включать и в тексты модулей обычных приложений, определив перед этим символическое имя Not_VxD. При этом определяются только полезные константы и типы, а определение специфических для VxD конструкций отключается. Структуры, обычно используемые в VxDVxD_Desc_Block - блок описателя устройстваОписывает структуру DDB. Заполняется статически, чтобы к моменту загрузки драйвера все поля имели нужные значения. ULONG DDB_Next;
Client_Reg_Struc - структура пакета регистров клиентаОписывает состояние регистров процессора в вызвавшей виртуальной машине/приложении (клиенте). ULONG Client_EDI; Поля с именами Client_xxx содержат значения соответствующих регистров на момент обращения виртуальной машины или приложения к системной функции. В поле Client_Error может быть занесен код ошибки.Для структуры введен синоним типа (typedef) с именем CRS. В файле VMM.H определены также вспомогательные структуры Client_Word_Reg_Struc и Client_Byte_Reg_Struc и объединение всех трех структур CLIENT_STRUCT. DIOCParams - параметры запроса DeviceIoControlDWORD Internal1;
Функции 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 - инициализация статического драйвера
Сообщение посылается после загрузки статического драйвера для его инициализации. SYS_DYNAMIC_DEVICE_INIT - инициализация динамического драйвераСообщение посылается после загрузки динамического драйвера для его инициализации. SYS_DYNAMIC_DEVICE_EXIT - завершение динамического драйвераСообщение посылается перед выгрузкой динамического драйвера для завершения его работы.CREATE_VM - создание новой виртуальной машины
Посылается в процессе создания новой виртуальной машины (VM), но до ее фактического запуска. На этом этапе VxD может определить, сможет ли он поддерживать создаваемую виртуальную машину, и запросить необходимые для поддержки ресурсы. Возврат установленного флага CF предотвращает создание машины. VM_INIT - инициализация новой виртуальной машины
Посылается в начале работы новой виртуальной машины, при ее инициализации, в контексте этой виртуальной машины. На этом этапе VxD может инициализировать ресурсы, выделенные для поддержки новой виртуальной машины.Флаг CF всегда должен возвращаться сброшенным. VM_TERMINATE - завершение виртуальной машины
Посылается в начале процесса завершения виртуальной машины.Флаг CF всегда должен возвращаться сброшенным. DESTROY_VM - уничтожение виртуальной машины
Посылается в конце процесса завершения машины, перед непосредственным удалением ее из системы.Флаг CF всегда должен возвращаться сброшенным. CREATE_THREAD - создание новой задачи
Посылается при создании в системе новой задачи. Создаваемая задача в этот момент еще не является текущей.Возврат установленного флага CF предотвращает создание задачи.THREAD_INIT - инициализация новой задачи EDI - идентификатор задачи.Посылается при инициализации задачи, в начале ее работы, в контексте задачи (новая задача является текущей).Флаг CF всегда должен возвращаться сброшенным.TERMINATE_THREAD - завершение задачи
Посылается в начале процесса завершения задачи. Завершаемая задача еще какое-то время может оставаться в системе, пока не будут завершены все ждущие операции ввода/вывода.Флаг CF всегда должен возвращаться сброшенным. DESTROY_THREAD - уничтожение задачи
Посылается в конце процесса завершения задачи, перед непосредственным удалением задачи из системы.Флаг CF всегда должен возвращаться сброшенным. SYSTEM_EXIT - завершение работы системы
Посылается в начале процесса завершения работы системы, при запросе закрытия системы (shutdown), перезагрузки (reboot) или при аварийном завершении. W32_DEVICEIOCONTROL - запрос от приложения Win32
0 - обработка завершена успешно;-1 - начата асинхронная операция. Возвращается только в том случае, если параметру был задан ненулевой параметр lpoOverlapped.код ошибки - если операция завершена неудачно.Вместе с возвратом результата в EAX драйвер может заносить необходимую информацию в буфер результата, если он указан в блоке параметров.
Некоторые сервисные функции VMMVMMCall, 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 (
Функция получает параметры в стеке, в стиле языков C, и возвращает в EAX линейный адрес, соответствующий началу сегмента, селектор которого задан параметром Sel, либо - 1 в случае ошибки. При помощи этой функции возможен доступ из VxD к данным приложений Win16. Полученный адрес действителен до момента возврата в VMM, после чего отображение может быть аннулировано. Чтобы сохранять отображение длительное время, необходимо использовать специальные средства работы со страницами - резервирование страниц, копирование элементов таблицы страниц и т.п. Некоторые функции-обертки, определенные в VXDWRAPSOut_Debug_String - вывод отладочного сообщения void Out_Debug_String (char *String);
Системный отладочный поток можно просматривать отладчиками WDEB386, SoftICE, а также любым отладочным монитором. _Sprintf - форматирование строки ULONG _Sprintf (char *Buffer, char *Format, ...);Функция аналогична стандартной функции sprintf языка C. К сожалению, VMM не предоставляет функции, аналогичной vsprintf, поэтому для реализации функций целевого назначения, в основе которых лежит спецификация формата и список аргументов переменной длины (например, функций отладочного вывода или формирования строк специального вида), приходится использовать _Sprintf, копируя переменную часть списка аргументов (например, 10-20 двойных слов) из стекового кадра целевой функции. Пример построения функции-оберткиПоскольку сервисная функция VMM _SelectorMapFlat не имеет стандартной обертки, возможная функция-обертка для нее могла бы выглядеть следующим образом: #pragma warning (disable: 4035) // Блокировка предупреждения о невозврате _declspec (naked) 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 - дважды помещать в стек список параметров. |