Блог → Проблемы установки VxD драйверов и как их решать

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

Прежде всего - инструментарий. Как минимум, нам потребуется Microsoft Driver Development Kit (DDK). Он включает в себя набор файлов заголовков (*.h), библиотеки импорта системных модулей (*.lib), исходные тексты примеров, файлы справок и несколько специализированных утилит. Кроме того, потребуется компилятор языка C из пакета Microsoft Visual C/C++. Конечно, никто не мешает использовать и компиляторы других производителей, но тогда вам придётся самостоятельно создавать *.lib-файлы и довольно долго мучиться, чтобы *.h-файлы DDK транслировались без ругательств.

Необязательным, но очень полезным средством является отладчик уровня ядра системы. Отладчик, рекомендуемый фирмой Microsoft (WinDbg), я использовать не советую, очень уж он неудобен в работе. Лучше воспользоваться отладчиком SoftIce фирмы Numega Compuware, который обладает большими возможностями и устойчивостью в работе.

С чего начать? Нужно почитать документацию из DDK и кратко уяснить себе, как работает ядро Windows вообще и драйвера (VxD) в частности. Так, VxD разделяются на два основных класса: загружаемые статически (static loadable) и динамически (dynamic loadable). Основное их отличие - в том, что статически загружаемые драйвера считываются в оперативную память при загрузке системы и остаются в ней до завершения работы Windows (во всяком случае, я не знаю способа принудительной выгрузки таких VxD). Динамически загружаемые драйвера считываются в оперативную память по мере необходимости, и при желании могут быть оттуда удалены.

Теперь разберёмся с классификацией устройств. Они могут быть Plug and Play (PnP) и т.н. legacy ("наследство" от более ранних ранних версий). PnP-устройства автоматически распознаются и конфигурируются операционной системой при подключении, тогда как legacy-устройства нужно устанавливать и конфигурировать вручную. PnP-устройствами являются все USB и SCSI-устройства, модемы, мыши, стандартные порты СОМ и LPT, а также практически все современные ISA и PCI-платы. Ну а всё остальное, что не может быть автоматически определено и сконфигурировано средствами Windows, относится к legacy-устройствам (нестандартные COM-порты и навешанное на них оборудование, старые ISA-платы, внешнее оборудование, не поддерживающее спецификацию PnP, а также драйвера, не работающие непосредственно с физическим оборудованием, например - TCP/IP стек).

Каждое устройство в системе представлено так называемым DevNode (совокупность данных, описывающая используемые ресурсы, точку входа в драйвер и некоторую дополнительную служебную информацию), а все известные системе устройства объединяются в DevNode Tree. При этом дерево (tree) устройств может иметь несколько уровней: например, имеется DevNode для драйвера шины PCI, у которого имеются дочерние DevNode для видеокарты и звуковой платы, подключенных к данной шине.

Ну и последнее замечание по классификации VxD: в Win9x определены три типа функциональных драйверов: загрузчики (loaders), перечислители (enumerators) и собственно драйверы (drivers). Device loader - практически всегда статический VxD, его назначение понятно из названия, это загрузка драйверов указанного типа. Обычно программисту нет необходимости писать свой загрузчик, можно воспользоваться одним из стандартных (*IOS - для драйверов файловой системы, *VCOMM -для последовательных и параллельных портов, *CONFIGMG - для PnP-устройств).

Enumerator - это штука, которая постоянно сидит в памяти и смотрит, не появилось ли новое известное ей PnP-устройство. Или не делось ли куда-нибудь существовавшее ранее. В этих ситуациях enumerator (посредством *CONFIGMG - который, по совместительству, является ещё и конфигуратором PnP-устройств) загружает или выгружает соответствующий драйвер устройства. Device driver - VxD, который и занимается непосредственно обслуживанием оборудования. Драйвер устройства может быть как статическим, так и динамическим (в последнем случае он загружается по запросу от enumerator'a, прикладной программы или любого другого VxD).

С остальной теорией можно разобраться самостоятельно, прочитав документацию из DDK. Поэтому переходим к практике! Вряд ли вам придется писать драйвер для устройства, вставляемого в штатный ISA или PCI-слот компьютера - обычно такие платы поддерживаются производителями оборудования на достаточно приличном уровне. Скорее всего, к вам в руки попадет нечто, подключаемое к СОМ или LPT-порту, да к тому же и не поддерживающее спецификацию PnP. Поэтому основной задачей будет:
- написание процедуры корректной установки драйвера, с "менюшками", и "чтобы был виден в менеджере устройств";
- определение всех имеющихся в системе портов СОМ и LPT (зачем озадачивать пользователя лишними вопросами при установке драйвера?);
- корректное определение наличия на указанном порту именно вашего устройства (представляете, как здорово, если в момент опроса портов полностью накрывается мышь или принтер перестает печатать);
- корректный захват и освобождение ресурсов, необходимых для функционирования вашего устройства.

Шаг первый - установка драйвера. Как мы уже выяснили, драйвера могут быть статическими и динамическими. Для загрузки статических драйверов их можно прописать в секции [386Enh] файла SYSTEM.INI. Но этот метод устарел и оставлен, фактически, только для совместимости со старыми версиями Windows (единственное исключение - драйвера, загрузка которых должна производиться еще до перевода процессора в защищённый режим работы - например, для обращения к real mode функциям BIOS). Вторым (предпочтительным) способом является записывание информации в registry, в ветку HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\ИМЯ_УСТРОЙСТВА, где значение ИМЯ_УСТРОЙСТВА может быть произвольным (на ваш вкус). В этой ветке должна находиться переменная типа REG_SZ с именем StaticVxD и со значением, содержащим имя вашего VxD-драйвера. Если файл располагается в директории %SystemRoot%\SYSTEM, то можно указать только его имя, в противном случае обязательно прописать полный путь. Кроме того, в этой же ветке нужно создать переменную Start типа REG_BINARY со значением, равным 0. В принципе, переменная Start предназначена только для совместимости с последующими версиями Windows.

При старте система сканирует ветвь HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD и загружает все описанные в ней VxD. Создать запись в registry можно как функциями Win32 API, так и через секцию REGISTRY *.INF-файла (подробнее об *.INF-файлах читайте в DDK). Естественно, драйвер будет активизирован только после выполнения перезагрузки системы, так что не забудьте сообщить об этом пользователю в финале вашей программы установки.

Hint: для загрузки VxD, использующих в качестве Device loader *IOS, можно просто переписать ваш VxD в директорию %SystemRoot%\SYSTEM\IOSUBSYS и установить расширение файла *.VXD или *.MPD.

Но использование статически загружаемых драйверов без особой необходимости не оправдано, так как ведет к повышенному расходу оперативной памяти, которой никогда не бывает много. Поэтому предпочтителен второй вариант - использование динамически загружаемых VxD. Если ваше устройство не поддерживает спецификацию PnP, оно по определению является legacy. Для legacy-устройств ядро Windows предоставляет некий эмулятор enumerator'a, который загружает все драйвера, которые описаны в registry в ветке HKEY_LOCAL_MACHINE\Enum\Root\. Фактически, для того, чтобы ваше не-PnP устройство стало аналогом PnP - нужно загрузить написанный вами VxD-enumerator, который и будет, при необходимости, заниматься загрузкой и выгрузкой непосредственно драйвера физического устройства. Кстати, по этой же причине enumerator и драйвер выполняют обычно в виде двух различных VxD (т.к. enumerator находится в оперативной памяти постоянно, а драйвер устройства подгружается по мере необходимости).

Итак, для установки написанного вами enumerator'a нужно задать минимальную информацию о нем в registry - например, в ветке HKEY LOCAL MACHINE\Enum\Root\ИМЯ_УСТРОЙСТВА\0000, где в качестве ИМЯ_УСТРОЙСТВА может быть любая строка (например, имя вашего enumerator'a), а 0000 означает, что это первое логическое устройство (а зачем нужно больше?). Из минимально необходимой информации необходимо в эту ветку записать следующие переменные (все значения имеют тип REG_SZ):

HardwareID=ИДЕНТИФИКАТОР_УСТРОЙСТВА
Class=КЛАСС_УСТРОЙСТВА
infName=ИМЯ_ФАЙЛА.INF


где ИДЕНТИФИКАТОР_УСТРОЙСТВА - любой выбранный вами идентификатор устройства (должен совпадать со значением HardwareID в соответствующей секции INF-файла), КЛАСС_УСТРОЙСТВА - класс вашего устройства (например, "Unknown"). В этой категории ваше устройство будет отображаться в окне Device Manager. ИМЯ_ФАЙЛА.INF - имя INF-файла для вашего драйвера (без пути). Всё это можно записать в registry стандартными средствами API Win32 (прямо в вашей программе установки).

Вот, в принципе, и всё. После перезагрузки системы появится сообщение Windows "обнаружено новое устройство и идет поиск программного обеспечения для него", дальше предложат вставить диск от производителя оборудования - и установка драйвера продолжится в обычном режиме. Возникает вопрос: а можно ли обойтись без перезагрузки системы? Оказывается - можно! Хотя наше устройство и является legacy, но сама Windows эмулирует его как PnP. В принципе, можно просто войти в Device Manager и на закладке, на которой находится device tree нажать кнопку Refresh (Обновить), и последствия будут такие же, как и после перезагрузки системы. А еще лучше - попросить *CONFIGMG (именно он занимается конфигурированием PnP-устройств) проверить все устройства (в т.ч. и вновь появившиеся в системе). Для этого после записи информации в registry вызовем функцию ReenumerateDevices().

Теперь рассмотрим некоторые неочевидные вещи. С установкой драйвера мы, вроде бы, разобрались. Для написания самого драйвера можно позаимствовать готовый скелет из примеров DDK. А здесь лучше рассказать о некоторых вещах, на уяснение которых мною было потрачено довольно много времени. Одна из таких нетривиальных задач - определить все имеющиеся в системе СОМ и LPT порты, не запрашивая у пользователя никакой дополнительной информации. И, поскольку с этим оборудованием мы потом будем работать напрямую (минуя штатные драйвера) - нужно узнать ещё и аппаратные характеристики устройств. Не факт, что устройство "СОМ1" будет обязательно иметь базовый адрес 0x3F8 и использовать IRQ4. Кроме того, в компьютере может быть установлена плата расширения портов, об особенностях которой мы можем не знать.

К сожалению, в Win9x нет штатных средств для определения количества и имён СОМ-портов, имеющихся в системе. Нумерация портов не обязательно будет сквозной, в системе могут быть, например, "СОМ1", "СОМ7" и "СОМ18". Да и вообще, последовательные порты не обязаны называться "COMx", а параллельные "LPTx". В принципе, имена портов могут быть абсолютно произвольными.

Для решения этой задачи воспользуемся тем обстоятельством, что все устройства в системе упорядочены в виде дерева устройств (DevNode tree). Кроме того, для любого последовательного и параллельного порта штатным перечислителем (enumerator) является драйвер *VCOMM, интерфейс к которому документирован в DDK. Так как количество портов в системе может быть разным, выделять память для хранения информации о них в виде статического массива не стоит - лучше воспользоваться средствами, предоставляемыми для VxD ядром системы (т.н. сервисные функции VMM).

Затем, когда мы уже имеем список всех физических портов, известных системе, нам остаётся определить, к какому из них подключено наше внешнее оборудование. И определить, по-возможности, в "горячем" режиме, так как пользователь может подключить и отключить наше оборудование без выключения питания компьютера и без перезагрузки системы.

Конечно, в качестве лобового способа можно использовать опрос по событиям таймера, запрещая на время опроса прерывания командой CLI и разрешая их потом командой STI (т.к. VxD выполняются на уровне ядра - команды CLI/STI выполняются непосредственно процессором, а не эмулируются операционной системой). Но такая тактика может привести к потерям прерываний от других устройств, да и быстродействие системы от этого не улучшится.

С другой стороны, мы не должны получать доступ к аппаратным ресурсам в "любой понравившийся нам момент времени" - не исключено, что с данным портом уже кто-нибудь работает, и мы можем ему просто помешать. Для решения этой проблемы VCOMM предлагает механизм, называемый contention handler. Его идея заключается в том, что если нам нужны физические ресурсы, управляемые другим драйвером (в данном случае - драйвером последовательного или параллельного порта), то мы обращаемся в точку входа contention handler с запросом на захват ресурса. Ресурс нам могут дать, а могут и не дать. Если ресурс нам выдали - мы его можем использовать совершенно свободно, а после завершения использования снова вызвать contention handler и освободить его. Ну а уж если ресурс недоступен, то делать нечего, придётся подождать и повторить попытку захвата на следующем цикле проверки.

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

А во-вторых, и это самое неприятное, contention handler некорректно реализован в стандартном драйвере последовательного порта. Мало того, что ни мышь, ни уже открытый какой-либо прикладной программой COM-порт никогда не отдаст свой ресурс нашему драйверу, так еще и в случае, когда мы корректно захватили и затем корректно освободили COM-ресурс, операционная система останется в нестабильном состоянии! Кроме того, callback-функция, отвечающая за перехват ресурса у нашего драйвера будет просто игнорировать возвращаемое нами значение: "уведомили - и ладно, ресурс отберем безусловно". Исходя из вышесказанного, использовать contention handler для последовательных портов категорически не рекомендуется (во всяком случае, я потратил несколько дней на изучение исходников в DDK и трассировку обработчика средствами SoftIce - и подходящего решения так и не нашел).

Ну что, заработало ваше устройство? Довольны? Ах, на "зелёных" материнских платах ваш драйвер либо вообще не работает, либо работает минуты три, а потом обмен с вашим устройством прекращается без объяснения причин? В принципе, эта ерунда впервые появилась в Windows 98 (в Windows 95 поддержка ACPI реализована в минимальном объеме и не мешает жить системному программисту). Дело в том, что при захвате ресурса посредством обращения к contention handler (и уж тем более, если вы захватываете его "нелегально", между командами CLI/STI), никто и не думает переводить порт из "спящего" режима в "рабочий". Короче говоря, на нем просто отсутствует напряжение питания! Кстати, это относится как к последовательным, так и к параллельным портам (особенно, если они интегрированы на "зелёной" материнской плате).

Вы знаете, как включать/выключать питание порту? Лично я - не знаю. А вот небезызвестный нам CONFIGMG - знает (интересно, откуда?). Можно обратиться к нему за помощью. В принципе, сохранять исходное состояние напряжения питания и восстанавливать его по окончании использования ресурсов абсолютно не обязательно, это делается исключительно для обеспечения "корректности" по отношению к другому драйверу, работающему с этим же устройством. Как говорится, "не делай другому того, чего не желаешь себе". Вообще, использование этого принципа при написании драйверов очень положительно влияет на совместимость с другим аппаратным и программным обеспечением.

Ну вот, в общем, и всё, что я хотел рассказать. Вооружайтесь отладчиком, изучайте ядро системы, смотрите примеры из DDK. И всё равно, даже будучи подготовленными морально, когда перейдёте к написанию драйверов под платформу Windows NT, будете плеваться и вспоминать недобрым тихим словом славную фирму Microsoft - за её реализацию VxD под Windows 9х.