Драйверная модель wdm
В продолжение предыдущего раздела хотелось бы отметить, что в конечном итоге, компания Microsoft разработала новую технологию — WDM (Windows Driver Model) — и включила ее в Windows 98 и Windows Me, наследников Windows 95. Кроме того, новая технология была включена в Windows 2000 и Windows ХР, наследников Windows NT 4.0 [7]. WDM предоставляла разработчикам драйверов доступ к практически полному диапазону технологий и интерфейсов, доступных для мира ядра Windows.
Вот теперь мы уже заговорили о таком понятии, как «драйверная модель» напрямую. Возможно, кому-то будет интересно узнать, что же это такое. Так вот, драйверная модель предоставляет собой абстрактный интерфейс для поддержки устройств. Задача разработчиков драйверов как раз и состоит в реализации данного интерфейса, чтобы поддерживать конкретные требования аппаратного устройства. Выражаясь более простым языком, задачей драйвера является предоставление связи и контролирование взаимодействия между приложениями и устройством.
Я думаю, нет необходимости в подробностях объяснять, что представляет собой драйвер, как он «пишется», как выполняется под управлением операционной системы и проч., ибо это тема заслуживает отдельного доклада. Скажу лишь то, что драйвер представляет собой «контейнер» для функций, которые самостоятельно вызываются операционной системой в нужный момент (когда она сочтет нужным). Да-да, здесь нет ничего мистического. Другое дело, что драйверы обычно выполняются в доверенном пространстве ядра, хотя, в драйверной модели WDF драйверы UMDF выполняются в пользовательском режиме, но об этом мы еще поговорим.
Архитектура wdm
Ни для кого не секрет, что тема драйверов достаточно объемная и сложная, поэтому мы не будем уходить в «дебри» системного низкоуровнего программирования, а рассмотрим лишь в общих чертах особенности драйверных моделей. А для большей наглядности, рассмотрим архитектуру WDM на небольшом примере.
Допустим, программе пользовательского режима потребовалось прочитать данные с устройства. Она вызывает функцию API ReadFile, к примеру. Модуль подсистемы — такой как KERNEL32.DLL — реализует вызов, передавая его к низкоуровневой функции API NtReadFile [7]. Низкоуровневый API – довольно важная составляющая WDM да и архитектуры Windows в целом, но о нем чуть позже.
Такие функции, как, например, NtReadFile, является частью системного компонента, называемого менеджером ввода/вывода (I/O Manager). Следует иметь ввиду, что в самой системе не существует конкретного исполняемого модуля с таким именем. Тем не менее, нам потребуется какое-то название для обозначения «облака» функций операционной системы, окружающего драйвер.
В рассматриваемом примере с ReadFile функция NtReadFile создает IRP с основным кодом функции IRP_MJ_READ. Дальнейшее может различаться в деталях, но чаще всего такие функции, как NtReadFile, возвращают управление вызывающей стороне пользовательского режима [7].
А теперь немного поводу низкоуровнего API. Низкоуровневый API представляет интерфейс, который позволяет «клиентским» приложениям обращаться к программам (в данном контексте — драйверам) режима ядра. Дело в том, что каждая точка входа данного API является тонкой оберткой для функции режима ядра. Все они проверяют свои параметры, предотвращая возможные дефекты безопасности. Сам вызов использует платформенно-зависимый интерфейс для передачи управления через границу пользовательского режима/режима ядра (например, инструкции ассемблера и т.п.). Функции низкоуровнего API работают в режиме ядра и обслуживают запросы приложений на обращение к устройству. Как правило, они создают структуру данных, называемую пакетом запроса ввода-вывода, или IRP (I/O Request Packet), и передают ее в точке входа некоторому драйверу устройства.
Для выполнения запроса IRP драйверу, в конечном счете, потребуется обратиться к своему устройству. Драйверы, хотя они и работают в режиме ядра и могут напрямую общаться с оборудованием, используют средства прослойки HAL (Hardware Abstraction Layer) для обращения к оборудованию. Функции HAL используют платформенно-зависимую реализацию для выполнения операций. Закончив операцию ввода/вывода, драйвер завершает обработку IRP, вызывая соответствующую функцию режима ядра. Завершение является последним этапом обработки IRP и позволяет ожидающему приложению продолжить работу. Для большей наглядности рекомендую обратиться к рисунку 1.

Рисунок 1 – Принцип работы драйверов WDM
Вот, собственно, по такому принципу и работают драйверы WDM. Конечно, то, что мы рассмотрели, это, примерно, сотая часть архитектуры WDM и всего того, что с ней связано. Однако, в рамках статьи, надеюсь, вполне достаточно.
Итак, вот несколько ключевых особенностей драйверной модели WDM (по сравнению с предыдущей драйверной моделью – «драйвер виртуального устройства», применяемой в Windows 9x/ME)[2, 3, 5]:
Совместимость на уровне двоичных кодов между драйверами для Windows 98 и NТ;
Поддержка управления питанием (power management), что дает системе возможность осуществления энергосбережения путем выборочного отключения питания нескольких или всех устройств в системе;
Поддержка Plug and Play;
Поддержка «продвинутого» шинного управления (advanced bus management);
Единая обработка операций ввода/вывода посредством общей структуры данных — IRP (в Windows 98/Ме существовали серьезные различия в работе с дисками, коммуникационными портами, клавиатурами и т. д.).
Форум МИРа NVIDIA
Гость«Операционные системы Windows 98 SE и Windows 2000, равно как и их потомки, поддерживают новую модель драйверов, получившую название WDM (Windows Driver Model). Это – попытка реализовать полную поддержку Plug&Play и ACPI, то есть дать возможность загружать и выгружать драйверы «на ходу», без перезагрузки системы, подключать их в виде фильтров-расширений к стандартным драйверам Microsoft, более гибко управлять энергосбережением и конфигурацией устройств и т.п. WDM-драйверы хранятся в каталоге SYSTEM32/DRIVERS. В частности, интерфейсы нового поколения USB и IEEE-1394 (FireWire) работают только под управлением WDM-драйверов»
Простейший WDM-драйвер
В данной статье описан процесс написания простейшего драйвера, который выводит скан-коды нажатых клавиш.
Также в данной статье описан процесс настройки рабочего места для написания драйверов.
Если Вам интересно, прошу под кат.
Подготовка стенда
Установка необходимого ПО для написания простейшего драйвера
- Windows DDK (Driver Development Kit);
- VMware Workstation или Virtual Box;
- Windows XP;
- Visual Studio 2005;
- DDKWizard;
- KmdManager
- DebugView;
Настройка рабочего места
Установка DDK
Установка предельно проста. Единственное на что необходимо обратить внимание — это диалог, в котором Вам предлагается выбрать компоненты, которые будут установлены. Настоятельно рекомендую отметить всю документацию и примеры.
Установка и настройка Microsoft® Visual Studio 2005
Установка Microsoft® Visual Studio 2005 ничем не сложнее установки DDK. Если Вы будете использовать её только для написания драйверов, то когда инсталлятор спросит какие компоненты необходимо установить, выберите только Visual C++.
Далее можно установить Visual Assist X. С помощью этой программы (аддона) можно будет легко настроить подсказки для удобного написания драйверов.
После установки Visual Assist X в Visual Studio 2005 появится новое меню VAssistX. Далее в этом меню: Visual Assist X Options -> Projects -> C/C++ Directories -> Platform: Custom, Show Directories for: Stable include files . Нажимаем Ins или на иконку добавить новую директорию и в появившейся строке, если у вас Windows XP вписываем %WXPBASE%\inc\ddk\wxp .
Установка и настройка DDKWizard
- Создать системные (рекомендуется) или пользовательские переменные со следующими именами и значением, которое соответствует пути к DDK
Версия DDK Имя переменной Путь по умолчанию Windows XP DDK WXPBASE C:\WINDDK\2600 Windows 2003 Server DDK WNETBASE C:\WINDDK\3790.1830 Windows Vista/Windows 2008 Server WDK WLHBASE Windows 7/Windows 2008 Server R2 WDK W7BASE Например, если я использую Windows XP DDK, то я должен создать переменную WXPBASE со значением, которое соответствует пути к DDK. Так как я не изменял путь установки, то значение у меня будет C:\WINDDK\2600.
- Скопируйте скачанный скрипт ddkbuild.cmd, например, в папку с DDK. У меня это C:\WINDDK\.
- Добавьте в конец системной переменной Path путь к скрипту ddkbuild.cmd.
Установка необходимого ПО для запуска драйверов
- DebugView (link) — это утилитка, которая позволяет просматривать отладочный вывод как режима пользователя так и режима ядра.
- KmdManager (link) — утилита динамической загрузки/выгрузки драйверов
Постановка задачи
Задача: написать драйвер, который будет выводить в дебаг скан-коды нажатых клавиш и их комбинаций.
Немного теории
- драйверы классов;
- минидрайверы;
- функциональные драйверы;
- фильтрующие драйверы.
Необязательно определять все возожные функции в своем драйвере, но он обязательно должен содержать DriverEntry и AddDevice .
IRP — это структура, которая используется драйверами для обмена данными.
- верхние фильтрующие драйверы;
- нижние фильтрующие драйверы.
Отличия между верхними и нижними фильтрующими драйверами
Через верхние фильтрующие драйверы проходят все запросы, а это значит, что они могут изменять и/или фильтровать информацию, идущую к функциональному драйверу, ну и далее, возможно, к устройству.
Пример использования верхних фильтрующих драйверов:
Фильтр-хук драйвер, который устанавливает свою хук-функцию для системного драйвера IpFilterDirver, для отслеживания и фильтрации траффика. Такие драйверы используются в брандмауэрах.
Через нижние фильтрующие драйверы проходит меньше запросов потому что большинство запросов выполняет и завершает функциональный драйвер.
Проблемы синхронизации
В драйвере, который мы будем писать, есть несколько «проблемных» секций. Для нашего драйвера вполне достаточно использования ассемблерных вставок:
Префикс lock позволяет безопасно выполнить идущую за ним команду. Она блокирует остальные процессоры, пока выполняется команда.
Экшен
Для начала необходимо включить заголовочные файлы «ntddk.h», «ntddkbd.h»
Также необходимо описать структуру DEVICE_EXTENSION
Объект pLowerDO это объект устройства, который находится ниже нас в стеке. Он нужен нам для того чтобы знать кому дальше отправлять IRP-пакеты.
Еще для работы нашего драйвера нам нужна переменная, в которой будет храниться количество не завершенных запросов.
Начнем с функции, которая является главной точкой входа нашего драйвера.
theDriverObject – объект драйвера, содержит указатели на все необходимые операционной системе функции, которые мы должны будем инициализировать.
ustrRegistryPath – имя раздела в реестре, где хранится информация о данном драйвере.
Для начала необходимо объявить и обнулить переменные:
Далее, как я и писал выше, нужно инициализировать указатели на функции
Функция DispatchRead будет обрабатывать запросы на чтение. Она будет вызываться, когда нажата или отпущена клавиша клавиатуры.
Функция DriverUnload вызывается, когда драйвер уже не нужен и его можно выгрузить из памяти, или когда пользователь сам выгружает драйвер. В данной функции должна производиться «зачистка», т.е. освобождаться ресурсы, которые использовались драйвером, завершаться все незавершенные запросы и т.д.
Функция DispatchThru это функция-заглушка. Все что она делает это передача IRP-пакета следующему драйверу (драйверу который находится под нашим в стеке, т.е. pLowerDO из DEVICE_EXTENSION ).
Далее мы вызываем нашу функцию, для создания и установки нашего устройства в стек устройств:
Эту функцию я опишу чуть ниже.
Возвращаем status , в котором, если функция InstallFilter завершилась удачей, хранится значение STATUS_SUCCESS .
Переходим к функции InstallFilter . Вот её прототип:
Эта функция создает объект устройства, настраивает его и включает в стек устройств поверх \\Device\\KeyboardClass0
pKeyboardDevice – это объект устройсва, которое мы должны создать.
Вызываем IoCreateDevice для создания нового устройства
- Первый аргумент это объект драйвера, который мы получили как параметр функции InstallFilter. Он передается в IoCreateDevice для того чтобы установить связь между нашим драйвером и новым устройством.
- Третий параметр это имя устройства
- Четвертый параметр это тип устройства
- Пятый параметр это флаги, которые обычно устанавливаются для запоминающих устройств.
- Шестой параметр описывает можно ли открывать манипуляторы устройства в количестве больше одного. Если FALSE можно открыть только один манипулятор. Иначе можно открыть любое количество манипуляторов.
- Седьмой параметр это память, в которой будем сохранен созданный объект устройства.
Флаги, которые мы устанавливаем для нашего устройства, должны быть эквивалентными флагам устройства, поверх которого мы включаемся в стек.
Далее мы должны выполнить преобразования имени устройства, которое мы включаем в стек.
Функция IoAttachDevice внедряет наше устройство в стек. В pdx->pLowerDO будет храниться объект следующего (нижнего) устройства.
Далее разберем функцию DispatchRead с прототипом:
Данная функция будет вызываться операционной системой при нажатии или отпускании клавиши клавиатуры
Увеличиваем счетчик незавершенных запросов
Перед тем как передать запрос следующему драйверу мы должны настроить указатель стека для драйвера. IoCopyCurrentIrpStackLocationToNext копирует участок памяти, который принадлежит текущему драйверу, в область памяти следующего драйвера.
Когда запрос идет вниз по стеку в нем еще нет нужных нам данных, поэтому мы должны задать функцию, которая вызовется, когда запрос будет идти вверх по стеку с нужными нам данными.
где ReadCompletionRoutine наша функция.
Передаем IRP следующему драйверу:
Теперь разберем функцию, которая будет вызываться каждый раз при завершении IRP . Прототип:
Структура PKEYBOARD_INPUT_DATA используется для описания нажатой клавиши.
Проверяем, удачно завершен запрос или нет
Чтобы достать структуру KEYBOARD_INPUT_DATA нужно обратиться к системному буферу IRP -пакета.
Узнаем количество клавиш
И выводим каждую клавишу:
И не забываем уменьшать количество не обработанных запросов
Возвращаем статус запроса
Разберем функцию завершения работы. Прототип:
Извлекаем устройство из стека:
Проверяем есть незавершенные запросы или нет. Если мы выгрузим драйвер без этой проверки, при первом нажатии на клавишу после выгрузки будет БСоД.
Как запустить драйвер и просмотреть отладочную информацию
Для запуска драйвера я использовал утилиту KmdManager. Для просмотра отладочной информации использовалась утилита DbgView.
P. S. Статью писал давно, ещё на третьем курсе, сейчас уже почти ничего не помню. Но если есть вопросы, постараюсь ответить.
P. P. S. Прошу обратить внимание на комментарии, в частности на этот
Windows Driver Model
Windows Driver Model (WDM) – фреймворк для драйверов устройств (также известен как Win32 Driver Model и Windows NT Driver Model), был введен в Windows 98 и Windows 2000 для замены устаревшего VxD, который использовался в старых версиях Windows таких как Windows 95 и Windows 3.1.
Содержание
Обзор
Microsoft Windows Driver Model определяет унифицированную модель драйвера для операционных систем Windows 98, Windows 2000 и старше, стандартизируя требования и уменьшая количество кода, необходимое для написания драйвера. В соответствии с концепцией WDM, драйверы могут быть бинарно совместимы. Так, например, драйвер для платформы x86, написанный для Windows 98 может подойти и к Windows Me, и к Windows 2000, и даже к Windows Vista. WDM-драйвера спроектированы для прямой совместимости, поэтому такой драйвер может быть запущен на более поздней версии Windows, чем та, для которой он был изначально написан. Но это также означает, что драйвер не сможет использовать новые возможности новой версии WDM-фреймворка. WDM-драйвера, главным образом, обратно не совместимы. Это означает, что нет никаких гарантий, что такой драйвер запустится на версии Windows, более старой чем та, для которой он был написан. Например, Windows XP может использовать драйвер, написанный для Windows 2000, но этот драйвер не может использовать новые возможности, добавленные в Windows XP. Однако, драйвер, написанный для Windows XP, может работать в Windows 2000, а может и не работать.
WDM-драйверы взаимодействуют друг с другом через пакеты запроса ввода — вывода (IRPs).
Технология WDM была разработана для увеличения функциональности и облегчения написания драйверов для Windows. Хотя WDM в основном был разработан для бинарной совместимости и совместимости на уровне исходного кода между Windows 98 и Windows 2000, зачастую это не всегда ожидаемо и поэтому специфические драйвера разрабатываются для каждой операционной системы отдельно.
WDM драйвера предназначены в общем для расширения стандартных возможностеи основного драйвера.
VxD, WDM и Windows 98
Операционные системы, основанные на Windows 98 (Windows 98, Windows 98 Second Edition и Windows Me), могут использовать оба стандарта драйверов — WDM и VxD. Обе модели драйверов могут предоставлять уникальные и различные возможности для одного и того же устройства. Однако, обычно более новый стандарт WDM предоставляет больше возможностей. Например, если TV-тюнер использует VxD-драйвер, это позволяет захватывать изображение разрешением 384 x 288 пикселей. Тот же TV-тюнер с драйвером WDM может захватывать изображение разрешением 768 x 576 пикселей.
Критика
Windows Driver Model, даже несмотря на значительные улучшения по сравнению с предшествующими ему VxD и Windows NT driver model, критикуется разработчиками драйверов [1], в основном по следующим причинам:
- WDM слишком сложен для изучения.
- Сложное взаимодействие с событиями управления питанием и Plug and Play. Это приводит ко множеству ситуаций, когда компьютеры, управляемые Windows, не могут перейти в спящий режим или правильно выйти из него из-за ошибок в коде драйвера.
- Отмену ввода/вывода практически невозможно правильно определить.
- Для каждого драйвера требуются тысячи строк сопровождающего кода.
- Нет поддержки для написания «чистых» драйверов пользовательского режима.
Было также много проблем из-за качества документации и примеров, предоставляемых Microsoft.
Из-за этих проблем Microsoft выпустила новый фреймворк, заменяющий WDM, названный Windows Driver Foundation, который включает в себя Kernel-Mode Driver Framework (KMDF) и User-Mode Driver Framework (UMDF). Windows Vista поддерживает оба стандарта: и WDM, и новый Windows Driver Foundation. KMDF также доступен для скачивания для Windows XP и даже Windows 2000, в то время, как UMDF доступен начиная только с Windows XP.