Создание VxD на Visual C++ без ассемблерных модулей
- Основные свойства и особенности драйвера VxD
- Смысл и назначение драйвера
- Имя и идентификатор драйвера
- Статические и динамические драйверы
- Порядок загрузки статических драйверов
- Системные сообщения драйверу
- Сервисные функции драйвера
- Интерфейс с прикладными программами
- Структура и функционирование драйвера
- Секции файла драйвера
- Блок описателя драйвера
- Контексты
- Доступ к памяти
- Повторная входимость
- Загрузка, работа и выгрузка драйвера
- Особенности разработки VxD на C++
- Общая схема драйвера VxD
- Программирование VxD
- Функции VxD, вызываемые из системы
- Системные средства поддержки VxD
- Некоторые системные сообщения
- DEVICE_INIT - инициализация статического драйвера
- SYS_DYNAMIC_DEVICE_INIT - инициализация динамического драйвера
- SYS_DYNAMIC_DEVICE_EXIT - завершение динамического драйвера
- CREATE_VM - создание новой виртуальной машины
- VM_INIT - инициализация новой виртуальной машины
- VM_TERMINATE - завершение виртуальной машины
- DESTROY_VM - уничтожение виртуальной машины
- CREATE_THREAD - создание новой задачи
- THREAD_INIT - инициализация новой задачи
- TERMINATE_THREAD - завершение задачи
- DESTROY_THREAD - уничтожение задачи
- SYSTEM_EXIT - завершение работы системы
- W32_DEVICEIOCONTROL - запрос от приложения Win32
- Некоторые сервисные функции VMM
- Некоторые функции-обертки, определенные в VXDWRAPS
- Пример простого VxD
Виртуальные драйверы устройств (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 тремя различными способами:
- Через диспетчер системных сообщений.
- Через таблицу обработчиков сервисных функций.
- Через точки входа интерфейсов прикладных программ.
Функции драйвера могут также вызываться в результате запроса самого драйвера — в ответ на прерывания от устройств, вызов перехваченных функций или программных прерываний, при наступлении различных событий и т.п.
Секции файла драйвера
Загружаемый файл драйвера стандартным образом может делиться на секции (сегменты) с различными атрибутами (резидентный, изначально загруженный, уничтожаемый и т.п.). Документация 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, так что выводимые сообщения будут видны только в нем.
Далее >>