Давайте на время абстрагируемся от концепции драйвера и начнем с общей теории. Для того, чтобы понять что же представляет собой драйвер в системе, сначала необходимо пройти минимум теории по общей архитектуре x86-64. Почему x86, да потому что именно эта платформа: а) выбрана мной для экспериментов, б) является наиболее распространенной в клиентском сегменте операционных систем Windows. Озвученные в данном разделе особенности дадут нам понимание многих аспектов работы как непосредственно операционной системы, так и, соответственно, драйверов в её составе.
Внутренняя структура любой операционной системы базируется на аппаратных особенностях платформы, на которой она работает. Центральным звеном является процессор, у процессоров архитектуры x86-64 имеются несколько режимов работы:
На заре эры развития персональных компьютеров архитектуры x86,
процессор работал в реальном режиме. Тем не менее, реальным режим
постепенно ушел в прошлое, поскольку имел ряд особенностей, делающих
невозможным дальнейшее развитие технологий: 16-битную шину данных и
20-битную шину адреса (ограничение по адресации), сегментную адресацию с
размерами сегментов в 64 килобайта (неудобство использования адресного
пространства), отсутствие разграничений доступа к адресному
пространству. С целью снятия существовавших ограничений был разработан
защищенный режим, который предоставлял ряд важных для развития
операционных систем особенностей: "многозадачность", механизм защиты
(доступ к привилегированным командам), обеспечивающий контроль доступа
различных участков кода (программ) друг к другу, модель виртуальной
памяти. В защищенном режиме процессоров Intel архитектуры x86
реализованы так называемые кольца защиты или уровни привилегий. Всего их
четыре: 0 (наиболее привилегированный), 1, 2 и 3 (наименее
привилегированный). Уровни привилегий призваны защитить код режима ядра
от пользовательских программ и пользовательских программ друг от друга,
поскольку это может привести к нарушению работоспособности. Однако
операционная система Windows не использует все перечисленные уровни, в
ней задействованы лишь два из них: 0-й и 3-й.
Для наглядности понимания этого приведем упрощенную схему взаимодействия компонентов Windows:

Как вы видите, внутренняя среда операционной системы Windows разделена на две части и поддерживает два режима выполнения:
Это стоит понять, осознать и запомнить раз и навсегда, поскольку,
собственно, это одна из базовых, основных концепций очень многих
современных операционных систем.
Режимы пользователя и режим ядра обладают следующим различиями:
В пользовательском режиме выполняются следующие процессы:
| Подсистема | Описание |
|---|---|
| Процессы обеспечения работоспособности системы (System Support Processes) |
|
| Процессы служб/сервисов (Service Processes) |
|
| Приложения (Applications) |
|
| Подсистемы окружения (Environment Subsystems) |
|
| Интерфейс к функциям ядра |
|
В режиме ядра выполняются:
| Подсистема | Описание |
|---|---|
| Исполнительная система (Executive) |
|
| Ядро (Kernel) | инициализация критических для системы драйверов этапа загрузки, межпроцессорная синхронизация, планирование и диспетчеризация процессов/потоков/прерываний, обработка/диспетчеризация исключений/ошибок и некоторые другие функции (ntoskrnl.exe, ntkrnlmp.exe, ntkrnlpa.exe, ntkrpamp.exe). |
| Драйверы устройств (Device Drivers) | драйверы физических/логических/виртуальных устройств: драйверы файловых систем, сети, дисков и прч. |
| Оконная/графическая подсистема (Windowing And Graphics System) | Подсистема поддержки окон и графики, обеспечивающая поддержку функций графического пользовательского интерфейса (Graphic User Interface, GUI). (win32k.sys) |
| Уровень абстрагирования от оборудования (Hardware Abstraction Layer, HAL) | обеспечивает независимость от аппаратной части платформы, изолирует компоненты ядра от специфики аппаратного обеспечения. (hal.dll) |
Поскольку любая операционная система попросту обязана уметь работать с аппаратными средствами, в дистрибутиве (комплекте установки/системных файлах) присутствуют драйверы ключевых компонентов аппаратного обеспечения, без которых система буквально лишится доступа к аппаратной части со всеми вытекающими из этого проблемами: не сможет функционировать или вовсе не пройдет процедуру собственной установки. Представлены эти "внутренние" драйверы в виде так называемой встроенной библиотеки драйверов, видоизменяющейся по составу от версии к версии, в зависимости от этапов эволюционирования аппаратного обеспечения и рыночных тенденций. Драйвера из состава данной библиотеки, при необходимости, устанавливаются на этапе инсталляции операционной системы в зависимости от обнаружения (идентификации) в компьютере тех или иных устройств. В общем случае, во время инсталляции, код модуля обнаружения оборудования выполняет определение установленных в компьютере устройств и проверяет в своей библиотеке наличие сопоставимых драйверов. Для тех устройств, для которых присутствуют системные драйвера, производится установка в автоматическом (фоновом) режиме. Тем самым "на выходе", после инсталляции операционной системы, мы можем получить минимально-необходимый для функционирования набор системных драйверов, который позволяет организовать работоспособную начальную рабочую среду. Но стоит помнить, что ограничиваться встроенными в дистрибутив драйверами не стоит, поскольку для полноценного функционирования большинства устройств могут потребоваться драйвера, предоставляемые производителем устройства.
Среди ключевых внутренних механизмов, определяющих функционирование
операционной системы Windows, имеется достаточно важная для понимания
принципов работы драйверов тема, обойти которую стороной вряд ли
получится. Механизм этот носит название уровня запросов прерываний
(Interrupt Request Level, IRQ Level, IRQL) и достаточно сложен для
понимания, поэтому углубленное его изучение выходит далеко за рамки
излагаемого материала, однако в данной статье мы предпримем попытку
краткого изложения (ну а в будущем выделим под него отдельную статью).
Откровенно говоря, сам я до сих пор путаюсь в концепции IRQL, поэтому
буду излагать собственное понимание планомерно, шаг за шагом, с опорой
на знания, полученные на каждом из этапов.
Исторически сложилось так, что термин прерывание всегда ассоциировался у
меня с реальным режимом работы процессора, перенося во времена
операционной системы MSDOS, когда все было достаточно просто:
существовал набор из 256 прерываний, доступных через таблицу векторов
прерываний. Часть этих прерываний были аппаратными, соответственно
генерировались самостоятельно по каким-либо внешним аппаратурным
событиям, другие же являлись программными, то есть могли вызываться из
кода приложений. Записи в таблице прерываний могли переопределяться,
потому как вектор обработчика прерывания был доступен для изменения по
своему усмотрению на адрес собственной процедуры обработки. Таких
понятий как уровень запросов прерываний не существовало, все было просто
и понятно. Однако, прогресс на месте не стоял и с эволюцией процессоров
и операционных систем появился сначала защищенный режим, а затем
Microsoft выпустила очередную версию своих операционных систем, которая
получила название Windows, и вот с этого самого момента все начало
стремительно усложняться.
Буквально внезапно, в первых же версиях Windows 95/NT, появилась
какая-то таблица (состоящая из 32 уровней запросов прерываний), уровни
которой градируются от самого низкого 0 (passive) до самого высокого 31
(high):
| Имя | Класс | Назначение | Уровень Intel x86-64 |
|---|---|---|---|
| HIGH | Аппаратный | Наивысший уровень. Немаскируемое прерывание и другие типы. | 31 |
| POWER | Аппаратный | События сбоя питания | 30 |
| IPI | Аппаратный | Межпроцессорный сигнал. Сигналы межпроцессорного взаимодействия. | 29 |
| CLOCK | Аппаратный | Такт системного таймера | 28 |
| PROFILE | Аппаратный | Контроль производительности. Таймер профилирования ядра (механизм измерения производительности системы). | 27 |
| DEVICE | Аппаратный | DIRQL (Devices IRQL). Аппаратные прерывания устройств. | 3-26 |
| DISPATCH | Программный | Операции планировщика/отложенные вызовы процедур (DPC). | 2 |
| APC | Программный | Асинхронные вызовы процедур. | 1 |
| PASSIVE | Программный | Пассивный уровень. Нет прерываний. Обычный уровень выполнения кода режима пользователя | 0 |
Как можно заметить, в приведенной таблице присутствует очень интересная особенностью: вместе сведены и программные и аппаратные уровни (0-2 это программные уровни, а с 3-31 это аппаратные).
Из этого утверждения следует, что модель собственная, программная, и уровни в ней не привязаны к какой-либо спецификации оборудования, это позволяет системе собрать в единую иерархию приоритетов аппаратные и не аппаратные типы прерываний. Низшие (не аппаратные/программные) уровни IRQL (PASSIVE, APC, DPC/DISPATCH) используются для синхронизации программных подсистем операционной системы: запуска операций планирования, таких как переключение потоков или обработка завершения ввода/вывода. Давайте рассмотрим их подробно:
WaitForSingleObjectEx, WaitForMultipleObjects
и других) в коде не приводит к мгновенному выполнению функции, вместо
этого поток (в контексте которого функция выполняется) переходит в
специальный статус и генерируется программное прерывание APC, вызов
функции ставится во внутреннюю очередь. В следующий раз, когда подошло
время выполняться этому потоку, запланированная APC-функция выполняется
на уровне APC. Потоки, работающие на уровне APC, соответственно не
получают запросы своего же уровня АРС, которые система использует для
операций завершения ввода/вывода.Если драйвер понимает, что требуется выполнение дополнительной работы, которая занимает существенное процессорное время, то он запрашивает DPC и перекладывает на него эту задачу. Когда уровень IRQL опускается до DISPATCH, происходит обратный вызов отложенной функции драйвера, которая и выполняет оставшуюся часть обработки. Реализуя подобный алгоритм на уровне IRQL DISPATCH, драйвер проводит на уровне DIRQL меньшее количество времени, и соответственно, уменьшает время задержки на обработку собственного прерывания, тем самым освобождая его для других устройств системы.
Хорошо, но я так и не понял, почему нельзя было отказаться от всех
этих уровней и сделать "плоскую" модель очередей, либо выполнять все эти
типы задач по мере поступления? Давайте смоделируем рабочую ситуацию:
представим какой-либо код, например небольшую программу, написанную "на
коленке". Вот мы запустили её на выполнение, соответственно в системе
сформировался процесс для нашей программы, в контексте которого начал
выполняться основной поток. Типовой поток (режима пользователя или
режима ядра) исполняется на самом низшем уровне IRQL PASSIVE. На
протяжении всего времени выполнения потока, часы (микросхема таймера)
периодически генерирует собственные прерывания для отсчета временных
интервалов, которые используются для указания операционной системе о
прохождении заданного промежутка времени. Процедура обработки прерывания
часов выполняется на уровне IRQL CLOCK, который (если посмотреть в
таблицу) выше по приоритету большинства уровней: и уровня DISPATCH, на
котором выполняется планировщик, и уровня PASSIVE, на котором
выполняется наша программа. Таким образом таймер постоянно вытесняет
работу и планировщика и нашей программы. С каждым переданным тиком
таймера, процедура обработки прерывания таймера уменьшает остающийся у
выполняющегося в данный момент нашего пользовательского потока квант
времени. В момент, когда квант времени выполняющегося потока уменьшается
до нуля, программа обработки прерывания часов генерирует прерывание
уровня DISPATCH, тем самым вызывая запуск планировщика для выбора им
следующего потока для выполнения. По факту генерирования прерывания
уровня DISPATCH, процедура обработки прерывания таймера заканчивает
исполнение своего кода и управление возвращается ядру системы. Ядро
находит в очереди запросов следующее прерывание с наиболее приоритетным
уровнем, находящееся в режиме ожидания. Каждое прерывание обслуживается
по очереди. Когда все прерывания выше уровня DISPATCH обслужены, то
выполняется процедура обработки прерывания уровня DISPATCH. Эта
программа обработки прерывания обрабатывает список DPC и затем вызывает
планировщик. Планировщик обнаруживает, что квант времени текущего потока
исчерпан, то есть уменьшен до нуля, после чего Планировщик выполняет
алгоритм планирования для выбора следующего потока на выполнение. Код
поставленного на выполнение потока будет выполнен когда система
опустится на уровень IRQL PASSIVE.
Таким вот образом реализуется приоритеты, и, соответственно, вытесняющая
многозадачность. Теперь представьте, что вы уберете из системы иерархию
уровней запросов прерываний, как в этом случае будет вести себя
система? В этой ситуации было бы непонятно что и когда выполнять,
система выполняла бы все поступающие задачи в порядке очереди, что
привело бы к тому, что потоки запросто могли бы вытеснить планировщик и
тем самым вообще разрушить или полностью вывести из стоя вытесняющую
многозадачность, что повлекло бы за собой непредсказуемую работу ОС.
Таким образом:
соответственно:
Назначение уровней IRQL в системе следующие:
Тем самым, на глобальном уровне механизм IRQL позволяет подпрограмме операционной системы:
Ну хорошо, а как это воздействует на драйвера? Мы знаем, что драйвера могут быть пользовательского режима и режима ядра, соответственно, выполняются пользовательском режиме и в режиме ядра. Отсюда следует, что:
И отсюда следует два достаточно важных вывода:
Представьте ситуацию, когда код драйвера выполняется на низком уровне IRQL, модифицирует какой-либо объект (например, файл file.txt), затем другой код на более высоком уровне IRQL внезапно прерывает его выполнение и модифицирует тот же файл file.txt другими данными. Когда управление вернется к нашему драйверу, он продолжит модификацию файла своими данным, тем самым затерев данные, поступившие от другого источника. Таким образом файл войдет в рассогласованное состояние. Для решения подобных проблем были введены различные системные объекты синхронизации. Для того, что бы код уровня ядра мог модифицировать определенные типы данных, объекты взаимного исключения, он должен сперва получить владение блокировками.
Соответственно выводы, следующие из этого утверждения, очевидны: для
взаимодействия системы с устройствами требуются отдельные интерфейсы,
возможно даже сложная совокупность нескольких интерфейсов. Концепция
драйвера была разработана для решения задачи сопряжения и используется в
моделях большинства современных систем, она основана на работе в
адресном пространстве ядра специального кода, который обеспечивает
взаимодействие ядра системы с любым типом логических/физических
устройств.
Учитывая общую ориентированность ресурса, в статье мы будем освещать
специфику исключительно драйверов операционной системы Windows. Итак,
для драйвера Windows, как, в общем то, драйверов других операционных систем, верны следующие утверждения:
то же, но другими словами:
Одно из приведенных определений отмечает немаловажную особенность драйвера: ошибочно представлять драйвер исключительно во взаимодействии с физическим устройством, поскольку драйвер не обязательно должен предоставлять доступ к функциям какого-либо оборудования, он может обеспечивать и исключительно программные функциональные особенности. Примерами подобных решений являются драйвера, устанавливаемые в систему антивирусами, системами шифрования данных, системами мониторинга. Общий алгоритм работы любого драйвера следующий: приложения посредством функций специального пользовательского интерфейса (в Windows это API Win32) или запросов ввода-вывода опосредовано/напрямую обращаются к функциям драйвера некоего устройства. Драйвер, в свою очередь, предоставляет доступ к функциональным особенностям интересующего устройства, а так же контролирует процесс взаимодействия между запросами приложений и непосредственно устройством. Естественно, что в драйвере должны быть определены (описаны) все принципы взаимодействия с обслуживаемым (подчиненным, собственным) устройством, должен присутствовать набор данных об управляемом объекте, инструкции (набор команд), с помощью которых системный/пользовательский код может корректно инициализировать устройство и начать с ним взаимодействие.
Очень интересно было бы увидеть, на какой именно стадии загрузки операционной системы начинает загружается и начинает выполняться первый драйвер Windows? Однако в детальном изложении процесс этот достаточно нетривиален и для глубокого понимания требует реверсинга кода многих компонентов загрузки, в дополнение ко всему необходимо учитывать множество сопутствующих моментов, как то: последовательность загрузки, обусловленную зависимостью между драйверами, по причине которой драйвера могут группироваться в так называемые "группы загрузки", сама загрузка драйверов может разделяться на несколько этапов и прочее. При этом, следует учесть, что в Сети имеется большое количество материалов относительно устаревших уже операционных систем, поэтому мы попытаемся актуализировать процесс загрузки драйверов Windows на примере (наиболее близкой мне по духу) операционной системы Windows 7. И для начала не мешало бы рассказать об основных компонентах ядра Windows, активно участвующих в процессе загрузки драйверов:
Эти два менеджера, то есть менеджер ввода-вывода и PnP менеджер, активно взаимодействуют между собой.
Теперь мы опишем процесс загрузки операционной системы, однако сделаем
это не в привычной нам форме, а кратко отметим ключевые моменты,
касающиеся работы описанных компонентов операционной системы с
драйверами:
SERVICE_BOOT_START. Драйвера на данном этапе начинают загружаться в зависимости от групп, к которым они принадлежат.SERVICE_BOOT_START), вызывая процедуру DriverEntry каждого драйвера. На данном этапе загружаются и зависимые драйвера.SERVICE_DEMAND_START.SERVICE_SYSTEM_START.SERVICE_AUTO_START), а также все драйвера, от которых они зависят.Из всего этого алгоритма загрузки драйверов нам необходимо уяснить следующие основные правила: драйвер может быть загружен (в зависимости от стадии/класса драйвера) при помощи PnP-менеджера, либо с помощью SCM, а вот в процессе функционирования драйвера активно принимает участие Менеджер ввода-вывода.
На что может быть похож драйвер по структуре? Неужели это какой-то особый класс программ, устроенных сугубо специфически? Я тоже так когда-то по наивности полагал, но если подумать, то с какой стати разработчикам операционной системы усложнять себе жизнь и изобретать новый специализированный формат образа исполняемого файла для каких-то ядерных компонентов? Намного проще адаптировать старый, давно уже отлаженный и сто раз проверенный.
IMAGE_NT_HEADERS, подструктура OptionalHeader) значение поля Subsystem = 1 (IMAGE_SUBSYSTEM_NATIVE).Тип подсистемы может быть задан при сборке исполняемого модуля. Сама по себе нативная подсистема характерна для приложений, которые функционируют по иным, отличным от классических, правилам: на стадии подготовки образа к исполнению им не требуется инициализация подсистемы Win32. В числе прочих подсистема native используется для кода режима ядра, коим и являются практически все драйвера.
Сделаем небольшое отступление и поговорим о таком понятии как объект. Дело в том, что весь процесс функционирования драйвера Windows, как и любых других модулей операционной системы, зависит от разнообразных системных структур данных. Эти структуры управляются ядром и могут содержать в себе потоки, события, запросы ввода-вывода, устройства и прочие сущности.
Поэтому (с большим натягом) все внутренние структуры операционной системы Windows называют объектами.
Теперь вернемся к процедурам драйвера, на деле так называемые
"процедуры" драйвера являются COM-объектами обратного вызова, которые
обрабатывают поступающие от соответствующих объектов инфраструктуры
операционной системы события, говорится что драйвер предоставляет ядру
операционной системы COM-интерфейс, заданный серией процедур,
реализуемых драйвером. Экспорт, то есть публикация (объявление) процедур
драйвера для дальнейшего обращения к ним извне, выполняется путем
регистрации в основной процедуре драйвера (стандартной для всех
драйверов), носящей название DriverEntry.
DriverEntry
состоит в том, чтобы разработчик драйвера реализовал в ней заполнение
объекта (записей структуры) драйвера указателями на различные внутренние
процедуры драйвера, обеспечивающие тот или иной функционал. В процедуре
DriverEntry можно задавать (менять) имя объекта
устройства, которое впоследствии используется приложениями для открытия
дескриптора устройства и отправки пакетов запросов ввода-вывода (IRP).Функция DriverEntry фактически является функцией
глобальной инициализации и выполняется единожды на этапе загрузки
драйвера. Эта функция может быть как предельно простой, так и содержать
расширенный функционал (дополнительные подпрограммы), такой, например,
как создание дополнительных объектов устройств, опрос устройства,
дополнительные фазы конфигурации и инициализации устройств(а).
После публикации собственных функций драйвер становится "видимым" ядром
операционной системы. Дабы не усложнять и без того достаточно непростую
теорию, будем считать, что с точки зрения ядра Windows любое устройство
является неким абстрактным "виртуальным устройством", оперирующим
стандартизированным набором команд, и доступным через внутренние
интерфейсы. Как было уже сказано выше, в ядре операционной системы
Windows присутствует специальный модуль исполнительной системы,
называемый диспетчером (менеджером) ввода-вывода,
обеспечивающий единый интерфейс взаимодействия для всех драйверов
режима ядра, включая драйверы физических устройств, драйверы логических
устройств и драйверы файловых систем. Соответственно, система
ввода-вывода ядра управляет драйверами, или можно сказать, что драйверы
используют интерфейс диспетчера ввода-вывода для обеспечения
функционирования в операционной системе. С дургой стороны, драйвер
обеспечивает преобразование (конвертацию) "стандартных команд",
поступающих от операционной системы, в команды, которые "понимает"
подконтрольное ему устройство (если оно имеется), и наоборот. Менеджер
ввода-вывода определяет набор (множество) стандартных процедур, которые
могут быть реализованы в драйвере, поскольку:
Для более глубокого понимания того, какие функциональные особенности
должен обеспечивать драйвер, давайте приведем общую схему ключевых
процедур драйвера:

Собственно, глядя на приведенную схему, становится понятно, какие именно виды взаимодействия, а именно группы процедур должен реализовывать абстрактный драйвер Windows. Давайте теперь перечислим некоторые из этих процедур:
DriverEntry),
которая предназначается для проведения действий по начальной настройке
объекта драйвера, регистрации всех иных процедур драйвера,
конфигурированию подчиненного устройства и выполнении иных действия в
интересах разработчика.Становится очевидным, что в процессе разработки драйвера Windows не
стоит задачи реализовать весь набор описанных выше процедур, каждый
драйвер уникален и разработчик волен обеспечивать собственный набор
реализаций, поддерживаемых драйвером. Когда драйвер при помощи
PnP-менеджера или SCM загружается в систему, диспетчер ввода-вывода
создает в пространстве имен объект "драйвер" (driver object) и вызывает
процедуру инициализации драйвера (обычно это DriverEntry), которая выполняет дальнейшие действия по инициализации.
Объект драйвера представляет код и данные драйвера в ядре: помимо прочего, через этот объект драйвер экспортирует точки входа своих процедур. Процедура инициализации драйвера записывает в атрибуты данного объекта точки входа всех экспортируемых процедур драйвера. После загрузки драйвер может создавать объекты "устройство" для представления устройств или даже для формирования интерфейса драйвера. Большинство драйверов создают объекты "устройство" следующим образом:
При создании объекта типа "устройство" (device), драйверу требуется
присвоить данному объекту имя. Затем этот вновь созданный объект
помещается в пространство имен диспетчера объектов
(Object Manager), который, как и диспетчер (менеджер) ввода-вывода,
является частью исполнительной подсистемы ядра. Менеджер объектов
предназначается для ведения базы всех ресурсов операционной системы,
представленных в качестве объектов. Имя объекта может определяться самим
драйвером в явном виде, либо генерироваться автоматически менеджером
ввода-вывода. По соглашению, объекты "устройство" должны размещаться в
каталоге \Device пространства имен менеджера объектов,
недоступном приложениям через Win32 API. А для того, чтобы объект
"устройство" стал доступным для приложений, драйвер должен создать в
каталоге \GLOBAL?? символьную ссылку на имя этого объекта в каталоге \Device.
Драйверы, не поддерживающие технологию Plug-and-Play, и драйверы
файловой системы обычно создают символьную ссылку с общеизвестным именем
(скажем, \Device\VMwareKbdFilter). Только после всех
перечисленных действий драйвер становится "виден" в системе и доступен
для вызова пользовательскими приложениями.
Каким же образом пользовательская программа может взаимодействовать с драйвером в системе? На этот случай имеется два способа:
Ну с первым случаем все достаточно просто, в прикладной программе вызывается какая-либо ординарная функция Win32 API (например, CreateFile),
которая, затем, в зависимости от целевого объекта (файла, каталога)
может вызвать в цепочке своих вызовов функцию обмена с драйвером.
Фактически, в этом случае код приложения не ставит своей задачей
взаимодействовать с каким-либо драйвером, просто по цепочке вызовов
процедур, на определенном этапе выполнение уходит в режим ядра и там
происходит вызов функции драйвера. Все это остается сокрытым от
разработчика, однако возможно отследить взаимодействие при помощи
отладочных средств.
Второй случай более интересен, он возникает когда под вызовом драйвера
подразумевается не косвенный вызов (посредством вызова типовой функции),
а передача при помощи специальной функции (например, DeviceIoControl)
так называемого запроса ввода/вывода (I/O control request), который, в
дальнейшем, инициирует формирование блока данных под названием пакета
запроса ввода-вывода.
Формально IRP это пакет, но фактически это объект ядра, то есть
структура (блок) данных с набором процедур для менеджера ввода-вывода,
обеспечивающая обмен данными между программой и драйвером, либо между
драйвером и драйвером. Как мы уже упоминали, архитектура Windows
построена таким образом, что в ней запрещено прямое взаимодействие
программы режима пользователя и драйвера, поэтому подобный обмен
сводится к посылке программой кода IOCTL, который уже приводит к
формированию менеджером ввода-вывода IRP пакета запроса. Именно менеджер
ввода-вывода, как ответственный за взаимодействие с драйверами,
оперирует пакетами IRP. Менеджер ввода-вывода получается запрос на
ввод-вывод от пользовательской программы, затем формирует IRP и передает
его соответствующему драйверу.
Пакет IRP состоит из двух частей:
В постоянной части IRP содержит старший и (не всегда) младший код функции. Старшие коды: IRP_MJ_CREATE, IRP_MJ_CLOSE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_CLEANUP, IRP_MJ_DEVICE_CONTROL, IRP_MJ_INTERNAL_DEVICE_CONTROL, IRP_MJ_SCSI, IRP_MJ_SYSTEM_CONTROL, IRP_MJ_POWER, IRP_MJ_PNP, IRP_MJ_SHUTDOWN. Пакет так же содержит стек размещения ввода-вывода - специальную структуру IO_STACK_LOCATION,
содержащую определенные параметры: это набор устройств, которые
обработают данный IRP пакет. Причем по стеку этот пакет передается
последовательно от устройства к устройству. Более чем одно размещение
стека говорит о том, что IRP может быть обработан несколькими
драйверами. "Ячейки стека" IRP и предназначены для хранения "переменной"
информации при хождении пакета IRP по стеку драйверов. Пакет IRP
проходит по опубликованным процедурам каждого драйвера, каждая из
которых извлекает из "своей" ячейки стека размещения ввода-вывода
необходимую ей информацию. Процедуры драйвера традиционно называются
"процедуры обратного вызова" (callback). Как мы уже упоминали, функция
инициализации драйвера DriverEtnry сообщает ядру
(публикует) имена этих процедур и позже ядро само вызывает ту или иную
процедуру при определенных обстоятельствах.
В отличие от штатной программы, драйвер не является классическим
процессом со своим адресным пространством и не имеет потока исполнения.
Вместо этого, функция драйвера выполняется в контексте того потока и
процесса, в котором она была вызвана. Контекст (пространство выполнения
кода) драйвера зависит от того, кто производит обращение (вызывает) к
драйверу. Обращение может быть инициировано:
Опять же, в отличии от штатной программы, драйвер не может вызывать
стандартные функции Win32 API, может лишь оперировать доступными в ядре
функциями, которые начинаются с префиксов Ex.., Hal.., Io.., Ke.., Ks.., Mm.., Ob.., Po.., Ps.., Rtl.., Se.., Zw.. и некоторых других.
В процессе эволюционирования и, соответственно, усложнения драйверной концепции, драйверы начали подразделяться на категории (или типы) в зависимости от назначения. Вот основные из них:
По уровню компонетизации драйверы бывают:
PnP драйвера под Windows подразделяются на:
По режиму выполнения драйверы Windows градируются:
На протяжении всего времени существования операционной системы, разработчики пытались стандартизировать и упростить разработку драйверов. В следствии чего появились модели.
Когда-то очень давно существовало две основных направления развития драйверной концепции Windows:
Однако, начиная с версии Windows 98/NT4.0 разработчики предприняли попытку унифицировать (универсализировать) разработку драйверов, в следствии чего на смену упомянутым моделям пришла новая модель WDM.
Модель WDM являлся этапом переопределения классического стека
драйвера Windows с целью обеспечения поддержки являющихся в то время
революционными технологий Plug-and-Play и ACPI. Модель дает возможность
загружать/выгружать драйверы "на лету", без необходимости в перезагрузке
операционной системы, разрабатывать драйвера в виде расширений
(фильтров) к стандартным системным драйверам, более гибко управлять
энергосбережением и конфигурацией устройств и прочее.
В рамках модели WDM любое аппаратного устройство поддерживается, как минимум, двумя драйверами:
На протяжении всего времени развития, модель WDM претерпевала множество изменений, существенно разрастаясь. Начиная с Windows Vista была предпринята очередная попытка развития концепции драйвера Windows, в сущности уже существовавшей на тот момент модели WDM, результатом чего явилось новой модели (надстройки над WDM) под названием WDF.
Связано это было с тем неоспоримым фактом, что разработчикам не
удалось достичь достаточного уровня абстракции модели WDM, а именно
недостаточной интеграцией подсистемы ввода-вывода с технологией
Plug-and-Play и управлением питанием. Это приводило к тому, что на
разработчике драйвера лежала громадная нагрузка по синхронизации этих
самых запросов ввода-вывода с событиями Plug-and-Play и запросами
энергопотребления. Очевидно, требовалось дальнейшее упрощение драйверной
модели. WDF пришла на смену WDM и считается наиболее современной
моделью.
WDF реализует следующие возможности:
Модель WDF подразделяется на два направления:
Подразделение сред по режимам пользователя и ядра в модели WDF достаточно условное, поскольку основное предназначение данного разграничения заключается в классификации разработки драйверов для тех или иных классов устройств.