Как мы знаем, на аппаратном уровне современный компьютер состоит из функциональных узлов, представляющих собой те или иные электронные компоненты. Широкому кругу пользователей персональных компьютеров знакомы такие функциональные блоки как: процессор, память, видеокарта, звуковая карта, жесткий диск, контроллер ввода-вывода (обеспечивающий работу клавиатуры, мыши, джойстика, USB-носителей (флешек)), принтер, сканер и некоторые другие. На физическом уровне данные устройства взаимодействуют между собой посредством специальных шин и протоколов, создавая совокупностью своего взаимодействия симбиоз операций, который, в общем случае, характеризует функционирование компьютера. Но разве компьютер представляет собой только лишь набор электронных компонентов? Конечно же нет, ведь один из основных аппаратных модулей, центральный процессор, спроектирован для выполнения машинных инструкций, из последовательностей которых, как мы знаем, состоят программы, в свете этого было бы уместно упомянуть еще об одном уровне - программном.
Теперь давайте вернемся в не столько далекое прошлое; на заре компьютерной эры код программ (которые часто писались непосредственно в машинных кодах/на низкоуровневых языках) мог легко взаимодействовать с аппаратурой напрямую, поскольку аппаратная архитектура была относительно простой. Однако со временем технологии развивались, аппаратный и программный уровни эволюционировали взаимосвязано, и первый пришел к появлению большого многообразия устройств, а второй к появлению огромного разнообразия программных модулей, обусловивших, в дальнейшем, появление операционных систем. Операционная система явилась ключевой вехой в истории развития компьютерной индустрии, поскольку именно она, среди прочего, выполняла роль связующего звена, своеобразного координатора (диспетчера), обеспечившего взаимодействие между устройствами и программами: принимала запросы от программного слоя (например, пользовательских программ) на обмен данными с тем или иным устройством и наоборот, то есть фактически выполняла роль сопряжения между аппаратной и программной частями (сама являясь программной частью).
Операционные системы, в свою очередь, тоже не стояли на месте, и если по началу взаимодействие операционной системы с аппаратурой компьютера было относительно простым, то по мере усложнения архитектуры и введения новых аппаратных возможностей, усложнялась и структура операционной системы. На протяжении всего времени развития операционных систем, разработчики пытались создать код, обеспечивающий полноценное взаимодействие с максимально возможным количеством имеющимся на рынке аппаратных устройств. Тем не менее, подобный подход, по мере усложнения архитектуры персональных компьютеров x86, привел к появлению концепции обособленного программного слоя, носящего название драйвер, ответственного за взаимодействие с тем или иным классом/типом устройств. Концепция драйвера оказалась настолько удачной, что помимо основного направления - поддержки физических устройств, была экстраполирована и на некоторые категории логических/виртуальных устройств. В данной статье мы будем рассказывать про то, что же из себя представляет драйвер Windows.
Теория
Давайте на время абстрагируемся от концепции драйвера и начнем с общей теории. Для того, чтобы понять что же представляет собой драйвер в системе, сначала нужно пройти необходимый минимум теории по общей архитектуре x86-64. Почему x86, да потому что именно эта платформа: а) выбрана мной для экспериментов, б) является наиболее распространенной в клиентском сегменте операционных систем Windows. Озвученные в данном разделе особенности дадут нам понимание многих аспектов работы как непосредственно операционной системы, так и, соответственно, драйверов в её составе.
Режимы работы процессора
Внутренняя структура любой операционной системы базируется на аппаратных особенностях платформы, на которой она работает. Центральным звеном является процессор, у процессоров архитектуры x86-64 имеются несколько режимов работы:
- Реальный режим (Real mode);
- Виртуальный режим (Virtual mode);
- Защищенный режим (Protected mode);
- Длинный режим (Long mode).
На заре эры развития персональных компьютеров архитектуры x86, процессор работал в реальном режиме. Тем не менее, реальным режим постепенно ушел в прошлое, поскольку имел ряд особенностей, делающих затруднительным дальнейшее развитие технологии: однозадачную аппаратную среду, 16-битную шину данных и 20-битную шину адреса (ограничение по адресации), сегментную адресацию с размерами сегментов в 64 килобайта (неудобство использования адресного пространства), отсутствие разграничений доступа к адресному пространству (невозможность создания изоляции процессов). С целью снятия существовавших ограничений был разработан защищенный режим, который предоставлял ряд важных для развития операционных систем особенностей: многозадачность, механизм защиты (доступ к привилегированным командам), обеспечивающий контроль доступа различных участков кода (программ) друг к другу, модель виртуальной памяти. В защищенном режиме процессоров Intel архитектуры x86 реализованы так называемые кольца защиты или уровни привилегий. Всего их четыре: 0 (наиболее привилегированный), 1, 2 и 3 (наименее привилегированный). Уровни привилегий призваны защитить код режима ядра от пользовательских программ и пользовательские программы друг от друга, поскольку это может привести к нарушению работоспособности. Однако операционная система Windows не использует все перечисленные уровни, в ней задействованы лишь два из них: 0-й и 3-й.
Для наглядности понимания приведем упрощенную схему взаимодействия компонентов Windows:
Как вы видите, внутренняя среда операционной системы Windows разделена на две части и поддерживает два режима выполнения:
- Пользовательский режим - непривилегированный режим, сопоставленный с аппаратным 3-им кольцом защиты процессора;
- Режим ядра - привилегированный режим, сопоставленный с аппаратным 0-м кольцом защиты процессора;
Это стоит понять, осознать и запомнить раз и навсегда, поскольку, собственно, это одна из базовых, основных концепций очень многих современных операционных систем.
Режимы пользователя и режим ядра обладают следующим различиями:
- Изолированными (не пересекающимися) виртуальными адресными пространствами: пространство пользовательского режима занимает "нижнюю" часть (адреса с 00000000 по 7FFFFFFF (для 32-бит)), пространство режима ядра занимает "верхнюю" (адреса с 7FFFFFFF по FFFFFFFF (для 32-бит));
- Разными привилегиями доступа кода к ресурсам (памяти, процессору, устройствам и прч).
В пользовательском режиме выполняются следующие процессы (пример для Windows 7):
Подсистема | Описание |
---|---|
Процессы обеспечения работоспособности системы (System Support Processes) |
|
Процессы служб/сервисов (Service Processes) |
|
Приложения (Applications) |
|
Подсистемы окружения (Environment Subsystems) |
|
Интерфейс к функциям ядра |
|
В режиме ядра выполняются (пример для Windows 7):
Подсистема | Описание |
---|---|
Исполнительная система (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) |
Поскольку любая операционная система попросту обязана уметь работать с аппаратными средствами, в дистрибутиве (комплекте установки/системных файлах) присутствуют драйверы ключевых компонентов аппаратного обеспечения, без которых система буквально лишится доступа к аппаратной части со всеми вытекающими из этого проблемами: не сможет функционировать или вовсе не пройдет процедуру собственной установки. Представлены эти "внутренние" драйверы в виде так называемой встроенной библиотеки драйверов, видоизменяющейся по составу от версии к версии, в зависимости от этапов эволюционирования аппаратного обеспечения и рыночных тенденций. Драйвера из состава данной библиотеки, при необходимости, устанавливаются на этапе инсталляции операционной системы в зависимости от обнаружения (идентификации) в компьютере тех или иных устройств. В общем случае, во время инсталляции, код модуля обнаружения оборудования выполняет перечисление (определение) установленных в компьютере устройств и проверяет в своей библиотеке наличие сопоставимых драйверов. Для тех устройств, для которых присутствуют системные драйвера, производится установка в автоматическом (фоновом) режиме. Тем самым "на выходе", после инсталляции операционной системы, мы можем получить минимально-необходимый для функционирования набор системных драйверов, который позволяет организовать работоспособную [начальную] рабочую среду. Но стоит помнить, что ограничиваться встроенными в дистрибутив драйверами не стоит, поскольку для полноценного функционирования большинства устройств могут потребоваться драйвера, предоставляемые производителем устройства.
Уровни запросов прерываний (IRQL)
Среди ключевых внутренних механизмов, определяющих функционирование операционной системы 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) используются для синхронизации программных подсистем операционной системы: запуска операций планирования, таких как переключение потоков или обработка завершения ввода/вывода. Давайте рассмотрим их подробно:
- 0-й (низший) приоритет IRQL (PASSIVE): является типовым уровнем запроса прерываний, на котором производится основная работа в операционной системе (как в пользовательском режиме, так и в режиме ядра). Код (программа), выполняющийся на данном уровне, может быть элементарно прерван (вытеснен) всем чем угодно: например потоки, исполняющиеся с уровнем IRQ PASSIVE подвергаются вытеснению планировщиком по истечении кванта времени, выделенного для них.
- 1-й уровень IRQL (APC): На этом уровне выполняются так называемые асинхронные вызовы процедур (APC). Фактически это процедуры, организующие асинхронный ввод-вывод, или обращающиеся/ждущие освобождения каких-либо (внешних, глобальных) системных объектов. То есть важная особенность их заключается в том, что они выполняются асинхронно в контексте содержащего их потока, дабы не тормозить процесс в целом. Использование APC-функций (таких как WaitForSingleObjectEx, WaitForMultipleObjects и других) в коде не приводит к мгновенному выполнению функции, вместо этого поток (в контексте которого функция выполняется) переходит в специальный статус и генерируется программное прерывание APC, вызов функции ставится во внутреннюю очередь. В следующий раз, когда подошло время выполняться этому потоку, запланированная APC-функция выполняется на уровне APC. Потоки, работающие на уровне APC, соответственно не получают запросы своего же уровня АРС, которые система использует для операций завершения ввода/вывода.
- 2-й уровень IRQL (DPC/DISPATCH):
- используется для обработки отложенных вызовов процедур (Deferred procedure call, DPC): Отложенные вызовы процедур - это подпрограммы обратного вызова, которые отложены для выполнения до того момента, когда произойдет переключение на уровень IRQL DISPATCH; Обычно DPC запрашиваются с высоких уровней IRQL с целью осуществления дополнительной работы, для которой затрачиваемое процессорное время не критично. Это довольно важная для производительности стадия, и сейчас я объясню почему. Драйверы устройств стараются выполнять минимально-возможное количество операций внутри собственных подпрограмм обработки прерывания (ISR), чтобы не занимать продолжительное время на уровне DIRQL (DEVICE), тем самым не блокируя остальные прерывания и не тормозя, в итоге, всю систему.
Чем выше уровень IRQL, тем меньше возможности процесса. Это побуждает разработчиков на высоком уровне IRQL выполнять только самые необходимые операции, а все остальные действия производить на низком.
Если драйвер понимает, что требуется выполнение дополнительной работы, которая занимает существенное процессорное время, то он запрашивает DPC и перекладывает на него эту задачу. Когда уровень IRQL опускается до DISPATCH, происходит обратный вызов отложенной функции драйвера, которая и выполняет оставшуюся часть обработки. Реализуя подобный алгоритм на уровне IRQL DISPATCH, драйвер проводит на уровне DIRQL меньшее количество времени, и соответственно, уменьшает время задержки на обработку собственного прерывания, тем самым освобождая его для других устройств системы.
- используется для выполнения задач планировщика: в операционных системах линейки Windows NT реализована вытесняющая многозадачность, которая означает, что каждому процессу, выполняющемуся в операционной системе, выделяется для выполнения определенное время. Поскольку IRQL планировщика потоков и DPC равен 2, то он выше приоритета пользовательских потоков (исполняемых на уровне 0). В свою очередь приоритет планировщика ниже чем приоритет аппаратных прерываний (прерываний от устройств), то есть он может быть прерван аппаратными прерываниями.
- используется для обработки отложенных вызовов процедур (Deferred procedure call, DPC): Отложенные вызовы процедур - это подпрограммы обратного вызова, которые отложены для выполнения до того момента, когда произойдет переключение на уровень IRQL DISPATCH; Обычно DPC запрашиваются с высоких уровней IRQL с целью осуществления дополнительной работы, для которой затрачиваемое процессорное время не критично. Это довольно важная для производительности стадия, и сейчас я объясню почему. Драйверы устройств стараются выполнять минимально-возможное количество операций внутри собственных подпрограмм обработки прерывания (ISR), чтобы не занимать продолжительное время на уровне DIRQL (DEVICE), тем самым не блокируя остальные прерывания и не тормозя, в итоге, всю систему.
Пример, поясняющий назначение IRQL
Хорошо, но я так и не понял, почему нельзя было отказаться от всех этих уровней и сделать "плоскую" модель очередей, либо выполнять все эти типы задач по мере поступления? Давайте смоделируем рабочую ситуацию: представим какой-либо код, например небольшую программу, написанную "на коленке". Мы запустили её на выполнение, соответственно в системе сформировался процесс для нашей программы, в контексте которого начал выполняться основной поток. Типовой поток (режима пользователя или режима ядра) исполняется на самом низшем уровне IRQL PASSIVE. На протяжении всего времени выполнения потока, часы (микросхема таймера) периодически генерирует собственные прерывания для отсчета временных интервалов, которые используются для указания операционной системе о прохождении заданного промежутка времени. Процедура обработки прерывания часов выполняется на уровне IRQL CLOCK, который (если посмотреть в таблицу) выше по приоритету большинства уровней: и уровня DISPATCH, на котором выполняется планировщик, и уровня PASSIVE, на котором выполняется наша программа. Таким образом таймер постоянно вытесняет работу и планировщика и нашей программы. С каждым переданным тиком таймера, процедура обработки прерывания таймера уменьшает остающийся у выполняющегося в данный момент нашего пользовательского потока квант времени. В момент, когда квант времени выполняющегося потока уменьшается до нуля, программа обработки прерывания часов генерирует прерывание уровня DISPATCH, тем самым вызывая запуск планировщика для выбора им следующего потока для выполнения. По факту генерирования прерывания уровня DISPATCH, процедура обработки прерывания таймера заканчивает исполнение своего кода и управление возвращается ядру системы. Ядро находит в очереди запросов следующее прерывание с наиболее приоритетным уровнем, находящееся в режиме ожидания. Каждое прерывание обслуживается по очереди. Когда все прерывания выше уровня DISPATCH обслужены, то выполняется процедура обработки прерывания уровня DISPATCH. Эта программа обработки прерывания обрабатывает список DPC и затем вызывает планировщик. Планировщик обнаруживает, что квант времени текущего потока исчерпан, то есть уменьшен до нуля, после чего Планировщик выполняет алгоритм планирования для выбора следующего потока на выполнение. Код поставленного на выполнение потока будет выполнен когда система опустится на уровень IRQL PASSIVE.
Теперь представьте, что вы уберете из системы иерархию уровней запросов прерываний, как в этом случае будет вести себя система? В этой ситуации было бы непонятно что и когда выполнять, система выполняла бы все поступающие задачи в порядке очереди, что привело бы к тому, что потоки запросто могли бы не дать планировщику выполниться, тем самым полностью уничтожить вытесняющую многозадачность, что повлекло бы за собой непредсказуемую работу ОС. Таким образом:
соответственно:
Назначение уровней IRQL в системе следующие:
- Маскировка: повышение уровня прерывания позволяет отрезать (замасировать) низлежащие уровни аппаратных прерываний на контроллере PIC. Это позволяет на время проигнорировать прерывания, возникающие на более низких уровнях, тем самым выигрывая время на выполнение процедуры обработки аппаратного прерывания на данном уровне.
- Аппаратная синхронизация: синхронизация данных между потоками, выполняющимися на разных процессорах/ядрах в многопроцессорной системе.
- Программная синхронизация: для определения когда различные APC/DPC-процедуры могут быть обслужены, для определения когда могут быть обслужены приложения пользовательского режима.
Тем самым, на глобальном уровне механизм IRQL позволяет подпрограмме операционной системы:
- Управлять повторной входимостью (реентерабельность)
- Гарантировать, что она может продолжать работу без приоритетного прерывания (вытеснения) некоторыми другими действиями.
Ну хорошо, а как это воздействует на драйвера? Мы знаем, что драйвера могут быть пользовательского режима и режима ядра, соответственно, выполняются пользовательском режиме и в режиме ядра. Отсюда следует, что:
И отсюда следует два достаточно важных вывода:
- Код драйвера является вытесняемым и прерываемым. Как и любой другой код в системе, он может быть прерван в любой момент по окончании выделенного кванта времени;
- Код драйвера должен использовать те или иные наборы системных функций в зависимости от уровня IRQL, на котором он выполняется.
Представьте ситуацию, когда код драйвера выполняется на низком уровне IRQL, модифицирует какой-либо объект (например, файл file.txt), затем другой код на более высоком уровне IRQL внезапно прерывает его выполнение и модифицирует тот же файл file.txt другими данными. Когда управление вернется к нашему драйверу, он продолжит модификацию файла своими данным, тем самым затерев данные, поступившие от другого источника. Таким образом файл войдет в рассогласованное состояние. Для решения подобных проблем были введены различные системные объекты синхронизации. Для того, что бы код уровня ядра мог модифицировать определенные типы данных, объекты взаимного исключения, он должен сперва получить владение блокировками.
Концепция драйвера
Соответственно выводы, следующие из этого утверждения, очевидны: для взаимодействия системы с устройствами требуются отдельные интерфейсы, возможно даже сложная совокупность нескольких интерфейсов. Концепция драйвера была разработана для решения задачи сопряжения и используется в моделях большинства современных систем, она основана на работе в адресном пространстве ядра специального кода, который обеспечивает взаимодействие ядра системы с любым типом логических/физических устройств.
Учитывая общую ориентированность ресурса, в статье мы будем освещать специфику исключительно драйверов операционной системы Windows. Итак, для драйвера Windows, как, в общем то, драйверов других операционных систем, верны следующие утверждения:
то же, но другими словами:
Одно из приведенных определений отмечает немаловажную особенность драйвера: ошибочно представлять драйвер исключительно во взаимодействии с физическим устройством, поскольку драйвер не обязательно должен предоставлять доступ к функциям какого-либо оборудования, он может обеспечивать и исключительно программные функциональные особенности. Примерами подобных решений являются драйвера, устанавливаемые в систему антивирусами, системами шифрования данных, системами мониторинга. Общий алгоритм работы любого драйвера следующий: приложения посредством функций специального пользовательского интерфейса (в Windows это Win32 API) или запросов ввода-вывода опосредовано/напрямую обращаются к функциям драйвера некоего устройства. Драйвер, в свою очередь, предоставляет доступ к функциональным особенностям интересующего устройства, а так же контролирует процесс взаимодействия между запросами приложений и непосредственно устройством. Естественно, что в драйвере должны быть определены (описаны) все принципы взаимодействия с обслуживаемым (подчиненным, собственным) устройством, должен присутствовать набор данных об управляемом объекте, инструкции (набор команд), с помощью которых системный/пользовательский код может корректно инициализировать устройство и начать с ним взаимодействие.
Загрузка драйверов при запуске операционной системы
Очень интересно было бы увидеть, на какой именно стадии загрузки операционной системы начинает загружается и начинает выполняться первый драйвер Windows? Однако в детальном изложении процесс этот достаточно нетривиален и для глубокого понимания требует реверсинга кода многих компонентов загрузки, в дополнение ко всему необходимо учитывать множество сопутствующих моментов, как то: последовательность загрузки, обусловленную зависимостью между драйверами, по причине которой драйвера могут группироваться в так называемые "группы загрузки", сама загрузка драйверов может разделяться на несколько этапов и прочее. При этом, следует учесть, что в Сети имеется большое количество материалов относительно устаревших уже операционных систем, поэтому мы попытаемся актуализировать процесс загрузки драйверов Windows на примере (наиболее близкой мне по духу) операционной системы Windows 7. И для начала не мешало бы рассказать об основных компонентах ядра Windows, активно участвующих в процессе загрузки драйверов:
- Диспетчер (менеджер) ввода/вывода (I/O Manager) - модуль режима ядра, входящий в состав исполнительной подсистемы, управляющий процессами ввода/вывода, обеспечивающий абстракцию физических и логических устройств для пользовательских приложений и системных компонентов, связывающий приложения пользовательского режима с драйверами. Контролирует стадии процесса взаимодействия с драйверами. Весь обмен данными менеджера ввода-вывода с драйверами осуществляется через обращение к процедурам обратного вызова драйвера (callback) и передачи им стандартизованной структуры данных IRP, в которой описана вся суть обращения к драйверу;
- Диспетчер (менеджер) Plug-and-Play (PnP Manager) - модуль режима ядра и пользовательского режима, входящий в состав исполнительной подсистемы, отвечающий за добавление, распознавание, удаление устройств в операционной системе. Часть режима ядра взаимодействует с остальными компонентами системы и драйверами в процессе установки (загрузки) программного обеспечения, необходимого для обслуживания имеющихся в системе устройств. Часть пользовательского режима отвечает за взаимодействие с программами режим пользователя (для интерактивного взаимодействия с пользователем) в ситуациях, требующих установки новых драйверов или настройки рабочих параметров в существующих. Управляет распределением аппаратных ресурсов в системе, так же умеет распознавать устройства, реагировать на их подключение/отключение, загружать соответствующие драйвера при обнаружении новых устройств;
- Диспетчер (менеджер) управления службами (Service Control Manager, SCM) - системный процесс, ответственный за создание, удаление, запуск и остановку служб и драйверов операционной системы. Так же обеспечивает: функционирование журнала событий, поддержку технологии удалённого вызова процедур (remote procedure call, RPC);
Эти два менеджера, то есть менеджер ввода-вывода и PnP менеджер, активно взаимодействуют между собой.
Теперь мы опишем процесс загрузки операционной системы, однако сделаем это не в привычной нам форме, а кратко отметим ключевые моменты, касающиеся работы описанных компонентов операционной системы с драйверами:
- Bootmgr(.efi) загружает модуль winload(.efi) и передает ему управление.
- Winload(.efi) сканирует куст реестра HKEY_LOCAL_MACHINE\System\services и получает список всех установленных в системе драйверов. В этом кусте реестра присутствуют разделы, сопоставляемые с конечными драйверами, в них присутствуют разнообразные относящиеся к драйверам параметры, такие как Group, Start, Type, LoadOrderGroup, DependOnGroup, DependOnServices, определяющие те или иные критерии загрузки драйвера.
- Winload(.efi) загружает драйвера, критичные для начальной стадии загрузки/функционирования операционной системы, такие как драйвера контроллеров накопителей, драйвера файловых систем. Очевидно, что подобные драйвера имеют наивысший приоритет, поскольку создают базис для загрузки остальных драйверов, поэтому в следствии этих а так же иных причин должны находиться в памяти в момент передачи управления ядру. Соответственно, маркируются они специальным типом
SERVICE_BOOT_START
. Драйвера на данном этапе начинают загружаться в зависимости от групп, к которым они принадлежат. - Winload(.efi) загружает непосредственно ядро из файла ntoskrnl.exe и передает ему управление.
- Ядро загружает Менеджер ввода-вывода и PnP-менеджер.
- Менеджер ввода-вывода создает глобальный каталог. Этот каталог, в дальнейшем, используется для регистрации объектов устройств.
- PnP-менеджер стартует драйвера, уже загруженные в память на предыдущем этапе (имеющие тип
SERVICE_BOOT_START
), вызывая процедуруDriverEntry
каждого драйвера. На данном этапе загружаются и зависимые драйвера. - PnP-менеджер строит дерево устройств системы, обходит его начиная с корня и загружает драйвера устройств, которые еще не были загружены.
- PnP-менеджер загружает оставшиеся незагруженными драйвера устройств, вне зависимости от значения параметра Start. Многие из подобных драйверов имеют тип
SERVICE_DEMAND_START
. - PnP менеджер загружает драйвера расширенного функционала. К таким драйверам относятся драйвер видеоадаптера, драйвера внешних устройств, драйвера стека TCP/IP. Такие драйвера имеют тип
SERVICE_SYSTEM_START
. - Ядро загружает Менеджер (диспетчер) управления сеансами/сессиями (Session Manager Subsystem Service, SMSS), который, в свою очередь, загружает Менеджер (диспетчер) управления службами (SCM). SCM сканирует куст реестра (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services) и на основе полученной информации монтирует внутреннюю базу данных служб/драйверов, формирует программный интерфейс для обслуживания установленных служб/драйверов. SCM загружает "автозапускаемые" не-PnP драйвера (имеющие тип
SERVICE_AUTO_START
), а также все драйвера, от которых они зависят.
Из всего этого алгоритма загрузки драйверов нам необходимо уяснить следующие основные правила: драйвер может быть загружен (в зависимости от стадии/класса драйвера) при помощи PnP-менеджера, либо с помощью SCM, а вот в процессе функционирования драйвера активно принимает участие Менеджер ввода-вывода.
Структура драйвера Windows
На что может быть похож драйвер по структуре? Неужели это какой-то особый класс программ, устроенных сугубо специфически? Я тоже так когда-то по наивности полагал, но если подумать, то с какой стати разработчикам операционной системы усложнять себе жизнь и изобретать новый специализированный формат образа исполняемого файла для каких-то ядерных компонентов? Намного проще адаптировать старый, давно уже отлаженный и сто раз проверенный.
IMAGE_NT_HEADERS
, подструктура OptionalHeader) значение поля Subsystem = 1 (IMAGE_SUBSYSTEM_NATIVE
).Тип подсистемы может быть задан при сборке исполняемого модуля. Сама по себе нативная подсистема характерна для приложений, которые функционируют по иным, отличным от классических, правилам: на стадии подготовки образа к исполнению им не требуется инициализация подсистемы Win32. В числе прочих подсистема native используется для кода режима ядра, коим и являются практически все драйвера.
Сделаем небольшое отступление и поговорим о таком понятии как объект. Дело в том, что весь процесс функционирования драйвера Windows, как и любых других модулей операционной системы, зависит от разнообразных системных структур данных. Эти структуры управляются ядром и могут содержать в себе потоки, события, запросы ввода-вывода, устройства и прочие сущности.
- Объекты. Блоки данных, содержащие записи свойств той или иной сущности операционной системы. Управляются диспетчером (менеджером) объектов. Многие объекты имеют дескрипторы (описатели), при помощи которых объект становится доступным для приложений.
- Структуры данных. Блоки данных, содержащие записи свойств той или иной сущности операционной системы. Управляются ядром. Отличаются от объектов, однако (по инерции) так же называются объектами
Поэтому (с некоторыми исключениями) все внутренние структуры операционной системы Windows называют объектами.
Теперь вернемся к процедурам драйвера, на деле так называемые "процедуры" драйвера являются COM-объектами обратного вызова, которые обрабатывают поступающие от соответствующих объектов инфраструктуры операционной системы события, говорится что драйвер предоставляет ядру операционной системы COM-интерфейс, заданный серией процедур, реализуемых драйвером. Экспорт, то есть публикация (объявление) процедур драйвера для дальнейшего обращения к ним извне, выполняется путем регистрации в основной процедуре драйвера (стандартной для всех драйверов), носящей название DriverEntry.
Функция DriverEntry фактически является функцией глобальной инициализации и выполняется единожды на этапе загрузки драйвера. Эта функция может быть как предельно простой, так и содержать расширенный функционал (дополнительные подпрограммы), такой, например, как создание дополнительных объектов устройств, опрос устройства, дополнительные фазы конфигурации и инициализации устройств(а).
После публикации собственных функций драйвер становится "видимым" ядром операционной системы. Дабы не усложнять и без того достаточно непростую теорию, будем считать, что с точки зрения ядра Windows любое устройство является неким абстрактным "виртуальным устройством", оперирующим стандартизированным набором команд, и доступным через внутренние интерфейсы. Как было уже сказано выше, в ядре операционной системы Windows присутствует специальный модуль исполнительной системы, называемый диспетчером (менеджером) ввода-вывода, обеспечивающий единый интерфейс взаимодействия для всех драйверов режима ядра, включая драйверы физических устройств, драйверы логических устройств и драйверы файловых систем. Соответственно, система ввода-вывода ядра управляет драйверами, или можно сказать, что драйверы используют интерфейс диспетчера ввода-вывода для обеспечения функционирования в операционной системе. С дургой стороны, драйвер обеспечивает преобразование (конвертацию) "стандартных команд", поступающих от операционной системы, в команды, которые "понимает" подконтрольное ему устройство (если оно имеется), и наоборот. Менеджер ввода-вывода определяет набор (множество) стандартных процедур, которые могут быть реализованы в драйвере, поскольку:
Для более глубокого понимания того, какие функциональные особенности может обеспечивать драйвер, давайте приведем общую схему ключевых процедур драйвера:
Собственно, глядя на приведенную схему, становится понятно, какие именно виды взаимодействия, а именно группы процедур должен реализовывать абстрактный драйвер Windows. Давайте теперь перечислим некоторые из этих процедур:
- Инициализация - Менеджер ввода-вывода запускает процедуру инициализации (носит название DriverEntry), которая предназначается для проведения действий по начальной настройке объекта драйвера, регистрации всех иных процедур драйвера, конфигурированию подчиненного устройства и выполнении иных действия в интересах разработчика.
- Добавление устройства - добавление (дополнительного) объекта "устройство". В данной процедуре драйвер обычно создает объекты "устройство" для каждого устройства, обслуживаемого драйвером. Обычно используется для Plug-and-Play драйверов.
- Обработка - набор процедур диспетчеризации (обработки различных состояний). Открытие, закрытие, чтение, запись в устройство, обработка состояний питания, событий PnP и состояний системы, а так же некоторые иные виды взаимодействия описываются в процедурах диспетчеризации. Фактически это основные процедуры, поскольку через процедуры диспетчеризации обрабатываются типовые операции ввода-вывода.
- Запуск (начало) ввода-вывода - вторая стадия обработки запроса ввода-вывода к устройству, непосредственно стартующая ввод-вывод устройства. Эта процедура может использоваться для начала передачи данных в/из устройства.
- Процедура обслуживания прерывания - когда устройство генерирует прерывание, диспетчер прерываний передает управление на данную процедуру.
- Обработка отложенных вызовов процедур - Процедура DPC берет на себя основную работу по обработке прерывания после выполнения ISR. Отложенные вызовы процедур выполняются на низких уровнях IRQL (DPC/DISPATCH) чем сама процедура ISR. Реализуется подобный алгоритм для исключения блокировки других прерываний.
- Процедура завершения ввода-вывода - многоуровневый драйвер может иметь процедуры завершения ввода-вывод, которые уведомляют о завершении обработки IRP низкоуровневым драйвером.
- Процедуры отмены ввода-вывода - если операции ввода-вывода могут быть прерваны, драйвер может определить одну или несколько подобных процедур. Когда драйвер получает IRP-пакет для запроса ввода-вывода, который может быть отменен, он назначает процедуру отмены IRP, и пакет IRP проходит через различные стадии обработки, которые эта процедура может изменить или убрать, если текущая операция не отменяема.
- Процедура быстрой отправки - Драйвера, которые активно используют Менеджер кеша, такие как драйвера файловых систем, обычно предоставляют подобные процедуры для обеспечения возможности обхода ядром типовых алгоритмов обработки ввода-вывода.
- Процедура выгрузки - должны быть реализованы в каждом драйвере, который работает (освобождают/занимают) с системными ресурсами, для того, чтобы Диспетчер ввода-вывода выгрузить драйвер из памяти.
- Процедура предупреждения о завершении - позволяет драйверу освободить все занимаемые ресурсы при завершении работы системы.
Становится очевидным, что в процессе разработки драйвера Windows не стоит задачи реализовать весь набор описанных выше процедур, каждый драйвер уникален и разработчик волен обеспечивать собственный набор реализаций, поддерживаемых драйвером. Когда драйвер при помощи PnP-менеджера или SCM загружается в систему, диспетчер ввода-вывода создает в пространстве имен объект "драйвер" (driver object) и вызывает процедуру инициализации драйвера (обычно это DriverEntry), которая выполняет дальнейшие действия по инициализации.
Объект драйвера представляет код и данные драйвера в ядре: помимо прочего, через этот объект драйвер экспортирует точки входа своих процедур. Процедура инициализации драйвера записывает в атрибуты данного объекта точки входа всех экспортируемых процедур драйвера. После загрузки драйвер может создавать объекты "устройство" для представления устройств или даже для формирования интерфейса драйвера. Большинство драйверов создают объекты "устройство" следующим образом:
- PnP-драйверы создают объекты "устройство" с помощью своих процедур добавления устройств, когда PnP-менеджер информирует их о присутствии управляемого ими устройства.
- не-PnP-драйверы создают объекты "устройство" при вызове менеджером ввода-вывода их процедур инициализации.
При создании объекта типа "устройство" (device), драйверу требуется присвоить данному объекту имя. Затем этот вновь созданный объект помещается в пространство имен диспетчера объектов (Object Manager), который, как и диспетчер (менеджер) ввода-вывода, является частью исполнительной подсистемы ядра. Менеджер объектов предназначается для ведения базы всех ресурсов операционной системы, представленных в качестве объектов. Имя объекта может определяться самим драйвером в явном виде, либо генерироваться автоматически менеджером ввода-вывода. По соглашению, объекты "устройство" должны размещаться в каталоге \Device
пространства имен менеджера объектов, недоступном приложениям через Win32 API. А для того, чтобы объект "устройство" стал доступным для приложений, драйвер должен создать в каталоге \GLOBAL??
символьную ссылку на имя этого объекта в каталоге \Device
. Драйверы, не поддерживающие технологию Plug-and-Play, и драйверы файловой системы обычно создают символьную ссылку с общеизвестным именем (скажем, \Device\VMwareKbdFilter
). Только после всех перечисленных действий драйвер становится "виден" в системе и доступен для вызова пользовательскими приложениями.
Взаимодействие с драйвером
Каким же образом пользовательская программа может взаимодействовать с драйвером в системе? На этот случай имеется два способа:
- Неявный -- вызов типовой функции Win32 API;
- Явный -- прямой запрос ввода-вывода к драйверу;
Ну с первым случаем все достаточно просто, в прикладной программе вызывается какая-либо ординарная функция 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). Как мы уже упоминали, функция инициализации драйвера DriverEntry сообщает ядру (публикует) имена этих процедур и позже ядро само вызывает ту или иную процедуру при определенных обстоятельствах.
В отличие от штатной программы, драйвер не является классическим процессом со своим адресным пространством и не имеет потока исполнения. Вместо этого, функция драйвера выполняется в контексте того потока и процесса, в котором она была вызвана. Контекст (пространство выполнения кода) драйвера зависит от того, кто производит обращение (вызывает) к драйверу. Обращение может быть инициировано:
- Прикладной программой (программой пользовательского режима). В этом случае контекст исполнения драйвера точно известен, и он совпадает контекстом прикладной программы;
- Другим (сторонним) драйвером. В этом случае контекст исполнения определить сложнее, он может быть как известным, так и случайным, это зависит от контекста исполнения функции вызывающего драйвера.
- Аппаратным/программным прерыванием. В этом случае контекст исполнения случайный, поскольку прерывание (и, соответственно, переключение на код драйвера) может произойти при выполнении абсолютно любого кода в операционной системе.
Опять же, в отличии от штатной программы, драйвер не может вызывать стандартные функции Win32 API, может лишь оперировать доступными в ядре функциями, которые начинаются с префиксов Ex..
, Hal..
, Io..
, Ke..
, Ks..
, Mm..
, Ob..
, Po..
, Ps..
, Rtl..
, Se..
, Zw..
и некоторых других.
Виды (типы) драйверов Windows
В процессе эволюционирования и, соответственно, усложнения драйверной концепции, драйверы начали подразделяться на категории (или типы) в зависимости от назначения. Вот основные из них:
- Драйверы классов (Class driver) - драйверы, разработанные Microsoft для определенного класса устройств.
- Драйверы файловых систем (File System Drivers) - драйверы, реализовывающие файловые системы на различного рода носителях информации.
- Унаследованные драйвера (Legacy drivers) - "устаревшие" (совместимые по структуре со старыми версиями ОС) драйверы режима ядра, самостоятельно напрямую контролирующие подчиненное устройство без каких-либо дополнительных драйверов устройства. Почему они носят такое название? Потому, что это тип драйверов, сохранившийся от первых версий ОС линейки Windows NT.
- Драйвер шины (Bus driver) - Драйверы, обеспечивающие функционал какой-либо шины компьютера (ISA,PCI,USB,IEEE1394 и прочих);
- Фильтрующие драйверы (Filter driver) - драйверы, использующиеся для мониторинга/изменения логики другого драйвера путем работы с данными, проходящими через него.
- Верхние фильтрующие драйверы (Upper-filter drivers) - подтип фильтрующих драйверов, находящийся выше функционального драйвера по стеку. Через верхние фильтрующие драйверы проходят все запросы, а это значит, что они могут изменять и/или фильтровать информацию, идущую к функциональному драйверу, ну и далее, возможно, к устройству. Примерами могут являться фильтр-драйвер, который отслеживает/фильтрует трафик, шифрует/перехватывает запросы чтения/записи. Такие драйверы используются в брандмауэрах.
- Нижние фильтрующие драйверы (Lower-filter drivers) - подтип фильтрующих драйверов, находящийся ниже функционального драйвера по стеку. Через подобные нижние фильтрующие драйверы проходит, как правило, меньше запросов по сравнению с остальными фильтрующими драйверами, потому как большинство запросов выполняет и завершает сам функциональный драйвер.
- Функциональные драйверы (Function driver) - драйверы, функционирующие самостоятельно и определяющие все аспекты, связанные с устройством.
- PnP драйвер (PnP Driver) - драйвер, поддерживающий технологию Plug-and-Play;
- Минидрайвер (минипорт, миникласс) (Miniport driver, Minidriver, Miniclass driver) - драйверы, которые обрабатывают задачи, связанные с конечным устройством и используют драйвера класса для управления устройством. Действуют как одна из частей пары драйверов, в которой данная категория действует как драйвера конечных устройств, выполняющая специфичные задачи устройства.
По уровню компонетизации драйверы бывают:
- Одноуровневые - обработка ввода/вывода реализуется в рамках одного исполняемого модуля (драйвера).
- Многоуровневые - обработчка ввода/вывода распределяется между несколькими драйверами.
PnP драйвера под Windows подразделяются на:
- Функциональный Драйвер
- Драйвер шины (шинный драйвер)
- Драйвер-фильтр (фильтр-драйвер)
По режиму выполнения драйверы Windows градируются:
- Драйвер пользовательского режима.
- Драйвер режима ядра.
Модели драйверов
На протяжении всего времени существования операционной системы, разработчики пытались стандартизировать и упростить разработку драйверов. В следствии чего появились модели.
Модель WDM
Когда-то очень давно существовало две основных направления развития драйверной концепции Windows:
- в Windows 95/98 применялась модель VxD (Virtual Device Driver);
- в Windows NT3.51 параллельно развивалась модель NT-драйвер (драйвер в стиле NT, NT Driver).
Однако, начиная с версии Windows 98/NT4.0 разработчики предприняли попытку унифицировать (универсализировать) разработку драйверов, в следствии чего на смену упомянутым моделям пришла новая модель WDM.
Модель WDM являлся этапом переопределения классического стека драйвера Windows с целью обеспечения поддержки являющихся в то время революционными технологий Plug-and-Play и ACPI. Модель дает возможность загружать/выгружать драйверы "на лету", без необходимости в перезагрузке операционной системы, разрабатывать драйвера в виде расширений (фильтров) к стандартным системным драйверам, более гибко управлять энергосбережением и конфигурацией устройств и прочее.
В рамках модели WDM любое аппаратного устройство поддерживается, как минимум, двумя драйверами:
- Функциональный драйвер (Function driver) - ответственен почти за все функциональные особенности обслуживаемого устройства: операции ввода-вывода, обработка прерывания и управление устройством;
- Драйвер шины (Bus driver) - ответственен за обслуживание соединения между устройством и компьютером, фактически поддержкой связующей шины (например, PCI, USB и прочие).
Модель WDF
На протяжении всего времени развития, модель WDM претерпевала множество изменений, существенно разрастаясь. Начиная с Windows Vista была предпринята очередная попытка развития концепции драйвера Windows, в сущности уже существовавшей на тот момент модели WDM, результатом чего явилось новой модели (надстройки над WDM) под названием WDF.
Связано это было с тем неоспоримым фактом, что разработчикам не удалось достичь достаточного уровня абстракции модели WDM, а именно недостаточной интеграцией подсистемы ввода-вывода с технологией Plug-and-Play и управлением питанием. Это приводило к тому, что на разработчике драйвера лежала громадная нагрузка по синхронизации этих самых запросов ввода-вывода с событиями Plug-and-Play и запросами энергопотребления. Очевидно, требовалось дальнейшее упрощение драйверной модели. WDF пришла на смену WDM и считается наиболее современной моделью.
WDF реализует следующие возможности:
- "Вынесение" некоторых некритичных к режиму исполнения классов драйверов в пользовательский режим, что дало сокращение общего количества сбоев в ядре.
- Большая часть обработки взаимодействия подсистемы ввода-вывода с Plug-and-Play и управлением электропитанием выполняется теперь встроенными механизмами модели WDF.
- Предоставление новых внутренних интерфейсов модели WDF, которые позволяют абстрагироваться от более сложных для понимания системных интерфейсов; В модели WDM/legacy довольно сложно реализовать логику некоторых частей взаимодействия с драйвером, не изучив все азы сложной архитектуры ядра, WDF же позволяет автоматизировать многие виды взаимодействия; Большое количество кода при разработке WDM-драйвера теперь может быть заменено вызовами процедур WDF.
- Возможность создания "канонического" драйвера. Присутствие шаблонов, которые предоставляют стороннему разработчику возможность переопределения уникальных для его драйвера критериев, тем самым сокращая время на разработку.
Модель WDF подразделяется на два направления:
- UMDF (Kernel-Mode Driver Framework) - среда разработки драйвера режима ядра.
- KMDF (User-Mode Driver Framework) - среда разработки драйвера режима пользователя.
Подразделение сред по режимам пользователя и ядра в модели WDF достаточно условное, поскольку основное предназначение данного разграничения заключается в классификации разработки драйверов для тех или иных классов устройств.
Великолепная статья! Огромное спасибо!
классная статья! Спасибо
доработать надо. имеются небольшие неточности.