Микс

Проблемы установки 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, который загружает все драйвера, которые описаны в registry в ветке HKEY_LOCAL_MACHINE\Enum\Root\. Фактически, для того, чтобы ваше не-PnP устройство стало аналогом PnP – нужно загрузить написанный вами VxD-enumerator, который и будет, при необходимости, заниматься загрузкой и выгрузкой непосредственно драйвера физического устройства. Кстати, по этой же причине enumerator и драйвер выполняют обычно в виде двух различных VxD (т. к. enumerator находится в оперативной памяти постоянно, а драйвер устройства подгружается по мере необходимости).

  • Итак, для установки написанного вами enumerator’a нужно задать минимальную информацию о нем в registry – например, в ветке HKEY LOCAL MACHINE\Enum\Root\ИМЯ_УСТРОЙСТВА00, где в качестве ИМЯ_УСТРОЙСТВА может быть любая строка (например, имя вашего 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