Программирование драйверов Windows

         

Программа NTDevices


Программа NTDevices (рис. 2.20) представляет собой существенно более удобный в использовании аналог упомянутой ранее программы DeviceTree, предназначенной для исследования стека драйверов в операционной системе. Позволяет также просматривать все символьные ссылки, зарегистрированные в системе, и список объектов устройств, имеющих подключенные (attached) объекты других устройств.

Рис.2.20

Программа NTDevices



Программа NTObjects


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



Программа Numega SymLinks


В составе пакета Driver Studio имеется утилита, которая позволяет просматривать все символьные ссылки, созданные реально функционирующими на данный момент драйверами.

На рисунке 2.12 показаны некоторые из символьных ссылок, имеющихся в системе. В частности, в двух первых строчках указаны символьные имена HCD0 и HCD1, соответствующие функциональным объектам устройств (с именами USBFDO-0 и USBFDO-1), которые обслуживаются драйвером USB контроллера. То есть в системе физически присутствуют два USB контроллера, к которым их клиенты могут обращаться с вызовом пользовательского режима CreateFiIe("\\\\.\\HCD0",...) или вызовом режима ядра ZwCreateFile (внутри параметров которого передается это же имя, правда, ритуал такой передачи несколько сложнее).

Рис.2.12

Программа SymLinks

Следует отметить, что программа SymLinks существенно лаконичнее программ WinObj и DevView, которые будут рассмотрены ниже.



Программа PEBrowseProfessional Interactive


Представляет собой программу просмотра "начинки" исполняемых exe, dll, sys модулей (файлов РЕ-формата, Portable Executable File Format) и дизассемблер (включая даже интерактивный отладчик программ пользовательского режима). Не самый удачный пpoдyкт SmidgeonSoft, поскольку в первом качестве он существенно уступает программе РЕ Explorer, а во втором — программе IDA Pro. Тем не менее, данная программа мощнее и удобнее упомянутой ранее программы Depends. Представляет интерес при изучении особенностей бинарных драйверных модулей (готовых драйверов). Кроме того, упрощенный вариант PEBrowse Professional позволяет просматривать содержимое lib-файлов.



Программа PoolTagот OSR Inc


Программа PoolTag Monitor (PoolMon &#8212 консольная версия), поставляемая в составе пакета DDK, динамически (с интервалом в несколько секунд) отображает на экране состояние страничного и нестраничного пулов памяти режима ядра.

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

Рис.2.18

Программа PoolTag



Программа просмотра файлов


Простая программа просмотра файлов, вызываемая в старом добром Norton Commander (впрочем как и в современном TotalCommander) по нажатию клавиши F3, также может быть грозным орудием в руках опытного программиста.

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

Рис. 2.19

Просмотр файла GiveIo.sys



Программа РЕ Explorer


Удобным в использовании средством изучения бинарных загружаемых модулей exe, dll, vxd, sys является программа РЕ Explorer фирмы Heaventools Software (рис. 2.21). 30-дневную испытательную версию можно загрузить с интернет-сайта фирмы по адресу heaventools.com.

Рис. 2.21

Окно просмотра заголовка файла Depends.exe в окне программы РЕ Explorer

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

При анализе импортируемых функций (импортируемых из системных динамических библиотек) программа РЕ Explorer показывает справочную информацию о протоколе вызова многих из них. По точности и подробности дизассемблирования программа РЕ Explorer приближается к дизассемблеру IDA Pro.

Своеобразной визитной карточкой РЕ Explorer является способность к просмотру и редактированию ресурсов, скомпилированных в данном бинарном модуле. Заметим, что в разделе ресурсов зачастую находится самая достоверная информация об авторе и дате создания драйвера (возможно даже — его предназначении), которая другим образом может быть получена, как правило, лишь системными средствами после установки драйвера, что далеко не всегда возможно и целесообразно.



Программа ReBase


Программа ReBase (консольное приложение) поставляется в составе Visual Studio и выполняет удаление отладочной информации из бинарных файлов, скомпилированных и собранных проектов, в нашем случае — из бинарных файлов драйверов (.sys файлов). Даже после сборки окончательной (релизной, free) версии драйвера при помощи программ DDK в нем еще остается некоторая отладочная информация (например, внутренние имена функций), которую программа ReBase может выделить и поместить в файл отладочных идентификаторов. Размер бинарного файла может уменьшиться при этом на четверть.

Ниже приведены примеры командной строки для запуска программы ReBase применительно к драйверу:

rebase -В 0x10000 -X . example.sys rebase -xa dbgdir -Ь 0x10000 -l protocol example.sys

Во втором примере ключ -xa (расширение ключа -x из первого примера) задает удаление всей отладочной информации (с перемещением ее в файл с расширением .dbg). Директория, где будет размещен этот .dbg файл в первом примере — текущая (поскольку указана точка после -x), во втором — вложенная поддиректория .\dbgdir. Ключ -b указывает базовый адрес (для драйверов режима ядра всегда указывается значение 0x10000), ключ -l (эль) указывает файл протокола (log file), во втором примере файл protocol.

Более подробно с командами ReBase можно ознакомиться через сообщение, которое программа выведет по команде

rebase -?



Программа редактирования Системного Реестра


Программа, без которой не обойтись ни одному разработчику драйвера — программа редактирования Системного Реестра, которая запускается командой Пуск - Выполнить - regedit (в Windows 2000 — командой regedit32). Целостность информации, находящейся в Системном Реестре весьма важна для операционной системы. Поэтому проводить эксперименты над Реестром следует весьма осторожно, поскольку даже резервное копирование Реестра (а также защита отдельных разделов от какого бы то ни было редактирования) может не спасти от необходимости переустановки всей системы. Без достаточной необходимости не следует оставлять изменения, сделанные в Системном Реестре (рекомендуется возвращать его состояние к начальному, имевшему место до экспериментов).

Ниже будет упомянута и еще одна полезная программа по изучению Системного Реестра &#8212 RegMon (Registry Monitor)



Программа RegMon


Программа RegMon (рисунок 2.13) перехватывает все обращения к Системному Реестру из приложений пользовательского режима и выдает сообщения о них в своем рабочем окне, что иногда бывает полезно, чтобы отследить, какие разделы этой общесистемной базы данных (Системного Реестра) использует конкретное приложение, и какие параметры его более всего интересуют.

Рис. 2.13

Программа RegMon

Важный психологический эффект от знакомства новичков с этой программой состоит в том, что она со всей наглядностью демонстрирует: программы, созданные разными производителями, используют Системный Реестр очень интенсивно, некоторые даже чрезмерно (вспомним Microsoft Internet Explorer).



Программа System Memory Browse


Программа просмотра содержимого виртуальной памяти диапазона системного адресного пространства. Позволяет в традиционном режиме (то есть из программы System Memory Browse, являющейся обычным приложением Windows NT) получить доступ к такой информации, которая ранее была доступна разве только из отладчиков режима ядра (и, разумеется, упомянутой программы w2k_mem).



Программа Task Manager (Диспетчер Задач)


Не следует недооценивать значение использования стандартных системных средств в процессе получения нужных сведений о функционировании приложений, использующих драйверы. Всем известное системное программное средство Диспетчер Задач, вызываемый комбинацией клавиш Ctrl-Alt-Delete, позволяет оперативно получать сведения о работающих процессах (рисунок 2.8). Необходимо лишь в пункте меню "Вид" выбрать интересующие параметры для отображения. Хотя относятся они к приложениям пользовательского режима, могут дать косвенное представление и о работе вызываемых драйверов.

Закладка "Быстродействие" позволяет настроить отображение времени пребывания процесса в режиме ядра (в процентном отношении) — параметр "Вывод времени ядра" в пункте меню "Вид" для этой закладки. По этим показаниям можно оценить, какую часть времени работы приложения занимает обращение к драйверу.

Рис.2.8

Рабочее окно системного Диспетчера Задач



Программа трансляции файла sources в проект Visual Studio


В составе пакета Driver Studio имеется утилита, которая выполняет достаточно корректное создание файла описания проекта Visual Studio (.dsp, файла описания проекта для Microsoft Visual Studio 6).

Для программы SrcToDsp (рисунок 2.11) требуется в качестве входной информации файл sources, управляющий обычно сборкой драйвера утилитой Build в пакете DDK. Среда программирование Microsoft Visual Studio 7 Net также способна воспринимать .dsp файлы, однако при первой загрузке проекта она предпочитает перевести их в формат .vcproj (текстовый XML формат, вполне читаемый и похожий на HTML).

Следует, тем не менее, критически относиться к результатам работы этой программы и не принимать все на веру, тщательно проверяя настройки проекта в Visual Studio. Кроме того, следует помнить, что чистовую сборку драйвера следует выполнять средствами пакета DDK (в котором настройки компиляции и сборки можно считать эталоном).

Рис.2.11

Программа SrcToDsp



Программа w2k_mem


Консольное приложение для Windows NT, позволяющее просматривать дамп любых областей памяти, включая внутренние области ядра (системной памяти с адресами более 0x80000000), что недоступно для приложений пользовательского режима.

Предположим, вы воспользовались командой

w2k_mem #0x200 0x898F9094 >display_mem1.txt

тогда в файле display_mem1.txt вы сможете увидеть дамп памяти длиной 512 байт (то есть 0x200) по шестнадцатеричному адресу 0xF98F9094 — конечно же, только в том случае, если память по этому адресу в настоящий момент выделена какому-нибудь процессу. В противном случае, результат будет следующим:

F98F9094..F98F9293: 0 valid bytes Address | 04 05 06 07-08 09 0A 0B : 0C 0D 0E OF-10 11 12 13 | 456789ABCDEF0123 ---------|-------------------------:-------------------------|----------------- F98F9094 | - : - | F98F90A4 | - : - | F98F90B4 | - : - | F98F90C4 | - : - | F98F90D4 | - : - | F98F90E4 | - : - |

. . . . . . . . . . . . . . . . . . . . . . . .

Для успешно выполненной команды (в примере ниже по адресу 0x80DC5C88 существует занятая область памяти)

w2k_mem #0x200 0x80DC5C88 >display_mem2.txt

результат выглядит следующим образом:

80DC5C88..80DC6087: 256 valid bytes Address | 08 09 0A 0B-0C 0D 0E 0F : 10 11 12 13-14 15 16 17 | 89ABCDEF01234567 ---------|-------------------------:-------------------------|----------------- 80DC5C88 | 03 00 C4 00-00 00 00 00 : 48 56 F4 FE-00 00 00 00 | ..A.....HVo?.... 80DC5C98 | 00 00 00 00-00 00 00 00 : 00 00 00 00-40 00 00 00 | ............@... 80DC5CA8 | 00 00 00 00-00 00 00 00 : 40 5D DC 80-22 00 00 00 | ........@]U?"... 80DC5CB8 | 01 00 00 00-00 00 00 00 : 00 00 00 00-00 00 00 00 | ................ 80DC5CC8 | 00 00 00 00-00 00 00 00 : 00 00 00 00-00 00 00 00 | ................ 80DC5CD8 | 00 00 00 00-00 00 00 00 : 00 00 00 00-00 00 00 00 | ................ 80DC5CE8 | 14 00 14 00-EC 5C DC 80 : EC 5C DC 80-00 00 00 00 | ....i\U?i\U?.... . . . . . . . . . . . . . . . . . . . . . . . .

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

Наиболее очевидный способ получения стартовых адресов для путешествия по памяти режима ядра заключается в использовании вывода сообщений из отладочных версий драйверного кода, которые видны в рабочих окнах программ Debug View или Debug Print Monitor.



Программа w2k_svc


Консольное приложение для Windows NT, позволяющее просматривать установленные службы и драйверы. Например, по команде

w2k_svc /any /all >list.txt

программа выводит список установленных системных служб и загруженных драйверов в текстовый файл list.txt. Начало этого файла приводится ниже.

// w2k_svc.exe // SBS Windows 2000 Service List V1.00 // 08-27-2000 Sven В. Schreiber // sbs@orgon.com

Found 261 drivers and processes: 1. Abiosdsk . . . . . . . . . . . . . . . . Abiosdsk 2. abp480n5 . . . . . . . . . . . . . . . . abp480n5 3. Драйвер . . . . . . . . . . . . . . . . Microsoft ACPI ACPI 4. ACPIEC . . . . . . . . . . . . . . . . . ACPIEC . . . . . . . . . . . . . . . . . . . . . . . .

Записи отсортированы в алфавитном порядке по имени сервиса (имя, которым представлен драйвер, например, в Системном Реестре). В частности, драйвер мини-порта Microsoft USB универсального хост-контроллера загружается из файла c:\Windows\System32\DRIVERS\usbuhci.sys, имеет имя сервиса "usbuhci". Соответственно, в данном примере запись об этой службе идет под номером 238 (в соответствии с положением "usbuhci" среди имен остальных служб).

Задать вывод информации только лишь о драйверах можно командой:

w2k_svc /drivers /active >list.txt



Программа w2k_sym


Консольное приложение для Windows NT, позволяющее просматривать процессы и работающие драйверы режима ядра. Например, по команде

w2k_sym /v /d >list.txt

программа выводит список загруженных драйверов (см. фрагмент распечатки ниже) в текстовый файл list.txt. В данном случае (для компьютера, на котором проводился тест) первые 27 записей абсолютно идентичны списку DLL и системных драйверов, выводимому при загрузке операционной системы, если в файл boot.ini ввести ключ загрузки /sos (обеспечивающий вывод на экран списка загружаемых драйверов во время старта системы).

121 drivers Monday, 12-30-2002, 14:29:07

# ADDRESS SIZE NAME ------------------------------------------------------------------- 1: 804D0000 1E4580 \WINDOWS\system32\ntoskrnl.exe 2: 806B5000 1F700 \WINDOWS\system32\hal.dll 3: FD998000 1B80 \WINDOWS\system32\KDCOM.DLL 4: FD8A8000 3000 \WINDOWS\system32\BOOTVID.dll 5: FD44B000 2BE00 ACPI.sys 6: FD99A000 1100 \WINDOWS\System32\DRIVERS\WMILIB.SYS 7: FD498000 F500 pci.sys 8: FD4A8000 8D00 isapnp.sys 9: FD99C000 1100 viaide.sys 10: FD718000 5C80 \WINDOWS\System32\DRIVERS\PCIIDEX.SYS 11: FD4B8000 9280 MountMgr.sys 12: FD42C000 1EA00 ftdisk.sys 13: FD99E000 1700 dmload.sys 14: FD408000 23C80 dmio.sys 15: FD720000 4900 PartMgr.sys 16: FD4C8000 C000 VolSnap.sys 17: FD3F2000 15280 atapi.sys 18: FD4D8000 8380 disk.sys 19: FD4E8000 AF80 \WINDOWS\System32\DRIVERS\CLASSPNP.SYS 20: FD3E0000 11300 sr.sys 21: FD3BC000 23580 Fastfat.sys 22: FD3A8000 13780 KSecDD.sys 23: FD380000 27700 NDIS.sys 24: FD728000 6B00 viaagp.sys 25: FD9A0000 1C80 nmfilter.sys 26: FD362000 1D320 Siwvid.sys 27: FD348000 19600 Мuр.sys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Интересно, что по командам

w2k_sym /v /m - выводить список загруженных модулей w2k_sym /v /p - выводить список работающих процессов

информация о драйверах не выводится. Это косвенный, но уверенный намек на то, что драйверы не представляют полноценных процессов (впрочем, как и динамические библиотеки, DLL), а работают в контексте того кода, который к ним обратился.



Программа WinObj


Программа WinObj является удобным средством просмотра директорий имен объектов операционной системы Windows NT (включая 2000, XP и Server 2003). Для разработчика драйвера, естественно, наиболее интересными являются директории имен устройств и имен символьных ссылок (symbolic links), Device и Global?? соответственно, см. рисунок 2.14.

Рис. 2.14

Программа WinObj

В начале отладки драйвера непременно следует поинтересоваться в программе WinObj, созданы ли ожидаемые имена объектов устройств и соответствующие символьные ссылки, позволяющие обращаться к драйверу из клиентского кода (из приложения пользовательского режима или из другого драйвера режима ядра). Отсутствие ожидаемых имен сигнализирует о неполадках в драйвере. Зачастую, отсутствие этих имен в положенных местах сигнализирует о серьезных недочетах в процедурах инициализации драйвера, что не позволяет системе выполнить загрузку драйвера, а разработчику &#8212 увидеть хотя бы минимальные признаки жизни драйвера, хотя бы в виде диагностических сообщений для программ типа DebugView и DebugPrint Monitor, которые будут описаны ниже.

Для просмотра некоторых из директорий имен объектов могут понадобиться привилегии администратора.



Программируемый ввод/вывод


Устройства, использующие программируемый ввод/вывод

(Programmed I/O, PIO), осуществляют передачу данных непосредственно через регистры устройства. Драйвер должен использовать инструкцию ввода/вывода (I/O instruction) для чтения из регистра или записи в регистр устройства каждого байта данных. При больших объемах драйвер должен поддерживать адресацию буферной области и счетчик переданных данных.

Поскольку реальная скорость передачи данных таких устройств много меньше, чем возможности процессора по чтению или записи в регистры данных, то устройства с таким подходом обычно генерирует прерывание для каждого байта (слова) передаваемых данных. Последовательный СОМ порт является одним из примеров PIO устройств. Более совершенные устройства используют накопительные FIFO, что позволяет им в одно прерывание передавать 4 или 16 байт данных. И все же количество прерываний относительно переданного объема данных весьма велико, отчего этот метод пригоден лишь для медленных устройств.



Программное средство проверки логики функционирования PreFast


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

> prefast cl /с myfile.cpp > prefast view

Вторая строка при удачной компиляции покажет (если таковые имеются) логические ошибки в тексте, представленном в файле myfile.cpp.

Каковы логические ошибки, пропущенные компилятором и выявляемые программой PreFast? Например, в следующем фрагменте

void *ptr1 = malloc(1000); void *ptr2 = malloc(1000); if(ptr2 == NULL) return FALSE; // не выделена область памяти free(ptr1); free(ptr2);

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

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

Самым важным недостатком поставки PreFast в пакете DDK Server 2003 сборки 3790 (на данный момент) является то, что поставляемые настройки позволяют работать только с кодом пользовательского режима (хотя и это бывает нужно разработчику драйвера), поскольку распознает только имена функций типа malloc, sprintf и т.п. В режиме ядра, как будет показано позже, используются совершенно другие имена, которые, пока что, выходят за рамки настроек распознавания PreFast.



Программное средство тестирования драйвера Driver Verifier


Программа Driver Verifier (последовательность старта Пуск — Программы — Development Kits — Windows DDK — Tools — Driver Verifier) проверяет драйвер на правильность выполнения следующих тестов:

Операции с пулами памяти.

Корректность уровней IRQL, на которых выполняется код драйвера.

Обнаружение взаимоблокировок.

Выполнение DMA операций.

Стресс-тест (нехватка ресурсов).

Нетипичные запросы к драйверу.

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



Программные компоненты Plug and Play


В сообщество участников поддержки PnP в операционной системе входят:

PnP Менеджер, который состоит из двух частей &#8212 работающей в режиме ядра и работающей в пользовательском режиме. Часть из режима ядра взаимодействует с аппаратурой и другими программными компонентами, функционирующими в режиме ядра, обеспечивая управление правильным определением и конфигурированием аппаратуры. Часть из пользовательского режима взаимодействует с компонентами пользовательского интерфейса, позволяя интерактивной программе делать запросы и изменять конфигурацию инсталлированного PnP программного обеспечения.

Менеджер Управления Энергопитанием (Power Manager), который определяет и обрабатывает события энергообеспечения.

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

Inf-файлы. Каждое устройство должно быть полностью описано файлом, который используется при инсталляции управляющего им драйвера. Inf-файл является рецептом, как и какую информацию об устройстве заносить, в частности, в Системный Реестр.

Драйверы для PnP устройств, которые можно разделить на две категории: WDM и NT драйвера. Последние являются "унаследованными" от NT драйверами, которые опираются на некоторые аспекты PnP архитектуры, но, с другой стороны, не полностью удовлетворяют модели WDM. Например, они могут использовать сервисы PnP Менеджера, чтобы получить информацию о конфигурации, но при этом не обрабатывают IRP пакеты сообщения с кодом IRP_MJ_PNP. Драйверы WDM модели, по определению, полностью соответствуют требованиям взаимодействия по правилам PnP.

Рис. 9.1

Программные PnP компоненты Windows 2000/XP



Программные потоки и синхронизация


Настоящая глава касается вопросов создания программных потоков, функционирующих в режиме ядра и, разумеется, проблемы, всегда актуальной в многозадачных системах &#8212 синхронизации выполнения и доступа к совместно используемым данным.



Программные проблемы


Поскольку драйвер работает в режиме ядра, для него весьма несложной задачей является "обрушение" всей операционной системы. Наиболее сложными для трассирования сценариями являются операции DMA, в которых некорректно установлены регистры отображения (mapping registers). Данные записываются устройством в случайные области памяти, и происходит сбой, виноватыми в котором кажутся совершенно другие подсистемы.



Программные продукты от Microsoft


Информация от Microsoft, полезная разработчику драйверов, может быть получена по нескольким каналам, которые имеют существенные различия.

Документация к MS Visual Studio, где описывается, хотя бы использование функций SCM Менеджера, что позволяет динамически загружать некоторые типы драйверов.

Platform Software Development Kit (PSDK), распространяемая как часть подписки MSDN. Содержит весьма полезные программы (в частности, Depends и WinObj).

Собственно пакет Device Driver Kit (DDK).

Материалы ежегодной конференции WinHEC по драйверам.

Тактика распространения программ для разработки фирмой Microsoft постоянно меняется. Хотя компиляторы Visual Studio изначально распространялись на коммерческой основе, однако, более специализированные средства разработки долгое время можно было бесплатно загрузить с интернет-сайта microsoft.com. Данная традиция прекратилась с выпуском DDK XP, который поставлялся исключительно на CD ROM, причем как бы бесплатно — необходимо только оплатить доставку курьерской службой. В настоящий момент этот прием применяется ко всем позднее выпущенным средствам для разработки и отладки драйверов (обновления Service Packs, файлы с отладочными символами, тесты на совместимость с типовой аппаратурой персонального компьютера и т.п.).



Программные средства из пакетов разработки драйверов от третьих фирм


В настоящее время две фирмы, Jungo Ltd. и CompuWare Corp., предлагают собственные коммерческие пакеты проектирования драйверов.

Фирма Jungo Ltd. предлагает разработчикам пакет WinDriver, позволяющий быстро создавать драйверы пользовательского режима (практически &#8212 динамические библиотеки), и пакет KernelDriver для создания кода, работающего в режиме ядра (что более эффективно в смысле производительности драйвера). Оба пакета имеют удобные заготовки для программирования устройств, подключаемых к шинам PCI, USB, ISA, и позволяют работать с ними программистам на Delphi и Basic. Однако собственный базис функций представляет собой почти что новый язык программирования (в той степени, как это можно сказать, например, о наборе функций MFC для программиста, ранее работавшего только с API функциями Windows). Кроме того, для уверенной работы с данными пакетами крайне необходима постоянная лицензионная поддержка.

Пакет Numega Driver Studio (от CompuWare Corp.) содержит в своем составе мощный отладчик SoftIce, ориентированный исключительно на платформу Intel. Отладчик SoftIce позволяет проводить отладку на одном компьютере (хотя опытные разработчики в категоричной форме рекомендуют не проводить отладку драйвера на компьютере с ценными данными и там, где установлены все программные средства разработки &#8212 время, потраченное на восстановление системы квалифицированным специалистом, зачастую стоит дороже дополнительного компьютера). И хотя интерфейс с пользователем остается практически неизменным со времен MS DOS, отладчик SoftIce обладает мощными возможностями, по функциональности вряд ли уступающими возможностям отладчиков Visual Studio для пользовательского режима.

Несколько полезных программных средств от CompuWare Corporation рассматриваются ниже.



Программные средства от Марка Руссиновича и SysInternals


Автор книги "Inside Windows 2000" Марк Руссинович является ветераном разработки программных средств для Windows, и некоторые из них будут весьма полезны разработчикам драйверов режима ядра. Ознакомиться с его программами и загрузить их shareware версии можно на интернет сайте sysinternals.com.



Программные средства от Microsoft


Основным средством разработки является Microsoft Windows DDK, Device Driver Kit, — пакет разработки драйверов, включающий компилятор, редактор связей (линкер), заголовочные файлы, библиотеки, большой набор примеров (часть из которых является драйверами, реально работающими в операционной системе) и, разумеется, документацию. В состав пакета входит также отладчик WinDbg, позволяющий проводить интерактивную отладку драйвера на двухкомпьютерной конфигурации и при наличии файлов отладочных идентификаторов операционной системы WinDbg кроме того, позволяет просматривать файлы дампа (образа) памяти, полученного при фатальных сбоях операционной системы (так называемый crash dump file).

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

В бесплатно распространяемом пакете DDK всегда отсутствовала интегрированная среда разработки. Поэтому программисты драйверов всегда были вынуждены подбирать для себя и средство редактирования исходного кода. Выбор был, практически, безальтернативен — пакет Visual С++ (теперь это Visual Studio 7 Net). При должной настройке этой среды процесс выявлений синтаксических ошибок существенно облегчается — неотъемлемое преимущество интегрированных сред программирования. Компилятор и редактор связей Visual Studio C++ создают нормальный бинарный код, вполне работоспособный при указании соответствующих опций (настроек) компиляции, однако эталоном следует считать бинарный код, получающийся при компиляции кода драйвера с использованием утилиты Build из состава пакета DDK. Разумеется, встроенный интерактивный отладчик Visual Studio и прилагаемая документация становятся для разработки драйвера совершенно бесполезными, поскольку не предназначены для работы с программным обеспечением для режима ядра.

Разработчику драйвера могут быть полезны некоторые вспомогательные программы, поставляемые теперь в составе Platform SDK, например утилита Depends, подробнее о которой будет сказано ниже.

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



Программные средства, применяемые при разработке драйверов


В данной главе будет рассмотрен набор программных средств, который следует считать минимальным для использования при разработке драйверов Windows. Часть из них успешно работает или имеет версии под Windows 98, однако можно с уверенностью сказать, что лишь под NT разработка драйверов имеет основательную инструментальную поддержку.



Программы ChkInf и GenInf


Программа ChkInf (точнее, скрипт для интерпретатора Perl) предназначена для проверки inf файлов, необходимых для выполнения установки драйвера в операционной системе, и подробно будет рассмотрена в главе 11.

Программа GenInf предназначена для генерации inf файлов в режиме вопросов и ответов (Wizard).



Программы от SmidgeonSoft


Если программы Свена Шрайбера интересны для исследования Windows и допускают использования своего кода в новых разработках, представляя собой всего лишь консольные приложения, то программы фирмы SmidgeonSoft являются завершенными программными продуктами с удобным графическим интерфейсом и реализующими практически все возможности из числа упомянутых ранее..

На интернет-сайте фирмы SmidgeonSoft по адресу smidgeonsoft.com

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



Программы Свена Шрайбера


Программы, о которых здесь пойдет речь — лишь часть из программ-примеров, прилагаемых к уже упомянутой книге Свена Шрайбера "Недокументированные возможности Windows 2000", где можно ознакомиться с ними более подробно, включая исходные тексты и рабочие проекты, позволяющие выполнить компиляцию и сборку действительно работающих приложений. В настоящее время программы и их исходные тексты можно загрузить также с авторского Интернет-сайта Свена Шрайбера по адресу orgon.com/w2k_internals/cd.html.



Производительность


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

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

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

Вопросы повышение производительности в значительной степени касаются и разработчиков драйверов. В момент, когда пользовательское приложение или системный поток посылает запрос к обслуживаемому драйвером устройству (точнее сказать, объекту устройства), жизненно важно, чтобы драйверный код не блокировал выполнения запроса. Если же запрос не может быть обработан немедленно (например, устройство занято или медленно работает), запрос должен быть поставлен в очередь для последующей обработки. Диспетчер ввода/вывода предоставляет необходимые для этого процедуры.



Промежуточный вывод на экран


Делать сообщения из недр отлаживаемого кода при помощи функции printf &#8212 это метод с давними и славными традициями. Его иногда называют "генерацией промежуточного вывода". Фактически, нет таких ошибок, которые нельзя было бы найти, если применить такой метод в достаточном (!) объеме.

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

и KdPrint. Обе функции посылают форматированные строки, созданные на целевом компьютере, отладчику WinDbg, работающему на хост-компьютере.

Как было сказано ранее, собирать отладочные сообщения можно и при помощи DebugView.

Вызов KdPrint на самом деле является макроопределением и превращается в пустышку (невыполняемый участок) в релизной сборке драйвера (free build).



Простейший драйвер для работы с прерываниями


Драйвер, работающий с прерываниями, прежде всего, должен зарегистрировать и подключить к источнику объект прерывания, имея собственную процедуру обработки поступающих прерываний. В нашем случае, при работе с заглушкой CheckIt, драйвер сам выдает в порт комбинацию данных, которая вследствие наличия обратной связи CR.2 -&#62 SR.6 в заглушке CheckIt (она к этому моменту должна быть включена в системный LPT порт) приводит к генерации прерывания и последующему вызову Isr процедуры драйвера.

После загрузки и старта драйвер пребывает в устойчивом состоянии и ожидает запросов от клиентов. В данном случае клиентом выступает консольное приложение, которое Win32 вызовом CreateFile открывает дескриптор для доступа к драйверу, после чего обращается к нему с запросом на запись данных. В тестирующем приложении это осуществляется при помощи Win32 вызова WriteFile. Соответствующий IRP пакет поступает в обработчик DispatchWrite драйвера, который после проверки корректности запроса и перенесения данных во входной внутренний буфер драйвера, записывает в параллельный порт половину первого байта поступивших данных (это делает функция DoNextTransfer), совмещая это с генерацией прерывания (функция ForceInterrupt). Такая запись приводит к переносу первого байта данных и к первому вызову функции обработки прерывания Isr. Для безопасного доступа к данным запуск DoNextTransfer оформлен в драйвере при поддержке системного вызова KeSynchronizeExecution, см. описание прототипа в таблице 10.14.

Для соблюдения целостности передачи данных (а передача данных считается здесь завершенной, когда весь входной буфер данных "перекочует" в другой, выходной, внутренний буфер драйвера, предварительно пройдя через параллельный порт) драйвер отвергает все запросы от клиента, как на чтение (из внутреннего выходного буфера), так и на запись данных. Если по окончании обработки запроса на запись от клиента поступил новый запрос на запись, то оба внутренних буфера драйвера (входной и выходной) заполняются новыми данными заново, без какого-либо сохранения старых данных, хотя бы частично &#8212 данный драйвер имеет простейшую организацию и не "церемонится" с не полностью перенесенными данными.


При обработке прерывания Isr функция планирует вызов DpcForIsr процедуры, которая получает управление, читает данные из регистра состояния, помещает их в буфер для хранения и запускает новый перенос (с генерацией прерывания).

Таким образом, начиная с момента первой генерации прерывания до окончания всех данных, поступивших от клиента в одном запросе на запись, перенос половины первого байта продолжается переносом половины второго байта и т.п. По окончании передачи половины последнего байта и прохождении последнего прерывания, процедура DpcForIsr, получив управление, выполняет последнее чтение из порта, но уже не вызывает DoNextTransfer. Перенос данных закончен фактически. Формально же обработка запроса от вызова WriteFile могла бы закончиться раньше &#8212 после запуска DoNextTransfer (разумеется, с подачи KeSynchronizeExecution) рабочая функция драйвера DispatchWrite (обработчик запросов от WriteFile) сразу же пытается завершить обработку IRP пакета, поскольку ее основная задача &#8212 заполнить внутренний буфер и стартовать первый перенос. Однако в том случае, когда заглушка на месте, в работу поочередно вступают высокоприоритетные фрагменты кода (Isr, DpcForIsr, функции, вызываемые с подачи KeSynchronizeExecution), что более вероятно &#8212 перенос данных завершится еще до выхода из DispatchWrite. Лишь при отсутствии заглушки CheckIt прерывания не влияют на работу драйвера. Запуская тестовое приложение в отсутствие заглушки, можем наблюдать, что процедура DispatchWrite завершается, а при входе в DispatchRead драйвер видит, что предыдущий перенос не завершен (нет заглушки &#8212 нет способа генерировать прерывания и передавать данные), о чем драйвер и сообщает клиенту кодом ошибки 170, который программой ErrLook расшифровывается как "Требуемый ресурс занят". Такое же сообщение можно увидеть и при повторных запусках тестового приложения (если заглушка CheckIt отсутствует).


Простой драйвер "в-стиле-NT": Example.sys


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

Первые из них, драйверы пользовательского режима, представляют собой обычный программный код, как правило, оформленный в хорошо всем знакомые динамически загружаемые библиотеки (DLL). Эти драйверы стеснены в обращении к системным ресурсам и опираются в своей работе на модули режима ядра, с которыми они тесно сотрудничают. Так устроен пакет проектирование драйверов WinDriver от фирмы Jungo Ltd, в котором клиентские приложения через функции пользовательского режима (библиотеку функций WinDriver UserMode Library, являющуюся, по сути, драйвером пользовательского режима) общаются с кодом режима ядра (модулем WinDriver Kernel).

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

и free) в режиме ядра применяются системные вызовы (например, ExAllocatePool и ExFreePool). Более подробно вопросы программирования в режиме ядра будут рассмотрены в главе 7. Пока что кратко рассмотрим на примере простого драйвера Example.sys, как организованы драйверы режима ядра и как происходит связь с ними из приложений пользовательского режима.

Приведенный ниже код драйвера Example.sys является завершенным драйвером, который готов к компиляции и использованию в операционной системе в качестве тестового примера. По своей сути, приведенный код не может быть полноценным WDM драйвером (в силу отсутствия в нем некоторых основных рабочих процедур драйверов PnP устройств), хотя и может быть успешно скомпилирован средствами Visual С или DDK с WDM директивами. Драйвер Example.sys более подходит под описание "монолитный драйвер в-стиле-NT", так что он вполне подойдет в качестве заготовки для экспериментов, которые будут над ним выполнены в последующих главах.


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

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



Пространство ввода/вывода


В некоторых реализациях процессорных архитектур доступ к регистрам устройств осуществляется при помощи специальных команд процессора &#8212 инструкций ввода/вывода. Они ссылаются на специальные наборы выводов процессора и определяют отдельное шинно-адресное пространство для устройств ввода/вывода. Адреса на этих шинах широко известны как порты (ports)

и не имеют никакого отношения к адресации памяти. В архитектуре Intel x86 адресное пространство ввода/вывода имеет размер 64 КБ (16 разрядов), а в языке ассемблера определено две инструкции для чтения и записи в этом пространстве: 'IN'

и 'OUT' (точнее, две группы инструкций, внутри которых различие имеет место по разрядности считываемых/записываемых данных).

Поскольку при создании драйвера следует избегать привязки к аппаратной платформе, Microsoft рекомендует избегать и использования реальных инструкций IN/OUT. Вместо этого следует использовать макроопределения HAL. Соответствие между традиционными инструкциям DOS/Windows ассемблера и макроопределениями HAL приводится в таблице 5.2.

Таблица 5.2. Макроопределения HAL для доступа к портам ввода/вывода

Ассемблер х86 Аналог HAL Описание
IN AL,DX

IN AL,port

READ_PORT_UCHAR Чтение 1 байта из порта ввода/вывода
IN AX,DX

IN AX,port

READ_PORT_USHORT Чтение 16-ти разрядного слова из порта ввода/вывода
IN EAX,DX

IN EAX,port

READ_PORT_ULONG Чтение 32-х разрядного слова из порта ввода/вывода
INSB READ_PORT_BUFFER_UCHAR Чтение массива байт из порта ввода/вывода
INSW READ_PORT_BUFFER_USHORT Чтение массива 16-ти разрядных слов из порта ввода/вывода
INSD READ_PORT_BUFFER_ULONG Чтение массива 32-х разрядных слов из порта ввода/вывода
OUT DX,AL

OUT port,AL

WRITE_PORT_UCHAR Запись 1 байта в порт ввода/вывода
OUT DX,AX

OUT port,AX

WRITE_PORT_USHORT Запись 16-ти разрядного слова в порт ввода/вывода
OUT DX,EAX

OUT port,EAX

WRITE_PORT_ULONG Запись 32-х разрядного слова в порт ввода/вывода
OUTSB WRITE_PORT_BUFFER_UCHAR Запись массива байт в порт ввода/вывода
OUTSW WRITE_PORT_BUFFER_USHORT Запись массива 16-ти разрядных слов в порт ввода/вывода
OUTSD WRITE_PORT_BUFFER_ULONG Запись массива 32-х разрядных слов в порт ввода/вывода



Проверка корректности вызовов кода, размещенного в страничной памяти


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

Для выполнения этой работы сконструировано специальное макроопределение 'PAGED_CODE();', которое проверяет текущий уровень IRQL (не превышает ли он APC_LEVEL) и генерирует отладочное сообщение при помощи вызовов KdPrint и RtlAssert, которое можно наблюдать, например, в окне программы DebugView, если она к тому моменту запущена.

Кстати, сам факт существования макроопределения PAGED_CODE говорит о том, что использование станичной памяти на высоких уровнях IRQL является некорректным не само по себе (иногда оно может "сойти с рук"). Причина в том, что данное стечение обстоятельств является потенциальной угрозой правилам системы: она откажется обслуживать ситуацию, когда данная страничная память окажется на диске, а уровень IRQL будет столь же высок.

В том случае, если выявлено диагностическое сообщение от макроопределения PAGED_CODE, следует изменить тактику использования страничной памяти в указанном месте программного кода (в сообщении указан файл и строка, где возникла некорректная ситуация), возможно, отказаться от нее.



Проверка синтаксиса INF файла


Отладка inf-файлов не является простой задачей. В состав DDK входит утилита CHKINF, служащая для проверки правильности синтаксиса и организации inf-файлов, и ее можно найти в подкаталоге Tools каталога DDK. Она опирается на интерпретатор Perl, который доступен для загрузки с интернет-сайта perl.com. Вывод результатов выполняется в форме HTML файла. Хотя нельзя не признать определенные достоинства этой утилиты, тем не менее, как указывают многие источники, она выдает ошибки при проверке вполне нормальных inf-файлов.

Установив ActivePerl интерпретатор, загруженный с сайта activestate.com, и запустив его командой из директории пакета DDK \tools\chkinf

chkinf.bat path\Example.inf /b

где path &#8212 это путь к проверяемому inf-файлу, ключ /b обеспечивает автозапуск IE (Internet Explorer) для просмотра результатов, получаем результаты проверки в окне IE, см. рисунок 12.5.

Текстовый вариант до исправления некоторых погрешностей выглядел следующим образом:

Summary of "k:\Ex\Example.inf" Total Errors: 2 Total Warnings: 6 ________________________________________________________________

Errors: Line 5: (E22.1.1081) Directive: CatalogFile required (and must not be blank) in section [Version] for WHQL digital signature. Line 11: (E22.1.1310) Class ExampleDrvClass (ClassGUID {DC16BE99-C06B-4801-A144-43A98BB99052}) is unrecognized.
________________________________________________________________

Warnings: Line 0: (W22.1.2212) No Copyright information found. Line 34: (W22.1.2023) Use a string token, and put localizable text in the [Strings] section. Line 45: (W22.1.2208) NT-specific section(s) found. Ignoring general section. Line 45: (W22.1.2083) Section [EXAMPLE.INSTALL] not referenced Line 49: (W22.1.2083) Section [EXAMPLE.ADDREG] not referenced Line 53: (W22.1.2083) Section [EXAMPLE.FILES.DRIVER] not referenced

Первая ошибка (E22.1.1081) обусловлена тем, что отсутствует .cat файл, где содержится подписанный Microsoft инсталляционный пакет.

Вторая ошибка (E22.1.1310) обусловлена тем, что утилита CHKINF


не отреагировала на секции и директивы установки нового класса устройств ExampleDrvClass (к тому же, к моменту проверки новый класс уже находился в Системном Реестре). Можно заменить класс на Unknown, но тогда утилита CHKINF будет "недовольна" тем, что это &#8212 устаревший класс. Если попробовать ввести класс USB, то утилита CHKINF будет вполне "удовлетворена", хотя не отследит неверный формат идентификатора модели (неверный для USB устройства, см. ниже), который задан здесь как "*svpBook\Example". Это лишний раз подтверждает тот факт, что CHKINF проверяет только базисные правила синтаксиса и структуры inf-файлов.

Предупреждения тоже имеют несложные объяснения.

Первое (W22.1.2212) указывает на то, что в inf-файле нет записи copyright. Исправляется этот недостаток следующим образом:

; Example.Inf - install information file ; Created 22 feb 2002 by SVP ; Copyright (c) SVP 2003. All rights reserved.

Последняя строка, которая добавлена для устранения данного предупреждения, хотя формально и является комментарием, но считается информативной относительно авторских прав (содержит ключевые слова Copyright (c)).

Второе предупреждение (W22.1.2023) указывает:

[SourceDisksNames] 1="Example build directory",,, ; (W22.1.2023) Use a string token, and put localizable text ; in the [Strings] section.

To есть рекомендуется заменить строку "Example build directory" на маркер, значение которого следует раскрыть в секции [Strings], например, следующим образом:

[SourceDisksNames] 1=%ExamDir%,,,

[Strings] . . . ; Раскрываем значение нового маркера ExamDir="Example build directory"

Третье предупреждение (W22.1.2208) информирует, что в файле найдены секции для операционной системы NT, поэтому стандартные секции (для Windows 98) будут проигнорированы.

Оставшиеся предупреждения даны потому, что указанные секции (введенные для использования в Windows 98) проигнорированы, соответственно, на них нет ни одной ссылки.

Рис. 12.5.

Результаты работы CHKINF (после исправлений)
В пакете DDK в подкаталоге Tools имеется также утилита GENINF.EXE, которая облегчает создание inf-файлов, но более всего подходит для изучения процесса их создания и может оказать некоторую помощь начинающим разработчикам.


Рабочая процедура обработки IOCTL запросов


Процедура DeviceControlRoutine предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодом IRP_MJ_DEVICE_CONTROL по результатам обращения к драйверу из пользовательских приложений с вызовами DeviceIoControl.

В нашем примере это самая важная функция. Она реализует обработку пяти IOCTL запросов:

IOCTL_PRINT_DEBUG_MESS — выводим отладочное сообщение в окно DebugView.

IOCTL_CHANGE_IRQL — проводим эксперимент, насколько высоко можно искусственно поднять уровень IRQL в коде драйвера.

IOCTL_MAKE_SYSTEM_CRASH — проводим эксперимент по "обрушению" операционной системы и пытаемся его предотвратить.

IOCTL_TOUCH_PORT_378H — проводим эксперимент по обращению к аппаратным ресурсам системы.

IOCTL_SEND_BYTE_TO_USER — отправляем байт данных в пользовательское приложение.

Эти IOCTL коды являются пользовательскими — они определены с помощью макроса CTL_CODE в файле Driver.h, который является частью данного проекта, и речь о котором пойдет ниже.

Определения используемых ниже непривычных для программистов Win32 типов данных (например, UCHAR или PUCHAR) можно найти в DDK в файле Windef.h .

// (Файл init.cpp) // DeviceControlRoutine: обработчик IRP_MJ_DEVICE_CONTROL запросов // Аргументы: // Указатель на объект нашего FDO // Указатель на структуру IRP, поступившего от Диспетчера ВВ // Возвращает: STATUS_XXX // #define SMALL_VERSION // В том случае, если не закомментировать верхнюю строчку v будет // выполнена компиляция версии, в которой будет обрабатываться только // один тип IOCTL запросов -- IOCTL_MAKE_SYSTEM_CRASH

NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp ) { NTSTATUS status = STATUS_SUCCESS; ULONG BytesTxd =0; // Число переданных/полученных байт (пока 0) PIO_STACK_LOCATION IrpStack=IoGetCurrentIrpStackLocation(Irp);

// Получаем указатель на расширение устройства PEXAMPLE_DEVICE_EXTENSION dx = (PEXAMPLE_DEVICE_EXTENSION)fdo->DeviceExtension; //------------------------------- // Выделяем из IRP собственно значение IOCTL кода, по поводу // которого случился вызов: ULONG ControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode; ULONG method = ControlCode & 0x03;


errDetected=1; }; #if DBG DbgPrint("-Example- Value of x is %X.",x); if(errDetected) DbgPrint("-Example- Except detected in Example driver."); #endif break; }

#ifndef SMALL_VERSION case IOCTL_TOUCH_PORT_378H: { unsigned short ECRegister = 0x378+0x402; #if DBG DbgPrint("-Example- IOCTL_TOUCH_PORT_378H."); #endif // Пробуем программно перевести параллельный порт 378, // сконфигурированный средствами BIOS как ECP+EPP, в // режим EPP. _asm { mov dx,ECRegister ; xor al,al ; out dx,al ; Установить EPP mode 000 mov al,095h ; Биты 7:5 = 100 out dx,al ; Установить EPP mode 100 } // Подобные действия в приложении пользовательского // режима под NT обязательно привело бы к аварийной // выгрузке приложения с сообщением об ошибке! // Практически эти пять строк демонстрируют, что можно // работать с LPT портом под Windows NT ! break; }

case IOCTL_SEND_BYTE_TO_USER: { // Размер данных, поступивших от пользователя: ULONG InputLength = //только лишь для примера IrpStack->Parameters.DeviceIoControl.InputBufferLength; // Размер буфера для данных, ожидаемых пользователем ULONG OutputLength = IrpStack->Parameters.DeviceIoControl.OutputBufferLength; #if DBG DbgPrint("-Example- Buffer outlength %d",OutputLength); #endif

if( OutputLengthAssociatedIrp.SystemBuffer; #if DBG DbgPrint("-Example- Method : BUFFERED."); #endif } else if (method==METHOD_NEITHER) { buff=(unsigned char*)Irp->UserBuffer; #if DBG DbgPrint("-Example- Method : NEITHER."); #endif } else { #if DBG DbgPrint("-Example- Method : unsupported."); #endif status = STATUS_INVALID_DEVICE_REQUEST; break; } #if DBG DbgPrint("-Example- Buffer address is %08X",buff); #endif *buff=33; // Любимое число Штирлица BytesTxd = 1; // Передали 1 байт break; } #endif // SMALL_VERSION // Ошибочный запрос (код IOCTL, который не обрабатывается): default: status = STATUS_INVALID_DEVICE_REQUEST; } // Освобождение спин-блокировки KeReleaseSpinLock(&MySpinLock,irql);



#if DBG DbgPrint("-Example- DeviceIoControl: %d bytes written.", (int)BytesTxd); #endif

return CompleteIrp(Irp,status,BytesTxd); // Завершение IRP }

Корректность фрагмента кода, посвященного обработке IOCTL запроса IOCTL_TOUCH_PORT_378H, может вызвать споры, поскольку действует "напролом", не обращая внимания на то, что в системе могут быть устройства и драйвера, работающие с этим портом, существование которых следует учитывать. Однако цель данного примера &#8212 показать, что само по себе обращение к аппаратным ресурсам в режиме ядра является делом тривиальным, не имеющим ограничений со стороны операционной системы.


Рабочая процедура обработки запросов открытия драйвера


Процедура Create_File_IRPprocessing предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодами IRP_MJ_CREATE по результатам обращения к драйверу из пользовательских приложений с вызовами CreateFile или из кода режима ядра с вызовами ZwCreateFile. В нашем примере эта функция не выполняет никаких особых действий (хотя можно было бы завести счетчик открытых дескрипторов и т.п.), однако без регистрации данной процедуры система просто не позволила бы клиенту "открыть" драйвер

для работы с ним (хотя сам драйвер мог бы успешно загружаться и стартовать).

// // (Файл init.cpp) // Create_File_IRPprocessing: Берет на себя обработку запросов с // кодом IRP_MJ_CREATE. // Аргументы: // Указатель на объект нашего FDO // Указатель на структуру IRP, поступившего от Диспетчера ВВ // NTSTATUS Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo,IN PIRP Irp) { PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp); // Задаем печать отладочных сообщений - если сборка отладочная #if DBG DbgPrint("-Example- Create File is %ws", &(IrpStack->FileObject->FileName.Buffer)); #endif return CompleteIrp(Irp,STATUS_SUCCESS,0); // Успешное завершение }



Рабочая процедура обработки запросов read/write


Процедура ReadWrite_IRPhandler предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодами IRP_MJ_READ/IRP_MJ_WRITE по результатам обращения к драйверу из пользовательских приложений с вызовами read/write или из кода режима ядра с вызовами ZwReadFile или ZwWriteFile. В данном примере наша функция обработки запросов чтения/записи ничего полезного не делает, и ее регистрация выполнена только для демонстрации, как это могло бы быть в более "развитом" драйвере.

Описание прототипов рабочих процедура драйвера (параметров их вызова) можно найти в документации DDK (2000, ХР, 2003), если в режиме указателя задать ключевые слова Dispatch..., например, DispatchRead. Если ваша программа просмотра файлов справки не поддерживает переходов между разными .chm файлами (представляющими полную документацию по DDK), то можно сразу обратиться к файлу kmarch.chm (который, собственно, и содержит информацию по рабочим процедурам). Там же можно узнать, на каком уровне IRQL происходит вызов конкретной функции.

// // (Файл init.cpp) // ReadWrite_IRPhandler: Берет на себя обработку запросов // чтения/записи и завершает обработку IRP вызовом CompleteIrp // с числом переданных/полученных байт (BytesTxd) равным нулю. // Аргументы: // Указатель на объект нашего FDO // Указатель на структуру IRP, поступившего от Диспетчера ввода/вывода NTSTATUS ReadWrite_IRPhandler( IN PDEVICE_OBJECT fdo, IN PIRP Irp ) { ULONG BytesTxd = 0; NTSTATUS status = STATUS_SUCCESS; //Завершение с кодом status // Задаем печать отладочных сообщений v если сборка отладочная #if DBG DbgPrint("-Example- in ReadWrite_IRPhandler."); #endif return CompleteIrp(Irp,status,BytesTxd); }



Рабочая процедура обработки запросов закрытия драйвера


Процедура Close_File_IRPprocessing предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодом IRP_MJ_CLOSE по результатам обращения к драйверу из пользовательских приложений с вызовами CloseHandle

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

// (Файл init.cpp) // Close_File_IRPprocessing: Берет на себя обработку запросов с // кодом IRP_MJ_CLOSE. // Аргументы: // Указатель на объект нашего FDO // Указатель на структуру IRP, поступившего от Диспетчера ввода/вывода NTSTATUS Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo,IN PIRP Irp) { #if DBG // Задаем печать отладочных сообщений - если сборка отладочная DbgPrint("-Example- In Close handler."); #endif return CompleteIrp(Irp,STATUS_SUCCESS,0);// Успешное завершение }



Рабочая процедура выгрузки драйвера


Процедура UnloadRoutine выполняет завершающую работу перед тем как драйвер растворится в небытии.

При следовании WDM модели, драйвер должен был бы зарегистрировать обработчик PnP запросов (то есть IRP_MJ_PNP) и перед вызовом UnloadRoutine получал бы IRP пакеты с кодом IRP_MJ_PNP и суб-кодом IRP_MN_STOP_DEVICE (например, когда пользователь решил отключить устройство, воспользовавшись окном Диспетчера Устройств в Настройках системы). В этом обработчике и следует выполнять действия, предшествующие удалению WDM драйвера.

// // (Файл init.cpp) // UnloadRoutine: Выгружает драйвер, освобождая оставшиеся объекты // Вызывается системой, когда необходимо выгрузить драйвер. // Как и процедура AddDevice, регистрируется иначе чем // все остальные рабочие процедуры и не получает никаких IRP. // Arguments: указатель на объект драйвера //

#pragma code_seg("PAGE") // Допускает размещение в странично организованной памяти // VOID UnloadRoutine(IN PDRIVER_OBJECT pDriverObject) { PDEVICE_OBJECT pNextDevObj; int i;

// Задаем печать отладочных сообщений v если сборка отладочная #if DBG DbgPrint("-Example- In Unload Routine."); #endif //========================================================== // Нижеприведенные операции в полномасштабном WDM драйвере // следовало бы поместить в обработчике IRP_MJ_PNP запросов // с субкодом IRP_MN_REMOVE_DEVICE, но в силу простоты // драйвера, сделаем это здесь. // Проходим по всем объектам устройств, контролируемым // драйвером pNextDevObj = pDriverObject->DeviceObject;

for(i=0; pNextDevObj!=NULL; i++) { PEXAMPLE_DEVICE_EXTENSION dx = (PEXAMPLE_DEVICE_EXTENSION)pNextDevObj->DeviceExtension; // Удаляем символьную ссылку и уничтожаем FDO: UNICODE_STRING *pLinkName = & (dx->ustrSymLinkName); // !!! сохраняем указатель: pNextDevObj = pNextDevObj->NextDevice;

#if DBG DbgPrint("-Example- Deleted device (%d) : pointer to FDO = %X.", i,dx->fdo); DbgPrint("-Example- Deleted symlink = %ws.", pLinkName->Buffer); #endif

IoDeleteSymbolicLink(pLinkName); IoDeleteDevice( dx->fdo); } } #pragma code_seg() // end PAGE section



Рабочие процедуры драйвера


Все функции, опубликованные в процедуре DriverEntry путем заполнения массива DriverObject-&#62MajorFunction [...], вызываются Диспетчером ввода/вывода для обработки соответствующих запросов от клиентов драйвера (приложений пользовательского режима или от модулей режима ядра). Запросы эти всегда оформлены в виде специальных структур данных &#8212 пакетов IRP.


Диспетчер ввода/вывода заполняет пропущенные места в массиве MajorFunction значением указателя на функцию _IoInvalidDeviceRequest прежде, чем обращается к процедуре DriverEntry.

Все функции драйвера, участвующие в обработке запросов и подлежащие занесению в таблицу (массив) MajorFunction используют один и тот же протокол вызовов, что включает число и тип передаваемых им параметров и тип вызова. Рабочие процедуры выполняются на PASSIVE_LEVEL уровне IRQL, что означает, что они могут обращаться к ресурсам страничной памяти. (Разумеется, возможны ситуации искусственного изменения этого правила, как это было показано в примере Example.sys главы 3.)

Таблица 8.7а. Прототип, описывающий рабочую (dispatch) процедуру драйвера

NTSTATUS DispatchRoutine IRQL == PASSIVE_LEVEL
Параметры Описание
IN PDEVICE_OBJECT pDevObject Указатель на объект устройства, для которого предназначается IRP запрос
IN PRIP pIrp Указатель на пакет IRP, описывающий этот запрос
Возвращаемое значение o STATUS_SUCCESS &#8212 запрос обработан o STATUS_PENDING &#8212 ожидается обработка запроса o STATUS_XXX &#8212 код ошибки

Рабочие процедуры обслуживания IOCTL запросов


Запросы данного типа формулируются в рамках двух более гибких типов запросов на ввод/вывод (IRP пакетах). Значение основного кода IRP_MJ_Xxx драйвер может найти в соответствующей ему ячейке стека IRP пакета, a IOCTL код размещен в Parameters.DeviceIoControl.IoControlCode той же ячейки стека IRP пакета.

IRP_MJ_DEVICE_CONTROL позволяет получать расширенные запросы от клиентов пользовательского режима посредством их действий через API вызов DeviceIoControl.

IRP_MJ_INTERNAL_DEVICE_CONTROL позволяет получать расширенные запросы от кода (клиента), функционирующего на уровне режиме ядра. Доступ к этим операциям из кода пользовательского режима не разрешается. Эта возможность используется, главным образом, другими драйверами в многоуровневом драйверном стеке для передачи специальных запросов. С другой стороны, версия для внутреннего пользования идентична стандартной версии. Значение IoControlCode помещается в IRP пакет инициатором запроса.

Следует отметить, что реализация процедуры разборки таких IRP запросов в драйвере требует вторичной диспетчеризации &#8212 в соответствии со значением IoControlCode. Это значение известно еще со времен MS DOS под именем IOCTL

&#8212 Input/Output ConTroL code.

Значения IOCTL, передаваемые в драйвер, могут быть определены разработчиком драйвера и имеют фиксированную внутреннюю организацию. Рисунок 8.2 демонстрирует поля 32-битной структуры IOCTL кода. Пакет DDK имеет в своем составе макроопределение CTL_CODE, которое обеспечивает приемлемый механизм генерации значений IOCTL, уже использованный в главе 3. Таблица 8.8 описывает аргументы этого макроопределения.

Рис. 8.2.

Структура блока данных IOCTL

Таблица 8.8. Аргументы макроопределения CTL_CODE

Параметры Описание
DeviceType FILE_DEVICE_XXX значения передаваемые в IoCreateDevice

• 0x0000 &#8212 0x7FFF &#8212 зарезервировано Microsoft

• 0x8000 &#8212 0xFFFF &#8212 определяется пользователем

ControlCode Определяемые драйвером IOCTL значения

• 0x000 &#8212 0x7FF &#8212 зарезервировано Microsoft (public)

• 0x800 &#8212 0xFFF &#8212 определяется пользователем

TransferType Способ получения доступа к буферу

• METHOD_BUFFERED

• METHOD_IN_DIRECT

• METHOD_OUT_DIRECT

• METHOD_NEITHER

RequiredAccess Требования инициатора относительно типа доступа

• FILE_ANY_ACCESS

• FILE_READ_DATA

• FILE_WRITE_DATA

• FILE_READ_DATA | FILE_WRITE_DATA

<
Операции драйвера, которые работают с IOCTL запросами, часто требуют задания буферной области для размещения входных либо выходных данных, то есть поступающих от пользовательского приложения в драйвер либо в обратном направлении, соответственно. Возможно, что в одном запросе используются сразу оба буфера. В самом деле, вызов функции пользовательского режима DeviceIoControl среди прочих входных параметров имеет два указателя на две буферные области, одну &#8212 для входных данных, другую &#8212 для выходных. Механизм переноса данных, обеспечиваемый Диспетчером ввода/вывода, определяется как раз в IOCTL. Это может быть либо буферизованный, либо прямой ввод-вывод, либо метод NEITHER. Как было сказано ранее относительно запросов чтения/записи, при буферизованном способе работы с данными, Диспетчер ввода/вывода копирует данные пользовательского буфера в/из промежуточного буфера, размещенного в нестраничном пуле, при работе с которым драйвер не будет испытывать сложностей. При прямом способе ввода/вывода драйвер получает прямой доступ к определенной пользователем буферной области памяти, которая предварительно зафиксирована в оперативной памяти.

В данном случае флаги, определяющие тип буферизации в объекте устройства (pDeviceObject-&#62Flags), не имеют значения при работе с IOCTL запросами. Механизм буферизированного обмена определяется при каждом задании значения IOCTL в специально предназначенном для этого фрагменте этой структуры данных. Данный подход обеспечивает максимальную гибкость при работе с вызовом пользовательского режима DeviceIoControl.

Поле TransferType (таблица 8.8) представляет собой два бита, которые определяют один из следующих типов буферизации:

METHOD_BUFFERED. Диспетчер ввода/вывода копирует пользовательский буфер в/из вспомогательного буфера, который он размещает в нестранично организованной памяти.

METHOD_IN_DIRECT. Диспетчер ввода/вывода предоставляет список страниц, которые представляют пользовательский буфер. Драйвер использует этот список для того, чтобы осуществить прямой ввод/вывод (используя DMA или программируемый ввод/вывод) от устройства к пользовательскому буферу, в Win32 API вызове DeviceIoControl



описанному 5-м (!!) параметром.

METHOD_OUT_DIRECT. Диспетчер ввода/ вывод предоставляет список страниц, которые представляют пользовательский буфер. Драйвер использует этот список для того, чтобы осуществить прямой ввод/вывод (используя DMA или программируемый ввод/вывод) от пользовательского буфера к устройству (буфер вводится в Win32 API вызове DeviceIoControl также 5-м параметром).

METHOD_NEITHER. Диспетчер ввода/вывода не предлагает буферизированной передачи данных. Пользовательский буфер (скорее всего, в странично организованной памяти) предоставляется драйверу.

Так как поле TransferType встроено в значение IOCTL, то документированные Microsoft программные компоненты определяют и механизм буферизации. Для значений IOCTL, определенных в коде драйвера, могут быть заданы любые требуемые значения для описания механизма переноса данных. Для переноса небольших объемов данных и при небольшой скорости обмена вполне приемлем буферизованный ввод/вывод. Для переноса больших объемов и быстрой работы более подойдет прямой ввод/вывод.

Как только у драйвера появляется объявленная им рабочая процедура для обслуживания IRP пакетов с кодами IRP_MJ_INTERNAL_DEVICE_CONTROL либо IRP_MJ_DEVICE_CONTROL, Диспетчер ввода/вывода начинает пропускать соответствующие пакеты IRP внутрь драйвера. Интерпретация поступающих кодов IOCTL управления устройством становится обязанностью и ответственностью драйвера, включая проверку значений полей внутри кода IOCTL. Любое 32 разрядное число, посланное инициатором запроса в качестве IOCTL кода, поступит в соответствующую рабочую процедуру драйвер, поскольку Диспетчер ввода/вывода не выполняет проверки корректности IOCTL кодов.

Типовая конфигурация рабочей процедуры по обслуживанию IOCTL запросов должна быть большим оператором switch, например:

NTSTATUS IoControlCodeHandler(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { NTSTATUS status = STATUS_SUCCESS; PMYDEVICE_EXTENSION pDevExt; ULONG ioctlCode, inSize, outSize; // Находим нужную ячейку стека IRP пакета PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(pIrp); // Находим код IOCTL запроса ioctlCode = pIrpStack -&#62Parameters.DeviceIoControl.IoControlCode; // и требуемого размера передаваемых данных inSize = pIrpStack-&#62Parameters.DeviceIoControl.InputBufferLenght; outsize = pIrpStack-&#62Parameters.DeviceIoControl.OutputBufferLenght;



switch(controlCode) { // Вторичная диспетчеризация case IOCTL_CODE_1: { // Всегда следует проверять входные параметры if(inSize &#62 0 || outSize &#62 0) { Status = STATUS_INVALID_PARAMETER; break; } } default: // Драйвер получил непредусмотренные коды IOCTL status = STATUS_INVALID_DEVICE_REQUEST; break; } pIrp-&#62IoStatus.Status = status; pIrp-&#62IoStatus.Information = 0; // нет данных для передачи IoCompleteRequest( pIrp, IO__NO_INCREMENT ); return status; }

Доступ к буферным областям, содержащим данные или предназначенным для данных, описывается таблицей 8.9.

Таблица 8.9. Передача адресов буферов данных в IRP пакетах, описывающих IOCTL запросы

METHOD_BUFFERED METHOD_IN_DIRECT

или METHOD_OUT_DIRECT
METHOD_NEITHER
Input Буфер с данными Использует буферизацию (системный буфер)

Адрес буфера в системном адресном пространстве указан в pIrp-&#62AssociatedIrp.SystemBuffer
Клиентский виртуальный адрес в Parameters.DeviceIoControl. Type3InputBuffer
Длина указана в Parameters.DeviceIoControl.InputBufferLength
Output

Буфер для данных
Использует буферизацию (системный буфер)

Адрес буфера в системном адресном пространстве указан в pIrp-&#62 AssociatedIrp.SystemBuffer
Использует прямой доступ, клиентский буфер преобразован в MDL список, указатель на который размещен в

PIrp-&#62MdlAddress
Клиентский виртуальный адрес в pIrp-&#62UserBuffer
Длина указана в Parameters.DeviceloControl.OutputBufferLength
Названия Input

и Output здесь и в литературе трактуются с точки зрения драйвера. Буфер 'Input' содержит данные, поступающие от клиента, скорее всего, предназначенные для вывода в устройство. Буфер 'Output' указывает на то место, куда следует поместить данные, ожидаемые клиентом, скорее всего, прочитанные из устройства. Кстати сказать, в описании вызова DeviceIoControl в документации MSDN никакого разночтения с данной трактовкой названий не наблюдается: буфер с данными для выполнения операции (3-й параметр вызова) называется lplnputBuffer, а буфер для получаемых данных (5-й параметр вызова) называется lpOutputBuffer.
<


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

Может показаться странным, что для метода METHOD_IN_DIRECT строится MDL список для выходного (output) буфера, а для входного как бы используется менее мощный метод METHOD_BUFFERED. Тем не менее, это так. Желающие могут обойти это самостоятельно, просто переставив в своих запросах DeviceIoControl

адрес input буфера на место output буфера (или учитывая это в драйвере, как это сделала фирма Cypress в драйвере Ezusb.sys). Следует отметить, что имеющаяся в документации DDK пометка относительно построения MDL списка для поставляемых клиентом данных (для input буфера с размещением в поле IRP пакета MdlAddress) на практике и в остальной литературе по данному вопросу подтверждения не находит.

Повторим сведения о размещении областей с данными/для данных при подготовке IRP пакета, описывающего IOCTL запрос к драйверу.


Рабочие процедуры обслуживания ввода/вывода


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

Вызванная драйверная процедура проверяет запрос и либо его обрабатывает, либо, при необходимости, делает запрос к Диспетчеру ввода/вывода, чтобы он отложил запрос к устройству для последующей реальной работы с ним. Во втором случае, вызванная рабочая процедура возвращает управление Диспетчеру ввода/вывода, помечая поступивший запрос как незавершенный (pending) &#8212 это случай простой задержки обработки IRP запроса (отличающийся от применения DPC процедур для завершения обработки прерывания).



Работа с ассоциативными списками


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

Такую возможность предоставляет механизм ассоциативных списков (lookaside list), который реализован в системных вызовах, представленных ниже. Поначалу, ассоциативный список &#8212 это всего лишь заранее созданный заголовок, который должен хранить информацию о состоянии списка, и не содержит никаких выделенных блоков памяти. По мере выполнения вызовов ExAllocateFrom(N)PagedLookasideList

(таблицы 7.9 и 7.10) такие блоки создаются &#8212 либо системным вызовом ExAllocatePoolWithTag, либо внутри предоставленной драйвером функции (указанной драйвером параметром pAllocFunction). По мере создания и, возможно, последующего освобождения выделенных ранее блоков (системным вызовом ExFreePool, либо предоставленной драйвером функцией), ассоциативный список может оказаться держателем некоторого количества блоков фиксированного размера в страничной либо нестраничной памяти (в зависимости от способа инициализации).

Таблица 7.9. Прототип вызова ExInitializePagedLookasideList

PVOID ExInitializePagedLookasideList IRQL &#60 DISPATCH_LEVEL
Параметры Создание ассоциативного списка блоков страничной памяти
IN PPAGED_LOOKASIDE_LIST pLookasideListHeader Указатель на предварительно выделенную драйвером область размером sizeof(PAGED_LOOKASIDE_LIST)
Остальные параметры совпадают с параметрами вызова ExInitializeNPagedLookasideList, см. ниже таблицу 7.10
Возвращаемое значение void

В настоящий момент, Windows XP и Server 2003 самостоятельно и динамически определяет максимальное число элементов в ассоциативном списке.
Для Windows 2000 в качестве такого параметра был анонсирован параметр Depth. (K сожалению, в документации способ определения значения этого параметра умалчивается, начиная с версии DDK Win98.) Следует отметить, что до достижения данного максимума освобождаемые драйвером блоки не возвращаются в системную память, оставаясь в составе списка. Если в списке имеются ранее освобожденные блоки, и поступил запрос на новый блок, то предоставляется указатель на один из них. Ситуация кардинально меняется если максимум достигнут. При запросе нового блока (и при этом ранее освобожденных блоков в списке нет) память под него берется непосредственно из системной памяти и при освобождении сразу же возвращается в соответствующий пул системной памяти &#8212 иными словами, исчезают преимущества использования ассоциативного списка.

Таблица 7.10. Прототип вызова ExInitializeNPagedLookasideList

VOID ExInitializeNPagedLookasideList IRQL &#60= DISPATCH_LEVEL
Параметры Создание ассоциативного списка блоков нестраничной памяти
IN PNPAGED_LOOKASIDE_LIST

pLookasideListHeader
Указатель на предварительно выделенную драйвером область размером sizeof(NPAGED_LOOKASIDE_LIST)
IN OPTIONAL PALLOCATE_FUNCTION

pAllocFunction
NULL или указатель на предоставляемую драйвером функцию, которая будет заниматься выделением блоков из массива системной нестраничной памяти (если NULL &#8212 будет использован системный вызов ExAllocatePoolWithTag)
IN OPTIONAL PFREE_FUNCTION

pFreeFunction
NULL или указатель на предоставляемую драйвером функцию, которая будет заниматься освобождением блоков (если NULL &#8212 будет использован вызов ExFreePool)
IN ULONG Flags Зарезервировано. Указывать 0
IN ULONG ByteSize Размер отдельных блоков, поддерживаемых данным списком
IN ULONG Tag Метка (тег) для создаваемых блоков, можно задавать как 4 символа, например, 'ABCD'
IN USHORT Depth Зарезервировано. Указывать 0
Возвращаемое значение void
Перед вызовом процедуры инициализации, для хранения заголовка ассоциативного списка драйвер должен получить область в нестраничной памяти



размером sizeof(PAGED_LOOKASIDE_LIST) или sizeof(NPAGED_LOOKASIDE_LIST) &#8212 в зависимости от того, какой список инициализируется. Для этих целей можно использовать описанные выше системные вызовы ExAllocatePool

или ExAllocatePoolWithTag. В конце работы со списком обязательно следует выполнить вызов ExDelete(N)PagedLookasideList.

Функции, на которые указывает pAllocFunction, имеют прототип:

PVOID MyAllocateFunction ( IN_POOL_TYPE PoolType, // PagedPool или NonPagedPool IN ULONG NumberOfBytes, // размер IN ULONG Tag // тег );

Функции, на которые указывает pFreeFunction, имеют прототип:

PVOID MyFreeFunction (PVOID pBuffer);

Таблица 7.11. Прототип вызова ExAllocateFromNPagedLookasideList

PVOID ExAllocateFromNPagedLookasideList IRQL &#60= DISPATCH_LEVEL
Параметры Выполняет выделение блока памяти из нестраничного списка
IN PNPAGED_LOOKASIDE_LIST

pLookasideList
Указатель на инициализированный ассоциативный список
Возвращаемое значение Указатель на блок фиксированного размера или NULL (если функция выделения памяти не смогла получить очередной блок)
Таблица 7.12. Прототип вызова ExAllocateFromPagedLookasideList

PVOID ExAllocateFromPagedLookasideList IRQL &#60 DISPATCH_LEVEL
Параметры Выполняет выделение блока памяти из страничного списка
IN PPAGED_LOOKASIDE_LIST

pLookasideList
Указатель на инициализированный ассоциативный список
Возвращаемое значение Указатель на блок фиксированного размера или NULL (если функция выделения памяти не смогла получить очередной блок)
Таблица 7.13. Прототип вызова ExFreeToNPagedLookasideList

VOID ExFreeToNPagedLookasideList IRQL &#60= DISPATCH_LEVEL
Параметры Возвращает блок в нестраничный ассоциативный список
IN PNPAGED_LOOKASIDE_LIST pLookasideList Указатель на инициализированный ассоциативный список
IN PVOID pEntry Указатель на ранее полученный из списка блок фиксированного размера
Возвращаемое значение void
Таблица 7.14. Прототип вызова ExFreeToPagedLookasideList



VOID ExFreeToPagedLookasideList IRQL &#60 DISPATCH_LEVEL
Параметры Возвращает блок в страничный ассоциативный список
IN PPAGED_LOOKASIDE_LIST

pLookasideList
Указатель на инициализированный ассоциативный список
IN PVOID pEntry Указатель на ранее полученный из списка блок фиксированного размера
Возвращаемое значение void
Таблица 7.15. Прототип вызова ExDeleteNPagedLookasideList

VOID ExDeleteNPagedLookasideList IRQL &#60= DISPATCH_LEVEL
Параметры Выполняет удаление нестраничного ассоциативного списка
IN PNPAGED_LOOKASIDE_LIST

pLookasideList
Указатель на ассоциативный список
Возвращаемое значение void
Таблица 7.16. Прототип вызова ExDeletePagedLookasideList

VOID ExDeletePagedLookasideList IRQL &#60 DISPATCH_LEVEL
Параметры Выполняет удаление страничного ассоциативного списка
IN PPAGED_LOOKASIDE_LIST

pLookasideList
Указатель на ассоциативный список
Возвращаемое значение void

Работа с драйвером Example.sys


Как уже было сказано, из всех возможных способов инсталляции и запуска драйвера Example.sys, ниже будет использован способ тестирования с применением тестирующего консольного приложения, которое само будет выполнять инсталляцию и удаление драйвера (прибегая к вызовам SCM Менеджера). Для поэтапного ознакомления с процессом взаимодействия драйвера и обращающегося к нему приложения рекомендуется запустить программу ExampleTest под отладчиком (например, Visual Studio) в пошаговом режиме.

Перед запуском тестирующей программы ExampleTest рекомендуется загрузить программу DebugView, чтобы в ее рабочем окне наблюдать сообщения, поступающие непосредственно из кода драйвера Example.sys (отладочной сборки).

Однако прежде чем перейти к рассмотрению сообщений от драйвера, после выполнения установки и запуска драйвера (программным кодом консольного приложения ExampleTest в пошаговом режиме), прежде следует обратиться к программам WinObj и DeviceTree или аналогичным им программным средствам для того, чтобы удостовериться, присутствует ли в них информация об установленном драйвере (как, например, после инсталляции Мастером Установки нового оборудования.)

Как уже было сказано в главе 2, протокол полученных программой DebugView отладочных сообщений драйвера можно сохранить в файле для последующего анализа. Ниже приведена информация из такого файла, отражающая события в драйвере Example.sys с момента его загрузки и вызова процедуры DriverEntry до момента выгрузки и вызова процедуры UnloadRoutine. Рассмотрим содержание этого файла подробнее.

Сообщения, отправляемые драйвером из процедуры DriverEntry (первое число — номер сообщения, второе — относительное время, не имеющие большого значения в данном случае), выглядят следующим образом:

00000000 0.00000000 =Example= In DriverEntry. 00000001 0.00003743 =Example= RegistryPath = \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Example. 00000002 0.00012823 =Example= FDO FF919C68, DevExt=FF919D20. 00000003 0.00021176 =Example= DriverEntry successfully completed.


В переменной RegistryPath содержится поступающий от системы путь (в формате UNICODE) внутри Системного Реестра, где можно найти информацию о запускаемом драйвере. Видим также, что созданный драйвером функциональный объект устройства (FDO) имеет адрес 80E57BE0, а недалеко от него (а именно — внутри) находится структура расширения устройства, которая была определена в файле Driver.h.

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

00000004 0.00172536 -Example- Create File is

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

Реакция на запрос IOCTL_PRINT_DEBUG_MESS:

00000005 0.00178850 -Example- In DeviceControlRoutine (fdo= FF919C68)
00000006 0.00180554 -Example- DeviceIoControl: IOCTL 222004.
00000007 0.00182230 -Example- PASSIVE_LEVEL (val=0)
00000008 0.00183515 -Example- IOCTL_PRINT_DEBUG_MESS.
00000009 0.00184940 -Example- DeviceIoControl: 0 bytes written.

Реакция на запрос IOCTL_CHANGE_IRQL:

00000010 0.00187594 -Example- In DeviceControlRoutine (fdo= FF919C68)
00000011 0.00189074 -Example- DeviceIoControl: IOCTL 222008.
00000012 0.00190331 -Example- PASSIVE_LEVEL (val=0)
00000013 0.00191477 -Example- IOCTL_CHANGE_IRQL.
00000014 0.00193125 -Example- DISPATCH_LEVEL value =2
00000015 0.00194466 -Example- IRQLs are old=2 new=10
00000016 0.00196226 -Example- DeviceIoControl: 0 bytes written.

Интересно, что попытки установить в Windows XP текущее значение IRQL, равное 25 (в середине диапазона аппаратных прерывания) не дала результата: было позволено только значение 10, что ниже границы начала аппаратных прерываний. В то же время, в Windows 98 и Windows Server 2003 (для сравнения) в этом месте выводится значение 25.

Реакция на запрос IOCTL_TOUCH_PORT_378H выглядит следующим образом:



00000017 0.00198377 -Example- In DeviceControlRoutine (fdo= FF919C68)
00000018 0.00199858 -Example- DeviceIoControl: IOCTL 222010.
00000019 0.00201115 -Example- PASSIVE_LEVEL (val=0)
00000020 0.00202344 -Example- IOCTL_TOUCH_PORT_378H.

Реакция на запрос IOCTL_SEND_BYTE_TO_USER:

00000021 0.00204104 -Example- DeviceIoControl: 0 bytes written.
00000022 0.00206870 -Example- In DeviceControlRoutine (fdo= FF919C68)
00000023 0.00208378 -Example- DeviceIoControl: IOCTL 222014.
00000024 0.00209664 -Example- PASSIVE_LEVEL (val=0)
00000025 0.00210921 -Example- Buffer outlength 1
00000026 0.00212066 -Example- Method : BUFFERED.
00000027 0.00213575 -Example- Buffer address is FF978828
00000028 0.00214944 -Example- DeviceIoControl: 1 bytes written.

Как видно из сообщений, здесь используется метод буферизации METHOD_BUFFERED, что и было описано в файле Driver.h при создании данного кода IOCTL. В соответствии с данным методом буферизации драйвер получает адрес буфера с явно системным значением (80E87E78 — адрес в системном пуле), что и должно было произойти при данном методе буферизации. Если бы в файле Driver.h при описании данного IOCTL кода был задан метод METHOD_NEITHER, то драйвер получил бы точный адрес переменной xdata из приложения ExampleTest (с типичным для пользовательского приложения адресом, типа 0002EFA0). Подробнее о том, как передаются данные при разных методах буферизации в IOCTL запросах, будет рассказано позже, в главе 6.

Размер буфера для передачи данных пользовательскому приложению равен 1. Реально переданных данных 1 байт, в то время как при обработке остальных запросов никакой передачи данных не происходит. (Размер буферных областей и переданных данных всегда исчисляется в байтах.)

Сообщения, отправляемые драйвером из функции Close_File_IRPprocessing, обработчика запросов Диспетчера ввода/вывода, которые тот делает к драйверу в результате обращения из тестирующего приложения с вызовом CloseHandle

:

00000029 0.00263888 -Example- In Close handler.

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



00000030 0.00447794 -Example- In Unload Routine.
00000031 0.00451985 -Example- Deleted device (0) : pointer to FDO = FF919C68.
00000032 0.00454220 -Example- Deleted symlink = \DosDevices\Example.

Выполнение IOCTL запроса с кодом IOCTL_MAKE_SYSTEM_CRASH, разумеется, здесь не приводится, потому что после такого действия система, например, Windows XP начинает перезагрузку (или получает управление отладчик SoftIce, если он установлен и активирован). Однако под Windows 98 данный вызов проходит как ни в чем не бывало, и драйвер выводит в рабочем окне DebugView сообщение о том, что ему удалось получить байт данных (равный, кстати, 0) по нулевому виртуальному адресу.

Чтобы ввести элемент интриги, можно добавить, что при устранении небольшого затруднения, мешающего перехвату обращения по нулевому виртуальному адресу, в Windows NT можно наблюдать, как это действительно срабатывает и переменная 'x' остается со своим значением 0xFF. Подробнее об этом можно узнать в 10 главе.


Работа с IRP пакетами-репликантами


Если промежуточный драйвер занимается "выпуском" собственных IRP пакетов, направляя при этом нижним драйверным слоям (одному или нескольким разным драйверам) по нескольку экземпляров сразу, то он рано или поздно попадет в следующую непростую ситуацию. До момента окончания обработки всех "вторичных" пакетов драйвер не может завершить обработку исходного (изначально поступившего) IRP пакета. Возможны два способа работы с размноженными вторичными пакетами.

В первом, "синхронном", случае рабочая процедура должна дождаться, пока все созданные драйвером IRP пакеты не будут обработаны в нижних уровнях. В общем случае, рабочая процедура выполняет следующее:

Выполняет вызовы IoBuildSynchronousFsdRequest или вызовы IoBuildDeviceIoControlRequest для того, чтобы создать необходимое количество IRP пакетов "синхронного" типа.

Выполняет вызовы IoCallDriver для передачи всех созданных драйвером пакетов IRP другим драйверам.

Выполняет вызовы KeWaitForMultipleObjects и ожидает завершения обработки всех переданных IRP пакетов.

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

Наконец, выполняет вызов IoCompleteRequest относительно исходного IRP пакета для того, чтобы возвратить его инициатору вызова.

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

Второй, "асинхронный", случай несколько сложнее, поскольку непонятно, где именно драйвер может остановиться и дожидаться завершения обработки всех пакетов. В этом случае драйверу рекомендуется подключить процедуру завершения к каждому созданному им IRP пакету, и процедура завершения (скорее всего &#8212 единственная для всех пакетов) должна сама определить, наступило ли время считать, что обработка исходного IRP запроса действительно завершена. План действий примерно таков:

Пометить пакет IRP, поступивший в рабочую процедуру от Диспетчера ввода/вывода как ожидающий обработки при помощи IoMarkPending.


Создать дополнительные пакеты IRP с использованием одного из описанных выше методов.

Подключить процедуру завершения (возможно &#8212 одну и ту же) к каждому из вновь созданных IRP пакетов вызовом IoSetCompletionRoutine. При выполнении этого вызова следует передать указатель на исходный IRP пакет в аргументе pContext.

Запомнить число созданных пакетов IRP в неиспользуемом поле исходного IRP пакета. Поле Parameters.Key текущей ячейки стека IRP пакета вполне годится.

Передать пакеты всем нужным драйверам вызовом IoCallDriver.

Возвратить значение STATUS_PENDING, поскольку обработка исходного запроса (пакета IRP) не завершена.

По окончании обработки каждого IRP пакета драйвером нижнего уровня во втором, "асинхронном", варианте вызывается процедура завершения рассматриваемого ("нашего") драйвера, которая выполняет следующие операции:

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

Уменьшает на единицу сохраненное ранее число незавершенных пакетов IRP. Это действие рекомендуется выполнять, приняв хотя бы минимальные меры по безопасному доступу к этому значению. Вполне подходит для этой цели вызов InterlockedDecrement.

В случае, если незавершенных пакетов не осталось, выполняет вызов IoCompleteRequest, что сигнализирует о полном завершении обработки исходного IRP запроса.

Возвращает управление Диспетчеру ввода/вывода с кодом завершения STATUS_MORE_PROCESSING_REQUIRED &#8212 для того, чтобы не допустить вызов процедур завершения вышестоящих драйверов для работы над пришедшим "снизу" IRP пакетом, созданным данным драйвером. Кстати заметить, к этому моменту рассматриваемый IRP пакет уже уничтожен.


Работа с MDL списками


Как было сказано выше, когда поступает IOCTL запрос от пользовательского приложения, в котором метод буферизации указан METHOD_IN_DIRECT либо METHOD_OUT_DIRECT, Диспетчер ввода/вывода выполняет построение MDL списка для выходного буфера (5-й параметр в пользовательском вызове DeviceIoControl). Указатель на этот MDL список передается в драйвер внутри структуры IRP пакета. Драйвер может зафиксировать (lock) область этого буфера в оперативной памяти вызовом MmProbeAndLockPages, после чего с этим буфером можно работать в разных контекстах, в том числе на высоких уровнях IRQL (DISPATCH_LEVEL и выше).

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

Таблица 7.17. Прототип вызова MmSizeOfMdl

ULONG MmSizeOfMdl IRQL &#8212 любой
Параметры Определяет размер структуры для MDL списка, который будет описывать область данного размера
IN PVOID Base Виртуальный адрес области, для которой следует определить размер предполагаемого MDL списка
IN ULONG Length Размер описываемой области в байтах
Возвращаемое значение Размер, необходимый для хранения MDL списка

Таблица 7.18. Прототип вызова MmCreateMdl

PVOID MmCreateMdl IRQL &#60 DISPATCH_LEVEL
Параметры Создает MDL список и инициализирует его (в документации DDK XP объявлена устаревшей функцией и вместо нее рекомендуется использовать вызов IoAllocateMdl)
IN PMDL OPTIONAL pMemoryDescriptorList Указатель на область размером не менее sizeof(MDL) или NULL. Во втором случае вызов самостоятельно выделяет память в нестраничной памяти.
IN PVOID Base Виртуальный адрес области, для которой следует построить MDL
IN ULONG Length Размер буфера в байтах
Возвращаемое значение Указатель на MDL список или NULL (при ошибке)
<
Таблица 7.19. Прототип вызова IoAllocateMdl

PVOID IoAllocateMdl IRQL &#60= DISPATCH_LEVEL
Параметры Создает MDL список и инициализирует его. При этом можно выполнить его привязку к нужному IRP пакету
IN PVOID VAddress Виртуальный адрес области, для которой будет построен MDL список
IN ULONG Length Размер виртуальной области, для которой следует построить MDL
IN BOOLEAN SecondaryBuffer Если pIrp равен NULL, см. ниже, то и параметр SecondaryBuffer должен быть равен NULL.

Если параметр pIrp не равен NULL, тогда при:

FALSE &#8212 указатель на созданную структуру MDL списка будет занесен в поле pIrp-&#62MdlAddress

TRUE &#8212 поле Next созданной MDL структуры будет указывать на MDL список, который прописан в поле pIrp-&#62MdlAddress
IN BOOLEAN ChargeQuota Если TRUE &#8212 вычесть из квоты текущего программного потока на создание MDL списков.

Обычно применяется FALSE
IN OUT PIRP pIrp NULL &#8212 не работаем с каким-либо IRP пакетом.

Иначе &#8212 см. описание SecondaryBuffer выше
Возвращаемое значение Указатель на MDL список.

Замечание. Инициализацию MDL списка можно считать завершенной только после вызова MmProbeAndLockPages, см. ниже
Поэтапный способ самостоятельного создания MDL списка состоит в следующем. Получив в результате вызова MmSizeOfMdl размер будущего MDL списка, драйвер должен выполнить выделение области памяти в нестраничном пуле, после чего можно переходить к инициализации MDL списка при помощи системного вызова MmInitializeMdl, описание которого приводится ниже.

Таблица 7.20. Прототип вызова MmInitializeMdl

VOID MmInitializeMdl IRQL &#60= DISPATCH_LEVEL
Параметры Выполняет инициализацию MDL списка
IN PMDL OPTIONAL pMdl Указатель на буфер для хранения MDL списка.
IN PVOID Base Виртуальный адрес области, для которой следует построить MDL
IN ULONG Length Размер буфера в байтах
Возвращаемое значение void

Замечание. Инициализацию MDL списка можно считать завершенной только после вызова MmProbeAndLockPages, см. ниже
<


Следует обратить внимание на то, как происходит завершение работы с MDL списками. Действия, выполненные функцией MmProbeAndLockPages, отменяются вызовом MmUnlockPages (см. ниже), а инициализированный MDL список следует "деактивировать" вызовом IoFreeMdl. Затем, если драйвер перед инициализацией самостоятельно выделял память под структуру MDL списка, то ее также следует явно освободить соответствующим системным вызовом (например, ExFreePool).

Таблица 7.21. Прототип вызова IoFreeMdl

VOID IoFreeMdl IRQL &#60= DISPATCH_LEVEL
Параметры Выполняет очистку MDL списка
IN PMDL pMdl Указатель на структуру, описывающую MDL список
Возвращаемое значение void
Таблица 7.22. Прототип вызова MmProbeAndLockPages

VOID MmProbeAndLockPages IRQL &#60 DISPATCH_LEVEL
Параметры Выполняет фиксацию страниц, описанных в MDL списке, в физической памяти
IN OUT PMDL pMdl Указатель на MDL список
IN KPROCESSOR_MODE AccessMode Режим, в котором будет проверяться доступ к анализируемым страницам:

KernelMode

UserMode
IN LOCK_OPERATION

Operation
Права доступа после того, как страницы будут зафиксированы. Одно из значений:

IoReadAccess

IoWriteAccess

IoModifyAccess
Возвращаемое значение void

Замечание. Часть MDL списка, описывающая физические страницы, после данного вызова, скорее всего, будет обновлена.
Таблица 7.23. Прототип вызова MmUnlockPages

VOID MmUnlockPages IRQL &#60= DISPATCH_LEVEL
Параметры Отменяет фиксацию страниц страничной памяти в оперативной памяти
IN PMDL pMdl Указатель на структуру, описывающую MDL список, соответствующий набору зафиксированных в оперативной памяти страниц
Возвращаемое значение void
После того как выполнена фиксация страниц в физической памяти, описываемых MDL списком (то есть описываемая область стала практически частью нестранично организованной памяти), имеет смысл поинтересоваться: какой же виртуальный адрес теперь у этой области памяти в терминах системного адресного пространства (адреса &#8212 более 0x8000000)? На этот вопрос может ответить системный вызов (макроопределение) MmGetSystemAddressForMdl, описываемый ниже.



Таблица 7.24. Прототип вызова MmGetSystemAddressForMdl

PVOID MmGetSystemAddressForMdl IRQL &#60= DISPATCH_LEVEL
Параметры Показывает системный виртуальный адрес начала буфера, описываемого MDL списком
IN PMDL pMdl Указатель на структуру MDL списка
Возвращаемое значение Адрес диапазона системных адресов
Непосредственный доступ к полям структуры MDL списка не приветствуется разработчиком Windows &#8212 фирмой Microsoft. (Хотя эта структура детально описана в заголовочных файлах wdm.h и ntddk.h, представленных в пакете DDK). Вместо этого предлагается использовать следующие функции.

Таблица 7.25. Прототип вызова MmGetMdlByteCount

ULONG MmGetMdlByteCount IRQL &#60= DISPATCH_LEVEL
Параметры Показывает размер буфера, описываемого MDL списком
IN PMDL pMdl Указатель на структуру MDL списка
Возвращаемое значение Размер области, описываемой MDL списком
Таблица 7.26. Прототип вызова MmGetMdlByteOffset

ULONG MmGetMdlByteOffset IRQL &#60= DISPATCH_LEVEL
Параметры Показывает смещение начала буфера на первой странице буферной области, описываемой данным MDL списком
IN PMDL pMdl Указатель на структуру MDL списка
Возвращаемое значение Смещение
Таблица 7.27. Прототип вызова MmGetMdlVirtualAddress

PVOID MmGetMdlVirtualAddress IRQL &#8212 любой
Параметры Показывает виртуальный адрес начала буфера, описываемого MDL списком
IN PMDL pMdl Указатель на структуру MDL списка
Возвращаемое значение Исходный виртуальный адрес описываемой данным MDL списком области памяти.

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


На самом деле, в составе пакета DDK имеется вызов, который позволит избежать многих бесполезных этапов в подготовке новой MDL структуры, если имеется другая, бывшая в употреблении ранее. Соответствующая функция называется MmPrepareMdlForReuse, см. описание ниже.

Таблица 7.28. Прототип вызова MmPrepareMdlForReuse

VOID MmPrepareMdlForReuse IRQL &#60= DISPATCH_LEVEL
Параметры Подготавливает MDL список к повторному использованию
IN PMDL pMdl Указатель на структуру MDL списка
Возвращаемое значение void

Работа с нижними слоями драйверного стека


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

Второе требование удовлетворить легче. Драйвер не знает и не может знать, что за клиент инициировал запрос, какова была его мотивация и полномочия. В этой ситуации драйвер просто получает IRP пакеты и должен добросовестно их обработать, отправляя их нижним слоям драйверов или выполняя всю обработку самостоятельно. Возвращая управление Диспетчеру ввода/вывода, рабочая процедура должна пометить текущий IRP пакет как завершенный (вызовом IoCompleteRequest, таблица 9.10) или как требующий дополнительной обработки (вызовом IoMarkIrpPending, таблица 9.12). Как должна развиваться ситуация в последнем варианте событий, было рассмотрено ранее.

Таблица 9.12. Прототип функции IoMarkIrpPending

VOID IoMarkIrpPending IRQL &#60= DISPATCH_LEVEL
Параметры Помечает пакет IRP как требующий дополнительной обработки
IN PIRP pIrp Указатель на текущий IRP пакет
Возвращаемое значение void

Драйвер обязан возвратить управление и пометить пакет как требующий дополнительной обработки в том случае, если пакет IRP, отправленный нижним слоям драйверов, вернулся со значением TRUE в поле pIRP-&#62PendingReturned (разумеется, если только драйвер сам не является создателем этого пакета).

Ситуация усложняется, если речь заходит о взаимодействии с нижними драйверными слоями. Если драйвер, получая запрос от Диспетчера ввода/вывода, просто отправляет его нижним слоям (устанавливая процедуру CompletionRoutine перехвата пакета "на обратном пути", или не делая этого), то все сводится к тому, чтобы правильно манипулировать вызовами IoSetCompletionRoutine, IoCallDriver, IoGetCurrentIrpStackLocation, IoSkipCurrentIrpStackLocation

и IoCopyCurrentIrpStackLocationToNext.
// Откуда берется указатель pTargetDevice, см. выше в 9 главе pNewIrp = IoAllocateIrp ( pTargetDevice -&#62 StackSize + 1, FALSE );

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

IoSetNextIrpStackLocation( pNewIrp ); pUsefulArea = IoGetCurrentIrpStackLocation ( pNewIrp ); // Теперь можем использовать пространство ячейки стека вывода, // на которую указывает pUsefulArea, по собственному усмотрению.

// Устанавливаем разнообразные необходимые значения в ячейке стека, // соответствующей нижнему драйверу pNextIoStackLocation = IoGetNextIrpStackLocation ( pNewIrp ); pNextIoStackLocation -&#62 MajorFunction = IRP_MJ_XXX; . . . . . . . // Подключаем процедуру завершения IoSetCompletionRoutine( pNewIrp, OurIoCompletionRoutine, NULL, TRUE, TRUE, TRUE );

// Посылаем IRP пакет целевому драйверу: IoCallDriver (pTargetDevice, pNewIrp );

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