Глава 13. Тестирование и отладка

[Next]

©Солдатов В.П., 2004
E-Book Copyright© by [Korvin]


Выходные данные

УДК ББК | 004.43 32.973.26-018.1
 | С60
Солдатов В.П. Программирование драйверов Windows. Изд. 2-е, перераб. и доп. — М.: ООО "Бином-Пресс", 2004 г. — 480 с: ил.

Книга представляет собой систематизированное введение в программирование драйверов Windows 2000/XP/Server 2003 и Windows 98/Ме с использованием Microsoft Windows DDK. Начиная с рассмотрения базовых понятий и терминов программирования драйверов, автор затем подробно рассматривает набор программных средств, необходимых для разработки драйверов режима ядра в операционной системе Windows, а после реализации законченного драйвера, что дает накопление стартового опыта, переходит к детальному рассмотрению структуры драйверов модели WDM и "драйверов-в-стиле-NT". Подробно рассмотрены особенности работы с памятью в режиме ядра, вопросы взаимодействия с подсистемой ввода/вывода Windows, создания и синхронизации программных потоков, особенности работы с совместно используемыми данными, процедуры для работы с файлами, текстом, временем и Системным Реестром в режиме ядра. Рассмотрены разные способы инсталляции драйверов — от взаимодействия с системным мастером Установки новой аппаратуры до динамической загрузки с использованием сервисов SCM Менеджера. Отдельная глава посвящена составлению и отладке inf-файлов — вопросу, крайне редко затрагиваемому в русскоязычной литературе по программированию. Затрагиваются также и общие вопросы работы с аппаратурой, включая обзор шин современного компьютера — PCI, USB, FireWire, PC Cards. Завершается книга рассмотрением методов тестирования и отладки драйверов. В приложениях приводится справочная информация, полезная разработчику драйверных систем как, впрочем, и всем профессиональным программистам в среде Windows.

Материал книги дает ясное представление об основных типах данных и структурах драйверов и механизмах их взаимодействия с операционной системой. Минимальным требованием к читателю является хорошее знание языка программирования С (без расширений C++).

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

ISBN 5-9518-0099-4 © Солдатов В.П., 2004
© Издательство Бином, 2004

Научно-техническое издание

Вячеслав Петрович Солдатов
Программирование драйверов Windows

Оформление обложки И.Ю. Буровой

Подписано в печать 19.07.2004. Формат 70х100/16. Усл. печ. л. 39
Гарнитура Петербург. Бумага газетная. Печать офсетная
Тираж 4 000 экз. Заказ #3489

Издательство "Бином-Пресс", 2004 г.
170026, Тверь, Комсомольский просп., 12

Отпечатано с готовых диапозитивов во ФГУП ИПК
"Ульяновский Дом печати". 432980, г. Ульяновск, ул. Гончарова, 14


Предисловие от автора

Предисловие

Когда кто-то приступает к большому делу, а для начала решает спросить у людей сведущих что-то вроде "Как съесть слона?", то самый правильный ответ, который он только может получить: "По частям!".

Несерьезно? Но зато как верно!

Данная книга — это попытка ввести Вас, Читатель, в не самое дружелюбное подпространство мира программ — разработку драйверов, а если быть совершенно точным — драйверов для операционных систем Microsoft Windows NT 5.x, представленных на сегодня версиями Windows 2000, Windows XP и Windows Server 2003. Идея книги была подсказана обескураживающей тишиной в этой области (разумеется, речь идет о России), когда лишь только 2002 год мог бы похвастаться заметным нарушением этого молчания.

Предназначенная для студентов ВУЗ'ов и специалистов, чья профессиональная деятельность заставляет их обратиться к разработке собственных драйверов для Windows или просто к программированию в режиме ядра Windows, книга предполагает наличие у читателей достаточной подготовки. Прежде всего, разработчик драйвера должен владеть программированием на языке С (без расширений С++), поскольку описание синтаксиса и применения конструкций этого языка не рассматриваются в данной книге вовсе. Во-вторых, разработчик драйверов, пусть начинающий, должен иметь твердо сформировавшееся представление о программировании в многозадачной среде при интенсивном использовании многопоточности. Конечно же, указанные требования не столь объемны и могут быть выполнены в результате короткого "самообразовательного штурма", но здесь придется корректировать свои планы на величину различия между этапами "я Это знаю" и "я умею Этим пользоваться".

Необходимость знания Читателем языка программирования С, как было сказано выше, продиктовано тем обстоятельством, что излагаемый материал ориентирует Читателя на использование пакета Microsoft DDK (Device Driver Kit — пакет программного обеспечения для разработки драйверов), хотя существуют коммерческие программные пакеты и от других фирм, которые базируются на использовании других языков программирования (подробнее эти вопросы будут рассмотрены далее, в главе 2).

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

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

Обилие англоязычных синонимов к используемым терминам в тексте так же решает свою задачу. Рано или поздно (скоре всего, уже случилось!) Читателю придется обратиться к чтению DDK документации, поставляемой вместе с программами и библиотеками фирмой Microsoft. По ряду причин это нельзя назвать простым делом. Поскольку чтение англоязычной документации — процесс, которого разработчику драйверов не избежать, чтобы облегчить вступление на этот нелегкий путь, в книге приводятся многочисленные наборы синонимичных терминов с развернутыми вариантами переводов.

За пределами рассмотрения данной книги остались вопросы, которые можно назвать "сложным программированием" драйверов. Не рассматриваются принтерные, SCSI, видео и сетевые драйверы, поскольку этот емкий материал может легко заслонить приоритетные задачи — объяснение, какова внутренняя логика подсистемы ввода/вывода Windows и ознакомление с приемами программирования в режиме ядра.

Книга ориентирована на разработчиков программного обеспечения, но некоторые ее части будут небесполезны и для разработчиков аппаратуры.


Предисловие от составителя :: [Korvin]

Предисловие от составителя эл. версии

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

Кроме того, если Вы интересуетесь драйверами, я бы мог посоветовать Вам, кроме источников, указанных в книге, цикл статей от Four-F "Драйверы режима ядра", который можно найти на превосходном сайте Windows Assembler (wasm.ru)

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

Координаты

С сообщениями об опечатках и ошибках в книге, а также с предложениями и информацией можно обращаться по этим координатам:

  • mail: korvin@nxt.ru
  • icq: 5953073

С уважением, [Korvin]


Глава 1. Драйверы. Общие понятия и термины

Глава 1

Драйверы. Общие понятия и термины

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

И хотя данная книга не представляет собой курс лекций, не будем отступать от традиций.


Глава 1. Драйверы. Общие понятия и термины :: Драйверы: крупный план. Unix-Linux, DOS-Windows

Драйверы: крупный план. Unix-Linux, DOS-Windows

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

Концепция драйвера как отдельного сменного модуля оформилась не сразу. Некоторые версии UNIX и по сию пору практикуют полную перекомпиляцию ядра при замене какого-либо драйвера, что совершенно не похоже на обращение с драйверами в Linux, Windows и MS DOS. Кстати, именно MS DOS ввела в массовое обращение понятие драйвера, как легко сменяемой насадки, позволяющей моментально (сразу после очередной перезагрузки) улучшить качество жизни пользователя: шрифты на мониторе радуют глаз, емкость дискеты возросла вдвое, в принтере наконец-то поселилась кириллица и т.п.

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

Наблюдение 1. В операционных системах MS DOS, Windows, Unix и всех клонах Linux принят способ работы с драйверами как с файлами. То есть при доступе к драйверу используются функции либо совпадающие (лексически), либо весьма похожие на функции для работы с файлами (open, close, read, write, CreateFile...).

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

Как ни удивительно покажется это рядовому пользователю операционной системы Windows, но и в ней предлагается точно такой же механизм. Для доступа к драйверу из своего приложение пользователь прибегает к помощи функции CreateFile (это могла бы быть функция OpenFile, но это название морально устарело, поскольку использовалась в старых 16-тиразрядных версиях Windows). Правда, имя файла, который предполагается "открыть", выглядит странно, и на жестком диске такого файла отыскать невозможно - он существует лишь в недрах операционной системы и выглядит, например, как "\\\\.\\myDevice". (Операционная система понимает его как символьную ссылку для идентификации конкретного драйвера, привлекаемого к работе.) И хотя дальнейшие операции, сформулированные создателем пользовательского приложения как вызовы read()-write(), все-таки преобразуются операционной системой в специальные запросы к драйверу, необходимо признать: формально процесс похож на работу с файлом.

Наблюдение 2. Драйверы стали легко заменяемой запасной частью в операционной системе. Если раньше и были различия между продуктами Microsoft и юниксоидными системами (драйверы в операционных системах Microsoft изначально были "подвижно-сменными", но в UNIX и ранних версиях Linux при их замене надо было заново выполнять перекомпиляцию ядра), то сейчас такие различия исчезли. При сохранении некоторых особенностей инсталляции, драйверы теперь повсеместно могут быть удалены/добавлены в систему редактированием одной записи в специальных системных файлах. Более того, загрузка "по требованию" (по запросу пользовательской программы) становится практически общей чертой Windows/Unix/Linux. Даже операционные системы реального времени, например, QNX также используют методику сменных драйверов.

Наблюдение 3. Концепция существования режима ядра (с большими функциональными возможностями и относительной бесконтрольности) и пользовательского режима (с жестким контролем со стороны системы) присутствует в Windows/Unix/Linux с незапамятных времен. Если внимательно посмотреть на то, как в Linux реализуется драйвер, то увидим, что это всего лишь модуль ядра, который имеет некое (дополнительное) отражение в виде файла в директории /dev/. Если посмотреть теперь на драйвер (режима ядра) в операционной системе Windows, то становится понятно: это не просто драйвер, это возможность войти в режим ядра со своим программным кодом. От судьбы не уйдешь. Microsoft не предоставила явной возможности создавать модули ядра, однако закрыть эту брешь в виде драйверов - невозможно! В великолепной книге Свена Шрайбера "Недокументированные возможности Windows 2000" как раз эксплуатируется эта "черта личности" драйвера Windows.

Завершая мини-экскурс в сравнительный анализ драйверов разных популярных ОС, нельзя не упомянуть и об общем для всех систем механизме воздействия на драйвер при помощи IOCTL запросов (работа с IOCTL в Windows будет подробно рассмотрена в главе 8).


Глава 1. Драйверы. Общие понятия и термины :: Словарь разработчика драйвера

Словарь разработчика драйвера

Впервые открывая документацию, поставляемую вместе с пакетом Microsoft DDK, новичок неожиданно обнаруживает, что понять ее практически невозможно - периодически на форумах в Интернете возникают удивленные сообщения об этом феномене. Между тем, никакого феномена нет. Документация DDK - это весьма лаконичное "издание" справочного характера (с коварным множеством "любезных" переходов по ссылкам), и изучение предмета по нему во многом похоже на изучение медицины по энциклопедии.

Для русскоязычного читателя-новичка, даже хорошо владеющего языком оригинала, положение усугубляется тем, что для многих терминов отсутствуют устоявшиеся отечественные аналоги. Особенно печально положение некоторых терминов, представленных в оригинале словосочетаниями. Порой, при самом добросовестном переводе так и не получается хорошего определения, поскольку добросовестный 'пословник' дает смысловые оттенки, как раз затрудняющие понимание внутренней логики предмета обсуждения (как, например, это происходит с термином 'dispatch routine').

Словарь терминов, который приводится ниже, призван уравнять шансы читателя в борьбе со сложностью материала и дать стартовые сведения новичкам. Статьи этого мини-словаря расположены в порядке, при котором более заметны внутренние взаимосвязи терминов. Некоторые термины, как, например 'IRP', переведены, но далее в книге будут использоваться в оригинальном виде, который компактнее выглядит и обеспечивает привыкание к синтаксису будущего программного кода.

Abstraction

Абстракция, представление сложного предмета искусственно созданным формальным описанием. Абстракция позволяет отойти от рассмотрения некоторых излишне конкретных вопросов реализации своего прототипа. Абстракции, вводимые Microsoft DDK, возникли, главным образом из стремления облегчить переносимость кода на другие аппаратные платформы (обеспечение HAL) и стремления уберечь разработчика драйвера от необходимости вникать в тонкости постоянно меняющихся версий аппаратного обеспечения на каждой конкретной платформы (например, объект адаптера позволяет абстрагироваться от реализаций контроллеров DMA).

Structure

Структура, тип данных языка С. Состоит из простых типов данных (char, int и т.п.) и вложенных структур или объединений (union), Например, тип данных LARGE_INTEGER иногда (файл ntdef.h) определяется как структура, состоящая из одного поля:

typedef struct _LARGE_INTEGER
{
    LONGLONG QuadPart;
} LARGE_INTEGER;

Union

Объединение, тип данных языка С. Состоит из простых типов данных (char, jnt и т.п.) и вложенных структур или объединений. Реализует доступ к одной и той же области памяти, как к данным разных типов. Например, тип данных LARGE_INTEGER может быть определен (в файле ntdef.h это выполняется при помощи условной компиляции) следующим образом:

typedef union _LARGE_INTEGER
{
    struct { ULONG LowPart;
         LONG HighPart;
           } u;
    LONGLONG QuadPart;
} LARGE_INTEGER;

Тип данных LARGE_INTEGER используется, например, в вызове KeSetTimer для установки таймера. Для того чтобы облегчить установку значений такого типа, можно применять вызов RtlConvertLongToLargeInteger, который скроет от разработчика, как конкретно реализован тип LARGE_INTEGER.

LARGE_INTEGER interval = RtlConvertLongToLargeInteger(100*10);

Здесь и далее обращения к системным функциям (типа RtlXxx, KeXxx, loXxx, read, CreateFile и т.п.) будут называться вызовами, а в тексте они будут обозначаться жирным шрифтом.

Object

Объект. В программировании драйверов объект всегда является структурой или объединением, с которым связана одна из абстракций. Например, объект устройства - это всего лишь структура языка С. Однако она заполнена такими данными и на нее возложена такая логическая нагрузка, что все это позволяет говорить об этой структуре — почти что — как об устройстве.

В режиме ядра имеются трудности с реализацией трюков С++ (оператора new, позднего связывания, идентификации типов во время выполнения и виртуальных методов), следовательно, и основных приемов объектно-ориентированного программирования (ООП). Соответственно, и объекты здесь "ненастоящие". Объекты режима ядра роднит с "настоящими" объектами (в смысле ООП) практически только одно обстоятельство: к каждому объекту прилагается набор функций, и фирма Microsoft рекомендует работать с объектами ядра только при помощи этих специализированных функций. Этим Microsoft достигает решения трех задач. Во-первых, скрывается внутренняя структура объектов (которая в будущем может модифицироваться разработчиком операционной системы или иначе реализовываться на разных платформах). Во-вторых, становится возможным ограничить пределы вмешательства программиста в жизнь ядра, что разработчик ОС считает потенциально опасным. В третьих, программист действительно делает меньше ошибок.

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

Kernel mode

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

mov dx,0378h
out dx,ах

то не случится ничего страшного. Но после такого поступка в Windows XP на экране монитора непременно появится сообщение, подобное следующему:

Рис. 1.1
Исключение при выполнении привилегированных инструкций пользовательским приложением..

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

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

User mode

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

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

Callback, callback function

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

Context

Контекст. Этот термин употребляется программистами для обозначения двух существенно различающихся явлений.

    1. Когда производится регистрация callback функции, производится определения параметра, который она получит в качестве аргумента при вызове. Как правило, это указатель на буфер с данными, которые callback функции необходимо знать, чтобы ориентироваться, зачем же ее вызвали. В драйверах чаще всего в роли контекста (контекстного указателя) выступает указатель на структуру описания устройства или структуру описания расширения устройства, например при регистрации DpcForISR (см. ниже). Именно так чаще всего и следует трактовать словосочетание "in device context" - в контексте устройства, иными словами: применительно к данному устройству.

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

Вольное, но все еще допустимое, значение слова "контекст" подразумевает некие признаки потока, вызвавшего одну из процедур драйвера (Driver Routine), которые перешли на вызванный код. С этой точки зрения, код драйвера режима ядра может выполняться в одном из трех контекстов:

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

Приняв это определение, несложно понять, почему становится возможным благополучное разрешение следующей ситуации. Предположим, в некотором драйвере рабочая (dispatch) процедура, предназначенная для обработки IOCTL запросов от пользовательских приложений, получает при методе буферизации METHOD_NEITHER виртуальный адрес буфера с пользовательскими данными (или для пользовательских данных). Этот адрес поступит в драйвер в том самом виде, как он был виден в пользовательском приложении (с адресом меньше 0x80000000, например, 0x00012A0). Однако этот виртуальный пользовательский адрес имеет смысл только в адресном пространстве вызывающего пользовательского приложения (такое же число в качестве адреса в другом приложении пользовательского режима указывало бы на совершенно другую область памяти) - и это одно из неотъемлемых свойств виртуальной адресации. Должен произойти крах системы?

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

Routine

Процедура. Строго говоря, под процедурой в программировании (начиная с "древнего" языка Fortran) понимается модуль, получающий через заголовок параметры и ничего не возвращающий, в отличие от функции. В языке С таких традиционно понимаемых процедур нет - он привык обходиться одними функциями (правда, "старого типа" процедуру легко можно представить функцией типа void). В результате "высвободилось" слово 'процедура'. Разработчики драйверов (как и многие программисты С и С++) стали позволять себе следующую вольность: вместо слова "функция" произвольно применяются и "функция", и "процедура" (впрочем, как и слово "вызов"). Поэтому, встречая в тексте книги слово "процедура", следует его понимать исключительно так: функция языка С. То же относится и ко всей документации на английском языке (относительно слова "routine").

ISR, Interrupt Service Routine

Процедура обслуживания прерываний. Функция, которую драйвер регистрирует для того, чтобы она получала управление в момент, когда аппаратура, обслуживаемая драйвером, передала сигнал прерывания. Задача этой функции выполнить некоторую самую минимальную работу и зарегистрировать callback функцию, называемую процедурой отложенного вызова для обслуживания прерывания (часто обозначается именем DpcForISR, однако автор драйвера может дать ей любое имя). Если учесть, что в операционной системе Windows на типовом компьютере ежесекундно "происходит" от 100 до 600 прерываний, то станет понятно, почему так вредно задерживаться на высоких приоритетных уровнях, которые имеют ISR функции.

DpcForISR, Deferred Procedure Call for Interrupt Service Routine

Процедура отложенного вызова для обслуживания прерываний. Функция, которую драйвер регистрирует в момент работы ISR-процедуры для выполнения основной работы при получения сигнала прерывания от устройства. Сама процедура ISR работает при очень высоком приоритете, так что задержка внутри нее может привести к серьезной деградации системы, поэтому длительные операции следует "скидывать" процедуре DpcForISR (далее в тексте практически всегда будет использоваться именно это имя для данной функции, однако автор драйвера может присвоить ей любое название).

Deferred Procedure Call

Процедура отложенного вызова. Функция, которая будет вызвана позже (можно считать, "в более спокойной обстановке"). Из таких функций составляется очередь, чем ведает Менеджер (Диспетчер) ввода/вывода. Одно из применений этого типа функций уже упомянуто выше (DpcForISR), o некоторых других будет рассказано чуть позже. Для учета DPC процедур операционная система поддерживает DPC объекты.

IOManager

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

Помимо ДВВ в операционной системе выделяются и другие "сгустки" кода, которые обслуживают какую-нибудь часть ее ресурсов и называются Менеджер или Диспетчер, например, Менеджер Памяти, Диспетчер Объектов, Менеджер Энергопотребления (Power Manager), PnP Менеджер, Менеджер Безопасности (Security Manager) и т.п.

IRP, Input/output Request Packet, IRP request, IRP packet

Пакет запроса на ввод/вывод. IRP запрос, IRP пакет. Равноценные переводы, поскольку обращение ДВВ к драйверу есть запрос посредством IRP пакета, a IRP пакет не имеет смысла, кроме как в контексте обращения с каким-то запросом к драйверу (либо от ДВВ, либо от другого драйвера, но, опять-таки, при посредничестве ДВВ). Сам по себе, пакет IRP есть структура, состоящая из фиксированной части и изменяющейся части, носящей название стека IRP или стека ввода/вывода (IO stack). В случае, если драйверы поручают друг другу обработку запроса, то информация, которая меняется при "блуждании по инстанциям", сохраняется в этой изменяемой части IRP.

IO stack location

Ячейка стека ввода/вывода в пакете IRP. Одна позиция в изменяемой части пакета IRP, называемой стеком ввода/вывода IRP пакета. Ячейка стека сама является составной структурой. Если драйверы объединены в цепочку (называемую стеком устройств, Device Stack(1)), то, как правило, число ячеек стека IRP равно числу устройств в Device Stack перед данным устройством (возможны варианты). Собственно, ячейки стека IRP и предназначены для хранения "переменной" информации при хождении пакета IRP по стеку драйверов (или стеку устройств - на некотором этапе разработчики драйверов перестают делать различия между этими словосочетаниями). Однако хотя информация и "переменная", но имеет вполне определенный формат. При путешествии по процедурам драйвера, те извлекают из "своей" ячейки стека ввода/вывода полезную информацию. В некоторых случаях, передавая IRP пакет вниз по стеку устройств (драйверов), драйверы могут и сохранять там, в пределах "своей" ячейки, некоторые текущие данные - если ожидают, что получат этот IRP пакет при его обратном движении по стеку устройств.

Dispatch Routines

Рабочие процедуры. Функции, которые регистрируется в вызываемой самой первой процедуре драйвера (DriverEntry). Регистрация производится путем заполнения элементов массива MajorFunction (указатель на начало этого массива DriverEntry получает косвенно через аргументы своего вызова). Индексом в этом массиве являются коды IRP_MJ_Xxx, то есть описанные числами типы пакетов IRP. Если драйвер считает необходимым обрабатывать IRP запросы какого-либо типа, то в соответствующем элементе массива MajorFunction он регистрирует соответствующую функцию (записывает ее адрес). Диспетчер ввода/вывода, ориентируясь на заполнение этого массива, вызывает нужные функции драйвера - dispatch routines. Смысл данного словосочетания - "диспетчеризуемые" процедуры драйвера (а не диспетчерские!), что правильнее будет заменить на "рабочие процедуры" (функции) драйвера. Поскольку вне драйвера важны только адреса рабочих процедур (которые и регистрирует функция DriverEntry), то все рабочие процедуры драйвера могут иметь совершенно произвольные имена. Редкие исключения составляют обязательные имена функций в некоторых специальных драйверах, например, видео.

Major IRP Code

Основной код IRP пакета. Число, которое обозначает назначение пакета IRP, а значит и основной смысл данного обращения к драйверу. В пакете Microsoft DDK каждое такое число имеет еще и символьное обозначение (установленное через #define директиву). Для наглядности в литературе всегда используются присвоенные таким образом имена. Например, код IRP_MJ_DEVICE_CONTROL имеет IRP пакет, который поступил в драйвер (разумеется, из кода Диспетчера ввода/вывода) в результате того, что пользовательское приложение вызвало функцию DeviceIoControl (см. пример драйвера в главе 3). Пакеты IRP, соответствующие вызовам функций чтения или записи (read, write) имеют коды IRP_MJ_READ, IRP_MJ_WRITE.

IOCTL

I/O ConTroL code. Код управления вводом/выводом. Позволяет обращаться к драйверу с запросами, отличающимися от операций чтения и записи в устройство (хотя и они легко реализуются через IOCTL запросы). Разработчик драйвера имеет возможность создавать свои собственные коды IOCTL. Данный код является одним из аргументов функции пользовательского режима DeviceIoControl (в приложениях пользовательского режима). Поступающий в драйвер в результате работы этой пользовательской функции и Диспетчера ввода/вывода пакет IRP будет иметь код IRP_MJ_DEVICE_CONTROL, а одним из внутренних параметров данного IRP пакета будет указанный в вызове функции DeviceIoControl код IOCTL.

Minor IRP Code

Младший код IRP пакета. Часто значение основного кода IRP пакета обозначает слишком большой диапазон проблем. Для уточнения были придуманы младшие коды IRP запросов, которым даны имена IRP_MN_Xxx (бывают случаи применения и третьего уровня детализации). Например, запрос типа IRP_MJ_PNP, который обязаны обрабатывать все драйверы PnP устройств, слишком обширен, и его конкретизирует, например, младший код IRP_MN_STOP_DEVICE.

Далее в тексте основной код IRP запроса будем называть просто код IRP, а младший код — суб-код IRP.

DriverEntry

Процедура DriverEntry. Функция драйвера, которая будет вызвана первой при его загрузке. Единственная функция драйвера, которую все разработчики предпочитают называть именно так (поскольку изменение стоит существенных и, в общем-то, бессмысленных усилий). Данная функция выполняется регистрацию основных процедур, в том числе рабочих (Dispatch Routines, см. выше). Для WDM и не-WDM драйверов задачи, которые должна решить эта функция, немного различаются. Функция DriverEntry выполняется на уровне IRQL равном PASSIVE_LEVEL и даже может быть размещена в странично организованной памяти (путем специальных директив для редактора связей, линкера).

WDM, Windows Driver Model

Драйверная модель для Windows. Ориентирована на устройства, поддерживающие спецификацию PnP (в особенности, самоидентификацию — сообщение своих идентификационных номеров — при подключении к соответствующей шине). В драйвер WDM модели введены новые рабочие процедуры, которые отражают стадии подключения-обнаружения PnP устройства, события в изменении энергоснабжения и факт отключения устройства. "Правильное" PnP устройство обнаруживается шинным драйвером (той шины, к которой оно подключается), затем о нем узнает PnP Менеджер, после чего и загружается драйвер — в соответствии с полученными идентификаторами. Если WDM драйвер "правильный", то он должным образом подключает себя к стеку драйверов на этой шине. После этого почти все в дальнейшей жизни драйвера зависит от обращения с IRP запросами к драйверам, подключившимся к стеку ранее (начиная от запросов о конфигурации шины и заканчивая запросами к "своему" устройству). Драйвер модели WDM использует только определения, доступные из файла wdm.h, в результате чего не может использовать "полупартизанские" функции прежних NT-драйверов (функции типа HalGetBusData и т.п.). Драйверы модели WDM зачастую полностью (вплоть до бинарной формы) совместимы для использования в Windows 98, Windows Me, Windows 2000, Windows XP и Windows Server 2003, хотя бывают и исключения.

Разумеется, можно не поддерживать обработку всех требуемых для PnP драйверов запросов, что вполне приемлемо для драйверов, которые нужны лишь в качестве окна в режим ядра. Однако для таких исследований проще использовать драйверы "в-стиле-NT" (NT style или legacy драйверы, о чем рассказывается ниже).

Layering

Многослойность. (Следовало бы перевести этот термин как "слоирование" — "насильственная многослойность", но такого слова нет в русском языке). Поддерживаемая моделью WDM возможность реализовывать стековое соединение между драйверами. Находясь в стеке, верхний драйвер (подключившийся к стеку позднее) имеет возможность адресовать/переадресовывать IRP запросы нижним драйверам (находящимся в стеке до него). Абстракциями, которые выступают действующими лицами в обменах запросами, на самом деле являются не драйверы, а объекты устройств - именно они соединяются в стек и являются адресатами в получении и передаче пакетов IRP.

По мнению автора, главный выигрыш от такого подхода достается разработчикам драйверов устройств, подключаемых к шинам. Например, если взять шину USB, то общение с внешним USB устройством сводится к "правильному разговору" разрабатываемого драйвера (для нового внешнего устройства) с шинным драйвером USB через специальные запросы. Альтернатива этому несложному подходу — работа по самостоятельному программированию всех существующих в мире вариантов USB хост-контроллеров. (Разумеется, многослойность дает еще возможность мельчить "крупногабаритные" запросы ввода/вывода, вести подсчеты и частично изменять свойства нижних драйверов в глазах пользователей, "наблюдающих" сверху.)

AddDevice

Функция, которую должны поддерживать WDM драйверы, для того, чтобы в своем составе иметь обработчик, который будет вызван в момент обнаружения устройства. Регистрация AddDevice выполняется в момент работы DriverEntry. Сама же процедура AddDevice в WDM драйверах, как правило, выполняет такую важную работу, как создание объекта устройства и подключение его к стеку устройств в момент вызова. (Справедливости ради, следует отметить, что если не-WDM драйвер зарегистрирует процедуру AddDevice, то ничего страшного не произойдет — и ее в соответствующий момент вызовет ДВВ, правда это событие уже не будет исполнено того смысла, как это имеет место в случае WDM драйверов для PnP устройств.)

Device Instance

Физический аппаратный компонент. Экземпляр устройства, возможно, один из нескольких других однотипных устройств, обслуживаемых данным драйвером. Это может быть как геометрически большой и автономный фрагмент (монитор или привод CD-ROM), так и мелкий, физически неотделимый от других, несомненно солидных устройств внутри современного компьютера, компонентов, например, набор микросхем на материнской плате для обслуживания PCI шины.

Device Object, PDO, FDO

Объект устройства, объект физического устройства (Physical Device Object), объект функционального устройства (Functional Device Object). Абстракции, созданные для представления дееспособных единиц в драйверной архитектуре. Объекты физических устройств (далее будем обозначать их аббревиатурой 'PDO') создаются шинными драйверами, когда те находят подключенные к шине реальные (физические) устройства - для отображения самого факта их существования. Именно к PDO объектам, принадлежащих шинному драйверу, будут подключаться объекты функциональных устройств (далее будем обозначать их как 'FDO' либо 'объект-устройство') полноценных WDM драйверов. Объект FDO создается собственно драйвером обнаруженного устройства (при помощи вызова IoCreateDevice). Объект FDO может иметь свою "символьную ссылку" (symbolic link), по которой будет получать запросы от клиентов своего драйвера. Следует обратить внимание на то, что IRP запросы получает не драйвер (хотя, и он тоже), а именно FDO объект, созданный в драйвере: в каждую рабочую процедуру (Dispatch Routine) драйвера в качестве аргументов передается не только указатель на пакет IRP запроса, но и указатель на FDO, которому адресован этот запрос. В результате подключения FDO-объекта (что внутри драйвера устройства) к PDO-объекту (что внутри шинного драйвера) создается впечатление, что один драйвер подключается к другому. Отсюда и берет начало жаргонизм "стек драйверов"!

Те, кто не боится трудностей, может, например, создать в своем драйвере несколько FDO и получить для каждого символьные ссылки. Чего этим можно добиться? Например, можно предоставлять разным клиентам драйвера функционально разные услуги по доступу к обслуживаемому данным драйвером устройству — в зависимости от того, какую символьную ссылку использовал новый клиент при открытии драйвера.

Драйвер в-стиле-NT, который не обслуживает реального устройства или обслуживает не-PnP устройство, не получает никаких PDO ни от каких шинных драйверов. Однако он все равно создает FDO объект, хотя бы только для того, чтобы иметь символьную ссылку (symbolic link) для связи с внешним миром. Возможны возражения, что существуют способы, как "добраться" до драйвера и без символьной ссылки — по зарегистрированному им при помощи вызова IoRegisterDeviceInterface интерфейсу (в большинстве случаев — путь для тех, кто коллекционирует трудности), однако, в данном случае для регистрации интерфейса опять-таки необходимо иметь указатель на PDO объект! Иными словами, трудно представить себе драйвер, которому не нужен был бы свой FDO объект, — этакий живущий сам по себе обрывок кода режима ядра.

Device Extension

Расширение объекта устройства. Структура, конечный вид которой определяется автором драйвера (иными словами, он может делать в ней все что угодно). Данная структура создается в самый момент создания объекта устройства при помощи системного вызова IoCreateDevice, одним из аргументов которого является ее размер — в момент создания объекта устройства система выделяет место (в нестраничном пуле памяти) и под эту "авторскую" структуру. В программировании драйверов плохим тоном считается создание переменных, глобальных для всего кода драйвера. Взамен, рекомендуется размещать эти претендующие на глобальность данные в структуре расширении устройства. Указатель на структуру расширения можно найти в объекте FDO сразу же после его создания по вызову IoCreateDevice.

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

Symbolic Link

Символьная ссылка, символическая ссылка, строго говоря — символьная связь. В области разработки драйверов — файловый объект с особыми свойствами. Выполняя операцию по открытию такого файла (CreateFile, ZwCreateFile), клиент драйвера получает к нему доступ (в виде ненулевого дескриптора, HANDLE), причем все запросы клиента будут адресованы как раз тому FDO объекту, с которым соотнесена данная символьная ссылка (созданная по запросу драйвером в результате вызова IoCreateSymbolicLink).

Device Stack, Driver Stack

Стек устройств, стек драйверов. Страшилка для легковерного новичка, которому может показаться, что все объекты устройств (PDO и FDO) всех устройств, создаваемые в драйверах, включаются в один стек, в соответствии с многослойным подходом WDM модели. Как там только не пропадают IRP пакеты и еще остается какое-то быстродействие?!

Между тем все устройства образуют довольно объемное дерево устройств (а вовсе не единый сквозной стек!) — и его можно рассмотреть, если обратиться в программе DeviceTree, поставляемой в составе пакета Microsoft DDK. B этом дереве, например, из точки, которую можно назвать "PCI шина" вырастают ветви шины USB, шины FireWire, ISA шины, устройств SCSI протокола. На самом деле, стеком следует считать относительно короткий отрезок соответствующей ветви, начиная от драйвера и его объекта устройства, куда вошел IRP запрос (например, от пользовательского приложения), до места, где запрос окончательно был обработан. Можно считать, что в случае монолитного драйвера стека устройств нет вовсе, поскольку драйвер сам занимается обработкой всех своих IRP. В случае же WDM драйвера для устройства на шине USB — стек от объекта устройства внутри этого драйвера "дотягивается" до объекта устройства внутри драйвера хост-контроллера USB.

Стек устройств формируется динамически по мере загрузки драйверов и подключения к нему устройств. Следовательно, подключение FDO нового драйвера к какому-нибудь драйверу в середине стека по окончании его формирования означает лишь одно — это будет боковой "сучок" и никакие запросы от верхних драйверов через него не пойдут (это замечание к популярной теме фильтр-драйверов).

Monolithic Driver

Монолитный драйвер. Драйвер, который не участвует ни в одном стеке устройств, получая и завершая обработку всех поступающих IRP пакетов, самостоятельно обращаясь к своему обслуживаемому устройству. Несложно представить себе монолитный драйвер, который обслуживает старые устройства (не-PnP) или служит лишь в качестве средства доступа к функциям режима ядра (для исследовательских целей). Однако практически невозможно представить современный драйвер для устройств SCSI, USB и FireWire, который был бы реализован как монолитный в прежнем смысле этого слова. В некоторых источниках информации по драйверам предпринята попытка модификации этого понятия. В них монолитным WDM драйвером (!) называется драйвер, который сам получает свои IRP запросы и доводит их до шинного драйвера, на чем его работа по общению с устройством завершается (остается лишь перехватить обратный отклик и его интерпретировать).

Legacy Driver, NT Style Driver

Унаследованный драйвер (устаревший драйвер), драйвер в-стиле-NT. Драйвер, который не является WDM драйвером, работает не с PnP устройством (если вообще работает с реальными устройствами) и не участвует в обмене данными по поводу изменений в энергоснабжении. При компиляции использует только определения из файла ntddk.h, отчего может в полной мере пользоваться устаревшими функциями HalGetBusData, HalGetInterruptVector и т.п., но зато должен надеяться только на свои способности, поскольку у него нет могущественных прародителей в лице шинных драйверов, способных предоставить поддержку.

"Правильный" драйвер в-стиле-NT может быть запущен и остановлен при помощи программы Monitor (из состава пакета Numega Kernel Driver) или процедурами SCM Менеджера. При наличии PnP вкраплений (в частности, из-за наличия зарегистрированной процедуры для обработки IRP_MJ_PNP запросов) эта возможность исчезает.

Следует обратить внимание, что при компиляции кода с использованием ntddk.h, не только становятся недоступными отдельные функции, открытые раннее (для модели WDM), но и изменяется назначение некоторых все еще доступных функций и полей внутри доступных структур. Тем не менее, подключение такого типа драйверов к другим драйверам по-прежнему возможно (при помощи вызовов IoAttachDeviceToDeviceStack или IoAttachDevice).

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

IRQL, Interrupt ReQuest Level

Уровень приоритета выполнения. Термин, исторически доставшийся от задач по обслуживанию прерываний (IRQ), но теперь связанный не только с ними. Приоритеты, принятые для программного кода, работающего в режиме ядра. Планирование потоков с приоритетами IRQL хотя бы на 1 выше минимального (PASSIVE_LEVEL) сильно отличается от планирования потоков в пользовательском режиме. В режиме ядра поток, работающий при некотором приоритете IRQL может быть прерван только для выполнения работы потоком с более высоким IRQL. Даже поток с равным приоритетом IRQL должен дожидаться естественного окончания работы своего "равноправного коллеги". Поток может самостоятельно повысить свой IRQL, однако, величина повышения в некоторых версиях Windows не произвольна. Например, работая на уровне PASSIVE_LEVEL (0), поток может получить от операционной системы Windows XP согласие только на уровень IRQL равный 10 (для сравнения, DISPATCH_LEVEL имеет численное значение 3, a INTERRUPT_LEVEL численно равен 13).

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

Приоритеты IRQL называются еще приоритетами диспетчеризации. Лишь внутри самого низкого приоритета IRQL, равного PASSIVE_LEVEL имеется градация потоков по приоритетам планирования (scheduling), часть из которых могут иметь потоки приложений пользовательского режима.

IRQ, Interrupt Request Line

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

DIRQL

Уровни аппаратных прерываний. Обработка сигналов прерываний (IRQ), поступающих от реальных устройств, считается важным делом и должна проходить при повышенных уровнях приоритета режима ядра (IRQL). Поэтому данным уровням дано особое название Device IRQL, то есть DIRQL. Уровни приоритетов DIRQL в системах на базе Intel занимают верхние 16 IRQL уровней в операционной системе.

Polling

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

Virtual Memory

Виртуальная память.

Рассмотрим пример. Предположим, некая фирма имеет оплачиваемое пространство на складе своего оптового поставщика. Учет ее имущества производится хозяином склада в следующей форме. Каждая коробка имеет двузначный номер ПК (Полка/Коробка), где К — это действительно номер коробки 0..9 на соответствующей полке, а вот с номером полки дело обстоит сложнее. Для того чтобы узнать сквозной (физический) номер полки на складе, необходимо заглянуть в журнал выделения полок фирмам и получить из него настоящий номер полки. Правда, склад в нашем примере небольшой — всего десять полок!

Неудивительной будет ситуация, если через несколько дней работы по предложенной системе коробка, относящаяся к рассматриваемой фирме, с виртуальным номером 19 будет находиться на полке 2, притом, что коробка номер 20 — уже на полке 8. Но нумерация коробок осталась непрерывной, что очень нравится владельцам коробок!

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

Аналогичная методика принята в современных операционных системах. Преимущества ее использования огромны. Приложениям не нужно ожидать получения непрерывных пространств памяти — достаточно наличия фрагментов стандартной длины. Более того, если какое-либо приложения имеет низкую активность, можно его виртуальную память "сбросить" в файл на жестком диске (этот процесс называется swapping), a физическую память предоставить активным приложениям. (То есть — хранить коробки в подвале, но к моменту приезда клиентов — элегантно размещать их на полках склада. При определенной сноровке можно внушить каждому клиенту с большими запасами, что склад используется только для хранения его товара.)

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

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

System Paging File

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

User Space

Пользовательское пространство памяти. Область виртуальной памяти, выделенная для работы пользовательских приложений. Обычно составляет 2 гигабайта и ограничена сверху адресом 0x79999999 (в серверной конфигурации возможны варианты, когда, в результате регулировки настроек операционной системы, приложения пользовательского режима получают возможность использовать 3 гигабайта виртуальной памяти, оставляя под системные нужды лишь 1 гигабайт). Все сказанное относится к 32-разрядным версиям Windows. B 64-разрядных версиях Windows XP и Windows Server 2003 ситуация более сложная, хотя для 32-разрядных приложений и там ничего не меняется. Подробнее вопросы адресации в 64-разрядных версиях Windows будут рассмотрены в главе 4.

Pool Memory

Память в пулах (страничном или нестраничном). Области в пространстве памяти ядра (адреса выше 0x80000000 для стандартной конфигурации системы — поскольку для сервера можно определить иначе), в которых можно динамически выделять (получать, аллокировать) и освобождать (деаллокировать) области памяти. Менеджер Памяти (Memory Manager) различает два типа пулов, к которым драйвер может получать доступ при помощи вызовов функций исполнительного блока Ex(ecutive):

  • Paged pool — страничный пул, в котором каждый процесс имеет собственный набор РТЕ (Page Table Entries) — записей в таблице страниц.
  • Nonpaged pool — нестраничный пул, в котором все процессы совместно используют один набор РТЕ — записей в таблице страниц.

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

Paged Memory, Paged Pool

Странично организованная память, страничный пул, страничная память.

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

Однако при работе в режиме ядра кода, имеющего IRQL уровень приоритета равный или выше DISPATCH_LEVEL, возникает катастрофическая ситуация. Если обстоятельства сложились так, что этот программный код должен получить доступ к виртуальной странице, сброшенной на диск, и эта страница могла бы быть размещена в физической памяти усилиями системного обработчика исключения типа "page fault", но... Но уровень IRQL обработчика ниже приоритета DISPATCH_LEVEL, и система не дает ему возможности приступить к работе, вместо этого прекращая всю работу системы! Обращаться к страничной памяти (если не предпринимать специальных мер) и производить получение новых областей категорически рекомендуется только из кода, работающего на IRQL уровнях PASSIVE_LEVEL или APC_LEVEL.

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

__try
{
    char x=*(char*)0x0L;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
    DbgPrint("Exception detected in my driver");
};

Однако такое решение позволяет лишь уйти от конкретной ошибки (да и работает только на IRQL уровне PASSIVE_LEVEL). Получение доступа к нужным данным так и остается нерешенной задачей. Другое, более правильное решение предлагается ниже.

Nonpaged Memory, Nonpaged Pool

Нестранично организованная память, нестраничный пул, нестраничная память.

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

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

Объем памяти, которую система отводит под нестраничную память, ограничен. Даже при наличии достаточного объема физической оперативной памяти Windows 2000 позволяла иметь до 660 Мбайт нестраничной памяти, а 32-разрядная версия Windows XP до 1,3 Гбайт. Использование физической памяти свыше 4 Гбайт в 32-разрядных версиях Windows (даже если это позволяет аппаратура) производится через механизм физических адресов, что определяется параметром /РАЕ (разрешающим использование Physical Address Extension через загрузку другой под-версии ядра) в файле конфигурирования процесса загрузки boot.ini, см. главу 4.

Scatter/Gather Problem

Проблема сборки/разборки адресов. Возникла в момент определения методов работы аппаратуры DMA в системах с виртуальной адресацией. В рамках общего подхода Windows, предлагается выполнять разборку непрерывной виртуальной области на локально непрерывные физические фрагменты. Результат помещается в MDL список, специально приспособленный для этого пакет данных. Драйвер получает MDL список и настраивает <своеЛ DMA устройство, так, чтобы оно могло выполнить DMA перенос в/из физически разрывной области клиентских данных (разумеется, используя информацию MDL списка) — как бы по фрагментам./p>

DMA, Direct Memory Access

Метод обмена данными между устройством и оперативной памятью без участия центрального процессора. Процесс DMA переноса данных может протекать либо под управлением самого устройства (bus-mastering DMA или first-party DMA) либо под управлением системного DМА контроллера (slave DMA или third-party DMA).

Access Violation

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

  • Попытка доступа к памяти за пределами доступного текущему процессу адресного пространства (length violation).
  • Ошибочная операция, например, попытка записи на странице "только для чтения".
  • Попытка доступа по запрещенному системой адресу, например, приложениям запрещено обращаться к нижним 64K адресного пространства, что упрощает обнаружение обращений по нулевому указателю.
  • Доступ к странице, которая в данный момент предназначена для использования одним из системных компонентов (хотя и присутствует в физической оперативной памяти).

SEH, Structured exception handling

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

Thread, Thread Object

Программный поток ("нитка"), объект потока.

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

Process, Process Object

Процесс, объект процесса.

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

Affinity

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

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

Synchronization Objects

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

В данную категорию входят События (Event), Мьютексы (Mutex, Mutual Exception — взаимное исключение), Семафоры (Semaphore), Спин-блокировки (Spin Lock) и даже объекты потоков. Не вполне объектами, но элементами синхронизации можно также считать аналог критических секций, что в режиме ядра реализуется при помощи вызовов KeEnterCriticalRegion и KeLeaveCriticalRegion.

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

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

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

PnP Manager

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

Все драйверы должны обеспечивать PnP поддержку. В противном случае будут существенно ограничены PnP поддержка и управление энергопотреблением системы в целом.

Если обратиться к процессу нумерации (enumeration) устройств и воспользоваться программой DeviceTree из состава пакета DDK, то станет очевидно, что во главе этого процесса стоит именно PnP Менеджер режима ядра (см. рисунки 2.6 и 2.7).

Enumeration

Процесс перечисления (нумерации). Обозначает последовательность действий со стороны системных компонентов по обнаружению устройств, реализованных в соответствии со спецификацией PnP. Возможен вследствие того, что устройства предоставляют о себе информацию в виде идентификационных кодов по протоколам, специфичным для каждого типа шины (PCI, USB и т.д.), подробнее см. главу 11.

Enumerator

Системный компонент, осуществляющий процесс перечисления (enumeration). Как правило, в этой роли выступают шинные драйверы, которые сообщают об обнаруженных устройствах PnP Менеджеру, после чего тот либо предпринимает шаги по загрузке и старту соответствующего драйвера (который он определяет по записям в Системном Реестре), либо начинает процесс установки драйвера (если подходящих записей не обнаружено). Нумерация некоторых устройств выполняется шинными фильтр-драйверами, например, ACPI драйвером.

ACPI

Advanced Configuration and Power Interface. Абстрактный интерфейс, определяющий механизмы управления энергопотреблением и конфигурирования аппаратного обеспечения и компонентов операционной системы. Является частью так называемой "Инициативы OnNow".

ACPI Driver

ACPI драйвер. В системах с ACPI BIOS программное обеспечение HAL обеспечивает загрузку ACPI драйвера во время старта операционной системы в корне дерева устройств. Драйвер ACPI функционально прозрачен для других драйверов, которые не должны делать каких-либо допущений о наличии или отсутствии в своих стеках устройств фильтр-объектов от ACPI драйвера. В обязанности ACPI драйвера входит поддержка PnP возможностей системы и управления энергопотреблением, так что результаты работы ACPI драйвера в виде многочисленных PDO или фильтр-объектов можно найти в ветвях дерева устройств (если воспользоваться программой DeviceTree), связанных с реальной аппаратурой.

Filter Device Object

Объект устройства, создаваемый фильтр-драйвером. Имеет следующее формальное отличие — это устройство остается безымянным (драйвер не указывает его имени при создании объекта) и не имеет символьной ссылки. Этот объект устройства, как правило, не предназначен для непосредственного доступа к нему, но после должного подключения его под или над устройством основного драйвера, это фильтр-устройство пропускает через себя все IRP пакеты, на самом деле предназначенные основному драйверу.

Filter Driver

Фильтр-драйвер. Драйвер, предназначенный для выполнения дополнительных манипуляций над IRP пакетами основного драйвера (вплоть до того, что самостоятельно отвергает их), к которому он подключается в стеке либо сверху, Upper Filter, либо снизу, Lower Filter. У одного основного драйвера может быть несколько фильтр-драйверов, но они для основного драйвера как бы не существуют. Как правило, разбиение на основные и фильтр-драйвера делает сам разработчик основного драйвера, определяя порядок их установки и загрузки, обеспечивающий должную конфигурацию стека устройств в этом месте дерева устройств.

HAL, Hardware Abstraction Layer

Слой аппаратных абстракций. Слой программного обеспечения в Windows NT, который призван скрыть специфику аппаратной платформ (Intel32, Intel64, Alpha) от остальных компонентов операционной системы, обеспечивая малые затраты при переносе системы или элементов программного обеспечения. Уровень HAL предоставляет процедуры, которые позволяют абстрагироваться от аппаратных тонкостей, как, например, детали реализации шин ввода/вывода, прерываний, DMA операций и т.п.

Для не-WDM драйверов, например, уровень HAL предоставляет функции для получения информации о подключенных к шинам устройствах. Следует особо отметить процедуру HAL для выполнения отображения прерывания на конкретной аппаратной шине на общесистемный вектор прерывания с соответствующим уровнем приоритета DIRQL.

Персонально драйверам WDM остается лишь скромный набор макроопределений для доступа к портам ввода/вывода.

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

Registry

Системный Реестр. База данных разнообразных настроечных и информационных параметров (вплоть до ключа, использованного при инсталляции данного экземпляра Windows), как для приложений пользовательского режима, так и конфигурационных параметров для всей системы. Файлы, составляющие эту базу, в 32-разрядных версиях операционной системы хранятся в директории %systemroot%\System32\Config\ и защищены системой от изменений. Несмотря на то, что программисты предпочитают обходить стороной это "страшное" место, тем не менее, запись конфигурационных параметров в Системный Реестр является нормальной и рядовой практикой. Такая программа, как Internet Explorer при простом движении курсора мышки по верхней части окна (где размещается меню и кнопки) делает до трех десятков операций над Реестром.

Информация об обнаруженном PnP оборудовании и установленных драйверах также размещается в Системном Реестре.

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

Для редактирования и просмотра содержимого Системного Реестра предназначена программа редактирования Реестра, которая в Windows 2000 запускается командой regedit32, а в Windows XP и Windows Server 2003 командой regedit.

Весь Реестр разделен на разделы. Самые крупные разделы носят названия hive (улей). Различают разделы HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, HKEY_CURRENT_USER, HKEY_USERS. (Здесь НКЕY, очевидно, образовано от HiveKEY.)

Наиболее употребительным для разработчика драйверов является раздел HKEY_LOCAL_MACHINE, который далее будет часто упоминаться в сокращенной форме как HKLM. Он содержит общую информацию об аппаратном обеспечении и операционной системе (в том числе — установленных службах и драйверах). Инсталляция не-WDM драйвера может быть сведена к созданию подраздела в HKLM\System\CurrentControlSet\Services с занесением туда трех-четырех параметров (и их значений) с последующей перезагрузкой, после чего драйвер появляется в системе.

Работа с Системным Реестром через системные функции в Windows NT требует использования кодировки Unicode.

hardware branch

Подраздел Системного Реестра HKLM\Hardware. Расширенное множество, описывающее всю аппаратуру, когда-либо установленную на данном компьютере. Подмножество этого списка, называемое hardware tree, резидентно находящиеся в оперативной памяти, содержит только реально присутствующие в системе устройства.

CurrentControlSet

Текущая работающая конфигурация. Подраздел Системного Реестра HKLM\System\CurrentControlSet, описывающий текущую конфигурацию системы, на самом деле представляет собой информацию из одного из подразделов Системного Реестра, называющихся HKLM\System\CurrentControlSet00X, где X - число от 1 до 3. Информация какого конкретно фрагмента реестра используется в качестве текущей (то есть значение X), можно определить по значению параметра Current в разделе HKLM\System\Select, который является указателем на один из подразделов HKLM\System\CurrentControlSet00X. (В самом деле, если что-то добавить или удалить в подразделе CurrentControlSet, то точно такое же изменение произойдет и в том подразделе CurrentControlSet00X, на который "указывает" параметр Current.)

LastKnownGood

Последняя работающая конфигурация. Подраздел Системного Реестра HKLM\System\CurrentControlSet00X, где X - число от 1 до 3, описывающий состояние системы, когда была выполнена полностью удачная загрузка. Какой же конкретно фрагмент реестра используется в качестве LastKnownGood (то есть значение X), можно определить по значению параметра LastKnownGood в разделе HKLM\System\Select.

Unicode

Двухбайтная кодировка символов алфавитов. Делает возможной поддержку всех языков, имеющих буквенный или слоговый алфавит. Давно и широко применяется в Windows NT. B режиме ядра существует достаточный набор функций для работы со строками Unicode.

DeviceID

Идентификатор устройства. Информация, идентифицирующая устройство. В inf-файле, используемом при инсталляции, а позже — в Системном Реестре информация об устройстве хранится в виде строки, формат которой может быть определен, например, как

 VEN_XXXX&DEV_YYYY&SUBSYS_ZZZZZZZZZ&REV_VV

Где XXXX — идентификатор производителя (VENDOR — поставщик), YYYY — идентификатор устройства, ZZZZZZZZ и W уточняющие параметры. (Заметим, что эти числа хранятся во внутренних регистрах PnP устройства и становятся доступными при его подключении к шине.)

Драйвер шины, к которой подключается устройство, передает идентификатор устройства в PnP Менеджер по запросу IRP_MN_QUERY_ID. PnP Менеджер использует эту информацию для того чтобы определить — какой драйвер следует использовать (если он уже был до этого установлен) или начинает процесс инсталляции драйвера, в результате чего будет создан и соответствующий подраздел в Реестре.

Подробно форматы идентификационных записей будут рассмотрены в главе 12.

Class Driver

Классовый драйвер. Высокоуровневый драйвер, который представляет поддержку целого класса устройств (например, клавиатуры и мыши). При этом классовый драйвер абстрагируется от их аппаратных особенностей, поддержка которых возлагается на драйверы более низкого уровня, общение с которыми происходит при помощи IOCTL запросов, callback функций или функций, экспортируемых этими драйверами нижнего уровня.

Port Driver

Порт-драйвер. Драйвер самого низкого уровня, который отвечает на стандартные системные запросы и, возможно, дополнительно — на специальные IOCTL запросы соответствующего классового драйвера. Порт-драйвер изолирует классовые драйверы от специфики аппаратуры (возможно, при помощи мини-порт-драйвеpa) и синхронизирует их операции.

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

Minidriver

Мини-драйвер. Представляет нечто меньшее, чем "полный" драйвер (являющийся для него оболочкой), и отражает аппаратную специфику обслуживаемого устройства. Реализуется, как правило, в виде динамически загружаемой библиотеки (DLL). Вероятно, самым известным примером мини-драйвера является SCSI мини-порт-драйвер (мини-драйвер для SCSI порт-драйвера). В данном термине в глоссарии MS Windows DDK наблюдается откровенный сбой, поскольку он трактуется как DLL, которая использует (?!) классовый драйвер для завершения своих запросов.

(1) Это совершенно другое понятие, вовсе не стек ввода/вывода IRP пакета!


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Источники информации

Разумеется, главным источником информации и истиной в последней инстанции относительно драйверов Windows следует считать документацию Microsoft, поставляемую в составе пакетов разработки драйверов DDK (Device Driver Kit). Однако сначала о других источниках.


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Печатные издания на русском языке

  1. А.В. Фролов, Г.В. Фролов, Операционная система MS-DOS, том 1, - M., "Диалог-МИФИ", 1991 - 240 с.
  2. Даниель Нортон, Драйвера Windows, ввиду малого тиража на русском языке и давности момента издания невозможно установить выходные данные. Издана около 1995 года.
  3. Свен Шрайбер, Недокументированные возможности Windows 2000 (+CD), - СПб.: Питер, 2002 - 544 с.
  4. Джеффри Рихтер, Windows для профессионалов: Создание эффективных Win32-пpилoжeний с учетом специфики 64-разрядной версии Windows. Пер. с англ. - 4 изд. - СПб.:Питер: М.:Издательство торговый дом "Русская редакция", 2001 - 752 с.
  5. С.И. Сорокина, А.Ю. Тихонов, А.Ю. Щербаков, Программирование драйверов и систем безопасности: Учебное пособие - СПб.:БХВ-Петербург, M.: Издатель Молгачева СВ., 2002 - 256 с.
  6. Ольга Кокорева, Реестр Windows XP, - СПб.:БХВ-Петербург: 2003 - 560 с.


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Издания, которые не были переведены на русский язык

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

  1. Karen Hazzah, Writing Windows VxDs & Device Drivers; Programming secrets for Virtual Device Drivers, 479 pages 2nd Bk&Dk edition (March 1, 1997) CMP Books; ISBN: 0879304383. Книга целиком посвящена разработке VxD драйверов под Windows 95, что позволяет говорить о ней, как о морально устаревшей.
  2. Alessandro Rubini, Jonatan Corbet, Linux Device Drivers, 2nd Edition, 562 pages 2nd edition (June 2001) O'Reilly & Associates; ISBN: 0596000081. Рассматриваются вопросы внутренней организации ядра Linux и создания драйверов как модулей ядра. Программисту в Windows книга может быть интересна для проведения сравнительного анализа программирования драйверов в Windows и Linux (Unix).
  3. Walter Oney, Programming the Microsoft Windows Driver Model, 624 pages Bk&Cd-Rom edition (September 1999), Microsoft Press; ISBN: 0735605882. Основательное издание, последовательно вводящее в программирование драйверов. На сопроводительном диске имеется отличный мастер инициации драйверных проектов на базе MS Visual Studio. Используются приемы программирования С++, особенно во втором (декабрь 2002 года) издании.
  4. Art Baker, Jerry Lozano, Windows 2000 Device Driver Book, The: A Guide for Programmers. 500 pages Bk&Cd-Rom edition (December 15, 2000) Prentice Hall PTR; ISBN: 0130204315. Хорошо соответствует подзаголовку - руководство для программистов. Наиболее подробно из всех зарубежных изданий рассмотрено программирование DMA операций, однако совершенно не рассматривается программирование USB.
  5. Edward N. Dekker, Jozeph M. Newcomer, Developing Windows NT Device Drivers. A Programmer's Handbook, 1227 pages (March, 1999) Addison Wesley Longman, Inc.; ISBN: 0201695901. Замечательная и чрезвычайно объемная книга, посвященная программированию драйверов Windows 2000 (хотя была завершена в момент выпуска ее бета-версии). Затронуто много вопросов, которые можно считать общесистемными. Дублирует много сведений из DDK, однако делает это с большим количеством комментариев. И хотя это та книга, которую должен иметь под рукой разработчик драйверов, ее нельзя считать книгой, которую новичку следует читать первой. Поскольку книга выпушена без CD-ROM, это компенсируется размещением большого количества кода на интернет-сайте одного из авторов.
  6. Chris Cant, Writing Windows Wdm Device Drivers: Covers Nt 4, Win 98, and Win 2000, 540 pages Bk&Cd Rom edition (July 1999) CMP Books; ISBN: 0879305657. Книга касается только драйверной модели WDM и сосредоточена, в основном, на программировании USB (хотя есть пример, связанный и с LPT портом). Из-за своеобразного стиля изложения читатель-новичок, доверившийся этой книге, получит поверхностные знания, особенно если не уделит должного внимания примерам. Следует отметить, что некоторые примеры, связанные с HID USB устройствами плохо работают под Windows XP. Тем не менее, несомненным плюсом данного издания является наличие на прилагаемом CD-ROM прекрасного отладочного средства, известного под названием DebugPrint.
  7. Peter G. Vascarola, W. Anthony Mason, Windows NT Device Driver Development, 684 pages, (November 1998) MacMillan Publishing Company; ISBN: 1578700582. Книга написана ветеранами разработки кода под Windows. И хотя авторы аннотировали ее как "not a cookbook" (не книга рецептов), что подтверждается малым количеством примеров кода и отсутствием CD-ROM, эту книгу ни в коем случае нельзя сбрасывать со счетов, даже учитывая почти полное отсутствие в ней материала по WDM. Отдельные тонкости работы в режиме ядра NT предельно тщательно освещены только в этом издании. Наиболее подробно здесь (из всех упомянутых непереведенных изданий) рассмотрены NDIS, SCSI miniport и видео драйверы.
  8. David A. Solomon, Mark Russinovich, Inside Microsoft Windows 2000 (Microsoft Programming Series) Microsoft Press; 3rd edition (September 2000) ISBN: 0735610215. Далеко небесполезная книга для разработчика драйверов, хотя имеет общеобразовательную направленность для программистов Windows NT, 2000, XP.
  9. Gary Nebett, Windows NT/2000 Native API Reference, 528 pages; MTP; ISBN 1578701996. Справочное руководство по набору Windows Native API функций. Небольшое количество комментариев, немного примеров.


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Материалы из пакетов разработки драйверов третьих фирм

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

  1. Фирма Jungo Ltd. (бывшая Wretch Ltd.), информацию о продуктах которой (WinDriver и KernelDriver) можно найти на интернет-сайте jungo.com.
  2. Фирма Compuware Corporation, распространяющая пакет разработки драйверов DriverStudio. По интернет-адресу compuware.com/products/driverstudio можно обнаружить много технической информации, касающейся программирования драйверов для Windows и Linux.

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


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

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

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

  1. Документация к MS Visual Studio, где описывается, хотя бы использование функций SCM Менеджера, что позволяет динамически загружать некоторые типы драйверов.
  2. Platform Software Development Kit (PSDK), распространяемая как часть подписки MSDN. Содержит весьма полезные программы (в частности, Depends и WinObj).
  3. Собственно пакет Device Driver Kit (DDK).
  4. Материалы ежегодной конференции WinHEC по драйверам.

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


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Документация Microsoft DDK

После инсталляции пакета DDK XP (build 2600) или DDK Server 2003 (build 3790) среди вновь созданных директорий можно обнаружить каталог \help\, в котором размещены файлы-справки, имеющие расширение .chm. При входе через "Пуск-Программы-..." чтение документации становится доступно в режиме, когда возможны переходы по ссылкам между разными файлами. (Таким образом, вся справочная информация выступает сплошным массивом, что вряд ли можно признать методически правильным в период начального ознакомления.) В противном случае, пользователь имеет дело с автономными файлами.

Перечислим наиболее значительные из них.

Gstart.chm | Введение для начинающих (Getting Started)
Kmarch.chm | Основное руководство по драйверной архитектуре Windows, модели WDM, деталям внутреннего устройства Windows NT, которые следует знать разработчику драйверов, справочник по функциям ЕхХхх, КеХхх, HalXxxx, IoXxx, MmXxx, ОЬХхх, PsXxx, RtlXxx, WMI и ZwXxx
Buses.chm | Особенности разработки драйверов устройств, подключаемых к шинам FireWire (IEEE 1394), USB (включая описание URB), SCSI, IDE и PCMCIA
Gloss.chm | Терминологический глоссарий
Install.chm | Освещены вопросы инсталляции драйвера и весьма подробно разобрана организация inf-файлов
Graphics.h | Вопросы проектирования видео драйверов и драйверов для принтеров

Вообще говоря, каждому, кто приступает к использованию пакета DDK, рекомендуется ознакомиться с содержанием всех файлов документации. Причина проста — некоторая информация встречается в не вполне ожидаемых местах, например, правила присвоения PnP идентификаторов устройствам шины PCI и другим устройствам, можно обнаружить в конце файла Appendix.chm.

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


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Online документация Microsoft

На интернет-сайте Microsoft, уже упомянутом выше, хранятся также статьи, посвященные частным случаям проблем, связанных с программированием в Windows при помощи продуктов Microsoft, в частности, связанные с разработкой драйверов при помощи DDK. Статьи, зачастую, возникают как ответы на поступившие вопросы пользователей и имеют последовательно возрастающую нумерацию. Таким образом, если на одном из форумов некто написал, что некая волнующая собеседника проблема решается способом, описанным в Q115486 от Microsoft, то это означает следующее: следует перейти по интернет адресу microsoft.com и ввести упомянутый выше код в окне поиска, что даст возможность ознакомиться со статьей под заголовком "HOWTO: Control Device Driver Load Order (Q115486)" — "Как управлять порядком загрузки драйверов?".

Более полную информацию о всех онлайн-ресурсах этого интернет-сайта, которая имеет прямое или косвенное отношение к драйверам, можно получить на странице с интернет адресом microsoft.com/hwdev/driver (хотя совершенно не исключено, что в будущем эта страница будет мигрировать).


Глава 1. Драйверы. Общие понятия и термины :: Источники информации

Заключение

В данной главе представлены начальные сведения по предметной области, называемой "Драйверы Windows".


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

Глава 2

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

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


Глава 2. Программные средства, применяемые при разработке драйверов :: Программные средства от Microsoft

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

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

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

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

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

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

Настройки проекта в Visual Studio 7 Net

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

Microsoft Visual Studio Solution File, Format Version 7.00
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Example",
    "Example.vcproj", "{E524BA09-7993-4528-91A9-7E27FAA3565F}"
EndProject
Global
    GlobalSection(SolutionConfiguration) = preSolution
    ConfigName.0 = Checked
    EndGlobalSection
    GlobalSection(ProjectDependencies) = postSolution
    EndGlobalSection
    GlobalSection(ProjectConfiguration) = postSolution
    {E524BA09-7993-4528-91A9-7E27FAA3565F}.Checked.ActiveCfg =
        Checked|Win32
    {E524BA09-7993-4528-91A9-7E27FAA3565F}.Checked.Build.0 =
        Checked|Win32
    EndGlobalSection
    GlobalSection(ExtensibilityGlobals) = postSolution
    EndGlobalSection
    GlobalSection(ExtensibilityAddIns) = postSolution
    EndGlobalSection
EndGlobal

Значительно более важным в проекте Example является файл Example.vcproj, который содержит конкретные значения настроек и описания используемых файлов. Точный текст Example.vcproj (файла настроек для компиляции и сборки простейшего не-WDM драйвера checked-версии в среде Visual Studio 7 Net) приводится ниже.

<?xml version="1.0" encoding = "windows-1251"?>
<VisualStudioProject ProjectType="Visual C++"
Version="7.00"
Name="Example"
SccProjectName=""
SccLocalPath="">
<Platforms><Platform Name="Win32"/></Platforms>
<Configurations> <Configuration Name="Release|Win32"
        OutputDirectory=".\checked"
        IntermediateDirectory=".\checked"
        ConfigurationType="2"
        UseOfMFC="0"
        ATLMinimizesCRunTimeLibraryUsage="FALSE"
        CharacterSet="1">
        <Tool Name="VCCLCompilerTool"
            AdditionalOptions="/Zel -cbstring /QIfdiv- /QIf /Gi- /Gm- /GX"
            Optimization="0"
            EnableIntrinsicFunctions="FALSE"
            OmitFramePointers="TRUE"
            OptimizeForProcessor="2"
            AdditionalIncludeDirectories="C:\WinDDK\2600\inc\ddk\w2k;
                C:\WinDDK\2600\inc\w2k;C:\WinDDK\2 600\inc\crt"
            PreprocessorDefinitions="_X86_=l;i386=l;
                CONDITION_HANDLING=1;NT_UP=1;
                NT_INST=0; WIN32=100;_NT1X_=100;WINNT=l;
                _WIN32_WINNT=0x0400;WIN32_LEAN_AND_MEAN=1;
                DEVL=l;DBG=l;FPO=0"
            IgnoreStandardIncludePath="TRUE"
            StringPooling="TRUE"
            ExceptionHandling="TRUE"
            RuntimeLibrary="O"
            StructMemberAlignment="4"
            BufferSecurityCheck="FALSE"
            EnableFunctionLevelLinking="TRUE"
            PrecompiledHeaderFile=".\checked/Example.pch"
            AssemblerListingLocation=".\checked/"
            ObjectFile=".\checked/"
            ProgramDataBaseFileName=".\checked\Example.pdb"
            WarningLevel="3"
            SuppressStartupBanner="TRUE"
            DebugInformationFormat="l"
            CallingConvention="2"
            CompileAs="0"
            ForcedIncludeFiles="warning.h"/>
        <Tool Name="VCCustomBuildTool"/>
        <Tool Name="VCLinkerTool" AdditionalOptions=""
            AdditionalDependencies="hal.lib ntoskrnl.lib int64.1ib
                msvcrt.lib "
            OutputFile=".\checked\Example.sys"
            Version="5.0"
            LinkIncremental="l"
            SuppressStartupBanner="TRUE"
            AdditionalLibraryDirectories="C:\WinDDK\2 600\lib\w2k\i386"
            IgnoreAllDefaultLibraries="TRUE"
            ProgramDatabaseFile=".\checked/Example.pdb"
            GenerateMapFile="TRUE"
            MapFileName="Example.map"
            StackReserveSize="262144"
            StackCommitSize="4096"
            OptimizeReferences="2"
            EnableCOMDATFolding="2"
            EntryPointSymbol="DriverEntry"
            SetChecksum="TRUE"
            BaseAddress="0x10000"
            ImportLibrary=""
            MergeSections=".rdata=.text"
            TargetMachine="1"/>
        <Tool Name="VCMIDLTool"
            MkTypLibCompatible="TRUE"
            SuppressStartupBanner="TRUE"
            TargetEnvironment="1"
            TypeLibraryName=".\checked/Example.tlb"/>
        <Tool Name="VCPostBuildEventTool"/>
        <Tool Name="VCPreBuildEventTool">
        <Tool Name="VCPreLinkEventTool"/>
        <Tool Name="VCResourceCompilerTool"/>
        <Tool Name="VCWebServiceProxyGeneratorTool"/>
        <Tool Name="VCWebDeploymentTool"/>
</Configuration>
</Configurations>
<Files> <Filter Name="Header Files" Filter=".h">
        <File RelativePath=".\Driver.h"> </File>
    </Filter>
    <Filter Name="Source Files" Filter=".c;.cpp">
        <File RelativePath="Init.cpp"> </File>
    </Filter>
</Files>
<Globals></Globals>
</VisualStudioProject>

Приведенный выше текст переформатирован (для удобства чтения в формате книги), поэтому расположение слов несколько отличается от их размещения в оригинальных .vcproj файлах, генерируемых средой Visual Studio 7 Net. Значения строковых параметров AdditionalIncludeDirectories и PreprocessorDefinitions обязательно должны быть записаны в одну строку.

Следует отметить, что особую важность для компиляции имеют значения пара метров IgnoreStandardIncludePath (здесь он отменяет стандартные пути для обнаружения заголовочных файлов, которые явно заданы теперь в параметре AdditionalIncludeDirectories), AdditionalOptions и PreprocessOrDefinitions (значения которых рекомендуется повторить в точности), CallingConvention (здесь определяет __stdcall).

Из параметров сборки следует отметить параметры IgnoreAllDefaultLibraries (здесь он отменяет использование библиотек, назначаемых в Visual Studio по умолчанию), AdditionalLibraryDirectories и AdditionalDependencies (они определяют используемые библиотеки — в данном случае для сборки не-WDM драйвера под Windows 2000), BaseAddress (обязательно следует указать равным 0x10000) и "неприметный" коварный параметр SetChecksum (должен быть "TRUE").

Все эти параметры можно настроить интерактивно и в самой интегрированной среде Visual Studio, однако, затем рекомендуется сравнить содержимое файла .vcproj с приведенным текстом, стараясь получить полное совпадение.

Компиляция и сборка драйвера утилитой Build пакета DDK

В том варианте, как поставляется пакет DDK, весьма просто использовать компилятор и редактор связей этого пакета. Для этого следует выбрать в меню запуска программ Пуск — Программы — ... запуск соответствующей среды (по ряду причин наиболее предпочтителен выбор среды Window 2000, checked или free), в результате чего появится консольное окно, для которого уже (автоматически) будут должным образом установлены переменные окружения. В том случае, если у разработчика имеются файлы makefile, sources (описывающие процесс сборки данного конкретного драйвера), а пакет DDK установлен корректно, то необходимо лишь перейти в рабочую директорию проекта командой cd (для драйвера, рассматриваемого в следующей главе, это — директория C:\Example) и ввести команду build (см. рисунок 2.1). Разумеется, что в случае ошибок компиляции или сборки вывод будет содержать и их диагностику.

Рис. 2.1
Рабочее окно сборки драйвера под Windows 2000 DDK версии checked

Программа Depends

Программа Depends предназначена для просмотра вызовов дополнительных библиотек. Программа Depends была создана в 1996 году и ранее поставлялась в составе Visual Studio 6. Теперь она является частью Platform SDK.

Скриншот (снимок экрана), представленный на рисунке 2.2, выполнен для просмотра дерева вызовов известного драйвера GiveIo.Sys. (Этот драйвер разблокирует доступ к портам ввода/вывода из приложений пользовательского режима при работе под Windows NT, используя при этом недокументированные возможности Windows.)

На приведенном рисунке видно, что драйвер обращается к функциям, экспортируемым NTOSKRNL.EXE, причем видно, что первыми (в порядке алфавита) являются вызовы IoCreateDevice, IoCreateSymbolicLink, IoDeleteDevice, IoDeleteSymbolicLink и др.

Программа может быть использована для просмотра вызовов, выполняемых из драйверов, исполняемых файлов (.exe файлов) и динамических библиотек. Программа работает и под Windows 98.

Рис. 2.2
Программа Depends для драйвера GiveIo

Программа ReBase

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

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

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

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

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

rebase -?

Программа ErrLook

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

Рис. 2.3
Программа ErrLook

Предположим, рабочая процедура драйвера возвратила управление Диспетчеру ввода/вывода, установив код завершения обработки пакета IRP равным STATUS_DEVICE_POWERED_OFF. В результате, если пользовательское приложение обратилось к драйверу с вызовом DeviceIoControl, но драйвером ответил отказом такого рода, то Диспетчер ввода/вывода передает приложению код ошибочного завершения вызова функции (для DeviceIoControl это 0) и устанавливает код ошибки (известный в документации MSDN под названием "system error code"). B свою очередь, пользовательское приложение может вызвать функцию GetLastError и в результате получит число 21 (которое и определил Диспетчер вода/вывода), что соответствует ошибке ERROR_NOT_READY — "The device is not ready". В примере, представленном на рисунке 2.3, программа ErrLook транслирует код 21 в текстовое сообщение, которое в русскоязычной версии Windows выдается в нижнем окошке на русском языке.

Следует обратить внимание, что, например, коду ошибки 21 (который выдается функцией GetLastError пользовательского режима в пользовательских приложениях) соответствует сразу несколько кодов завершения обработки IRP пакетов в драйвере:

  • STATUS_DEVICE_NOT_CONNECTED
  • STATUS_DEVICE_NOT_READY
  • STATUS_DEVICE_OFF_LINE
  • STATUS_DEVICE_POWER_FAILURE
  • STATUS_DEVICE_POWERED_OFF

Эту особенность неоднозначного соответствия кодов ошибок в пользовательском режиме (system error code) и кодов завершения обработки запросов на ввод/вывод от Диспетчера BB (IRP запросов к драйверу) следует учитывать при выборе соответствующих значений STATUS_Xxx для достоверного информирования клиентов драйвера об ошибочных ситуациях.

Транслировать код ошибки в текстовую форму программно можно при помощи несложной функции, текст которой приведен ниже. Функция получает код ошибки (system error code, например, 21) и выводит текст, расшифровывающий это значение.

#include <winbase.h>
#include <stdio.h>
void PrintErrorMessage(DWORD err)
{
    LPTSTR msg;
    DWORD res= ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM |
            FORMAT_MESSAGE_ALLOCATE_BUFFER,
            NULL,
            err,    // код ошибки
            0,      // идентификатор языка по-умолчанию
            (LPTSTR) &msg, 0, NULL);
    if(res == 0) { /* неудача */ }
    else { /* успешное завершение */
        printf("%s",msg);    // вывод сообщения на экран
        LocalFree(msg);     // освобождение буфера с текстом сообщения
    } return;
}

Программа GuidGen (UUIDGEN)

Программа GuidGen (UUIDGEN — ее консольная версия) выполняет генерацию 128 разрядного уникального ключа (GUID — глобально уникальный идентификатор), который может использоваться для регистрации интерфейса драйвера, в процессе инсталляции и т.п. Вероятность повторения данного значения весьма и весьма низка (хотя и не равна нулю), так что программа GuidGen является типовым инструментом для этих целей. Программа встречается во всех пакетах Microsoft.

Как показано на рисунке 2.4, программа создала GUID в формате, удобном для включения в текст на языке С. Нажав на кнопку "Сору" можно перенести его текст в буфер обмена и вставить в нужное место в тексте программы или драйвера.

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

Рис. 2.4
Программа GuidGen

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

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

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

Программа DeviceTree

В составе Windows DDK поставляется программа DeviceTree (рисунок 2.5), абсолютно незаменимая при самостоятельном изучении WDM модели, поскольку визуализирует представление о стеке драйверов (устройств) в операционной системе Windows.

Данная программа выполняет построение дерева устройств с двух точек зрения: с точки зрения принадлежности объектов устройств драйверам (режим D, рисунок 2.6) и с точки зрения взаимной подчиненности объектов устройств при выполнении операции перечисления устройств, enumeration process (режим P, рисунок 2.7). Программа позволяет отслеживать подчиненность объектов устройств в локальных стеках драйверов, их принадлежность драйверам, выявлять существующие фильтр-драйверы, устанавливать (выяснять) коды IPP пакетов, обслуживание которых объявил драйвер, и некоторую другую специфическую информацию.

Рис. 2.5
Заставка программы DeviceTree

На рисунке 2.6 показан фрагмент дерева устройств на участке шинного драйвера PCI. 11 объектов устройств (за исключением безымянного нижнего) представляют созданные этим шинным драйвером физические объекты устройств (так называемые PDO) для всех имеющихся в системе реальных PCI устройств, включая мосты (PCI-PCI, PCI-ISA), контроллеры USB, AGP адаптер, аудио на материнской плате, PCI адаптер Ethernet и т.п.

Рис. 2.6
Шинный драйвер PCI и его объекты устройств

Другой взгляд на этот же участок драйверного стека приведен на рисунке 2.7. Здесь показана взаимная подчиненность объектов устройств, возникающая в операционной системе при последовательно проводимой переписи устройств. Данное дерево отражает в своей структуре иерархию реальных устройств и очередность их обнаружения драйверами родительских устройств. Например, шинный драйвер PCI обнаруживает подключенные к шине устройства, что приводит к загрузке их драйверов, что, в свою очередь, приводит к обнаружению новых устройств, подключенных к ним — как в случае с шиной USB (ee контроллер является дочерним устройством шины PCI).

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

Рис.2.7
Драйверный стек от ACPI драйвера до шинного драйвера PCI

Программа DevCon

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

devcon stack =usb >stack_usb.txt

(собрать информацию обо всех устройствах в стеке USB) данная утилита выводит в файл stack_usb.txt следующую информацию (немного изменен формат):

PCI\VEN_1106&DEV_3038&SUBSYS_12340925&REV_1A\3&61AAA01&0&3A
Name: VIA Rev 5 USB Setup
Class: {36FC9E60-C465-11CF-8056-444553540000} USB
Controlling service:        usbuhci

PCI\VEN_1106&DEV_3038&SUBSYS_12340925&REV_1A\3&61AAA01&0&3B
Name: VIA Rev 5 USB Setup
Class: {36FC9E60-C465-11CF-8056-444553540000} USB
Controlling service:        usbuhci

USB\ROOT_HUB\4&1E8F7657&0
Name: Setup
Class: {36FC9E60-C465-11CF-8056-444553540000} USB
Controlling service:        usbhub

USB\ROOT_HUB\4&BD5C5B2&0
Name: Setup
Class: {36FC9E60-C465-11CF-8056-444553540000} USB
Controlling service:        usbhub

4 matching device(s) found.

Программа DevCtl

Консольное приложение DevCtl из состава вспомогательных утилит пакета DDK проводит тестирование драйвера путем применения к нему наиболее употребительных вызовов ввода/вывода пользовательского режима, например NtCreateFile или DeviceIoControl. B процессе тестирования могут быть выявлены серьезные упущения в программировании драйвера, например, некорректная обработка неожиданных (для тестируемого драйвера) IOCTL запросов или запрос на получение данных, когда приложение указало заведомо малый размер буфера для получаемых данных.

В качестве указателя на тестируемый драйвер программе DevCtl следует передавать имя устройства (из числа тех, что видны в директории Device в рабочем окне программы WinObj, описание которой см. ниже). Более подробное описание программы DevCtl можно найти в DDK.

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

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

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

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

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

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

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

Системный апплет "Производительность"

Более глубоко лежащим средством мониторинга системы и отдельных процессов и служб является апплет "Производительность" системной панели "Администрирование", вызываемой запускающей последовательностью Пуск — Настройка — Панель управления — Администрирование — Производительность.

Рабочее окно данного системного апплета показано на рисунке 2.9. По нажатию правой кнопки мышки появляется меню, допускающее добавление новых графиков в окно работающего апплета ("Добавить счетчики").Сочетание параметров, которые можно просматривать таким образом, настолько велико, что для их описания потребовалась бы отдельная глава. Для просмотра доступны параметры функционирования памяти, процессора, КЭШа, протоколов IP, TCP, UDP, отдельных процессов и сервисов, включая их отдельные потоки, — всего чуть менее полусотни информативных единиц. Для каждой такой единицы возможен просмотр достаточно большого набора счетчиков (как правило, не менее полутора десятков). Например, для процессора среди счетчиков, доступных для просмотра, можно назвать счетчики числа прерываний в секунду, процента времени бездействия, процента времени работы в пользовательском и привилегированном режимах, счетчика DPC процедур, поставленных в очередь, в секунду.

Краткие пояснения, касающиеся смысла указываемых счетчиков, можно получить при выборе счетчика по нажатию кнопки "Объяснение".

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

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

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

  • Операции с пулами памяти.
  • Корректность уровней IRQL, на которых выполняется код драйвера.
  • Обнаружение взаимоблокировок.
  • Выполнение DMA операций.
  • Стресс-тест (нехватка ресурсов).
  • Нетипичные запросы к драйверу.

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

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

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

> prefast cl /с myfile.cpp
> prefast view

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

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

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

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

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

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


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

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

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

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

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

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

Программа Monitor от CompuWare Corporation

Программа Monitor от Numega (теперь CompuWare Corporation) позволяет динамически загружать, запускать, останавливать и выгружать драйверы, выполненные в-стиле-NT (не-WDM), в большинстве случаев без перезапуска системы и без создания собственной программы загрузки драйвера при помощи SCM сервисов, а также без использования inf файлов и системного Менеджера Устройств. Таким образом, достаточно подготовить лишь .sys файл и затем воспользоваться программой Monitor.

Вообще говоря, имеются и иные программы с данным сервисом, однако Monitor от CompuWare Corporation имеет наиболее завершенный вид (младшие версии работали еще с VxD драйверами) и удобный графический интерфейс.

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

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

Рис. 2.10
Программа Monitor

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

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

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

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

Рис.2.11
Программа SrcToDsp

Программа Numega SymLinks

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

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

Рис.2.12
Программа SymLinks

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


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

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

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

Программа RegMon

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

Рис. 2.13
Программа RegMon

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

Программа WinObj

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

Рис. 2.14
Программа WinObj

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

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

Программа DebugView

Программа DebugView (рисунок 2.15) позволяет наблюдать в своем рабочем окне текст сообщений, которые во время своей работы выводит драйвер, если он использует специальные отладочные printf-подобные функции, такие как вызовы режима ядра DbgPrint в Windows NT или Win32 вызов OutputDebugString. Пример использования этих функций (а именно — DbgPrint) в программном коде драйвера режима ядра можно увидеть в следующей главе (на примере драйвера Example.sys).

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

Рис. 2.15
Программа DebugView

Для просмотра сообщений, которые выдают драйверы при работе инициализационных процедур (например, DriverEntry или AddDevice), но которые загружаются при запуске системы, можно использовать следующий прием. В случае, если драйвер был инсталлирован при помощи Мастера Установки и inf файла, в результате чего драйвер виден в Панели Настроек в окне Диспетчера Устройств следует выполнить отключение устройства, после чего из системы выгрузится драйвер (в частности, отработает процедура Unload). (Заметим, кстати, что между включением и отключением можно произвести замену файла драйвера на новую версию — если это необходимо). После этого необходимо выполнить там же, в Диспетчере Устройств, включение драйвера — в русскоязычной версии Windows это действие обозначено словом "задействовать". В результате будет динамически загружен ранее отключенный драйвер, а его работа начнется с вызова процедуры DriverEntry и т.д., что позволит увидеть все диагностические сообщения для программы DebugView (разумеется, если разработчик их предусмотрел).


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

Программа DebugPrint

Весьма интересная программа DebugPrint (полное название — Debug Print Monitor), была разработана фирмой PHD Computer Consultant Ltd., с программными продуктами которой подробнее можно ознакомиться на интернет сайте phdcc.com.

Совместно с программой пользовательского режима DebugPrint работает драйвер режима ядра DebugPrt.sys (разработанный в PHD CC Ltd.), который должен быть установлен заранее. Доступ к DebugPrt.sys клиентский драйвер должен получить средствами специального программного кода (использующего, в частности, редкий для драйверного кода вызов ZwCreateFile). Разработчик присоединяет к отладочной версии своего драйвера (в данной ситуации — клиентского по отношению к DebugPrt.sys) этот специальный программный код, предоставляемый в форме исходного текста на языке С, обеспечивая таким образом связь с DebugPrt.sys и вывод сообщений в его внутренние буферные области. Работа собственно программы Debug Print Monitor состоит в том, чтобы выводить в своем рабочем окне сообщения отладочного характера, переданные драйверу DebugPrt.sys клиентским драйвером.

В том случае, если при старте операционной системы драйвер-получатель сообщений (DebugPrt.sys) загружается раньше отлаживаемого драйвера, все сообщения из отлаживаемого драйвера, даже переданные в процедурах инициализации, будут сохранены драйвером-получателем. Впоследствии, все сообщения можно просмотреть при помощи Debug Print Monitor, имеющей удобный графический интерфейс и позволяющей сохранять сообщения в текстовом файле для последующего анализа.

Для разработчика драйвера использование функций передачи выглядят почти что как использование функций printf или sprintf. Однако следует изначально использовать приемы условной компиляции, чтобы использование этого отладочного трюка не навредило в окончательной версии драйвера (где такие операции являются непозволительной роскошью и при слишком обильной диагностике — могут приводить к сбою системы), а переход от отладочной (checked) версии к релизной (free) и наоборот отнимал бы минимум усилий у разработчика. При правильном применении условной компиляции разработчику достаточно выбрать среду компиляции DDK (checked/free), что уже определяет полностью версию собранного проекта (см. п. 2.1.2).

Драйвер DebugPrt.sys, необходимый для работы программы Debug Print Monitor, устанавливается при помощи мастера установки оборудования и inf файла и запускается в процессе загрузки операционной системы.

Программа DevView от Уолтера Оней

Программа DevView (полное название Device Object Viewer), созданная Уолтером Оней, автором замечательной книги "Programming The Microsoft Windows Driver Model", вышедшей уже в двух изданиях, является полным аналогом программы WinObj, правда, работающим только под Windows NT.

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

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

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

Рис.2.18
Программа PoolTag

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

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

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

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

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

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

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

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

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

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

Дизассемблер IDA

Интерактивный дизассемблер IDA фирмы DataRescue позволяет получать крупинки знания о взаимодействии драйверов с операционной системой путем изучения хорошо зарекомендовавших себя драйверов, исходный текст которых зачастую недоступен. Для исследования драйверов NT, которые собираются в бинарный модуль (файл) с расширением sys, наиболее подходят версии IDA после 4.15. Последнюю испытательную версию можно загрузить (после регистрационного запроса) с Интернет-сайта фирмы datarescue.com. На момент подготовки книги к печати это была версия 4.6.

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

Рис. 2.22
Диаграмма вызовов в одном из окон дизассемблера IDA

Безусловно, рассмотреть возможности дизассемблера IDA Pro весьма сложно даже в рамках отдельной главы. Поэтому, учитывая мощь и полезность этой программы для самообразования разработчика драйверов, следует порекомендовать книгу Криса Касперских "Образ мышления — дизассемблер IDA", изд. Солон-Р, 2001, ISBN 5-93455-093-4, 480 страниц (правда, читателю следует быть готовым к тому, что в упомянутой книге рассмотрена далеко не последняя версия программы).


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

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

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

Программа w2k_svc

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

w2k_svc /any /all >list.txt

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

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

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

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

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

w2k_svc /drivers /active >list.txt

Программа w2k_sym

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

w2k_sym /v /d >list.txt

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

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

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

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

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

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

Программа w2k_mem

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

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

w2k_mem #0x200 0x898F9094 >display_mem1.txt

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

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

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

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

w2k_mem #0x200 0x80DC5C88 >display_mem2.txt

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

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

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

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


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

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

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

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

Программа PEBrowseProfessional Interactive

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

Программа NTDevices

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

Рис.2.20
Программа NTDevices

Программа NTObjects

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

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

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


Глава 2. Программные средства, применяемые при разработке драйверов :: Заключение

[Previous] [Next]

Заключение

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


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

Глава 3

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

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

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

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

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

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

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


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

Процедура DriverEntry и предварительные объявления

Все приведенные ниже отрывки кода следует последовательно поместить в один файл (обычно, файл, содержащий описание DriverEntry, разработчики называют Init.c). Редактирование, разумеется, удобнее всего выполнять в каком-нибудь редакторе интегрированной среды. Рекомендуется использовать редактор из среды Visual Studio, поскольку в нем производится динамический контроль синтаксиса и типов данных языка С. В главе 2 приводится содержимое файлов настройки проекта для драйвера Example, соблюдение которых позволит воспользоваться динамическими подсказками среды во время редактирования и позволит также выполнять контрольную компиляцию кода. Последнее весьма удобно, поскольку в интегрированной среде легко перейти к месту возникновения ошибки по диагностическому сообщению.

Окончательную компиляцию драйвера (как чистовую, так и отладочную) категорически рекомендуется выполнять утилитой Build из среды DDK, поскольку иные способы компиляции могут быть источником необъяснимых странностей в поведении драйвера.

/////////////////////////////////////////////////////////////////////
// init.cpp: Инициализация драйвера
// Замечание. Рабочая версия данного драйвера должна быть
// скомпилирована как не-WDM версия. В противном случае - драйвер
// не сможет корректно загружаться и выгружаться с использованием
// программы monitor (пакет Numega Driver Studio) и сервисов SCM
// Менеджера.

/////////////////////////////////////////////////////////////////////
// DriverEntry	          Главная точка входа в драйвер
// UnloadRoutine          Процедура выгрузки драйвера
// DeviceControlRoutine   Обработчик DeviceIoControl IRP пакетов
/////////////////////////////////////////////////////////////////////
#include "Driver.h"

// Предварительные объявления функций:
NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp );
VOID     UnloadRoutine(IN PDRIVER_OBJECT DriverObject);
NTSTATUS ReadWrite_IRPhandler( IN PDEVICE_OBJECT fdo, IN PIRP Irp );
NTSTATUS Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);
NTSTATUS Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);

// Хотя и нехорошо делать глобальные переменные в драйвере...
KSPIN_LOCK MySpinLock;
#pragma code_seg("INIT") // начало секции INIT
/////////////////////////////////////////////////////////////////////
// (Файл init.cpp)
// DriverEntry - инициализация драйвера и необходимых объектов
// Аргументы:  указатель на объект драйвера
//             раздел реестра (driver service key) в UNICODE
// Возвращает: STATUS_Xxx

extern "C"
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject,
                      IN PUNICODE_STRING RegistryPath  )
{
    NTSTATUS status = STATUS_SUCCESS;
    PDEVICE_OBJECT  fdo;
    UNICODE_STRING  devName;

    #if DBG
    DbgPrint("=Example= In DriverEntry.");
    DbgPrint("=Example= RegistryPath = %ws.", RegistryPath->Buffer);
    #endif

    // Экспорт точек входа в драйвер (AddDevice объявлять не будем)
    // DriverObject->DriverExtension->AddDevice= OurAddDeviceRoutine;
    DriverObject->DriverUnload = UnloadRoutine;
    DriverObject->MajorFunction[IRP_MJ_CREATE]= Create_File_IRPprocessing;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = Close_HandleIRPprocessing;
    DriverObject->MajorFunction[IRP_MJ_READ]  = ReadWrite_IRPhandler;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = ReadWrite_IRPhandler;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]= DeviceControlRoutine;
    //========================================================
    // Действия по созданию символьной ссылки
    // (их нужно было бы делать в OurAddDeviceRoutine, но у нас
    // очень простой драйвер и эта процедура отсутствует):
    RtlInitUnicodeString( &devName, L"\\Device\\EXAMPLE" );

    // Создаем наш Functional Device Object (FDO) и получаем
    // указатель на созданный FDO в нашей переменной fdo.
    // (В WDM драйвере эту работу также следовало бы выполнять
    // в процедуре OurAddDeviceRoutine.) При создании FDO
    // будет выделено место и под структуру расширения устройства
    // EXAMPLE_DEVICE_EXTENSION (для этого мы передаем в вызов
    // ее размер, вычисляемый оператором sizeof):
    status = IoCreateDevice(DriverObject,
                            sizeof(EXAMPLE_DEVICE_EXTENSION),
                            &devName, // может быть и NULL
                            FILE_DEVICE_UNKNOWN,
                            0,
                            FALSE, // без эксклюзивного доступа
                            &fdo);
    if(!NT_SUCCESS(status)) return status;

    // Получаем указатель на область, предназначенную под
    // структуру расширение устройства
    PEXAMPLE_DEVICE_EXTENSION dx = (PEXAMPLE_DEVICE_EXTENSION)fdo->DeviceExtension;
    dx->fdo = fdo;  // Сохраняем обратный указатель

    // Применяя прием условной компиляции, вводим функцию DbgPrint,
    // сообщения которой мы сможем увидеть в окне DebugView, если
    // выполним сборку нашего драйвера как checked (отладочную)
    // версию:
    #if DBG
    DbgPrint("=Example= FDO %X, DevExt=%X.",fdo,dx);
    #endif

    //=======================================
    // Действия по созданию символьной ссылки
    // (их нужно было бы делать в OurAddDeviceRoutine, но у нас
    // очень простой драйвер):
    UNICODE_STRING symLinkName;   // Сформировать символьное имя:
    // #define   SYM_LINK_NAME   L"\\??\\Example"
    // Такого типа символьные ссылки ^^ проходят только в NT.
    // (То есть, если перенести бинарный файл драйвера в
    // Windows 98, то пользовательские приложения заведомо
    // не смогут открыть файл по такой символьной ссылке.)
    // Для того, чтобы ссылка работала в и Windows 98 и в NT,
    // необходимо поступать следующим образом:
    #define	SYM_LINK_NAME	L"\\DosDevices\\Example"
    RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME );
    dx->ustrSymLinkName = symLinkName;

    // Создаем символьную ссылку
    status = IoCreateSymbolicLink( &symLinkName, &devName );
    if (!NT_SUCCESS(status))
    { // при неудаче √ удалить Device Object и вернуть управление
        IoDeleteDevice( fdo );
        return status;
        } // Теперь можно вызывать CreateFile("\\\\.\\Example",...);
          // в пользовательских приложениях

        // Объект спин-блокировки, который будем использовать для
        // разнесения во времени выполнения кода обработчика
        // IOCTL запросов. Инициализируем его:
        KeInitializeSpinLock(&MySpinLock);

        // Снова используем условную компиляцию, чтобы выделить код,
        // компилируемый в отладочной версии и не компилируемый в
        // версии free (релизной):
        #if DBG
        DbgPrint("=Example= DriverEntry successfully completed.");
        #endif
        return status;
}
#pragma code_seg() // end INIT section

Функция CompleteIrp

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

//
// (Файл init.cpp)
// CompleteIrp: Устанавливает IoStatus и завершает обработку IRP
// Первый аргумент - указатель на объект нашего FDO.
//
NTSTATUS CompleteIrp( PIRP Irp, NTSTATUS status, ULONG info)
{
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = info;
    IoCompleteRequest(Irp,IO_NO_INCREMENT);
    return status;
}

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

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

Описание прототипов рабочих процедура драйвера (параметров их вызова) можно найти в документации DDK (2000, ХР, 2003), если в режиме указателя задать ключевые слова Dispatch..., например, DispatchRead. Если ваша программа просмотра файлов справки не поддерживает переходов между разными .chm файлами (представляющими полную документацию по DDK), то можно сразу обратиться к файлу kmarch.chm (который, собственно, и содержит информацию по рабочим процедурам). Там же можно узнать, на каком уровне IRQL происходит вызов конкретной функции.
//
// (Файл init.cpp)
// ReadWrite_IRPhandler: Берет на себя обработку запросов
// чтения/записи и завершает обработку IRP вызовом CompleteIrp
// с числом переданных/полученных байт (BytesTxd) равным нулю.
// Аргументы:
// Указатель на объект нашего FDO
// Указатель на структуру IRP, поступившего от Диспетчера ввода/вывода
NTSTATUS ReadWrite_IRPhandler( IN PDEVICE_OBJECT fdo, IN PIRP Irp )
{
    ULONG BytesTxd = 0;
    NTSTATUS status = STATUS_SUCCESS; //Завершение с кодом status
    // Задаем печать отладочных сообщений √ если сборка отладочная
    #if DBG
    DbgPrint("-Example- in ReadWrite_IRPhandler.");
    #endif
    return CompleteIrp(Irp,status,BytesTxd);
}

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

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

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

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

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

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

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

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

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

  • IOCTL_PRINT_DEBUG_MESS — выводим отладочное сообщение в окно DebugView.
  • IOCTL_CHANGE_IRQL — проводим эксперимент, насколько высоко можно искусственно поднять уровень IRQL в коде драйвера.
  • IOCTL_MAKE_SYSTEM_CRASH — проводим эксперимент по "обрушению" операционной системы и пытаемся его предотвратить.
  • IOCTL_TOUCH_PORT_378H — проводим эксперимент по обращению к аппаратным ресурсам системы.
  • IOCTL_SEND_BYTE_TO_USER — отправляем байт данных в пользовательское приложение.

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

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

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

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

    // Получаем текущее значение уровня IRQL √ приоритета,
    // на котором выполняется поток (вообще говоря, целое число):
    KIRQL irql,
    currentIrql = KeGetCurrentIrql();

    #if DBG
    DbgPrint("-Example- In DeviceControlRoutine (fdo= %X)\n",fdo);
    DbgPrint("-Example- DeviceIoControl: IOCTL %x.", ControlCode );
    if(currentIrql==PASSIVE_LEVEL)
        DbgPrint("-Example- PASSIVE_LEVEL (val=%d)",currentIrql);
    #endif
    // Запрашиваем владение объектом спин-блокировки. В данном
    // примере не выполняется никаких критичных действий, но,
    // вообще говоря, этот прием может быть полезен и даже
    // незаменим, если в приведенном ниже коде должны будут
    // выполнены манипуляции, которые можно делать только
    // эксклюзивно. Пока потоку выделен объект спин-блокировки √
    // никакой другой поток не сможет войти в оператор switch:
    KeAcquireSpinLock(&MySpinLock,&irql);

    // Диспетчеризация по IOCTL кодам:
    switch( ControlCode) {

    #ifndef SMALL_VERSION
    case IOCTL_PRINT_DEBUG_MESS:
    {     // Только вводим сообщение и только в отладочной версии
        #if DBG
        DbgPrint("-Example- IOCTL_PRINT_DEBUG_MESS.");
        #endif
        break;
    }
    case IOCTL_CHANGE_IRQL:
    {
        #if DBG
        // Эксперименты по искусственному повышению
        // IRQL √ только в отладочной версии!
        DbgPrint("-Example- IOCTL_CHANGE_IRQL.");
        KIRQL dl = DISPATCH_LEVEL, // только для распечатки (2)
        oldIrql,
        newIrql=25; // Новый уровень IRQL (например, 25)
        // Устанавливаем newIrql, сохраняя текущий в oldIrql:
        KeRaiseIrql(newIrql,&oldIrql);
        newIrql=KeGetCurrentIrql(); // Что реально получили?

        DbgPrint("-Example- DISPATCH_LEVEL value =%d",dl);
        DbgPrint("-Example- IRQLs are old=%d new=%d",
                        oldIrql,newIrql);
        KeLowerIrql(oldIrql); // Возвращаем старое значение
        #endif
        break;
    }
    #endif // SMALL_VERSION

    case IOCTL_MAKE_SYSTEM_CRASH:
    {
        int errDetected=0;
        char x = (char)0xFF;

        #if	DBG  // Вообще говоря, под NT мы этого уже не увидим:
        DbgPrint("-Example- IOCTL_MAKE_SYSTEM_CRASH.");
        #endif
        // Вызываем системный сбой обращением по нулевому адресу
        __try {
        x = *(char*)0x0L; // ошибочная ситуация
            //^^^^^^^^^^^^ здесь случится сбой NT, но не Win98
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {   // Перехват исключения не работает!
            // Эта занимательная ситуация объяснена в 10.2.6,
            // при рассмотрении объектов спин-блокировок.
            errDetected=1;
        };
        #if DBG
        DbgPrint("-Example- Value of x is %X.",x);
        if(errDetected)
            DbgPrint("-Example- Except detected in Example driver.");
        #endif
        break;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заголовочный файл Driver.h

Ниже приводится полный текст файла Driver.h, содержащий объявления, необходимые для компиляции драйвера Example.sys.

#ifndef _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_
#define _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_
// Выше приведены две строки (в конце файла имеется еще #endif),
// которые в больших проектах запрещают повторные  проходы по тексту,
// который находится внутри h-файла (что весьма удобно для повышения
// скорости компиляции).
// (Файл Driver.h)

#ifdef __cplusplus
extern "C"
{
#endif

#include "ntddk.h"

//#include "wdm.h"
// ^^^^^^^^^^^^^^ если выбрать эту строку и закомментировать
// предыдущую, то компиляция  в среде DDK (при помощи утилиты Build)
// также пройдет успешно, однако драйвер Example не станет от этого
// настоящим WDM драйвером.

#ifdef __cplusplus
}
#endif
// Определяем структуру расширения устройства. Включим в нее
// указатель на FDO (для удобства последующей работы UnloadRoutine) и
// имя символьной ссылки в формате UNOCODE_STRING.

typedef struct _EXAMPLE_DEVICE_EXTENSION
{
    PDEVICE_OBJECT	fdo;
    UNICODE_STRING	ustrSymLinkName; // L"\\DosDevices\\Example"
} EXAMPLE_DEVICE_EXTENSION, *PEXAMPLE_DEVICE_EXTENSION;

// Определяем собственные коды IOCTL, с которыми можно будет
// обращаться к драйверу при помощи вызова DeviceIoControl.
// Определение макроса CTL_CODE можно найти в файле DDK Winioctl.h.
// Там же можно найти и численные значения, скрывающиеся под именами
// METHOD_BUFFERED и METHOD_NEITHER.

// Внимание! Текст приведенный ниже должен войти в файл Ioctl.h,
// который будет необходим для компиляции тестового приложения.
// (Разумеется, за исключением последней строки с "#endif".)

#define IOCTL_PRINT_DEBUG_MESS CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_CHANGE_IRQL CTL_CODE(\
    FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_MAKE_SYSTEM_CRASH CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_TOUCH_PORT_378H CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_SEND_BYTE_TO_USER CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)

// Вариант :
//#define IOCTL_SEND_BYTE_TO_USER CTL_CODE( \
//    FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS)
#endif

Третий параметр CTL_CODE называется Function и при составлении собственных (пользовательских) IOCTL кодов его значение не должно быть менее 0x800. Пересечение пользовательских IOCTL кодов со значениями IOCTL кодов других драйверов не имеет никакого значения, поскольку они действуют только в пределах конкретного драйвера.


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

Компиляция и сборка драйвера Example.sys

Компиляция и сборка отладочной версии драйвера в среде Visual Studio 7 Net требует выбора пункта меню Rebuild Solution, после чего будет выполнена компиляция и сборка драйвера, а результат (в соответствии с настройками Example.sln и Example.vcproj) будет размещен в поддиректории .\checked.

Для компиляции и сборки драйвера утилитой Build пакета DDK потребуется создать два файла описания проекта — Makefile и Sources.

Файл Makefile

Этот файл управляет работой программы Build и в нашем случае имеет стандартный вид (его можно найти практически в любой директории примеров DDK), а именно:

# Файл Makefile
#
# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source
# file to this component. This file merely indirects to the real make file
# that is shared by all the driver components of the Windows NT DDK
#

!INCLUDE $(NTMAKEENV)\makefile.def

Файл Sources

Файл sources отражает индивидуальные настройки процесса компиляции и сборки. В нашем случае файл Sources чрезвычайно прост и имеет вид:

# Файл Sources
TARGETNAME=Example
TARGETTYPE=DRIVER
#DRIVERTYPE=WDM
TARGETPATH=obj
SOURCES=init.cpp

Данный файл задает имя выходного файла Example. Поскольку проект (TARGETTYPE) имеет тип DRIVER, то выходной файл будет иметь расширение .sys. Промежуточные файлы будут размещены во вложенной директории .\obj. Строка SOURCES задает единственный файл с исходным текстом — это файл init.cpp.

Если бы мы выполняли компиляцию и сборку WDM драйвера, то нужно было бы в тексте Driver.h использовать #include "wdm.h" (взять определения из заголовочного файла "wdm.h" вместо "ntddk.h"), а в данном файле Sources — удалить символ '#' (который вводит строку-комментарий) в первой позиции третьей строки. После этого строка
DRIVERTYPE=WDM
стала бы указывать утилите Build на то, что выполняется компиляция и сборка WDM драйвера.

Компиляция и сборка при помощи утилиты Build

Разместим для определенности все файлы (нам понадобятся файлы init.cpp, Driver.h, Makefile и sources) в директорию C:\Example. После этого процесс компиляции и сборки checked (отладочной) версии драйвера при помощи утилиты Build пакета DDK полностью описывается во 2 главе ("Компиляция и сборка драйвера утилитой Build пакета DDK").

Результат сборки можно будет найти в поддиректории .\objchk_w2k\i386 (поскольку используются настройки переменных среды сборки под Windows 2000).


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

Инсталляция и запуск драйвера Example.sys

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

Инсталляция внесением записей в Системный Реестр

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

Модификация Системного Реестра Windows 98

Как ни удивительно это будет узнать, но данный драйвер (собранный как версия checked для Windows 2000) устанавливается, запускается и работает под Windows 98 SE. Для запуска драйвера следует переписать его бинарный файл Example.sys в директорию C:\Windows\System32\Drivers и создать файл (назовем его Example98.reg) со следующими записями:

REGEDIT4
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Example]
"ErrorControl"=dword:00000001
"Type"=dword:00000001
"Start"=dword:00000002
"ImagePath"="\\SystemRoot\\System32\\Drivers\\Example.sys"

После этого следует войти в редактор Системного Реестра (Пуск — Выполнить — regedit) и произвести импорт созданного файла. Импорт данного файла в Реестр Windows 98 можно выполнить, если дважды кликнуть мышкой на этом файле в стандартной программе Проводник (после этого последует предложение импортировать файл в реестр Windows 98, на которое следует ответить утвердительно).

В результате импорта в Системном Реестре будет создан новый подраздел \Example в ветви HKLM\System\CurrentControlSet\Services. В этот подраздел будут занесены параметры ErrorControl, ImagePath, Start и Туре, значение которых обсуждается ниже.

Параметр Туре определяет драйвер режима ядра (значение 1).

Параметр ImagePath определяет местонахождение файла загружаемого модуля (в нашем случае — C:\Windows\System32\Drivers\Example.sys).

Параметр Start определяет момент загрузки сервиса — автостарт после загрузки системы (значение 2).

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

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

Модификация Системного Реестра Windows 2000, XP, Server2003

Файлы импорта в Системный Реестр Windows NT должны быть в формате UNICODE, поэтому их невозможно создать простым текстовым редактором (например, Notepad), как это можно было сделать в случае Windows 98.

Для модификации Системного Реестра Windows 2000, XP, Server 2003 необходимо выполнить модификацию Системного Реестра вручную непосредственно в Реестре. Для этого необходимо войти в редактор Системного Реестра (Пуск — Выполнить — regedit32 для Windows 2000 и Пуск — Выполнить — regedit для Windows XP, Server 2003) и найти там подраздел HKLM\System\CurrentControlSet\Services. В этом подразделе следует создать вложенный подраздел \Example и внести в него следующие параметры и их значения:

  • dword параметр ErrorControl со значением 1
  • dword параметр Туре со значением 1
  • dword параметр Start co значением 2
  • строковый параметр ImagePath со значением "\\SystemRoot\\System32\\Drivers\\Example.sys"

После того как указанные данные будут внесены в Системный Реестр, можно выполнить экспорт подраздела HKLM\System\CurrentControlSet\Services\Example во внешний файл (например, ExampleNT.reg) и в следующий раз (например, на другом компьютере) выполнять импорт этого файла в Системный Реестр вместо набора вручную.

Смысл вносимых параметров тот же, что и в случае Windows 98.

Запуск драйвера

После того как выполнена описанная модификация Системного Реестра и файл Example.sys был размещен в директории C:\Windows\System32\Drivers\, необходимо выполнить перезагрузку операционной системы (как в случае Windows 98, так и в случае Windows 2000, XP, Server 2003) для того, чтобы драйвер был загружен и начал работу.

Инсталляция с использованием INF файла

Для такого способа инсталляции драйвера потребуется создать текстовый файл (назовем его Example.inf), в котором будет представлена информация для работы Мастера Установки нового оборудования. В данном файле имеет значение даже то, куда поставлена запятая. Поэтому его следует повторить в точности. (Более подробно составление inf-файлов обсуждается в документации DDK, файл справки install.chm, и в главе 12.)

; Example.Inf - install information file
;  Created 2 feb 2003 by SVP
[Version]
Signature="$Chicago$"
Class=Unknown
Provider=%SVPBook%
DriverVer=02/22/2003,1.0.0.2

[Manufacturer]
%SVPBook%=SVP.Science

[SVP.Science]
%Example%=Example.Install, *svpBook\Example

[DestinationDirs]
Example.Files.Driver=10,System32\Drivers  ; куда копировать для Win98
Example.Files.Driver.NTx86=10,System32\Drivers ; куда копировать для NT

[SourceDisksNames]
1="Example build directory",,,       ;  первая цифра -- единица

[SourceDisksFiles]
Example.sys=1,drv\w98   ; где находится новый драйвер для Win98

[SourceDisksFiles.x86]
Example.sys=1,drv\nt   ; где находится новый драйвер для NT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Windows 98
[Example.Install]
CopyFiles=Example.Files.Driver
AddReg=Example.AddReg

[Example.AddReg]
HKR,,DevLoader,,*ntkern
HKR,,NTMPDriver,,Example.sys
[Example.Files.Driver]
Example.sys
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Windows 2000, XP, Server 2003
[Example.Install.NTx86]
CopyFiles=Example.Files.Driver.NTx86

[Example.Files.Driver.NTx86]
Example.sys,,,%COPYFLG_NOSKIP%

[Example.Install.NTx86.Services]
AddService = Example, %SPSVCINST_ASSOCSERVICE%, Example.Service

[Example.Service]
DisplayName    = %Example.ServiceName%
ServiceType    = %SERVICE_KERNEL_DRIVER%
StartType      = %SERVICE_AUTO_START%
ErrorControl   = %SERVICE_ERROR_NORMAL%
ServiceBinary  = %10%\System32\Drivers\Example.sys
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Strings
[Strings]
SVPBook="Introduction to Driver Programming"
Example="Example driver: checked build"
Example.ServiceName="Example NTDDK driver (V.001)"

SPSVCINST_ASSOCSERVICE=0x00000002
COPYFLG_NOSKIP=2	; Do not allow user to skip file
SERVICE_KERNEL_DRIVER=1
SERVICE_AUTO_START=2
SERVICE_DEMAND_START=3 ;  см. п. 11.1.10
SERVICE_ERROR_NORMAL=1

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

В корневой каталог дискеты следует поместить данный файл, Example.inf, a также создать директорию a:\drv со вложенными поддиректориями a:\drv\w98 и a:\drv\nt, куда следует поместить по одной копии файла драйвера Example.sys. (В том случае, если решено устанавливать драйвер из директории на жестком диске, то указанная структура информации должна быть также соблюдена).

Теперь (когда уже создан inf-файл) можно приступать к установке драйвера при помощи Мастера Установки нового оборудования (Пуск — Настройка — ...). При его работе важно выполнить следующие действия:

  1. Следует самостоятельно выбрать устанавливаемое устройство (а не пользоваться услугами автоматического обнаружения).
  2. В Windows 98 следует указать тип устройства "? Другие устройства".
  3. Выбрать установку драйвера с диска, после чего следует указать диск 'a:' (либо директорию на жестком диске, где находится Example.inf, поддиректории \drv\nt и \drv\w98 и две копии Example.sys, как было указано выше).

После идентификации inf файла Мастер Установки нового оборудования самостоятельно скопирует файл Example.sys из соответствующей директории drv\w98 или drv\nt (в нашем случае эти файлы идентичны) в \System32\Drivers внутри системной директории. Мастер Установки произведет модификацию записей в Системном Реестре, в результате чего драйвер будет загружаться после загрузки системы (когда она произойдет в следующий раз).

Для запуска данного драйвера сразу после установки Мастером Установки не требуется перезагрузки системы. (Но для других драйверов под Windows 98 это может потребоваться.)

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

Результаты работы Мастера Установки с записями Системного Реестра следует искать в разделе HKLM\System\CurrentControlSet\Services\Class\Unknown (для Win98) и в разделе HKLM\System\CurrentControlSet\Services\Unknown\Example (для Windows 2000/XP/Server 2003).

Следует отметить, что информацию о драйвере Example.sys после установки можно увидеть в Настройках Системы (Система/Диспетчер Устройств в Windows NT, либо Система/Устройства в Windows 98), однако многие информационные поля там не будут определены (в случае Windows NT таких полей будет меньше). Это объясняется тем, что информация, для которой указано "неизвестна" должна поступать из файла драйвера, для чего в нем должны быть предусмотрены информационные ресурсы, обычно размещающиеся в .rc файле проекта. В данном проекте такого файла нет, поэтому не вся желаемая информация предоставляется системным службам.

Возникает вопрос, почему драйвер, предназначенный для NT, запускается и работает под Windows 98?! Ответ прост. В Windows 98 установлен модуль ntkern.vxd, который так выполняет свою работу, что драйверам NT кажется, что они имеют дело с NT-системой, К сожалению, возможности его не безграничны, иначе Windows 98 была бы Windows NT.

Другой вопрос, который может возникнуть после описанной процедуры: почему мы смогли установить драйвер, в сущности, "никакого" устройства?! Ответ также несложен. Поскольку к системе могут подключаться устройства, не поддерживающие PnP (legacy devices), которые не могут быть автоматически обнаружены и которые не могут быть подключены (загружены их драйверы) иначе, чем по указанию администратора системы, то фирма Microsoft обязана предоставить способ установки драйверов "по желанию". Что и произошло в нашем случае.

При установке драйвера под операционной системой Windows2000/XP/Server 2003 может появиться сообщение следующего вида (рисунок 3.1).

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

Рис. 3.1
Предупреждение о том, что драйвер не подписан

По завершении инсталляции в окне Диспетчера Устройств (свойства устройства) можно увидеть сообщения об установленном драйвере, в частности, в форме, представленной на рисунке 3.2 (в графе "Цифровая подпись" для данного драйвера указано, что она отсутствует).

Рис. 3.2
Свойства драйвера в окне Диспетчера Устройств

Следует также удостовериться при помощи перечисленных в главе 2 программ, поступила ли информация и драйвере (и в достаточном ли объеме) в операционную систему. Программа DeviceTree предоставляет информацию, показанную на рисунке 3.3. На нем показаны коды IRP_MJ_Xxx, для которых драйвер зарегистрировал собственные процедуры обработки, а также более общая информация о драйвере, в частности, операционная система сама установила для него флаг LEGACY_DRIVER.

Рис. 3.3
Общие свойства драйвера в окне DeviceTree, поддерживаемые IRP_MJ_Xxx

Более подробно вопросы составления inf файлов для установки драйверов будут рассмотрены в главе 12.

В процессе установки драйвера при помощи Мастера установки система выполняет резервное копирование файлов, отражающих ее состояние перед установкой драйвера. Результаты этой работы сохраняются в директории \System Volume Information\RpNn на одном из логических дисков (Nn — это номер резервной точки). В том случае, если инсталляция драйвера приведет к нестабильной работе системы, можно восстановить ее состояние на момент сохранения данной резервной точки (reserve point) через запуск системной утилиты Пуск — Программы — Стандартные — Служебные — Восстановление Системы. Эта же утилита позволит администратору выполнить принудительное создание резервной копии, если в том имеется необходимость. Более подробно эти вопросы освещены в обстоятельной книге Ольги Кокоревой "Реестр Windows XP", рассматривающей многие аспекты организации Системного Реестра Windows NT, весьма важные для разработчика драйверов.

В большинстве случаев драйвер, установленный с помощью Мастера установки, можно отключить и включить (задействовать) снова, даже не выполняя при этом перезагрузку системы. Если между отключением и включением выполнить подмену бинарного файла драйвера (например, на новую версию), то в результате таких манипуляций в работу вступит новая версия драйвера. Разумеется, чтобы обеспечить такую возможность, драйвер должен иметь корректно написанные процедуры завершения работы и выгрузки. Подмену файла WDM драйвера РпР устройства вполне успешно можно выполнять в то время, когда отключены все PnP устройства, обслуживаемые таким драйвером. Однако выполнение установки новых версий драйвера только с помощью Мастера установки дает следующее небольшое преимущество: средствами системы можно восстанавливать предыдущую версию драйвера (если она существовала). Для этого необходимо в окне — см. рисунок 3.2 — выбрать пункт "Откатить".

Инсталляция с использованием программы Monitor

Как было сказано в главе 2, для запуска драйверов "в-стиле-NT" под управлением Windows NT предназначена программа Monitor, разработанная CompuWare Corporation и входящая в состав пакета Driver Studio (в том числе, в 30-дневную trial версию).

Эта программа прекрасно подходит для загрузки, запуска, остановки и удаления драйвера Example.sys. Интуитивно понятный графический интерфейс этой программы практически не требует дополнительных пояснений. Для запуска драйвера не следует перезагружать систему и самостоятельно модифицировать Системный Реестр, что удобно для проведения быстрых автономных тестов.

Если перед запуском драйвера из программы Monitor предварительно запустить программу DebugView, то все отладочные сообщения, которые были введены даже в функции DriverEntry, будут отображены в рабочем окне DebugView и могут быть сохранены в файле протокола (LOG-файле).

Более подробно режимы ознакомительного тестирования драйвера Example.sys при помощи собственного приложения пользовательского режима будут рассмотрены ниже.

Инсталляция с использованием сервисов SCM Менеджера

В операционной системе Windows NT имеется компонент, называемый Service Control Manager (SCM Менеджер). Удобство предоставляемых им услуг состоит в том, что, используя его функции в приложениях пользовательского режима, можно динамически запускать и выгружать драйверы, требующиеся только данному конкретному приложению, не прибегая к вызову Мастера Установки нового оборудования. Таким образом, приложение само определяет время присутствия драйвера в операционной системе.

Достаточно подробное описание программирования приложений с использованием функций SCM имеется в документации MSDN (практически, единственное место ее соприкосновения с потребностями собственно разработки драйверов), поставляемой отдельно или в составе пакетов Microsoft Visual Studio.

Работа с сервисами SCM менеджера начинается с вызова функции OpenSCManager (это имя можно рассматривать как "точку входа" в документацию MSDN по программированию с применением SCM функций) и завершается вызовом функции CloseServiceHandle.

Пример работы с функциями SCM будет рассмотрен ниже, в примере консольного приложения для тестирования драйвера Example.sys.

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


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

Приложение для тестирования драйвера Example.sys

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

////////////////////////////////////////////////////////////////////
// (Файл ExampleTest.cpp)
// Консольное приложение для тестирования драйвера Example.sys
//	22-Feb-2003  1.0.0 SVP
////////////////////////////////////////////////////////////////////

// Заголовочные файлы, которые необходимы в данном приложении:
#include
#include
#include
#include

// Внимание! Файл Ioctl.h должен быть получен из файла Driver.h
// (см. комментрарии к Driver.h) и размещен в одной директории с
// данным файлом (TestExam.cpp).
#include "Ioctl.h"

// Имя объекта драйвера и местоположение загружаемого файла
#define DRIVERNAME    _T("Example")
//#define DRIVERBINARY  _T("C:\\Example\\Example.sys")
//#define DRIVERBINARY  _T("C:\\Ex\\objchk_w2k\\i386\\Example.sys")
#define DRIVERBINARY  _T("C:\\Ex\\tester\\Example.sys")

// Функция установки драйвера на основе SCM вызовов
BOOL InstallDriver( SC_HANDLE  scm, LPCTSTR DriverName, LPCTSTR driverExec )
{
    SC_HANDLE Service =
            CreateService ( scm,    // открытый дескриптор к SCManager
                    DriverName,      // имя сервиса - Example
                    DriverName,      // для вывода на экран
                    SERVICE_ALL_ACCESS,    // желаемый доступ
                    SERVICE_KERNEL_DRIVER, // тип сервиса
                    SERVICE_DEMAND_START,  // тип запуска
                    SERVICE_ERROR_NORMAL,  // как обрабатывается ошибка
                    driverExec,            // путь к бинарному файлу
                    // Остальные параметры не используются - укажем NULL
                    NULL,    // Не определяем группу загрузки
                    NULL, NULL, NULL, NULL);
    if (Service == NULL) // неудача
    {
        DWORD err = GetLastError();
        if (err == ERROR_SERVICE_EXISTS) {/* уже установлен */}
        // более серьезная ощибка:
        else  printf ("ERR: CanТt create service. Err=%d\n",err);
        // (^^ Ётот код ошибки можно подставить в ErrLook):
        return FALSE;
    }
    CloseServiceHandle (Service);
return TRUE;
}

// Функция удаления драйвера на основе SCM вызовов
BOOL RemoveDriver(SC_HANDLE scm, LPCTSTR DriverName)
{
    SC_HANDLE Service =
            OpenService (scm, DriverName, SERVICE_ALL_ACCESS);
    if (Service == NULL) return FALSE;
    BOOL ret = DeleteService (Service);
    if (!ret) { /* неудача при удалении драйвера */ }

    CloseServiceHandle (Service);
return ret;
}

// Функция запуска драйвера на основе SCM вызовов
BOOL StartDriver(SC_HANDLE  scm, LPCTSTR DriverName)
{
    SC_HANDLE Service =
            OpenService(scm, DriverName, SERVICE_ALL_ACCESS);
    if (Service == NULL) return FALSE; /* open failed */
    BOOL ret =
            StartService( Service, // дескриптор
                          0,       // число аргументов
                          NULL  ); // указатель  на аргументы
    if (!ret) // неудача
    {
        DWORD err = GetLastError();
        if (err == ERROR_SERVICE_ALREADY_RUNNING)
            ret = TRUE; // OK, драйвер уже работает!
        else { /* другие проблемы */}
    }

    CloseServiceHandle (Service);
return ret;
}
// Функция останова драйвера на основе SCM вызовов
BOOL StopDriver(SC_HANDLE  scm, LPCTSTR DriverName)
{
    SC_HANDLE Service =
            OpenService (scm, DriverName, SERVICE_ALL_ACCESS );
    if (Service == NULL)  // Невозможно выполнить останов драйвера
    {
        DWORD err = GetLastError();
        return FALSE;
    }
    SERVICE_STATUS serviceStatus;
    BOOL ret =
    ControlService(Service, SERVICE_CONTROL_STOP, &serviceStatus);
    if (!ret)
    {
        DWORD err = GetLastError();
        // дополнительная диагностика
    }

    CloseServiceHandle (Service);
return ret;
}

// Соберем вместе действия по установке, запуску, останову
// и удалению драйвера (для обобщения сведений).
// (Однако пользоваться этой функцией в данном примере не придется.)
/* Закомментируем ее.
void Test_SCM_Installation(void)
{
    SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
    if(scm == NULL) // неудача
    {
        // Получаем код ошибки и ее текстовый эквивалент
        unsigned long err = GetLastError();
        PrintErrorMessage(err); // см. п. 2.1.5
        return;
    }
    BOOL res;
    res = InstallDriver(scm, DRIVERNAME, DRIVERBINARY );
    // Ошибка может оказаться не фатальной. Продолжаем:
    res = StartDriver  (scm, DRIVERNAME );
    if(res)
    {
        //Е Здесь следует разместить функции работы с драйвером
        .. .. ..
        res = StopDriver   (scm, DRIVERNAME );
        if(res) res = RemoveDriver (scm, DRIVERNAME );
    }
CloseServiceHandle(scm);
return;
}
*/

#define SCM_SERVICE
// ^^^^^^^^^^^^^^^^ вводим элемент условной компиляции, при помощи
// которого можно отключать использование SCM установки драйвера
// в тексте данного приложения. (Здесь Ц использование SCM включено.)

// Основная функция тестирующего приложения.
// Здесь минимум внимания уделен диагностике ошибочных ситуаций.
// В действительно рабочих приложениях следует уделить этому
// больше внимания!

int __cdecl main(int argc, char* argv[])
{
    #ifdef SCM_SERVICE
    // Используем сервис SCM для запуска драйвера.
    BOOL res; // Получаем доступ к SCM :
    SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
    if(scm == NULL) return -1; // неудача

    // Делаем попытку установки драйвера
    res = InstallDriver(scm, DRIVERNAME, DRIVERBINARY );
    if(!res) // Неудача, но возможно, он уже инсталлирован
        printf("Cannot install service");

    res = StartDriver  (scm, DRIVERNAME );
    if(!res)
    {
        printf("Cannot start driver!");
        res = RemoveDriver (scm, DRIVERNAME );
        if(!res)
        {
            printf("Cannot remove driver!");
        }
        CloseServiceHandle(scm); // Отключаемся от SCM
        return -1;
    }
    #endif

    HANDLE hHandle =           // Получаем доступ к драйверу
            CreateFile( "\\\\.\\Example",
                        GENERIC_READ | GENERIC_WRITE,
                        FILE_SHARE_READ | FILE_SHARE_WRITE,
                        NULL,
                        OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL );
    if(hHandle==INVALID_HANDLE_VALUE)
    {
        printf("ERR: can not access driver Example.sys !\n");
        return (-1);
    }
    DWORD BytesReturned;    // Переменная для хранения числа
                            // переданных байт
    // Последовательно выполняем обращения к драйверу
    // с различными кодами IOCTL:

    unsigned long ioctlCode=IOCTL_PRINT_DEBUG_MESS;
    if( !DeviceIoControl(   hHandle,
                    ioctlCode,
                    NULL, 0,	// Input
                    NULL, 0,	// Output
                    &BytesReturned,
                    NULL )  )
    {
        printf( "Error in IOCTL_PRINT_DEBUG_MESS!" );
        return(-1);
    }

    ioctlCode=IOCTL_CHANGE_IRQL;
    if( !DeviceIoControl(   hHandle,
                    ioctlCode,
                    NULL, 0,	// Input
                    NULL, 0,	// Output
                    &BytesReturned,
                    NULL )  )
    {
        printf( "Error in IOCTL_CHANGE_IRQL!" );
        return(-1);
    }

    ioctlCode=IOCTL_TOUCH_PORT_378H;
    if( !DeviceIoControl(   hHandle,
                    ioctlCode,
                    NULL, 0,  // Input
                    NULL, 0,  // Output
                    &BytesReturned,
                    NULL )  )
    {
        printf( "Error in IOCTL_TOUCH_PORT_378H!" );
        return(-1);
    }

    // Следующий тест. Получаем 1 байт данных из драйвера.
    // По окончании данного вызова переменная xdata должна
    // содержать значение 33:
    unsigned char xdata = 0x88;
    ioctlCode=IOCTL_SEND_BYTE_TO_USER;
    if( !DeviceIoControl(   hHandle,
                    ioctlCode,
                    NULL, 0,  // Input
                    &xdata, sizeof(xdata),// Output
                    &BytesReturned,
                    NULL )  )
    {
        printf( "Error in IOCTL_SEND_BYTE_TO_USER!" );
        return(-1);
    }

    // Вывод диагностического сообщения в консольном окне:
    printf("IOCTL_SEND_BYTE_TO_USER: BytesReturned=%d xdata=%d",
                    BytesReturned, xdata);

    // Выполнение следующего теста в Windows NT приведет к
    // фатальному сбою операционной системы (намеренно выполненное
    // падение ОС может быть полезно при изучении, например,
    // организации crash dump файла и работы с отладчиком).
    /*
    ioctlCode=IOCTL_MAKE_SYSTEM_CRASH;
    if( !DeviceIoControl(   hHandle,
                    ioctlCode,
                    NULL, 0,		        // Input
                    NULL, 0,	// Output
                    &BytesReturned,
                    NULL )  )
    {
        printf( "Error in IOCTL_MAKE_SYSTEM_CRASH!" );
        return(-1);
    }
    */
    // Закрываем дескриптор доступа к драйверу:
    CloseHandle(hHandle);

    #ifdef SCM_SERVICE
    // Останавливаем и удаляем драйвер. Отключаемся от SCM.
    res = StopDriver   (scm, DRIVERNAME );
    if(!res)
    {
        printf("Cannot stop driver!");
        CloseServiceHandle(scm);
        return -1;

    }

    res = RemoveDriver (scm, DRIVERNAME );
    if(!res)
    {
        printf("Cannot remove driver!");
        CloseServiceHandle(scm);
        return -1;
    }

    CloseServiceHandle(scm);
    #endif

return 0;
}

 Сообщения намеренно введены на английском языке. Использование кириллицы в консольных приложениях Windows для правильного отображения на экране требует дополнительного преобразования с использованием функции CharToOem.

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

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

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

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

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

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

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

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

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

00000004  0.00172536 -Example- Create File is

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

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

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

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

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

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

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

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

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

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

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

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

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

00000029  0.00263888 -Example- In Close handler.

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

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

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

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


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

Заключение

В данной главе был рассмотрен простейший драйвер Example.sys, реализованный как драйвер "в-стиле-NT" (тип драйвера, называемый еще Legacy Driver). Ha его примере были представлены начальные сведения об основных этапах разработки простого драйвера: компиляция и сборка checked версии в среде Microsoft Visual Studio, в среде Windows DDK, реализация тестирующего приложения пользовательского режима, инсталляция и запуск. Изложение этого материала можно признать поверхностным и беглым, однако, предназначено оно для того, чтобы дать первый фактический материал начинающему разработчику для более продуктивного чтения последующих глав этой книги.


Глава 4. Архитектура Windows NT 5. Введение

Глава 4

Архитектура Windows NT 5. Введение

Различия базисов операционных систем Windows 2000, Windows XP и Windows Server 2003 настолько невелики, что позволяет в большинстве случаев говорить о них собирательно, используя термин Windows NT 5. Однако Windows 2000 является первой из них, потому о ней особенная речь.

Операционная система Windows NT 5.0 (то есть Windows 2000) представляет наиболее энергичную попытку создания гибкой и самоуправляемой операционной системы в компьютерной истории.

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


Глава 4. Архитектура Windows NT 5. Введение

Цели разработки

Впервые концепции Windows NT ('New Technology', безусловно, вызывающее название из числа тех еще!) начали приобретать черты реальности в начале 1989 года. Однако хотя дата рождения столь отдаленна, пять фундаментальных NT задач остались неизменными.

  • Совместимость. Операционная система должна поддерживать максимально возможное множество программного и аппаратного обеспечения.
  • Переносимость. Операционная система должна функционировать на максимально возможном количестве имеющихся сейчас и ожидаемых в перспективе аппаратных платформ.
  • Расширяемость. Поскольку требования рынков постоянно растут, операционная система должна уметь с легкостью расширять набор своих возможностей и перечень поддерживаемой аппаратуры при минимальном вмешательстве в свой внутренний код.
  • Надежность и устойчивость. Операционная система должна быть стойкой к неумышленному или преднамеренному неправильному использованию компонентов. Пользовательские приложения не должны иметь возможности приведения системы к фатальным сбоям. (Несмотря на огромное количество злых шуток в адрес зависаний Windows, следует, наконец, признать, что XP Prof работает хорошо.)
  • Производительность. Операционная система должна обеспечивать хорошую производительность на всех поддерживаемых аппаратных платформах.

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

Уровни аппаратных привилегий в Windows NT 5

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

Пользовательское приложение функционирует в специальном режиме (относительно аппаратного обеспечения), называемом 'user mode' — пользовательский режим. В пределах этого режима, код приложения ограничен выполнением "безвредных" инструкций. Например, через реализацию "таинственного" маппинга (mapping, отображение) виртуальной памяти (страничное представление виртуальной памяти) пользовательский код лишается возможности доступа к виртуальной памяти, предоставленной другим приложениям (за исключением случаев обоюдного согласия, что реализуется специально предназначенными на тот случай методами). Инструкции аппаратного ввода/вывода также не могут быть выполнены кодом пользовательского режима. Целый класс инструкций центрального процессора (называемых привилегированными) запрещен в Windows NT для выполнения кодом пользовательского режима, как, например, команды процессора IN, OUT (результат таких попыток запечатлен на рисунке 1.1). Если вдруг приложению потребуется выполнить что-нибудь из числа таких запрещенных для нее действий, оно должно запросить соответствующую службу операционной системы.

Код самой операционной системы выполняется в так называемом 'kernel mode' — режиме ядра (режиме уровня ядра). Код режима ядра вправе выполнить любую процессорную инструкцию, не исключая инструкций ввода/вывода. Память, принадлежащая любому приложению, может быть доступна коду режима ядра, конечно, если страничная память приложения в данный момент не сброшена на жесткий диск.

Современные процессоры реализуют несколько форм привилегированного режима в отличие от непривилегированного. Код режима ядра выполняется в привилегированном контексте, в то время как пользовательский код выполняется в непривилегированной среде. Так как разные процессоры (и платформы на их основе) реализуют привилегированные режимы по-разному, то, для обеспечения переносимости, разработчики операционной системы ввели особые абстрактные элементы программирования, которые позволяют разграничивать пользовательский режим и режим ядра. Код операционной системы использует их для переключения привилегированного/непривилегированного контекста, следовательно, при перенесении операционной системы только лишь код этих дополнительных элементов необходимо "портировать" (переписывать конкретно под специфику новой аппаратной платформы). На платформе Intel пользовательский режим реализуется из набора инструкций Ring 3 (Кольца 3), в то время как режим ядра реализован с использованием Ring 0 (Кольца 0).

Драйверы уровня ядра (режима ядра) работают в привилегированном контексте. Соответственно, плохо написанный драйверный код может оказаться вредоносным для операционной системы. Разработчик должен с особым вниманием относиться к создаваемому коду, чтобы не обрушить все здание операционной системы. Фирма Microsoft пытается решить проблему надежности драйверов, поставляемых в составе дистрибутива Windows, через механизм тестирования и подписания драйверов.

Переносимость

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

Рис. 4.1
Слои операционной системы Windows NT 5

Слой аппаратных абстракций (Hardware Abstraction Layer, HAL) изолирует процессорные и платформенные особенности от кода операционной системы. Его услугами Microsoft предлагает пользоваться и разработчику драйверного кода. Вполне возможно так написать драйвер, что для перенесения его на другую платформу потребуется разве что перекомпилировать его. Как можно это сделать, если изначально драйвер есть такая программная единица, которая жестко привязана и к своему устройству, и к конкретному процессору, и к конкретной платформе?! Просто драйвер должен обратиться к использованию средств уровня HAL для взаимодействия с аппаратными регистрами и аппаратной поддержкой шин. В отдельных случаях разработчик драйвера может опереться на код, предоставляемый Диспетчером ввода/вывода, для работы с совместно используемыми аппаратными ресурсами. Например, при DMA операциях (прямого доступа к памяти) используется такая абстракция программирования, как объект адаптера

Расширяемость

На рисунке 4.1 обозначена и еще одна важная особенность представленной архитектуры — ядро отделено от слоя, который носит название "исполнительные компоненты" (Executive).

В данном случае, ядро несет ответственность за планировку активности программных потоков (threads). Поток является всего лишь "независимой тропинкой" в выполнении программного кода. Чтобы сохранить независимость от деятельности других потоков, для каждого из них необходимо сохранять уникальный потоковый контекст (thread context). Потоковый контекст состоит из состояния регистров процессора (включая также изолированный стек и счетчик инструкций, Program Counter), сохраненного ID (идентификатора потока, так называемого Thread ID или TID), значения приоритета, распределения памяти, связанной с потоком (Thread Local Storage), и другой информации, имеющей отношение к данному потоку.

Обязанностью планировщика потоков является определение, какой поток должен выполняться в данный момент. В среде с единственным процессором, в каждый момент времени только один поток получает в свое распоряжение процессор. В многопроцессорной конфигурации разные потоки могут выполняться на разных процессорах, реализуя настоящую параллельность выполнения кода. Планировщик в большинстве случаев выделяет потоку процессор на фиксированный временной интервал, известный под названием thread time quantum (потоковый временной квант). Предоставление процессора происходит, главным образом, на основе величины приоритета потока.

Так как основной задачей ядра является управление потоками, работа по управлению памятью, вопросами доступа (security) и действиями по вводу/выводу возлагается на другие компоненты операционной системы. Эти компоненты известны под собирательным названием 'Executive', Исполнительные Компоненты. Они сконструированы как модульное программное обеспечение (хотя, Диспетчер ввода/вывода сам является существенным исключением из этого правила).

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

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

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

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

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

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


Глава 4. Архитектура Windows NT 5. Введение

Исполнительные компоненты

Так как Исполнительные компоненты представляют базисные сервисы операционной системы Windows NT 5 (в дополнение к планированию потоков, осуществляемых ядром), их обязанности ясно очерчены.

В таблице 4.1 представлены наименования основных исполнительных компонентов операционной системы. В первом столбце указаны сокращения, обычно являющиеся первыми символами имен функций, которые данный компонент предоставляют разработчику для использования при программировании в режиме ядра. Например, функция RtlCopyMemory, являющаяся аналогом известной функции пользовательского режима memcpy, предоставляется библиотекой времени выполнения Rtl (Run Time Library).

Таблица 4.1. Исполнительные компоненты Windows NT5

Сс | Диспетчер кэша
Dbg | Поддержка отладки
Ex | Поддержка исполняющей подсистемы Ex(ecutive)
FsRtl | Библиотека времени выполнения для поддержки файловой системы (File System Run-Time Library)
Hal | Диспетчер уровня аппаратных абстракций, the Hardware Abstraction Layer (HAL)
Inbv | Драйвер инициализации системы/загрузки VGA
Init | Инициализация системы
Interlocked | Потокобезопасное оперирование переменными
Io | Диспетчер ввода/вывода Io (I/O Manager)
Kd | Поддержка Kernel Debugger
Ке | Подпрограммы ядра (Kernel)
Ki | Обработка ядра
Ldr | Загрузчик образа
Lpc | Локальный вызов процедур (Local Procedure Call)
Lsa | Local Security Authority
Mm | Менеджер памяти (Менеджер Виртуальной памяти, VMM)
Nls | Лингвистическая поддержка (National Language Support)
Nt | NT Native API
Ob | Менеджер объектов (Object Manager)
Pfx | Обработка префиксов
Po | Менеджер электропитания
Ps | Поддержка процессов (Process Structure)
Rtl | Библиотека времени выполнения (Run-Time Library)
Se | Управление безопасностью и обеспечение привилегий
Zw | Альтернативный интерфейс Native API
другие | Вспомогательные функции и библиотека времени выполнения С

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

Интерфейс системных служб

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

Менеджер (диспетчер) объектов

Object Manager. Практически все услуги, предоставляемые операционной системой, оперируют с такой распространенной абстракцией, как объекты, хотя это и не стопроцентные объекты, по некоторым признакам, известным из объектно-ориентированного программирования. Например, программа, выполняемая в пользовательском режиме, которой необходимо синхронизировать несколько собственных потоков, может запросить у операционной системы объект синхронизации Event (событие), а на самом деле — просто особым образом обслуживаемую структуру данных. Система предоставляет Event в форме системного объекта, на который из программы пользовательского режима можно ссылаться только по дескриптору (handle). Файлы, процессы, потоки, события (Events), секции памяти (Memory Sections) и даже подразделы Системного Реестра (Registry Keys) поддерживаются операционной системой как системные объекты. Все объекты создаются и уничтожаются централизованно — Менеджером Объектов. Это позволяет получить унификацию (единообразие) доступа к объектам, контроля над их временем жизни и обеспечивать безопасность и права доступа к ним.

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

Менеджер конфигурирования

Configuration Manager. Менеджер конфигурирования Windows NT 5 конструирует модель всей доступной аппаратуры и всего инсталлированного программного обеспечения, которое имеется на компьютере. Для хранения образа этой модели используется база данных, хорошо известная под названием Системный Реестр. Драйверы устройств используют информацию Реестра для уточнения множества характеристик окружения, в котором им придется работать. С введением спецификации Plug and Play роль Системного Реестра для драйверов, реализованных по WDM модели, существенно снизилась.

Менеджер процессов

Process Manager. Предоставляет функции, начинающиеся с префикса Ps (например, PsGetVersion — получить версию операционной системы). Функции PsXxx могут выполняться на разных уровнях IQRL, что следует уточнять в документации DDK.

В чем заключается работа Менеджера процессов?

Процесс является средой, в которой существует (выполняется) поток. Каждый процесс определяет собственное адресное пространство и содержит элементы идентификации для определения прав доступа (security identity). Важно отметить, что в Windows процесс не выполняется; выполняется поток, который является единицей выполнения, в то время, как процесс является "фигурой" собственности. Процесс владеет одним или несколькими потоками.

Менеджер процессов Windows 2000/XP/2003 является исполнительным компонентом, который управляет созданием процесса и предоставляет ему окружение, в котором работают программные потоки. Менеджер Процессов в своей работе опирается, главным образом, на другие исполнительные компоненты (например, Менеджера Объектов и Менеджера виртуальной памяти), так что можно сказать, что он представляет верхний уровень абстрагирования над другими системными сервисами более низкого уровня.

Драйверы редко контактируют непосредственно с Менеджером процессов. Как правило, они опираются на другие системные сервисы для доступа к среде процесса (process environment). Например, драйвер хочет быть уверен, что буферная область памяти в области адресов, находящихся во владении процесса, удерживается в течение всего времени передачи данных. Системные процедуры с префиксом Mm (относящиеся к Менеджеру памяти) предоставляют драйверу средства для такого удержания.

Менеджер виртуальной памяти

Virtual Memory Manager, VMM. В операционной системе Windows 2000/XP/2003 (впрочем, как и в Windows 9x) адресное пространство процесса является непрерывным и непрерывно адресуемым (flat). Размер адресуемого таким образом пространства в 32 разрядной версии Windows составляет 4 ГБ (2 в степени 32), что соответствует использованию 32 разрядного указателя. При этом только нижние 2 ГБ доступны для использования кодом пользовательского режима. Программный код и данные пользовательских программ должны размещаться в этой нижней половине адресного пространства. В случае, если пользовательские программы используют совместно динамически подключаемые библиотеки (DLL), то этот библиотечный код также должен размещаться в первых двух гигабайтах адресного пространства (эта схема претерпевает некоторые изменения — в сторону увеличения пользовательского адресного пространства до 3 ГБ — лишь в Enterprise Server при его соответствующей настройке).

Верхние 2 ГБ адресного пространства каждого процесса содержат код и данные, доступ к которым возможен только из программного кода, выполняющегося на уровне ядра. Верхние 2 ГБ используются кодом уровня ядра совместно от процесса к процессу. Если вы выполните распечатку адресов каких-нибудь объектов, переменных или процедур в окне DebugView или DebugPrint, то сразу же увидите, что код драйвера отображается на адресное пространство выше 2 ГБ (адреса превышают 0x80000000).

Менеджер памяти (VMM) осуществляет управление памятью от имени всей операционной системы. Для обычной программы пользовательского режима это означает выделение памяти и управление адресным пространством и физической памятью ниже границы 2 ГБ. В том, достаточно обычном, случае, когда процессу пользовательского режима не хватает физической памяти, VMM создает иллюзию наличия памяти путем виртуализации запроса. Необходимая память выделяется страницами (то есть блоками соответствующего размера, для Intel платформы — 4 КБ) на жестком диске (что называется — paging). По мере необходимости доступа к ней со стороны потоков процесса выделенные страницы перемещаются в физическую память. Таким образом, физическая память становится совместно используемым ресурсом всех процессов.

Менеджер Виртуальной Памяти выступает также и в роли ответственного за распределение памяти в том смысле, что управляет "кучей" (heap area) для программного кода уровня ядра. Для своих нужд драйверы через соответствующие вызовы (например, вызов ExAllocatePool, в конечном счете, обрабатываемый VMM) могут запросить выделение областей памяти в страничной (то есть организованной странично и допускающей сброс на жесткий диск) или нестраничной памяти.

Средства локальных процедурных вызовов

Local Procedure Call (LPC), локальный процедурный вызов, является механизмом вызовов между процессами на одном компьютере. Так как меж-"процессный" вызов (interprocess call) может проходить между разными адресными пространствами, то существуют и соответствующий Исполнительный компонент, который делает это действие возможным и эффективным. Как правило, драйверному коду не требуется осуществление вызова другого процесса и, соответственно, LPC средства.

Диспетчер (менеджер) ввода/вывода

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

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

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

При посредничестве Диспетчера ввода/вывода с драйвером могут общаются и компоненты уровня ядра, например, могут обращаться другие драйверы (вспомним, хотя бы, случай с программой Debug Print Monitor, глава 2).

Расширения базовой операционной системы

Исполнительные компоненты Windows 2000/XP/2003 определяют и представляют основные сервисы операционной системы. Однако эти сервисы никогда не предоставляются программам пользовательского режима непосредственно. Вместо этого разработчики из Microsoft определили несколько интерфейсов прикладного программирования (Application Programming Interfaces), при помощи которых код пользовательского режима может обращаться к абстракциям системных служб.

Эти интерфейсы формируют различные среды (environmental subsystems), в которых и обитают прикладные программы. В настоящее время в Windows NT 5 представлены:

  • Win32 подсистема, являющаяся собственным (native-mode) API для 32-разрядных версий Windows. Bce остальные среды (environmental subsystems) используют эту подсистему для выполнения своей работы. Все новые приложения 32-разрядных Windows 2000/XP/2003 (а также и все перенесенные) полагаются на Win32 как на среду своего функционирования. Из-за важности (и достаточно интересной реализации) эта подсистема будет рассмотрена далее более детально. Следует, однако, отметить, что в 64-разрядной версии Windows (версии XP/Server 2003) она сама становится клиентом WOW64 (см. ниже).
  • Virtual DOS Machine (VDM, виртуальная DOS машина) подсистема обеспечивает 16-разрядную MS DOS операционную среду для старых DOS приложений. Несмотря на уверения в совместимости, множество существующих DOS программ в этой среде не работают надлежащим образом. Происходит это по той причине, что Microsoft, проповедуя консервативный подход, предоставляет эмуляцию аппаратуры вместо возможности непосредственного обращения к ней. В результате, прямой доступ к аппаратуре приводит к ограничению со стороны операционной системы и, зачастую, отказу данного DOS приложения работать.
  • Подсистема 'Windows on Windows' (WOW) поддерживает операционную среду для возможности работы старых 16-битных приложений Windows (например, Windows 3.x). В 64-разрядных клонах Windows XP/Server 2003 подсистема WOW 64 служит для запуска созданных ранее 32-разрядных приложений, перенесенных на новые аппаратные платформы.
  • Подсистема POSIX обеспечивает выполнение Unix-приложений, которые удовлетворяют стандарту POSIX 1003.1. К сожалению, большинство перенесенных Unix-подобных систем приложений не работает должным образом в этой подсистеме. В данном случае большинство Unix-приложений переносятся под Windows путем переписывания под Win32 подсистему или они изначально создаются с использованием специальных программных пакетов, типа MainWin, Motif и OpenMotif.
  • Подсистема OS/2 создает среду выполнения для 16-разрядных программ операционной системы OS/2 — по крайней мере, тех из них, которые не используют в своей работе сервисов такого компонента OS/2, как Presentation Manager (PM). На эту подсистему можно рассчитывать только в версии Windows для платформы Intel (x86).

Каждое приложение однозначно связано с одной средой выполнения. Приложения не могут осуществлять API вызовы к другим исполнительным средам. Кроме того, подсистема Win32 является основной в 32-разрядных версиях Windows NT 5.x. Другие подсистемы эмулируют соответствующие свойства реализуемых сред через средства и методы Win32. Соответственно, параметры выполнения программ в этих средах деградируют и существенно уступают аналогичным программам для Win32.

Подсистема Win32

В качестве основного для Windows 2000/XP/Server 2003 интерфейса API (32-разрядных версий), подсистема Win32 ответственна за:

  • графический пользовательский интерфейс (Graphical User Interface, GUI), который наблюдает пользователь системы. Win32 отвечает за реализацию видимых окон, диалоговых элементов и элементов управления (кнопок, полос прокрутки и т.п.), то есть общий стиль оформления системы.
  • Консольный ввод/вывод, включая клавиатуру, мышь и дисплей для всей операционной системы и других подсистем.
  • Функционирование Win32 API, при помощи которого приложения и другие подсистемы взаимодействуют с исполнительными компонентами режима ядра.

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

  • Функции, которые предоставляются пользователю для управления окнами, меню, диалогами и элементами контроля (кнопками, полосами прокрутки, переключателями, закладками и т.п.).
  • Функции GDI, реализующие прорисовку изображений на физических устройствах, экране, принтере, графопостроителе.
  • Функции, которые управляют неграфическими ресурсами, такими как процессы, программные потоки, файлы и объекты синхронизации. KERNEL-функции тесно связаны с системными службами исполнительных компонентов.

Со времен NT 4.0 большая часть функций первых двух категорий из приведенной выше классификации была реализована в режиме ядра. Пользовательские процессы, которые запрашивают услуги GUI, обращаются непосредственно к коду режима ядра при использовании System Service Interface (Интерфейса Системных Служб). Код, представляющий эти функции и работающие в режиме ядра, локализован в модуле WIN32K.SYS.

Функции третьей категории при обработке запросов от пользовательских процессов опираются на стандартный серверный процесс CSRSS.exe (Client-Server Runtime Subsystem), который и обращается собственно к коду исполнительных компонентов для завершения обработки этих обращений.

Другие существенные компоненты операционной системы

В дополнение к подсистемам, реализующим среду выполнения кодов DOS, Windows, POSIX и OS/2, имеется еще несколько ключевых системных компонентов, которые реализованы как процессы пользовательского режима. Среди них:

  • Security Subsystem, подсистема безопасности, которая управляет локальной и удаленной безопасностью (защитой от неправомерного доступа) с использованием ряда процессов и динамических библиотек. Часть работы Active Directory протекает как раз среди этой логической подсистемы.
  • Service Control Manager (SCM, функции которого использовались для запуска драйвера в Главе 3), Менеджер Управления Сервисами, управляет процессами-демонами (сервисами), и драйверами устройств.
  • Процессы поддержки RPC вызовов (Remote Procedure Call, вызов удаленной процедуры), которые оказывают поддержку приложениям, распространяемым по сети. Прибегая к использованию вызовов удаленных процедур, приложения могут выполнять свою работу с использованием множества сетевых компьютеров.

Компоненты обслуживания операций ввода/вывода, работающие в режиме ядра

Цели разработки подсистемы ввода/вывода

Подсистема ввода/вывода вносит корректировки в список задач новых систем Windows, но особенно следует отметить:

  • Конфигурируемость и в терминах аппаратуры, и в терминах программного обеспечения. Для драйверов Windows это означает полную поддержку PnP спецификации для шин и устройств.
  • Приоритетность и прерываемость. Код, написанный для обслуживания ввода/вывода, никогда не должен блокироваться и должен содержать безопасные программные потоки.
  • Безопасность при использовании на многопроцессорных платформах. Один и тот же драйверный код должен безошибочно работать и на однопроцессорных и на многопроцессорных компьютерах.
  • Объектная ориентированность. Услуги, предоставляемые кодом, должны "формулироваться" в терминах вполне определенных структур данных, которые служат выполнению разрешенных операций.
  • Пакетное управление. Запросы, сделанные к подсистеме ввода/вывода формулируются, передаются и отслеживаются с помощью четкого формата "рабочего рецепта", известного как IRP пакет (I/o Request Packet, пакет запроса на ввод/вывод).
  • Поддержка асинхронного ввода/вывода. Подсистема ввода/ввода должна позволять коду программы, по обращению которой создан запрос IRP, выполняться параллельно тому программному коду, который осуществляет обработку запроса. Также должен существовать механизм оповещения инициатора вызова о полном завершении обработки запроса.

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

Типы драйверов Windows NT5

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

Если взять самый крупный план, то можно разделить драйверы, используемые Windows NT 5, на две группы: драйверы пользовательского режима и драйверы режима ядра. Первые, как подразумевает их название, являются системным программным кодом, функционирующим в пользовательском режиме. В качестве примера можно назвать драйверы-симуляторы (виртуализаторы) для воображаемой аппаратуры или новых исполнительных подсистем (WOW, POSIX, OS/2). Так как Windows 2000 не допускает непосредственной работы с аппаратурой для кода пользовательского режима, то такие драйверы должны полагаться в этой части на драйверы, работающие в режиме ядра. Предположим, в примере главы 3 (драйвер Example.sys) была бы создана DLL, которая работала бы в пользовательском режиме и общалась бы с драйвером на манер приложения ExampleTest. В свою очередь, если бы пользовательские приложения обращались бы к ней (а не к помощи функций CreateFile, DeviceIoControl и т.п.) в те моменты, когда желали бы обратиться к драйверу, то тогда такая DLL и была бы типичным драйвером пользовательского режима.

Драйверы режима ядра (kernel-mode drivers) целиком состоят из кода системного уровня, выполняющегося в режиме ядра. Поскольку коду режима ядра разрешено работать непосредственно с аппаратурой (мы это видели в коде примера главы 3 при обработке IOCTL запроса IOCTL_TOUCH_PORT_378H), такие драйверы имеют прямой доступ к управлению устройствами, содержащимися в компьютере или подключенными к компьютеру. Разумеется, ничто не может помешать такому драйверу представлять вымышленную аппаратуру — воля разработчика, где этим заниматься — в пользовательском режиме или режиме ядра.

Ограничившись категорией драйверов режима ядра, чему и посвящена данная книга, перемещаемся как раз в код режима ядра. На этом уровне можно выполнить деление драйверов еще на две категории: наследованные (legacy, доставшиеся как наследство от Windows NT 3.5, 4) и WDM драйверы. Пример драйвера Example.sys, рассмотренный в предыдущей главе, как раз и является примером legacy драйвера. Он использует функции заголовочного файла ntddk.h, скомпилирован без директивы DRIVERTYPE=WDM, не зарегистрировал ни одной процедуры типового WDM драйвера, не выполнил подключение объекта своего устройства к родительскому объекту и не реализует свои запросы путем обращения к стеку устройств. Драйверы типа legacy (если не говорить о задачах проникновения в режим ядра с задачами чистого программирования или исследования) предназначены для работы с теми устройствами, которые не поддерживают PnP спецификацию, поскольку только разработчик драйвера знает, как различить его присутствие в системе, не говоря уже о приемах работы с ним.

К счастью, практически все знания, касающиеся наследуемых драйверов NT, полностью применимы к модели WDM, по которой можно построить драйверы, работающие в Windows 2000/XP/Server 2003 (и Windows 98, Ме). Правда, как мы видели на примере Example.sys, и некоторые экземпляры драйверов типа legacy ("в-стиле-NT") могут работать во всех перечисленных ОС.

Способность драйверов WDM работать по PnP спецификации включает в себя: участие в управлении энергоснабжением системы, автоматическое конфигурирование устройства и возможность его "горячего" подключения. Корректно написанный WDM драйвер может быть использован и под Windows NT 5.x и под Windows 98/Ме, хотя Microsoft не гарантирует бинарной совместимости (простого переноса .sys файла в другую ОС, как это получилось с Example.sys). B большинстве случаев все еще необходима перекомпиляция под Windows 98 DDK.

Наследуемые и WDM драйверы можно разделить также и на другие три категории: высокоуровневые, средне и низкоуровневые драйверы. Как подразумевает эта классификация, высокоуровневые драйверы зависят от драйверов среднего и низкого уровня в выполнении своих задач. Драйверы среднего уровня (intermediate, промежуточные), соответственно, в своей работе зависят от функционирования драйверов низкого уровня (low-level drivers).

К высокоуровневым драйверам, например, относятся драйверы файловых систем (file system drivers, FSDs). Такие драйверы предоставляют инициаторам запросов нефизическую абстракцию получателя, и уже эти запросы транслируется в специфические запросы к лежащим ниже драйверам. Необходимость в создании высокоуровневых драйверов возникает тогда, когда основные услуги аппаратуры уже реализованы драйверами нижнего уровня, и требуется только создать новую фигуру абстрагирования, которая необходима для предъявления инициатору запросов (клиенту драйвера).

Фирма Microsoft поставляет комплект программного обеспечения Installable File System (IFS) Kit, который распространяется отдельно от MSDN или других продуктов. Пакет IFS Kit требует наличия пакета DDK (и некоторых других средств) для того, чтобы заняться разработкой файловой системы всерьез. При этом существуют многочисленные ограничения на то, какие типы файловых систем могут быть получены при помощи этого пакета (IFS Kit). Дополнительную информацию по пакету IFS Kit можно получить на интернет-сайте Microsoft.

Драйверы среднего уровня могут быть проиллюстрированы такими примерами, как драйверы зеркальных дисков, классовые драйверы (class drivers), мини-драйверы (mini drivers) и фильтр-драйверы (filter drivers). Эти драйверы позиционируют себя между высокоуровневыми абстракциям высокоуровневых драйверов и средствами физической поддержки на нижних уровнях. Например, драйвер зеркальных дисков получает запрос от высокоуровневого драйвера файловой системы (FSD), транслирует этот запрос в два запроса к двум разным дисковым драйверам более низкого уровня. При этом нет никакой необходимости в том, чтобы кто-нибудь из драйверов верхнего или нижнего уровней был в курсе, как на самом деле произошла "зеркализация" и была ли она вообще.

Драйверы класса являются возможностью повторного использования кода в пределах драйверной модели. Так как много драйверов определенного типа могут иметь много общего, то программный код, описывающий общие места, может быть помещен в общий для данного типа классовый драйвер, отдельно от специфичного для обслуживания конкретных устройств кода. Например, драйверы HID (human interface device, устройства ввода по шине USB) используют такие сходства. Драйверы специфических HID устройств могли бы быть, в такой ситуации, реализованы как мини-драйверы, взаимодействующие с классовыми драйверами. Мини-драйверы отличаются тем, что они, как правило, общаются только с другими драйверами верхнего уровня (представляющими для них оболочку), не выходя на "прямой контакт" с Диспетчером ввода/вывода. Мини-драйвер и его клиент наверху имеют заранее обусловленный протокол общения, и обычно мини-драйвер экспортирует набор функций своего интерфейса по запросу драйвера-оболочки, после чего возможно общение между драйверами минуя Диспетчер ввода/вывода, что существенно ускоряет работу.

Фильтр-драйверы (filter drivers) являются драйверами среднего уровня, которые позиционирует себя во время загрузки над или под интересующим их драйвером и перехватывают запросы, идущие к нему или от него. Фильтр-драйверы, как правило, предназначены для модификации запроса к существующему драйверу или для реализации некоей дополнительной функции, изначально не заложенной в существующем драйвере (в простейшем случае это может быть подсчет пропущенных IRP пакетов). В операционной системе фильтр-драйверы с большой долей вероятности можно опознать по отсутствию имен у объектов устройств, созданных такими драйверами (при использовании программы DeviceTree).

Наконец, документация DDK упорно внедряет такую категорию (по отношению к WDM драйверам) как функциональные драйверы (functional drivers), причем эти драйверы могут быть либо классовыми, либо мини-драйверами. Что имеет в виду документация DDK, когда вводит термин "функциональный"? Дело в том, что такие драйверы всегда работают как интерфейс между абстрактным запросом ввода/вывода и кодом низкоуровневого физического драйвера, "физического" — в том смысле, что он связан непосредственно с устройством и в его функционировании хорошо просматриваются особенности этого устройства. Характерна в данном случае следующая деталь. Когда типовой WDM драйвер нормального PnP устройства собирается подключить себя к стеку устройств (это должно происходить в процедуре AddDevice), он выполняет подключение функционального объекта устройства (FDO), созданного им самим, к физическому объекту устройства (PDO), предоставленному ему родительским драйвером. Как правило, родительским является драйвер шины, который первоначально обнаружил подключение данного устройства, инициировав затем обращение к рассматриваемому WDM драйверу устройства. Вот здесь и проявляется функциональность последнего: он получает общие функциональные запросы, а превращает их в низкоуровневые (например, в URB запросы для устройств USB), понятные шинному драйверу и устройству, олицетворяя при этом для своего клиента функции устройства, а не его конструкцию и внутреннюю логику.

Специальные драйверные архитектуры

Microsoft предлагает специфические драйверные архитектуры для нескольких типов или классов устройств, а именно:

  • Видеодрайверы.
  • Драйверы принтеров.
  • Драйверы устройств мультимедиа.
  • Сетевые драйверы.

Рассмотрение этих архитектур выходит за рамки задач данной книги, поэтому ограничимся лишь данным перечислением.


Глава 4. Архитектура Windows NT 5. Введение

Отличия между версиями

Отличия версий Windows 2000 (NT 5.0), XP (NT 5.1) и Server 2003 (NT 5.2), разумеется, существуют. И касаются они не только пользовательского интерфейса и наполнения сервисными программами, но и наборов системных вызовов, предоставляемых в режиме ядра. Тем, кого интересует большее количество деталей, нежели будет представлено далее, можно порекомендовать три статьи, размещенные сегодня в Интернете:

  1. статья Марка Руссиновича и Дэвида Соломона "Windows XP: Kernel Improvements Create a More Robust, Powerful, and Scalable OS", размещенная на сайте Microsoft по адресу msdn.microsoft.com/msdnmag/issues/01/12/XPKernel/default.aspx;
  2. статья Марка Валла и Роберта Вильямса "Windows .NET Structure and Architecture", размещенной в Интернет-журнале "Network Windows & .NET Magazine" по адресу windowsitlibrary.com/Content/717/02/toc.html;
  3. статья "Compare the Editions of Windows Server 2003" (Standard, Enterprise, Datacenter & Web Edition), размещенная на Интернет-сайте Microsoft по адресу microsoft.com/windowsserver2003/evaluation/features/compareeditions.mspx.

Не вдаваясь в подробности, отметим наиболее важные для разработчика драйверов отличия. Прежде всего, следует различать 32 и 64-разрядные клоны. Если 64 разрядная Windows 2000 была собрана лишь один раз в тестовом режиме, то Windows XP (и уж тем более, Server 2003) в 64-разрядной сборке представляет собой реальный коммерческий продукт. Организация адресного пространства памяти в 64 разрядной версии сильно отличается от организации виртуального пространства в 32-разрядном исполнении (таблица 7.1). Приложения, созданные как 32-разрядные программы, запускаются под управлением подсистемы Win32 в рамках модели WOW (Windows On Windows 64), аналогично тому, как 16-разрядные приложения работают в 32-разрядной среде.

Драйвер в 64 разрядной версии компилируется как 64-разрядный код — в частности, с соответствующей длиной указателей. В данной ситуации драйверу актуально знать, от какого приложения он получил запрос — от нового 64-разрядного или старого 32-разрядного. Эта проблема не представляет большого затруднения, поскольку 64-разрядный драйвер может выяснить тип вызывающего процесса при помощи системной функции IoIs32bitProcess, доступной для применения в 64-разрядных сборках драйверов. Пример приводится ниже.

if( IoIs32bitProcess(Irp) == TRUE )
{
    #if DBG
        DbgPrint("IRP request is made by 32 bit process.");
    #endif
}
else
{
    #if DBG
        DbgPrint("IRP request is made NOT by 32 bit process.");
    #endif
}

В соответствии с ответом драйвер может скорректировать некоторые свои операции.

С какой конкретно версией операционной системы драйвер имеет дело, можно легко определить по тому, какую версию модели WDM поддерживает система. Это можно узнать с помощью системного вызова IoIsWdmVersionAvailable, например:

if (IoIsWdmVersionAvailable(l, 0x30)) {
    // Windows Server 2003
} else if (IoIsWdmVersionAvailable(l, 0x20)) {
    // Windows XP
} else if (IoIsWdmVersionAvailable(l, 0x10)) {
    // Windows 2000
} else if (IoIsWdmVersionAvailable(l, 0x05)) {
    // Windows Me
} else {
    //    Windows 98
}

Тем не менее, пока что широкое использование в России дорогих 64-разрядных конфигураций не предвидится, что можно с некоторой степенью уверенности констатировать на основании неширокого применения в прошлом и настоящем относительно "продвинутых" процессоров XEON.

Сосредоточим внимание на 32-разрядных версиях. Среди них произошли следующие изменения.

При переходе от Windows 2000 к Windows XP переписан загрузчик NTLDR (см. Приложение Б), в результате чего процесс загрузки ускорился в 4-5 раз.

Модуль NTOSKRNL.EXE Windows 2000 экспортировал 1129 функций (большинство из которых — системные вызовы, доступные из драйверов режима ядра), Windows XP экспортирует 1464, a Windows Server 2003 — уже 1525.

Модуль HAL.DLL экспортирует, соответственно, 95, 92 и 92 вызова. Причем, по косвенным признакам заметно, что от версии 2000 к XP его постигла значительная переработка, а от версии XP к Server 2003 — лишь процесс "шлифовки".

Пополнение системных вызовов в Server 2003 произошло, в основном, за счет вызовов с суффиксом Ex, то есть функций в чем-то расширяющих существующие системные вызовы Windows XP. Например, IoCsqInitializeEx получился из вызова IoCsqInitialize.

При переходе от Windows 2000 к Windows XP были смягчены многие ограничения на предельные размеры.

Операционная система Windows 2000 ограничивала общий размер адресного пространства под драйверами 220 Мбайтами, в XP этот предел отодвинут до 960. Таким же он остался и в 32 разрядной версии Server 2003.

Предельный общий размер файлов Системного Реестра Windows XP жестко не фиксирован, в то время как в Windows 2000 он не должен был превышать 376 Мбайт. Увеличен размер системных страничных таблиц, в результате чего системное виртуальное адресное пространство увеличилось до 1.3 Гбайт — против 660 Мбайт в Windows 2000. При этом 960 Мбайт в системном виртуальном адресном пространстве XP непрерывны, в других же его "местах" имеются разрывы. (Заметим, что 64 разрядная версия поддерживает размер виртуального системного адресного пространства равный 128 Гбайт.) Весьма вероятно, что такие же параметры остались и у Windows Server 2003 (точные сведения отсутствуют).

Поскольку, страничные таблицы и большая часть Системного Реестра размещаются в физической памяти резидентно, очевидно, что цена такого "послабления" — новое повышение требований к минимальному размеру установленной на компьютере оперативной памяти — до 128 Мбайт.

Увеличено число процессоров, которые может поддерживать операционная система в симметричной многопроцессорной конфигурации (SMP). В редакции Datacenter Windows Server 2003 может поддерживать до 64 процессоров (в 64 разрядной версии). 32-разрядная версия (как и Datacenter Windows 2000) поддерживает по-прежнему до 32 процессоров. Разумеется, такая архитектура обязана быть должным протестирована, и Microsoft анонсировала особый подход к продажам таких "тяжелых" серверов: операционная система будет поставляться только вместе с аппаратурой OEM поставщиками.

И, наконец, самые интересный вопрос: что же означают громадные цифры поддерживаемой оперативной памяти в 32-разрядных версиях: от 2 Гбайт (редакция Web Windows Server 2003) до 64 Гбайт (редакция Datacenter Windows Server 2003)?

Ответ кроется в двух аббревиатурах: AWE и РАЕ, Address Windowing Extension и Physical Address Extension, соответственно. Операционная система, подчиняясь параметру /РАЕ, заданному в файле boot.ini (см. Приложение Б), загружается в модифицированной конфигурации, поддерживающей режим работы РАЕ. В таком режиме возможна манипуляция физическими адресами оперативной памяти (тип PHYSICAL_ADDRESS) за пределами 4 Гбайтного пространства функциями категории MmAllocatePagesForMdl. Наиболее простое и находящееся "на поверхности" применение данного расширения — создание драйверов, реализующих RAM диск. Правда, осложняет дело высокая цена оборудования. На сегодня доля материнских плат, поддерживающих размер оперативной памяти более 8 Гбайт, не превышает 1,5% рынка.

В заключение отметим еще два факта.

Во-первых, по-прежнему все рассмотренные версии продолжают поддерживать все три файловых системы: FAT, FAT32, NTFS. Иными словами, Windows Server 2003 Datacenter Edition вполне устанавливается на логическом диске FAT32, занимая при этом чуть более 1,3 Гбайт.

Во-вторых, Microsoft окончательно (в Windows Server 2003) отошла от поддержки подсистем POSIX и OS/2.

Впрочем, осталось неизменным самое важное — драйверная модель, заложенная в Windows 2000, обеспечивает практически полную совместимость программного кода (а иногда — и бинарного, как можно было убедиться на примере главы 3) во всех рассматриваемых версиях.


Глава 4. Архитектура Windows NT 5. Введение

Заключение

Операционные системы ряда Windows NT 5.x (Windows 2000/XP/Server 2003) представляют богатые возможности для разработки приложений. Схема обработки операций ввода/вывода в Windows 2000/XP/Server 2003 сложна и требует достаточных усилий, чтобы охватить картину в целом, что необходимо для продуктивной работы. Главы 5 и 6 рассматривают эти вопросы более подробно.


Глава 5. Прикасаясь к аппаратуре

Глава 5

Прикасаясь к аппаратуре

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

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


Глава 5. Прикасаясь к аппаратуре

Основные сведения об аппаратном обеспечении

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

  • Поддерживает ли устройство спецификацию PnP и как устройство может известить систему о своем существовании при подключении?
  • Как происходит доступ к регистрам состояния и контроля устройства?
  • Как устройство может генерировать сигнал прерывания?
  • Как устройство участвует в передаче данных?
  • Использует ли устройство какую-либо встроенную память?
  • Как устройство конфигурируется и какэто можно сделать программно?

Автоматическое распознавание и конфигурирование

За каждым аппаратным устройством, подключенным к компьютеру, закрепляются системные ресурсы, которые могут включать:

  • диапазон адресов ввода/вывода (портов вода/вывода);
  • диапазон отведенных адресов памяти;
  • номер прерывания IRQ;
  • DMA канал.

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

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

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

Регистры устройств

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

  • Регистр состояния. Обычно считывается драйвером, когда тому необходимо получить информацию о текущем состоянии устройства.
  • Регистр команд. Биты этого регистра управляют устройством некоторым образом, например, начиная или прекращая передачу данных. Драйвер обычно производит запись в такие регистры.
  • Регистры данных. Обычно такие регистры используются для передачи данных между устройством и драйвером. В выходные (output) peгucmpы, регистры вывода, драйвер производит запис ь, в то время как информация входных (input) регистров, регистров ввода, считывается драйвером.

Доступ к регистрам устройства достигается в результате выполнения инструкций доступа к портам ввода/вывода (port address) или обращения к определенным адресам в адресном пространстве оперативной памяти (memory-mapped address), что и интерпретируются системой как доступ к аппаратным регистрам.

Простые устройства (такие, как стандартный интерфейс параллельного порта, см. таблицу 5.1 — не путать с регистрами устройств, которые могут подключаться извне к параллельному порту!) имеют небольшое число ассоциированных регистров. В то же время, сложное аппаратное обеспечение (например, графические адаптеры) может иметь значительно больше регистров. Число и назначение регистров определяется разработчиками аппаратного обеспечения и должно быть полно и однозначно описано в документации. Однако зачастую такая однозначность так и остается недостижимой мечтой, а разработчику драйвера приходится определять реальное назначение нужных битов в устройствах используя случайно добытый тестовый программный пример неизвестного автора или метод собственных проб и собственных ошибок. Более того, часто выясняется, что биты, объявленные в документации как "зарезервированные" (reserved), вовсе не являются тем безобидным предметом, о котором не следует и беспокоиться.

Таблица 5.1. Регистры интерфейса стандартного параллельного порта (SPP)

Смещение | Доступ | Регистр | Описание
0 | Read/Write | Data (DR) | Байт данных, передаваемый через параллельный порт
1 | Read only Биты 0-1 Бит 2 Бит 3 Бит 4 Бит 5 Бит 6 Бит 7 | Status (SR) PIRQ ERROR# SELECT OUT_OF_PAPER ACK# BUSY# | Текущее состояние порта Зарезервированы 0 — прерывание было запрошено портом (т.е. если сигнал ACK# вызвал прерывание) 0 — произошла ошибка 1 — принтер выбран (включен) 1 — в принтере отсутствует бумага отображает состояния линии Ack# 0 — принтер занят (1 - разрешение на вывод очередного байта)
2 | Read/Write Бит 0 Бит 1 Бит 2 Бит 3 Бит 4 Биты 5-7 | Control (CR) STROBE# AUTO_LF INIT# SELECT_IN# ENABLE_INT | Команды, посылаемые в порт 1 — строб передачи данных в/из порта 1 — автоматическая подача строки 0 — инициализировать принтер 1 — выбрать принтер 1 — разрешает прерывания по спаду сигнала на линии ACK# зарезервированы

Коварность отдельных устройств проявляется еще и в том (например, в случае с адаптерами EGA и VGA), что смысл ассоциированного регистра во второй операции доступа к нему определяется значением данных, отправленных в этот регистр во время первой операции доступа. Пусть у нас имеется X — указатель на регистр в "терминах" адресов памяти, и сначала мы сделали *X=1 и после этого получили n=*X — число конных матросов на лекции А.Блока. Но если мы сделали *X=2, то можем получить n=*X — температуру, при которой следует готовить сапоги всмятку, то есть совершенно другие по смыслу значения.

Доступ к регистрам устройств

Когда функционирование устройства стало почти понятным, останется небольшая проблема: как программно получить доступ к регистрам устройства.

Как правило, регистры следуют друг за другом в своем адресном пространстве. Следовательно, для начала, необходим адрес первого из них. К сожалению, значение термина 'адрес' сильно варьируется при использовании его относительно виртуальных адресных пространств на различных платформах.

  • Вариант первый. Используем специфическую для данного процессора инструкцию ввода/вывода в порт, а значит, нужен адрес порта ввода/вывода.
  • Вариант второй. Особые области в памяти совмещены с адресами доступа к устройству. Запись по адресу в памяти означает перенос данных в устройство. Самый распространенный прием для доступа к видеопамяти — доступ по специальным адресам с использованием стандартных обращений к памяти.

Пример старой DOS программы (назовем ee Abc), выполняющей вывод алфавита на экран в текстовом режиме, построен как раз на том, что видимая область первой текстовой страницы DOS экрана "совмещалась" с оперативной памятью и начиналась с адреса B800:0000 (что в переводе на современный... где-то... 0xB8000).

#include <dos.h>
int main(void)
{
    int i;
    unsigned int far *screen;  // <- адрес начала видимой 1-й страницы
   // Собираем адрес из сегмента (0xB800) и смещения:
    screen = (unsigned int *) MK_FP(0xB800, 0);
    for (i = 0; i < 26; i++)
        screen[i] = 0x0F00 + ('a' + i);  // <- Вывод одного символа
    return 0;
}

В результате работы этого кода (его следует собрать DOS компилятором) в верхней строке экрана появится строка букв о 'а' до 'z'. (Здесь 0x0F — байт означающий, что символы будут белыми на черном фоне.)

Если говорить в терминах ассемблера, то инструкции для доступа к пространству памяти — это MOV (load/store для других типов ассемблера), а для доступа к пространству ввода/вывода используются инструкции типа IN или OUT.

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

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

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

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

Ассемблер х86 | Аналог HAL | Описание
IN AL,DX IN AL,port | READ_PORT_UCHAR | Чтение 1 байта из порта ввода/вывода
IN AX,DX IN AX,port | READ_PORT_USHORT | Чтение 16-ти разрядного слова из порта ввода/вывода
IN EAX,DX IN EAX,port | READ_PORT_ULONG | Чтение 32-х разрядного слова из порта ввода/вывода
INSB | READ_PORT_BUFFER_UCHAR | Чтение массива байт из порта ввода/вывода
INSW | READ_PORT_BUFFER_USHORT | Чтение массива 16-ти разрядных слов из порта ввода/вывода
INSD | READ_PORT_BUFFER_ULONG | Чтение массива 32-х разрядных слов из порта ввода/вывода
OUT DX,AL OUT port,AL | WRITE_PORT_UCHAR | Запись 1 байта в порт ввода/вывода
OUT DX,AX OUT port,AX | WRITE_PORT_USHORT | Запись 16-ти разрядного слова в порт ввода/вывода
OUT DX,EAX OUT port,EAX | WRITE_PORT_ULONG | Запись 32-х разрядного слова в порт ввода/вывода
OUTSB | WRITE_PORT_BUFFER_UCHAR | Запись массива байт в порт ввода/вывода
OUTSW | WRITE_PORT_BUFFER_USHORT | Запись массива 16-ти разрядных слов в порт ввода/вывода
OUTSD | WRITE_PORT_BUFFER_ULONG | Запись массива 32-х разрядных слов в порт ввода/вывода

Доступ через адресацию в памяти

Далеко не все создатели процессоров находили целесообразным организацию "портового" доступа к регистрам устройств через адресное пространство ввода/вывода. В альтернативном подходе доступ к регистрам устройств осуществлялся путем обращения к определенным адресам в пространстве памяти (memory address space). В пример можно привести архитектуру PDP-11 Unibus, где вовсе не было портов ввода/вывода и инструкций процессора для работы с ними: все регистры всех устройств имели свое место в общем пространстве адресации памяти и реагировали на обычную инструкцию доступа к памяти. В некоторых случаях (например, для видеоадаптеров в архитектуре Intel x86) допускаются оба способа доступа сразу: и через пространство ввода/вывода, и через адресное пространство

Таблица 5.3. Макроопределения HAL для доступа к регистрам устройств через адресацию в памяти.

Поля | Описание
READ_REGISTER_XXX | Чтение одного значения из регистра ввода/вывода
WRITE_REGISTER_XXX | Запись одного значения в регистр ввода/вывода
READ_REGISTER_BUFFER_XXX | Чтение массива значений из последовательного набора регистров ввода/вывода
WRITE_REGISTER_BUFFER_XXX | Запись массива значений в набор из следующих друг за другом регистров ввода/вывода

Как и в предыдущем случае, определены макросы HAL для доступа к таким memory mapped (то есть с доступом посредством адресации в памяти) регистрам, что описывается в таблице 5.3 (XXX принимает значения UCHAR, USHORT или ULONG). Так как эти макроопределения по содержанию отличаются от HAL макроопределений для операций с портами ввода/вывода, драйверный код должен быть разработан так, чтобы компиляция для разных платформ (с разными методами доступа к регистрам) проходила корректно. Хорошим приемом является составление таких макроопределений условной компиляции, которые указывали бы на один из нужных HAL макросов в зависимости от ключей компиляции.


Глава 5. Прикасаясь к аппаратуре

Сигналы прерываний

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

В обязанности процессора, если он намерен перейти к обслуживанию по поступившему сигналу, входит сохранение состояния процессора, контекста выполняемого программного потока. Лишь после этого процессор(будет правильнее сказать — операционная система) может выполнить переход к программному коду Процедуры Обслуживания Прерывания (Interrupt Service Routine, ISR), которую регистрирует драйвер, взявший на себя работу по представлению данного устройства в операционной системе.

Обычно устройства генерируют сигналы прерываний в ситуациях:

  • Устройство завершило обработку предыдущего запроса (поступившего от драйвера) и теперь готово к обработке нового.
  • Буфер или очередь FIFO устройства почти полны (во время ввода) или почти пусты (во время вывода). Такое прерывание разрешает драйверу получение из буфера устройства или вывод в буфер устройства данных для обеспечения его непрерывной работы.
  • Устройство столкнулось с нештатной или ошибочной ситуацией во время выполнения операции, и прерывание может быть специально для того предназначенной формой завершения операции.

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

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

Приоритеты прерываний

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

Для чего приоритет прерывания нужен? В момент, когда процессор занят выполнением программного кода в ответ на прерывание от низкоприоритетного устройства (например, выполняет его ISR-процедуру обработки прерываний), вполне может поступить сигнал прерывания от устройства с высоким приоритетом. В данном случае, центральный процессор получит два прерывания — второе над первым — и перейдет к обработке того, которое имеет больший приоритет, сохраняя, разумеется, контекст отложенного "в сторону" потока. И наоборот, обработка прерывания низкого уровня, поступившего при обслуживании высокоприоритетного, задерживается до момента, пока высокоприоритетное не будет обработано полностью и объявлено завершенным.

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

Векторы прерывания

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

Передача сигналов прерываний

При генерации прерываний используются две основные стратегии. Более старый и менее предпочтительный механизм носит название edge-triggered (управляемые перепадом, управляемые фронтом) или latched прерывания. Устройства, которые генерируют такие прерывания, сигнализируют о потребности в обслуживании осуществлением перехода на физически существующем проводе (например, на одном из проводников шины или соединительного кабеля) из одного логического состояния в другое, скажем, из 1 в 0. Как только этот переходной процесс прошел, устройство может отпустить линию (отпустить прерывание), восстановив параметры электрического сигнала, в данном случае, обозначающие логическую 1. Другими словами, линия прерывания такого устройства "пульсирует" под его руководством, а процессор должен обратить внимание на проходящие импульсы.

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

Классическим примером оборудования, где случаются потери такого типа прерываний, являются последовательные СОМ порты. По традиции, COM1 и COM3 используют одно определяемое по фронту прерывание, а именно IRQ4. В результате, невозможно одновременно подключить к этим двум портам оборудование, которое использует управляемое прерываниями программное обеспечение. Попытки включить мышь в COM1 и модем в COM3 рано или поздно, но — неминуемо приводят к остановке одного из драйверов, который ожидает "пропавший" сигнал о прерывании.

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

Сродство к процессору

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

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

Механизмы передачи данных

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

  • Программируемый ввод/вывод.
  • Прямой доступ к памяти (Direct Memory Access, DMA).
  • Совместно используемые области памяти.

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

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

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

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

Прямой доступ к памяти

Прямой доступ к памяти (Direct Memory Access, DMA) использует выгоды от вовлечения в работу вторичного процессора, называемого контроллером DMA (DMA controller, DMAC, контроллер ПДП). Этот контроллер является вспомогательным процессором с ограниченным набором возможностей и обязанностей, но при этом с достаточным интеллектом и статусом, чтобы выполнять передачу данных между устройством и оперативной памятью. Контроллер DMA работает параллельно с основным процессором (процессорами), и обычно его деятельность почти незаметна в общем функционировании системы.

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

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

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

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

DMA операции с использованием системных контроллеров

Первоначальная спецификация персонального компьютера по IBM (и последовавшие стандарты) включали основную плату (называемую ныне "материнской") с набором общих DMA котроллеров. Каждый DMA контроллер предоставляет DMA каналы (DMA channel), и данное устройство может быть сконфигурировано так, чтобы использовать один (или более) доступных каналов. Изначально существовало четыре канала, которые были расширены до семи при введении спецификации AT. Системный DMA (system DMA) известен еще как slave DMA.

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

Операции bus master DMA

Более сложные устройства, которые не желают быть зависимыми от системных DMA контроллеров, содержат собственные аппаратные средства обеспечения DMA. Так как эти средства принадлежат собственно устройству, то передача происходит по "воле" устройства — если, разумеется, протокол шины поддерживает такие действия. Для выполнения своей операции устройству необходимо получить "контроль над шиной" в результате процедуры, которая описывается в протоколах соответствующих шин.

Память, отведенная устройству

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

Рис. 5.1
Доступ к памяти, имеющейся в устройстве

В некоторых случаях эти диапазоны определены жестко, как в случае с диапазоном адресов 0x0A0000-0x0AFFFF буфера адаптера VGA.

Память, отведенная устройству, является ресурсом, предоставляемым драйверу устройства операционной системой, и обычно с ним можно ознакомиться в апплете свойств драйвера в настройках системы (Пуск - Настройка - Панель Управления - Система - Устройства - Диспетчер Устройств - устройство - ресурсы).

Ресурсы, используемые устройством

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

  • Идентификатор производителя (Manufacturer ID).
  • Идентификатор типа устройства (Device type ID).
  • Требования к пространству ввода вывода (I/O space requirements).
  • Требования по использованию прерываний.
  • Требования по использованию каналов DMA.
  • Требования относительно памяти, отведенной устройству.

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

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


Глава 5. Прикасаясь к аппаратуре

Шины в компьютерных системах

Шина (bus) представляет из себя совокупность данных, адресов и линий (проводников на печатной плате или в кабеле или шлейфе) сигналов контроля, которые позволяют устройствам организовать сообщение между собой. Некоторые шины являются "широкими", обеспечивая одновременную (параллельную) передачу многих битов данных и битов контроля. Другие представляют собой всего лишь пару проводов, позволяющую устройствам передавать данные и управляющие сигналы последовательно. Некоторые шины позволяют связываться с любым другим устройством на шине. Другие требуют наличия "хозяина шины", master controller (центральный процессор или контроллер ввода/вывода), выступающего в роли получателя или отправителя данных.

От того, какими свойствами обладает главная шина компьютера зависит производительность всей системы (недостаточно корректно сказано, но все-таки можно ее определить, как шину, к которой подключается большинство системных устройств и которая ближе всего "подходит" к процессору). В середине 80-x, например, рабочая станция Apollo на базе процессора 68020 была ориентирована на ISA шину как на стандарт. В настоящее время персональные компьютеры с Intel-архитектурой эксплуатируют шину PCI, хотя и претерпевшую модификации, но очевидно главную — стоит лишь нарисовать блок-схему системы в целом. (Вообще говоря, PCI шина применима на многих платформах, что и было изначально заложено в стандарт.)

Архитектура драйверов в операционной системе Windows NT 5 эффективно поддерживает введение новых шин, а поддержка многих популярных шин встроена в стандартную поставку Windows.

ISA: Industry Standard Architecture

Шина ISA была предложена фирмой IBM для PC/AT в начале 1980-x. Она поддерживала подключение и 8-разрядных, и 16-разрядных устройств. По той причине, что эта шина изобреталась два десятилетия назад, она не отличалась ни быстротой, ни простотой конструкции. Эта шина является динозавром компьютерных технологий, однако ее невозможно забыть по двум причинам.

Во-первых, столь популярные и до сих пор обязательные устройства настольного компьютера, какими являются параллельный порт LPT и последовательные СОМ порты, являются устройствами ISA шины. Эта ISA шина через мост подключается к шине PCI, и существует в компьютере даже тогда, когда на материнской плате нет ни одного ISA разъема. (Для фанатиков же ISA устройств некоторые фирмы, например PHILIPS, выпускает микросхемы для реализации подключения ISA плат к шине USB.)

Во-вторых, при усовершенствовании компьютеров, которые ISA шина поначалу вполне устраивала, выявилось столько неудобств и ограничений, что стало очевидным — к принятию шинных протоколов следует подходить более ответственно. Невысокая пропускная способность (максимум 8 МБ/сек), отсутствие стандарта на использование адресов ввода/вывода, невозможность совместного использования прерываний (их всего 16, большая часть которых уже занята системными устройствами, оставляя вновь подключаемым устройствам всего лишь 2 или 3) настойчиво указывали на необходимость новых решений. Например, старые ISA платы не могут использовать все 16 разрядов адресов ввода/вывода, а декодируют только первые 10 разрядов адреса, в результате чего отзываются по адресам через 0x400 адресов. Устройство с адресом 0x300 отвечает также и по адресу 0x700. Появление таких устройств в системе приводит к откровенной бесполезности большей части 64 Кбайтного адресного пространства ввода/вывода. И, наконец, шина ISA имеет возможность всего лишь 24 разрядной адресации, что определяет ограничение на выполнение переноса данных только в нижние 16 МБ физической памяти. Этот артефакт доставляет Windows особые проблемы при работе с ISA шиной, если возникает необходимость применить DMA способ передачи данных.

Как уже было сказано, устройства ISA шины не объявляли себя, они не предоставляли списка требуемых ресурсов, и они не нуждались в динамическом конфигурировании со стороны программного обеспечения. Конфигурирование этих устройств выполнялось вручную и обычно сводилось к требуемой установке перемычек (jumpers) или переключателей DIP.

Современные ISA устройства пытаются скорректировать эту ситуацию, предлагая Plug and Play расширение к стандарту ISA. Такие устройства значительно расширили свою популярность с введением операционной системы Windows 95. Версии NT, существовавшие до Windows 2000, реально не поддерживали спецификацию PnP, отчего этим новым ISA устройствам приходилось полностью полагаться на специальные программы инсталляции, которые обеспечивали их должное функционирование под NT, и услуги Системного Реестра. Однако с выходом Windows 2000 поддержка таких устройств стала вполне корректной.

EISA: Extended Industry Standard Architecture

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

Разумеется, требование на обеспечение совместимости ограничивало модернизацию шины по многим направлениям. Например, в то время, как разрядность передаваемых данных (в литературе — data bus width) была увеличены до 32 бит, тактовая частота осталась равной 8 МГц. Соответственно, максимальная скорость передачи данных по шине стала составлять около 32 Мбайт в секунду. Кроме того, поскольку разъемы EISA на материнской плате должны были принимать еще и платы собственно ISA, то появились существенные сложности в устранении электрических шумов, обусловленных топологией разводки соединений ISA. Компьютерные пользователи постарше могут припомнить, что разъем EISA на материнской плате состоял из ISA разъема и присоединенного к нему в продолжение разъема покороче и поизящнее, что вместе и составляло EISA-слот.

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

PCI: Peripheral Component Interconnect

Быстрые сетевые приложения, высококачественное видео, 16 и 20 разрядный звук и быстро развивающиеся музыкальные приложения, дисплеи с глубиной цвета 24 бит — все эти новшества потребовали введения нового стандарта шины с высокими скоростями передачи данных. Шина PCI явилась попыткой удовлетворить все эти возросшие потребности. И хотя первоначальная конструкция зародилась в Intel (первая спецификация версии 1.0 появилась в июне 1992), шине PCI оказана поддержка со стороны других компьютерных производителей. Она является относительно независимой от процессорных конфигураций и успешно используется теперь в компьютерах Alpha(DEC) и PowerPC (Motorola). Ha рисунке 5.2 показана типовая конфигурация системы, содержащей PCI шину.

Рис. 5.2
Типовая конфигурация системы с шиной PCI

Повысив тактовую частоту до 33 МГц (позже и до 66 МГц) и применив множество технических уловок, разработчики PCI добились того, что пропускная способность PCI шины возросла до 132 Мбайт/сек при передаче данных по 32-разрядной шине. При передаче данных по 64-разрядной шине пропускная способность увеличивается вдвое. Некоторые из принципов, которые позволили обеспечить столь значительный прогресс, таковы:

  • Протокол PCI подразумевает, что каждая передача данных происходит как пакетно-монопольная операция (burst operation), в результате чего для быстрых устройств, предающих большие объемы данных, действительно достигаются физически предельные показатели PCI.
  • Протокол PCI поддерживает режим многих "хозяев шины" (bus master) и разрешает прямую передачу данных устройство-устройство (без промежуточного использования памяти), что может быть использовано для повышения степени параллельности между операциями ввода/вывода и работой процессора.
  • Центральный шинный арбитр (central bus arbiter) совмещает операции арбитрирования и собственно перенос данных, чем уменьшается время задержек. Это позволяет следующей операции стартовать сразу же, как только текущая операция заканчивается.
  • Мост PCI, наделенный существенными способностями, между главным процессором и собственно шиной представляет разнообразные возможности кэширования и выполнения операций упреждающего чтения данных (readahead functions). Bce это позволяет уменьшить время, которое процессор тратит на ожидание данных.
  • Мост PCI является устройством шины, позволяющим подключать вторичные шины PCI. В таком случае мост называется 'PCI-to-PCI bridge'. (В частности, ранее это было решением проблемы увеличения числа слотов PCI, если их требовалось более 4.)

Архитектура PCI позволяет подключить к шине до 32 физических единиц, называемых устройствами (devices). Каждая из этих единиц может содержать до 8 отдельных функциональных единиц, называемых функциями (functions). После отбрасывания одного адреса функции для генерации широковещательных сообщений (broadcast messages — сообщений, предназначенных всем устройствам), остается 255 адресуемых единиц-функций на одной шине PCI. (Учитывая, что PCI-PCI мост является обычным устройством шины, возникает возможность подключения к основной шине до 255 дополнительных шин PCI).

Доступ к регистрам

Шина PCI позволяет использовать 32 разрядные адреса, но системное пространство ввода/вывода все еще ограничено адресами до 64K (16 разрядов). Соответственно, там должны пребывать и все регистры PCI. Более того, в системах, содержащих шины EISA или ISA, разработчикам следует воздерживаться и от использования адресов, которые могли бы использовать устаревшие устройства, подключаемые к этим шинам, по причинам, указанным выше.

Наряду с адресацией пространства ввода вывода и адресацией памяти, архитектура PCI определяет диапазон адресов во внутренней памяти устройства, называемых конфигурационным пространством (configuration space). При обсуждении автоматического конфигурирования PCI будет объяснено, какую структуру это конфигурационное пространство имеет и как используется в процессе идентификации устройств (плат) на шине PCI.

Механизмы прерываний

Шина PCI использует четыре равно-приоритетных линии запросов на прерывание INTA-INTD. Эти линии являются активными по низкому уровню (сигналом прерывания является низкий уровень в линии), переключаемые по уровню (информативным значением является логический уровень — в отличие от того случая, когда информативным является фронт сигнала, то есть переход от одного уровня к другому). Данные линии допускают возможность совместного использования (это, возможно, станет технически более понятным, если сказать, что такая линия прерываний реализуется по электронной схеме "open drain", N-МОП аналог "открытого коллектора" для ТТЛ логики). Устройство, подключаемое к PCI и представляющее одну функцию, должно использовать только линию INTA.

Многофункциональные устройства могут использовать комбинации из четырех линий, начиная с INTA. Единственное ограничение состоит в том, что каждая из восьми функций (возможных в одном устройстве) может использовать только одну линию прерывания. Соответственно, устройство с внутренними восемью функциями может задействовать имеющиеся линии INTA, INTB, INTC, INTD следующим образом:

  • Все восемь функций подключены к INTA.
  • Семь подключены к INTA, одна к INTB.
  • Две подключены к INTA, две к INTB, две к INTC и две к INTD.
  • Четыре подключены к INTA, четыре к INTB.
  • И т.п.

Спецификация PCI относительно безразлична к приоритетам прерываний. Приоритеты, в данном случае, зависят от внешнего контроллера, который переадресует запрос на прерывание PCI устройства в соответствующую линию системных прерываний. Рекомендуемые схемы представления прерываний PCI устройств с использованием программируемых редиректоров (redirector или router) прерываний разной сложности можно найти в книге Тома Шанли и Дона Андерсона, PCI System Architecture, 4 издание, стр. 227-230, содержащей изложение спецификации PCI с полезными комментариями.

Например, на персональных компьютерах редиректор может преобразовать запрос функциональной единицы PCI по линиям INTA-INTD в запрос по одной из линий IRQ0-IRQ15, схема 14-4 в указанном издании. Чтобы можно было сделать это, любая функция внутри PCI устройства, которая генерирует прерывание, должна задействовать два конфигурационных регистра (в своем конфигурационном пространстве):

  • Регистр вывода прерывания (Interrupt pin register, обозначенный Int PIN в таблице 5.4). Является регистром "только для чтения", который идентифицирует линию прерывания PCI (INTA-INTD), используемую данной функцией данного PCI устройства.
  • Регистр линии прерывания (Interrupt line register, обозначенный Int Line в таблице 5.4). Является регистром "только для записи", который указывает приоритет и вектор (номер системного прерывания), которые редиректор прерываний должен присвоить данной функции. В персональных компьютерах Intel x86 значения 0x00-0x0F соответствуют IRQ0-IRQ15.

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

Возможности DMA

Спецификация PCI не включает понятия slave DMA. Вместо этого, собственные функции PCI (функциональные единицы) являются либо "хозяевами шины" (bus masters), выполняющими свой собственный DMA перенос данных, либо они используют программируемый ввод/вывод. Единственным типом устройств, которые могут использовать DMA перенос данных с использованием системных контроллеров (slave DMA), являются не-PCI платы, подключенные через (E)ISA мост.

В DMA операциях собственно PCI шины, участники называются агентами (agents), и в каждой транзакции всегда участвуют два из них, а именно:

  • Инициатор (Initiator). Это "хозяин шины" (bus master), который выиграл право доступа к шине и хочет определить операцию переноса.
  • Цель (Target). Это PCI функция (функциональная единица PCI устройства), адресуемая в настоящий момент инициатором с целью выполнения передачи данных.

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

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

Память, отведенная устройствам

Отведенная память, используемая РСI функциями (функциональными единицами PCI устройств), может быть размещена в любом месте 32 разрядного адресного пространства. Эта возможность может быть использована при работе в базисе "функция устройства" — "функция устройства".

Интересная возможность спецификации PCI состоит в том, что в конфигурационном пространстве предусмотрено место для указания ссылки на область расширения ПЗУ (Extension ROM Base Address, см. таблицу 5.4), встроенного в данное PCI устройство. В этом ПЗУ могут быть записаны фрагменты кодов инициализации, предназначенные для инициализации устройства на разных аппаратных платформах, что дает возможность поставлять один и тот же продукт для разных компьютерных платформ. Спецификация PCI определяет стандартный заголовочный формат для этих блоков ПЗУ, поэтому программное обеспечение, выполняющее инициализацию, может определить необходимый фрагмент, хранящийся в ПЗУ и загрузить именно его для выполнения инициализации на имеющейся платформе.

Автоматическое распознавание и конфигурирование

Спецификация PCI обязывает каждую отдельную функцию (функциональную единицу устройства, а значит, и шины) иметь свою собственную область для хранения конфигурационных данных, размер которой равен 256 байт. Эта область известна как конфигурационное пространство функциональных единиц PCI (PCI function's configuration space).

Первые 64 байта такого конфигурационного пространства (называемого заголовком) имеют предопределенную структуру (таблица 2.4), в то время как остальные 192 байта могут быть использованы по усмотрению разработчика данной PCI платы. Системное программное обеспечение может использовать этот заголовок для идентификации функциональной единицы PCI и выделения ей ресурсов. Заголовок включает:

  • Информацию о поставщике (Vendor ID), тип устройства (Device ID) и версию устройства (Revision ID).
  • Два стандартных регистра состояния команд для того, чтобы имелась возможность работы с ошибками в устройстве (Status Register и Command Register).
  • Список ресурсов, в котором описываются требования к памяти и пространству ввода/вывода.
  • Регистр вывода прерывания (Int Pin) и регистр линии прерывания (Int Line), описанные выше.
  • Указатель на расширение ПЗУ (Expansion ROM Base Address), специфичное для данного устройства.

Регистр Class Code внутренне разбит на три поля (Class Code, старший байт, Sub-Class Code и ProgIF, младший байт) и содержит информацию для использования классовыми системными драйверами (предназначенными для обслуживания целого класса родственных устройств, например, сетевых контроллеров, устройств мультимедиа, docking-станций, шинных мостов, кодирующих/декодирующих контроллеров, интеллектуальных контроллеров ввода/вывода). Например, значение Class Code, равное 0C (hex), в спецификации PCI 2.2 определяет контроллеры последовательных шин. Тогда значение Sub-Class Code, равное 00, определяет IEEE 1394, 01 — ACCESS.bus, 02 — SSA (Serial Storage Architecture), 03 — USB, 04 — Fibre Channel, 05 — SMBus (System Management Bus). Для подкласса USB значение ProgIF, равное 00, описывает USB контроллер, соответствующий спецификации Universal host Controller. Значению 10 (hex) соответствует контроллер спецификации Open Host Controller, а значение FE (hex) описывает USB устройство (не являющееся хост-контроллером).

Байты конфигурационного пространства |  |  |  |
3 | 2 | 1 | 0 |
Device ID |  | Vendor ID |  | 00
Status Register |  | Command Register |  | 01
Class Code |  | Revision ID |  | 02
BIST | Header Type | Lat Timer | CL Size | 03
Base Address 0 |  |  |  | 04
Base Address 1 |  |  |  | 05
Base Address 2 |  |  |  | 06
Base Address 3 |  |  |  | 07
Base Address 4 |  |  |  | 08
Base Address 6 |  |  |  | 09
Card Bus CIS Pointer |  |  |  | 0A
Subsystem ID |  | Subsystem Vendor ID |  | 0B
Expansion ROM Base Address |  |  |  | 0C
Зарезервировано |  |  | Cap.Pointer | 0D
Зарезервировано |  |  |  | 0E
Max_Lat | Min_Gnt | int Pin | int Line | 0F
 Таблица 5.4.
Заголовок конфигурационного пространства одной функции устройства PCI

Регистр BIST (Built-In-Self-Test) отражает способность устройства к проведению операции самотестирования. Единица в старшем (седьмом) бите этого регистра означает, что данная функция устройства может выполнять операцию самотестирования. Шестой бит, установленный в 1, означает проведение тестирования, причем на эту операцию функции устройства дается не более 2-х секунд. В младших четырех битах содержится код ошибки либо 0, при удачном завершении.

Регистры Base Address Register 5..0 содержат адреса портов ввода/вывода (или диапазонов отведенной памяти) для доступа к внутренней памяти данной PCI функции.

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

  • Конфигурационный адресный регистр. Этот регистр определяет номер шины (bus number), устройство, функцию и адрес доступа к данным в конфигурационном пространстве.
  • Конфигурационный регистр данных. Этот регистр действует как буфер между процессором и конфигурационным пространством. После установки значения в адресном регистре, чтение или запись в этот регистр данных приводит к собственно переносу информации из/в конфигурационное пространство.

Операционная система предоставляет HAL функции для доступа к конфигурационным данным. Для этого предлагается использовать функции HalGetBusData, HalSetBusData и HalAssignSlotResources. Правда, в WDM модели они считаются устаревшими (даже их описание в DDK отсутствует) — рекомендовано работать через запросы к нижнему драйверу стека.

Для идентификации драйвера, загружаемого операционной системой для обслуживания данного устройства, спецификация PCI рекомендует разработчикам ОС использовать информацию из конфигурационных регистров Vendor ID, Device ID, Revision ID, Class Code, Subsystem Vendor ID, Subsystem ID (последние два стали обязательными только в спецификации версии 2.2).

IEEE 1394: Firewire Bus

Предложенная и реализованная Apple Computers, a впоследствии определенная институтом IEЕЕ как высокоскоростная одноранговая (peer-to-peer) последовательная шина, IEEE 1394 предназначена для приложений, когда USB шина версии 1.1 не могла использоваться из-за низкой скорости передачи данных. Спецификация IEEE 1394 (на текущий момент — IEEE 1394a-2000) описывает три стандартных скорости передачи данных 100, 200 и 400 Мбит/сек. (Спецификация IEEE 1394b поддерживает большие скорости.) Но даже при таких скоростях, шина IEEE 1394 уже в состоянии переносить более 10 Мбайт в секунду, что превышает показатели ISA.

Наименование Firewire представляет собой торговую марку Apple Computers. Термин 1394 обычно используется для того, чтобы обозначить принадлежность этой спецификации к аппаратному обеспечению персональных компьютеров. Фирма Sony и другие производители видеокамер используют для обозначения своей модификации 1394 термин i.Link(TM).

Каждое устройство IEEE 1394 может быть подключено к шине кабелем 4,5 м, содержащем 6 или 4 провода. К шине может быть подключено до 63 устройств шлейфовым образом (daisy-chained) при общей длин соединения 72 м. Можно использовать шинные мосты и устройства разветвления, что позволяет подключить дополнительно до 62 устройств. При использовании шинных мостов можно задействовать до 1024 шин. В результате, число устройств, которые теоретически можно подключить к компьютеру в конфигурации 1394, составляет 64K. При подключении устройства, ему присваивается 16-разрядный идентификатор (Node ID).

6-проводной кабель содержит 2 проводника для передачи данных, 2 для передачи сигналов синхронизации и 2 для энергоснабжения подключаемых устройств. Кабель экранирован и оканчивается разъемом, который получен модификацией разъема игровой приставки Nintendo Gameboy.

Доступ к регистрам

Спецификация IEEE 1394 удовлетворяет стандарту IEEE 1212 архитектуры Control and Status Register (CSR). Стандарт CSR определяет фиксированную 64-разрядную схему адресации, включающую 10-разрядный шинный номер и 6-разрядный идентификатор узла (Node ID) c остающимся 48-разрядным полем для использования по усмотрению устройства.

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

Механизмы прерываний

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

Семейство стандартов 1394 включает Open Host Controller Interface. Спецификация OHCI является наиболее значимым стандартом для разработчиков драйверов, работающих с устройствами шины 1394. Этот интерфейс обеспечивает общий механизм работы с прерываниями и DMA передачей данных. Ассоциация 1394 Trade Association предоставляет информацию по спецификации OHCI и другие связанные с ним данные на интернет сайте 1394.ta.org.

Возможности DMA

Хост-контроллер IEEE 1394 использует DMA для передачи данных и команд в/из системной памяти. Устройства, подключенные к шине 1394, не могут иметь прямого доступа к системной памяти, поэтому адаптеры OHCI предоставляют диапазон адресов (в системной памяти),с которыми, собственно, и работают прикладные программы и контроллер DMA. Таким образом, возникает иллюзия возможности DMA передачи данных для каждого устройства, подключенного к шине.

Автоматическое распознавание и конфигурирование

Спецификация шины 1394 была разработана с непосредственной поддержкой спецификации PnP. Каждое устройство при подключении к шине сигнализирует о своем существовании при выполнении шинной операции 'reset', что в сильной степени напоминает способ обращения с шиной USB и ее устройствами. Хост-компьютер (или другие узлы) выполняют операцию "перечисления" конфигурационных ПЗУ для того, чтобы обнаружить данное устройство и обеспечить запуск (а при необходимости — и инсталляцию) соответствующего драйвера.

USB: Universal Serial Bus

Спецификация USB была разработана консорциумом компаний, включая Intel и Microsoft. Целью нового стандарта было обеспечение организации недорогой среднескоростной шины в таких областях применения, как передача цифровых изображений, компьютерная телефония и мультимедийные игры. Текущими версиями спецификации USB является версия 1.1 и версия 2.0 (разумеется, во вторую заложены более высокие скоростные характеристики), и список компаний-членов, участвующих в консорциуме, постоянно растет.

Предельная скорость передачи данных по шине USB спецификации 1.1 составляет 12 Мбит/сек (Full Speed). Медленные используют низкую скорость передачи 1,5 Мбит/сек (Low Speed). Стандарт USB версии 2.0 поддерживает физическую скорость передачи 480 Мбит/сек (High Speed). Реально же максимальная скорость соединения компьютер - USB устройство (в разрабатываемом автором приборе) достигала 32 Мбайт/с. Данные передаются последовательно по паре проводников. Питание для некоторых устройств доступно по отдельным проводникам питания и заземления (для устройств с небольшим энергопотреблением).

Устройства USB могут быть подключены 5-метровым кабелем (а практически — и более длинным). Использовании USB хаба (hub — сетевой концентратор), позволяет увеличить дальность размещения устройств от хост-компьютера и увеличить количество устройств, подключаемых к одной шине USB. Последовательно можно включить до пяти устройств-хабов, обеспечив длину соединения 30 метров. Пример конфигурации "сети", построенной с использованием USB-хабов приводится на рисунке 5.4.

Рис. 5.4
Пример конфигурации устройств шины USB

Следует заметить, что устройства High Speed (для USB 2.0) должны поддерживать функционирование и на скорости Full Speed. Более того, процесс "перечисления" проводится как раз на этой скорости.

При конфигурировании устройств при использовании внешних USB хабов следует помнить, что хаб, предназначенный для работы на некоторой скорости (например, Full Speed), ограничит скорость работы устройств, стоящих в цепочке устройств далее от хост-компьютера (например, High Speed устройства), и последние не смогут работать на скорости, превышающую скорость этого медленного хаб-концентратора. При отсутствии возможности работать на скорости Hi Speed устройства Hi Speed работают на скорости Full Speed.

Работа программиста, создающего драйвер внешнего (не находящегося на материнской плате) USB устройства сводится к тому, чтобы воспользоваться программным интерфейсом системных драйверов шины USB, общение с которым происходит при помощи пакетов, называемых URB (USB Request Block) пакетами. Работа с регистрами USB контроллеров на материнской плате теперь стала уделом узкого круга специалистов — разработчиков материнских плат и операционных систем. Всем остальным разработчикам USB устройств с управлением программ под Windows предлагается достаточно развитый программный интерфейс к системным WDM драйверам, которые берут на себя все аппаратно-ориентированные операции.

К хост-компьютеру можно подключить до 127 устройств, шинный адрес которых устанавливается динамически при подключении устройств.

Доступ к регистрам

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

Механизм передачи данных является асинхронным и блочным. В каждом блоке передаваемых данных, называемом USB-фреймом, может передаваться до 1280 байт данных. Каждый фрейм занимает фиксированный временной интервал 1 мсек.

Оперирование командами и блоками данных реализуется при помощи такой логической абстракции, как USB pipe-канал. Pipe-канал является каналом связи между хост-контроллером и конечной точкой (endpoint — еще одна логическая абстракция спецификации USB) внешнего устройства, которой обычно является буфер некоторой протяженности внутри внешнего устройства. Открытый pipe-канал можно сравнить с открытым файлом. Кстати сказать, с ним ассоциирован дескриптор открытого канала.

Для передачи команд (и данных, входящих в состав команд) используется pipe-канал по умолчанию (default pipe), в то время как для передачи данных может быть использовано любое количество потоковых pipe-каналов (stream pipes) и pipe-каналов сообщений (message pipes).

Концепция "конечных точек" (endpoints) USB сильно напоминает концепцию функций устройств PCI, поскольку после конфигурирования устройства за конечными точками внутри него закрепляется и определенная функция (0 — только функции управления и контроля, остальные — по замыслу разработчика, но — на все время работы в системе).

Механизмы прерываний

Для шины USB настоящего механизма прерываний, строго говоря, не определено. Вместо этого, USB интерфейс хост-компьютера опрашивает подключенные устройства на предмет наличия данных о прерывании. Опрос происходит в фиксированные интервалы времени, обычно каждые 1-32 миллисекунды. Устройству разрешается посылать в момент опроса до 64 байт данных.

С точки зрения драйвера, возможности работы с прерываниями и DMA передачей данных фактически определяются USB хост-контроллером, который и обеспечивает поддержку физической реализации USB интерфейса. Достаточно много усилий было предпринято для стандартизации интерфейса хост-адаптера, в результате чего появились два типа интерфейсов: Open Host Controller Interface (OpenHCI) и Universal Host Controller Interface (UHCI). Собственно хост-контроллер и поддерживает общепринятые механизмы прерываний и DMA передачи данных. Внешние пользовательские USB устройства должны лишь пассивно и в соответствии с протоколом участвовать в обмене данными.

Возможности DMA

Устройства USB шины не имеют прямого доступа к системной памяти. Они изолированы от системных ресурсов USB интерфейсом хост-компьютера и не поддерживают способа DMA передачи данных в привычном смысле. Тем не менее, USB интерфейс хост-компьютера обеспечивает "иллюзию" поддержки DMA для логических pipe-каналов, обеспечивающих доступ к конечным точкам (т.е. буферам) внутри подключенных к шине устройств. По мере получения данных от подключенных USB устройств, интерфейс хост-контроллера использует DMA доступ для того, чтобы поместить полученные из устройства данные в системную память. Таким образом, USB интерфейс, состоящий из внешнего устройства, контроллера в хост-компьютере и системные драйверы (но не каждое USB устройство по отдельности!), поддерживает возможность DMA передачи данных — или, строго говоря, ее иллюзию.

Автоматическое распознавание и конфигурирование

Спецификация USB была разработана с непосредственной поддержкой спецификации PnP (иногда кажется, что — с откровенной ориентацией на WDM модель драйверов Windows). Каждое USB устройство при подключении к шине USB сигнализирует о своем существовании и сообщает идентификатор производителя (vendor ID) и идентификатор устройства (device ID). Эти идентификаторы являются определяющей информацией в выборе загружаемого драйвера — соответствующая информация должна присутствовать в Системном Реестре (если ничего подходящего там не обнаруживается, то инициируется процесс установки нового драйвера).

Шина PC Card (PCMCIA)

Около десяти лет назад, несколько компаний совместно разработали стандарт шинной архитектуры для мобильных устройств. Первоначально, внимание было сфокусировано на платах оперативной памяти, и эта группа получила название Personal Computer Card International Association (PCMCIA). Мобильные устройства ограничены в размерах и энергоресурсах, и на этом сделан акцент стандартов данной группы. На сегодня более 300 компаний являются членами PCMCIA.

Первоначально, стандарт PC Card определял 68-выводной интерфейс при одной из трех величин толщины печатной платы, типа I, типа II или типа III. Данный стандарт определяет скорость передачи данных практически такую же, что и для ISA шины. Но в данном случае более высокий приоритет был установлен для минимизации энергопотребления и размеров, а не для улучшения скоростных параметров шины.

Термин PCMCIA зачастую используется вместо термина PC card, и наоборот. Такое использование создает некое противоречие: PCMCIA является организацией, в то время, как PC Card определяет шинный интерфейс. В настоящее время PCMCIA определяет, по крайней мере, три стандарта: PC Card, DMA и CardBus. Таким образом, когда используется термин 'PCMCIA card', следует насторожиться, поскольку, строго говоря, он не описывает точно, какой же конкретно тип устройств обсуждается.

Первоначально, тактовая частота шины PC Card была установлена равной тактовой частоте шины ISA, то есть 8 МГц, что может быть использовано при работе с 8 и 16 разрядными устройствами. Соответственно, максимальная пропускная способность шины при работе 16 разрядными картами достигает 16 Мбайт в секунду.

Архитектура CardBus позволяет использовать 32 разрядные устройства. Кроме того, тактовая частота увеличена до величины, определенной для шины PCI, то есть 33 МГц, что увеличивает пропускную способность CardBus до более чем 128 Мбайт/сек.

Доступ к регистрам

Стандарт PC Card определяет 26 разрядный адресный диапазон (64 Мбайт). С другой стороны, адресация происходит по схеме, сходной со схемой, используемой в ISA архитектуре. В архитектуре CardBus схема использования пространства, адресуемого 32 разрядным адресом, аналогична схеме, примененной в архитектуре PCI.

Механизмы прерываний

В стандартах PC Card и CardBus определен один проводник (линия) для прерываний — IREQ, или CINT. Эта линия прерываний управляется уровнем (level sensitive), следовательно, может быть использована совместно несколькими картами на одной и той же шине. Однако при использовании многофункциональных PCMCIA карт (если линия прерываний используется совместно несколькими функциями карты) должно быть обеспечено арбитрирование средствами программного обеспечения.

Возможности DMA

Первоначально предложенный стандарт PC CARD не предполагал возможности DMA передачи данных. Более поздний стандарт, принятый в 1995 году, ввел DMA расширение в стандарт, названный 'DMA'. Это расширение стандарта позволяет выполнять передачу 16 разрядных слов в манере, сходной со стандартом ISA. Этот стандарт подразумевает, что все устройства, подключенные к шине, выступают в роли 'bus slave' устройств, совместно использующих DMA контроллеры. Так же, как и в архитектуре ISA, трудно реализовать режим 'bus master DMA'.

Стандарт CardBus позволяет выполнять операции DMA передачи данный почти что таким же образом, как это выполняется в архитектуре PCI. Данное дополнение, позволяющее работать с устройствами в роли 'bus master', является весьма важным. 16 и 32 разрядные операции DMA передачи данных в стандарте CardBus могут выполняться при частоте 33 МГц. Правда, фактор ограниченности геометрических размеров требует использования компонентов (ИС) более высокой степени интеграции.

Автоматическое распознавание и конфигурирование

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

Программное обеспечение для стандартов PC Card или CardBus разделяется на два крупных фрагмента: Сервисы Сокетов, Socket Services (в данном случае, socket — гнездо, разъем), и Службы Карт (Card Services). Первый представляет программное обеспечение уровня BIOS, и оно управляет в системе одним или более разъемов. Это программное обеспечение отвечает за обнаружение и объявление факта подключения нового устройства. Вторая часть (Card Service) управляет аппаратными ресурсами данной карты.


Глава 5. Прикасаясь к аппаратуре

Советы по работе с аппаратурой

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

  • Используемая шинная архитектура.
  • Регистры управления и способ доступа к ним из программного обеспечения.
  • Способ сообщения о текущем состоянии устройства и ошибках.
  • Поведение устройства в связи с использованием прерываний.
  • Механизмы передачи данных.
  • Память, реализованная в устройстве.

Архитектура шины

Шинная архитектура, в которую должно вписаться новое устройство, будет иметь определяющее значение для конструкции нового драйвера — сможет ли он опираться на лежащие ниже в стеке драйверы или вынужден будет реализовывать весь физический интерфейс заново. Должны быть понятны все особенности, касающиеся автоматического распознавания и конфигурирования нового устройства. Следует рассчитывать на то, что новое устройство будет участвовать в общих механизмах PnP и обмена сообщениями об изменениях энергоснабжения, предлагаемых Windows 2000/XP/Server 2003.

Регистры управления

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

  • Отдельные регистры могут быть регистрами только для чтения или только для записи.
  • Один и тот же регистр может представлять одни функции при операциях чтения и совершенно иные при операциях записи.
  • Регистры данных или состояния могут содержать неверные данные до истечения некоторого временного интервала после поступления команды-запроса.
  • Доступ к регистру (регистрам) может проходить в специфической последовательности.

Получение информации о состоянии устройства и об ошибках

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

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

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

Механизмы передачи данных

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

Используйте интеллект нового устройства

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

Использование встроенных возможностей интеллектуальных устройств может дать существенно лучшие результаты в работе и в диагностике такой аппаратуры.

Тестирование аппаратуры

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

Базисные тесты. Прежде всего, следует удостовериться, что все устройства и все соединительные кабели являются совместимыми и их подключение выполнено правильно и надежно. Например, неэкранированный кабель USB будет удовлетворительно работать с USB 1.1 (Low и Full Speed), но, возможно, будет причиной сбоев при работе на скорости Hi Speed в USB 2.0. Что касается подключения внешних устройств к параллельному порту, то совершенно одинаковые внешне кабели LPT могут создавать совершенно разные условия подключения, а при попытке повысить скорость передачи в режимах EPP или ECP некачественные кабели могут быть причиной экзотических сбоев.

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

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

Наконец, следует испытать встроенные средства диагностики, введя устройство в ошибочное, но предусматриваемое диагностическими средствами состояние. Встроенное программное обеспечение должно обнаружить эту ошибку и выдать должное сообщение.


Глава 5. Прикасаясь к аппаратуре

Заключение

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


Глава 6. Обслуживание ввода/вывода в режиме ядра

Глава 6

Обслуживание ввода/вывода в режиме ядра


Глава 6. Обслуживание ввода/вывода в режиме ядра

Контекст выполнения программного кода

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

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

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

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

Контекст исключения или внутреннего прерывания (trap)

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

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

Контекст прерывания

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

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

Код обслуживания прерывания может использовать области памяти, указатели на которые он может получить через внутренние переменные и ссылки собственно в драйвере. Однако следует внимательно следить за тем, чтобы используемые указатели или области памяти, на которые они указывают, не оказались в страничной памяти, поскольку это потенциально опасно и рано или поздно приведет к краху системы. В частности, следует аккуратно использовать директивы указаний компилятору типа #pragma code_seg("PAGE") и #pragma alloc_text("PAGE", MyFunctionName). При сомнениях, относительно корректности размещения кода в страничной памяти можно применять макроопределение PAGED_CODE(), которое выявит случаи входа в данный код с недопустимо высокими приоритетами (разумеется, только в отладочной версии драйвера).

Контекст программного потока режима ядра

Последний возможный вариант контекста выполнения кода драйвера состоит в том, что фрагмент кода выполняется в контексте 'отдельного потока программного кода режима ядра'.

Некоторые драйверы предпочитают создавать дополнительные программные потоки для выполнения специфических задач, связанных, например, с обслуживаем устройств, которые требуют последовательного опроса (polling), или при работе в условиях, которые продиктованы специфическими схемами организации ожидания. Эти потоки, выполняющиеся в режиме ядра, не так сильно отличаются от привычных потоков пользовательского режима, подробно описанных в книгах по Win32 программированию. Они выполняются в соответствии с решениями планировщика ядра и в соответствии с присвоенными им приоритетами. Объясняется это "простое", несмотря на режим ядра, поведение тем, что создаются такие потоки вызовом PsCreateSystemThread, который может быть сделан только из кода, работающего на уровне IRQL_PASSIVE_LEVEL. Соответственно, начинают работу они на этом же уровне IRQL среди приоритетов пользовательского режима.

Такой программный поток не имеет ни собственного TEB (Thread environment block), ни контекста пользовательского режима, хотя вполне возможно, что "ответвился" от потока, у которого такой контекст имелся. Новый программный поток работает только в режиме ядра. Он не может интерпретировать виртуальные адреса, полученные из приложений пользовательского режима — даже если он их как-то получит (например, через внутренние переменные драйвера).

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


Глава 6. Обслуживание ввода/вывода в режиме ядра

Приоритеты выполнения программного кода

Поскольку разные процессорные архитектуры реализуют разные подходы к управлению аппаратурой при помощи прерываний, разделяемых по приоритетам, операционная система Windows NT использует идеализированную схему, которая удовлетворяет особенностям всех аппаратных платформ. Практическая сторона данной схемы состоит в использовании процедур слоя аппаратный абстракций HAL, которые и берут на себя специфические особенности аппаратуры, позволяя создавать платформенно-независимый драйверный код.

Основой этой схемы абстрактных приоритетов прерываний является IRQL (interrupt request level) — уровень запроса на прерывание. Уровень IRQL представляет собой число, определяющее приоритет. Программный код, выполняющийся на данном уровне IRQL, не может быть прерван программным кодом, имеющим равный или более низкий IRQL. В таблице 6.1 приводятся уровни IRQL, используемые в Windows 2000/XP/Server 2003. Именно так уровни IRQL видятся драйверу, независимо от того, на каком процессоре или в какой шинной архитектуре приходится драйверу работать. Важно также и то, что в любой конкретный момент времени каждая инструкция (оператор программного кода) выполняется на одном определенном уровне приоритета со специфическим значением IRQL. Уровень IRQL входит в состав контекста выполнения каждого потока, следовательно, в любой момент времени операционной системе достоверно известен его текущий уровень IRQL.

Таблица 6.1. Уровни IRQL

Генерируется | Наименование | Назначение
Аппаратным обеспечением | HIGH_LEVEL | Проверка компьютера и шинные ошибки
 | POWER_LEVEL | Прерывание по сбою в энергоснабжении
 | IPI_LEVEL | Прерывания межпроцессорного взаимодействия для многопроцессорных систем
 | CLOCK_LEVEL | Интервальный таймер
 | PROFILE_LEVEL | Таймер профилирования
 | DIRQL | Платформенно-зависимое число уровней для прерываний устройств ввода/вывода
Программным обеспечением | DISPATCH_LEVEL | Планирование потоков и выполнение отложенных процедурных вызовов (DPC)
 | APC_LEVEL | Выполнение асинхронных процедурных вызовов (1)
 | PASSIVE_LEVEL | Уровень нормального исполнения потоков (0)

В приведенной схеме значения IQRL аппаратных прерываний лежат в интервале выше DISPATCH_LEVEL и ниже PROFILE_LEVEL. Эти уровни еще встречаются в литературе под названием 'device IRQL', DIRQL — уровни IRQL устройств. Уровни выше PASSIVE_LEVEL называются повышенными (elevated IRQLs). Программные потоки, работающие на повышенных уровнях, могут быть вытеснены только потоками с более высоким уровнем IRQL. Такой способ работы с потоками называется в литературе dispatching, диспетчеризация.

Потоки, работающие на уровне PASSIVE_LEVEL, попадают под управление планировщика заданий (sheduler). Приоритеты, которые различает планировщик заданий для потоков с уровнем PASSIVE_LEVEL, принимают значения от 0 до 32 (MAXIMUM_PRIORITY) и называются в ряде источников 'приоритетом планирования' (sheduler priority, 'приоритет планировщика').

Между потоками PASSIVE_LEVEL, имеющими приоритеты планирования Real-Time и Normal имеется существенное различие. Первые продолжают свою работу до тех пор, пока не появится поток с большим приоритетом, так что потоки низких приоритетов должны дожидаться, пока текущий поток RealTime не завершит работу естественным путем. Потоки с приоритетами Normal планируются по другим правилам. Для работы им выделяется определенный квант времени, после чего управление передается другим потокам такого же приоритета. Время от времени планировщик может повышать приоритет отложенного потока в пределах диапазона Normal, в результате чего все программные потоки среди потоков этой группы, даже имеющие самые низкие приоритеты, рано или поздно получают управление.

Таблица 6.2. Приоритеты планирования для потоков уровня PASSIVE_LEVEL IRQL

Приоритеты | Наименование | Назначение
RealTime (Приоритеты реального времени) | HIGH_PRIORITY (31) : | Приоритеты системных программных потоков (программного кода режима ядра)
 | : LOW_REALTIME_PRIORITY (16) |
Normal (Динамические приоритеты) | Normal maximum (15) : | Приоритеты потоков пользовательских приложений
 | : Normal Idle (1) |
 | LOW_PRIORITY (0) | Системный поток обнуления страничной памяти

Драйвер имеет возможность создавать системные программные потоки с приоритетами планирования, укладывающимися в диапазон RealTime и регулировать их приоритеты в этом диапазоне при помощи вызова KeSetPriorityThread (рекомендуемым DDK документацией значением для этой операции является LOW_REALTIME_PRIORITY, равное 16, см. заголовочные файлы wdm.h или ntddk.h). Подробнее вопросы работы с программными потоками будут рассмотрены в главе 10. Получить текущее значение приоритета планирования известного потока можно при помощи вызова KeQueryPriorityThread, в то время как получить текущее значение IRQL (при работе внутри самого потока режима ядра) можно при помощи вызова KeGetCurrentIrql, как это было сделано в коде драйвера Example, см. главу 3.

Значение приоритета системного потока сразу после его создания (без искусственного изменения) в Windows XP равно 8.

Что касается программных потоков пользовательского режима, то они могут иметь как приоритеты планирования из диапазона Normal, так и более низкие. Например, приоритет THREAD_BASE_PRIORITY_IDLE имеет численное значение — 15.

Обработка прерываний

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

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

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

Следует особо отметить, что такая схема позволяет запросу с более высоким значением IRQL прерывать работу процедур, обслуживающих прерывания с низким значением IRQL, то есть прерывать прерывания. Благодаря тому, что весь этот механизм для сохранения состояний использует стек, то каких-либо недоразумений не возникает. Однако при этом существенно повышается значение синхронизации выполнения потоков и обращения к совместно используемым данным.

Прерывания, вызванные программно

Нижние строки таблицы 6.1 описывают уровни IRQL, связанные с прерываниями, вызванными программно. Некоторые виды обработки прерываний инициируются программным кодом, работающим в режиме ядра, путем выполнения привилегированных инструкций (процессорных команд). Операционная система Windows NT 5 использует эти программно вызываемые прерывания для расширения своей схемы приоритетов, и это позволяет ей лучше выполнять планирование потоков. Введение этих IRQL позволяет разрешить противоречия между соревнующимися потоками путем принудительного повышения приоритета одного из них по сравнению с другими, например, при выполнении критических операций, связанных с конкретным устройством. В примере кода драйвера в главе 3 такая операция выполнена при помощи вызова KeRaiseIrlq.

Доступ к областям памяти пользовательских приложений

В момент, когда программный код приложений, выполняющийся в пользовательском режиме, делает запрос на ввод/вывод, то он передает адрес буферной области, которая содержит данные (или предназначена для их получения) и размещена в пользовательском адресном пространстве. Поскольку адреса пользовательского режима указывают в нижнюю часть (ниже 2 Гбайт для обычных версий ОС) страничных таблиц, драйвер должен рассматривать возможность того, что страничные таблицы могут (и будут) меняться до окончания обработки запроса. Это может произойти в случае, если рабочая ветвь кода драйвера выполняется в контексте прерывания или в контексте потока, выполняющегося в режиме ядра. Нижняя часть страничных таблиц меняется при каждом переключении процесса (process switch). Таким образом, далеко не весь программный код драйвера может рассчитывать на то, что все адреса пользовательского режима будут верны и применимы в течение всего времени обработки запроса.

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

Общим правилом является то, что виртуальная память пользовательских приложений является страничной, а к такой памяти нельзя обращаться из программного кода, выполнение которого происходит на IRQL уровне DISPATCH_LEVEL или выше. Этому постулату следовать необходимо по той простой причине, что работа по возвращению страницы памяти из swap-файла (в случае, если она туда попала) в оперативную память выполняется службами, которым для завершения этой операции могут потребоваться устройства, работающие при DIRQL более низком, чем IRQL кода-инициатора обращения к этой "неудачной" странице виртуальной памяти.

Способы доступа к буферным областям

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

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

Описанный выше метод носит название buffered I/O — буферизованного ввода/вывода. Он используется медленными устройствами, которые редко работают с большими объемами данных. Этот метод не сложен для реализации в логике драйвера, но требует дополнительных временных затрат на операции по копированию буферных областей.

Вторая стратегия позволяет избежать операций копирования путем предоставления драйверу прямого доступа к пользовательской буферной области в оперативной физической памяти. В начале выполнения операции Диспетчер ввода/вывода фиксирует всю область пользовательского буфера в памяти, что предотвращает перемещение этого блока в swap-файл и саму возможность возникновения ошибки отсутствующей страницы (page fault). Затем он создает список элементов страничной таблицы, которые отображаются на область памяти выше 2 Гбайт (на системную область), таким образом, устраняя повод для переключений контекста процесса. В сложившейся ситуации, когда память и элементы страничной таблицы зафиксированы на время обработки всего запроса ввода/вывода, драйверный код может без опаски работать с пользовательским буфером. Правда, свойства этого буфера существенно изменились: теперь эта область зафиксирована в памяти (фактически стала нестраничной), а оригинальный пользовательский адрес транслирован в другой адрес, пригодный для использования только в коде, работающем в режиме ядра.

Второй метод хорошо подходит для использования драйверами быстрых устройств, выполняющих перенос больших объемов данных. Этот метод известен как direct I/O — прямой ввод/вывод. Устройства, имеющие способность к операциям DMA, практически всегда используют этот механизм.


Глава 6. Обслуживание ввода/вывода в режиме ядра

Отложенные процедурные вызовы (DPC)

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

Для разрешения такого типа проблем, программный код, предназначенный для работы в режиме ядра, должен быть сконструирован таким образом, чтобы избегать продолжительной работы при повышенных уровнях IRQL. Одним из самых важных компонентов этой стратегии являются Deferred Procedure Calls (DPC) — отложенные процедурные вызовы.

Функционирование DPC

Схема применения отложенных процедурных вызовов позволяет построить процесс выполнения таким образом, что задача может быть запланирована кодом, работающим на высоком уровне IRQL, но она еще не выполняется. Такая отсрочка выполнения применима, если производится обслуживание прерывания в драйвере, и при этом нет никаких причин блокировать выполнение другого программного кода более низкого уровня IRQL. Иными словами — когда обработка данной ситуации может быть безболезненно перенесена на более позднее время.

Для учета заявок на вызов DPC процедур операционная система поддерживает очередь объектов DPC.

Для начала, ограничимся рассмотрением более простого случая работы с DPC процедурами, предназначенными для использования совместно с процедурами обработки прерываний. Данный тип DPC процедур получил в литературе специальное название DpcForIsr.

Объект DPC для использования в процедурах обработки прерываний создается по вызову IoInitializeDpcRequest, выполняемому обычно в стартовых процедурах драйвера. Данный вызов регистрирует предлагаемую драйвером DpcForIsr процедуру и ассоциирует ее с создаваемым объектом — достаточно распространенная методика в Windows. Следует особо отметить, что DPC объект, созданный данным вызовом так и останется в недрах операционной системы, недоступным разработчику драйвера. (Отличие DpcForIsr от других DPC процедур состоит только в том, что работа с последними проходит при помощи вызовов Ke...Dpc, а создаваемые для них DPC объекты доступны разработчику драйвера.)

Если драйвер зарегистрировал свою процедуру DpcForIsr, то во время обработки прерывания ISR процедурой в системную очередь DPC может быть помещен соответствующий DPC объект (фактически, запрос на вызов этой DpcForIsr процедуры позже) — при помощи вызова IoRequestDpc. Процедура DpcForIsr и завершит позже обработку полученного ISR процедурой запроса, что будет выполнено в менее критичных условиях и при низком уровне IRQL.

В общих чертах, функционирование DPC процедур (в данном случае, DpcForIsr) складывается из следующих операций:

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

Особенности механизма DPC

Как правило, работа с отложенными процедурными вызовами не является сложной, поскольку операционные системы Windows 2000/XP/Server 2003 предлагают большой набор системных вызовов, скрывающих большую часть деталей этого процесса. Тем не менее, особо следует выделить два наиболее обманчивых момента в работе с DPC.

Во-первых, Windows NT 5 устанавливает ограничение, состоящее в том, что один экземпляр DPC объекта может быть помещен в системную DPC очередь в определенный временной промежуток. Попытки поместить в очередь DPC объект, в точности совпадающий с уже там присутствующим, отвергаются. В результате, происходит только один вызов DPC процедуры, даже если драйвер ожидает выполнение двух вызовов. Это может произойти, если два прерывания были вызваны обслуживаемым устройством, а обработка первого отложенного процедурного вызова еще не начиналась. Первый экземпляр DPC объекта еще пребывает в очереди, в то время как драйвер уже приступил к обработке второго прерывания.

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

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

Если присмотреться к списку параметров вызовов IoInitializeDpcRequest и IoRequestDpc (предназначенных для работы с DpcForIsr процедурами), то нетрудно заметить, что DPC объект "привязан" к объекту устройства. При размещении этого объекта в DPC очереди в момент работы ISR процедуры также указывается объект устройства. Этим и достигается определенность, вызов какой конкретно DPC процедуры "заказан" ISR процедурой (соотнесение по объекту устройства). Это же говорит о том, что драйвер, который создал несколько объектов устройств (достаточно редкий случай), может эксплуатировать и несколько процедур DpcForIsr — по одной для каждого объекта устройства.

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

Выше было рассмотрено использование DPC процедур для завершения обработки прерываний, то есть DpcForIsr. Однако DPC процедуры можно использовать и в другом ключе, например, совместно с таймерами для организации ожидания. Для этого создастся объект DPC при помощи вызова KeInitializeDPC, который связывает этот объект с DPC процедурой, входящей в состав драйвера. После этого можно выполнять установку времени ожидания в предварительно инициализированном (используя KeInitializeTimer или KeInitializeEx) объекте таймера. Для установки интервала ожидания используется вызов KeSetTimer, которому в качестве одного из параметров необходимо передать указатель на инициализированный DPC объект. Пo истечении интервала ожидания DPC объект будет внесен в системную DPC очередь, и DPC процедура, ассоциированная с ним, будет вызвана так скоро, насколько этo будет возможно. Процедуры DPC данного, второго, типа обозначены в документации DDK термином 'Custom DPC'. (Этот вариант использования DPC процедур делает их весьма напоминающими APC вызовы пользовательского режима.)

Для размещения в системной DPC очереди объектов, соответствующих второму типу DPC процедур (не связанных с прерываниями), следует использовать вызов KeInsertQueueDpc. Соответственно, код-инициатор вызова должен работать на уровне IRQL не ниже DISPATCH_LEVEL.

Для очистки системной DPC очереди от Custom DPC процедур, например, если драйвер должен срочно завершить работу, предназначен вызов KeRemoveQueueDpc, который может быть вызван из кода любого уровня IRQL.


Глава 6. Обслуживание ввода/вывода в режиме ядра

Общий взгляд на структуру драйвера режима ядра

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

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

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

Ниже приводится краткое описание основных категорий процедур, входящих в состав драйвера режима ядра.


Глава 6. Обслуживание ввода/вывода в режиме ядра

Процедуры инициализации драйвера и очистки

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

Процедура DriverEntry

Диспетчер ввода/вывода вызывает данную процедуру при загрузке драйвера. Возможно, это произойдет в процессе загрузки операционной системы, но драйвер может быть загружен и динамически в любое время, как это было сделано с драйвером Example.sys в главе 3. Процедура DriverEntry решает первоочередные задачи, в частности, регистрацию в специальном массиве адресов других драйверных процедур (для того, чтобы Диспетчер ввода/вывода имел возможность позже производить их вызов по адресу). В случае если драйвер не работает по модели WDM (legacy драйвер, драйвер "в-стиле-NT"), то в этой же начальной процедуре можно провести локализацию оборудования, которое будет обслуживать драйвер, выделить или подтвердить использование аппаратных ресурсов (портов ввода/вывода, прерываний, каналов DMA) и предоставить имя, видимое всей остальной системе, для каждого обнаруженного устройства. Для WDM драйверов, поддерживающих спецификацию PnP, процесс определения аппаратных ресурсов откладывается на более позднее время и выполняется драйверной процедурой AddDevice.

Строго говоря, определенное имя (то есть DriverEntry) должно быть только лишь у этой "входной" процедуры. Имена остальных драйверных процедур не столь жестко определены, поскольку они не будут никому известны, за исключением обладателя исходного текста или хакера-исследователя, работающего с отладочной версией. От остальных процедур в системе остаются только их адреса, и к ним предъявляется требование иметь определенный интерфейс вызова, согласованный по типу получаемых и возвращаемых параметров. Поэтому все приведенные в книге имена драйверных процедур можно рассматривать как "говорящие фамилии", кратко описывающие назначение этих процедур. При разработке собственных драйверов можно использовать любые названия.

Процедура ре-инициализации

Некоторые драйверы, по природе обслуживаемого ими устройства, иногда не могут завершить процесс инициализации во время работы DriverEntry. Это может иметь место, если драйвер зависит от других драйверов или системных служб, которые еще не загружены к моменту вызова DriverEntry. Такого типа драйвера должны сделать запрос определенного вида, чтобы их инициализация была отложена. Отложенная инициализация будет выполнена вызовом процедуры ре-инициализации ReInitialize, адрес которой необходимо сообщить во время работы DriverEntry вызовом IoRegisterDriverReinitialization, см. таблицу 8.2.

Процедура выгрузки Unload

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

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

Процедура Shutdown

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

Процедура обратного вызова Bugcheck

В том случае, если драйверу необходимо получить управление при крахе системы, он должен реализовать callback-процедуру Bugcheck. Эта процедура, при должной ее регистрации, будет вызвана ядром операционной системы в соответствующем месте выполнения 'crash'-процесса — сколь ни противоречиво звучит эта фраза.


Глава 6. Обслуживание ввода/вывода в режиме ядра

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

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

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

Обработчики запросов Open и Close

Все драйверы, если только они не тестового назначения, должны иметь в своем составе процедуру CreateDispatch (в примере Example.sys эта процедура называлась Create_File_IRPprocessing), которая производит обработку пользовательского запроса CreateFile. Помимо того, драйверы, которые должны выполнять действия по очистке в ответ на пользовательский запрос CloseHandle, обязаны иметь в своем распоряжении еще и процедуру CloseDispatch (в примере Example.sys — процедура Close_HandleIRPprocessing).

Процедуры передачи данных

В зависимости от типа обслуживаемого устройства, драйвер может иметь отдельные рабочие процедуры для выполнения операций по переносу данных и операций управления устройством. Функции пользовательского режима ReadFile, WriteFile и DeviceIoControl "переключаются" на соответствующие процедуры, из числа процедур, предложенных драйвером Диспетчеру ввода/вывода. Драйверу необходимо всего лишь предложить свои процедуры для тех операций, которые он собирается обслуживать.

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

Драйвер Example.sys для обслуживания указанных запросов зарегистрировал функции ReadWrite_IRPhandler и DeviceControlRoutine.

Процедура StartIo

Выполняя регистрацию рабочей процедуры StartIo, драйвер соглашается участвовать в процессе, называющемся System Queuing, то есть создание очередей необработанных запросов при помощи системных средств — в отличие от Driver Queuing— ведение учета необработанных запросов средствами собственно драйвеpa (Для создания и ведения внутренних очередей драйверы используют вызовы KeInitalizeDeviceQueue и прочие Ke...Queue).

В том случае, если какая-либо из рабочих процедур (например, обработки запросов Read-Write) не может завершить обработку запроса сразу, то она помечает текущий IRP пакет как 'pending' при помощи вызова, а на самом деле — макроопределения (см. файлы wdm.h или ntddk.h) IoMarkIrpPending, таблица 9.12. После этого она делает вызов IoStartPacket и возвращает управление Диспетчеру ввода/вывода с кодом STATUS_PENDING.

В результате вызова IoStartPacket Диспетчер ввода/вывода вызывает драйверную процедуру StartIo или, если устройство занято (то есть уже существует очередь необработанных пакетов IRP), помещает очередной пакет с состоянием 'pending' в очередь необработанных запросов. Таким образом, Диспетчер ввода/вывода выполняет сериализацию IRP запросов, вызывая рестарт ввода/вывода только по окончании обработки предыдущего запроса.

Как правило, процедура StartIo организует внутреннюю сортировку поступающих в нее IRP пакетов по кодам IRP_MJ_Xxx (например, обычным оператором 'switch'). Завершается работа StartIo тем, что текущий IRP пакет помечается как обработанный вызовом IoCompleteRequest (таблица 9.10), после чего выполняется вызов IoStartNextPacket, что побуждает Диспетчера ввода/вывода вызывать StartIo снова — для следующего из оставшихся в очереди IRP пакетов.

В том случае, если невозможность обработки запроса была обусловлена временной неработоспособностью устройства, то запуск обработки очереди накопившихся IRP пакетов может быть выполнен следующим образом при участии ISR процедуры. Устройство сигнализирует о способности к работе прерыванием, ISR процедура обработки прерывания планирует запуск соответствующей DPC процедуры, которая делает вызов IoStartNextPacket (поскольку он может быть сделан только программным кодом уровня DISPATCH_LEVEL). Этот вызов и запускает обработку помещенных в очередь IRP пакетов, побуждая Диспетчера ввода/вывода вызывать процедуру StartIo. Аналогично, вместо автоматического запуска обработки следующего IRP пакета в конце процедуры StartIo, как было указано выше, можно запускать следующую операцию ввода/вывода также по сигналу прерывания от устройства.

Размещение необработанных пакетов в очереди производится на принципах FIFO, то есть в конец очереди. Однако можно изменить это правило, задавая определенные значения параметра Key в вызове IoStartPacket. Изменить порядок извлечения очередного необработанного пакета из очереди можно, задавая определенные значения параметра Key в вызове IoStartNextPacketByKey.

Процедура StartIo регистрируется во время работы DriverEntry записью ее адреса в поле DriverStartIo в структуре объекта драйвера (что похоже на регистрацию процедуры AddDevice).

Процедура обслуживания прерываний

Процедура обслуживания прерываний (Interrupt Service Routine, ISR), входящая в набор процедур драйвера, вызывается диспетчером прерываний ядра (Kernel's interrupt dispatcher) всякий раз, когда устройство генерирует сигнал прерывания. На этой процедуре лежит обязанность полного обслуживания аппаратного прерывания.

Собственно в ISR процедуре драйвера должна быть реализована самая минимальная обработка создавшейся ситуации. Если дополнительная, требующая больших временных затрат обработка прерывания требуется по логике работы устройства, то следует прибегнуть к использованию механизма DPC (отложенных процедурных вызовов), то есть запланировать отложенный процедурный вызов в текущей процедуре обработки прерываний (ISR). После этого, остаток работы ISR процедуры можно завершить на уровне IRQL ниже уровня аппаратных прерываний (DIRQL), понизив приоритет выполняемого кода данных комплексом мер.

Процедуры DPC

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


Глава 6. Обслуживание ввода/вывода в режиме ядра

Процедуры обратного вызова для синхронизации доступа к ресурсам

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

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

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

Существует три типа callback процедур, поддерживаемых Диспетчером ввода/вывода, о чем ниже.

Процедура ControllerControl

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

В Windows NT эта абстракция реализована в виде объектов контроллера, которые создается вызовом IoAllocateController. Как правило, процедура, запускающая операцию ввода/вывода, выполняет запрос "владения" объектом контроллера при помощи вызова IoAllocateController, одновременно устанавливая процедуру, которая получит управление, как только запрос на владение будет удовлетворен. Эта callback процедура известна в литературе под именем ControllerControl. По завершении обработки запроса ввода/вывода драйвер освобождает контроллер вызовом IoFreeController.

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

Объекты контроллера в модели WDM не поддерживаются, то есть разработчики DDK ограничили применение этой абстракции только драйверами "в-стиле-NT" (legacy драйверами).

Процедура AdapterControl

Аппаратное обеспечение DMA (прямого доступа к памяти) является другим совместно используемым (разделяемым) ресурсом, который должен передаваться от драйвера к драйверу. Перед выполнением DMA операции драйвер делает запрос на исключительное владение необходимым аппаратным обеспечением (что реализовано через абстракцию объекта адаптера), обычно, это — DMA канал. Когда доступ подтверждается, выполняется процедура обратного вызова AdapterControl.

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

Процедуры SynchCritSection

Обслуживание прерывания происходит на одном из уровней аппаратных DIRQL (что зависит от типа устройства), в то время как весь остальной код драйвера работает на уровне приоритетов не выше DISPATCH_LEVEL. В случае, если когда-либо этому относительно низкоприоритетному коду понадобится поработать с ресурсами, которые использует и ISR (процедура обслуживания прерываний) драйвера, то эти действия должны выполняться только внутри callback процедуры SynchCritSection. Тот программный код, которому хочется корректно обратиться к ресурсам, что, возможно, затребует неожиданно вступившая в права высокоприоритетная процедура обслуживания прерываний, должен воспользоваться посредническими услугами callback процедуры SynchCritSection. Запустить эту callback процедуру необходимо вызовом KeSynchronizeExecution из программного кода, который работает на любом из уровней не выше DIRQL прерывания. Сама процедура SynchCritSection будет работать с приоритетом прерывания, поэтому пребывание внутри ее кода следует минимизировать (постоянное требование для кода высоких уровней IRQL).

Таким образом, фрагмент кода с изначально низким уровнем IRQL через процедуру обратного вызова получает возможность сделать работу при уровне DIRQL устройства, не опасаясь, что в это время управление будет передано в ISR процедуру. По окончании SynchCritSection прежнее значение IRQL восстанавливается.


Глава 6. Обслуживание ввода/вывода в режиме ядра

Другие процедуры драйвера

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

Таймерные процедуры

Драйвер, которому нужно выполнять точный отсчет временных интервалов, должен использовать либо DPC процедуру IoTimer (которая, если она зарегистрирована должным образом, всегда вызывается один раз в секунду), либо Custom DPC процедуру для таймера (кратко была рассмотрена выше). В литературе последняя упоминается как CustomTimerDpc.

Процедура IoCompletion

Драйвер модели WDM, работающий внутри многослойной драйверной структуры, может испытывать потребность в том, чтобы его уведомляли, когда будет завершена обработка IRP запроса, посланного драйверу нижнего уровня. Для этой цели драйвер может зарегистрировать callback процедуру IoCompletion (так она называется в документации DDK), при помощи которой он может выполнять весьма впечатляющие трюки. В частности, драйвер может разбивать крупные операции ввода/вывода на более мелкие. Механизм достаточно прост: по окончании частичного переноса данных, управление оказывается в процедуре IoCompletion, которая тут же начинает следующий перенос данных. Регистрация этой callback процедуры выполняется при помощи вызова IoSetCompletionRoutine.

Процедура CancelRoutine

Драйвер должен учитывать возможность того, что какой-либо из IRP запросов (текущий или находящийся в состоянии ожидания обработки) может быть отменен. Простейшая ситуация, которая к этому ведет, возникает, когда пользовательское приложение, не дождавшись отклика от устройства и драйвера, попросту снимается при помощи диспетчера задач. Тем временем драйвер, будто ничего не случилось, пытается завершить порученные ему задачи. Диспетчер ввода/вывода выполняет удаление IRP запросов, находящихся в ожидании обработки, и мог бы сообщить об этом драйверу вызовом callback процедуры CancelRoutine (под таким именем она фигурирует в документации DDK), если бы тот зарегистрировал ее при помощи вызова IoSetCancelRoutine. Особенность применения этих процедур состоит в том, что при помощи вызова IoSetCancelRoutine можно назначать разные CancelRoutine процедуры для отслеживания разных экземпляров IRP пакетов. Более того, даже на разных уровнях работы драйвера можно заново определять процедуру CancelRoutine (необходимо только хранить указатели на интересующие IRP пакеты), а то и вовсе отменить отслеживание уничтожения IRP пакетов — указав NULL вместо адреса CancelRoutine при вызове IoSetCancelRoutine.

Другая ситуация удаления пакетов IRP, ожидающих обработки, возникает в случае, если пользовательское приложение применило вызов API функции CancelIo (см. заголовочный файл winbase.h или документацию MSDN). Правда, применение этого вызова требует, чтобы доступ к драйверу был получен с использованием флага FILE_FLAG_OVERLAPPED, а этот прием программисты применяют крайне редко. Подробнее о CancelRoutine см. в 9 главе.


Глава 6. Обслуживание ввода/вывода в режиме ядра

Последовательность обслуживания запросов ввода/вывода

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

  • Предварительная обработка Диспетчером ввода/вывода.
  • Предварительная обработка драйвером устройства.
  • Старт устройства и обслуживание прерывания.
  • Пост-обработка драйвером.
  • Пост-обработка Диспетчером ввода/вывода.

Предварительная обработка Диспетчером ввода/вывода

На данном этапе производится не связанная с устройством подготовка запроса и его предварительная верификация.

  1. Подсистема Win32 (ограничимся этой подсистемой) преобразует запрос в системный сервисный вызов (native system service call). Диспетчер системного сервиса переходит в режим ядра и управление передается Диспетчеру ввода/вывода.
  2. Диспетчер ввода/вывода выделяет память под структуру данных, известную под именем I/O RequestPacket (IRP), пакет запроса на ввод/вывод (пакет IRP). В следующей главе эта структура описывается детальнее, но в первом приближении, вполне можно считать пакет IRP рабочим рецептом (предписанием), выдаваемым драйверу и, соответственно, устройству, которое тот обслуживает. Структура данных IRP заполняется необходимой информацией, включая код, которым обозначается тип запроса на ввод/вывод.
  3. Диспетчер ввода/вывода осуществляет некоторую проверку правильности аргументов, переданных из запроса (инициированного из кода пользовательского режима). Проверка включает верификацию дескриптора файла (имеется в виду, что доступ к устройству из программного кода пользовательского режима осуществляется как доступ к файлу — по файловому дескриптору, ассоциированному с нужным устройством, например, посредством вызова ReadFile). Кроме того, выполняется проверка прав доступа к этому файловому объекту, проверка, предоставил ли драйвер процедуру для обслуживания такого типа запроса, и проверка адресов пользовательских буферных областей памяти, необходимых для обслуживания запроса.
  4. В случае, если запрос является операцией буферизованного ввода/вывода (buffered I/O), Диспетчер ввода/вывода получает область памяти в области нестранично организованной памяти (нестраничном пуле) под создание буфера, после чего копирует данные из области пользовательского буфера в этот системный буфер. В случае, если запрос к устройству требует прямого ввода/вывода (direct I/O), производится блокирование пользовательского буфера в физической памяти и создается список дескрипторов страниц (MDL список), через которые драйвер имеет возможность доступа к этому пространству физической памяти.
  5. Диспетчер ввода/вывода производит вызов необходимых рабочих процедур драйвера (dispatch routines).

Предварительная обработка в драйвере

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

  1. Выполняет дополнительную проверку параметров входящего запроса. Драйвер может устанавливать зависящие от устройства ограничения, которые не могут быть известны Диспетчеру ввода/вывода. Например, пользователь может затребовать передачу большого объема данных, который превышает возможности обслуживаемого устройства (или драйвера).
  2. В случае, если запрос не требует работы с физическим устройством (например, считывание 0 байт данных), то вызываемая драйверная процедура может завершить обработку и "отправить" IRP пакет обратно Диспетчеру ввода/вывода, пометив его как обработанный.
  3. В случае, если запрос требует для завершения обработки реального обращения к обслуживаемому физическому устройству, рабочая процедура может пометить IRP пакет как незавершенный (pending). Таким образом, Диспетчеру ввода/вывода сообщается, что необходимо поместить в очередь запрос на вызов процедуры StartIo как только устройство освободится — возможно, что устройство еще обрабатывает предыдущий запрос. Вообще говоря, этап общения с устройством может быть реализован весьма экзотическими приемами, тут как раз место, где фантазия разработчиков разыгрывается не на шутку.

Старт операции ввода/вывода

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

Ожидается, что процедура старта ввода/вывода, реализация которой возлагается исключительно на разработчика драйвера, выполняет следующие задачи (или часть из них):

  1. Проверяет код IRP функции (чтение, запись и т.п.) и выполняет установочные действия для данного типа операций.
  2. В случае, если устройство (то есть объект устройства, которому адресован IRP пакет) по логике работы олицетворяет только одну из функций сложного реального устройства, запрашивает исключительный доступ к заранее созданному объекту контроллера, планируя при этом вызов соответствующей процедуры ControllerControl.
  3. В случае, если запрос требует DMA операций, выполняет надлежащие операции над объектом адаптера и планирует вызов процедуры AdapterControl (которая будет вызвана, сразу же, как только условия исключительного доступа к соответствующему DMA каналу будут удовлетворены).
  4. Использует процедуру SynchCritSection для выполнения безопасного доступа к тем ресурсам, которые могут потребоваться процедуре обработки прерываний.
  5. Возвращает управление Диспетчеру ввода/вывода в ожидании сигналов прерывания от устройства.

Пример драйвера с использованием механизма System Queuing подробно рассматривается в главе 11.

Процедура обслуживания прерываний ISR

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

  1. Проверяет, ожидалось ли прерывание (относится ли поступившее прерывание к обслуживающему устройству).
  2. Освобождает (завершает) прерывание.
  3. В случае, если ранее была начата операция программируемого ввода/вывода (не DMA), но передача данных еще не завершена окончательно, процедура ISR могла бы начать операцию передачи следующей порции данных и завершить свою работу, пока она не будет вызвана по поводу следующего прерывания.
  4. В случае, если ранее была начата операция DMA и остались еще не переданные данные, то ISR могла бы запланировать вызов DPC процедуры для настройки DMA аппаратуры и передачи следующей порции данных.
  5. В случае, если произошла ошибка или передача данных не была завершена, то ISR могла бы запланировать вызов DPC процедуры для того, чтобы выполнить пост-обработку при более низком уровне IRQL.

Пост-обработка, выполняемая драйвером

Диспетчер DPC выполняет вызовы DPC процедур драйвера для того, чтобы решать задачи пост-обработки, а именно:

  1. В случае, если выполнялся набор операции по переносу данных и некоторая часть данных еще не была передана, DPC процедура производит установку аппаратуры, производит "старт" устройства и, в ожидании нового прерывания, возвращает управление Диспетчеру ввода/вывода. При этом IRP пакет остается пока в состоянии 'pending' (о его завершении будет объявлено позже — по окончании переноса).
  2. В случае, если произошла ошибка или превышено время ожидание отклика (таймаут), DPC процедура может записать это событие во внутреннюю очередь, поддерживаемую для данного объекта устройства, и затем либо сделать повторную попытку, либо прервать обработку запроса на ввод/вывод. Очереди IRP пакетов для устройств, Device Queue, могут поддерживаться драйверами как альтернатива системным очередям (System Queuing).
  3. DPC процедура освобождает необходимые для переноса ресурсы, удерживаемые драйвером (среди которых могут быть DMA ресурсы).
  4. DPC процедура помещает размер переданных данных и информацию о финальном состоянии в IRP пакет.
  5. Наконец, если обработка IRP пакета действительно завершена, DPC процедура сообщает Диспетчеру ввода/вывода об окончании обработки текущего IRP запроса тем, что помечает его как завершенный (вместо 'pending' — ожидающий обработки) и делает вызов IoStartNextPacket. Это указывает Диспетчеру ввода/вывода на то, что следует переходить к вызову процедуры StartIo для следующего IRP пакета, если таковой ожидает обработки.

Пост-обработка, выполняемая Диспетчером ввода/вывода

После того как DPC процедура или одна из рабочих процедур помечают IRP пакет как завершенный, Диспетчер ввода/вывода (разумеется, когда получит управление) осуществляет завершающие действия, которые в литературе и документации DDK собирательно называются "очисткой" (cleanup). Это означает:

  1. В том случае, если выполнялась операция записи при буферизованном вводе/выводе (buffered I/O), Диспетчер ввода/вывода освобождает буферную область, использованную во время только что завершенного переноса данных.
  2. В случае, если выполнялась процедура прямого ввода/вывода (direct I/O), Диспетчер ввода/вывода отменяет фиксацию (lock) в оперативной памяти тех страниц, в которых размещался пользовательский буфер.
  3. Диспетчер ввода/вывода помещает в очередь запрос к программному потоку, инициатору первоначального запроса, на выполнение асинхронного процедурного вызова (asynchronous procedure call, APC), работающего в режиме ядра. Этот APC вызов будет выполнять программный код Диспетчера ввода/вывода в контексте потока-инициатора запроса на операцию ввода/вывода.
  4. Выполняющийся в режиме ядра программный АРС-поток производит перенос информации о состоянии и размере переданных данных в пространство пользовательского приложения.
  5. В случае, если выполнялась процедура чтения категории буферизованного ввода/вывода (buffered I/O), процедура APC производит копирование содержимого буферной области, размещенной в нестраничном пуле памяти, в область памяти, предоставленную пользовательским приложением в качестве рабочего буфера. Затем освобождается системный буфер в нестраничном пуле.
  6. В случае, если исходный запрос был сделан для выполнения асинхронного ввода/вывода, процедура APC устанавливает ассоциированное событие (event) и/или файловый объект в состояние, сигнализирующее пользователю об окончании обработки запроса. (Если читатель не знаком с асинхронным вводом/выводом, то может получить доступ к его описанию в документации MSDN по ключевым словам OVERLAPPED, CancelIo, ReadFileEx и т.п.)
  7. В случае, если исходный запрос содержал процедуру завершения (как, например, при использовании функций пользовательского режима ReadFileEx/WriteFileEx в пользовательском приложении), APC процедура производит планирование вызова в дальнейшем APC процедуры пользовательского режима, которая выполнит вызов процедуры завершения, указанных в параметрах вызовов ReadFileEx/WriteFileEx.


Глава 6. Обслуживание ввода/вывода в режиме ядра

Заключение

Краткое введение в подсистему ввода/вывода операционной системы Windows NT 5 завершено. Полностью, хотя и весьма кратко, описана структура драйвера Windows 2000/XP/Server 2003, который предназначен для работы в режиме ядра.

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


Глава 7. Приемы программирования в режиме ядра

Глава 7

Приемы программирования в режиме ядра

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


Глава 7. Приемы программирования в режиме ядра

Соглашения об описании данных и функций

Дополнительные описатели типов

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

Помимо типов данных, представляющих сложные данные (структуры и объединения, типа DRIVER_OBJECT), в драйверных исходных текстах широко применяются новые обозначения известных стандартных типов данных. Заголовочный файл ntdef.h содержит множество определений, которые придают новый вид старым и хорошо известным описателям типов языка С. Например, описатели беззнаковых целых типов и указателей на переменные такого типа принимают вид:

typedef unsigned char UCHAR;
typedef unsigned short USHORT;
typedef unsigned long ULONG;
.......
typedef UCHAR *PUCHAR;
typedef USHORT *PUSHORT;
typedef ULONG *PULONG;
.......
typedef unsigned __int64 ULONGLONG;
.......
typedef ULONGLONG *PULONGLONG;

Для чего это затеяна эта игра? В основном — для унификации стиля, когда в обращение вводятся совершенно новые типы данных, которые не являются базисными для языка С, скажем типа WCHAR (двухбайтный Unicode символ), или типа данных (а на самом деле — объединения) LARGE_INTEGER, наиболее часто используемого при программировании таймерных объектов, например:

typedef union _LARGE_INTEGER {
    struct {
        ULONG LowPart;
        LONG HighPart;
    };
    struct {
        ULONG LowPart;
        LONG HighPart;
    } u;
    LONGLONG QuadPart;
} LARGE_INTEGER;

Другая цель этих дополнений — достичь универсальности исходных текстов при переходе с 32-разрядных платформ на 64-разрядные.

Квалификаторы IN, OUT, OPTIONAL

Еще одним небесполезным элементом украшения программного кода, активно используемого в DDK, являются макроопределения IN, OUT и другие. Как можно увидеть в файле ntdef.h, они не обозначают ровным счетом ничего, но зато повышают наглядность программного кода при его чтении, поскольку сообщают о назначении переменных, рядом с которыми используются. (Поскольку они ничего не значат, что является нетипичным для программирования, то позволим себе применить к ним нетипичный термин "квалификатор", который тоже ничего особенного не обозначает.)

#define IN
..........
#define OUT
..........
#define OPTIONAL
..........
#define CRITICAL

Если обратиться к использованию этих элементов, то можно рассмотреть определение функции RtlZeroMemory, которая может быть использована для обнуления некоторой области виртуальной памяти (обнуляет страничную память на уровнях IRQL только ниже DISPATCH_LEVEL, нестраничную — на любых), а именно:

VOID
RtlZeroMemory(IN VOID UNALIGNED *Destination, IN ULONG Length);

В этом описании квалификатор IN сообщает, что указатель Destination и длина буфера Length являются параметрами, передаваемыми внутрь вызова RtlZeroMemory.

В иных случаях бывают даже сочетания IN OUT, что сигнализирует: через этот параметр вызова информация передается внутрь данного вызова и возвращается обратно. Квалификатор OPTIONAL обозначает необязательные параметры.

Типы возвращаемых значений функций

Все функции, описываемые в языке С, либо возвращают значение определенного типа, либо не возвращают ничего (в последнем случае они описываются типом void). В переводе на новые термины, предлагаемые DKK, функции возвращают значения типа CHAR, UCHAR, SHORT, USHORT и т.п., либо описываются как VOID функции.

В дополнение к этим, знакомым и понятным типам функций в DDK добавлен тип NTSTATUS. Что означает этот новый тип?

Тип NTSTATUS несет код завершения функции и, на самом деле, является простым переопределением типа integer long, что можно увидеть, например, в файле ntdef.h, а именно:

typedef LONG NTSTATUS;
typedef NTSTATUS *PNTSTATUS;

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

Когда Диспетчер ввода/вывода вызывает одну из процедур драйвера, а она возвращает ему управление, передавая при этом код NTSTATUS, то сигнализирует ему об условиях окончания работы. Файл NTSTATUS.h содержит символьные имена для всех возможных значений кодов NTSTATUS.

Код, сообщающий об удачном завершении, носит имя STATUS_SUCCESS (кстати, равный 0). В случае, если работа завершена с ошибкой, код неудачного завершения, например, STATUS_UNEXPECTED_IO_ERROR (равный 0xC00000E9), транслируется системой в системный код ошибки и передается пользовательскому приложению, для которого работала данная процедура драйвера.

Для работы с кодами завершения NTSTATUS существуют несколько полезных макроопределений (описанных в файле ntdef.h), среди которых самым употребительным является макроопределение NT_SUCCESS(). При употреблении в логических выражениях, оно позволяет проверять, означает ли анализируемый код удачное завершение работы, например:

NTSTATUS Status = IoConnectInterrupt();
if( !NT_SUCCESS(Status) ) { <обработка ошибки> }

Соглашения об именах функций драйвера и системных вызовов

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

Поскольку Microsoft следует определенным правилам составления имен своих вызовов, то системные функции отличить в тексте драйвера несложно: они имеют префикс из числа представленных в таблице 4.1, например, HalGetInterruptVector, вызов, относящийся к множеству аппаратных абстракций, что обозначено префиксом Hal. Правда, Microsoft все-таки не до конца последовательно проводит эту линию в жизнь. Например, в именах функций, обслуживающих объект адаптера (структура DMA_OPERATIONS), нет уже никаких префиксов такого типа, как, скажем, в именах FreeAdapterChannel, CalculateScatterGatherList и AllocateCommonBuffer.

В пакете DDK все имена типов данных и все макроопределения вводятся прописными (большими) буквами, например, PVOID, PHYSICAL_ADDRESS, READ_PORT_UCHAR. Упоминание о так называемой венгерской нотации в DDK практически не встречается, однако, Microsoft рекомендует разработчикам драйверов использовать собственные короткие префиксы для обозначения собственных функций и, возможно, идентификаторов переменных.


Глава 7. Приемы программирования в режиме ядра

Операции с плавающей точкой

Редко, но все-таки случаются ситуации, когда необходимо использовать операции с плавающей точкой и, соответственно, задействовать сопроцессор (FPU). Особенность этой ситуации состоит в том, что в режиме ядра программный поток, намеревающийся использовать сопроцессор, обязан сохранить текущее состояние всех регистров сопроцессора (ST0-ST7, MMX0-MMX7, XMM0-XMM7). Для сохранения этой информации в наборе системных вызовов режима ядра предусмотрены функции KeSaveFloatingPointState и KeRestoreFloatingPointState, первая из которых сохраняет состояние регистров сопроцессора в предоставляемом буфере, описываемом типом KFLOATING_SAVE, а вторая выполняет восстановление состояния сопроцессора по окончании работы с ним. Пример кода для работы с сопроцессором в режиме ядра приводится ниже:

double          Xfloat;
NTSTATUS        status;
KFLOATING_SAVE  savedFPUData;
status = KeSaveFloatingPointState(&savedFPUData);

if (NT_SUCCESS(status))
{// Работа с сопроцессором
    Xfloat = 0.;
    . . . . . . . . . .
    // Восстановление состояния сопроцессора из savedFPUData :
    KeRestoreFloatingPointState(&savedFPUData);
}

Следует обратить внимание на то, что работа с сопроцессором выполняется только в случае, если удачно завершен вызов функции KeSaveFloatingPointState, которая должна вызываться на уровне IRQL не выше DISPATCH_LEVEL. Вызов функции KeRestoreFloatingPointState для восстановления состояния сопроцессора должен выполняться на том же уровне, что было выполнено сохранение.

Операции с памятью

Операционная система Windows оперирует тремя типами адресов:

  • Виртуальные адреса, которые транслируются в физические адреса перед доступом к области памяти.
  • Физические адреса, которые реально указывают в область физической памяти. Следует отметить, что по этим адресам содержимое памяти всегда появляется на шине доступа к памяти, независимо от того, реально ли она присутствует в ОЗУ, или содержится внутри обслуживаемых драйвером устройств.
  • Логические адреса. Этот тип описывает специальные адреса, используемые уровнем HAL при общении с устройствами. Соответственно, уровень HAL и отвечает за операции с этими адресами.

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

Адреса 4 гигабайтного виртуального пространства памяти 32-разрядных версий операционной системы Windows NT 5 (об отличиях для 64-разрядных версий было сказано в главе 4) делятся на 2 нижних гигабайта памяти пользовательского виртуального пространства, имеющего смысл только в контексте пользовательского приложения (процесса), которому оно выделено, и 2 верхних гигабайт системного виртуального пространства режима ядра. Системное адресное пространство доступно всем программным потокам режима ядра. (Иначе, как смогло бы работать программное обеспечение режима ядра собственно операционной системы?!) Все 4-х гигабайтное адресное пространство можно представить в виде книги с одной обложкой. Толстая обложка — это системное адресное пространство, тонкие бумажные листы — это виртуальные и автономные пользовательские адресные пространства.

Системное виртуальное пространство памяти режима ядра делится на диапазоны (обычная архитектура x86), представленные в таблице 7.1.

Адреса с 0xC0000000 по 0xC0800000 используются для хранения данных Менеджера памяти, который поддерживает механизм виртуальной памяти.

Диапазон с 0xFFBE0000 по 0xFFC00000 используются для хранения информации о страничном файле (файле подкачки), которая используется для сброса содержимого физической памяти в этот файл. (Методология crash дампа предусматривает создание crash dump файла из этого файла подкачки при следующей загрузке системы.)

Адреса с 0xE1000000 по 0xFFBE0000 занимают области странично и нестранично организованной памяти, что вместе составляет менее 500 мегабайт.

Какие трюки можно проделывать с виртуальной памятью? На этот не слишком конкретный вопрос существует ответ в виде вопроса: а для достижения чего именно?

Рассмотрим довольно незамысловатую ситуацию, предпосылки которой рассматривались ранее. Предположим, драйвер создал программный поток (вызовом PsCreateSystemThread), который в некоторой ситуации должен выполнять некоторую работу, например, по сигналу функции обработки IOCTL запросов — выполнить перенос данных в буфер, предоставленный пользовательским приложением. Предположим, что разработчик драйвера так задал IOCTL код (используя метод буферизации NEITHER), что в драйвер поступает пользовательский адрес буфера (значение меньше 0x80000000). Разработчик драйвера через внутренние переменные передает этот адрес программному потоку, который должен выполнить перенос, и... Наступает сбой системы.

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

Когда драйверная функция обработки IOCTL запросов пользовательского режима (по вызову DeviceIoControl) получает адрес пользовательского буфера по методу буферизации NEITHER, то этот пользовательский виртуальный адрес имеет смысл в этой функции, поскольку она работает в контексте пользовательского программного потока и интерпретация пользовательского виртуального адреса не вызовет проблем. Другое дело, если этот адрес окажется в распоряжении программного потока, созданного драйвером по вызову PsCreateSystemThread, где интерпретация данного адреса вызовет ошибку, поскольку, как указано в документации DDK, этот системный программный поток не имеет никакого пользовательского контекста. Это одна из проблем.

HAL | 0xFFFFFFFF
 | ...
Информация CRASH DUMP | 0xFFC00000
 | ...
Нестраничный пул | 0xFFBE0000
 | ...
Страничный пул | ...
 | 0xE1000000
Файловый кэш | ...
 | 0xC1000000
Пространство файлового кэш-менеджера | ...
 | 0xC0C00000
Не используется | ...
 | 0xC0800000
Зарезервировано | ...
 | 0xC0400000
Элементы директории страниц (PDE) | ...
 | 0xC0300000
Элементы таблицы страниц (РТЕ) | ...
 | 0xC0000000
Не используется | ...
 | 0xA3000000
Memory Mapped files |
Копия операционной системы | ...
 | 0x80000000
 Таблица 7.1. Диапазоны памяти системного адресного пространства Windows 2000

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

Чтобы решить задачу в данной постановке необходимо, во-первых, создать список MDL (структуру, хранящую отображение блока виртуальной памяти на физическую память), зафиксировать пользовательский буфер в физической оперативной памяти и передать MDL список (или соответствующий виртуальный адрес системного адресного пространства) рабочему потоку. По выполнении работы, следует разблокировать страницы пользовательского буфера и освободить структуру MDL списка. Соответственно, при этом делаются вызовы системных функций: IoAllocateMdl, MmProbeAndLockPages, MmGetSystemAddressForMdl, MmUnlockPages и IoFreeMdl.

Вызов IoAllocateMdl создает структуру MDL списка для указанного виртуального адреса (пользовательского или системного адресного пространства) с указанной длиной.

Вызов MmProbeAndLockPages проверяет присутствие страниц в физической памяти, подгружает (для страничной памяти), если они отсутствуют, и производит их фиксацию (после чего данные страницы не могут быть сброшены в страничный файл на жестком диске). Для корректного вызова MmProbeAndLockPages по поводу MDL, составленного для страничной памяти, программный поток должен работать на уровне IRQL ниже DISPATCH_LEVEL — чтобы позволить отработать системному коду, если страница окажется в страничном файле. (В случае, если исходный буфер находился бы в нестраничной памяти, то данный вызов можно было бы сделать с уровня IRQL равном DISPATCH_LEVEL или ниже.)

Функция MmGetSystemAddressForMdl возвращает виртуальный адрес, вычисленный из MDL списка, так, будто рассматриваемая область памяти находится в системном адресном пространстве, а именно — в нестраничном пуле. Этот адрес можно использовать в любом месте кода драйвера на любых уровнях IRQL, даже в процедуре обработки прерываний. Контекст выполнения для этого адреса не имеет никакого значения. (Справедливости ради, следует отметить, что перевод в системный адрес не является обязательной операцией, можно и далее использовать MDL список, что позволяют делать, в частности, вызовы нижних драйверов в стеке.) Вызов (точнее, макроопределение) MmGetSystemAddгessForMdl являeтcя устаревшим, и его следует использовать только в WDM драйверах, предназначенных для работы еще и в Windows 98. Использование макроопределения MmGetSystemAddressForMdlSafe является предпочтительным. Оба эти макроопределения могут быть вызваны из кода, работающего на уровне IRQL не выше DISPATH_LEVEL.

Вызов MmUnlockPages отменяет фиксацию страниц в оперативной памяти, а вызов IoFreeMdl уничтожает структуру MDL списка.

Чтобы отследить ошибки, связанные с фиксацией блока виртуальной памяти в памяти оперативной, рекомендуется выполнять вызов MmProbeAndLockPages внутри try-except блока, например:

__try { MmProbeAndLockPages( pMdl, UserMode, IoModifyAccess); }
__except( EXCEPTION_EXECUTE_HANDLER)
{
    pIrp->IoStatus.Status = STATUS_ACCESS_VIOLATION;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_CONFLICTING_ADDRESSES;
}

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

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

Вызовы для выделения и освобождения областей виртуальной памяти

Для манипуляций с областями памяти (выделение и освобождение) в режиме ядра используются специальные системные вызовы, отличающиеся от API вызовов режима ядра: ExAllocatePool, ExAllocatePoolWithTag, ExFreePool. В режиме ядра возможно выделение и освобождение области физически непрерывной памяти, что выполняется при помощи вызовов MmAllocateContiguousMemory и, соответственно, MmFreeContiguousMemory. Эти вызовы рассмотрены ниже.

Таблица 7.2. Прототип вызова ExAllocatePool

PVOID ExAllocatePool | IRQL < DISPATCH_LEVEL
Параметры | Выполняет выделение области памяти
IN POOL_TYPE PoolType | Тип виртуальной памяти, в которой следует выделять область. Наиболее употребительны значения: • PagedPool — страничная • NonPagedPool — нестраничная, тогда данную функцию можно вызывать при любом уровне IRQL
IN ULONG NumberOfBytes | Размер запрашиваемой области
Возвращаемое значение | Указатель на выделенную область либо NULL (в случае, если память выделить невозможно). Выделенная область всегда выравнивается на 8 байт. В случае, если запрашиваемый размер превышает PAGE_SIZE, то выделяемая область выравнивается на размер страницы.

Таблица 7.3. Прототип вызова ExAllocatePoolWithTag

PVOID ExAllocatePoolWithTag | IRQL < DISPATCH_LEVEL
Параметры | Выполняет выделение области памяти
IN POOL_TYPE PoolType | См. описание ExAllocatePool выше.
IN ULONG NumberOfBytes | Размер запрашиваемой области
IN ULONG Tag | Метка (тег) для данной области, можно задавать как 4 символа, например, 'ABCD'. Удобно для отладки.
Возвращаемое значение | См. описание ExAllocatePool выше.

Таблица 7.4. Прототип вызова ExFreePool

VOID ExFreePool | IRQL < DISPATCH_LEVEL
Параметры | Выполняет освобождение области памяти
IN PVOID pBuffer | Указатель на освобождаемую область памяти, выделенную вызовами ExAllocatePool или ExAllocatePoolWithTag. Если освобождается нестраничная память, вызов может быть сделан из кода на уровне DISPATCH_LEVEL IRQL
Возвращаемое значение | void

Таблица 7.5. Прототип вызова MmAllocateContiguousMemory

PVOID MmAllocateContiguousMemory | IRQL == PASSIVE_LEVEL
Параметры | Выполняет выделение физически непрерывной области памяти
IN ULONG NumberOfBytes | Размер запрашиваемой области
IN PHYSICAL_ADDRESS maxAcceptableAddress | Верхний предел адресов для запрашиваемой области. Поле HighPart=0, поле LowPart принимает значения, например: • 0x000FFFFF (до 1 МБ) • 0x00FFFFFF (до 16 МБ) • 0xFFFFFFFF(до 4ГБ)
Возвращаемое значение | Виртуальный адрес или NULL (при неудаче). (Для повышения вероятности успешного завершения рекомендуется выполнять вызов в DriverEntry, поскольку память при работе системы быстро становится сильно дефрагментированной)

Таблица 7.6. Прототип вызова MmFreeContiguousMemory

VOID MmFreeContiguousMemory | IRQL == PASSIVE_LEVEL
Параметры | Выполняет освобождение области памяти
IN PVOID pBuffer | Указатель на область памяти, выделенную ранее с использованием системного вызова MmAllocateContiguousMemory
Возвращаемое значение | void

Таблица 7.7. Прототип вызова MmIsAddressValid

BOOLEAN MmlsAddressValid | IRQL <= DISPATCH_LEVEL
Параметры | Выполняет проверку виртуального адреса
IN PVOID VirtualAddress | Виртуальный адрес, который следует проверить
Возвращаемое значение | TRUE — если присутствует в оперативной памяти FALSE — если вызовет прерывание PAGE FAULT Замечание. Если адрес не находится в нестраничной памяти (или не зафиксирован в оперативной памяти), возврат TRUE не гарантирует отсутствие проблем при работе на повышенных уровнях IRQL (поскольку к моменту использования данного виртуального адреса ситуация может измениться).

Таблица 7.8. Прототип вызова MmGetPhysicalAddress

PHYSICAL_ADDRESS MmGetPhysicalAddress | IRQL — любой
Параметры | Определяет физический адрес, соответствующий данному виртуальному
IN PVOID VirtualAddress | Анализируемый виртуальный адрес
Возвращаемое значение | Физический адрес. Перед данным вызовом следует воспользоваться MmlsAddressValid

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

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

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

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

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

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

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

VOID ExInitializeNPagedLookasideList | IRQL <= DISPATCH_LEVEL
Параметры | Создание ассоциативного списка блоков нестраничной памяти
IN PNPAGED_LOOKASIDE_LIST pLookasideListHeader | Указатель на предварительно выделенную драйвером область размером sizeof(NPAGED_LOOKASIDE_LIST)
IN OPTIONAL PALLOCATE_FUNCTION pAllocFunction | NULL или указатель на предоставляемую драйвером функцию, которая будет заниматься выделением блоков из массива системной нестраничной памяти (если NULL — будет использован системный вызов ExAllocatePoolWithTag)
IN OPTIONAL PFREE_FUNCTION pFreeFunction | NULL или указатель на предоставляемую драйвером функцию, которая будет заниматься освобождением блоков (если NULL — будет использован вызов ExFreePool)
IN ULONG Flags | Зарезервировано. Указывать 0
IN ULONG ByteSize | Размер отдельных блоков, поддерживаемых данным списком
IN ULONG Tag | Метка (тег) для создаваемых блоков, можно задавать как 4 символа, например, 'ABCD'
IN USHORT Depth | Зарезервировано. Указывать 0
Возвращаемое значение | void

Перед вызовом процедуры инициализации, для хранения заголовка ассоциативного списка драйвер должен получить область в нестраничной памяти размером sizeof(PAGED_LOOKASIDE_LIST) или sizeof(NPAGED_LOOKASIDE_LIST) — в зависимости от того, какой список инициализируется. Для этих целей можно использовать описанные выше системные вызовы ExAllocatePool или ExAllocatePoolWithTag. В конце работы со списком обязательно следует выполнить вызов ExDelete(N)PagedLookasideList.

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

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

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

PVOID MyFreeFunction (PVOID pBuffer);

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

PVOID ExAllocateFromNPagedLookasideList | IRQL <= DISPATCH_LEVEL
Параметры | Выполняет выделение блока памяти из нестраничного списка
IN PNPAGED_LOOKASIDE_LIST pLookasideList | Указатель на инициализированный ассоциативный список
Возвращаемое значение | Указатель на блок фиксированного размера или NULL (если функция выделения памяти не смогла получить очередной блок)

Таблица 7.12. Прототип вызова ExAllocateFromPagedLookasideList

PVOID ExAllocateFromPagedLookasideList | IRQL < DISPATCH_LEVEL
Параметры | Выполняет выделение блока памяти из страничного списка
IN PPAGED_LOOKASIDE_LIST pLookasideList | Указатель на инициализированный ассоциативный список
Возвращаемое значение | Указатель на блок фиксированного размера или NULL (если функция выделения памяти не смогла получить очередной блок)

Таблица 7.13. Прототип вызова ExFreeToNPagedLookasideList

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

Таблица 7.14. Прототип вызова ExFreeToPagedLookasideList

VOID ExFreeToPagedLookasideList | IRQL < DISPATCH_LEVEL
Параметры | Возвращает блок в страничный ассоциативный список
IN PPAGED_LOOKASIDE_LIST pLookasideList | Указатель на инициализированный ассоциативный список
IN PVOID pEntry | Указатель на ранее полученный из списка блок фиксированного размера
Возвращаемое значение | void

Таблица 7.15. Прототип вызова ExDeleteNPagedLookasideList

VOID ExDeleteNPagedLookasideList | IRQL <= DISPATCH_LEVEL
Параметры | Выполняет удаление нестраничного ассоциативного списка
IN PNPAGED_LOOKASIDE_LIST pLookasideList | Указатель на ассоциативный список
Возвращаемое значение | void

Таблица 7.16. Прототип вызова ExDeletePagedLookasideList

VOID ExDeletePagedLookasideList | IRQL < DISPATCH_LEVEL
Параметры | Выполняет удаление страничного ассоциативного списка
IN PPAGED_LOOKASIDE_LIST pLookasideList | Указатель на ассоциативный список
Возвращаемое значение | void

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

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

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

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

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

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

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

Таблица 7.19. Прототип вызова IoAllocateMdl

PVOID IoAllocateMdl | IRQL <= DISPATCH_LEVEL
Параметры | Создает MDL список и инициализирует его. При этом можно выполнить его привязку к нужному IRP пакету
IN PVOID VAddress | Виртуальный адрес области, для которой будет построен MDL список
IN ULONG Length | Размер виртуальной области, для которой следует построить MDL
IN BOOLEAN SecondaryBuffer | Если pIrp равен NULL, см. ниже, то и параметр SecondaryBuffer должен быть равен NULL. Если параметр pIrp не равен NULL, тогда при: FALSE — указатель на созданную структуру MDL списка будет занесен в поле pIrp->MdlAddress TRUE — поле Next созданной MDL структуры будет указывать на MDL список, который прописан в поле pIrp->MdlAddress
IN BOOLEAN ChargeQuota | Если TRUE — вычесть из квоты текущего программного потока на создание MDL списков. Обычно применяется FALSE
IN OUT PIRP pIrp | NULL — не работаем с каким-либо IRP пакетом. Иначе — см. описание SecondaryBuffer выше
Возвращаемое значение | Указатель на MDL список. Замечание. Инициализацию MDL списка можно считать завершенной только после вызова MmProbeAndLockPages, см. ниже

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

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

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

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

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

VOID IoFreeMdl | IRQL <= DISPATCH_LEVEL
Параметры | Выполняет очистку MDL списка
IN PMDL pMdl | Указатель на структуру, описывающую MDL список
Возвращаемое значение | void

Таблица 7.22. Прототип вызова MmProbeAndLockPages

VOID MmProbeAndLockPages | IRQL < DISPATCH_LEVEL
Параметры | Выполняет фиксацию страниц, описанных в MDL списке, в физической памяти
IN OUT PMDL pMdl | Указатель на MDL список
IN KPROCESSOR_MODE AccessMode | Режим, в котором будет проверяться доступ к анализируемым страницам: KernelMode UserMode
IN LOCK_OPERATION Operation | Права доступа после того, как страницы будут зафиксированы. Одно из значений: IoReadAccess IoWriteAccess IoModifyAccess
Возвращаемое значение | void Замечание. Часть MDL списка, описывающая физические страницы, после данного вызова, скорее всего, будет обновлена.

Таблица 7.23. Прототип вызова MmUnlockPages

VOID MmUnlockPages | IRQL <= DISPATCH_LEVEL
Параметры | Отменяет фиксацию страниц страничной памяти в оперативной памяти
IN PMDL pMdl | Указатель на структуру, описывающую MDL список, соответствующий набору зафиксированных в оперативной памяти страниц
Возвращаемое значение | void

После того как выполнена фиксация страниц в физической памяти, описываемых MDL списком (то есть описываемая область стала практически частью нестранично организованной памяти), имеет смысл поинтересоваться: какой же виртуальный адрес теперь у этой области памяти в терминах системного адресного пространства (адреса — более 0x8000000)? На этот вопрос может ответить системный вызов (макроопределение) MmGetSystemAddressForMdl, описываемый ниже.

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

PVOID MmGetSystemAddressForMdl | IRQL <= DISPATCH_LEVEL
Параметры | Показывает системный виртуальный адрес начала буфера, описываемого MDL списком
IN PMDL pMdl | Указатель на структуру MDL списка
Возвращаемое значение | Адрес диапазона системных адресов

Непосредственный доступ к полям структуры MDL списка не приветствуется разработчиком Windows — фирмой Microsoft. (Хотя эта структура детально описана в заголовочных файлах wdm.h и ntddk.h, представленных в пакете DDK). Вместо этого предлагается использовать следующие функции.

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

ULONG MmGetMdlByteCount | IRQL <= DISPATCH_LEVEL
Параметры | Показывает размер буфера, описываемого MDL списком
IN PMDL pMdl | Указатель на структуру MDL списка
Возвращаемое значение | Размер области, описываемой MDL списком

Таблица 7.26. Прототип вызова MmGetMdlByteOffset

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

Таблица 7.27. Прототип вызова MmGetMdlVirtualAddress

PVOID MmGetMdlVirtualAddress | IRQL — любой
Параметры | Показывает виртуальный адрес начала буфера, описываемого MDL списком
IN PMDL pMdl | Указатель на структуру MDL списка
Возвращаемое значение | Исходный виртуальный адрес описываемой данным MDL списком области памяти. Замечание. В том случае, если область была предоставлена пользовательским приложением, здесь можно увидеть виртуальный адрес из диапазона пользовательского адресного пространства (для интерпретации которого требуется контекст этого приложения).

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

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

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

Функции библиотеки времени выполнения для работы с памятью

Операционная система Windows предоставляет набор Rtl (библиотека времени выполнения) функций для работы с памятью, которые в режиме ядра заменяют столь привычные программистам пользовательских приложений вызовы memcpy, memset и т.п. Некоторые наиболее употребительные вызовы описаны ниже.

Таблица 7.29. Прототип вызова RtlFillMemory

VOID RtlFillMemory | IRQL — любой (если это допускает тип памяти заполняемого буфера)
Параметрам | Заполняет область памяти значением Pattern
IN VOID UNALIGNED *Destination | Указатель на буфер-приемник (область без выравнивания)
IN ULONG Length | Размер заполняемой области в байтах
IN UCHAR Pattern | Значение, которым будет заполнена указанная область (байт)
Возвращаемое значение | void

Таблица 7.30. Прототип вызова RtlZeroMemory

VOID RtlZeroMemory | IRQL — любой (если это допускает тип памяти обнуляемого буфера)
Параметры | Обнуляет область памяти
IN VOID UNALIGNED *Destination | Указатель на буфер-приемник (область без выравнивания)
IN ULONG Length | Размер обнуляемой области в байтах
Возвращаемое значение | void

Таблица 7.31. Прототип вызова RtlCopyMemory

VOID RtlCopyMemory | IRQL — любой (если это допускают типы памяти копируемых буферов)
Параметры | Копирует содержимое одного буфера в другой
IN VOID UNALIGNED *Destination | Указатель на буфер-приемник (область без выравнивания)
IN CONST VOID UNALIGNED *Source | Указатель на буфер-источник (область без выравнивания)
IN ULONG Length | Размер копируемой области в байтах
Возвращаемое значение | void Замечание. Области источника и приемника не должны перекрываться.

Таблица 7.32. Прототип вызова RtlMoveMemory

VOID RtlMoveMemory | IRQL — любой (если это допускают типы памяти копируемых буферов)
Параметры | Копирует содержимое одного буфера в другой
IN VOID UNALIGNED *Destination | Указатель на буфер-приемник (область без выравнивания)
IN CONST VOID UNALIGNED *Source | Указатель на буфер-источник (область без выравнивания)
IN ULONG Length | Размер копируемой области в байтах
Возвращаемое значение | void Замечание. Допускается перекрытие областей источника и приемника.

Существует также вызов RtlCopyBytes, совершенно идентичный приведенному выше вызову RtlMoveMemory.


Глава 7. Приемы программирования в режиме ядра

Управление размещением кода драйвера в памяти

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

Определение размещения при компиляции

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

#pragma code_seg("INIT")
<программный текст>
#pragma code_seg()

#pragma code_seg("PAGE")
<программный текст>
#pragma code_seg()

Здесь первая строка вводит программный код категории INIT. Этот код, подобно сгоревшей ступени ракеты, растворится в небытии сразу по окончании инициализации драйвера (работы драйверной процедуры DriverEntry). Традиции такого кода восходят еще ко временам операционной системы DOS, когда малый размер драйвера был его важнейшим достоинством.

Директива '#pragma code_seg()' восстанавливает правила по умолчанию.

Директива '#pragma code_seg("PAGE")' обеспечивает размещение кода в областях странично организованной памяти.

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

Аналогичным образом директивы компилятора применимы и к сегментам (секциям) данных, см. два примера ниже.

#pragma data_seg("INIT")
<описание переменных>
#pragma data_seg()

#pragma data_seg("PAGE")
<описание переменных>
#pragma data_seg()

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

#ifdef ALLOC_PRAGMA
#pragma alloc_text( "INIT", DriverEntry )
#pragma alloc_text( "PAGE", MyUnloadProcedure )
#endif

В приведенном выше фрагменте процедура DriverEntry будет отнесена к категории INIT, а процедура MyUnloadProcedure будет размещена в станичной памяти.

Динамическое перемещение кода драйвера в страничную память

Наконец, третий способ перемещения кода драйвера в страничную память является динамическим и происходит под управлением самого драйвера. Весь программный код драйвера, обычно размещенный в области кода операционной системы (начинающейся с адреса 0x80000000), можно пометить как "страничный" и переместить в странично организованную память системным вызовом MmPageEntireDriver, которому в качестве параметра передается любой адрес или указатель на любую процедуру в составе кода драйвера, например, AddDevice (если эта процедура присутствует). Обратное действие выполняет вызов MmResetDriverPaging, который восстанавливает статус всех секций кода драйвера, данный им при компиляции, и все секции, которые были созданы как нестраничные, фиксируются в оперативной памяти.

Вызовы MmPageEntireDriver и MmResetDriverPaging следует выполнять только в коде, работающем на уровне IRQL равном PASSIVE_LEVEL.

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

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

Соответственно, если пользователь прекращает работу с драйвером вызовом CloseHandle (в результате чего вызывается драйверная процедура, обслуживающая запрос IRP_MJ_CLOSE), то драйвер может выполнить перевод всего своего кода в область страничной памяти, после чего он, возможно, окажется сброшенным на диск. Разумеется, имеет смысл проверить предварительно, сколько раз был получен доступ к драйверу (возможно, имеются еще пользовательские приложения, имеющие открытый дескриптор). Если драйвер подключен к прерыванию, следует отключиться от него. Обратные действия (восстановления прежнего состояния драйверных процедур) можно проделать в процедуре, обслуживающей IRP_MJ_CREATE, то есть когда пользовательское приложение пытается получить доступ к драйверу и получить соответствующий дескриптор вызовом пользовательского режима CreateFile.

Контроль за количеством обращений к драйверу можно вести при помощи системных вызовов InterlockedIncrement и InterlockedDecrement, обеспечивающих безопасное обращение к счетчику, который можно разместить, например, в расширении структуры устройства (device extension), состав которой полностью зависит от воли разработчика драйвера.

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

Фиксация страничных секций кода и данных в оперативной памяти

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

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

Применение вызовов MmLockPagableDataSection/MmLockPagableCodeSection повторно к одной и той же области является расточительным приемом. Поэтому, если драйвер вынужден использовать эти вызовы повторно, то следует использовать функцию MmLockPagableSectionByHandle, используя первоначально полученный дескриптор. В противном случае, рекомендуется вести учет вызовов функций MmLockPagableDataSection/MmLockPagableCodeSection, чтобы не выполнять лишней работы.

Для того чтобы программно контролировать отдельный набор данных, можно поместить эти данные автономно, как показано ниже:

#pragma data_seg("MY_DATA")
<описание переменных>
#pragma data_seg()

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

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

Все упомянутые выше вызовы следует выполнять только в коде, работающем на уровне IRQL равном PASSIVE_LEVEL.

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

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

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

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

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


Глава 7. Приемы программирования в режиме ядра

Операции над строками UNICODE_STRING

Операционная система Windows NT издавна ориентирована на использование так называемых "широких" символов (занимающих два байта), что, в отличие от символов ASCII, o которых будет сказано ниже, без особых затруднений обеспечивает поддержку всех типов алфавитов, включая поддержку языков Юго-Восточной Азии.

Собственно тип данных UNICODE_STRING описывается в пакете DDK следующим образом (см. заголовочный файл ntdef.h):

typedef struct _UNICODE_STRING {
    USHORT Length;        // Длина строки (в двухбайтных символах)
    USHORT MaximumLength; // Максимально возможная длина строки
    PWSTR Buffer;         // указатель на буфер с двухбайтными символами
} UNICODE_STRING, *PUNICODE_STRING;

Иногда UNICODE_STRING определяется выражением "counted string", что довольно точно передает сущность этого типа данных, то есть — "счетная строка", строка, где поддерживается учет действующих символов.

Нетрудно догадаться, что у программистов, долгое время привыкавших к простым ASCII кодировкам и столь же несложным функциям, оперирующим ASCIIZ строками, переход к использованию кодировки Юникод не вызовет энтузиазма.

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

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

Прежде всего, простая буква 'L', примененная перед строкой символов, дает указание препроцессору, трактовать эту строку как строку "широких" символов (строку WCHAR, но еще — не UNICODE_STRING). Соответственно, перейти от обычного текста, набираемого на клавиатуре компьютера, к строке UNICODE_STRING можно так:

UNICODE_STRING myNewUString;
RtlInitUnicodeString( &myNewUString, L"My Unicode text example." );

В следующем примере посредником при инициализации UNICODE_STRING выступает тип данных ANSI_STRING (счетная строка однобайтных символов):

ANSI_STRING     myNewANSIString;
UNICODE_STRING  myNewUString;
RtlInitAnsiString( &myNewUString, "My second text example." );
RtlAnsiStringToUnicodeString(&myNewUString, &myNewANSIString, TRUE);

Третий параметр, указанный как TRUE, заставляет данную функцию выделять память под буфер двухбайтных символов. Соответственно, по окончании работы с UNICODE_STRING (а в данном случае — при выходе из текущей функции, поскольку myNewUString определена как локальная переменная) следует выполнить освобождение буфера двухбайтных символов вызовом RtlFreeUnicodeString. To же необходимо проделать и в первом примере. Более того, аналогичное требование и к типу данных ANSI_STRING, для которого следует использовать RtlFreeAnsiString.

Сам тип ANSI_STRING определяется следующим образом (см. ntdef.h):

typedef struct _STRING {
    USHORT Length;
    USHORT MaximumLength;
    PCHAR Buffer;    // Здесь 'PCHAR' - просто 'char' указатель
} STRING, *PSTRING;
typedef STRING ANSI_STRING;

Очевидно, второй способ более трудоемкий, и им редко кто пользуется.

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

Таблица 7.33. Прототип вызова RtlAppendUnicodeStringToString

NTSTATUS RtlAppendUnicodeStringToString | IRQL — любой (если это допускает тип памяти буферов двухбайтных символов)
Параметры | Объединяет строки UNICODE_STRING
IN OUT PUNICODE_STRING Destination | Указатель на строку-получатель
IN OUT PUNICODE_STRING AppendString | Указатель на присоединяемую строку
Возвращаемое значение | STATUS_SUCCESS — строка присоединена и длина строки получателя обновлена STATUS_BUFFER_TOO_SMALL — слишком мал размер буфера двухбайтных символов строки-получателя

При анализе приведенного выше описания возникает правомерный вопрос: что предпринимать, если объединение строк завершилось неудачей по причине недостаточно большого буфера принимающей строки UNICODE_STRING? Достойного ответа в пакете DDK на этот вопрос просто нет. Если требуемый размер буфера не составит труда вычислить как

(Destination->Length + AppendString->Length) * sizeof(WCHAR)

то расширение буфера строки-получателя — задача несколько более сложная. Хотя логично было бы иметь соответствующую системную функцию, однако в документации DDK o подобном вызове нет никакого упоминания.

Таблица 7.34. Прототип вызова RtlCompareUnicodeString

LONG RtlCompareUnicodeString | IRQL == PASSIVE_LEVEL
Параметры | Выполняет сравнение строк UNICODE_STRING
IN PUNICODE_STRING pString1 | Указатель на первую строку
IN PUNICODE_STRING pString2 | Указатель на вторую строку
BOOLEAN CaseInSensitive | TRUE — игнорировать регистр (верхний/нижний)
Возвращаемое значение | 0 — строки идентичны < 0 — строка 1 меньше строки 2

Таблица 7.35. Прототип вызова RtlEqualUnicodeString

BOOLEAN RtlEqualUnicodeString | IRQL == PASSIVE_LEVEL
Параметры | Выполняет сравнение строк UNICODE_STRING
IN PUNICODE_STRING pString1 | Указатель на первую строку
IN PUNICODE_STRING pString2 | Указатель на вторую строку
BOOLEAN CaseInSensitive | TRUE — игнорировать регистр (верхний/нижний)
Возвращаемое значение | TRUE — строки идентичны FALSE — строки различаются

Таблица 7.36. Прототип вызова RtlInt64ToUnicodeString

NTSTATUS RtlInt64ToUnicodeString | IRQL == PASSIVE_LEVEL
Параметры | Преобразует число int64 в UNICODE_STRING
IN ULONGLONG Value | Исходное число
IN ULONG Base | Формат: 16 — шестнадцатеричный, 8 — октавный, 2 — двоичный, 0 или 10 — десятичный.
IN OUT PUNICODE_STRING s | Строка UNICODE_STRING
Возвращаемое значение | STATUS_SUCCESS STATUS_BUFFER_OVERFLOW — слишком мал размер буфера UNICODE_STRING STATUS_INVALID_PARAMETER — ошибочен параметр Base

Аналогичный прототип вызова имеют также функции преобразования целых чисел без знака и указателей в строку UNICODE_STRING — RtlIntegerToUnicodeString и RtlIntPtrToUnicodeString, соответственно.

Таблица 7.37. Прототип вызова RtlUpcaseUnicodeString

NTSTATUS RtlUpcaseUnicodeString | IRQL == PASSIVE_LEVEL
Параметры | Преобразует все символы строки src в символы верхнего регистра
IN OUT OPTIONAL PUNICODE_STRING dest | Указатель на строку с буфером, подготовленным для приема преобразованной строки или NULL (в последнем случае преобразование происходит по месту)
IN OUT PUNICODE_STRING src | Исходная строка
IN BOOLEAN AllocateDstStringBuff | Если TRUE — выделить буфер под результат преобразования (в этом случае его следует освободить вызовом RtlFreeUnicodeString по окончании работы с этой строкой)
Возвращаемое значение | STATUS_SUCCESS STATUS_BUFFER_OVERFLOW — слишком мал размер буфера UNICODE_STRING STATUS_INVALID_PARAMETER — ошибочен параметр Base

Операции над строками ANSI символов

Работа со строками символов ANSI (8-битными) более привычна для программистов. Такие средства тоже присутствуют в наборе встроенных функций пакета и в наборе системных вызовов RtlXxx.

Строками простых 8-битных символов, в которых окончание обозначено нулем (так называемые строки SZ, String, Zero end), можно манипулировать при помощи уцелевших встроенных функций strlen, memcpy, strcpy, strncpy, strcat и даже strstr. Набор функций, которыми можно попытаться воспользоваться, приведен в файле DDK XP в заголовочном файле inc\crt\string.h, хотя в официальной документации DDK они не упоминаются вовсе, так что к ним следует применять правила использования по-умолчанию. Например, не обращаться к строкам, размещенным в страничной памяти, на высоких уровнях IRQL и т.п. Другие функции, например, мощную sprintf или хотя бы atoi, компилятор DDK не предоставляет.

Вариант счетной строки обычных 8-битных символов вводится в пакете DDK как тип данных ANSI_STRING. Это аналогичная UNICODE_STRING структура данных, в которой собственно строка хранится по адресу, указанному в поле Buffer, как уже было показано выше. Длину строки можно взять из поля Length. Хотя в большинстве случаев вполне успешно проходит обращение со строкой по адресу Buffer, как если бы она была строкой SZ (заканчивающейся нулем), однако, строго говоря, надеяться на это не следует, поскольку документация DDK не указывает, что на это можно вполне рассчитывать.

Можно ли перейти от строки UNICODE_STRING к обычной строке 8-битных символов? Можно, но при этом произойдет потеря информации о языковой локализации. Ниже показан несложный способ, как перейти к счетной строке ANSI, а затем и к обычной строке SZ.

// Полагаем, что уже существует счетная строка UNICODE_STRING
// по адресу ptrToUNICODE_STRING
ANSI_STRING ansiStr; // стековая переменная (не инициализирована)
char        str[100];// стековая переменная

NTSTATUS stat =      // инициализируем строку ANSI
RtlUnicodeStringToAnsiString( &ansiStr, ptrToUNICODE_STRING, TRUE );

if(NT_SUCCESS(stat))
{
// Теперь поле ansiString.Buffer указывает на выделенный буфер,
// куда помещена строка ANSI символов, полученная из строки
// UNICODE_STRING, изначально находившейся по адресу, куда
// указывает переменная ptrToUNICODE_STRING.
// Поле ansiStr.Length уже содержит информацию о длине строки.
    int len= (ansiStr.Length > 99? 99: ansiStr.Length);
    strncpy(str, ansiStr.Buffer, len);
    str[len+1]=0;

    DbgPrint("SZ string = %s(len=%d).", str, len);

    // Освобождаем буфер, выделенный под хранение ANSI строки:
    RtlFreeAnsiString(&ansiStr);
}

Заметим, что переменная str будет хранить строку 8-битных символов до выхода из данного программного модуля, поскольку str — локальная переменная.

Прототип вызова функции RtlUnicodeStringToAnsiString описан в таблице ниже. В том случае, если третий параметр указан как TRUE, то будет выполнено выделение памяти для хранения строки, полученной из строки UNICODE_STRING. Соответственно, по окончании использования строки ANSI_STRING следует освободить числящуюся за драйвером область памяти, что выполняется вызовом RtlFreeAnsiString.

Таблица 7.38. Прототип вызова RtlUnicodeStringToAnsiString

NTSTATUS RtlUnicodeStringToAnsiString | IRQL == PASSIVE_LEVEL
Параметры | Преобразует информацию строки UNICODE_STRING в строку ANSI_STRING
IN OUT PANSI_STRING ansiStringPtr | Указатель на строку ANSI_STRING, которая должна быть получена в результате вызова
IN PUNICODE_STRING Source | Указатель на исходную строку UNICODE_STRING
BOOLEAN AllocFlag | TRUE — выделить память под буфер строки ANSI_STRING
Возвращаемое значение | STATUS_SUCCESS или код ошибки

Работа с текстом в драйверах случается нечасто, но, тем не менее, следует быть к ней готовым. Например, при работе с файлами.


Глава 7. Приемы программирования в режиме ядра

Функции для работы с файлами

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

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

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

Поскольку вызов ZwCreateFile имеет большое разнообразие флаговых значений практически для каждого из входных параметров, ограничимся кратким общим обзором, представленным в таблице ниже. Более полную информацию обо всех возможных значения параметров вызова ZwCreateFile следует получить из документации пакета DDK. (Джозеф Ньюкамер уделил в своей книге описанию этой функции 7 страниц!)

Таблица 7.39. Прототип вызова ZwCreateFile

NTSTATUS ZwCreateFile | IRQL == PASSIVE_LEVEL
Параметры | Предоставляет доступ к системным ресурсам (в том числе файлам) в режиме ядра
OUT PHANDLE pHandle | Указатель на переменную, куда следует поместить дескриптор открытого объекта (файла, подраздела реестра и т.п.)
IN ACCESS_MASK DesiredAccess | Характеристика доступа к объекту. Для файлов вполне приемлемы значения GENERIC_READ или GENERIC_WRITE, которые представляют сложные комбинации из более простых масок доступа (типа FILE_APPEND_DATA и т.п.)
IN POBJECT_ATTRIBUTES pObjAttributes | Указатель на заполненную вызывающим кодом структуру данных, которая описывает имя, местоположение и некоторые другие характеристики открываемого объекта (см. ниже)
OUT PIO_STATUS_BLOCK pIOStatus | Указатель на буфер, в котором будет размещена информация об открытом объекте в формате структуры IO_STATUS_BLOCK
IN PLARGE_INTEGER AllocationSize OPTIONAL | Начальный размер файла в байтах. Ненулевое значение принимается во внимание только при создании и перезаписи файла
IN ULONG FileAttributes | Атрибуты открываемого файла. Типовым является значение FILE_ATTRIBUTE_NORMAL
IN ULONG SharedAccessFlags | Описывает, разрешен ли совместный доступ, например, FILE_SHARE_READ — для чтения
IN ULONG CreateDispositionFlags | Способ открытия файла, например, FILE_OPEN_IF — если не существует, создать
IN ULONG CreateOptions | Комбинация флагов создания, например, FILE_SYNCHRONOUS_IO_NONALERT — все операции над файлом выполняются как синхронные (DesiredAccess должен включать флаг SYNCHRONIZE)
IN PVOID EaBuffer OPTIONAL | Для драйверов устройств и драйверов средних слоев следует указывать NULL
IN ULONG EaLength | Для драйверов устройств и драйверов средних слоев следует указывать 0
Возвращаемое значение | STATUS_SUCCESS или код ошибки (несколько более подробную информацию можно найти в структуре IO_STATUS_BLOCK)

Поле pIOStatus->Information (структуры IO_STATUS_BLOCK) может после вызова иметь одно из следующих значений: FILE_CREATED, FILE_OPENED, FILE_OVERWRITTEN, FILE_SUPERSEDED, FILE_EXISTS или FILE_DOES_NOT_EXIST.

Ниже приводится пример создания (или открытия — при повторных обращениях) файла C:\Example\testfile.txt и запись в него строки " string : test write!\"

NTSTATUS          status;
UNICODE_STRING    fullFileName;
HANDLE            fileHandle;
IO_STATUS_BLOCK   iostatus;
OBJECT_ATTRIBUTES oa;

RtlInitUnicodeString( &fullFileName,
                      L"\\??\\C:\\Example\\testfile.txt");

InitializeObjectAttributes( &oa,
                            &fullFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
                            NULL,
                            NULL );

status = ZwCreateFile ( &fileHandle,
                        GENERIC_WRITE | SYNCHRONIZE,
                        &oa,
                        &iostatus,
                        0,  // alloc size = none
                        FILE_ATTRIBUTE_NORMAL,
                        FILE_SHARE_WRITE,
                        FILE_OPEN_IF,
                        FILE_SYNCHRONOUS_IO_NONALERT,
                        NULL,
                        0);
// Здесь:
// GENERIC_WRITE равно STANDARD(0x40000000L)
//
// FILE_GENERIC_WRITE равно STANDARD_RIGHTS_WRITE|FILE_WRITE_DATA |
//    FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA |
//    SYNCHRONIZE, что можно увидеть в заголовочном файле winnt.h

if( NT_SUCCESS(status))
{
    // Строка для записи в файл
    char myString[100]="string : test write!\r\n";
    // Структура, которая поможет определить длину файла:
    FILE_STANDARD_INFORMATION fileInfo;

    status =        // Получаем информацию о файле
         ZwQueryInformationFile( fileHandle,
                               &iostatus,
                               &fileInfo,
                               sizeof(FILE_STANDARD_INFORMATION),
                               FileStandardInformation
                               );
    ULONG len = strlen(myString);
    if( NT_SUCCESS(status) )
    {
        LARGE_INTEGER ByteOffset = fileInfo.EndOfFile;
        status = ZwWriteFile(fileHandle,
                            NULL,
                            NULL,
                            NULL,
                            &iostatus,
                            myString, len,   // Записываемая строка
                            &ByteOffset,     // a если NULL? см. ниже
                            NULL);
        if( !NT_SUCCESS(status) || iostatus.Information != len )
        {
            DbgPrint("Error on writing. Status = %x.", status);
        }
    }
    ZwClose(fileHandle);
break;

}

Следует заметить, что без магической комбинации "\\??\\" в начале имени файла, вызов ZwCreateFile непременно возвратит ошибку. Этот префикс является обязательным для файлов на диске.

Таблица 7.40. Прототип вызова ZwWriteFile

NTSTATUS ZwWriteFile | IRQL == PASSIVE_LEVEL
Параметры | Производит модификацию объекта (файла), указанного открытым дескриптором
IN HANDLE FileHandle | Дескриптор открытого для модификации файлового объекта
IN HANDLE Event OPTIONAL | Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
IN PIO_APC_ROUTINE pApcRoutine OPTIONAL | Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
IN PVOID ApcContext OPTIONAL | Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
OUT PIO_STATUS_BLOCK pIoStatusBlock | В поле pIoStatusBlock->Information по завершении вызова находится число реально записанных байт
IN PVOID Buffer | Буфер с данными для записи
IN ULONG Length | Размер записываемой порции данных
IN PLARGE_INTEGER pByteOffset OPTIONAL | Указатель на переменную, где содержится смещение в файле (от начала), по которому следует производить запись данных
IN PULONG Key OPTIONAL | Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
Возвращаемое значение | STATUS_SUCCESS или код ошибки

При помощи ZwCreateFile можно получать и доступ к драйверам, как это делается из вызова CreateFile пользовательского режима. Таким образом, в частности, предлагается получать доступ к драйверу отладочной печати DebugPrint. Разумеется, вид имени для использования будет несколько другим. Для драйвера DebugPrint это будет имя L"\\Device\\PHDDebugPrint". По данному имени через ZwCreateFile можно получить доступ к драйверу из другого драйвера, несмотря на то, что символьная ссылка этим драйвером не создается. Аналогично, к нашему драйверу Example можно получить доступ из другого драйвера по имени L"\\Device\\Example", но ничего не получится, если пытаться использовать имя L"\\\\.\\Example", как в вызове CreateFile.

Рассмотрим подробнее некоторые функции, использованные в приведенном выше примере.

Макроопределение InitializeObjectAttributes используется для заполнения полей структуры OBJECT_ATTRIBUTES, что делает эту операцию компактнее. Это макроопределение вводится в заголовочном файле ntdef.h пакета DDK. Там же описана и внутренняя организация OBJECT_ATTRIBUTES.

Собственно запись в файл в приведенном выше примере выполняется системной функцией ZwWriteFile.

Способ вызова ZwReadFile во многом повторяет прототип для ZwWriteFile, приведенный в таблице 7.40.

Если параметр pByteOffset указан равным NULL, то в большинстве случаев это воспринимается как нулевое смещение, и запись была бы выполнена (скажем, в приведенном примере) с начала файла. Однако на это допущение лучше не полагаться. Например, если бы мы пытались произвести запись в упомянутое выше устройство PHDDebugPrint, то нас преследовали бы ошибки, пока значение *pByteOffset не было бы задано явно, то есть:

pByteOffset->QuadPart = 0i64;   // 0 для LARGE_INTEGER.

 Упоминания в документации DDK XР, по поводу того, что при наличии в параметре DesiredAccess (при вызове ZwCreateFile для получения доступа к файлу) флага FILE_APPEND_DATA запись всегда производится в конец файла, мягко говоря, не совсем справедливы. При наличии этого флага (среди параметров вызова ZwCreateFile) значение ByteOffset играет по-прежнему ту же роль в вызове ZwWriteFile, что и 6eз использования этого флага при открытии файла.

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

  • FileBasicInformation — для получения общих параметров открываемого файла, времени создания, времени последнего обращения, аргументов.
  • FileStandardInformation — для получения размера файла и признака того, что данный объект может быть директорией и т.п.
  • FilePositionInformation — для получения текущей позиции в файле.
  • FileAlignmentInformation — для получения информации о способе выравнивания буфера для работы с данным объектом.
  • FileNameInformation — для получения системной информации об имени файла.

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

Функции для работы со ссылками на объекты

В некоторых ситуациях необходимо получать доступ к объектам режима ядра. В операционной системе Windows объектами представлены многие компоненты: программные потоки, объекты синхронизации, файловые объекты, драйверы, устройства, объекты DPC процедур, прерываний и т.п. Правильнее говорить, что эти явления представлены структурами данных для удобства ведения их учета. Структуры эти в полной мере (например, в смысле ООП) объектами не являются. У них нет конструкторов, деструкторов, виртуальных методов в привычном смысле С++. Тем не менее, понятие "объект" широко используется и, в некотором смысле, справедливо — оперировать этими структурами предпочтительнее специально для того предназначенными системными вызовами, которые и можно считать методами для данных объектов.

Рассмотрим пример, в котором объектом синхронизации является программный поток. Программные потоки могут своим окончанием сигнализировать другим программным потокам о том, что пора приступать к работе. В частности, программный поток может быть запущен вызовом PsCreateSystemThread (см. главу 10). Другой поток может дожидаться его окончания, организовав ожидание при помощи одного из вызовов KeWaitForXxx. Возникает небольшое затруднение. Системный вызов PsCreateSystemThread, создавая поток, возвращает его дескриптор (HANDLE), a функции KeWaitForXxx принимают в качестве входного аргумента указатель на объект, по состоянию которого организуется ожидание.

Выход состоит в использовании вызова ObReferenceObjectByHandle, который в состоянии по корректному дескриптору возвратить указатель на соответствующий ему объект. Прототип ее вызова описан ниже.

Таблица 7.41. Прототип вызова ObReferenceObjectByHandle

NTSTATUS ObReferenceObjectByHandle | IRQL == PASSIVE_LEVEL
Параметры | Предоставляет указатель на объект режима ядра по открытому дескриптору
IN HANDLE Handle | Исходный дескриптор объекта
IN ACCESS_MASK DesiredAccess | Маска доступа, интерпретация которой зависит от типа рассматриваемого объекта
IN POBJECT_TYPE ObjectType OPTIONAL | Можно установить NULL (если параметр AccessMode равен KernelMode) или одно из значений IoFileObjectType or ExEventObjectType
IN KPROCESSOR_MODE AccessMode | • KernelMode — для работы в режиме ядра • UserMode
OUT PVOID *ppObject | Указатель на переменную типа PVOID, в которой будет возвращен указатель, соответствующий исходному дескриптору
OUT POBJECT_HANDLE_INFORMATION Handlelnfо OPTIONAL | Указатель на структуру, в которой будет представлена дополнительная информация об объекте и правам доступа к нему
Возвращаемое значение | • STATUS_SUCCESS • STATUS_OBJECT_TYPE_MISMATCH • STATUS_ACCESS_DENIED • STATUS_INVALID_HANDLE

Следует отметить, что в результате выполнения системного вызова ObReferenceObjectByHandle, увеличивается на единицу счетчик ссылок на этот объект, который поддерживается операционной системой.

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

Таблица 7.42. Прототип вызова ObReferenceObjectByPointer

NTSTATUS ObReferenceObjectByPointer | IRQL == PASSIVE_LEVEL
Параметры | Предоставляет указатель на объект режима ядра по открытому дескриптору
IN PVOID pObject | Указатель на объект
IN ACCESS_MASK DesiredAccess | Маска доступа, интерпретация которой зависит от типа рассматриваемого объекта
IN POBJECT_TYPE ObjectType OPTIONAL | Можно установить NULL (если параметр AccessMode равен KernelMode) или одно из значений IoFileObjectType or ExEventObjectType
IN KPROCESSOR_MODE AccessMode | • KernelMode — для работы в режиме ядра • UserMode
Возвращаемое значение | • STATUS_SUCCESS • STATUS_OBJECT_TYPE_MISMATCH

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

Таблица 7.43. Прототип вызова ObDereferenceObject

VOID ObDereferenceObject | IRQL == PASSIVE_LEVEL
Параметры | Уменьшает счетчик ссылок на объект
IN PVOID pObject | Указатель на объект
Возвращаемое значение | void

Заметим, что при выполнении вызова ZwClose происходит автоматическое уменьшение счетчика ссылок на объект и проверка, не пора ли его удалить из системы.

Функции для работы с системным представлением времени

Внутреннее время в Windows 2000/XP/Server 2003 хранится как число 100-наносекундных отсчетов с 1 января 1601 года. Это очень большое число, и для его хранения используется 64-разрядный тип данных в структуре, обозначенной как тип данных LARGE_INTEGER. В таблице 7.44 перечислены функции, предназначенные для работы с такими данными.

Таблица 7.44. Функции для работы с системным представлением времени

Функции | Описание
KeQuerySystemTime | Возвращает 64-разрядное значение абсолютного системного времени
KeQueryTickCount | Возвращает число прерываний системного таймера (часов) с момента последней загрузки системы
KeQueryTimelncrement | Возвращает число 100-нс интервалов, добавляемых к системному времени при каждом прерывании, поступающем от системного таймера (часов)
RtlTimeToTimeFields | Разбивает 64-разрядное системное время на поля даты и времени
RtlTimeFieldsToTime | Преобразует дату и время в 64-разрядное значение абсолютного системного времени
RtlConvertLongToLargeInteger | Создает знаковое LARGE_INTEGER
RtlConvertULongToLargelnteger | Создает положительное LARGE_INTEGER
RtlLargelntegerXxx | Различные арифметические и логические операции над типом данных LARGE_INTEGER (вместо этих функций в пакете DDK рекомендовано использовать встроенную поддержку компилятора для 64-разрядных операций)

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

Таблица 7.45. Прототип вызова KeQuerySystemTime

VOID KeQuerySystemTime | IRQL == любой
Параметры | Возвращает текущее время
OUT PLARGE_INTEGER CurrentTime | Время в 100 нc интервалах с 1 января 1601 года
Возвращаемое значение | void


Глава 7. Приемы программирования в режиме ядра

Функции для работы с Системным Реестром

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

Перед тем, как перейти к рассмотрению собственно функций доступа к Системному Реестру из кода режима ядра, еще раз повторим, что Реестр разбит на основные разделы, среди которых основным является раздел HKEY_LOCAL_MACHINE (в литературе часто используется сокращение HKLM). Разделы могут быть разбиты на подразделы произвольной глубины вложения. В англоязычной литературе подразделы могут называться словом "path", однако, чаще употребляется "key" или "registry key". B подразделе (даже если он содержит вложенные подразделы) могут содержаться параметры (values), которые имеют имя (обычно, в литературе называемые ValueName) и значение.

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

Функции доступа к Системному Реестру, предоставляемые Диспетчером ввода/вывода

Функции доступа к Реестру, предоставляемые Диспетчером ввода/вывода, предоставляют доступ по указателю на объект устройства, либо по имени зарегистрированного интерфейса (аналог символьной ссылки). На практике, программисты выбирают в качестве имени интерфейса глобальный идентификатор GUID в строковом представлении (генерируется программой GuidGen).

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

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

IoGetDeviceProperty запрашивает из Системного Реестра установочную информацию об устройстве.

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

IoOpenDeviceRegistryKey возвращает дескриптор доступа к подразделу Системного Реестра для драйвера или устройства по указателю на объект устройства. Полученный таким образом дескриптор должен по окончании использования быть закрыт вызовом ZwClose.

IoSetDeviceInterfaceState разрешает или запрещает доступ к зарегистрированному ранее интерфейсу устройства. Приложения пользовательского режима и другие компоненты системы могут получать доступ только к незапрещенным интерфейсам.

Функции RtlXxx прямого доступа к Системному Реестру

RtlCheckRegistryKey возвращает STATUS_SUCCESS в том случае, если указанный вложенный подраздел существует внутри подраздела, описанного первым параметром вызова. Первый параметр указывается как одно из значений RTL_REGISTRY_Xxx. Например, раздел \Registry\Machine\System\CurrentControlSet\Services описывается значением RTL_REGISTRY_SERVICES, а раздел \Registry\User\CurrentUser описывается значением RTL_REGISTRY_USER (см. заголовочные файлы ntddk.h или wdm.h).

RtlCreateRegistryKey создает подраздел внутри подраздела Реестра, указанного вторым параметром, который работает так же, как и в описанной выше функции RtlCheckRegistryKey.

RtlQueryRegistryValues позволяет одним вызовом получить значения нескольких параметров из всего поддерева указанного подраздела Реестра. При обнаружении требуемого параметра происходит вызов callback процедуры QueryRoutine, предоставляемой драйвером. Тем не менее, во всем многообразии примеров DDK нет ни одного случая использования этого вызова с callback процедурой QueryRoutine. На практике, вызов RtlQueryRegistryValues выполняется с первым параметром, имеющим в своем составе флаг RTL_QUERY_REGISTRY_DIRECT, при котором callback процедура не используется.

RtlWriteRegistryValue производит запись значения параметра Системного Реестра. Попутно может быть создан один вложенный подраздел, см. пример ниже.

UNICODE_STRING newParameterUnicodeTextValue;
RtlInitUnicodeString( &newParameterUnicodeTextValue,
                      L"Example text parameter 1");

NTSTATUS status =
RtlWriteRegistryValue( RTL_REGISTRY_SERVICES,
    L"Example1\\InnerKey",
    L"NewParameter",    // можно даже L"Вложенный Раздел"
    REG_SZ,
    newParameterUnicodeTextValue.Buffer,
    sizeof(WCHAR)*( wcslen(newParameterUnicodeTextValue.Buffer)+1)
    );
if ( !NT_SUCCESS( status ) )
{
    #if DBG
    DbgPrint("RtlWriteRegistryValue call is unsuccessful.");
    #endif
}

Первый параметр RTL_REGISTRY_SERVICES указывает на то, что действия будут выполняться в разделе \Registry\Machine\System\CurrentControlSet\Services, или, в общепринятых терминах, HKLM\SYSTEM\CurrentControlSet\Services. В нем должен существовать вложенный подраздел \Example1. Тогда возможны два варианта: в подраздел \Example1 вложен существующий подраздел \InnerKey, либо раздел \InnerKey не существует (и тогда он будет создан). В результате вызова получится подраздел HKLM\SYSTEM\CurrentControlSet\Services\Example1\InnerKey, в котором будет присутствовать строковый параметр NewParameter со значением "Example text parameter 1".

В том случае, если не существует не только подраздел \InnerKey, но и подраздел \Example1, вызов RtlWriteRegistryValue завершится неудачей.

В том случае, если все указанные подразделы присутствуют, и параметр NewParameter уже имеет какое-то значение, то в результате последовательности действий, как в описанном выше примере, в Системный Реестр будет записано новое значение, то есть "Example text parameter 1" .

RtlDeleteRegistryVaIue удаляет параметр из указанного подраздела, например:

status = RtlDeleteRegistryValue( RTL_REGISTRY_SERVICES,
                                 L"Example1\\InnerKey ",
                                 L"NewParameter" );
if( !NT_SUCCESS(x_status ) )
{
    #if DBG
    DbgPrint("RtlDeleteRegistryValue call is unsuccessful.");
    #endif
}

В результате, в HKLM\SYSTEM\CurrentControlSet\Services\Example1\InnerKey исчезнет параметр NewParameter.

Работа с Системным Реестром через вызовы ZwXxx

Все-таки наиболее богатым и основательным является набор функций для работы с Системным Реестром ZwXxx.

ZwCreateKey открывает доступ к существующему подразделу Системного Реестра или создает новый. Возвращает дескриптор открытого объекта.

ZwOpenKey открывает доступ к существующему разделу Системного Реестра и возвращает дескриптор открытого объекта (см.пример ниже).

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

ZwEnumerateKey возвращает информацию о вложенных подразделах предварительно открытого подраздела Системного Реестра.

ZwEnumerateValueKey возвращает информацию о параметрах и их значениях для предварительно открытого подраздела Системного Реестра.

ZwQueryValueKey возвращает информацию о значении параметра, присутствующего в данном предварительно открытом разделе Системного Реестра. Характер возвращаемой информации определяет третий аргумент вызова, который принимает одно из значений KeyValueBasicInformation, KeyValueFullInformation или KeyValuePartialInformation. Пример применения данной функции приводится ниже.

ZwSetValueKey создает или изменяет значение параметра в открытом подразделе Системного Реестра. Для возможности применения этой функции, дескриптор подраздела при открытии должен быть получен с применением маски DesiredAccess, содержащей флаг KEY_SET_VALUE

ZwFlushKey форсирует фиксацию изменений, сделанных в открытом подразделе вызовами ZwCreateKey или ZwSetValueKey, на диске.

ZwDeleteKey удаляет открытый подраздел из Системного Реестра.

ZwClose закрывает дескриптор открытого ранее подраздела Системного Реестра, фиксирует произведенные изменения на жестком диске.

Ниже приводится пример программного кода, выполняющий операции по получении значения параметра ErrorControl из раздела Системного Реестра, который был создан для описания драйвера и поступил в процедуру DriverEntry в аргументе RegistryPath. Для случая драйвера Example.sys этот подраздел называется HKLM\SYSTEM\ControlSet001\Services\Example, а собственно строка RegistryPath хранит значение:

L"\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Services\\Example"

Вся работа с Системным Реестром вынесена в отдельную функцию GetRegValueDword.

// Сначала объявляем прототип GetRegValueDword:
int GetRegValueDword(PCWSTR RegPath,PCWSTR ValueName,PULONG pValue);
extern "C"
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject,
                      IN PUNICODE_STRING RegistryPath )
{
 . . .

if(!GetRegValueDword( RegistryPath->Buffer,
                      L"ErrorControl",
                      &ulValue))
{
    #if DBG
    DbgPrint("Error in GetRegValueDword.");
    #endif
}
else
{
    #if DBG
    DbgPrint("RegistryPath\\ErrorControl = %x.", ulValue);
    #endif
}

int GetRegValueDword(PCWSTR RegPath, PCWSTR ValueName, PULONG pValue)
{
int                            ReturnValue = 0;
NTSTATUS                       status;
OBJECT_ATTRIBUTES              ObjectAttributes;
HANDLE                         KeyHandle;
KEY_VALUE_PARTIAL_INFORMATION  *pInformation;
ULONG                          uInformationSize;
UNICODE_STRING	                UnicodeRegPath;
UNICODE_STRING	                UnicodeValueName;
// Инициализация UNICODE_STRING полученными
// не-счетными строками двухбайтных символов
RtlInitUnicodeString(&UnicodeRegPath,   RegPath);
RtlInitUnicodeString(&UnicodeValueName, ValueName);

// Описание атрибутов, в частности, полного имени подраздела
InitializeObjectAttributes(&ObjectAttributes,
                           &UnicodeRegPath,
                           0,     // Flags
                           NULL,  // Root directory
                           NULL); // Security descriptor

status = ZwOpenKey( &KeyHandle,
                    KEY_QUERY_VALUE,
                    &ObjectAttributes );

if( !NT_SUCCESS(status) )  // Если не получен доступ к подразделу:
{
    #if DBG
    DbgPrint("=Example= Can not open reg path %ws .",
        UnicodeRegPath.Buffer);
    DbgPrint("=Example= Status = %x.",status);
    if (Status==STATUS_INVALID_HANDLE)
        DbgPrint("=Example= STATUS_INVALID_HANDLE.");
    if(Status==STATUS_ACCESS_DENIED)
        DbgPrint ("=Example= STATUS_ACCESS_DENIED.");
    #endif
return 0;
}

// Вычисляем размер буфера для получения информации о параметре:
uInformationSize =
        sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(ULONG);

// Выделение области в страничной памяти.
// Область будет помечена тегом 'EXaM'
pInformation =
    (KEY_VALUE_PARTIAL_INFORMATION*)
    ExAllocatePoolWithTag(PagedPool, uInformationSize,'EXaM');

if( pInformation == NULL )  // Не выделена память
{
    ZwClose(KeyHandle);
    return 0;
}

// Получить описание типа KeyValuePartialInformation:
//
status = ZwQueryValueKey(KeyHandle,
                         &UnicodeValueName,
                         KeyValuePartialInformation,
                         pInformation,
                         uInformationSize,
                         &uInformationSize );

if( !NT_SUCCESS(status) )
{
    #if DBG
    DbgPrint("=Example= ZwQueryValueKey not successful.");
    #endif
}
else
{
    if( pInformation->Type == REG_DWORD &&
        pInformation->DataLength == sizeof(ULONG) )
    {
        RtlCopyMemory(pValue, pInformation->Data, sizeof(ULONG));
        ReturnValue = 1;
    }
}

// Завершение работы
ExFreePool(Information);
ZwClose(KeyHandle);

return ReturnValue;
}

Практически все функции для работы с Системным Реестром должны вызываться из кода, работающего на уровне IRQL равном PASSIVE_LEVEL


Глава 7. Приемы программирования в режиме ядра

Заключение

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

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


Глава 8. Основные процедуры драйвера

Глава 8

Основные процедуры драйвера

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


Глава 8. Основные процедуры драйвера

Процедура DriverEntry

Процедура DriverEntry присутствует в любом драйвере и имеет данное стандартное имя. Драйверы "в-стиле-NT" выполняют в DriverEntry большую работу, нежели WDM драйвера — последние откладывают часть работы по инициализации устройства до момента обнаружения устройства системой, когда будет вызвана процедура AddDevice.

Таблица 8.1. Параметры вызова функции DriverEntry

NTSTATUS DriverEntry | IRQL == PASSIVE_LEVEL
Параметры | Описание
IN PDRIVER_OBJECT pDriverObject | Адрес объекта драйвера
IN PUNICODE_STRING pRegistryPath | Путь в регистре к подразделу драйвера
Возвращаемое значение | • STATUS_SUCCESS • STATUS_XXX — код ошибки

Получив от Диспетчера ввода/вывода указатель на структуру DRIVER_OBJECT (см. заголовочные файлы DDK ntddk.h или wdm.h), драйвер должен заполнить в ней определенные поля, а именно:

  • Поле pDriverObject->DriverUnload — для регистрации собственной функции Unload, которая вызывается перед выгрузкой драйвера.
  • Поле pDriverObject->DriverStartIo — для регистрации собственной функции StartIo, которая необходима для организации обработки очереди необработанных запросов System Queuing.
  • Поле pDriverObject->DriverExtension->AddDevice — в структуре расширения объекта драйвера DRIVER_EXTENSION (см. ntddk.h или wdm.h), в котором WDM драйвер регистрирует собственную процедуру AddDevice.
  • В массиве pDriverObject->MajorFunction[IRP_MJ_Xxx] драйвер регистрирует точки входа в собственные рабочие процедуры.

Регистрация рабочих процедур происходит обычно в виде:

DriverObject->MajorFunction[IRP_MJ_READ]= ReadWrite_IRPhandler;
DriverObject->MajorFunction[IRP_MJ_WRITE]=ReadWrite_IRPhandler;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=
    DeviceControlRoutine;
. . . . . . . . .

Однако для фильтр-драйверов модели WDM, которым интересен только определенный тип запросов, возможен следующий тип регистрации:

int i;
for( i=0; i<IRP_MJ_MAXIMUM_FUNCTION; i++)
{
    DriverObject->MajorFunction[i]= myPassIrpDown;
}
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=
    DeviceControlRoutine;

Здесь все запросы будут приводить к вызову функции myPassIrpDown, которая занимается только лишь переадресацией запросов нижним драйверам в стеке WDM драйверов. Исключение составит функция-обработчик IOCTL запросов, которые будут поступать в функцию DeviceControlRoutine фильтр-драйвера.

Помимо регистрации функций, процедура DriverEntry драйвера "в-стиле-NT" может выполнять следующую работу:

  1. DriverEntry определяет аппаратное обеспечение, которое драйвер будет контролировать. Это аппаратное обеспечение выделяется драйверу, то есть помечается как находящееся под управлением данного драйвера.
  2. Если драйвер управляет многокомпонентным (multiunit) или многофункциональным контроллером, используется IoCreateController для создания объекта контроллера, после чего инициализируется структура расширения контроллера.
  3. Выполняет вызов IoCreateDevice для создания объекта устройства для каждого физического или логического устройства под управлением данного драйвера, в процессе которого инициализируется структура расширения устройства для каждого созданного объекта устройства. Рекомендуется сразу же после этого вызова явно установить флаги (поле Flags в объекте устройства), описывающие способ буферизации, используемый данным устройством.
  4. Созданные устройства затем делаются видимыми для приложений пользовательского режима путем выполнения вызова IoCreateSymbolicLink.
  5. Устройство подключается к объекту прерываний. В случае, если ISR процедура требуют использования объекта DPC (отложенного процедурного вызова), то он создается и инициализируется на этом этапе.
  6. Шаги с 3 по 5 повторяются для каждого физического или логического устройства, работающего под управлением данного драйвера.
  7. В случае успешного завершения, функция DriverEntry должна возвратить Диспетчеру ввода/вывода значение STATUS_SUCCESS.

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

Таблица 8.2. Параметры системного вызова IoRegisterDriverReinitialization

VOID IoRegisterDriverReinitialization | IRQL == PASSIVE_LEVEL
Параметры | Регистрирует функцию драйвера для отложенной инициализации
IN PDRIVER_OBJECT pDriverObject | Указатель на объект драйвера
IN PDRIVER_REINITIALIZE DriverReinitializationRoutine | Указатель на процедуру реинициализации, предоставляемую драйвером (см. таблицу 8.3 ниже).
IN PVOID Context | Контекстный указатель, который получит регистрируемая функция при вызове
Возвращаемое значение | void

Таблица 8.3. Описание параметров вызова myReinitializeFunction

VOID myReinitializeFunction | IRQL == PASSIVE_LEVEL
Параметры | Функция драйвера, регистрируемая для выполнения отложенной инициализации
IN PDRIVER_OBJECT pDriverObject | Указатель на объект драйвера
IN PVOID Context | Контекстный блок, указанный при регистрации
IN ULONG | Количество вызовов процедуры ре-инициализации (отсчет от 0)
Возвращаемое значение | void

Как было указано в главе 7, процедуру DriverEntry можно разместить в "отстреливающемся" сегменте кода INIT.

Процедура AddDevice

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

Процедура Unload

Как правило, однажды загруженный драйвер остается в системе до перезагрузки. Для того чтобы сделать драйвер выгружаемым, необходимо написать и зарегистрировать процедуру выгрузки Unload. Диспетчер ввода/вывода затем производит вызов этой процедуры в момент ручной либо автоматической выгрузки драйвера — как раз перед удалением драйвера из памяти.

Таблица 8.4. Описание прототипа функции Unload

VOID Unload | IRQL == PASSIVE_LEVEL
Параметры | Выполняет завершающие действия
IN PDRIVER_OBJECT pDriverObject | Указатель на объект драйвера
Возвращаемое значение | void

Хотя действия процедуры Unload могут меняться от драйвера к драйверу, общими являются следующие шаги, характерные более для драйверов "в-стиле-NT".

  1. Для некоторых типов аппаратуры необходимо сохранить ее состояние в Системном Реестре. При последующей загрузке драйвера эти данные могут быть использованы в процедуре DriverEntry. Скажем, драйвер принтера может сохранить последнее значение разрешения печати.
  2. Если прерывания разрешены для обслуживаемого устройства, то процедура выгрузки должна запретить их и произвести отключение от объекта прерываний. Ситуация, когда устройство будет порождать запросы на прерывание в то время, как объект прерываний не существует, неминуемо приведет к краху системы.
  3. Символьная ссылка должна быть удалена из пространства имен, видимого пользовательскими приложениями. Это выполняется при помощи вызова IoDeleteSymbolicLink.
  4. Объект устройства должен быть удален вызовом IoDeleteDevice.
  5. В случае, если драйвер управляет многокомпонентным (multiunit) контроллером, необходимо повторить шаги 3 и 4 для каждого устройства, подключенного к контроллеру, а затем необходимо удалить сам объект контроллера при помощи вызова IoDeleteController.
  6. Следует выполнить освобождение памяти, выделенной драйверу, во всех типах оперативной памяти.

Драйверы WDM модели выполняют практически все из описанных выше действий в обработчике IRP_MJ_PNP запросов с субкодом IRP_MN_REMOVE (то есть посвященном удалению устройства из системы).

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

Процедура выгрузки драйвера Unload не вызывается в момент отката системы, и если существует необходимость выполнять какую-либо работу при откате системы, то это следует сделать в специально предназначенной на то процедуре драйвера, зарегистрированной для обработки IRP пакетов с кодом IRP_MJ_SHUTDOWN. Объект устройства должен быть с помощью вызова IoRegisterShutdownNotification занесен в очередь объектов, получающих уведомление о перезагрузке, — только при этом условии будет вызвана процедура, зарегистрированная для обработки пакетов с кодом IRP_MJ_SHUTDOWN.

Адресация и доступ к данным в IRP пакетах чтения/записи

Сразу же после создания объекта устройства (будет ли это сделано в процедуре DriverEntry, как это было указано выше для драйвера "в-стиле-NT", или в процедуре AddDevice для WDM драйверов) рекомендуется явно описать способ, каким новый объект устройства готов воспринимать поля пакета IRP, описывающие адреса областей памяти, через которые будет происходить обмен между драйвером и его клиентами. Например:

PDEVICE_OBJECT pNewDeviceObject;
IoCreateDevice(. . . , &pNewDeviceObject);
pNewDeviceObject->Flags |= DO_BUFFERED_IO;

либо:

pNewDeviceObject->Flags |= DO_DIRECT_IO;

По умолчанию подразумевается 'pNewDeviceObject->Flags |= 0' — метод NEITHER (ни один из первых двух).

В том случае, если новый объект устройства ориентирован на работу с нижним драйвером в стеке драйверов, следует скопировать эти флаги из объекта устройства, к которому подключен данный (одним из вызовов IoAttachXxx, см. главу 9), например:

pNewDeviceObject->Flags |=
    (pUnderlyingDevObject->Flags) & (DO_BUFFERED_IO | DO_DIRECT_IO);

По традиции, главными считаются запросы в форме пакетов IRP с основным кодом IRP_MJ_READ (в результате вызова ReadFile) либо IRP_MJ_WRITE (в результате вызова WriteFile).

Диспетчер ввода/вывода, если он замечает в описании устройства установленный флаг DO_DIRECT_IO, непременно проверяет возможность доступа к буферу, который клиент драйвера указывает в своем запросе как буфер с данными (WRITE) или для данных (READ), подготавливает MDL список для него и фиксирует страницы в оперативной памяти. Адрес подготовленного таким образом MDL списка вносится в поле IRP пакета под названием MdlAddress. Если попробовать получить виртуальный адрес от данного MDL списка вызовом MmGetMdlVirtualAddress, то получится именно виртуальный адрес (пользовательского адресного пространства), который предоставило пользовательское приложение в качестве адреса буфера с выводимыми данными. Адрес в терминах системного адресного пространства для той же области данных можно получить, если вызвать MmGetSystemAddressForMdl. Когда обработка запроса ввода/вывода полностью завершена, клиентский буфер де-блокируется и удаляется из схемы распределения системной области памяти.

В том случае, если объект устройства, которому адресован IRР пакет, описан флагом DO_BUFFERED_IO, то драйвер должен взять адрес буферной области из поля IRP пакета AssociatedIrp.SystemBuffer. Данный адрес будет адресом в системном адресном пространстве. Диспетчер ввода/вывода выделяет этот буфер в нестраничной памяти после проверки на доступность предоставленного клиентом буфера. При запросе ввода данных (READ-запрос) Диспетчер ввода/вывода по окончании операции переносит данные из системного буфера в клиентский, а адрес клиентского буфера запоминается в поле IRP пакета UserBuffer. При запросе вывода данных (WRITE-запрос) в системный буфер переносятся данные из клиентского буфера (поле UserBuffer равно NULL). B обоих описанных выше случаях, предоставленные в IRP пакете адреса AssociatedIrp.SystemBuffer можно использовать в произвольном контексте в потоках режима ядра.

В том случае, если устройство не объявило признаков DO_DIRECT_IO или DO_BUFFERED_IO, то Диспетчер ввода/вывода не делает никаких особых действий, а просто помещает адрес буферной области, переданной ему инициатором запроса в поле IRP пакета UserBuffer. Оба поля Associate.SystemBuffer и MdlAddress устанавливаются равными NULL.

В последнем случае помещенный в пoлe UserBuffer виртуальный адрес является "нехорошим" адресом. В большинстве случаев инициатором обращения к драйверу является программный поток пользовательского режима. Соответственно, предоставленный им виртуальный адрес требует для своей корректной интерпретации контекст соответствующего пользовательского потока. Что, разумеется, исключает возможность использования такого адреса в произвольных контекстах режима ядра без предварительной подготовки (подготовки MDL списка и т.п.). Поэтому метод NEITHER можно рекомендовать только для драйверов самых верхних драйверных слоев.

Длина буфера во всех случаях передается в полях Parameters.Write.Length (при WRITE-запросах) или Parameters.Read.Length (при READ-запросах).


Глава 8. Основные процедуры драйвера

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

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


Глава 8. Основные процедуры драйвера

Пакеты IRР

Практически весь процесс ввода/вывода, имеющий место в Windows, является пакетно-управляемым. Отдельная транзакция ввода/вывода описывается рабочим рецептом, предписывающим драйверу, что делать. При помощи IRP прослеживается также обработка запроса в подсистеме ввода/вывода. Этот рабочий рецепт имеет форму структуры данных, называемой I/o Request Packet (IRP) — пакет запроса на ввод/вывод.

При каждом запросе из программного кода клиента драйвера на выполнение операции ввода/вывода, включая IOCTL запросы (управляющие воздействия на аппаратуру), Диспетчер ввода/вывода выделяет под IRP область нестраничной памяти. Определив по дескриптору открытого файла, к какому драйверу и объекту устройства адресовано обращение, и по запрошенному коду операции ввода/вывода (IRP_MJ_Xxx), Диспетчер передает сформированный пакет IRP в соответствующую рабочую (см.ниже) процедуру драйвера. (Следует отметить, что для доступа из программного кода клиента применяется "файловая абстракция" процесса взаимодействия с драйвером — открытие, чтение, запись, дескрипторы и т.п.)

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

Рис. 8.1
Структура пакета IRP

Заголовок IRP

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

Таблица 8.5. Заголовок пакета IRP

Поля | Описание
IO_STATUS_BLOCK IoStatus | Код состояния (статус) запроса
PVOID AssociatedIrp.SystemBuffer | Указатель на системный буфер для случая, если устройство поддерживает буферизированный ввод/вывод
PMDL MdlAddress | Указатель на MDL список в случае, если устройство поддерживает прямой ввод/вывод
PVOID UserBuffer | Адрес пользовательского буфера для ввода/вывода
BOOLEAN Cancel | Индикатор того, что пакет IRP должен быть аннулирован

Фрагмент структуры IRP под названием IoStatus фиксирует окончательное состояние данной операции ввода/вывода. Когда драйвер готов завершить обработку пакета IRP, он устанавливает в поле IoStatus.Status значение STATUS_XXX. В поле IoStatus.Information этого блока записывается 0 (если произошла ошибка) или другое определенное операцией ввода/вывода значение, чаще всего — количество переданных/полученных байт данных (которое может быть и равно нулю).

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

Ячейки стека ввода/вывода

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

Таблица 8.6. Некоторые элементы ячейки стека ввода/вывода

IO_STACK_LOCATION, *PIO_STACK_LOCATION
Поля | Описание
UCHAR MajorFunction | Код IRP_MJ_XXX, описывающий назначение операции
UCHAR MinorFunction | Суб-код операции
PDEVICE_OBJECT DeviceObject | Указатель на объект устройства, которому был адресован данный запрос IRP
PFILE_OBJECT FileObject | Файловый объект для данного запроса, если он задан
union Parameters (трактовка определяется значением MajorFunction):
struct Read | Параметры для IRP типа IRP_MJ_READ: • ULONG Length • ULONG Key • LARGE_INTEGER ByteOffset
struct Write | Параметры для IRP типа IRP_MJ_WRITE: • ULONG Length • ULONG Key • LARGE_INTEGER ByteOffset
struct DeviceIoControl | Параметры для IRP типа IRP_MJ_DEVICE_CONTROL: • ULONG OutputBufferLenght • ULONG InputBufferLenght • ULONG IoControlCode • PVOID Type3InputBuffer

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

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

Когда драйвер передает IRP пакет нижнему драйверному уровню, Диспетчер ввода/вывода автоматически изменяет указатель стека ввода/вывода IRP пакета таким образом, что он указывает на стековую ячейку, предназначенную для драйвера очередного нижнего уровня. Когда обработка пакета драйвером нижнего уровня завершена, и он "отпускает" пакет IRP, указатель стека снова возвращается в исходное положение и указывает на ячейку стека для лежащего выше драйвера. Разумеется, для получения указателя на текущую ячейку стека существует специальный системный вызов IoGetCurrentStackLocation.


Глава 8. Основные процедуры драйвера

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

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

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

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

Метод объявления процедур ввода/вывода позволяет также объявить одну процедуру (функцию драйвера) для обслуживания нескольких запросов ввода/вывода. Для этого нужно лишь поместить адрес такой универсальной (если она действительно для этого предназначена) функции в нескольких ячейках массива MajorFunction, как это было показано выше для DriverEntry фильтр-драйвера. Так как пакет IRP содержит код IRP_MJ_Xxx, то с его помощью всегда можно восстановить точный код требующегося действия.

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

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

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

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

Набор рабочих процедур

Все драйверы обязаны поддерживать функцию, которая отвечает за обработку запроса с кодом IRP_MJ_CREATE, так как этот код генерируется в ответ на вызов пользовательского режима CreateFile. Без поддержки этого запроса пользовательское приложение не будет иметь никакой возможности получить дескриптор (handle) для доступа к драйверу устройства, а значит, и к самому устройству. Разумеется, должна существовать функция, обрабатывающая запрос с кодом IRP_MJ_CLOSE, который генерируется по обращению приложения к драйверу при помощи функции API пользовательского режима CloseHandle. Кстати заметить, вызов CloseHandle выполняется системой автоматически в момент завершения приложения пользовательского режима для всех дескрипторов ресурсов, оставленных эти приложением открытыми.

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

Таблица 8.7б. Коды запросов IRP и функции API пользовательского режима

IRP коды | Вызовы API пользовательского режима
IRP_MJ_CREATE | CreateFile
IRP_MJ_CLEANUP | Очистка ожидающих обработки пакетов IRP при закрытии дескриптора при отработке вызова CloseHandle
IRP_MJ_CLOSE | CloseHandle
IRP_MJ_READ | ReadFile
IRP_MJ_WRITE | WriteFile
IRP_MJ_DEVICE_CONTROL | DeviceIoControl
IRP_MJ_INTERNAL_DEVICE_CONTROL | Действия по управлению устройством, доступные только для клиентов, работающих в режиме ядра (недоступно для вызовов пользовательского режима)
IRP_MJ_QUERY_INFORMATION | Передача длины файла в ответ на вызов GetFileSize
IRP_MJ_SET_INFORMATION | Установка длины файла по вызову SetFileSize
IRP_MJ_FLUSH_BUFFERS | Запись или очистка служебных буферов при отработке вызовов (например): • FlushFileBuffers • FlushConsolelnputBuffer • PurgeComm
IRP_MJ_SHUTDOWN | Действия, которые нужно выполнить драйверу в процессе подготовки системы к завершению работы

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

Рабочая процедура обычно может отслеживать состояние обработки запроса только с использованием пакета IRP. В том случае, если рабочая процедура использует какие-либо данные за пределами пакета IRP, драйвер должен обеспечить достаточно надежные меры по синхронизации доступа к этим данным. Это означает, что можно было бы использовать спин-блокировку для координации действий с другими процедурами, выполняющимися на уровне IRQL не выше DISPATCH_LEVEL. Для синхронизации доступа из процедур, выполняющихся на уровне кода обслуживания прерываний, следует использовать KeSyncronizeExecution.

Пакеты IRP являются данными коллективного пользования, хотя и с последовательным доступом. В частности, Диспетчер ввода/вывода использует поля объединения Parameters для того, чтобы завершить обработку запроса. Например, по окончании обработки буферизованного ввода/вывода, ему необходимо выполнить копирование данных из области памяти нестраничного пула в буфер, заданный пользовательским приложением. По завершении копирования Диспетчер ввода/вывода должен освободить буфер в нестраничном пуле. Одно из полей объединения (union) Parameters как раз указывает на этот буфер, и изменение кодом драйвера данного указателя приведет к нарушению работы системы.

Последовательность действий рабочих процедур

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

  1. Вызов IoGetCurrentIrpStackLocation для того, чтобы получить указатель на ячейку стека IRP пакета, относящуюся к ведению данного драйвера.
  2. Дополнительную проверку параметров, специфичную для данного типа запроса и устройства.
  3. Продолжение обработки IRP до момента успешного завершения или возникновения ошибочной ситуации, препятствующей дальнейшей обработке.

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

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

Случай 1: Ошибочная ситуация

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

  1. Соответствующий код ошибки сохраняется в поле Status в блоке IoStatus пакета IRP и производится обнуление поля Information.
  2. Производится вызов IoCompleteRequest для того, чтобы завершить обработку пакета IRP (без повышения приоритета).
  3. Рабочая процедура, возвращая управление, должна возвратить тот же код ошибки, что был помещен в поле IoStatus.Status пакета IRP.
NTSTATUS
WriteRequestHandler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
    :
    // Запрос не поддерживается данным устройством (например):
    pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED;
    pIrp->IoStatus.Information = 0;   // Ни одного байта не передано
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);  // без изменения приоритета
return STATUS_NOT_SUPPORTED;
}

Вызов IoCompleteRequest будет подробно рассмотрен в следующей главе, но сейчас следует отметить, что после него область памяти, занятая под собственно пакет IRP может оказаться свободной. Поэтому категорически нельзя экономить и писать операторы типа "return pIrp->IoStatus.Status;", впрочем, как и обращаться по адресу pIrp в каких бы то ни было целях после вызова IoCompleteRequest.

Случай 2: Завершение работы над IRP запросом

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

  1. Поместить код успешного завершения в поле Status в блоке IoStatus пакета IRP и указать приемлемое значение в поле Information.
  2. Выполнить вызов IoCompleteRequest, чтобы освободить пакет IRP без повышения приоритета.
  3. Возвратить управление с кодом STATUS_SUCCESS.
NTSTATUS
CloseRequestHandler( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
    :
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest ( pIrp, IO_NO_INCREMENT );
    return STATUS SUCCESS;
}

Случай 3: Работа через очереди IRP пакетов

Разумеется, простейший драйвер может инициализировать свое устройство в процедуре DriverEntry, в обработчике запросов IRP_MJ_READ сразу же считывать данные из устройства (например, LPT порта). Тем не менее, полномасштабный ритуал работы с подсистемой ввода/вывода Windows (то есть Диспетчером ввода/вывода) диктует иную последовательность действия. Рабочая процедура должна поместить пакет IRP в очередь для последующей обработки процедурой StartIO и сразу же возвратить Диспетчеру ввода/вывода сообщение о том, что обработка IRP не завершена, а именно:

  1. Выполнить вызов IoMarkIrpPending — чтобы информировать Диспетчера ввода/вывода о том, что пакет IRP поставлен в очередь на обработку.
  2. Выполнить вызов IoStartPacket, чтобы поместить пакет IRP в системную очередь для последующей его обработки процедурой StartIO. Драйвер может реализовывать и свои очереди IRP пакетов.
  3. Возвратить управление из рабочей процедуры с кодом завершения STATUS_PENDING.

Фрагмент кода, приведенный ниже, демонстрирует, как рабочая процедура размещает IRP запрос в очереди на обработку.

NTSTATUS ReadRequestHandler( IN PDEVICE_OBJECT  pDeviceObject,
                             IN PIRP            pIrp )
{
    :
    // IRP "в работе", но работа с ним будет происходить
    // через очередь пакетов (в данном случае - системную):
    IoMarkIrpPending( pIrp );

    // Четвертый параметр позволяет указывать процедуру удаления
    // CancelRoutine, что подробно обсуждается в конце главы 9.
    IoStartPacket( pDeviceObject, pIrp, 0, NULL );

    return STATUS_PENDING;
}

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


Глава 8. Основные процедуры драйвера

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

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

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

Следует отметить, что реализация процедуры разборки таких IRP запросов в драйвере требует вторичной диспетчеризации — в соответствии со значением IoControlCode. Это значение известно еще со времен MS DOS под именем IOCTL — Input/Output ConTroL code.

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

 Рис. 8.2.
Структура блока данных IOCTL

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

Параметры | Описание
DeviceType | FILE_DEVICE_XXX значения передаваемые в IoCreateDevice • 0x0000 — 0x7FFF — зарезервировано Microsoft • 0x8000 — 0xFFFF — определяется пользователем
ControlCode | Определяемые драйвером IOCTL значения • 0x000 — 0x7FF — зарезервировано Microsoft (public) • 0x800 — 0xFFF — определяется пользователем
TransferType | Способ получения доступа к буферу • METHOD_BUFFERED • METHOD_IN_DIRECT • METHOD_OUT_DIRECT • METHOD_NEITHER
RequiredAccess | Требования инициатора относительно типа доступа • FILE_ANY_ACCESS • FILE_READ_DATA • FILE_WRITE_DATA • FILE_READ_DATA | FILE_WRITE_DATA

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

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

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

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

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

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

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

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

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

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

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

 | METHOD_BUFFERED | METHOD_IN_DIRECT или METHOD_OUT_DIRECT | METHOD_NEITHER
Input Буфер с данными | Использует буферизацию (системный буфер) Адрес буфера в системном адресном пространстве указан в pIrp->AssociatedIrp.SystemBuffer |  | Клиентский виртуальный адрес в Parameters.DeviceIoControl. Type3InputBuffer
 | Длина указана в Parameters.DeviceIoControl.InputBufferLength
Output Буфер для данных | Использует буферизацию (системный буфер) Адрес буфера в системном адресном пространстве указан в pIrp-> AssociatedIrp.SystemBuffer | Использует прямой доступ, клиентский буфер преобразован в MDL список, указатель на который размещен в PIrp->MdlAddress | Клиентский виртуальный адрес в pIrp->UserBuffer
 | Длина указана в Parameters.DeviceloControl.OutputBufferLength

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

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

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

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

При методе METHOD_BUFFERED

Диспетчер ввода/вывода выделяет единственный буфер в нестраничной памяти, достаточно большой, чтобы вместить входной или выходной буфер инициатора вызова. Адрес этой области размещается в пакете IRP в поле AssociatedIrp.SystemBuffer. Затем производится копирование входного (input) буфера с данными инициатора запроса в эту область. В поле UserBuffer пакета IRP заносится оригинальный адрес буфера для получения данных инициатора запроса.

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

При методе METHOD_IN_DIRECT и METHOD_OUT_DIRECT

Диспетчер ввода/вывода проверяет приемлемость выходного (output) буфера инициатора вызова и производит его фиксацию (lock) в физической памяти. Затем производит построение списка MDL (Memory Descriptor List) для выходного буфера и сохраняет указатель на MDL в поле MdlAddress пакета IRP.

Кроме того, Диспетчер ввода/вывода выделяет временную область в нестраничном пуле и сохраняет этот адрес в поле AssociatedIrp.SystemBuffer пакета IRP. Производится копирование содержимого входного (input) буфера инициатора вызова в выделенный системный буфер, а в поле UserBuffer производится запись значения NULL. После этого IRP пакет поступает в вызываемую рабочую процедуру драйвера.

При методе METHOD_NEITHER

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


Глава 8. Основные процедуры драйвера

Обслуживание прерываний

В операционной системе Windows NT 5 все прерывания изначально обрабатываются ядром. Это сделано для облегчения переносимости операционной системы на разные аппаратные платформы. Ядро обеспечивает диспетчеризацию прерываний между драйверами путем создания и последующего подключения объектов прерывания по вызову IoConnectInterrupt (см. таблицу 8.10). Этот вызов получает адрес процедуры для обслуживания прерываний (ISR, Interrupt Service Routine) драйвера, и таким образом операционная система связывает указанное аппаратное прерывание с определенным драйвером и принадлежащей ему ISR функцией.

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

Когда операционная система получает сигнал прерывания от устройства, она использует свой список объектов прерывания для локализации ISR процедуры, в ведении которой находится обслуживание данного события. Она "пробегает" по всем объектам прерывания, подключенным к DIRQL этого прерывания, и вызывает ISR процедуры до тех пор, пока одна из них не заявит о своих правах на него.

Диспетчер прерываний режима ядра вызывает ISR процедуру на уровне синхронизации SynchronizeIrql, указанном в вызове IoConnectInterrupt. Обычно, это один из DIRQL уровней. Кроме того, диспетчер прерываний получает владение над объектом спин-блокировки pSpinLock и удерживает ее во время выполнения ISR процедуры, что предохраняет от выполнения ISR процедуры на других процессорах.

При выполнении на столь высоком уровне IRQL существует некоторые вещи, которые процедура ISR не может себе позволить. В дополнении к обычным предостережениям избегать манипуляций со страничной памятью, ISR процедура не должна пытаться получать или освобождать какие-либо системные ресурсы, даже нестраничную память. Если разработчик предполагает сделать системный вызов из ISR процедуры, следует обязательно обратить внимание на уровень, на котором тот может выполняться. Вполне вероятно, что такие системные вызовы придется перепоручить DPC процедуре, запуск которой вполне может запланировать данная функция обслуживания прерываний.

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

NTSTATUS IoConnectInterrupt | IRQL == PASSIVE_LEVEL
Параметры | Регистрирует процедуру обслуживания прерывания, предоставляемую драйвером, и "подключает" ее к источнику прерываний
OUT PKINTERRUPT *pInterruptObject | Адрес указателя, в котором будет возращен указатель на объект прерывания
IN PKSERVICE_ROUTINE ServiceRoutine | Процедура (функция) драйвера, которая теперь будет обслуживать прерывание
IN PVOID pServiceContext | Аргумент, передаваемый в процедуру ISR, обычно рекомендуется приводить здесь указатель на структуру расширения объекта устройства
IN PKSPIN_LOCK pSpinLock | Инициализированный объект спин-блокировки
IN ULONG Vector | Транслированное значение вектора прерывания
IN KIRQL Irql | Значение DIRQL для данного устройства
IN KIRQL SynchronizeIrql | Обычно равно значению Irql
IN KINTERRUPT_MODE InterruptMode | Аппаратный тип прерывания. Одно из значений: • LevelSensitive • Latched
IN BOOLEAN isSharableVector | Если TRUE — данный вектор прерывания является совместно используемым (разделяемым)
IN KAFFINITY ProcessorEnableMask | Установить набор процессоров, которые могут получать сигналы прерывания
IN BOOLEAN doFloatingSave | Если TRUE — сохранять состояние регистров сопроцессора (FPU). Обычно используется FALSE
Возвращаемое значение | • STATUS_SUCCESS • STATUS_INVALID_PARAMETER • STATUS_UNSUFFUCIENT_RESOURCES

Таблица 8.11. Прототип функции драйвера для обслуживания прерываний

BOOLEAN ISR | IRQL == DIRQL
Параметры | Процедура драйвера, предоставляемая им для обслуживания прерывания
IN PKINTERRUPT *pInterruptObject | Объект прерывания, "генерирующий" прерывания
IN VOID pServiceContext | Контекстный аргумент, указанный при регистрации в IoConnectInterrupt
Возвращаемое значение | • TRUE — прерывание было обслужено ISR • FALSE — прерывание не обслуживается

Параметр InterruptMode вызова IoConnectInterrupt интерпретируется операционной системой следующим образом. Когда драйверы подключили свои ISR процедуры к прерыванию, считая его LevelSensitive, операционная система вызывает все подключенные таким образом ISR функции до тех пор, пока одна из них не возвратит TRUE. В противном случае, то есть если указано значение Latched для параметра InterruptMode, то операционная система вызывает все из подключенных таким образом ISR процедур, и эти вызовы повторяются до тех пор, пока все ISR процедуры не возвратят значение FALSE.

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

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

Во-вторых, в случае, если процедура ISR управляет более чем одним прерыванием (вектором прерывания), следует позаботиться о том, чтобы значение, указанное в качестве SynchronizeIrql было наибольшим значение DIRQL из всех обслуживаемых прерываний.

Наконец, процедура ISR драйвера должна быть готова к работе с момента выполнения вызова IoConnectInterrupt. Даже в том случае, если выполнены еще не все действия по инициализации драйвера.

В общих чертах, процедура обслуживания прерываний должна придерживаться следующего плана:

  1. Определить, относится ли поступившее прерывание к данному драйверу. Если не относится — немедленно возвратить FALSE.
  2. Выполнить все операции над устройством, необходимые для того, чтобы подтвердить устройству получение прерывания.
  3. Определить, существует ли необходимость в передаче данных и дополнительных действиях, которые могут быть выполнены на низких уровнях IRQL. Если такая работа имеется, то следует запланировать вызов DPC процедуры (предоставляемой драйвером), то есть поставить в очередь DPC-запрос вызовом IoRequestDpc.
  4. Возвратить значение TRUE.

Таблица 8.12. Прототип вызова IoRequestDpc

VOID IoRequestDpc | IRQL == DIRQL
Параметры | Помещает DPC вызов в очередь
IN PDEVICE_OBJECT pDevObject | Объект устройства, для которого зарегистрирована DPC процедура
IN PIRP pIrp | Указатель на интересующий IRP пакет
IN VOID pServiceContext | Контекстный аргумент
Возвращаемое значение | void

Если сравнить прототип ISR процедуры и прототип вызова IoRequestDpc, который делается из ISR процедуры для планирования последующего вызова DPC процедуры (для завершения работы над прерыванием), то становится очевидной проблема.

Обычно рабочие процедуры драйвера получают указатель на объект устройства и указатель на адресованный этому объекту IRP пакет через заголовок при вызове. Но прототип ISR процедур этого не предусматривает. Как же тогда сделать из нее вызов IoRequestDpc, чтобы запланировать DPC процедуру?

Данное затруднение решается, если при регистрации ISR процедуры вызовом IoConnectInterrupt в качестве контекстного аргумента pServiceContext ввести указатель на структуру расширения структуры (извините за неблагозвучные повторы) данного объекта устройства, то есть указатель на DEVICE_EXTENSION. При условии заблаговременного сохранения там указателя на объект устройства (например, как это было сделано в DriverEntry примера главы 3) процедура ISR драйвера не будет испытывать затруднения с тем, откуда ей взять данный указатель.

Остается вопрос, что такое pIrp и где его найти?

Возможны два варианта. Во-первых, для обработки прерывания действительно требуется текущий обрабатываемый драйвером IRP пакет, и тогда его можно найти, как pDeviceObject->CurrentIrp, но только для пакета из системной очереди, SystemQueuing. Во-вторых, IRP пакет может и не потребоваться (по логике прерывания и работы драйвера), тогда можно просто указать NULL. Более того, во многих случаях IRP пакет указать невозможно или нежелательно. Поскольку Диспетчер ввода/вывода не анализирует этот параметр, то в нем можно передавать нужные (по логике работы) данные так же, как и в указателе pServiceContext. В результате, ISR процедура может выглядеть следующим образом:

BOOLEAN OnInterrupt ( PKINTERRUPT pInterruptObject,
                      PVOID pServiceContext )
{
    PMYDEVICE_EXTENSION pDevExt=(PMYDEVICE_EXTENSION) pServiceContext;

// Считываем нужный регистр нашего устройства чтобы
// определить, действительно ли оно посылало сигнал прерывания
ULONG intrStatus =
    READ_PORT_ULONG((PULONG) (pDevExt->InterruptStateReg));
if( intrStatus!=. . . ) return FALSE; // Это чужое прерывание

// Некоторые действия, например, изменение состояния
// регистров устройства, чтобы оно знало: о нем помнят.
. . .
// Планируем вызов DPC процедуры, например, без IRP пакета:
IoRequestDpc( pDevExt->DeviceObject, NULL, pDevExt);

return TRUE; // Прерывание обработано
}

Детальное рассмотрение вопросов обработки прерываний и применения DPC процедур выполнено в главе 11, "Обработка аппаратных прерываний".

Процедуры отложенного вызова обслуживания прерываний DpcForIsr

Вызов процедуры DpcForIsr может быть запланирован из собственно ISR процедуры вызовом IoRequestDpc. Прототип этой функции драйвера описан в таблице 8.13.

Таблица 8.13. Прототип функции для процедуры DpcForIsr

VOID DpcForIsr | IRQL == DISPATCH_LEVEL
Параметры | Завершает обработку прерывания, начатую в ISR процедуре драйвера
IN PKDPC pDpc | DPC-объект
IN PDEVICE_OBJECT pDevObj | Указатель на объект устройства, для которого зарегистрирована данная DPC процедура
IN PIRP pIrp | Интересующий пакет IRP
IN VOID pContext | Контекстный указатель, переданный вызовом IoRequestDpc
Возвращаемое значение | void

При анализе прототипа вызова IoRequestDpc и примера кода перед таблицей 8.13, возникает правомерный вопрос: вызов какой функции планирует ISR процедура, выполняя вызов IoRequestDpc? Ведь DpcForIsr функция не фигурирует ни при регистрации ISR процедуры вызовом IoConnectInterrupt, ни в тексте OnInterrupt.

Ответ состоит в том, что DpcForIsr регистрируется для конкретного объекта устройства вызовом IoInitializeDpcRequest (см. таблицу 8.14). Если обратить внимание на структуру DEVICE_OBJECT, описанную в заголовочных файлах wdm.h и ntddk.h (см. пакет DDK), то несложно заметить поле 'KDPC Dpc', предназначенное как раз для хранения DPC объекта. Соответственно, выполняя вызов IoRequestDpc c указателем на объект устройства в качестве первого аргумента, ISR процедура драйвера однозначно определяет, какая именно DpcForIsr функция будет вызвана позже — хранящаяся в объекте устройства.

Таблица 8.14. Прототип вызова IoInitializeDpcRequest

VOID IoInitializeDpcRequest | IRQL == PASSIVE_LEVEL
Параметры | Регистрирует DpcForIsr процедуру для данного объекта устройства, создает и инициализирует DPC объект
IN PDEVICE_OBJECT pDevObj | Указатель на объект устройства, для которого регистрируется данная DPC процедура
IN VOID (*DpcForIsr) | Адрес DpcForIsr процедуры, которая должна иметь прототип, описанный в таблице 8.13
Возвращаемое значение | void

Регистрацию DpcForIsr процедуры (и ее связывание с объектом устройства) для PnP драйверов рекомендуется делать в процедуре AddDevice драйвера.

Первый параметр pDpc в описании прототипа процедуры DpcForIsr (таблица 8.13) не должен пугать разработчика. Если используется вызов IoInitializeDpcRequest, то именно он создает данный объект, а в вызов DpcForIsr поступит указатель на уже готовый DPC объект.

Выполнения кода процедуры DpcForIsr

В ответ на выполненный ISR процедурой вызов IoRequestDpc, принадлежащая данному драйверу процедура DpcForIsr добавляется в очередь DPC объектов (DPC dispatch queue). Когда значение процессорного IRQL падает ниже DISPATCH_LEVEL, диспетчер процедур обработки отложенных вызовов выполняет вызов DpcForIsr данного драйвера. Процедура DpcForIsr выполняется на уровне DISPATCH_LEVEL, что означает: она не должна работать с адресами страничной памяти.

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

Перечень обязанностей типовой процедуры DpcForIsr может включать:

  • Установку данных в блоке IoStatus данного пакета IRP, то есть размещение нужного кода STATUS_XXX в поле Status и числа действительно переданных байт данных в поле Information.
  • Выполнение вызова IoCompleteRequest для завершения обработки пакета IRP с соответствующим повышением приоритета. Будучи однажды вызван, пакет IRP не должен обрабатываться снова.
  • Выполнение вызова IoStartNextPacket — чтобы инициировать поступление следующего IRP пакета в процедуру StartIO.
VOID DpcForIsr( PKDPC Dpc,
                PDEVICE_OBJECT pDevObject,
                PIRP junk,
                PDEVICE EXTENSION pDevExtension)
{
    // Неким образом получаем текущий IRP (например, из очереди
    // queueReadWrite, которую поддерживает сам драйвер):
    PIRP pIrp = GetCurrentIrp(&pDevExtension->queueReadWrite);

    // Инициируем поступление IRP из внутренней очереди в
    // в процедуру StartIO():
    TodoStartNextPacket(&pDevExtension->dqReadWrite, pDevObject)

    // Даем возможность отработать процедурам завершения всех
    // вышестоящий драйверов, если они есть:
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
}

Отключение от источника прерываний

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

Отключение от источника прерываний является двухступенчатой процедурой. Во-первых, следует использовать KeSynchronizeExecution и процедуру SynchCritSection для того, чтобы обеспечить такое состояние устройства, когда он не будет производить генерацию сигналов на прерывание. Во-вторых, следует произвести удаление ISR процедуры из системного списка обработчиков прерываний путем осуществления вызова IoDisconnectInterrupt (с передачей этому вызову в качестве аргумента указателя на полученный ранее объект прерывания для данного устройства).

Таблица 8.15. Прототип вызова IoDisconnectInterrupt

VOID IoDisconnectInterrupt | IRQL == PASSIVE_LEVEL
Параметры | Регистрирует DpcForIsr процедуру для данного объекта устройства
IN PKINTERRUPT pInterruptObject | Указатель на объект прерывания, полученный ранее в результате вызова IoConnectInterrupt
Возвращаемое значение | void

 Нерассмотренным остается один весьма деликатный момент. При регистрации ISR процедуры для обслуживания конкретного прерывания используется вызов IoConnectInterrupt, подробно описанный в таблице 8.10. Наиболее важным и трудным в обращении является параметр Vector, представляющий транслированное прерывание, к которому и производится подключение регистрируемой ISR процедуры. Данная процедура имеет свою специфику для каждого типа не-WDM драйверов (в зависимости от того, к шине какого типа подключено устройство, обслуживаемое драйвером, например, ISA или PCI). Однако для WDM драйверов эта процедура универсальна. В данном случае, когда PnP Менеджер делает запрос с кодом IRP_MJ_PNP и субкодом IRP_MN_START_DEVICE, то в каждом таком запросе он передает и список присвоенных драйверу ресурсов.

Драйвер WDM, например, из пакета разработки устройств PCI шины фирмы PLX Technology, выходит из положения следующим образом.

Прежде всего, данный драйвер, получив IRP пакет с указанными признаками (IRP_MJ_PNP, субкодом IRP_MN_START_DEVICE), дает возможность сначала обработать его нижележащим драйверам, перехватывая его на обратном пути. После этого он производит анализ полученных ресурсов, в частности, подключает собственную функцию обработки прерываний PlxOnInterrupt к прерыванию, как оно было предопределено PnP Менеджером, передавшим выделенные ресурсы в ячейке стека пакета IRP.

int i, count;
BOOLEAN interruptPresented = FALSE;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation( pIrp );
PCM_PARTIAL_RESOURCE_LIST pRawResource=
    &stack->Parameters.StartDevice.AllocatedResources->
        List[0].PartialResourceList;
count = pRawResource->Count;
for (i = 0; i < count; i++, pRawResource++)
{
    //Просмотр всех выделенных драйверу ресурсов
    switch (pRawResource ->Type)
    {
    case CmResourceTypeInterrupt:     // Прерывание
        interruptPresented = TRUE;
        IrqL = (KIRQL) Resource->u.Interrupt.Level;
        vector = Resource->u.Interrupt.Vector;
        affinity = Resource->u.Interrupt.Affinity;
        if (ResourceRaw->Flags == CM_RESOURCE_INTERRUPT_LATCHED)
            mode = Latched;
        else
            mode = LevelSensitive;
        break;
        . . .
    }
}
if (interruptPresented)
{
    // Здесь следует запретить прерывания от ведомого устройства
    . . .
    // Создание объекта прерывания и подключение к нему
    status = IoConnectInterrupt(
                                &pDevExtension->pInterruptObject,
                                PlxOnInterrupt,
                                pDevExtension,
                                NULL,
                                vector,
                                IrqL,
                                IrqL,
                                mode,
                                TRUE,
                                affinity,
                                FALSE
                                );
    if ( !NT_SUCCESS(status) )
    { // обработка ошибки
    }
    else
    { // разрешить прерывания от устройства
    }
}

Здесь pDevExtension указывает на структуру расширения объекта устройства (соответственно, эта структура должна предусмотреть в своем составе место для хранения указателя на создаваемый объект прерывания, здесь pInterruptObject), a PlxOnInterrupt передает адрес предоставляемой данным драйвером ISR процедуры, созданной в соответствии с прототипом, описанным в таблице 8.11.


Глава 8. Основные процедуры драйвера

Заключение

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

Практические примеры, посвященные отработке приемов обслуживания прерываний будут рассмотрены в главе 11, "Обработка аппаратных прерываний".

В следующей главе будут рассмотрены дополнения, привнесенные драйверной моделью WDM.


Глава 9. Драйверная модель WDM

Глава 9

Драйверная модель WDM


Глава 9. Драйверная модель WDM

Развитие спецификации Plug & Play

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

В начале девяностых годов пользователь персонального компьютера должен был уметь настраивать свой ПК, экономно при этом расходуя запас "незанятых" прерываний, переставляя перемычки и меняя положение DIP-переключателей на дополнительный картах в поисках оптимального быстродействия. Предварительная подготовка касалась не только необходимого знания портов ввода/вывода, настроек режима прямого доступа к памяти. Необходимо было знать, насколько хорошо сочетаются программы (которые предполагается установить) как с имеющимся "железом", так и между собой. И так далее, и так далее, включая "разгон" процессоров и преодоление проблем перевода ОС и принтеров на родной язык...

Разумеется, о таких "пустяках", как возможность подсоединения новых устройств без выключения компьютера, не было речи вовсе! Максимум сервиса предлагали игровые программы, которые могли предложить "поиграть" настройками, спрашивая в конце каждой итерации "Слышите ли Вы звук в колонках?"

Пик беспорядка в вопросах конфигурирования устройств, составляющих компьютер, пришелся на то время, когда массовый пользователь вместе со своими любимыми программами вырос из рамок возможностей шины ISA. Разумеется, проблемы конфигурирования не давали покоя специалистам и раньше (скажем, со времен Unibus периферии для PDP-11), но только в данной временной точке распространенность вычислительной техники сделали преодоление этой проблемы делом почти что первостепенным.

Решение пришло в виде разработки спецификации Plug and Play, согласно которой устройства должны выдерживать определенные механические и электрические нормы. Основное же требование Plug and Play состоит в том, что устройства должны уметь предоставлять идентификационную информацию о себе в формате, определенном для данного типа (PCI, USB, FireWire, CardBus) подключения.

С выходом Windows 95 (и появлением некоторых сдвигов в подходах к разработке аппаратной части) усилия были сконцентрированы на автоматизации конфигурирования системы при добавлении и удалении устройств. Эти попытки усилили тенденции перехода пользователей на Windows 95, что в свою очередь ускорило миграцию на 32-разрядные операционные системы Microsoft, в частности на Windows NT. Наконец, с выпуском Windows 2000 Microsoft реализовала законченную архитектуру Plug and Play для подсистем ввода/вывода.

В настоящее время подключение устройств по шинам USB, CardBus (модифицированная PCMCIA) и FireWire (IEEE-1394) при работающем основном компьютере является безопасным (и даже штатным) режимом работы. Драйверная архитектура Windows 2000/XP/2003 полностью поддерживает эти события "появления" в системе новых устройств, позволяя конфигурировать и делать их доступными для использования без выключения питания компьютера и перезагрузки операционной системы.

Главным плюсом использования методологии Plug and Play является обеспечение автоматической поддержки инсталляции и удаления системных устройств. Чтобы добиться этого, необходимо выполнить несколько условий.

  • Устройства должны быть ориентированы на выполнение программного конфигурирования. Должна существовать возможность установки портов ввода/вывода, задействованных прерываний и ресурсов DMA (параметров прямого доступа к памяти) из управляющего программного обеспечения, исключая механическое конфигурирование при помощи переставляемых перемычек и DIP-переключателей на платах.
  • Система должна обеспечивать надежное автоматическое обнаружение нового устройства или факт удаления существующего. Устройство и шина, к которой устройство подключено (было подключено), должны информировать управляющее программное обеспечение о том, что аппаратная конфигурация претерпела частичные изменения.
  • Необходимые драйверы для новых устройств должны загружаться автоматически, по мере обнаружения этих устройств операционной системой. (Вмешательство пользователя все-таки требуется, но только лишь для установки драйверов для никогда ранее не присутствовавших в системе нестандартных устройств.)
  • В тех случаях, когда устройство и его интерфейсная шина позволяют, операционная система должна поддерживать и так называемое "горячее" (при включенном питании) присоединение аппаратуры. То есть должна быть возможность подключения/отключения устройства непосредственно в "живую" систему без создания для нее стрессовых ситуаций.

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

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

  • PnP Менеджер, который состоит из двух частей — работающей в режиме ядра и работающей в пользовательском режиме. Часть из режима ядра взаимодействует с аппаратурой и другими программными компонентами, функционирующими в режиме ядра, обеспечивая управление правильным определением и конфигурированием аппаратуры. Часть из пользовательского режима взаимодействует с компонентами пользовательского интерфейса, позволяя интерактивной программе делать запросы и изменять конфигурацию инсталлированного PnP программного обеспечения.
  • Менеджер Управления Энергопитанием (Power Manager), который определяет и обрабатывает события энергообеспечения.
  • Системный Реестр Windows, являющийся базой данных установленного аппаратного и программного обеспечения, поддерживающего спецификацию PnP. Содержимое реестра помогает драйверам и другим компонентам при определении ресурсов, используемых любым конкретным устройством.
  • Inf-файлы. Каждое устройство должно быть полностью описано файлом, который используется при инсталляции управляющего им драйвера. Inf-файл является рецептом, как и какую информацию об устройстве заносить, в частности, в Системный Реестр.
  • Драйверы для PnP устройств, которые можно разделить на две категории: WDM и NT драйвера. Последние являются "унаследованными" от NT драйверами, которые опираются на некоторые аспекты PnP архитектуры, но, с другой стороны, не полностью удовлетворяют модели WDM. Например, они могут использовать сервисы PnP Менеджера, чтобы получить информацию о конфигурации, но при этом не обрабатывают IRP пакеты сообщения с кодом IRP_MJ_PNP. Драйверы WDM модели, по определению, полностью соответствуют требованиям взаимодействия по правилам PnP.

Рис. 9.1
Программные PnP компоненты Windows 2000/XP

Роль Системного Реестра

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

  • Драйвер получал список потенциально возможных ресурсов аппаратуры (адреса портов ввода/выводов, используемых каналов прямого доступа к памяти, DMA, и номера прерываний) для каждого устройства, с которым возможно взаимодействие. Путем "подергивания" за каждый потенциально возможный ресурс во время исполнения DriverEntry, драйвер создавал приемлемый объект устройства (вызовом IoCreateDevice).
  • Драйвер полагался на инсталляционную программу, которая методом проб и ошибок или при активном участии пользователя производила приемлемое описание ресурсов и устройств, которыми мог бы управлять драйвер впоследствии. Список таких устройств и их ресурсов сохранялся в Системном Реестре.
  • Операционная система NT выполняла как часть загрузочного процесса (см. Приложение Б) испытание стандартных устройств и ресурсов. Например, параллельные порты представлены обычно по адресам 0x378 или 0x278, что и проверяется в процессе загрузки. Все обнаруженное заносилось в Системный Реестр.

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

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

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

С выходом Windows 95, а затем и Windows 98, и Windows 2000, данная модель была преобразована в обратную. Устройства объявляли о себе сами, либо во время загрузки, либо во время "горячего" подключения (hot plug), таким образом, настаивая на установке соответствующего регистрируемого драйвера. Такой метод получил название "аппаратно-центричного".

Следует отметить, что в настоящий момент пользователь может самостоятельно инициировать установку драйвера для устройств, не поддерживающих PnP (или даже — "как бы" устройств "как бы" не поддерживающих, что было в примере драйвера Example.sys несуществующего устройства, глава 3). Соответствующая информация будет сохранена в Системном Реестре для последующих загрузок — иными словами, старый "драйверо-центричный" механизм сохранен.

Более того, информация о PnP устройствах, однажды обнаруженных системой полностью из Системного Реестра не удаляется, даже если устройство не будет подключено при следующей загрузке системы — система "помнит" обо всех ранее произведенных подключениях и установленных драйверах, что позволяет ей экономить время, если вдруг, после длительного перерыва, пользователь решит использовать это устройство снова (см. раздел HKLM\System\CurrentControlSet\Enum).

Изначально предназначенная для Windows 95, WDM модель поддерживала методологию PnP, что существенно отличало ее от драйверной модели Windows NT. Компания Microsoft настойчиво продвигалась к достижению совместимости драйверных сред, и в результате NT модель была дополнена поддержкой PnP. Родился обобщенный подход к драйверной среде для Windows 98 и Windows NT 5, а новая общая модель получила наименование Windows Driver Model (WDM).


Глава 9. Драйверная модель WDM

[Previous] [Next]

Модель WDM и методология PnP


Глава 9. Драйверная модель WDM

AddDevice — новая процедура в драйверах модели WDM

В драйверах модели WDM функция DriverEntry все еще служит в качестве начальной точки соприкосновения операционной системы с драйвером. Однако теперь обязанности ее сократились. В частности, роль DriverEntry теперь состоит только в том, чтобы "опубликовать" (передать Диспетчеру ввода/вывода соответствующие адреса для вызова функций по адресу) остальные процедуры драйвера. Теперь DriverEntry не создает объект устройства (Device Object) для подконтрольного аппаратного обеспечения.

Обязанности по созданию объекта устройства возложены на новую функцию драйвера AddDevice, которая теперь публикуется (обратите внимание!) в структуре расширения драйвера (Driver Extension) во время работы DriverEntry. Структура расширения драйвера является строго определенной структурой — не следует путать ее с определяемой разработчиком драйвера структурой расширения устройства (Device Extension). Пример публикации AddDevice был закомментирован в теле функции DriverEntry в главе 3. Повторим еще раз:

DriverObject->DriverExtension->AddDevice = MyAddDeviceRoutine;

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

NTSTATUS AddDevice | IRQL == PASSIVE_LEVEL
Параметры | Описание
IN PDRIVER_OBJECT pDriverObject | Указатель на объект данного драйвера
IN PDEVICE_OBJECT pPDO | Указатель на объект физического устройства, созданного родительским (шинным) драйвером
Возвращаемое значение | STATUS_SUCCESS или код ошибки

Основной обязанностью MyAddDeviceRoutine является создание объекта устройства (теперь уже — функционального) с использованием вызова системного IoCreateDevice и, скорее всего, подключение его к объекту физического устройства (вызовом IoAttachDevice), указатель на который поступает в параметре pPDO.

Откуда взялся объект физического устройства, указатель на который получает процедура AddDevice? Его создает шинный драйвер, когда обнаруживает подключенное PnP устройство. Зачем нужно подключаться к шинному драйверу?

Роль драйверных слоев в модели WDM

Драйверная модель WDM построена на организации и манипуляции слоями Объектов Физических устройств (Physical Device Object, PDO) и Объектов Функциональных устройств (Functional Device Object, FDO). Объект PDO создается для каждого физически идентифицируемого элемента аппаратуры, подключенного к шине данных, и подразумевает ответственность за низкоуровневый контроль, достаточно общий для набора функций, реализуемых этим аппаратным элементом. Объект FDO предлагает "олицетворение" каждой логической функции, которую "видит" в устройстве программное обеспечение верхних уровней.

В качестве примера рассмотрим привод жесткого диска и его драйвер. Привод диска может быть представлен объектом PDO, который реализует функции шинного адаптера (присоединяет IDE диск к шине PCI). Как только возникает PDO объект, можно реализовывать объект FDO, который примет на себя выполнение функциональных операций над собственно диском. Обращаясь к FDO, можно будет сделать конкретный функциональный запрос к диску, например, чтение или запись сектора. Однако FDO может выбрать и передачу без модификации конкретного запроса своим партнерам по обслуживанию данного устройства (например, сообщение о снижении напряжения питания).

В действительности, роль PDO объектов быстро усложняется и становится рекурсивной. Например, USB хост-контроллер начинает жизнь как физическое устройство, подключенное к шине PCI. Ho вскоре этот хост-контроллер сам начинает выступать в роли шинного драйвера и, по мере обнаружения устройств, подключенных к USB шине, создает свою коллекцию PDO объектов, каждый из которых контролирует собственный FDO объект.

Эта методология в дальнейшем усложняется еще более, поскольку Функциональным Объектам устройств (FDO) разрешается окружать себя Объектами-Фильтрами (filter device objects, FiDO). Соответственно, каждому FiDO объекту сопоставлен драйвер, выполняющий определенную работу (иначе — зачем их создавать?). Эти фильтрующие объекты верхнего и нижнего уровня могут существовать в любом количестве. Назначение их в том, чтобы модифицировать или обогатить процесс обработки запросов ввода/вывода возможностью использования всего результирующего стека объектов устройств. Следует отметить, что FDO и FiDO объекты отличаются только в смысловом отношении — FDO объект и его драйвер являются главной персоной, FiDO объекты и их драйверы являются вспомогательными (вплоть до того, что предпочитают не иметь собственных имен).

Для того чтобы сделать различие между FDO объектами, которые представляют аппаратные шины, и FDO объектами, которые аппаратные шины не представляют, в документации DDK используются термины шинные FDO (bus FDO) и не-шинные FDO (nonbus FDO). Первые реализуют обязанности драйвера по перечислению (enumerating) всех устройств, подключенных к шине. Такой шинный FDO объект затем создает новые PDO объекты для каждого из подключенных к шине устройств.

Добавляет проблем тот факт, что существует лишь небольшая смысловая разница между не-шинным FDO и фильтрующим объектом устройства (filter device object). C точки зрения Менеджера PnP, все объекты устройств позиционируют себя в стеке устройств (device stack), a тот факт, что некоторые устройства считают себя более чем просто объектами-фильтрами, кажется ему малозначительным.

Последовательность в стеке устройств показана на рисунке 9.2. Различия между шинными и не-шинными FDO отражены на рисунке 9.3.

Рис. 9.2
Стек устройств

Понимание концепции стека устройств важно для того, чтобы правильно описать, когда вызывается функция AddDevice конкретного устройства. Общий алгоритм, используемый для загрузки драйверов и вовлечения в работу функции MyAddDeviceRoutine, описывается следующей последовательностью:

  1. Во время инсталляции операционной системы, операционная система обнаруживает и составляет список (enumerate) всех шин в Системном Реестре (System Registry). Кроме того, детектируется и регистрируется топология и межсоединения этих шин.
  2. Во время процесса загрузки производится загрузка шинного драйвера для каждой известной системе шины. Как правило, Microsoft поставляет все шинные драйверы, однако могут быть установлены и специализированные драйвера для патентованных шин данных.
  3. Одна из первоочередных задач шинного драйвера состоит в том, чтобы составить перечень (enumerate) всех устройств, подключенных к шине. Объект PDO создается для каждого обнаруженного устройства.
  4. Для каждого обнаруженного устройства в Системном Реестре определен класс устройств (class of device), который определяет верхний и нижний фильтры, если таковые имеются, так же, как и драйвер для FDO.
  5. В случае если фильтрующий драйвер или FDO драйвер еще не загружены, система выполняет загрузку и вызывает DriverEntry.
  6. Функция AddDevice вызывается для каждого FDO, которая, в свою очередь, вызывает IoCreateDevice и IoAttachDeviceToDeviceStack, обеспечивая построение стека устройств (device stack).
Рис. 9.3
Шинные и не-шинные FDO

Функция IoAttachDeviceToStack вызывается из AddDevice для того, чтобы разместить FDO в вершине (на текущий момент) стека устройств. Прототип функции IoAttachDeviceToStack описывается в таблице 9.2.

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

PDEVICE_OBJECT IoAttachDeviceToDeviceStack | IRQL == PASSIVE_LEVEL
Параметры | Выполняет подключение вновь созданного объекта устройства, pNewDevice, к стеку устройств
IN PDEVICE_OBJECT pNewDevice | Указатель на подключаемый к стеку объект (созданный в данном драйвере)
IN PDEVICE_OBJECT pOldDevice | Указатель на объект устройства, к которому подключается новое устройство
Возвращаемое значение | • Указатель на устройство, бывшее на вершине стека до данного вызова • NULL (в случае ошибки, например, если драйвер целевого устройства еще не загружен)

Возвращаемый вызовом IoAttachDeviceToDeviceStack указатель может отличаться от переданного значения pOldDevice, например, по той причине, что над объектом устройства pOldDevice уже размещены объекты устройств, предоставленные фильтр-драйверами.

Как видно из таблицы 9.2, для подключения данного объекта устройства (по указателю pNewDevice) необходимо владеть указателем на целевой объект устройства (pOldDevice). Прекрасна ситуация, когда драйвер подключает свой объект устройства к родительскому объекту устройства (шинного драйвера), указатель на который поступает в процедуру AddDevice при вызове через заголовок (pPDO, см. таблицу 9.1). Но что делать, если имеется желание подключить новый объект устройства к объекту устройства другого драйвера, отличающегося от pPDO? (Заметим, что подключение к стеку устройств не есть исключительное право процедуры AddDevice драйверов WDM модели — это могут делать и драйверы "в-стиле-NT", правда, к результатам такой операции следует относиться критически — по причинам, о которых ниже.)

При подключении драйвера к произвольному объекту устройства можно поступить двумя способами. Во-первых, если известно имя нужного устройства, можно получить указатель на искомый объект устройства, воспользовавшись предварительно вызовом IoGetDeviceObjectPointer (см. таблицу 9.3). Полученный указатель на искомый объект устройства (возвращаемый по адресу ppDevObj), можно применить в вызове IoAttachDeviceToDeviceStack, описанном выше.

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

NTSTATUS IoGetDeviceObjectPointer | IRQL == PASSIVE_LEVEL
Параметры | Получает указатель на объект устройства по имени устройства
IN PUNICODE_STRING DeviceName | Имя устройства
IN ACCESS_MASK Access | Маска доступа: FILE_READ_DATA, FILE_WRITE_DATA или FILE_ALL_ACCESS
OUT PFILE_OBJECT *ppFileObj | Указатель на файловый объект, которым представлен искомый объект устройства для кода пользовательского режима
OUT PDEVICE_OBJECT *ppDevObj | Указатель на искомый объект устройства
Возвращаемое значение | • STATUS_SUCCESS • STATUS_Xxx — код ошибки

Вызов IoGetDeviceObjectPointer может быть интересен и тем, что драйвер мог бы адресовать IRP запросы непосредственно искомому объекту устройства (при помощи IoCallDriver), создавая IRP пакеты с размером стека StackSize+1, где значение StackSize получено из найденного объекта устройства.

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

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

NTSTATUS IoAttachDevice | IRQL == PASSIVE_LEVEL
Параметры | Выполняет подключение вновь созданного объекта устройства, pNewDevice
IN PDEVICE_OBJECT pNewDevice | Указатель на подключаемый объект устройства
IN PUNICODE_STRING TagDevName | Имя целевого устройства
OUT PDEVICE_OBJECT *ppTagDevice | Указатель на объект устройства, к которому подключается новое устройство (точнее, указатель на место для указателя)
Возвращаемое значение | • STATUS_SUCCESS • STATUS_Xxx — код ошибки

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

В результате вызовов IoAttachDeviceToDeviceStack или IoAttachDevice будет найден объект устройства, находящийся на вершине стека над указанным целевым объектом (по имени или по указателю). К нему и будет подключен новый объект устройства. Соответственно, разработчик, подключающий свой объект устройства к устройству в "середине" стека и надеющийся, что таким образом через его драйвер будут "протекать" IRP запросы от вышестоящих драйверов к нижестоящим, глубоко заблуждается. На самом деле, для достижения этой цели необходимо не просто выполнить подключение к нужному объекту устройства, но и сделать это в строго определенный момент загрузки — ранее, чем будет выполнена загрузка вышестоящих драйверов, чьи запросы предполагается перехватывать. Однако рассмотрение данной проблемы выходит за рамки данной книги.

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

Заключительной задачей функции AddDevice драйверов модели WDM является создание символьного имени-ссылки (symbolic link name), если это необходимо, для вновь созданных и доступных устройств. Для этого используется вызов IoCreateSymbolicLink, применение которого было продемонстрировано ранее в DriverEntry, глава 3.

Новые рабочие процедуры в WDM драйверах

Процедура AddDevice, вызываемая PnP Менеджером, только лишь производит инициализацию объекта устройства и, если необходимо, структуры данных расширения объекта устройства. В процедуре AddDevice, по правилам хорошего тона WDM модели, действия над собственно аппаратурой не должны совершаться. Но тогда остаются нерешенными две важные задачи:

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

Все это должен сделать драйвер по получении IRP пакета с кодом IRP_MJ_PNP. Такие IRP пакеты посылается PnP Менеджером, когда происходят события включения или выключения устройства, либо возникают вопросы по конфигурированию устройства.

Категория IRP_MJ_PNP пакетов включает запросы широкого спектра, которые детализируются суб-кодами IRP_MN_Xxx. Поскольку они пропускается через единственную рабочую процедуру, то ее обязанностью является вторичная диспетчеризация по этим суб-кодам, содержащимся в IRP пакете и описывающим специфические действия, осуществления которых ожидает PnP Менеджер.

Регистрация новой для WDM модели рабочей процедуры, которой будет поручено обрабатывать запросы IRP_MJ_PNP со всеми подтипами IRP_MN_Xxx, производится традиционным образом в процедуре DriverEntry:

pDriverObj->MajorFunction[IRP_MJ_PNP] = MyPnP_Handler;

Пример программного кода для осуществления вторичной диспетчеризации на основе суб-кодов IRP_MN_Xxx приводится ниже.

NTSTATUS MyPnP_Handler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    // Получить указатель на текущую ячейку стека IRP пакета
    PIO_STACK_LOCATION pIrpStackLocation = IoGetCurrentIrpStackLocation( pIrp );

    switch (pIrpStackLocation ->MinorFunction) {
    case IRP_MN_START_DEVICE: . . .
    // Внимание. Все ветви оператора switch должны возвратить
    // результаты обработки
    . . .
    default:
    // если не поддерживается здесь, то передать запрос вниз:
    IoSkipCurrentIrpStackLocation(pIrp);
    return IoCallDriver(. . ., pIrp);
    }
}

Первым параметром вызова IoCallDriver (см. таблицу 9.5), разумеется, является указатель на объект устройства, к которому было произведено подключение в процедуре AddDevice.

Вызов IoSkipCurrentIrpStackLocation (см. таблицу 9.6) сообщает Диспетчеру ввода/вывода, что драйвер отказывается от дальнейшего участия в судьбе данного IRP пакета. В том случае, если драйвер желает получить управление над IRP пакетом в момент, когда его обработка нижними слоями драйверов будет завершена, то он должен воспользоваться системным вызовом IoCopyCurrentIrpStackLocationToNext (см. таблицу 9.7) и зарегистрировать процедуру CompletionRoutine. Она будет вызвана в соответствующий момент.

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

NTSTATUS IoCallDriver | IRQL <= DISPATCH_LEVEL
Параметры | Обращается к другому драйверу с запросом, сформулированным в пакете IRP (запросы типа IRP_MJ_POWER следует выполнять при помощи вызова PoCallDriver)
IN PDEVICE_OBJECT pDevObj | Указатель на объект устройства, которому адресован IRP запрос
IN PIRP pIrp | Указатель на отправляемый IRP пакет
Возвращаемое значение | • STATUS_SUCCESS • STATUS_PENDING — в случае, если пакет требует дополнительной обработки • STATUS_Xxx — в случае ошибки

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

VOID IoSkipCurrentIrpStackLocation | IRQL <= DISPATCH_LEVEL
Параметры | Изменяет указатель стека IRP так, что нижестоящий драйвер будет считать текущую ячейку стека IRP своей
IN PIRP pIrp | Указатель на модифицируемый IRP пакет
Возвращаемое значение | void

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

VOID IoCopyCurrentIrpStackLocationToNext | IRQL <= DISPATCH_LEVEL
Параметры | Копирует содержимое ячейки стека IRP для текущего драйвера в ячейку стека для нижестоящего драйвера
IN PIRP pIrp | Указатель на модифицируемый IRP пакет
Возвращаемое значение | void

Процедура завершения ввода/вывода CompletionRoutine есть обратный вызов от Диспетчера ввода/вывода, который позволяет перехватить IRP пакет после того, как низкоуровневый драйвер завершит его обработку. Процедура завершения ввода/вывода регистрируется вызовом IoSetCompletionRoutine (см. таблицу 9.8).

Таблица 9.8. Прототип макроопределения IoSetCompletionRoutine

VOID IoSetCompletionRoutine | IRQL <= DISPATCH_LEVEL
Параметры | Выполняет регистрацию callback-функции завершения обработки IRP пакета
IN PIRP pIrp | Указатель на отслеживаемый IRP пакет
IN PIO_COMPLETE_ROUTINE CompletionRoutine | Функция, которая должна получить управление, когда обработка IRP будет завершена
IN PVOID pContext | Параметр, который получит регистрируемая callback функция CompletionRoutine
IN BOOLEAN doCallOnSuccess | Вызывать CompletionRoutine в случае успешного завершения обработки данного IRP пакета
IN BOOLEAN doCallOnError | Вызывать CompletionRoutine в случае завершения обработки данного IRP с ошибкой
IN BOOLEAN doCallOnCancel | Вызывать CompletionRoutine в случае прерванной обработки данного IRP пакета
Возвращаемое значение | void

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

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

Таблица 9.9. Прототип функции завершения ввода/вывода CompletionRoutine

NTSTATUS CompletionRoutine | IRQL == см. текст ниже
Параметры | Перехватывает пакет IRP после завершения работы нижнего драйверного слоя
IN PDEVICE_OBJECT pDevObj | Объект устройства (в составе данного драйвера), которому был ранее адресован данный IRP пакет
IN PIRP pIrp | Указатель на IRP пакет, обработка которого только что завершена
IN PVOID pContext | Аргумент, указанный в IoSetCompleteRoutine
Возвращаемое значение | STATUS_MORE_PROCESSING_REQUIRED STATUS_SUCCESS

Однозначно предсказать, на каком уровне IRQL выполняется процедура завершения, невозможно. В том случае, если нижележащий драйвер, вызывает IoCompleteRequest (подробности — ниже) с уровня IRQL равного PASSIVE_LEVEL, то процедура завершения находящегося выше драйвера выполняется на уровне PASSIVE_LEVEL. В случае, если лежащий ниже драйвер завершает обработку IRP пакета на уровне DIPATCH_LEVEL (например, из DPC процедуры), то и процедура завершения лежащего выше драйвера выполняется на уровне DISPATCH_LEVEL.

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

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

VOID IoCompleteRequest | IRQL <= DISPATCH_LEVEL
Параметры | Вызывается, когда драйвер желает полностью завершить обработку данного IRP пакета. Обеспечивает вызов процедур завершения всех драйверов, имеющихся над данным (см. ниже)
IN PIRP pIrp | Указатель на текущий IRP пакет, обработка которого только что завершена
IN CCHAR PriorBoost | Величина, на которую следует изменить приоритет потока, выполняющего обработку данного IRP пакета. Величина IO_NO_INCREMENT используется, если никаких изменений делать не нужно.
Возвращаемое значение | void

Чтобы устранить упомянутую выше неоднозначность уровня IRQL работы процедуры завершения, можно прибегнуть к следующей уловке. Предположим, что мы имеем программный код рабочей процедуры. Известно также, что PnP Менеджер (как, впрочем, и Диспетчер ввода/вывода) всегда выполняет вызов рабочей процедуры драйвера на уровне PASSIVE_LEVEL. Тогда, отправляя пакет IRP нижним слоям драйвера, организуем ожидание (средствами объекта события режима ядра) не выходя из кода данной рабочей процедуры, пока отосланный нижним слоям IRP пакет не возвратится в зарегистрированную функцию CompletionRoutine. Как только это произойдет, объект события кодом функции CompletionRoutine будет переведен в сигнальное состояние, и стадия ожидания в основном потоке завершится. Таким образом, мы получим сигнал о завершении обработки пакета IRP на вполне определенном уровне IRQL, равном именно PASSIVE_LEVEL. Полностью данный метод описывается в примере ниже:

. . . . . .
// код рабочей процедуры, выполняющийся на уровне PASSIVE_LEVEL
. . . . . .
IoCopyCurrentIrpStackLocationToNext(pIrp);

// Резервируем место под объект события:
KEVENT myEvent;
// Инициализируем его, состояние не сигнальное:
KeInitializeEvent( &myEvent, NotificationEvent, FALSE );
// Регистрируем свою процедуру завершения обработки IRP пакета.
// Указатель на объект myEvent передаем как дополнительный параметр.
IoSetCompletionRoutine( pIrp,
                        MyCompleteRoutine,
                        (PVOID)&myEvent,
                        TRUE, TRUE, TRUE);

// Предположим, что указатель на объект устройства,
// к которому был подключен текущий объект устройства, был ранее
// сохранен в структуре расширения текущего объекта устройства.
PDEVICE_EXTENSION pDeviceExtension = (PDEVICE EXTENSION) pDeviceObject->DeviceExtension;
PDEVICE_OBJECT pUnderlyingDevObj = pDeviceExtension->pLowerDevice;

// Отправляем IRP пакет на обработку нижними драйверными слоями
IoCallDriver( pUnderlyingDevObj, pIrp );

// Организуем ожидание, пока не закончится работа на нижних уровнях
KeWaitForSingleObject( &myEvent,
                       Execute,
                       KernelMode,
                       FALSE,
                       NULL);

// Теперь завершаем обработку IRP пакета.
// Его адрес не изменился - pIrp.
// По "возвращении" из "ожидания" уровень IRQL остался прежним для
// данного потока.
// Поскольку Диспетчер ввода/вывода и PnP Менеджер вызывают
// рабочие процедуры драйвера на уровне PASSIVE_LEVEL, то таким
// он в данном потоке и остался.
. . .
}
NTSTATUS MyCompleteRoutine( IN PDEVICE_OBJECT pDevObj,
                            IN PIRP pIrp,
                            IN PVOID pContextArgument )
{
// Вычисляем указатель на Объект События:
PEVENT pEvent = (PEVENT) pContextArgument;
// Устанавливаем его в сигнальное состояние
KeSetEvent( pEvent, 0, FALSE );    // IRQL <=DISPATCH_LEVEL

// Пакет IRP получен. Завершение работы здесь. Но не окончательно.
return STATUS_MORE_PROCESSING_REQUIRED;
}

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

Когда код IoCompleteRequest благополучно достигает в своем рассмотрении вершины стека, то Диспетчер ввода/вывода выполняет действия по освобождению данного IRP пакета (наряду с некоторыми другими операциями).

Отсюда несколько важных следствий.

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

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

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

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


Глава 9. Драйверная модель WDM

Ограничения, накладываемые на WDM драйверы спецификацией PnP

Для того чтобы соответствовать драйверной модели WDM, драйвер обязан поддерживать обработку специфичных PnP IRP пакетов, каких конкретно — это определяется конкретным типом объекта устройства — не-шинный FDO, шинный FDO и PDO. Во всяком случае, IRP пакеты с приведенными в таблице кодами IRP_MN_Xxx должны поддерживаться драйверами всех типов.

Таблица 9.11. Суб-коды IRP_MN_Xxx

IRP_MN_Xxx | Значение
IRP_MN_START_DEVICE | (Ре)Инициализация устройства с заданными ресурсами
IRP_MN_QUERY_STOP_DEVICE | Осуществима ли остановка устройства для возможного переопределения ресурсов?
IRP_MN_STOP_DEVICE | Остановка устройства с потенциальной возможностью перезапуска или удаления из системы
IRP_MN_CANCEL_STOP_DEVICE | Уведомляет, что предыдущий запрос QUERY_STOP не получит дальнейшего развития
IRP_MN_QUERY_REMOVE_DEVICE | Может ли быть выполнено безопасное удаление устройства в текущий момент?
IRP_MN_REMOVE_DEVICE | Выполнить работу, обратную работе AddDevice
IRP_MN_CANCEL_REMOVE_DEVICE | Уведомляет, что предыдущий запрос QUERY_REMOVE не получит дальнейшего развития
IRP_MN_SURPRISE_REMOVAL | Уведомляет, что устройство было удалено без предварительного предупреждения

Передача PnP IRP пакетов нижним драйверным слоям

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

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

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

Чтобы передать PnP запрос вниз, драйвер помечает IRP пакет как "завершенный" установкой соответствующих значений в полях IoStatus.Status и IoStatus.Information, а затем производит вызовы IoCopyCurrentStackLocationToNext и IoCallDriver. Нижележащий драйвер известен еще при выполнении AddDevice (из вызова IoAttachDeviceToDeviceStack), а указатель на него рекомендуется сохранять в структуре расширении объекта устройства. Пример кода, выполняющего эти действия, приводится ниже.

...
IoCopyCurrentIrpStackLocationToNext( pIrp );
PDEVICE_EXTENSION pThisDeviceExtension =
    (PDEVICE_EXTENSION) pThisDeviceObject->DeviceExtension;
IoCallDriver( pThisDeviceExtension ->pUnderlyingDevice, pIrp );
...

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

NTSTATUS
OnlyTranslateIrpDown(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    IoSkipCurrentIrpStackLocation( pIrp );
    PDEVICE_EXTENSION pDeviceExtension =
        (PDEVICE_EXTENSION) pDeviceObject ->DeviceExtension;
    return IoCallDriver(pThisDeviceExtension->pUnderlyingDevice, pIrp);
}

Бывают случаи, когда драйвер вынужден пропускать вниз PnP запросы раньше, чем он завершает собственную работу над ними. Например, при обработке запроса с кодом IRP_MN_START_DEVICE драйверу, как правило, необходимо дождаться, пока стартуют низкоуровневые драйверы перед началом работы их собственного аппаратного обеспечения. Шина и любое низкоуровневое аппаратное обеспечение инициализируется до старта отдельных устройств. Таким образом, высокоуровневые драйвера должны сначала транслировать вниз запрос и затем дождаться завершения низкоуровневой обработки перед продолжением своей работы.

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

Device Enumeration — всеобщая перепись устройств

Как было сказано ранее, Менеджер конфигурирования PnP ответственен за выполнение переписи устройств, обнаруженных в системе. Стартовало ли устройство при загрузке системы, или оно добавляется/удаляется позже, шинный драйвер отвечает за идентификацию и ведение списка подключенной аппаратуры. Аппаратные ресурсы, необходимые устройству, предоставляются драйверу этого устройства, когда ему отправляется сообщение (IRP пакет) с кодом IRP_MJ_PNP и с суб-кодом IRP_MN_START_DEVICE. Весьма показателен в этом отношении пример из пакета DDK, посвященный драйверам шины и устройств TOASTER.


Глава 9. Драйверная модель WDM

Многослойные драйверы

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

В рамках многослойного подхода можно определить три типа драйверов:

  • Шинные драйверы — обеспечивают интерфейс аппаратных шин в базисе "один слот — одна единица" и создают один или более физических объектов устройств (PDO, Physical Device Object), соответствующих каждому обнаруженному устройству, подключенному к шине. Шинный драйвер конструирует PDO и управляет им, вследствие чего часто его называют физическим драйвером.
  • Функциональные драйверы — обеспечивают чтение, запись и прочую логику функционирования отдельного устройства. Они создают и управляют одним или более функциональными объектами устройств (FDO, Functional Device Object).
  • Фильтр-драйверы — обеспечивают модификацию запроса на ввод/вывод перед предъявлением его драйверам более низких уровней. Фильтры могут быть размещены вокруг функционального драйвера либо над шинным драйвером.


Глава 9. Драйверная модель WDM

Когда следует применять многослойную архитектуру?

Один из самых первых и самых важных вопросов конструирования драйвера состоит в том, следует ли реализовать драйвер в виде набора слоев или он должен быть монолитным?

Доводы "за"

Драйвер, реализованный по многослойной методике, имеет два преимущества. Использование слоев позволяет отделить вопросы использования высокоуровневых протоколов от вопросов, связанных с управлением собственно оборудованием. Это позволяет осуществлять поддержку аппаратуры от разных производителей без переписывания больших объемов кода. Многослойная архитектура позволяет повысить гибкость системы за счет использования при одном драйвере, реализующем протокол, сразу нескольких драйверов аппаратуры, подключаемых непосредственно во время работы. Этот прием реализован в сетевых драйверах Windows NT 5.

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

Создание аппаратных шин USB и IEEE 1394 базируется на многослойном подходе к драйверам именно по перечисленным выше причинам.

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

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

Недостатки многослойной архитектуры

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

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

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


Глава 9. Драйверная модель WDM

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

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

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

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

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

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

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

Диспетчер ввода/вывода конструирует пакеты IRP по запросу драйвера, который тот может осуществить с помощью вызовов:

  • IoBuildAsynchronousFsdRequest
  • IoBuildDeviceIoControlRequest
  • IoBuildSynchonousFsdRequest

Сконструированный пакет IRP содержит столько стековых ячеек, сколько указано в поле StackSize целевого (куда передается этот пакет IRP) объекта устройства. Указатель на целевой объект устройства передается в качестве аргумента этим перечисленным выше функциям. Таким образом, созданные этими функциями пакеты IRP содержат достаточное количество стековых ячеек, для обеспечения вызовов всех нижележащих драйверов, но не содержит ячейки для самого промежуточного драйвера.

В случае, если промежуточный драйвер использует IoAllocateIrp или ExAllocatePool (см. далее в тексте) для создания IRP пакета, то есть создает IRP с нуля — начиная с выделения памяти, то драйвер должен явным образом указать количество ячеек стека в пакете IRP при его создании или учесть их количество при выделении памяти. Общепринято использование поля StackSize в целевом объекте устройства для этой цели.

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

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

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

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

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

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

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


Глава 9. Драйверная модель WDM

Создание IRP пакетов вызовами IoBuild(A)SynchronousFsdRequest

Как уже, наверное, понял читатель, пакеты IRP можно создавать с нуля (обладая только областью памяти достаточного размера), но можно и прибегнуть к помощи рекомендованных системных вызовов IoBuildSynchronousFsdRequest, IoBuildAsynchronousFsdRequest и IoBuildDeviceControlRequest. Первые два вызова предназначены для конструирования IRP пакетов с кодами IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_FLUSH_BUFFERS и IRP_MJ_SHUTDOWN, вполне пригодные для использования во всех драйверах, несмотря на устрашающий суффикс Fsd. Последний из этих вызовов, IoBuildDeviceControlRequest, предназначен для конструирования таких IRP пакетов, как если бы они были инициированы пользовательским API вызовом DeviceIoControl, то есть с кодом IRP_MJ_DEVICE_CONTROL или IRP_MJ_INTERNAL_DEVICE_CONTROL

Таблица 9.13. Описание прототипа функций IoBuild(A)SynchronousFsdRequest

PIRP IoBuildSynchronousFsdRequest PIRP IoBuildAsynchronousFsdRequest | IRQL == PASSIVE_LEVEL IRQL <= DISPATCH_LEVEL
Параметры | Построение IRP пакета (выделение памяти и настройка полей)
IN ULONG MajorFunction | • IRP_MJ_PNP или • IRP_MI_READ или • IRP_MJ_WRITE или • IRP_MJ_FLUSH_BUFFERS или • IRP_MJ_SHUTDOWN
IN PDEVICE_OBJECT pTargetDevice | Объект устройства, которому отдается IRP
IN OUT PVOID pBuffer | Адрес буфера данных ввода/вывода
IN ULONG uLenght | Размер порции данных в байтах
IN PLARGE_INTEGER StartingOffset | Смещение в устройстве, где начинается/продолжается операция ввода/вывода
Только для IoBuildSynchronousFsdRequest: IN PREVENT pEvent | Объект события, используемый для сигнализации об окончании ввода/вывода (должен быть инициализирован к моменту вызова). Объект переходит в сигнальное состояние, когда нижний драйвер завершил обработку данного IRP пакета.
OUT PIO_STATUS_BLOCK Iosb | Для получения завершающего статуса операций ввода/вывода
Возвращаемое значение | • Не NULL — адрес нового пакета IRP • NULL — невозможно создать новый IRP

Число ячеек, создаваемых в стеке ввода/вывода, размещающемся в пакете IRP, равно значению, указанному в поле pTargetDevice->StackSize. В данном случае нет простого способа создать дополнительную ячейку в стеке пакета IRP собственно для самого вызывающего драйвера.

Значения аргументов Buffer, Length и StartingOffset требуются для операций чтения и записи. Для операций flush и shutdown они должны быть установлены равными 0.

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

Здесь общие черты этих двух функций заканчиваются. Начинаются различия.

Как следует из названия функции IoBuildSynchronousFsdRequest, она работает синхронно. Другими словами, поток, который выполняет вызов IoCallDriver, прекращает свою работу до тех пор, пока не завершится операция ввода/вывода в нижних драйверных слоях. Для более удобной реализации такой блокировки, в создаваемый пакет IRP в виде аргумента передается адрес инициализированного объекта события (event object). Затем, после передачи созданного пакета драйверу нижнего уровня (вызовом IoCallDriver) следует использовать функцию KeWaitForSingleObject — для организации ожидания перехода этого объекта синхронизации в сигнальное состояние. Когда драйвер нижнего уровня завершит обработку данного пакета IRP, Диспетчер ввода/вывода переведет данный объект события в сигнальное состояние, что и "разбудит" данный драйвер в нужный момент. Аргумент Iosb позволяет получить информацию о том, как завершилась обработка. Заметим, что, поскольку текущий драйвер узнает о завершении обработки нового IRP пакета от функции KeWaitForSingleObject, то он не должен устанавливать свою процедуру завершения перед тем, как обратиться к нижнему драйверу вызовом IoCallDriver. Если же процедура завершения все-таки установлена, она всегда должна возвращать STATUS_SUCCESS.

Пакеты, созданные функцией IoBuildSynchronousFsdRequest, должны освобождаться только косвенно — в результате вызова IoCompleteRequest после получения сигнала от объекта события, а Диспетчер ввода/вывода уже сам очистит и освободит память, занятую IRP пакетом. Это включает освобождение системных буферных областей или MDL, выделенных для использования в обработке этого IRP. Использовать IoFreeIrp нельзя, так как такой IRP пакет участвует в очереди, организованной для пакетов, ассоциированных с данным программным потоком. Применение к нему вызова IoFreeIrp ранее, чем он будет удален из данной очереди, приведет к краху системы. Кроме того, во избежание неприятностей, следует следить за тем, чтобы объект события существовал к моменту, когда Диспетчер ввода/вывода соберется перевести его в сигнальное состояние.

Соответственно, фрагмент кода, который создает синхронный IRP и адресует его объекту устройства pTargetDeviceObject в нижнем драйвере, мог бы выглядеть следующим образом:

PIRP pIrp;
KEVENT Event;
IO_STATUS_BLOCK iosb;
KeInitializeEvent(&Event, NotificationEvent, FALSE);
pIrp = IoBuildSynchronousFsdRequest(IRP_NJ_Xxx,
                                    pTargetDeviceObject, . . .
                                    &Event,
                                    &iosb);
status = IoCallDriver(pTargetDeviceObject, pIrp);
if( status == STATUS_PENDING )
{ // Ожидаем окончания обработки в нижних слоях
    KeWaitForSingleObject(&Event, Executive, KErnelMode, FALSE,NULL);
    status = iosb.Status;
}
. . .

В отличие от пакетов IRP, производимых по запросу синхронной версии, функция IoBuildAsynchronousFsdRequest конструирует пакеты, которые не освобождаются автоматически по окончании работы над ним в нижнем драйвере. Вместо этого, драйвер, создающий "асинхронный" пакет IRP должен обязательно подключить свою процедуру завершения, которая и должна выполнять вызов IoFreeIrp. Процедура завершения и должна выполнить очистку IRP с освобождением выделенных ему системных буферных областей или MDL, а затем и освобождения памяти, занятой под структуру самого IRP пакета. В данном случае, процедура завершения должна возвратить значение STATUS_MORE_PROCESSING_REQUIRED. Соответствующий пример кода может выглядеть следующим образом:

. . . . .
PIRP pIrp;
IO_STATUS_BLOCK iosb;
pIrp = IoBuildAsynchronousFsdRequest(IRP_NJ_Xxx,
                                     pTargetDeviceObject, . . .
                                     &iosb);
IoSetCompletionRoutine( pIrp,
    (PIO_COMPLETION_ROUTINE) MyCompletionRoutine,
    pThisDevExtension,
    TRUE,TRUE,TRUE);
//Чтобы целевое устройство не "растворилось" за время обработки IRP:
ObReferenceObject(pTargetDeviceObject);
status = IoCallDriver(pTargetDeviceObject, pIrp);
ObDereferenceObject(pTargetDeviceObject);
. . . . .
// Процедура завершения, зарегистрированная ранее
NTSTATUS MyCompletionRoutine( PDEVICE_OBJECT pThisDevice,
                              PIRP pIrp,
                              VOID pContext )
{
    // Действия по очистке IRP
    . . . .
    IoFreeIrp( pIrp );
    return STATUS_MORE_PROCESSING_REQUIRED;
}

Драйверы, которые реализуют блокирующийся механизм работы (как это получается при синхронизации по объекту события), могут привести к деградации системы. Такое может случиться, если они будут выполнять вызовы IoCallDriver с повышенных уровней IRQL. В этом случае они могут остановиться на неопределенно долгое время, ожидая отклика с нижних уровней. Это противоречит общей философии построения Windows NT 5. Видимо, поэтому разработчики Windows искусственно затруднили построение синхронных IRP пакетов на повышенных уровнях IRQL тем, что вызов IoBuildSynchronousFsdRequest можно сделать только с уровня IRQL, равного PASSIVE_LEVEL.

Кроме того, объект события, используемый для организации ожидания, когда же закончится обработка пакета IRP на нижних уровнях, должен использоваться с максимальной осторожностью, поскольку при использовании такого драйвера в многопоточной манере могут возникнуть сложные ситуации. Допустим, два программных потока одного и того же пользовательского процесса делают запрос на запись с использованием одного и того же дескриптора (иными словами, делают запрос к одному и тому же драйверу). Тогда рабочая процедура WriteRequestHandler выполняется в контексте первого потока и останавливается в том месте, где она желает дождаться сигнала от объекта события. Затем, та же самая процедура WriteRequestHandler, выполняемая в контексте другого потока, использует повторно тот же самый объект события для обработки другого запроса. Когда запускаются оба потока, то ни один из них не может быть уверен, чей же конкретно пакет IRP обработан, поскольку при окончании обработки любого из IRP с одинаковым успехом возникает сигнал от объекта события. Решение может состоять в том, чтобы подстраховать объект события при помощи быстрого мьютекса или даже создавать новые объекты события для каждого вновь конструируемого IRP пакета.

Создание IRP пакетов вызовом IoBuildDeviceIoControlRequest

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

Таблица 9.14. Описание прототипа функции IoBuildDeviceIoControlRequest

PIRP IoBuildDeviceIoControlRequest | IRQL == PASSIVE_LEVEL
Параметры | Формирует IRP пакет (с выделением памяти), описывающий обращение с IOCTL запросом
IN ULONG IoControlCode | Код IOCTL, принимаемый (допускаемый) к обработке целевым устройством
IN PDEVICE_OBJECT pTargetDevice | Объект устройства, которому предназначен формируемый пакет IRP
IN PVOID pInputBuffer | Адрес буфера ввода/вывода, передаваемого драйверу нижнего уровня
IN ULONG inputLenght | Длина буфера pInputBuffer в байтах
OUT PVOID pOutputBuffer | Адрес буфера ввода/вывода для данных, возвращаемых драйвером нижнего уровня
IN ULONG outputLenght | Длина буфера pOutputBuffer в байтах
IN BOOLEAN InternalDeviceIoControl | TRUE — буден сформирован IRP пакет с кодом IRP_MJ_INTERNAL_DEVICE_CONTROL FALSE — с кодом IRP_MJ_DEVICE_CONTROL
IN PREVENT pEvent | Объект события (event object), используемый для сообщения об окончании ввода/вывода
OUT PIO_STATUS_BLOCK pIosb | Для получения завершающего статуса операций ввода/вывода
Возвращаемое значение | • Адрес нового пакета IRP либо • NULL — невозможно создать новый IRP

Следует также отметить, что этот вызов может конструировать IRP как с синхронным способом обработки, так и асинхронным. Для получения "синхронного" IRP в функцию необходимо просто передать адрес инициализированного объекта события. После того как IRP пакет будет передан нижнему драйверу вызовом IoCallDriver, следует использовать KeWaitForSingleObject для организации ожидания сигнала от этого объекта события. Когда драйвер нижнего уровня завершит обработку IRP, Диспетчер ввода/вывода переведет объект события в "сигнальное" состояние, и в результате будет разбужен драйвер, который "организовал" весь этот процесс. Блок данных по указателю pIosb сообщает об окончательном состоянии пакета IRP. Так же, как и в случае с IoBuildSynchronousFsdRequest, следует аккуратнее работать в многопоточном режиме.

Диспетчер ввода/вывода автоматически выполняет очистку и освобождение IRP пакетов, созданных по вызову IoBuildDeviceIoControlRequest по завершении их обработки, включая подключенные к этому пакету системные буферные области или MDL. Для запуска такой очистки драйвер должен просто сделать вызов IoCompleteRequest.

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

Метод буферизации, который указан в IOCTL коде, влияет на формирование IRP пакета. В том случае, если IOCTL код описан как METHOD_BUFFERED, внутри вызова IoBuildDeviceIoControlRequest выполняется выделение области нестраничной памяти, куда производится копирование содержимого буфера по адресу pInputBuffer. Когда обработка IRP завершается, содержимое буфера в нестраничном пуле автоматически копируется в область памяти по адресу pOutputBuffer.

В случае, если IOCTL код содержит флаги METHOD_OUT_DIRECY или METHOD_IN_DIRECT, то IoBuildDeviceIoControlRequest всегда выполняет построение MDL списка для буфера pOutputBuffer и всегда использует буфер в нестраничной памяти для буфера pInputBuffer, независимо от того, указан ли METHOD_IN_DIRECT или METHOD_OUT_DIRECT. В общем-то, формирование IRP пакета в обоих случаях происходит совершенно аналогично тому, как если бы в Win32 обрабатывался вызов DeviceIoControl, поступивший из приложения пользовательского режима.

Создание IRP пакетов "с нуля"

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

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

Попробуем отказаться и от этой услуги Диспетчера ввода/вывода и создать пакет IRP "совершенно с нуля" на примере IRP пакета для буферизованного ввода/вывода.

В данном случае память под IRP пакет выделяется в нестраничном пуле при помощи вызова ExAllocatePool, а затем производится инициализация необходимых полей внутри созданной области. Общая инициализация выделенной области по типу "IRP пакет" должна быть выполнена при помощи вызова IoInitializeIrp. Установка полей в той ячейке стека IRP пакета, которую будет разбирать драйвер-получатель (владеющий устройством pTargetDevice), и буферных областей для передачи данных возлагается на текущий драйвер.

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

. . . . .
#define BUFFER_SIZE (1024)
CCHAR nOfRequiredStackLocs = pTargetDevice->StackSize;
USHORT irpSize = IoSizeOfIrp(nOfRequiredStackLocs);
PIO_STACK_LOCATION pTagDevIrpStackLocation;

PIRP pCreatedIrp = (PIRP) ExAllocatePool( NonPagedPool, irpSize );
IoInitializeIrp( pCreatedIrp, irpSize, nOfRequiredStackLocs);

// Получаем указатель на ячейку стека IRP, которая после вызова
// IoCallDriver будет ассоциирована с нижним драйвером:
pTagDevIrpStackLocation = IoGetNextIrpStackLocation( pCreatedIrp );

// Подразумевая операцию чтения, устанавливаем поля ячейки:
pTagDevIrpStackLocation->MajorFunction = IRP_MJ_READ;
pTagDevIrpStackLocation->Parameters.Read.Length = BUFFER_SIZE;
pTagDevIrpStackLocation->Parameters.Read.ByteOffset.QuadPart = 0i64;

// B запросе IRP_MJ_READ список MDL не может использоваться.
// Передаем собственный буфер в качестве системного,
// требующегося при данном типе запросов:
PVOID newBuffer = ExAllocatePool ( NonPagedPool, BUFFER_SIZE );
pCreatedIrp -> AssociatedIrp.SystemBuffer = newBuffer;

// Если вызываемое устройство имеет свойство (флаг) DO_DIRECT_IO:
if( pTargetDevice->Flags & DO_DIRECT_IO )
{
    // Описание IoAllocateMdl см. в таблице 7.19. Поскольку третий
    // параметр равен FALSE, указатель на созданный MDL список будет
    // сразу занесен в поле pCreatedIrp-> MdlAddress
    PMDL pNewMdl = IoAllocateMdl ( newBuffer,
                                  BUFFER_SIZE,
                                  FALSE, FALSE,
                                  pCreatedIrp);

    // для буфера в нестраничной памяти:
    MmBuildMdlForNonPagedPool( pNewMdl);
}

// Копируем информацию о потоке инициатора вызова:
pCreatedIrp -> Tail.Overlay.Thread =
pOriginalIrp -> Tail.Overlay.Thread;

// Устанавливаем процедуру завершения обработки сформированного IRP
IoSetCompletionRoutine ( pCreatedIrp,
                         MyIoCompletionRoutine,
                         NULL, TRUE, TRUE, TRUE );

// Передаем созданный пакет драйверу нижнего уровня
IoCallDriver ( pTargetDevice, pCreatedIrp );
. . . .

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

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

NTSTATUS MyIoCompletionRoutine(IN PDEVICE_OBJECT pThisDeviceObject,
                               IN PIRP  pIrp,
                               IN PVOID pContext )
{
. . .
// Очистка структуры MDL списка:
IoFreeMdl( pIrp->MdlAddress );

// Освобождение специального буфера:
IoFreePool ( pIrp->AssociatedIrp.SystemBuffer );

// Освобождение собственно IRP:
IoFreeIrp ( pIrp );

return STATUS_MORE_PROCESSING_REQUIRED;
}

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

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

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

Ниже приводятся описания прототипов использованных функций.

Таблица 9.15. Описание прототипа функции IoAllocateIrp

PIRP IoAllocateIrp | IRQL <= DISPATCH_LEVEL
Параметры | Формирует IRP пакет с выделением памяти (не требует последующего вызова IoInitializeIRP)
IN CCHAR StackSize | Количество ячеек стека во вновь создаваемом IRP пакете
IN BOOLEAN ChargeQuota | FALSE
Возвращаемое значение | Адрес нового пакета IRP либо NULL — невозможно создать новый IRP

Таблица 9.16. Описание прототипа функции IoInitializeIrp

VOID IoInitializeIrp | IRQL <= DISPATCH_LEVEL
Параметры | Формирует IRP пакет в ранее выделенной области памяти (не должна использоваться для пакетов, созданных вызовом IoAllocateIrp)
IN PIRP pIrp | Указатель на область, используемую под IRP
IN USHORT PacketSize | Заранее вычисленный общий размер IRP пакета (можно использовать вызов IoSizeOfIrp)
IN CCHAR StackSize | Количество ячеек стека во вновь создаваемом IRP пакете
Возвращаемое значение | void

Таблица 9.17. Описание прототипа функции IoFreeIrp

VOID IoFreeIrp | IRQL <= DISPATCH_LEVEL
Параметры | Очищает и освобождает IRP пакеты, созданные вызовами IoAllocateIrp или IoBuildAsynchronousFsdRequest
IN PIRP pIrp | Указатель на освобождаемый IRP пакет
Возвращаемое значение | void

Как было сказано ранее, пакеты, созданные IoBuildSynchronousFsdRequest или IoBuildDeviceIoControlRequest, освобождаются самим Диспетчером ввода/вывода, когда драйвер завершает обработку такого пакета вызовом IoCompleteRequest. Освобождения пакетов, сделанных нестандартными способами (например, с помощью ExAllocatePool) выполняет сам драйвер.

Таблица 9.18. Описание прототипа функции IoSizeOfIrp

USHORT IoSizeOfIrp | IRQL — любой
Параметры | Определяет размер IRP пакета, как если бы он имел StackSize ячеек стека
IN CCHAR StackSize | Предполагаемое число ячеек стека IRP пакета
Возвращаемое значение | Размер в байтах


Глава 9. Драйверная модель WDM

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

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

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

  1. Выполняет вызовы IoBuildSynchronousFsdRequest или вызовы IoBuildDeviceIoControlRequest для того, чтобы создать необходимое количество IRP пакетов "синхронного" типа.
  2. Выполняет вызовы IoCallDriver для передачи всех созданных драйвером пакетов IRP другим драйверам.
  3. Выполняет вызовы KeWaitForMultipleObjects и ожидает завершения обработки всех переданных IRP пакетов.
  4. Выполняет действия по переносу информации из полученных пакетов и их последующую очистку и освобождение.
  5. Наконец, выполняет вызов IoCompleteRequest относительно исходного IRP пакета для того, чтобы возвратить его инициатору вызова.

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

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

  1. Пометить пакет IRP, поступивший в рабочую процедуру от Диспетчера ввода/вывода как ожидающий обработки при помощи IoMarkPending.
  2. Создать дополнительные пакеты IRP с использованием одного из описанных выше методов.
  3. Подключить процедуру завершения (возможно — одну и ту же) к каждому из вновь созданных IRP пакетов вызовом IoSetCompletionRoutine. При выполнении этого вызова следует передать указатель на исходный IRP пакет в аргументе pContext.
  4. Запомнить число созданных пакетов IRP в неиспользуемом поле исходного IRP пакета. Поле Parameters.Key текущей ячейки стека IRP пакета вполне годится.
  5. Передать пакеты всем нужным драйверам вызовом IoCallDriver.
  6. Возвратить значение STATUS_PENDING, поскольку обработка исходного запроса (пакета IRP) не завершена.

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

  1. Выполняет необходимый перенос информации, очистку и удаление созданного драйвером IRP пакета, вернувшегося от нижнего драйвера.
  2. Уменьшает на единицу сохраненное ранее число незавершенных пакетов IRP. Это действие рекомендуется выполнять, приняв хотя бы минимальные меры по безопасному доступу к этому значению. Вполне подходит для этой цели вызов InterlockedDecrement.
  3. В случае, если незавершенных пакетов не осталось, выполняет вызов IoCompleteRequest, что сигнализирует о полном завершении обработки исходного IRP запроса.
  4. Возвращает управление Диспетчеру ввода/вывода с кодом завершения STATUS_MORE_PROCESSING_REQUIRED — для того, чтобы не допустить вызов процедур завершения вышестоящих драйверов для работы над пришедшим "снизу" IRP пакетом, созданным данным драйвером. Кстати заметить, к этому моменту рассматриваемый IRP пакет уже уничтожен.

Удаление IRP пакетов

Как бывает и в реальной жизни, кто-то, инициировавший IRP запрос, может передумать и инициализировать снятие запроса "с повестки". Пользовательское приложение может запросить уничтожение пакета после длительного ожидания. Приложение может вовсе прекратить работу, бросив все на попечение операционной системы. Наконец, приложение может попытаться завершить свою асинхронную операцию Win32 API вызовом CancelIo.

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

BOOLEAN IoCancelIrp | IRQL <= DISPATCH_LEVEL
Параметры | Помечает пакет IRP как требующий удаления и вызывает процедуры удаления, если таковые определены
IN PIRP pIrp | Указатель на удаляемый IRP пакет
Возвращаемое значение | TRUE — если пакет удален FALSE — в случае неудачи

В режиме ядра для удаления запроса выполняется вызов IoCancelIrp (таблица 9.19). Операционная система также вызывает IoCancelIrp для всех IRP пакетов, относящихся к потоку, выполнение которого прекращается.

Предположим, некий код режима ядра направил пакет (в данном случае — синхронный) другому драйверу. Как он может выполнить удаление отправленного пакета, например, в результате превышения времени ожидания? Пример ниже иллюстрирует этот случай.

// формирует синхронный пакет:
PIRP pIrp= IoBuildSynchronousFsdRequest(. . ., &event, &iosb);
// Подключаем процедуру завершения:
IoSetCompletionRoutine( pIrp, MyCompletionRoutine, (VOID*)&event,
                        TRUE, TRUE, TRUE );
NTSTATUS status = IoCallDriver(. . .);
if( status == STATUS_PENDING )
{	// Некоторое время ожидаем естественного завершения
    LARGE_INTEGER waitDelay;
    waitDelay.QuadPart = - 10000; // относительное время
    if( KeWaitForSingleObject( &event,
        KernelMode, FALSE, &waitDelay) == STATUS_TIMEOUT )
    {
        IoCancelIrp(pIrp);
        KeWaitForSingleObject( &event, KernelMode, FALSE, NULL);
    }
}
// Синхронные IRP пакеты - их удаляет Диспетчер ввода/вывода:
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
. . .

// Процедура завершения
NTSTATUS MyCompletionRoutine( PDEVICE_OBJECT pThisDevice,
                              PIRP pIrp,
                              PVOID pContext )
{
    if (pIrp->PendingReturned)
        KeSetEvent((PKEVENT) pContext, IO_NO_INCREMENT, FALSE);
    return STATUS_MORE_PROCESSING_REQUIRED;
}

Процедура IoCancelIrp устанавливает флаг (cancel bit) в IRP пакете и выполняет вызов процедуры CancelRoutine, если таковая имеется в IRP пакете.

Таблица 9.20. Прототип предоставляемой драйвером функции CancelRoutine

VOID CancelRoutine | IRQL == DISPATCH_LEVEL
Параметры | Выполняет действия, сопутствующие удалению пакета IRP
IN PDEVICE_OBJECT pDevObj | Указатель на объект устройства, которое (точнее — драйвер) и зарегистрировало ранее эту функцию в IRP пакете вызовом IoSetCancelRoutine (см. ниже)
IN PIRP pIrp | Указатель на удаляемый IRP пакет
Возвращаемое значение | void

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

PDRIVER_CANCEL IoSetCancelRoutine | IRQL <= DISPATCH_LEVEL
Параметры | Устанавливает (переустанавливает) определяемую драйвером функцию CancelRoutine для данного IRP пакета
IN PIRP pIrp | Указатель на IRP пакет, которому будет соответствовать устанавливаемая функция CancelRoutine
IN PDRIVER_CANCEL CancelRoutine | Указатель на функцию, которая соответствует прототипу, описанному в таблице 9.20, или NULL (если следует отменить функцию CancelRoutine для данного пакета IRP)
Возвращаемое значение | Указатель на ранее установленную для данного IRP пакета функцию CancelRoutine. Соответственно, если таковой не было, то возвращается NULL. Значение NULL возвращается также, если пакет находится в обработке и не может быть удален

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

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

Код вызова IoCancelIrp устроен примерно следующим образом — по крайней мере, так уверяет Уолтер Оней:

BOOLEAN IoCancelIrp(PIRP pIrp)
{
    IoAcquireCancelSpinLock(&pIrp->CancelIrql);
    pIrp->Cancel=TRUE;
    PDRIVER_CANCEL CancelRoutine = IoSetCancelRoutine(pIrp, NULL);
    if( CancelRoutine != NULL)
    {
        PIO_STACK_LOCATION currentCell =
            IoGetCurrentIrpStackLocation(pIrp);
        (*CancelRoutine)(currentCell->DeviceObject, pIrp);
        return TRUE;
    }
    else
    {
        IoReleaseCancelSpinLock(pIrp->CancelIrql);
        return FALSE;
    }
}

Для ограничения доступа к удаляемому пакету, код IoCancelIrp, прежде всего, запрашивает объект спин-блокировки вызовом IoAcquireCancelSpinLock (в переменной pIrp->CancelIrql сохраняется значение текущего уровня IRQL для использования при последующем вызове IoReleaseCancelSpinLock). В случае, если за IRP пакетом закреплена процедура CancelRoutine, то она вызывается (теперь на нее возложена задача освобождения спин-блокировки). Если же такой процедуры нет, то вызов IoCancelIrp завершает работу, освобождая спин-блокировку.

Процедура CancelRoutine, получив управление, может оказаться в одной из описанных ниже ситуаций.

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

Irp->IoStatus.Status = STATUS_IO_TIMEOUT;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);

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

Выяснить, обрабатывается ли рассматриваемый пакет именно сейчас, можно при помощи следующего кода, поскольку, если задействован механизм System Queuing и какой-либо пропущенный через него IRP пакет в настоящий момент обрабатывается, то именно адрес этого IRP пакета "лежит" в поле pDeviceObject->CurrentIrp (иначе там будет NULL):

VOID MyCancelRoutine ( IN PDEVICE_OBJECT pDeviceObject,
                       IN PIRP pIrp)
{
    if( pIrp == pDeviceObject->CurrentIrp )
    {
    . . .

Конкретная реализация действий по удалению текущего пакета (если она возможна) остается задачей разработчика драйвера.

Существенно проще становится ситуация, когда пакет, предназначенный для уничтожения, только что поступил в процедуру StartIo либо еще находится в очереди отложенных (pending) пакетов.

VOID StartIo ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIRp)
{
    KIRQL CancelIrql;
    IoAcquireCancelSpinLock(&CancelIrql);
    If(pIrp->Cancel)
    {
        IoReleaseCancelSpinLock(CancelIrql);
        return;
    }
    // Удаляем процедуру обработки удаления, делая пакет
    // "not cancelable" - неуничтожаемым
    IoSetCancelRoutine(pIrp, NULL);
    IoReleaseCancelSpinLock(CancelIrql);
    . . .
}

В случае, если удаляемый IRP пакет пока находится в системной очереди (которая называется еще "управляемая StartIo"), a перед его размещением там (то есть вместе с вызовом IoMarkIrpPending) была зарегистрирована процедура MyCancelRoutine для этого IRP пакета, то действия по удалению такого пакета (не текущего — не находящегося в обработке) могут выглядеть следующим образом:

VOID MyCancelRoutine( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    if( pIrp == pDeviceObject->CurrentIrp )
    {	// Текущий IRP
        IoReleaseCancelSpinLock(pIrp->CancelIrql);
        // Вряд ли можно сделать что-то еще...
    }
    else
    {	// Удаляем из системной очереди:
        KeRemoveEntryDeviceQueue( &pDeviceObject->DeviceQueue,
            &pIrp->Tail.Overlay.DeviceQueueEntry);
        // Только теперь можно освободить спин-блокировку:
        IoReleaseCancelSpinLock(pIrp->CancelIrql);

        pIrp->IoStatus.Status=STATUS_CANCELLED;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    }
    return;
}

Приведенный ниже пример выполняет удаление пакета из очереди, поддерживаемой собственно драйвером (так называемой "Device-Managed Queue").

VOID MyOtherCancelRoutine ( IN PDEVICE_OBJECT pDeviceObject,
                            IN PIRP pIrp )
{
    KIRQL oldIRQL;
    PMYDEVICE_EXTENSION pDevExt =
        (PMYDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
        IoSetCancelRoutine(pIrp, NULL);
    // Освобождаем спин-блокировку, установленную еще IoCancelIrp
    IoReleaseCancelSpinLock(pIrp->CancelIrp);

    // Удаляем IRP из очереди под защитой спин-блокировки
    KeAcquireSpinLock(&pDevExt->QueueLock, &oldIRQL);
    RemoveEntryList (&pIrp->Tail.Overlay.ListEntry);
    KeReleaseSpinLock(&pDevExt->QueueLock, oldIRQL);
    //
    pIrp->IoStatus.Status = STATUS_CANCELLED;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return;
}

Предполагается, что объект спин-блокировки, используемый для синхронизации доступа к очереди пакетов, pDevExt->QueueLock был заранее создан и сохранен в структуре расширения данного устройства.

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

В заключение, следует отметить, что после вызова IoCompleteRequest в конце процедуры CancelRoutine (обработки удаления IRP пакета в данном драйвере) запускаются вызовы процедур завершения вышестоящих драйверов — если такие драйвера имеются и если соответствующие процедуры были зарегистрированы для данного IRP пакета на случай его удаления (см. описание вызова IoSetCompletionRoutine в таблице 9.8).


Глава 9. Драйверная модель WDM

Заключение

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


Глава 10. Программные потоки и синхронизация

Глава 10

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

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


Глава 10. Программные потоки и синхронизация

Системные программные потоки

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

Потоки пользовательского режима хорошо известны программистам пользовательских приложений. Несколько отличаются от них потоки режима ядра. Системный поток есть такой поток, который выполняется исключительно в режиме ядра. Он не имеет контекста пользовательского режима и не может получить доступ в пользовательское адресное пространство. Соответственно, программный код рабочих процедур (в частности, код процедуры обработки IOCTL запросов, который может пользоваться виртуальными адресами пользовательского приложения) не может считаться системным потоком, хотя и относится к коду драйвера режима ядра. Системные программные потоки созданы специальными вызовами и не имеют возможности интерпретировать виртуальные пользовательские адреса (ниже 0x80000000) ни в одном из пользовательских контекстов. Системный поток не имеет корней в пользовательском режиме. Данная особенность накладывает основное ограничение, свойственное системным программным потокам, — они могут пользоваться только адресами системного адресного пространства. Все остальные адреса (виртуальные адреса пользовательских приложений), переданные им каким-нибудь способом, будут не просто бесполезны — их использование может привести к краху операционной системы.

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

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

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

Таблица 10.1. Прототип вызова PsCreateSystemThread

NTSTATUS PsCreateSystemThread | IRQL == PASSIVE_LEVEL
Параметры | Создает системный программный поток
OUT PHANDLE pThreadHandle | Указатель на переменную для сохранения дескриптора нового программного потока
IN ULONG DesiredAccess | THREAD_ALL_ACCESS (или 0L) для создаваемого драйвером потока
IN POBJECT_ATTRIBUTES Attrib | NULL для создаваемого драйвером потока
IN HANDLE ProcessHandle | NULL для создаваемого драйвером потока
OUT PCLIENT_ID ClientId | NULL для создаваемого драйвером потока
IN PKSTART_ROUTINE StartAddr | Стартовая функция потока — точка входа в поток
IN PVOID Context | Аргумент, передаваемый в стартовую функцию
Возвращаемое значение | • STATUS_SUCCESS — поток создан • STATUS_Xxx — код ошибки

Не последнюю роль в потребности использовать программные потоки в драйвере играет и тот факт, что (стандартно) их код выполняется на уровне PASSIVE_LEVEL. Как поступить, если разработчик драйвера желает протоколировать события в драйвере, записывая их в файл на диске, включая события, происходящие при повышенных приоритетах IRQL? Ведь функция ZwCreateFile и ZwWriteFile работают только на уровне PASSIVE_LEVEL, следовательно, о протоколировании из ISR и DPC процедур следует забыть? Подобная задача достаточно легко решается, если высокоприоритетный код будет помещать свои записи в промежуточный буфер, который будет сброшен на диск позже системным программным потоком, выполняющимся на подходящем для этого уровне PASSIVE_LEVEL.

Системные программные потоки создаются вызовом PsCreateSystemThread (таблица 10.1), а завершиться они должны самостоятельно — выполнением вызова PsTerminateSystemThread.

Таблица 10.2. Прототип вызова PsTerminateSystemThread

NTSTATUS PsTerminateSystemThread | IRQL == PASSIVE_LEVEL
Параметры | Вызывается системным программным потоком при окончании работы
IN NTSTATUS ExitStatus | Код завершения потока
Возвращаемое значение | STATUS_SUCCESS — поток прекращен

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

Создаваемые драйвером системные программные потоки имеют при создании уровень IQRL равный PASSIVE_LEVEL в диапазоне приоритетов Normal (см. таблицу 6.2), хотя могут иметь любой приоритет в диапазоне Normal и RealTime.

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

VOID ThreadStartRoutine (PVOID pContext)
{
    :
    KeSetPriorityThread ( KeGetCurrentThread(),
                         LOW_REALTIME_PRIORITY);
}

Заметим, что численное значение LOW_REALTIME_PRIORITY в заголовочных файлах DDK установлено равным 16 (это нижняя граница диапазона RealTime).

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

Системные рабочие потоки

Для нерегулярных коротких операций на уровне IRQL, равном PASSIVE_LEVEL, использование полноценных потоков, которые создаются и тут же завершаются, вряд ли будет эффективным. Альтернативой этому может быть создание системных рабочих потоков, system worker threads.

Для того чтобы проделать какую-нибудь несложную и не очень продолжительную работу, драйвер должен выделить память под структуру типа WORK_QUEUE_ITEM, затем инициализировать ее вызовом ExInitializeWorkItem, связав с ней собственную функцию (callback-функцию), и поместить ее в очередь объектов WORK_QUEUE_ITEM вызовом ExQueueWorkItem. Приоритет, на котором будет работать код вызываемой callback-функции, зависит от второго параметра вызова ExQueueWorkItem, QueueType, то есть от того, в какую очередь помещен данный объект WORK_QUEUE_ITEM, например, DelayedWorkQueue. В конце своей работы вызванная callback-функция должна освободить память, занятую под объектом типа WORK_QUEUE_ITEM (указатель на него поступает в callback-функцию при вызове).

Перечисленные функции ExInitializeWorkItem и ExQueueWorkItem считаются устаревшими (предлагаемые теперь функции будут рассмотрены ниже), однако они были удобны тем, что позволяли использовать в качестве объекта-посредника структуры данных большего размера, например, при следующем техническом приеме. Описываем структуру:

typedef struct _MY_WORK_ITEM {
    WORK_QUEUE_ITEM Item;
    char AdditionalData[64];
} MY_WORK_ITEM, *PMY_WORK_ITEM;

Данная структура создается и удаляется драйвером, что позволяет ей иметь нестандартную длину — главное, что начальный блок используется обычным для системы способом (как для WORK_QUEUE_ITEM).

Таблица 10.3. Прототип вызова IoAllocateWorkItem

PIO_WORKITEM IoAllocateWorkItem | IRQL<=DISPATCH_LEVEL
Параметры | Создает объект рабочего потока
IN PDEVICE_OBJECT pDevObject | Объект устройства инициатора вызова
Возвращаемое значение | Указатель на созданный объект или NULL в случае неудачи

Новые (поддерживаются в Windows Me/2000/XP/2003) предлагаемые вызовы IoAllocateWorkItem, IoQueueWorkItem и IoFreeWorkItem перераспределили обязанности. Теперь вызов IoAllocateWorkItem (таблица 10.3) создает структуры типа IO_WORKITEM (разумеется, только размером sizeof(IO_WORKITEM)), которые "записываются" за соответствующим объектом устройства. Объект IO_WORKITEM инициализируется вызовом IoQueueWorkItem, который связывает с ним callback-процедуру драйвера и передаваемый при ее вызове контекстный аргумент. Вызов IoQueueWorkItem также помещает объект IO_WORKITEM в очередь объектов, тип которой определяется значением третьего параметра, то есть QueueType. В конце работы callback-процедура драйвера должна выполнить освобождение созданного объекта IO_WORKITEM вызовом IoFreeWorkItem.

Таблица 10.4. Прототип вызова IoQueueWorkItem

VOID IoQueueWorkItem | IRQL<=DISPATCH_LEVEL
Параметры | Инициализирует объект рабочего потока и помещает его в очередь (обычно используется сразу после вызова IoAllocateWorkItem)
IN PIO_WORKITEM pWorkItem | Объект рабочего потока, созданный вызовом IoAllocateWorkItem
IN PIO_WORKITEM_ROUTINE pWorkRoutine | Callback-процедура, предоставляемая драйвером (ее прототип описан в таблице 10.5)
IN WORK_QUEUE_TYPE QueueType | Тип очереди. Драйвер должен предоставить одно из значений: • CriticalWorkQueue • DelayedWorkQueue
IN PVOID pContext | Контекстный аргумент. Его получит callback-функция при вызове
Возвращаемое значение | void

В том случае, если параметр QueueType при вызове будет равен CriticalWorkQueue, то объект IO_WORKITEM будет помещен в очередь для объектов с приоритетом в диапазоне RealTime (таблица 6.2), при значении DelayedWorkQueue — в очередь объектов с приоритетом Normal.

Следует помнить, что число объектов IO_WORKITEM, которые операционная система позволяет получить каждому объекту устройства (соответственно, разместить в своих очередях) не бесконечно, поэтому следует проверять результат вызова IoAllocateWorkItem на равенство NULL. Кроме того, не рекомендуется надолго задерживаться в callback-функции, поскольку это может затормозить извлечение из соответствующей очереди других IO_WORKITEM объектов, принадлежащих другим драйверам. В частности, не рекомендуется из таких потоков обращаться к другим драйверам вызовом IoCallDriver. Для выполнения продолжительных операций рекомендуется использовать полноценные системные программные потоки, создаваемые вызовом PsCreateSystemThread.

 Термин 'системные рабочие потоки' (system worker threads) нельзя считать удачным, потому что он в точности копирует термин API пользовательского режима, обозначающий программные потоки пользовательского режима, которые уже никакими временными ограничениями не стеснены. Кроме того, лексическое отличие от "нормальных" системных программных потоков, с описания которых началась данная глава, просто неуловимо.

Таблица 10.5. Прототип callback-функции рабочего потока

VOID workCallback | IRQL == PASSIVE_LEVEL
Параметры | Функция, предоставляемая драйвером, которая будет вызвана при извлечении из очереди объекта IO_WORKITEM (вызывается в контексте, как для системного программного потока, см. выше)
IN PDEVICE_OBJECT pDevObject | Объект устройства, которому принадлежит извлеченный из очереди объект IO_WORKITEM
IN PVOID pContext | Контекстный аргумент — для получения дополнительной информации, "запланированной" при вызове IoQueueWorkItem (например, указатель на IRP пакет и т.п.)
Возвращаемое значение | void

Вызываемая callback-функция должна освобождать IO_WORKITEM объект вызовом IoFreeWorkItem, таблица 10.6.

Таблица 10.6. Прототип вызова IoFreeWorkItem

VOID IoFreeWorkItem | IRQL<=DISPATCH_LEVEL
Параметры | Удаляет объект рабочего потока
PIO_WORKITEM pWorkItem | Объект рабочего потока, созданный вызовом ранее IoAllocateWorkItem
Возвращаемое значение | void

 Драйвер не должен делать какие-либо предположения о внутренней организации объектов IO_WORKITEM и изменять данные внутри. Для работы с этими объектами следует использовать только описанные выше вызовы.


Глава 10. Программные потоки и синхронизация

[Previous] [Next]

Синхронизация в режиме ядра

Синхронизационные функции и примитивы позволяют организовать временные задержки в исполнении программных потоков, которые бывают нужны либо одному отдельно взятому потоку, либо для согласования действий нескольких потоков. Разумеется, следует помнить, что при использовании приемов синхронизации, ориентированных на несколько программных потоков, действие этих приемов распространяется только на тех участников, которые признают эти правила. Никакой код операционной системы не останавливает программный поток по указке кого-то третьего. Иными словами, регулировщик движения (объект синхронизации) действует только на тех участников движения (программные потоки), которые признают его право это движение регулировать. Состояние объекта синхронизации является своего рода жестом регулировщика, который плохие участники могут игнорировать, конечно же, внося угрозу неправильной работы всей системы.


Глава 10. Программные потоки и синхронизация

Интервалы ожидания для отдельного потока

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

Поток, который желает приостановить свою работу на время до 50 мкс, может использовать вызов KeStallExecutionProcessor.

Таблица 10.7. Прототип вызова KeStallExecutionProcessor

VOID KeStallExecutionProcessor | IRQL == любой
Параметры | Останавливает работу на указанный интервал, независимо от производительности процессора
IN ULONG IntervalCount | Время задержки в 1 мкс интервалах
Возвращаемое значение | void

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

Более сложным является вызов KeDelayExecutionThread (таблица 10.8). Он удаляет программный поток из очереди "ready to run", следовательно, не мешает выполнению других потоков, готовых к работе. Минимальный временной интервал определяемой им задержки составляет 100 нс.

Рекомендуемые для драйверов значения WaitMode=KernelMode и Alertable=FALSE ограничивают применимость вызова KeDelayExecutionThread кодом системных потоков, созданных самим драйвером, и кодом процедур инициализации и завершения работы драйвера (то есть работающего заведомо вне пользовательского контекста).

Таблица 10.8. Прототип вызова KeDelayExecutionThread

NTSTATUS KeDelayExecutionThread | IRQL == PASSIVE_LEVEL
Параметры | Останавливает работу на указанный интервал, независимо от производительности процессора
IN KPROCESSOR_MODE WaitMode | Для драйверов: KernelMode
IN BOOLEAN Alertable | Для драйверов: FALSE
IN PLARGE_INTEGER TimeInterval | Время задержки в 100нс интервалах
Возвращаемое значение | STATUS_SUCCESS — ожидание завершено

Значение TimeInterval может описывать как относительные, так и абсолютные временные интервалы. Для задания абсолютных интервалов следует использовать вызов KeQuerySystemTime (см. таблицу 7.45), при помощи которого можно получить текущее системное время — как время начала ожидания. Функции, которые можно использовать для операции над типом данных LARGE_INTEGER, перечислены в таблице 7.44.

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

Таблица 10.9. Прототип вызова IoInitializeTimer

NTSTATUS IoInitializeTimer | IRQL == PASSIVE_LEVEL
Параметры | Выполняет регистрацию callback-функции IoTimerRoutine, предоставляемой драйвером
IN PDEVICE_OBJECT pDevObject | Объект устройства инициатора вызова, за которым будет "закреплен" создаваемый данным вызовом объект таймера
IN PIO_TIMER_ROUTINE pIoTimerRoutine | Указатель на регистрируемую callback-функцию IoTimerRoutine
IN PVOID pContext | Аргумент, передаваемый впоследствии в callback-функцию IoTimerRoutine
Возвращаемое значение | STATUS_SUCCESS при успешном завершении

В результате вызова IoInitializeTimer (таблица 10.9) операционная система создает таймерный объект режима ядра и связывает его с объектом устройства и callback-функцией IoTimerRoutine, предоставляемой драйвером. Регистрацию функции IoTimerRoutine лучше всего выполнять сразу после создания объекта устройства в процедуре AddDevice или DriverEntry (для не-WDM драйверов). Поскольку функция IoTimerRoutine при вызове будет получать указатель на объект устройства (из которого можно легко определить местоположение структуры расширения объекта устройства), то необходимые контекстные параметры можно разместить и в расширении устройства, в частности счетчик вызовов. Подробнее вопросы подсчета односекундных интервалов будут обсуждены ниже.

Таблица 10.10. Прототип функции обратного вызова IoTimerRoutine

VOID IoTimerRoutine | IRQL == DISPATCH_LEVEL
Параметры | Callback-функция, вызываемая через 1 сек. интервал
IN PDEVICE_OBJECT pDeviceObject | Указатель на объект устройства, с которым соотнесена данная функция
IN PVOID pContext | Контекстный аргумент
Возвращаемое значение | void

Собственно создание функции IoTimerRoutine и ее регистрация при помощи вызова IoInitializeTimer еще не приводят к работе таймера и периодическим вызовам IoTimerRoutine.

Если внимательно присмотреться к структуре DEVICE_OBJECT, то несложно заметить, что поле "PIO_TIMER Timer" в этой структуре единственное. Это недвусмысленно подразумевает, что более одной функций IoTimerRoutine для данного устройства использовать просто невозможно, хотя ничто не запрещает использовать одну callback-функцию IoTimerRoutine c несколькими объектами устройств.

Для запуска таймера, ассоциированного с callback-функцией IoTimerRoutine, используется вызов IoStartTimer. Останавливается таймер вызовом IoStopTimer.

Таблица 10.11. Прототип функции обратного вызова IoStartTimer

VOID IoStartTimer | IRQL<=DISPATCH_LEVEL
Параметры | Запуск таймера, в результате чего callback-функция IoTimerRoutine, соотнесенная с данным объектом устройства будет вызываться каждую секунду
IN PDEVICE_OBJECT pDeviceObject | Указатель на объект устройства, с которым соотнесен таймер, который следует запустить
Возвращаемое значение | void

Таблица 10.12. Прототип функции обратного вызова IoStopTimer

VOID IoStopTimer | IRQL<=DISPATCH_LEVEL
Параметры | Остановка таймера
IN PDEVICE_OBJECT pDeviceObject | Указатель на объект устройства, с которым соотнесен таймер, который следует остановить
Возвращаемое значение | void

Выполнение вызова IoStopTimer из функции IoTimerRoutine не допускается.

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

Работа с использованием callback-функции IoTimerRoutine может протекать следующим образом.

  • В процедуре AddDevice (DriverEntry) выполняется вызов IoInitializeTimer и связываются таймерная функция IoTimerRoutine и конкретный объект устройства.
  • В момент, когда вызывается драйверная рабочая процедура, предназначенная для обслуживания IRP_MJ_CREATE (то есть вызова Win API CreateFile), выполняется вызов IoStartTimer. Bce время, пока дескриптор доступа к драйверу (устройству) остается открытым, производятся вызовы функции IoTimerRoutine.
  • Каждый раз, когда производится вызов callback-процедуры IoTimerRoutine, значение счетчика таймерных "тиков" (если он задан) уменьшается (увеличивается) на единицу.
  • Когда пользовательская программа делает вызов CloseHandle, рабочая процедура драйвера, обслуживающая IRP_MJ_CLOSE, должна вызвать IoStopTimer, который прекратит вызовы функции IoTimerRoutine по сигналам таймера.

Разумеется, практически использовать собственно код callback-функции IoTimerRoutine можно весьма ограниченно, поскольку она стоит в стороне от "главных дорог" драйверных потоков. Как правило, при работе с этой функцией привлекаются еще DPC процедуры и/или другие синхронизационные примитивы (например, объекты события).

Рассмотрим несложный частный случай.

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

Дополняем структуру расширения объекта устройства счетчиком времени, оставшегося до наступления таймаута (превышения времени ожидания) с момента последнего прерывания, поступившего от обслуживаемого устройства:

typedef struct {

. . .
LONG Remaining; // сколько еще осталось секунд
. . .
} MYDEVICE_EXTENSION, *PMYDEVICE_EXTENSION;

В процедуре AddDevice инициализируем таймер, связанный с данным объектом устройства. Значение счетчика не устанавливаем до момента реального запуска таймера.

NTSTATUS AddDevice ( IN PDRIVER_OBJECT pDriverObject,
                     IN PDEVICE_OBJECT pDeviceObject )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
    pDeviceObject ->DeviceExtension;
    . . .
    IoInitializeTimer(pDeviceObject, MyIoTimerRoutine, pDevExt);
    . . .
}

Рабочая процедура драйвера CreateRequestHandler вызывается, когда в пользовательском приложении была попытка доступа к устройству через Win API вызов CreateFile. В этот момент вполне можно запустить таймер. Он продолжает отсчеты до тех пор, пока дескриптор доступа к устройству из пользовательского приложения остается открытым. Поскольку таймер работает, а его отсчеты нам еще не нужны, то необходимо инициализировать таймер таким значением, которое показывало бы, что его можно игнорировать (это будет -1).

NTSTATUS
CreateRequestHandler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
    pDevObj->DeviceExtension;
    . . .
    // ближе к концу инициализируем и запускаем таймер
    pDevExt->Remaining = -1;
    IoStartTimer(pDevObj);
    . . .
}

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

Использование системного вызова InterlockedExchange обеспечивает безопасное обновление и считывание 32-разрядного счетчика срабатываний таймера, который был ранее размещен в полностью определяемой разработчиком структуре расширения объекта устройства.

#define MY_INTERRUPT_TIMEOUT (10)

VOID StartIo( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
        pDevObj->DeviceExtension;
    . . .
    InterlockedExchange(&pDevExt->Remaining, MY_INTERRUPT_TIMEOUT);
    // Старт устройства:
    MyTransmitDataRoutine(pDevObj, pIrp);
    . . .
}

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

Процедура ISR по прибытии ожидаемого прерывания устанавливает соответствующее значение счетчика секунд ожидания. В случае, если нет работы (все операции ввода/вывода завершены), логично установить значение счетчика -1.

BOOLEAN
OnInterrupt ( IN PKINTERRUPT pInterruptObject, IN PVOID pContext )
{
    PDEVICE_EXTENSION pDeviceExt = (PDEVICE_EXTENSION)pContext;
    . . .
    // В случае, если остались еще данные для передачи, то
    // обновить счетчик
    if( IHaveTransmitBytes( pDeviceExt ) )
        InterlockedExchange( &pDeviceExt->Remaining,
                            MY_INTERRUPT_TIMEOUT );
    else // иначе - очистить счетчик
        InterlockedExchange ( &pDevExt->Remaining, -1 );
    . . .
}

Наконец, callback-функция MyIoTimerRoutine, которая вызывается всякий раз по срабатыванию таймера (каждую секунду), как только он запущен. В том случае, если данная процедура установила, что время ожидания активности устройства истекло, то она посредством процедуры DpcForIsr завершает работу над текущим IRP пакетом, объявляя его невыполненным.

VOID
MyIoTimerRoutine( IN PDEVICE_OBJECT pDeviceObj, IN PVOID pContext )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pContext;
    // Проверить время ожидания
    if( (pDevExt->Remaining,-1) < 0 )
        return; // значение счетчика не важно (поскольку -1)
    if( InterlockedDecrement(&pDevExt->Remaining) == 0 )
    {
        // Время ожидания истекло
        InterlockedExchange( &pDevExt->Remaining, (-1) );
        PIRP pCurrentIrp = pDeviceObj->CurrentIrp;
        pCurrentIrp->IoStatus.Status = STATUS_IO_TIMEOUT;
        pCurrentIrp->IoStatus.Information = 0;
        IoRequestDpc( pDeviceObj, pCurrentIrp, NULL )
        // Некоторые делают совсем "просто":
        // MyDpcForIsr(NULL, pDeviceObj, pCurrentIrp, pDevExt);
    }
    return;
}

Существует маленький временной зазор между тем, как функция MyIoTimerRoutine убедилась, что счетчик активен, и моментом, когда произошло его уменьшение на единицу. Если предположить, что в этот момент "вклинилась" процедура OnInterrupt и установила значение счетчика в -1, то функция MyIoTimerRoutine, получив управление, сделает значение счетчика равным -2. Код, приведенный выше, учитывает эту возможность, сравнивая Remaining c нулем.

Зарегистрированная соответствующим образом процедура DpcForIsr может выглядеть следующим образом:

VOID
MyDpcForIsr( IN PKDPC pDpcObj, IN PDEVICE_OBJECT pDeviceObj,
             IN PIRP pIrp,     IN PVOID pContext )
{
    . . .
    // Инициируем поступление IRP из внутренней очереди в
    // процедуру StartIO ():
    TodoStartNextPacket(&pDevExtension->dqReadWrite, pDevObject);

    // Даем возможность отработать процедурам завершения всех
    // вышестоящих драйверов, если они есть:
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    . . .
}


Глава 10. Программные потоки и синхронизация

Разделение времени и данных с ISR процедурой

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

Для выполнения такой работы (например, модификации совместно используемых данных из низкоприоритетной процедуры) создается обособленная функция IsrRoutineConcurrent по прототипу, описанному в таблице 10.13.

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

BOOLEAN IsrRoutineConcurrent | IRQL == см. ниже
Параметры | Манипуляции на уровне IRQL прерывания
IN PVOID pContext | Контекстный указатель
Возвращаемое значение | TRUE — в случае успешного завершения (с точки зрения разработчика драйвера) или FALSE

При необходимости выполнить некоторую работу, которая не может быть прервана функцией обработки прерывания, следует выполнить вызов KeSynchronizeExecution, см. таблицу 10.14.

Вызов KeSynchronizeExecution повышает уровень IRQL до значения SynchronizeIrql, указанного при создании объекта прерывания pInterruptObj системным вызовом IoConnectInterrupt, см. таблицу 8.10, в результате чего с данным объектом прерывания оказалась связана ISR процедура драйвера. Кроме того, данный вызов получает доступ к объекту спин-блокировки, связанному с данным объектом прерывания. В результате доступ к данным по контекстному указателю pContext становится безопасным в том смысле, что другие низкоприоритетные процедуры драйвера просто не могут работать в это время, так же, как не может стартовать и процедура обработки прерывания (если, разумеется, значения Irql и SynchronizeIrql равны, таблица 8.10). В том случае, если Irql превышает SynchronizeIrql, то доступ по указателю pContext из функции IsrRoutineConcurrent остается безопасным по причине владения упомянутым объектом спин-блокировки.

Таблица 10.14. Прототип вызова KeSynchronizeExecution

BOOLEAN KeSynchronizeExecution | IRQL <= IRQL прерывания
Параметры | Callback-функция, вызываемая через 1 сек. интервал
IN PKINTERRUPT pInterruptObj | Указатель на объект прерывания, с ISR процедурой которого и должна конкурировать функция IsrRoutineConcurrent
IN PKSYNCHRONIZE_ROUTINE IsrRoutineConcurrent | Функция IsrRoutineConcurrent (таблица 10.14), которая получит управления в результате данного системного вызова KeSynchronizeExecution
IN PVOID pContext | Контекстный аргумент, который получит функция IsrRoutineConcurrent при вызове
Возвращаемое значение | TRUE — если вызов IsrRoutineConcurrent успешен

Инициатор вызова KeSynchronizeExecution должен работать на уровне IRQL не выше SynchronizeIrql (см. таблицу 8.10) того объекта прерывания, с чьей ISR процедурой должна будет "конкурировать" функция IsrRoutineConcurrent.

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

BOOLEAN MyIsrRoutineConcurrentRoutine ( PDEVICE_OBJECT pDeviceObj );

VOID
MyIoTimerRoutine( IN PDEVICE_OBJECT pDeviceObj, IN PVOID pContext )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pContext;
    // предоставляем возможность поработать процедуре
    // MyIsrRoutineConcurrentRoutine
    KeSynchronizeExecution (pDevExt->InterruptObject,
        (PKSYNCHRONIZE_ROUTINE) MyIsrRoutineConcurrentRoutine,
        pDevExt);
}

BOOLEAN MyIsrRoutineConcurrentRoutine ( PDEVICE_OBJECT pDeviceObj );
{
    PDEVICE_EXTENSION pDevExt =
    (PDEVICE_EXTENSION) pDeviceObj->DeviceExtension;
    if( pDevExt->Remaining < 0 || (--pDevExt->Remaining)ɬ )
        return TRUE;
    PIRP pCurrentIrp = pDeviceObj->CurrentIrp;
    pCurrentIrp->IoStatus.Status = STATUS_IO_TIMEOUT;
    pCurrentIrp->IoStatus.Information = 0;
    // Планируем вызов DPC процедуры:
    IoRequestDpc( pDeviceObj, pCurrentIrp, NULL )
    return TRUE;
}

Потоки как объекты синхронизации

В качестве синхронизационного примитива может выступать и объект программного потока. Выполняющийся программный поток имеет несигнальное состояние. Состояние становится сигнальным в момент прекращения работы потока, что может сделать только сам поток "естественным" образом, то есть вызовом PsTerminateSystemThread. Другой программный поток, каким-либо способом получивший дескриптор нужного для синхронизации потока, может остановить свою работу вызовом KeWaitForSingleObject, как показано ниже.

PKTHREAD pThreadObject;
// Предположим, поток уже работает, его дескриптор hThread.
// Получаем указатель на его объект:
NTSTATUS status = ObReferenceObjectByHandle( hThread,
                THREAD_ALL_ACCESS,
                NULL,
                KernelMode,
                (PVOID *)& pThreadObject,
                NULL);
if( !NT_SUCCESS(status) )
{
    // Действия по обработке ошибки. Может быть поток уже завершен?
}

// Ожидаем окончания потока hThread
status = KeWaitForSingleObject( (PVOID) pThreadObject,
                                Suspended,
                                KernelMode,
                                FALSE,
                                (PLARGE_INTEGER)NULL);
// Поток завершился.
// Даем системе возможность удалить объект потока
ObDereferenceObject(pThreadObject);
. . .

Исполнительские ресурсы

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

Исполнительский ресурс является объектом типа ERESOURCE (с закрытыми для разработчика полями — в том смысле, что он не должен их использовать непосредственно из своего кода) и применяется для синхронизации доступа к одному или нескольким элементам данных. Любой код, прикасающийся к этим данным, должен сначала сделать запрос на владение соответствующим объектом ERESOURCE.

Если открыть определение структуры ERESOURCE в файле, например, wdm.h, то несложно понять, что исключительный доступ к данным, охраняемым объектом типа ERESOURCE, реализуется через механизм спин-блокировок.

Для работы с исполнительскими ресурсами используются вызовы, описанные в таблице 10.47. Как и быстрые мьютексы, эти объекты имеют собственные вызовы для запроса на владение, а не вызовы KeWaitForXxx. Разумеется, перед получением доступа следует выделить память под структуру ERESOURCE в нестраничной памяти и инициализировать ее при помощи вызова ExInitializeResourceLite.

Таблица 10.47. Функции для работы с исполнительскими ресурсами

Действие | Используемый вызов
Создание | ExInitializeResourceLite
Запрос на владение | ExAcquireResourceExclusiveLite ExAcquireResourceSharedLite ExTryToAcquireResourceExclusizeLite ExConvertExclusizeToSharedLite ExAcquireSharedStarveExclusive ExAcquireSharedWaitForExclusive
Запрос состояния | ExIsResourceAcquiredExclusiveLite ExIsResourceAcquiredSharedLite
Освобождение | ExReleaseResourceForThreadLite
Удаление | ExDeleteResourceLite

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

Ре-инициализация исполнительского ресурса может быть выполнена вызовом ExReinitializeResourceLite, который заменяет сразу три вызова (по удалению ресурса, выделению памяти под новую структуру и инициализации) и экономит память.

Группа функций (Ex)InterlockedXxx

В том случае, если разработчика драйвера устраивает то, что размер охраняемых данных составит размер sizeof(LONG) или sizeof(PVOID), то тогда в его распоряжении оказывается набор вызовов InterlockedXxx, например, InterlockedExchange. Эти вызовы реализуют доступ к переменной типа LONG и некоторые операции над ней в эксклюзивном (атомарном) режиме, например, операции увеличения и уменьшения на единицу, сравнения и т.п., хотя многие из них не документированы в DDK. Операции безопасного доступа и сравнения имеются и для указателей.

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

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

Изменение приоритетов как средство синхронизации

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

DPC процедуры как средство синхронизации

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

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

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

Следует также помнить, что к моменту вызова KeInsertQueueDpc должен существовать инициализированный DPC объект (см. описание вызова KeInitializeDpc, таблица 10.23), соотнесенный с интересующей DPC процедурой.


Глава 10. Программные потоки и синхронизация

Таймеры и их использование

В рассмотренном выше методе организации временных задержек с использованием предоставляемой драйвером callback-функции IoTimerRoutine объект таймера, хотя и участвовал, но в скрытой форме. К тому же, во всем обширном наборе примеров, прилагаемых к DDK, этот прием используется всего 2 раза. (Правда, возможно оттого, что столь длительные интервалы ожидания редко требуются в современной компьютерной технике.)

Простейшим из синхронизационных примитивов является объект события, event, который имеет два состояния: сигнальное и несигнальное. Переход в сигнальное состояние стимулирует работу функции KeWaitXxx, а выполняется он под влиянием вызова KeSetEvent. Подробнее объекты события будут рассмотрены ниже, пока что отметим, что пребывание в сигнальном либо несигнальном состоянии — есть самое основное свойство всех объектов синхронизации.

Не составляют исключения и таймеры. Можно сказать, что таймер — это объект события, который самостоятельно переходит в сигнальное состояние по истечении некоторого времени, заданного при запуске таймера. При этом таймер может выполнять дополнительные "услуги", например, планировать запуск DPC процедуры (специально созданной и зарегистрированной драйвером процедуры отложенного вызова), что будет рассмотрено позже.

Системные вызовы для работы с таймерными объектами перечислены в таблице 10.15.

Таблица 10.15. Системные вызовы для работы с таймерными объектами

Системные вызовы | Описание
KeInitializeTimer KeInitializeTimerEx | Инициализация таймерного объекта
KeSetTimer KeSetTimerEx | Установка таймерного объекта в несигнальное состояние (подготовка к срабатыванию)
KeCancelTimer | Прекращает работу таймера
KeReadStateTimer | Возвращает TRUE, если таймер в сигнальном состоянии
KeInitializeDpc | Инициализирует DPC объект, подготавливая его для работы с таймерными вызовами

Можно сказать, что вызов KeInitializeDpc находится в чужой компании, однако, этот вызов совершенно необходим, если таймер будет работать с DPC функциями.

Прежде чем перейти к подробному рассмотрению системных вызовов, обслуживающих операции над таймерными объектами, разберем сначала вызовы KeWaitXxx, то есть KeWaitForSingleObject и KeWaitForMultipleObjects.

Если есть сигнализирующие объекты (события, мьютексы, семафоры, таймеры и т.п.), то у программных потоков должны быть и специальные средства, которые позволили бы им остановиться и ожидать изменений в состоянии этих объектов. Именно такими средствами, применяемыми программными потоками для остановки и ожидания, являются вызовы KeWaitForSingleObject и KeWaitForMultipleObjects.

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

Таблица 10.16. Прототип вызова KeWaitForSingleObject

NTSTATUS KeWaitForSingleObject | IRQL <= DISPATCH_LEVEL
Параметры | Приостанавливает данный программный поток до момента перехода указанного объекта в сигнальное состояние либо до момента превышения значения Timeout
IN PVOID pObject | Указатель на инициализированный ранее объект синхронизационного примитива (объект события, объект таймера, объект потока и т.п.)
IN KWAIT_REASON Reason | Для драйверов: Executive
IN KPROCESSOR_MODE WaitMode | Для драйверов: KernelMode
IN BOOLEAN bAlertable | Для драйверов: FALSE
IN PLARGE_INTEGER Timeout | • Предельное время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах) • NULL при безусловном (неограниченном) ожидании
Возвращаемое значение | Для драйверов: • STATUS_SUCCESS — ожидание успешно завершено • STATUS_TIMEOUT — превышено предельное время ожидания

Вызов KeWaitForSingleObject на уровне IRQL равном DISPATCH_LEVEL следует выполнять только при нулевом значении параметра Timeout. Ha практике этот вызов выполняется из программного потока, работающего на уровне IRQL, равном PASSIVE_LEVEL.

"Самоостанов" программного потока вызовом KeWaitForSingleObject в ожидании, пока соответствующий синхронизационный примитив не перейдет в нужное состояние является широко распространенным приемом при синхронизации потоков и даже, для задержки выполнения одного потока при помощи объекта таймера, как это будет показано ниже.

С функцией KeWaitForMultipleObjects (таблица 10.17) следует обращаться с осторожностью, поскольку существуют системные ограничения на количество объектов, которые могут быть заданы в качестве влияющих на ожидание. Каждый поток имеет встроенный массив Wait-блоков, который он использует для действующих совместно "операций ожидания". Поток может использовать этот массив для ожидания переходов состояния более чем THREAD_WAIT_OBJECTS объектов. В случае, если число THREAD_WAIT_OBJECTS недостаточно, драйвер должен предложить собственный массив Wait-блоков при выполнении вызова KeWaitForMultipleObjects. В любом случае, число объектов, от которых зависит завершение ожидания, не может превышать MAXIMUM_WAIT_OBJECTS.

Таблица 10.17. Прототип вызова KeWaitForMultipleObjects

NTSTATUS KeWaitForMultipleObjects | IRQL <= DISPATCH_LEVEL
Параметры | Приостанавливает данный программный поток до момента перехода всех или хотя бы одного из указанных объектов в сигнальное состояние либо до момента превышения значения Timeout
IN ULONG Count | Число объектов, по которым определяется момент окончания ожидание
IN PVOID pObjects[] | Массив указателей на инициализированные объекты
IN WAIT_TYPE WaitType | • WaitAll — ожидание все объектов • WaitAny — ожидание хотя бы одного
IN KWAIT_REASON Reason | Для драйверов: Executive
IN KPROCESSOR_MODE WaitMode | Для драйверов: KernelMode
IN BOOLEAN bAlertable | Для драйверов: FALSE
IN PLARGE_INTEGER Timeout | • Предельное время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах) • NULL при безусловном (неограниченном) ожидании
IN PKWAIT_BLOCK WaitBlocks[] OPTIONAL | Массив Wait-блоков для этой операции (можно указать NULL)
Возвращаемое значение | Для драйверов: • STATUS_SUCCESS — ожидание успешно завершено • STATUS_TIMEOUT — превышено предельное время ожидания

Вызов KeWaitForMultipleObjects (также как и описанный выше вызов KeWaitForSingleObject) на уровне IRQL равном DISPATCH_LEVEL следует выполнять только при нулевом значении параметра Timeout.

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

Прежде всего, следует создать объект таймера. Для этого необходимо выделить область памяти и инициализировать ее вызовами KeInitializeTimer или KeInitializeTimerEx. Выделенная память должна быть резидентна. Иными словами, ее следует выделять в нестраничном пуле, например, вызовом ExAllocatePool.

Таблица 10.18. Прототип вызова KeInitializeTimer

VOID KeInitializeTimer | IRQL <= DISPATCH_LEVEL
Параметры | Инициализирует таймер типа NotificationTimer
IN PKTIMER pTimerObj | Указатель на место для объекта таймера, заранее подготовленное инициатором вызова
Возвращаемое значение | void

Таблица 10.19. Прототип вызова KeInitializeTimerEx

VOID KeInitializeTimerEx | IRQL <= DISPATCH_LEVEL
Параметры | Инициализирует таймер с указанием типа
IN PKTIMER pTimerObj | Указатель на место для объекта таймера, заранее подготовленное инициатором вызова
IN TIMER_TYPE Type | • NotificationTimer • SynchronizationTimer
Возвращаемое значение | void

Таймер типа NotificationTimer запускает выполнение всех потоков, ожидавших его перехода в сигнальное состояние, и остается в сигнальном состоянии до тех пор, пока кто-то не переведет его явным образом (вызовом KeSetTimerEx или KeSetTimer) в несигнальное.

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

После того как объект таймера создан (это можно сделать в самом начале работы, например, поместив указатель на объект таймера в структуру расширения объекта устройства), следует его запустить, разумеется, в нужном месте программного кода драйвера — в соответствии с логикой работы. Делается это вызовами KeSetTimer либо KeSetTimerEx.

Когда время ExpirationTime, заданное в KeSetTimer, истекает, объект таймера извлекается из системной очереди таймерных объектов и переходит в сигнальное состояние. Если задана процедура отложенного вызова CustomTimerDpc, то в этот момент соответствующий ей объект pTimerDpcObject помещается в системную очередь DPC объектов (разумеется, если там его еще нет) с тем, чтобы выполнить CustomTimerDpc в ближайшее время. Ранее окончания ожидания процедура CustomTimerDpc вызвана быть не может. Для многократного запуска автоматического таймера следует использовать вызов KeSetTimerEx.

Таблица 10.20. Прототип вызова KeSetTimer

BOOLEAN KeSetTimer | IRQL <= DISPATCH_LEVEL
Параметры | Инициализирует таймер с указанием типа
IN PKTIMER pTimerObject | Указатель на объект таймера, инициализированный вызовами KeInitializeTimer или KeInitializeTimerEx
IN LARGE_INTEGER ExpirationTime | Время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах)
IN PKDPC pTimerDpcObject OPTIONAL | Указатель на объект DPC процедур (прототип см. в таблице) либо NULL
Возвращаемое значение | TRUE — если объект таймера находился в системной очереди таймерных объектов в момент вызова

Когда время ExpirationTime, заданное в KeSetTimerEx, истекает, объект таймера извлекается из системной очереди таймерных объектов и переходит в сигнальное состояние. Если существует процедура отложенного вызова CustomTimerDpc, то в этот момент соответствующий ей объект pTimerDpcObject помещается в системную очередь DPC объектов (разумеется, если там его еще нет) с тем, чтобы выполнить CustomTimerDpc в ближайшее время. Ранее времени окончания ожидания ExpirationTime процедура CustomTimerDpc вызвана быть не может, зато по окончании ExpirationTime она вызывается через каждый интервал Period, если таковой задан.

Таблица 10.21. Прототип вызова KeSetTimerEx

BOOLEAN KeSetTimerEx | IRQL <= DISPATCH_LEVEL
Параметры | Инициализирует таймер с указанием типа
IN PKTIMER pTimerObject | Указатель на объект таймера, инициализированный вызовами KeInitializeTimer или KeInitializeTimerEx
IN LARGE_INTEGER ExpirationTime | Время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах)
IN LONG Period OPTIONAL | Значение периода (в миллисекундах) вызова функции CustomTimerDpc, если она указана.
IN PKDPC pTimerDpcObject OPTIONAL | Указатель на объект DPC процедур (прототип см. в таблице) либо NULL
Возвращаемое значение | TRUE — если объект таймера находился в системной очереди таймерных объектов в момент вызова

Процедура CustomTimerDpc может прекратить существование объекта таймера, если KeSetTimerEx указал нулевое значение параметра Period.

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

Функция CustomTimerDpc (разумеется, ее имя определяется разработчиком драйвера, данное же используется лишь для определенности при изложении материала) является процедурой отложенного вызова, которая связывается с таймерами. Она запускается (однократно или многократно) не ранее окончания интервала ожидания, выставленного в таймере. Операционная система автоматически организует очередь из объектов, соответствующих DPC процедурам, ожидающим выполнения. Диспетчер DPC процедур извлекает данный объект из очереди, и лишь тогда CustomTimerDpc начинает свою работу. Практически всегда имеется некоторая задержка между моментом срабатывания таймера, когда интервал ожидания истек, и стартом CustomTimerDpc.

Как и все другие DPC процедуры, CustomTimerDpc работает на уровне IRQL равном DISPATCH_LEVEL. В таблице 10.22 описан прототип ее вызова. Следует обратить внимание на то, что эта процедура получает два зарезервированных параметра, значение которых на настоящий момент еще не определено.

Таблица 10.22. Прототип CustomTimerDpc

VOID CustomTimerDpc | IRQL == любой
Параметры | Описание
IN PKDPC pDpc | DPC объект, инициализировавший вызов
IN PVOID pContext | Контекст, указанный в вызове KeInitializeDpc при инициализации данной функции как DPC процедуры
IN PVOID SystemArg1 | Зарезервирован
IN PVOID SystemArg2 | Зарезервирован
Возвращаемое значение | void

Работа с процедурой CustomTimerDpc достаточно проста. Следует выполнить следующие действия:

  1. Получить область в нестраничной памяти (возможно, запомнить полученный указатель в структуре расширения объекта устройства) для объекта KDPC, например, при помощи вызова ExAllocatePool.
  2. Выполнить, например, во время работы AddDevice, вызов KeInitializeDpc (см. описание прототипа в таблице 10.23) для того, чтобы связать с функцией CustomTimerDpc передаваемые ей при вызове контекстные указатели (pDpc и pContext). Адрес расширения структуры объекта устройства также является хорошим кандидатом для передачи в вызываемую функцию CustomTimerDpc.

Для того чтобы отменить срабатывание активного таймера используется вызов KeCancelTimer (прототип описан в таблице 10.24). Этот же вызов может остановить работу "многократного" таймера и, соответственно, вызовы процедуры CustomTimerDpc. Следует обратить внимание, что освобождать память, занятую под объектом таймера или объектом DPC следует только после вызова KeCancelTimer — нарушение этого правила легко приводит к краху системы. Для определения, истекло ли время ожидания таймера, можно использовать вызов KeReadStateTimer (см. таблицу 10.25).

Таблица 10.23. Прототип вызова KeInitializeDpc

VOID KeInitializeDpc | IRQL == PASSIVE_LEVEL
Параметры | Описание
IN PKDPC pDpc | DPC объект, для которого инициатор данного вызова предоставил область памяти.
IN PKDIFERRED_ROUTINE DeferredProcedure | Указатель на процедуру, которая будет вызываться в момент извлечения объекта DPC из очереди, в данном случае— CustomTimerDpc
IN PVOID pContext | Контекстный указатель, передаваемый вызываемой DPC процедуре, в данном случае — CustomTimerDpc
Возвращаемое значение | void

Таблица 10.24. Прототип вызова KeCancelTimer

BOOLEAN KeCancelTimer | IRQL <= DISPATCH_LEVEL
Параметры | Описание
IN PKTIMER Timer | Указатель на объект таймера, который следует "отменить".
Возвращаемое значение | • TRUE — если таймер был "взведен" (несигнальное состояние) перед вызовом • FALSE — в противном случае

Таблица 10.25. Прототип вызова KeReadStateTimer

BOOLEAN KeReadStateTimer | IRQL <= DISPATCH_LEVEL
Параметры | Описание
IN PKTIMER Timer | Указатель на объект опрашиваемого таймера.
Возвращаемое значение | • TRUE — если таймер "истек" и перешел в сигнальное состояние • FALSE — если таймер еще "взведен" и находится в несигнальном состоянии

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

Рассмотрим пример, реализующий добровольную задержку программного потока драйвера при использовании таймера. В данном примере задержка вставлена в драйверный код обработчика IOCTL запросов пользовательского приложения, который работает в контексте пользовательского потока (что характерно для обработчика IOCTL запросов), соответственно, уровень IRQL данного кода не превышает PASSIVE_LEVEL. По этой причине использование задержек в 1-10 секунд не вызывает никаких возражений со стороны операционной системы.

// Хотя и нехорошо делать глобальные переменные в драйвере:
PKTIMER pTimer=NULL;	   // указатель на таймер
PKDPC   pDpcObject=NULL;  // указатель на объект DPC
#define IDLE_INTERVAL (10000)

VOID MyDeferredRoutine( IN PKDPC pthisDpcObject,
            IN PVOID DeferredContext,
            IN PVOID SystemArgument1,
            IN PVOID SystemArgument2 )
{
    PKTIMER ptrTimer = (PKTIMER)DeferredContext;
    DbgPrint("-Example- In MyDeferredRoutine.");
    if( KeReadStateTimer(ptrTimer) )
    {
        DbgPrint("-Example- DPC: KeReadStateTimer returns TRUE.");
    }
    else
    {
        DbgPrint("-Example- DPC: KeReadStateTimer returns FALSE.");
    }
}
// Обработчик IOCTL вызовов:
NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp )
{
    NTSTATUS status;
    . . .
    switch( ControlCode) {
    case IOCTL_TEST_TIMER:
    {
        NTSTATUS status;
        // Выводим сообщения только в отладочной версии
        DbgPrint("-Example- IOCTL_PRINT_DEBUG_MESS.");
        int shortCycles;

        if( pTimer==NULL ) // если объект таймера не существует:
        {
            // выделяем память под объект таймера
            pTimer=(PKTIMER)ExAllocatePool(NonPagedPool,sizeof(KTIMER));
            KeInitializeTimer(pTimer);   // инициализируем объект таймер
            // выделяем память под DPC объект и инициализируем его
            pDpcObject=(PKDPC)ExAllocatePool(NonPagedPool,sizeof(KDPC));
            KeInitializeDpc(pDpcObject, MyDeferredRoutine, pTimer);
        }

        LARGE_INTEGER dueTime;
        dueTime.QuadPart = -10000 * IDLE_INTERVAL; // 10000*10000*1нс
        // "взводим" таймер:
        KeSetTimerEx( pTimer,
                dueTime, // время ожидания, относительный интервал
                (IDLE_INTERVAL/2), // период 5 с, то есть 5000*1 мс
                pDpcObject );

        // во время ожидания сигнального состояния таймера
        // выполним 100 циклов по 50 мкс:
        for ( shortCycles=0; shortCycles < 100; shortCycles++)
        {
            DbgPrint("-Example-KeStallExecutionProcessor.shortCycles=%d.",
                        shortCycles);
            KeStallExecutionProcessor(50);
            if( KeReadStateTimer(pTimer) )
            {
                DbgPrint("-Example- KeReadStateTimer returns TRUE.");
            }
            else
            {
                DbgPrint("-Example- KeReadStateTimer returns FALSE.");
            }
        }
        // Останавливаем поток
        status = KeWaitForSingleObject( pTimer,
                       Executive,   // IN KWAIT_REASON WaitReason,
                       KernelMode,  // IN KPROCESSOR_MODE WaitMode,
                       FALSE,  // IN BOOLEAN Alertable,
                       NULL);  // IN PLARGE_INTEGER Timeout OPTIONAL

        if( !NT_SUCCESS(status) )
        {
            DbgPrint("-Example- Error in KeWaitForSingleObject.");
            DbgPrint("-Example- STATUS eq %x.",status);
        }
        else
        {
            DbgPrint("-Example- KeWaitForSingleObject OK.");
        }

        // Считываем состояние таймера после окончания ожидания:
        if(KeReadStateTimer(pTimer))
        {
            DbgPrint("-Example- KeReadStateTimer returns TRUE (after).");
        }
        else
        {
            DbgPrint("-Example- KeReadStateTimer returns FALSE (after).");
        }
    break;
    }
    case IOCTL_CANCEL_TIMER: // Удаляем объект таймера и объект DPC
    {
        BOOLEAN result = KeCancelTimer(pTimer);
        if(result)
        {
            DbgPrint("-Example- KeCancelTimer returns TRUE.");
        }
        else
        {
            DbgPrint("-Example- KeCancelTimer returns FALSE.");
        }
        ExFreePool(pTimer);
        ExFreePool(pDpcObject);
        pTimer=NULL;
    break;
    }

Ниже приводится фрагмент кода пользовательского приложения, которое тестирует рассмотренный выше код драйвера.

DWORD BytesReturned;
unsigned long ioctlCode=IOCTL_PRINT_DEBUG_MESS;
if( !DeviceIoControl(  hHandle,
                       ioctlCode,
                       NULL, 0, // Input
                       NULL, 0, // Output
                       &BytesReturned,
                       NULL ) )
{
    printf( "Error in IOCTL_PRINT_DEBUG_MESS!" );
}
// Запуская данное приложение в отладчике в пошаговом режиме,
// здесь сделаем паузу, давая отработать несколько раз DPC
// процедуре, шаги с 216 по 241 в распечатке ниже.
// Интервал вызовов DPC процедуры составляет 5 секунд.
ioctlCode=IOCTL_CHANGE_IRQL;
if( !DeviceIoControl(  hHandle,
                       ioctlCode,
                       NULL, 0, // Input
                       NULL, 0, // Output
                       &BytesReturned,
                       NULL ) )
{
    printf( "Error in IOCTL_CHANGE_IRQL!" );
}

Функция MyDeferredRoutine начинает запускаться только после перехода таймера в сигнальное состояние. Период ее запусков определен значением третьего параметра в вызове KeSetTimerEx.

Ниже приводится распечатка log-файла сообщений, выводимых макросами DbgPrint в окно программы DebugView. Вторая колонка — отсчеты в секундах.

00000010   0.00241344 -Example- IOCTL_TEST_TIMER.
00000011   0.00243578 -Example- KeStallExecutionProcessor.shortCycles=0.
00000012   0.00250171 -Example- KeReadStateTimer returns FALSE.
00000013   0.00251568 -Example- KeStallExecutionProcessor.shortCycles=1.
00000014   0.00257910 -Example- KeReadStateTimer returns FALSE.
00000015   0.00259810 -Example- KeStallExecutionProcessor.shortCycles=2.
00000016   0.00266179 -Example- KeReadStateTimer returns FALSE.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
00000208   0.01016721 -Example- KeReadStateTimer returns FALSE.
00000209   0.01018118 -Example- KeStallExecutionProcessor.shortCycles=99.
00000210   0.01024572 -Example- KeReadStateTimer returns FALSE.

00000211   9.99725244 -Example- In MyDeferredRoutine.
00000212   9.99728234 -Example- DPC: KeReadStateTimer returns TRUE.
00000213   9.99730357 -Example- KeWaitForSingleObject OK.
00000214   9.99732033 -Example- KeReadStateTimer returns TRUE (after).
00000215   9.99734128 -Example- DeviceIoControl: 0 bytes written.

00000216  15.00447208 -Example- In MyDeferredRoutine.
00000217  15.00450169 -Example- DPC: KeReadStateTimer returns TRUE.
00000218  20.01165400 -Example- In MyDeferredRoutine.
00000219  20.01168780 -Example- DPC: KeReadStateTimer returns TRUE.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
00000240  80.09805464 -Example- In MyDeferredRoutine.
00000241  80.09808928 -Example- DPC: KeReadStateTimer returns TRUE.

00000242  80.18758250 -Example- IOCTL_CANCEL_TIMER.
00000243  80.53109152 -Example- KeCancelTimer returns TRUE.

Заметим, что выполнение освобождения памяти вызовами ExFreePool без выполнения вызова KeCancelTimer здесь неминуемо привело бы к краху системы.

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

VOID MyDeferredRoutine( IN PKDPC pthisDpcObject,
                        IN PVOID DeferredContext,
                        IN PVOID SystemArgument1,
                        IN PVOID SystemArgument2
                        )
{
    . . .
    PKTIMER ptrTimer = (PKTIMER)DeferredContext;
    LARGE_INTEGER dueTime;
    dueTime.QuadPart = -10000 * IDLE_INTERVAL;  // 10secs
    KeSetTimerEx( ptrTimer,
                  dueTime,
                  (IDLE_INTERVAL/2), // 5000 ms
                  pthisDpcObject
                );
}

Как было сказано ранее, указатели на объекты таймера и DPC рекомендуется хранить в структуре расширения объекта устройства. Следовательно, если объект устройства удаляется (например, при выключении питания PnP устройства) и драйвер удаляется, то необходимо корректно очистить занимаемую такими объектами память. Проблемы отладки ситуаций, когда драйвер завершает работу с еще активным таймером, настолько сложны, что Driver Verifier (программное средство "тренировки" драйверов) делает специальную проверку относительно освобождения памяти, которая содержит работающие таймеры.


Глава 10. Программные потоки и синхронизация

Объекты события

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

Если присмотреться к определению объекта события в заголовочных файлах DDK, то становится очевидным, что объект события состоит только лишь из структуры DISPATCHER_HEADER, в то время как другие синхронизационные примитивы — таймеры, семафоры и мьютексы — содержат, помимо DISPATCHER_HEADER, некоторые дополнительные поля. То есть, события — самые простые из них.

Объекты события делятся на две категории: объекты для уведомления (Notification Events) и объекты для синхронизации (Synchronization Events). Тип выбирается в момент инициализации объекта. Эти два типа объекта событий проявляют различие в своем поведении в момент, когда объект переводится в сигнальное состояние.

Как только объект уведомляющего (Notification) события переходит в сигнальное состояние, все потоки, реагирующие на него, выходят из состояния ожидания. Однако объект такого типа необходимо перевести в несигнальное состояние явно (вызовом KeClearEvent), иначе он так и останется в активном состоянии. Поведение данного типа объектов аналогично поведению объектов события пользовательского режима, которые управляются ручной установкой.

Поведение синхронизационных (Synchronization) объектов события несколько отличается. Когда синхронизационный объект события переходит в сигнальное состояние, то он остается в этом состояние лишь столько времени, сколько это необходимо для выполнения одного вызова KeWaitForXxx. Затем объект переводит себя в несигнальное состояние автоматически. Другими словами, ворота остаются открытыми только до тех пор, пока кто-то первый не прошел через них, после чего они закрываются автоматически. Этот тип эквивалентен событиям с авто-сбросом (auto-reset) в пользовательском режиме.

Таблица 10.26. Функции для работы с объектами событий

Что необходимо сделать... | Какой вызов нужно использовать...
Создать событие | KeInitializeEvent
Создать именованное событие | IoCreateSynchronizationEvent IoCreateNotificationEvent
Изменить состояние события | KeSetEvent KeClearEvent KeResetEvent
Запросить состояние | KeReadStateEvent

Для использования объекта события сначала необходимо получить память для его хранения размером sizeof(KEVENT), и только после этого можно выполнять вызовы функций, перечисленные выше. Рассмотрим некоторые из них.

Несигнальное состояние объекта событий можно установить при помощи вызовов KeResetEvent и KeClearEvent. Разница между ними заключается в том, что функция KeResetEvent еще и возвращает состояние объекта, в котором тот пребывал перед данным вызовом. Функция KeClearEvent работает несколько быстрее, поэтому в тех случаях, когда нет необходимости знать предыдущее состояние объекта, следует использовать этот вызов.

Таблица 10.27. Прототип вызова KeInitializeEvent

VOID KeInitializeEvent | IRQL == PASSIVE_LEVEL
Параметры | Инициализация объекта события и установка его начального состояния
IN PKEVENT pEvent | Указатель на область памяти для объекта события
IN EVENT_TYPE Type | Одно из двух значений • NotificationEvent • SynchronizationEvent
IN BOOLEAN bInitalState | Начальное состояние объекта • TRUE — сигнальное состояние • FALSE — несигнальное состояние
Возвращаемое значение | void

Таблица 10.28. Прототип вызовов KeClearEvent u KeResetEvent

VOID KeClearEvent LONG KeResetEvent | IRQL <= DISPATCH_LEVEL
Параметры | Установка объекта события в несигнальное состояния
IN PKEVENT pEvent | Указатель на инициализированный объект события
Возвращаемое значение | KeResetEvent возвращает предыдущее состояние объекта события

Таблица 10.29. Прототип вызова KeSetEvent

LONG KeSetEvent | IRQL <= DISPATCH_LEVEL
Параметры | Переводит объект события в сигнальное состояние
IN PKEVENT pEvent | Указатель на инициализированный объект события
IN KPRIORITY Increment | Обычно используется значение IO_NO_INCREMENT
IN BOOLEAN bWait | Обычно используется значение FALSE
Возвращаемое значение | Возвращает ненулевое значение, если предыдущее состояние объекта события было сигнальным

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

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

Совместное использование двумя несвязанными драйверами одного объекта события, созданного вызовом KeInitializeEvent, есть весьма непростая задача. Более простого способа передать его, иначе как по специальному предварительному соглашению (например, с использованием специального внутреннего кода IOCTL), не существует. Имеется и такая проблема: как гарантировать, что драйвер, создавший объект события, и в момент получения указателя другим драйвером и во все время его использования все еще останется загруженным?

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

При использовании именованного объекта события совместно драйвером и приложением пользовательского режима следует создавать такой объект сначала в пользовательском приложении. Причина кроется в том, что пользовательские объекты события должны размещаться в директории объектов \BaseNamedObjects, которая создается после инициализации подсистемы Win32 и к моменту запуска драйвера, возможно, еще не существует. После этого, в результате IOCTL запроса (выступающего в роли команды) к драйверу, последний должен получить доступ к объекту события по заранее определенному имени либо должен получить некоторую дополнительную информацию из IOCTL запроса — имя или дескриптор созданного объекта события.

Таблица 10.30. Прототип вызовов IoCreateSynchronization(Notification)Event

PKEVENT IoCreateSynchronizationEvent PKEVENT IoCreateNotificationEvent | IRQL == PASSIVE_LEVEL
Параметры | Создает новый или получает доступ к существующему объекту события по имени
IN PUNICODE_STRING EventName | Имя объекта, заканчивающаяся нулем строка широких (UNICODE) символов
OUT PHANDLE EventHandle | Указатель, по которому будет возвращен дескриптор объекта.
Возвращаемое значение | Указатель на созданный или существующий объект события с данным именем либо NULL в случае ошибки.

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

  • Выполнить вызов ObReferenceObjectByHandle. Эта функция возвращает указатель на собственно объект и увеличивает на единицу число ссылок на данный объект.
  • Поскольку собственно дескриптор становится ненужным, необходимо вы полнить вызов ZwClose со значением дескриптора. Эта функция выполнит уменьшение на единицу счетчика ссылок на данный объект.
  • Когда объект события станет ненужным, необходимо выполнить вызов ObDereferenceObject для того, чтобы уменьшить на единицу счетчик ссылок на объект, что, возможно, уничтожит его.

Эти вызовы могут быть выполнены только с уровня IRQL равного PASSIVE_LEVEL, что накладывает ограничения на то, где драйвер сможет их использовать.

В том случае, если драйвер получает от приложения дескриптор через IOCTL запрос, то этот дескриптор имеет силу, поскольку код драйвера (обработчика IOCTL запросов) работает в контексте пользовательского потока, обратившегося к драйверу.

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

Семафоры

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

Для увеличения на единицу значения внутреннего счетчика семафора следует выполнить вызов KeReleaseSemaphore. Предположим, два потока t1 и t2 ожидают (используя вызов KeWaitForSingleObject) сигнального состояния семафора S, счетчик которого в настоящий момент равен 0. Когда третий поток t0 выполнит вызов KeReleaseSemaphore, значение счетчика возрастет до 1. Следовательно, одному из ожидающих потоков будет разрешено продолжить работу. Вызов KeWaitForSingleObject вернется из состояния ожидания, уменьшив счетчик семафора на единицу. Соответственно, второй поток останется заблокированным. Какому конкретно потоку повезет, почти что неизвестно — в том смысле, что не следует строить на этом расчет. Если это имеет большое значение, то следует усложнить схему синхронизации.

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

Таблица 10.31. Функции для работы с объектами семафоров

Что необходимо сделать | Используемый вызов
Создать семафор | KeInitializeSemaphore
Увеличить счетчик семафора | KeReleaseSemaphore
Запросить состояние | KeReadStateSemaphore
Уменьшить счетчик семафора | KeWaitForSingleObject KeWaitForMultipleObject

Для инициализации семафора используется вызов, KeInitializeSemaphore, которому необходимо передать не только два параметра будущего семафора (см. таблицу 10.32), но и область памяти под будущий объект, выделенную, например, вызовом ExAllocatePool.

Таблица 10.32. Прототип вызова KeInitializeSemaphore

VOID KeInitializeSemaphore | IRQL == PASSIVE_LEVEL
Параметры | Инициализирует объект семафора и устанавливает текущее значение его счетчика и предельное значение, которого этот счетчик может достигать
IN PKSEMAPHORE pSemaphore | Указатель на область, подготовленную для объекта семафора
IN LONG CountValue | Текущее (начальное) значение счетчика
IN LONG CountLimit | Предел для значений счетчика (должно быть положительным)
Возвращаемое значение | void

Вызов KeReadStateSemaphore получает указатель на объект семафора и возвращает значение типа LONG, равное текущему значению счетчика семафора. Соответственно, нулевое возвращенное значение указывает на то, что семафор пребывает в несигнальном состоянии. Вызов KeReadStateSemaphore может выполняться на любом уровне IRQL, что однозначно указывает на то, какие правила применяет система к объекту семафора: он непременно должен размещаться в области нестраничной памяти.

Параметры вызова KeReleaseSemaphore описаны в таблице 10.33.

Таблица 10.33. Прототип вызова KeReleaseSemaphore

VOID KeReleaseSemaphore | IRQL == PASSIVE_LEVEL
Параметры | Инициализирует объект семафора и устанавливает текущее значение его счетчика и предельное значение, которого этот счетчик может достигать
IN PKSEMAPHORE pSemaphore | Указатель на область, подготовленную для объекта семафора
IN LONG CountValue | Текущее (начальное) значение счетчика
IN LONG CountLimit | Предел для значений счетчика (должно быть положительным)
Возвращаемое значение | void

Мьютексы

Мьютекс является синхронизационным примитивом (объектом), которым может владеть только один поток в данный конкретный момент времени. Термин mutex является сокращением от 'mutual exclusion', совместное исключение. Объект этого типа имеет несигнальное состояние, когда поток им владеет, и сигнальное — когда объект свободен. Мьютексы обеспечивают несложный механизм координации исключительного доступа к совместно используемым ресурсам, обычно — областям памяти.

Предположим, потоки t1 и t2 ожидают освобождения мьютекса, которым владеет поток t0. В момент, когда поток t0 освободит мьютекс, один из ожидающих потоков "пробудится" и станет его владельцем.

Для использования мьютекса необходимо получить блок памяти размером sizeof(KMUTEX) в области нестраничной памяти (например, вызовом ExAllocatePool), после чего следует выполнить его инициализацию вызовом KeInitializeMutex (таблица 10.34). Следует помнить, что когда мьютекс инициализируется, то он сразу же устанавливается в сигнальное состояние.

В случае, если некий поток выполняет вызов KeWaitForXxx относительно того мьютекса, которым он уже владеет, никакого ожидания не случится. Вместо этого, происходит увеличение на единицу внутреннего счетчика объекта мьютекса, что всего лишь отражает факт повторного запроса на владение данным мьютексом со стороны потока. Когда поток пожелает освободить мьютекс, то ему придется сделать столько вызовов KeReleaseMutex (таблица 10.35), сколько ранее было сделано запросов на владение им. Только после этого объект мьютекса перейдет в сигнальное состояние. Точно такое же поведение демонстрируют мьютексы и в программировании приложений пользовательского режима. Мьютексы похожи на семафоры с максимальным значение счетчика 1. Правда, с одним существенным отличием: сам программный поток, получивший владение мьютексом, может сделать это еще много раз (столько же раз он должен и освободить мьютекс).

Таблица 10.34. Прототип вызова KeInitializeMutex

VOID KeInitializeMutex | IRQL == PASSIVE_LEVEL
Параметры | Инициализирует объект мьютекса и устанавливает его начальное состояние — сигнальное.
IN PKMUTEX pMutex | Указатель на область, подготовленную для объекта мьютекса
IN LONG Level | Уровень, присвоенный мьютексу разработчиком
Возвращаемое значение | void

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

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

Драйвер должен освобождать все мьютексы, которые находятся в его владении, перед тем, как передаст управление в пользовательский режим (то есть завершит рабочую процедуру и вернет управление Диспетчеру ввода/вывода). Ядро воспримет это как ошибку. Процедура AddDevice, DriverEntry или какая-либо рабочая процедура драйвера, получая для себя мьютекс, не должны планировать его освобождение в другой рабочей процедуре или в другом программном потоке данного драйвера.

Таблица 10.35. Прототип вызова KeReleaseMutex

LONG KeReleaseMutex | IRQL == PASSIVE_LEVEL
Параметры | Уменьшает на единицу "счетчик занятости" объекта мьютекса, обозначая намерение инициатора вызова тут же вызвать (или не вызывать) KeWaitXxx.
IN PKMUTEX pMutex | Указатель на объект мьютекса
IN BOOLEAN doCallOfKeWaitXxx | • TRUE — следом за данным вызовом текущий программный поток собирается сделать вызов KeWaitForXxx (используется редко) • FALSE — применяемое на практике значение (см. документацию DDK)
Возвращаемое значение | 0, если объект мьютекса перешел в сигнальное состояние

Запрос на владение мьютексом выполняется вызовом KeWaitForSingleObject либо вызовом KeWaitForMultipleObject из кода, работающего на уровне IRQL равном PASSIVE_LEVEL. Специально для мьютексов придумано также макроопределение KeWaitForMutexObject, которое есть текстуальная подстановка все того же системного вызова KeWaitForSingleObject.

Единственная функция, которую можно вызывать из кода уровня IRQL выше, чем PASSIVE_LEVEL, — это вызов KeReadStateMutex (таблица 10.36).

Таблица 10.36. Прототип вызова KeReadStateMutex

LONG KeReadStateMutex | IRQL <= DISPATCH_LEVEL
Параметры | Возвращает состояние объекта мьютекса
IN PKMUTEX pMutex | Указатель на объект мьютекса
Возвращаемое значение | 1, если объект мьютекса находится в сигнальном состоянии

Описанные выше простые мьютексы применяются теперь не так широко, как быстрые мьютексы, которые рассмотрены ниже.

Быстрый мьютекс (fast mutex) — это синхронизационный объект, который работает практически так же, как и описанный выше обычный мьютекс режима ядра, однако не допускает повторных (рекурсивных) запросов на владение из одного и того же программного потока. Такой мьютекс выполняет меньше работы и функционирует быстрее.

Таблица 10.37. Функции для работы с объектами быстрых мьютексов

Что необходимо сделать | Используемый вызов
Создать быстрый мьютекс | ExInitializeFastMutex
Сделать запрос на владение | ExAcquireFastMutex ExAcquireFastMutexUnsafe ExTryToAcquireFastMutex
Освободить объект | ExReleaseFastMutex ExReleaseFastMutexUnsafe

Объект быстрого мьютекса описывается типом FAST_MUTEX (см. например, заголовочный файл DDK wdm.h) и используется для синхронизации доступа к одному или нескольким элементам данных. Любой код, задумавший воспользоваться этими данными, должен сначала сделать запрос на владение соответствующим объектом FAST_MUTEX.

Следует обратить внимание на то, что эти объекты имеют собственные вызовы для выполнения запроса на владение. Функция KeWaitForXxx в данном случае не может быть использована.

Перед использованием функций ExAcquireFastMutex и ExReleaseFastMutex следует выполнить инициализацию объекта быстрого мьютекса вызовом ExInitializeFastMutex, см. таблицу 10.38. И хотя память под структуру объекта выделяет инициатор этого вызова, как и в ранее описанных случаях для других объектов синхронизации, непосредственно обращаться к полям этого объекта не следует — необходимо пользоваться только вызовами, предлагаемыми в DDK.

Таблица 10.38. Прототип вызова ExInitializeFastMutex

VOID ExInitializeFastMutex | IRQL <= DISPATCH_LEVEL
Параметры | Инициализирует объект быстрого мьютекса
IN PFAST_MUTEX pFastMutex | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта быстрого мьютекса
Возвращаемое значение | void

В случае если запрос на владение вызовом ExAcquireFastMutex удовлетворен быть не может (у объекта быстрого мьютекса уже есть владельцы), поток блокируется до наступления сигнального состояния. Блокируется также и процедура APC, адресованная данному программному потоку. При успешном завершении вызова поток инициатора вызова выполняется на уровне IRQL равном APC_LEVEL, а прежнее значение сохраняется в объекте быстрого мьютекса (оно будет восстановлено при освобождении объекта быстрого мьютекса вызовом ExReleaseFastMutex).

Таблица 10.39. Прототип вызова ExAcquireFastMutex

VOID ExAcquireFastMutex | IRQL < DISPATCH_LEVEL
Параметры | Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса
Возвращаемое значение | void

Таблица 10.40. Прототип вызова ExAcquireFastMutexUnsafe

VOID ExAcquireFastMutexUnsafe | IRQL == APC_LEVEL
Параметры | Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса
Возвращаемое значение | void

В случае если запрос на владение вызовом ExAcquireFastMutexUnsafe удовлетворен быть не может, поток блокируется до наступления сигнального состояния, однако, процедура APC, адресованная данному программному потоку, не блокируется. Инициатор вызова ExAcquireFastMutexUnsafe должен обеспечить условия, чтобы во время вызова не мог быть выполнен APC вызов для данного потока. Для этого есть два способа. Первый состоит в том, чтобы увеличить уровень IRQL равный APC_LEVEL. Второй способ состоит в том, чтобы непосредственно перед вызовом ExAcquireFastMutexUnsafe выполнить KeEnterCriticalRegion, что временно блокирует простые APC вызовы (в отличие от специальных APC вызовов режима ядра). В последнем случае не следует забывать делать отменяющий вызов KeLeaveCriticalRegion.

Освобождение быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe, следует выполнять только при помощи специально для того предназначенного вызова ExReleaseFastMutexUnsafe.

Можно пытаться получить владение объектом быстрого мьютекса без блокировки вызывающего потока (в случае занятости нужного объекта быстрого мьютекса) при помощи вызова ExTryToAcquireFastMutex. B случае неудачи этот вызов возвратит значение FALSE. При удовлетворении запроса возвращается, соответственно, значение TRUE, см. таблицу 10.41.

Владение объектом быстрого мьютекса отменяется его текущим владельцем по вызову ExReleaseFastMutex. Редакция пакета DDK для XP настаивает на том, чтобы этот вызов выполнялся из кода, работающего на уровне IRQL равном DISPATCH_LEVEL, вплоть до того, что инициатор вызова должен установить явно этот уровень перед вызовом ExReleaseFastMutex. Обычно не следует об этом беспокоиться, если уровень IRQL не менялся со времени последнего вызова ExAcquireFastMutex, поскольку он автоматически устанавливает именно это значение.

Таблица 10.41. Прототип вызова ExTryToAcquireFastMutex

BOOLEAN ExTryToAcquireFastMutex | IRQL < DISPATCH_LEVEL
Параметры | Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса
Возвращаемое значение | TRUE — при успешном завершении, иначе — FALSE

Таблица 10.42. Прототип вызова ExReleaseFastMutex

VOID ExReleaseFastMutex | IRQL == APC_LEVEL
Параметры | Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса
Возвращаемое значение | void

Таблица 10.43. Прототип вызова ExReleaseFastMutexUnsafe

VOID ExReleaseFastMutexUnsafe | IRQL <= APC_LEVEL
Параметры | Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса
Возвращаемое значение | void

Спин-блокировки

Чуть позже будет рассмотрено использование изменения уровня IRQL для синхронизации доступа к данным. Однако в многопроцессорных системах изменение IRQL одного процессора никак не сказывается на значении IRQL программного кода, исполняемого на другом процессоре. То есть IRQL предоставляет способ защиты совместно используемых данных только при работе с одним процессором. Для безопасного доступа к данным в мультипроцессорной среде, Window NT использует синхронизационные объекты, называемые спин-блокировками (spin locks).

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

Если рассматривать функционально полную группу вызовов KeInitializeSpinLock — KeAcquireSpinLock — KeReleaseSpinLock, то можно сказать, что объект спин-блокировки должен запрашиваться из программного кода, работающего на уровнях IRQL ниже DISPATCH_LEVEL, а освобождается на уровне IRQL, равном DISPATCH_LEVEL.

Таблица 10.44. Прототип вызова KeInitializeSpinLock

VOID KeInitializeSpinLock | IRQL == любой
Параметры | Инициализирует объект спин-блокировки
IN PKSPIN_LOSK pSpinLock | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки
Возвращаемое значение | void

Ограничение на выделение памяти под объект спин-блокировки только из пула нестраничной памяти проистекает из того, что программный код, получивший владение объекта спин-блокировки, начинает работать на уровне DISPATCH_LEVEL.

После получения владения объектом спин-блокировки в результате вызова KeAcquireSpinLock (таблица 10.45), программный код данного потока получает уровень IRQL равный DISPATCH_LEVEL, что автоматически означает торможение всех программных потоков, выполняемых на данном процессоре с IRQL ниже DISPATCH_LEVEL. Таким образом, на этом процессоре реализуется синхронизация доступа к данным методом повышения IRQL. (Разумеется, это не спасет, если за данными обратятся из процедуры обработки прерывания, работающей на более высоких уровнях DIRQL.)

Таблица 10.45. Прототип вызова KeAcquireSpinLock

VOID KeAcquireSpinLock | IRQL <= DISPATCH_LEVEL
Параметры | Инициализирует объект спин-блокировки
IN PKSPIN_LOCK pSpinLock | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки
OUT PKIRQL pOldIrql | Место для сохранения старого значения уровня IRQL для использования позже в вызове KeReleaseSpinLock
Возвращаемое значение | void

В главе 3, в драйверной процедуре обработки IOCTL запросов был применен вызов KeAcquireSpinLock, в результате чего значение IRQL становилось равным 2 (DISPATCH_LEVEL):

00000015  0.00203462 -Example- IRQLs are old=2 ...

хотя изначально обработчик IOCTL запросов драйвера вызывается драйвером на уровне PASSIVE_LEVEL (0). Эта неявная работа вызова KeAcquireSpinLock приводит к тому, что при обработке запроса IOCTL_MAKE_SYSTEM_CRASH в драйвере Example.sys не происходит перехвата исключительной ситуации конструкцией try-exception, нормально работающей при уровне PASSIVE_LEVEL.

Таблица 10.46. Прототип вызова KeReleaseSpinLock

VOID KeReleaseSpinLock | IRQL == DISPATCH_LEVEL
Параметры | Освобождает объект спин-блокировки
IN PKSPIN_LOCK pSpinLock | Указатель на освобождаемый объект спин-блокировки
IN PKIRQL pNewIrql | Устанавливаемый данным вызовом уровень IRQL (предполагается, что это — сохраненное ранее вызовом KeAcquireSpinLock значение)
Возвращаемое значение | void

Не рекомендуется удерживать объект спин-блокировки более 25 микросекунд. Категорически не рекомендуется обращаться к страничной памяти из кода, получившего спин-блокировку: рано или поздно это приведет к краху системы.

Попытка получить объект спин-блокировки на процессоре, который уже владеет этим объектом, приводит к надежному "замерзанию" процессора. В драйвере Example.sys такая ситуация легко моделируется следующим образом. Если при выходе из обработчика IOCTL запросов не освободить объект спин-блокировки MySpinLock, то при следующем входе в этот код система "подвисает": процессор ждет, когда он сам освободит объект спин-блокировки.

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

Рассмотренный тип спин-блокировок носит название спин-блокировок выполнения (executive spin locks), и их основная область применения — охрана различных структур данных при совместном использовании несколькими программными потоками. Уровень IRQL, на котором они применимы, ограничивается значением DISPATCH_LEVEL.

Помимо рассмотренных "явно выраженных" объектов спин-блокировок выполнения (которые создаются драйвером), существуют и спин-блокировки, косвенно "ощущаемые" драйвером. Например, с объектом прерывания ассоциирован объект спин-блокировки, который практически используется при участии вызова KeSynchronizeExecution (см. таблицу 10.14 и пояснительный текст к ней). Спин-блокировки этого типа носят название спин-блокировок прерываний (interrupt spin locks), их область применения — охрана различных структур данных на уровнях приоритетов DIRQL.

Общая схема использования спин-блокировок выполнения такова.

  • Следует определить, какие элементы данных должны оберегаться и как много спин-блокировок следует использовать.
  • Дополнительные спин-блокировки позволяют более точно настроить доступ к данным. Однако когда для получения доступа необходимо получить более одного объекта спин-блокировок, возрастает опасность возникновения взаимоблокировок.
  • Резервирование памяти под структуру (структуры) типа KSPIN_LOCK в памяти нестраничного типа. Имеет смысл запомнить указатель на полученную область памяти в структуре расширения объекта устройства.
  • Инициализация спин-блокировки вызовом KeInitializeSpinLock. Этот вызов может быть сделан из кода любого уровня IRQL, хотя лучше всего это сделать там, где создается структура расширения объекта устройства (AddDevice или DriverEntry, для драйверов "в-стиле-NT").
  • Перед обращением к охраняемым данным следует выполнить получение прав на владение объектом спин-блокировки при помощи вызова KeAcquireSpinLock. Эта функция повышает значение IRQL до уровня DISPATCH_LEVEL, получает спин-блокировку и возвращает значение IRQL на момент перед вызовом (не восстанавливает, а возвращает значение в одном из параметров, см. таблицу 10.45), которое следует сохранить либо в локальной переменной (если это возможно по логике работы) либо в переменной в нестраничной памяти. Эта функция должна вызываться из кода на уровне ниже уровня DISPATCH_LEVEL IRQL.
  • Когда доступ к ресурсам завершен, следует освободить объект спин-блокировки вызовом KeReleaseSpinLock (см. таблицу 10.46), восстанавливающим ранее сохраненное значение IRQL. Это делается из кода уровня DISPATCH_LEVEL.

Дополнение к п. 5 и п. 6. Если программный код уже выполняется на уровне DISPATCH_LEVEL, то для получения спин-блокировки следует применять вызов KeAcquireSpinLockAtDpcLevel, а для освобождения, соответственно, вызов KeReleaseSpinLockFromDpcLevel, который освобождает объект спин-блокировки без изменения IRQL. Эти вызовы получают единственный параметр, указатель на объект спин-блокировки, поскольку значение IRQL теперь предполагается вполне определенным, то есть равным DISPATCH_LEVEL.

Стремление уменьшить вероятность неточного или недобросовестного программирования (в частности, исключить возможность передачи в KeReleaseSpinLock значения уровня IRQL, отличного от значения, полученного ранее из вызова KeAcquireSpinLock) привело к тому, что в Windows XP появился новый тип спин-блокировок. Этот усовершенствованный тип объектов спин-блокировки получил название квид-спин-блокировок (вольный перевод термина queued spin locks). Практически, помимо некоторого ускорения в работе, новый тип отличается для разработчика драйвера только тем, что уровень IRQL, предшествующий запросу спин-блокировки, сохраняется без его участия — он ассоциирован теперь с дескриптором, возвращаемым при запросе на владение спин-блокировкой. Можно сказать, что логически квид-спин-блокировка состоит из простой спин-блокировки и дескриптора, полученного при доступе к спин-блокировке при помощи соответствующего вызова, см. ниже. Тем не менее, нельзя смешивать работу над спин-блокировками при помощи разнотипных вызовов (то есть KeXxxQueuedSpinLockXxx, см. ниже, и KeXxxSpinLockXxx).

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

  • Создание (получение области нестраничной памяти и инициализацию вызовом KeInitializeSpinLock) объекта спин-блокировки обычным способом на уровне IRQL равном PASSIVE_LEVEL.
  • При необходимости синхронизировать доступ к охраняемым данным следует получить право на владение объектом спин-блокировки вызовом KeAcquireInStackQueuedSpinLock либо вызовом KeAcquireInStackQueuedSpinLockAtDpcLevel (в зависимости от уровня IRQL кода, из которого производится вызов — если код уже выполняется на уровне DISPATCH_LEVEL, то следует применять второй вызов). Примененный вызов возвращает (по адресу, переданному во втором параметре) дескриптор полученной спин-блокировки, которая к этому моменту уже может считаться квид-спин-блокировкой. При этом текущий программный код безусловно приобретает уровень DISPATCH_LEVEL.
  • Выполнив необходимую работу над совместно используемыми данными, драйвер должен (максимально быстро) освободить спин-блокировку либо вызовом KeReleaseInStackQueuedSpinLock, либо вызовом KeReleaseInStackQueuedSpinLockFromDpcLevel, в зависимости от того, как был получен доступ к объекту спин-блокировки ранее. Единственным параметром, который передается этим вызовам, является дескриптор квид-спин-блокировки, полученный ранее.

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


Глава 10. Программные потоки и синхронизация

Взаимоблокировки

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

Разумеется, каждый случай такой "неразберихи" уникален, тем более, что здесь может оказывать влияние еще один квази-ресурс — приоритет. Тем не менее, можно выделить три направления решения этой проблемы.

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

Второй способ состоит в пере-разбиении или укрупнении ресурсов, что сокращает "количество поводов" для разногласия.

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


Глава 10. Программные потоки и синхронизация

Заключение

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

Следующая глава будет посвящена рассмотрению двух примеров драйверов, работающих с аппаратными прерываниями LPT порта.


Глава 11. Обработка аппаратных прерываний

Глава 11

Обработка аппаратных прерываний

Обработка прерываний в драйверах уже неоднократно рассматривалась ранее, см. гл. 6, 8 и 10. Однако это происходило в теоретическом аспекте, так что пришло время применить полученные сведения на практике. Данная глава посвящена рассмотрению двух вариантов несложного драйвера LPTPORT.sys, работающего с реальными аппаратными прерываниями и позволяющего подробно и всесторонне рассмотреть их "живьем".


Глава 11. Обработка аппаратных прерываний

Постановка эксперимента

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

Тестовое приспособление CheckIt Loopback Device

Читатель вправе задать вопрос: для повторения эксперимента, с каким бы то ни было драйвером и аппаратными прерываниями на его персональном компьютере, обязательно понадобится устройство, которое эти прерывания генерирует. Где его взять?

Идея простых тестовых устройств, позволяющих тестировать параллельный порт (до сих пор все еще существующий в персональных компьютерах на пока еще существующей внутренней шине ISA) и получать в нем прерывания, возникла практически в момент появления параллельного порта. Одно из таких устройств называется "заглушка CheckIt" (CheckIt Loopback Device), которую по настоящее время производит фирма Smith Micro Software (см. Интернет сайт smithmicro.com). Данная конструкция неоднократно использовалась авторами книг по драйверам, например, Артом Бейкером и Джерри Лозано, и идеально подходит для практического ознакомления с процедурами обслуживания прерываний. Внутреннее устройство этого приспособления давно является всеобщим достоянием, и его несложно найти в Интернете. Самостоятельное "приготовление" доступно каждому и состоит в том, что следует взять стандартный 25-выводной male-разъем и замкнуть в нем 5 пар контактов. Схему для выполнения этой манипуляции можно взять, например, в книге Михаила Гука "Аппаратные средства IMM PC, Энциклопедия", 2-е издание, СПб, Питер, 2002, в разделе "Неисправности и тестирование параллельных портов". Однако чтобы не отвлекать читателя поисками столь простой схемы, приведем ее еще раз, см. рис. 11.1.

Если сопоставить схему с информацией, приведенной ранее в таблице 5.1, Регистры интерфейса стандартного параллельного порта (SPP), то становится вполне понятной идея этого приспособления. Стандартный параллельный порт имеет регистр данных DR (для ввода/вывода), регистр управления CR (для вывода) и регистр состояния SR (для ввода). Согласно схеме 11.1, выполняя вывод в регистр данных и управления, можно получать выведенные данные в регистре состояния. Поскольку один из разрядов (один из пяти используемых для вывода) поступает на вывод ACK# (бит 6 регистра состояния, SR.6), предназначенный для получения сигнала о прерывании, то получается, что передать можно 4 бита информации и сигнал о прерывании. При выводе данных в регистры DR и CR, они поступят снова в параллельный порт в регистр SR. Включив определенное воображение, можно считать, что к параллельному порту компьютера подключено "сложное" внешнее устройство, которое способно генерировать сигналы прерываний.

 Рис. 11.1
Разводка тестовой заглушки CheckIt, вид со стороны пайки

В соответствии со схемой рис. 11.1 и свойствами параллельного порта (режим SPP), получаем следующие переносы данных.

  • Бит CR.0 (Strobe#, контакт 1, единичное значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.4 (Select, контакт 13, высокий уровень напряжения на линии соответствует единичному значение бита, низкий — нулю в регистре), то есть CR.0=1 -> SR.4=0. Бит при передаче инвертируется.
  • Бит CR.1 (Auto LF#, контакт 14, единичное значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.5 (Paper End, контакт 12, высокий уровень напряжения на линии соответствует единичному значение бита, низкий — нулю в регистре), то есть CR.1=1 -> SR.5=0. Бит при передаче инвертируется.
  • Бит CR.2 (Init#, контакт 16, нулевое значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.6 (Ack#, контакт 10, высокий уровень напряжения на линии соответствует единичному значению бита, низкий — нулю в регистре), то есть CR.2=0 -> SR.6=0. Бит передается нормально.
  • Бит CR.3 (Select In#, контакт 17, единичное значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.7 (Busy, контакт 11, низкий уровень в линии соответствует единичному значению бита), то есть CR.3=1 -> SR.7=1. Бит при передаче инвертируется дважды, то есть передается нормально.
  • Бит DR.0 (нулевой бит регистра данных, вывод 2) поступает в регистр состояния как бит SR.3 (Error#, контакт 15, высокий уровень на линии соответствует единичному значению бита), то есть DR.0=1 -> SR.3 =1. Бит передается нормально.

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

В упомянутом тесте, описанном Артом Бейкером и Джерри Лозано, распознавание поступившего прерывания в ISR процедуре производится по значению бита SR.2 (PIRQ, внутренний бит 2 регистра состояния SR). В некоторых реализация параллельного порта этот бит действительно хранит флаг прерывания. Он имеет нулевое значение, если прерывание имело место (напряжение на выводе Ack#, SR.6, контакт 10, выполнило отрицательный перепад — переход из высокого в низкое значения при условии CR.4=1). Единичное значение бита SR.2 устанавливается по аппаратному с6росу или при чтении регистра состояния. Однако на всех компьютерах, доступных автору, параллельный порт в конфигурации SPP вел себя, точно следуя первоисточникам (например, ]an Axelson, Parallel Port Complete), где указывается, что бит SR.2 не используется. Соответственно, тест Арта Бейкера и Джерри Лозано не работает без отключения строки кода, выполняющего эту проверку. Строго говоря, такому обстоятельству может быть и несколько иное объяснение. В Windows XP, где разрабатывались приведенные ниже примеры, тяжело отказаться от услуг всех системных компонентов, потенциально касающихся параллельного порта. Поэтому при тестах представляемых драйверов сам стандартный системный драйвер параллельного порта было решено не отключать, а именно он (его процедура обслуживания прерывания) и считывает регистр состояния SR перед тем как получит управление Isr процедура собственно испытываемых драйверов. Даже если сам порт и позволил бы использовать бит SR, то все равно Isr процедура собственно испытываемых драйверов могла бы считать только единичное значение бита SR.2 (что говорило бы об отсутствии прерывания).

Настройка операционной системы

При работе с приводимыми тестовыми драйверами и заглушкой CheckIt использовалась операционная система Windows XP. Несмотря на ее способности к автоконфигурированию, для проведения тестов потребовались некоторые изменения в ее настройках и настройках BIOS.

Прежде всего, чтобы избежать разночтений и странных ошибок, следует выставить в BIOS компьютера настройки SPP параллельного порта по адресу 378 с использованием прерывания 7. Эти фиксированные настройки как раз и будет использовать драйвер.

Во-вторых, после загрузки операционной системы следует обратиться к настройкам системного драйвера параллельного порта, который будет выполнять начальное инициирование параллельного порта без участия испытываемых драйверов. Для этого следует выполнить Пуск — Настройка — Панель управления — Система — Свойства системы — Диспетчер устройств — Оборудование — Порты (СОМ и LPT) — Порт принтера (LPT). Запустив системный апплет "Свойства: Порт принтера (LPT1)" следует проверить, что порту выделены ресурсы портов ввода-вывода (0378) и прерывания 7. Затем в закладке "Параметры порта" указать, что стандартный системный драйвер должен использовать прерывание, см. рисунок 11.2.

Рис. 11.2
Настройки системного драйвера для использования прерываний

Использование системного драйвера обусловлено тем, что в противном случае пришлось бы самостоятельно заниматься регистрацией ресурсов LPT порта как устройства шины ISA. Теоретически это не является большим затруднением, однако на практике регистрация этих ресурсов всегда завершается неудачей, поскольку ресурсы оказываются выделенными другим системным компонентам. В данном случае испытываемые драйверы объявляют совместное использование прерывания, что не вызывает затруднений при их запуске и работе "рядом" с системным драйвером.

Используемые инструментальные программы

Запуск драйверов выполнялся при помощи программы Monitor (из пакета Numega Driver Studio) после полной загрузки операционной системы. Этот простой способ запуск драйверов позволяет проследить все диагностические сообщения всех рабочих процедур испытываемых драйверов, начиная от DriverEntry и заканчивая процедурой DriverUnload.

Сборка драйвера выполнялась в отладочной среде Win 2K Checked Build Environment, что уже стало традицией для примеров данной книги. Драйвер собирался как Legacy Driver при помощи определений ntddk.h и простейшего файла Sources:

TARGETNAME=LPTPort
TARGETTYPE=DRIVER
TARGETPATH=.
INCLUDES= $(BASEDIR)\inc;.
SOURCES=driver.cpp

Компиляция драйверов как Legacy Driver обеспечила отсутствие проблем при старте и остановке драйверов программой Monitor. Программный код драйверов размещен в двух файлах Driver.cpp (собственно исполняемые процедуры) и Driver.h (заголовочный файл). Для двух вариантов драйверов, работающих с прерываниями, которые описываются в данной главе, исходные тексты приводятся ниже полностью по причине необходимости большого количества дополнительных комментариев.

Поскольку драйверы компилировались в отладочной среде DDK, это позволило выводить отладочную диагностику при помощи условно компилируемых фрагментов вида:

#if DBG==1
    DbgPrint( "LPTPORT: Interrupt %d converted to kIrql = %d, "
              "kAffinity = %d, kVector = %X(hex)\n",
              pDevExt->Irq, kIrql, kAffinity, kVector);
#endif

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

При работе с объектом события во втором варианте драйвера привлекалась программа WinObj, которая подтвердила создание именованного объекта события "BaseNamedObjects\LPTPORT_EVENT" в соответствующей ситуации.

Кроме того, при отсутствии документированного описания должного уровня детализации на функцию IoCreateSynchronizationEvent во всех версиях DDK (включая примеры) было предпринято дизассемблирование некоторых системных драйверов при помощи дизассемблера IDA. Это позволило установить причину отказа данной функции от нормального завершения при вызовах с целью создания именованного объекта события в драйвере и правильно выполнить ее вызов.

Для интерпретации ошибок по их коду использовалась программа ErrLook, описанная ранее.


Глава 11. Обработка аппаратных прерываний

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

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

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

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

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

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


Глава 11. Обработка аппаратных прерываний

Заголовочный файл Driver.h

В заголовочном файле, относящемся к первому варианту драйвера, описывается структура расширения объекта устройства, которую разработчик драйвера определяет самостоятельно. В этой структуре сохранены имена устройства, символьная ссылка, внутренние рабочие буферы для данных, подлежащих выдаче в параллельный порт, и для данных, уже прошедших через параллельный порт. Данные второго буфера можно передать по запросу Win32 вызова ReadFile, при условии завершения переноса, "заказанного" предшествующим Win32 вызовом WriteFile. Размер рабочих буферов задается макроопределением MAX_BUFFER_SIZE.

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

Здесь же, в структуре расширения, сохраняется указатель на создаваемый объект прерывания pIntObj и резервируется место под DPC объект DpcForIsr_Object.

Макроопределения WriteControlRegister, WriteDataRegister и ReadStatusRegister скрывают использование HAL определений, предназначенных для чтения и записи в порт ввода вывода, в данном случае в параллельный порт 378.

//===================================================================================
// Driver.h - заголовочный файл для драйвера обслуживания
// заглушки CheckIt ( Вариант 1 )

// By SVP, 20 June 2004
//===================================================================================
#pragma once

extern "C" {
#include <NTDDK.h>
}

#define MAX_BUFFER_SIZE (32)

typedef struct _DEVICE_EXTENSION
{
    PDEVICE_OBJECT  pDevice;

    UNICODE_STRING  ustrDeviceName;	 // внутреннее имя устройства
    UNICODE_STRING  ustrSymLinkName; // внешнее имя (символьная ссылка)

    UCHAR deviceOutBuffer[MAX_BUFFER_SIZE], // для вывода в устройство
          deviceInBuffer [MAX_BUFFER_SIZE]; // для получения из устройства

    ULONG  xferCount, // текущий передаваемый байт
           xferRest;  // остаток непереданных байт (индикатор завершенности)
    //=============================================
    PUCHAR portBase;  // адрес порта ввода/вывода
    ULONG Irq;        // Irq в терминах шины ISA для параллельного порта
    //=============================================
    PKINTERRUPT pIntObj;	        // interrupt object
    KDPC        DpcForIsr_Object;   // DPC object

} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

// Маски для выделения бит в регистре управления:
#define CR_NOT_RST 0x04	// CR.2 - 0 Reset printer
#define CR_INT_ENB 0x10	// CR.4 - 1 Interrupt enable
#define CR_DEFAULT 0xC0	// неиспользуемые биты

// Макроопределения для записи и чтения в параллельный порт
#define DATA_REG          0
#define STATUS_REG        1
#define CONTROL_REG       2

#define WriteControlRegister( pDeviceExtension, byte )   \
     ( WRITE_PORT_UCHAR( pDeviceExtension->portBase + CONTROL_REG, byte ) )

#define WriteDataRegister( pDeviceExtension, byte )      \
     ( WRITE_PORT_UCHAR( pDeviceExtension->portBase + DATA_REG, byte ) )

#define ReadStatusRegister( pDeviceExtension )           \
     ( READ_PORT_UCHAR( pDeviceExtension->portBase + STATUS_REG ) )

Исполняемый код драйвера

В файле Driver.cpp размещен исходный текст всех функций драйвера.

В процедуре DriverEntry выполняется регистрация процедур DriverUnload (отвечает за завершающие операции при выгрузке драйвера), DispatchCreate (при получении клиентом дескриптора для доступа к драйверу), DispatchClose (при закрытии дескриптора, полученного для доступа к драйверу), DispatchWrite (обработка IRP пакета, поступившего вследствие вызова WriteFile в приложении-клиенте), DispatchRead (обработка IRP пакета, поступившего вследствие вызова ReadFile в приложении-клиенте).

Действия по созданию объекта устройства, символьной ссылки и подключению драйвера к прерыванию в данном Legacy драйвере тоже выполняются в DriverEntry, только лишь оформлены они в виде автономной функции CreateDevice. (В WDM драйвере реального PnP устройства эти операции следовало бы выполнять в процедуре AddDevice и обработчике IRP_MJ_PNP + IRP_MN_START_DEVICE, поскольку загрузка драйвера является только частью старта PnP устройства).

//=========================================================================
// Файл driver.c
// Драйвер обслуживания заглушки CheckIt (параллельный порт 378h)
// By SVP, 20 June 2004
//=========================================================================
#include "driver.h"

// Предварительные объявления функций
static NTSTATUS CreateDevice (   IN PDRIVER_OBJECT pDriverObject,
                    IN ULONG          portBase,
                    IN ULONG          Irq  );
static NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
static NTSTATUS DispatchClose  ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
static VOID     DriverUnload   ( IN PDRIVER_OBJECT pDriverObject );
static NTSTATUS DispatchWrite  ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
static NTSTATUS DispatchRead   ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );

BOOLEAN Isr  ( IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext );

BOOLEAN DoNextTransfer ( IN PVOID pContext );

VOID DpcForIsr( IN PKDPC pDpc,
           IN PVOID DeferredContext,
           IN PVOID pArg1, IN PVOID pArg2 );
//=========================================================================
// Функция:     DriverEntry
// Назначение:  Инициализирует драйвер, подключает объект устройства для
//              получения прерываний.
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
//              pRegistryPath - указатель на Юникод-строку,
//              обозначающую раздел Системного Реестра, созданный
//              для данного драйвера.
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
extern "C" NTSTATUS DriverEntry (  IN PDRIVER_OBJECT  pDriverObject,
                                   IN PUNICODE_STRING pRegistryPath )
{
    NTSTATUS status;
    #if DBG==1
    DbgPrint("LPTPORT: in DriverEntry, RegistryPath is:\n     %ws. \n",
                   pRegistryPath->Buffer);
    #endif
    // Регистрируем рабочие процедуры драйвера:
    pDriverObject->DriverUnload = DriverUnload;

    pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
    pDriverObject->MajorFunction[IRP_MJ_CLOSE]  = DispatchClose;
    pDriverObject->MajorFunction[IRP_MJ_WRITE]  = DispatchWrite;
    pDriverObject->MajorFunction[IRP_MJ_READ]   = DispatchRead;

    // Работа по созданию объекта устройства, подключению
    // ресурсов, прерывания, созданию символьной ссылки:
    status = CreateDevice(pDriverObject, 0x378, 0x7);

return status;
}
//=========================================================================
// Функция:     CreateDevice
// Назначение:  Создание устройства с точки зрения системы
// Аргументы: pDriverObject - поступает от Диспетчера ввода/вывода
//            portBase - адрес базового регистра параллельного порта (378h)
//            Irq - прерывание (в терминах шины ISA) для обслуживания порта
// Возвращаемое значение:
//            NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS CreateDevice ( IN PDRIVER_OBJECT   pDriverObject,
                        IN ULONG            portBase,
                        IN ULONG            Irq   )
{
    NTSTATUS status;
    PDEVICE_OBJECT pDevObj;
    PDEVICE_EXTENSION pDevExt;
    // Создаем внутреннее имя устройства

    UNICODE_STRING devName;
    RtlInitUnicodeString( &devName, L"\\Device\\LPTPORT" );
    // Создаем объект устройства
    status= IoCreateDevice( pDriverObject,
                            sizeof(DEVICE_EXTENSION),
                            &devName,
                            FILE_DEVICE_UNKNOWN,
                            0,
                            TRUE,
                            &pDevObj
                            );
    if (!NT_SUCCESS(status))  return status;

    // Будем использовать метод буферизации BUFFERED_IO
    pDevObj->Flags |= DO_BUFFERED_IO;
    // Заполняем данными структуру Device Extension
    pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    pDevExt->pDevice        = pDevObj;   // сохраняем - это пригодится
    pDevExt->ustrDeviceName = devName;
    pDevExt->Irq            = Irq;
    pDevExt->portBase       = (PUCHAR)portBase;
    pDevExt->pIntObj        = NULL;
    pDevExt->xferRest       = 0; // сейчас нет неотправленных данных
    pDevExt->pIntObj        = NULL;
    //================================================
    // Инициализируем объект DPC для последующего использования
    // при обработки прерываний:
    KeInitializeDpc( &(pDevExt->DpcForIsr_Object),
                     DpcForIsr,
                     pDevExt  // <- pDeferredContext в функции DpcForIsr
                     );
    //================================================
    // На всякий случай блокируем поступление прерываний:
    WriteControlRegister ( pDevExt, CR_DEFAULT );
    //================================================
    // Создаем и подключаем объект прерываний:
    KIRQL     kIrql;
    KAFFINITY kAffinity;

    ULONG kVector =
        HalGetInterruptVector(Isa, 0, pDevExt->Irq, pDevExt->Irq,
                              &kIrql,    &kAffinity);
    // Замечание. Для Isa шины второй параметр (номер шины) обычно
    // равен 0, а третий и четвертый параметры равны.

    #if DBG==1
        DbgPrint( "LPTPORT: Interrupt %d converted to kIrql = %d, "
        "kAffinity = %d, kVector = %X(hex)\n",
        pDevExt->Irq, kIrql, kAffinity, kVector);
    #endif

    status =
       IoConnectInterrupt (
                 &pDevExt->pIntObj, // Здесь будет создан Interrupt Object
                 Isr,       // Наша функция ISR
                 pDevExt,   // Этот указатель ISR функция будет
                            // получать при вызове (контекстный указатель)
                 NULL,      // Не будем использовать spin-блокировку для
                            // безопасного доступа к совместно используемым
                            // данным
                 kVector,   // транслированное значение прерывания
                 kIrql,     // DIRQL
                 kIrql,     // DIRQL
                 Latched,   // Прерывание по перепаду
                 TRUE,      // Совместно используемое (Shared) прерывание
                 kAffinity, // Поцессоров в мультипроцессорной системе
                 FALSE );   // Не сохранять значения регистров сопроцессора
    if (!NT_SUCCESS(status))
    {
        // В случае неудачи удаляем объект устройства
        IoDeleteDevice( pDevObj );
        return status;
    }
    #if DBG==1
        DbgPrint("LPTPORT: Interrupt successfully connected.\n");
    #endif
    //================================================
    // Создаем символьную ссылку:
    UNICODE_STRING symLinkName;
    // Сформировать символьное имя:
    //#define SYM_LINK_NAME L"\\??\\LPTPORT0"
    //                        ^^ проходит только в NT
    // Для того, чтобы работало в Windows 98 & XP :
    #define SYM_LINK_NAME L"\\DosDevices\\LPTPORT0"

    RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME );

    // Создать символьную ссылку:
    status = IoCreateSymbolicLink( &symLinkName, &devName );
    if (!NT_SUCCESS(status))
    {	// При неудаче - отключаемся от прерывания и
        // удаляем объект устройства:
        IoDisconnectInterrupt( pDevExt->pIntObj );
        IoDeleteDevice( pDevObj );
        return status;
    }

    pDevExt->ustrSymLinkName = symLinkName;
    #if DBG==1
        DbgPrint("LPTPORT: Symbolic Link is created: %ws. \n",
                           pDevExt->ustrSymLinkName.Buffer);
    #endif
return STATUS_SUCCESS;
}

Работа c DPC процедурами может проходить по двум существенно различающимся сценариям. В первом из них, который будет реализован в следующем варианте драйвера, DPC процедура соотносится с объектом устройства вызовом IoInitializeDpcRequest, и код драйвера может запланировать ее вызов путем применения IoRequestDpc со ссылкой на объект устройства. Таким образом, за объектом устройства можно закрепить одну DPC функцию. А сам DPC объект "обитает" в объекте устройства и его не рекомендуется "касаться" непосредственно.

Другой сценарий, реализуемый ниже, предлагает связывание DPC объекта (неинициализированный DPC объект — это просто область памяти под структурой типа KDPC) с одной из функций драйвера вызовом KeInitializeDpc, см. таблицу 10.23. Такой инициализированный DPC объект может быть вставлен в очередь DPC объектов с помощью вызова KeInsertQueueDpc — так можно запланировать к вызову связанную с ним DPC функцию драйвера в любом месте кода драйвера, правда, работающем при уровне IRQL не ниже DISPATCH_LEVEL. При использовании данного сценария, драйвер (в том числе, его процедура обработки прерывания) может планировать для последующего вызова разные DPC функции. Заметим, что, временно повысив IRLQ при помощи вызова KeRaiseIrql, драйвер может планировать вызовы DPC функций при помощи KeInsertQueueDpc даже внутри кода, работающего при IRQL, равном PASSIVE_LEVEL.

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

//=========================================================================
// Функция:     DriverUnload
// Назначение:  Останавливает и удаляет объекты устройств, отключает
//              прерывания, подготавливает драйвер к выгрузке.
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID DriverUnload ( IN PDRIVER_OBJECT pDriverObject )
{
    #if DBG==1
    DbgPrint("LPTPORT: in DriverUnload now\n");
    #endif
    PDEVICE_OBJECT	pNextObj = pDriverObject->DeviceObject;

    // Проход по всем устройствам, контролирумым драйвером
    for( ; pNextObj!=NULL; )
    {
        PDEVICE_EXTENSION pDevExt =
            (PDEVICE_EXTENSION)pNextObj->DeviceExtension;
        // Удаляем объект прерываний:
        if (pDevExt->pIntObj)
        {
            // На всякий случай блокируем поступление прерываний
            // и очищаем DPC очередь от нашего DPC объекта
            WriteControlRegister( pDevExt, CR_DEFAULT);
            KeRemoveQueueDpc( &(pDevExt->DpcForIsr_Object) );
            IoDisconnectInterrupt( pDevExt->pIntObj );
        }
        // Удаляем символьную ссылку:
        IoDeleteSymbolicLink(&pDevExt->ustrSymLinkName);
        #if DBG==1
        DbgPrint("LPTPORT: SymLink %ws deleted\n",
                pDevExt->ustrSymLinkName.Buffer);
        #endif
        // Сохраняем ссылку на следующее устройство и удаляем
        // текущий объект устройства:
        pNextObj = pNextObj->NextDevice;
        IoDeleteDevice( pDevExt->pDevice );
    }
    // Замечание. Поскольку мы использовали ресурс (параллельный порт)
    // объявленные не нами, то освобождение этого ресурса можно опустить.
}
//=========================================================================
// Функция:     DispatchCreate
// Назначение:  Обрабатывает запрос по поводу Win32 вызова CreateFile
// Аргументы: pDevObj - поступает от Диспетчера ввода/вывода
//            pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: STATUS_SUCCESS
//
NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    #if DBG==1
        DbgPrint("LPTPORT: in DispatchCreate now\n");
    #endif
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0; // ни одного байта не передано
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return STATUS_SUCCESS;
}
//=========================================================================
// Функция:      DispatchClose
// Назначение:   Обрабатывает запрос по поводу Win32 вызова CloseHandle
// Аргументы:    pDevObj - поступает от Диспетчера ввода/вывода
//               pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: STATUS_SUCCESS
//
NTSTATUS DispatchClose ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    #if DBG==1
        DbgPrint("LPTPORT: in DispatchClose now\n");
    #endif
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0; // ни одного байта не передано
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:     DispatchWrite
// Назначение:  Обрабатывает запрос по поводу Win32 вызова  WriteFile
// Аргументы:   pDevObj - поступает от Диспетчера ввода/вывода
//              pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS DispatchWrite ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    #if DBG==1
        DbgPrint("LPTPORT: in DispatchWrite now\n");
    #endif
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
    PDEVICE_EXTENSION  pDevExt   =
                (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
    ULONG              xferSize  = pIrpStack->Parameters.Write.Length;

    if( xferSize == 0 ) // Нет данных для передачи :
    {
        #if DBG==1
            DbgPrint("LPTPORT: DispatchWrite: no bytes to transfer.\n");
        #endif
        pIrp->IoStatus.Status = STATUS_SUCCESS;
        pIrp->IoStatus.Information = 0; // Нет переноса
        IoCompleteRequest( pIrp, IO_NO_INCREMENT );
        return STATUS_SUCCESS;
    }
    if( pDevExt->xferRest> 0 )
    {	// Не начинаем обрабатывать новый запрос, если остались
        // непереданные данные (в буфере deviceOutBuffer драйвера)
        #if DBG==1
            DbgPrint("LPTPORT: DispatchWrite: not all data transferred\n");
        #endif
        pIrp->IoStatus.Status = STATUS_DEVICE_BUSY;
        pIrp->IoStatus.Information = 0; // Нет переноса
        IoCompleteRequest( pIrp, IO_NO_INCREMENT );
        return STATUS_DEVICE_BUSY;
    }
    if( xferSize > MAX_BUFFER_SIZE )
    {
        // Слишком большой запрос. Завершаем обработку IRP пакета:
        #if DBG==1
            DbgPrint( "PLPTPORT: DispatchWrite: xferSize > MAX_BUFFER_SIZE\n" );
        #endif
        pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
        pIrp->IoStatus.Information = 0; // Нет переноса
        IoCompleteRequest( pIrp, IO_NO_INCREMENT );
        return STATUS_INSUFFICIENT_RESOURCES;
    }
    // Буфер с данными, поступивший от клиента, переносим в
    // рабочий буфер:
    PUCHAR userBuffer = (PUCHAR)pIrp->AssociatedIrp.SystemBuffer;
    RtlCopyMemory( pDevExt->deviceOutBuffer, userBuffer, xferSize );

    pDevExt->xferRest  = xferSize;
    pDevExt->xferCount = 0;

    // Запускаем перенос данных в первый раз
    KeSynchronizeExecution( pDevExt->pIntObj,
                            DoNextTransfer,
                            pDevExt );
    // Формально -- передача завершена:
    pIrp->IoStatus.Information = xferSize;
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}

Обработчик запросов от Win32 вызова WriteFile переносит данные во внутренний буфер deviceOutBuffer и инициирует процесс переноса вызовом DoNextTransfer при посредничестве KeSyncronizeExecution. Последний повышает текущий уровень IRQL работы до уровня, ассоциированного с объектом прерывания, указанного в качестве первого параметра pDevExt->pIntObj. В результате (это будет видно позже в распечатке log-файла из программы DebugView) код функции DoNextTransfer выполняется на уровне IRQL равном IRQL кода функции ReadDataSafely и кода функции Isr, которые равны 8 в данном тесте.

//=========================================================================
// Функция:     DispatchRead
// Назначение:  Обрабатывает запрос по поводу Win32 вызова ReadFile
// Аргументы:   pDevObj - поступает от Диспетчера ввода/вывода
//              pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS DispatchRead ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    PDEVICE_EXTENSION  pDevExt =
    (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
    ULONG              xferSize, xferredSize;
    #if DBG==1
        DbgPrint("LPTPORT: in DispatchRead now\n");
    #endif

    if( pDevExt->xferRest> 0 )
    {	// Не начинаем обрабатывать новый запрос, если остались
        // непереданные данные
        #if DBG==1
            DbgPrint("LPTPORT: DispatchRead: Exists nonprocessed request\n");
        #endif
        pIrp->IoStatus.Status = STATUS_DEVICE_BUSY;
        IoCompleteRequest( pIrp, IO_NO_INCREMENT );
        return STATUS_DEVICE_BUSY;
    }

    // Определяем размер запроса:
    xferSize = pIrpStack->Parameters.Read.Length;
    if( xferSize > MAX_BUFFER_SIZE ) xferSize = MAX_BUFFER_SIZE;

    // Передаем не более данных, чем размер числа переданных байт
    xferredSize = pDevExt->xferCount;
    xferSize = (xferSize < xferredSize ? xferSize : xferredSize );
    if(xferSize> 0)
    {	// Копируем содержимое внутреннего входного буфера
        // в буфер клиента
        PVOID userBuffer = pIrp->AssociatedIrp.SystemBuffer;
        RtlCopyMemory(userBuffer, pDevExt->deviceInBuffer, xferSize);
    }

    // Завершаем обработку IRP пакета:
    pIrp->IoStatus.Status      = STATUS_SUCCESS;
    pIrp->IoStatus.Information = xferSize;	// число переданных байт
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    #if DBG==1
        DbgPrint("LPTPORT: DispatchRead: %d byted transferred.\n", xferSize);
    #endif
return STATUS_SUCCESS;
}

//=========================================================================
// Процедура обслуживания прерывания:
//
BOOLEAN Isr (IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pServiceContext;
    KIRQL             currentIrql = KeGetCurrentIrql();
    #if DBG==1
        DbgPrint("LPTPORT: In Isr procedure, ISR_Irql=%d\n", currentIrql);
    #endif
    //===============================================================
    // Строго говоря, следовало бы проверить, имело ли место
    // прерывание и наше ли это прерывание:
    // UCHAR status = ReadStatusRegister( pDevExt );
    // if( status & 0x04 ) return FALSE; // прерывания не было
    // Однако в силу упомянутых накладок с использованием бита SR.2,
    // не проверяем это вовсе, делая допущение, что если Isr получила
    // управление, то прерывание √наше.
    //===============================================================
    // Общей практикой является блокирование поступления прерываний
    // в этом месте:

    // WriteControlRegister( pDevExt, CR_DEFAULT);

    // Однако, мы не будем этого делать, чтобы не испортить данные,
    // находящиеся сейчас в Status Register. Полагаем, что кроме
    // нашего драйвера такие прерывания никто не генерирует, а драйвер
    // защищен тем, что Write запросы отвергаются до полного переноса
    // данных.
    //================================================================
    // Планируем вызов DPC процедуры для обработки прерывания позднее
    //
    KeInsertQueueDpc( &(pDevExt->DpcForIsr_Object),
                      (VOID *)NULL,  // <- Arg1 in DpcForIsr
                      (VOID *)NULL); // <- Arg2 in DpcForIsr
return TRUE;    // нормальное завершение обработки прерывания
}

Процедура обработки прерывания Isr планирует вызов DpcForIsr DPC функции через размещение DPC объекта в системной очереди. Этим ее функции и ограничиваются в столь простом драйвере.

//=========================================================================
// Код, который посылает в порт данные, вызывающие (при наличии
// CheckIt заглушки) сигнал прерывания:
//
VOID ForceInterrupt( PDEVICE_EXTENSION pDevExt, UCHAR bits )
{
    // Генерируем сигнал прерывания
    WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT );
    KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс
    // Удерживая информационные биты, снимаем импульс ACK#
    WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_NOT_RST | CR_DEFAULT  );
    KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс
    // Удерживая информационные биты, снимаем импульс ACK#
    WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT );
}
//=========================================================================
// Функция DoNextTransfer безопасно (от вмешательства кода прерывания, ISR
// функции) записывает данные в параллельный порт:
//
BOOLEAN DoNextTransfer ( IN PVOID pContext )
{

    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pContext;
    UCHAR nextByte =
        0x0F & ( pDevExt->deviceOutBuffer[pDevExt->xferCount] );
    #if DBG==1
        DbgPrint("LPTPORT: DoNextTransfer:\n");
        DbgPrint("LPTPORT: Sending 0x%02X to port %X\n",
                           nextByte, pDevExt->portBase);
    #endif
    // Отправка полубайта данных.
    //= 1 ===============================================================
    // Заглушка CheckIt работает не самым простым образом.
    // Бит 0 отсылаемого полубайта нужно отправить как бит 0
    // в Data Register
    WriteDataRegister ( pDevExt, nextByte & 0x01);
    //
    // Это бит будет считан как бит 3 из Status Register

    //= 2 ===============================================================
    // Биты 1-3 отсылаемого полубайта нужно отправить как
    // биты 0, 1 и 3 в Control Register
    UCHAR bits = (nextByte & 0x8) + ((nextByte & 0x6)>> 1);
    // Таким образом бит 2 всегда равен 0

    bits ^= 0x3; // Инвертируем биты (0 & 1) перед
                 // записью в Control Register
    // Эти биты будут считаны в Status Register как биты 4,5 и 7
    #if DBG==1
        DbgPrint("LPTPORT: generating next interrupt...\n");
    #endif
    // Собственно отправляем данные вместе с генерацией
    // сигнала прерывания:
    ForceInterrupt( pDevExt, bits );
return TRUE;
}

//=========================================================================
// Функция ReadDataSafely выполняет чтение данных из устройства
// без опасения быть прерванной кодом ISR функции:
//
BOOLEAN ReadDataSafely ( IN PVOID pContext )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pContext;
    UCHAR status = ReadStatusRegister( pDevExt );
    // Преобазуем полученные через Status register биты в понятную форму
    UCHAR readByte =
        ((status & 0x8)<< 1) | ((status & 0x30)<< 1) | (status & 0x80);
    readByte >>= 4;
    pDevExt->deviceInBuffer[pDevExt->xferCount++] = readByte;

    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint( "LPTPORT: ReadDataSafely, currentIrql=%d ReadStatus = %02X"
                  " ReadByte = %02X\n",     currentIrql,    status,    readByte );
        DbgPrint( "LPTPORT: \n");
    #endif

    pDevExt->xferRest--;  // Число непереданных байт уменьшилось
return (pDevExt->xferRest< 1 ? FALSE : TRUE ); // это значение возвратится
                // через вызов KeSynchronizeExecution
}

//=========================================================================
// Функция:    DpcForIsr
// Назначение: Данная функция начинает работу по "заказу" ISR функции
//             и выполняет ввод/вывод путем безопасного  вызова функций
//             ReadDataSafely и DoNextTransfer, то есть реализует
//             низкоуровневый ввод/вывод
// Аргументы:  Указатель на текущий DPC объект (не используется)
//             pDeferredContext - контекстный указатель - так передается
//             указатель на структуру Device Extension
// Возвращаемое значение: нет
//
VOID
DpcForIsr( IN PKDPC pDpc,             // не используется
        IN PVOID pDeferredContext,
       IN PVOID pArg1,                 // не используется
       IN PVOID pArg2                  // не используется
       )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDeferredContext;
    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: We are now in DpcForIsr, currentIrql=%d xferCount = %d\n",
                 currentIrql, pDevExt->xferCount );
    #endif
    // Безопасно читаем данные из устройства:
    BOOLEAN dataNotTransferrered =
            KeSynchronizeExecution( pDevExt->pIntObj,
                                    ReadDataSafely,
                                    pDevExt  );
    if ( dataNotTransferrered )   // остались непереданные данные :
    {
        // Если остались данные, то записываем следующую порцию
        // данных в порт и запускаем прерывание:
        KeSynchronizeExecution( pDevExt->pIntObj,
                                DoNextTransfer,
                                pDevExt
                                );
    }
    else
    {
        #if DBG==1
            DbgPrint("LPTPORT: We are now in DpcForIsr, all data transmitted.\n");
        #endif
    }
}
//=========================================================================

Приложение для тестирования драйвера

Простое консольное приложение выполняет несложные операции в последовательной манере. Здесь нет сложных одновременных обращений из многих потоков. Тестирование сводится к передаче в драйвер 17 байт и последующему ожиданию от него ответа, размером 34 байта. Разумеется, драйвер может возвратить только полученные ранее данные, ни байтом больше.

Сборку приложения можно осуществлять при помощи такого файла Sources:

TARGETNAME=test
TARGETTYPE=PROGRAM
UMTYPE=console
UMENTRY=main
UMBASE=0x400000
TARGETPATH=.
INCLUDES= $(BASEDIR)\inc

SOURCES=test.cpp
А это, собственно, исходный код тестового приложения:
//=======================================================================
// Файл тестовой программы test.cpp
//=======================================================================

#include <windows.h>
#include <stdio.h>

// Предварительное объявление
int ReadWrite(HANDLE devHandle);

#define BUFFSIZE (17)
static unsigned char outBuffer[BUFFSIZE], inBuffer[BUFFSIZE*2];

int __cdecl main()
{
    printf("\n\n\n\n\nParallel Port CheckIt Loopback Device Test Program.\n" );

    HANDLE devHandle;
    devHandle = CreateFile(  "\\\\.\\LPTPORT0",
                             GENERIC_READ | GENERIC_WRITE,
                             0,         // share mode none
                             NULL,	    // no security
                             OPEN_EXISTING,
                             FILE_ATTRIBUTE_NORMAL,
                             NULL );    // no template

    if ( devHandle == INVALID_HANDLE_VALUE )
    {
        printf("Error: can not open device PLPTPORT0. Win32 errno %d\n",
               GetLastError() );
        return -1;
    }
    printf("Congratulation. LPTPORT0 device is open.\n\n");
    //==========================================
    DWORD i=3,j=0;
    for ( ; j<sizeof(outBuffer); )  outBuffer[j++] = (unsigned char)i++;
    //==========================================
    //for(i=0; i< 100000; i++)
    //{
        int result = ReadWrite(devHandle);
    //	if(result) break;
    //}
    //==========================================
    // Завершение работы
    if ( ! CloseHandle(devHandle) )
    {
        printf("\n Error during CloseHandle: errno %d.\n", GetLastError() );
        return 5;
    }
    printf("\n\n\n Device LPTPORT0 successfully closed. Normal exit.\n");
return 0;
}

//==========================================================================
// Выделим запись и чтение данных в отдельную функцию:
//
int ReadWrite(HANDLE devHandle)
{
    //==========================================
    // Передача данных драйверу
    printf("Writing to LPTPORT0 device...\n");

    DWORD bytesWritten, outCount = sizeof(outBuffer);
    if ( !WriteFile(devHandle, outBuffer, outCount, &bytesWritten, NULL) )
    {
        printf("Error during WriteFile: errno %d.\n", GetLastError() );
        return 1;
    }
    if ( outCount != bytesWritten )      // если не все передалось:
    {
        printf("Error: while wrote %d bytes, WriteFile reported %d bytes.\n",
               outCount, bytesWritten);
        return 2;
    }
    printf("Successfully written %d bytes.\n Buffer content was: \n",
            outCount);
    for (DWORD i=0; i<bytesWritten; i++ ) printf("%02X ",outBuffer[i]);
    //==========================================
    //Sleep(10); // Ожидание 10 миллисекунд
    //==========================================
    // Получение данных из драйвера
    printf("\n\nReading from device LPTPORT0...\n");

    DWORD bytesRead, inCount = sizeof(inBuffer);
    if ( !ReadFile(devHandle, inBuffer, inCount, &bytesRead, NULL) )
    {
        printf("Error during ReadFile: errno %d.\n", GetLastError() );
        return 3;
    }
    if ( bytesRead != bytesWritten )
    {   // размер записанных и прочитанных данных не совпадает
        printf("Error: is to read %d bytes, but ReadFile reported %d bytes.\n",
                bytesWritten, inCount);
        return 4;
    }
    printf("Succesfully read %d bytes.\n Buffer content is: \n",
            bytesRead);
    for ( i=0; i<bytesRead; i++ ) printf( "%02X ", (UCHAR)inBuffer[i] );
return 0; // Нормальное завершение
}

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

L:\#ex_lpt\test\>test
Parallel Port CheckIt Loopback Device Test Program.
Congratulation. LPTPORT0 device is open.

Writing to LPTPORT0 device...
Successfully written 17 bytes.
 Buffer content was:
03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13

Reading from device LPTPORT0...
Successfully read 17 bytes.
 Buffer content is:
03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00 01 02 03

Device LPTPORT0 successfully closed. Normal exit.

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

Ниже приводится информация из отладочного вывода, перехваченного программой DebugView (log-файл этой программы). Средняя часть этого файла, сообщения с 28 по 90, опущена, поскольку в них содержится однообразная и малоинтересная информация.

00000000  0.00000000 LPTPORT: in DriverEntry, RegistryPath is:
00000001  0.00000223 \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\LPTPort.
00000002  0.00003911 LPTPORT: Interrupt 7 converted to kIrql = 8,
                              kAffinity = 1, kVector = 191(hex)
00000003  0.00004833 LPTPORT: Interrupt successfully connected.
00000004  0.00007878 LPTPORT: Symbolic Link is created: \DosDevices\LPTPORT0.
00000005  5.38805379 LPTPORT: in DispatchCreate now
00000006  5.38822867 LPTPORT: in DispatchWrite now
00000007  5.38823342 LPTPORT: DoNextTransfer:
00000008  5.38823985 LPTPORT:                SendingOxO3 to port 378
00000009  5.38824487 LPTPORT:                generating next interrupt...
00000010  5.38835690 LPTPORT: In Isr procedure, ISR_Irql=8
00000011  5.38836388 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 0
00000012  5.38837115 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus=1F
                              ReadByte=03
00000013  5.38837422 LPTPORT:
00000014  5.38837785 LPTPORT: DoNextTransfer:
00000015  5.38838260 LPTPORT:                Sending 0x04 to port 378
00000016  5.38838763 LPTPORT:                generating next interrupt...
00000017  5.38849854 LPTPORT: In Isr procedure, ISR_Irql=8
00000018  5.38850524 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 1
00000019  5.38851167 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 27
                              ReadByte = 04
00000020  5.38851446 LPTPORT:
00000021  5.38851781 LPTPORT: DoNextTransfer:
00000022  5.38852228 LPTPORT:                Sending 0x05 to port 378
00000023  5.38852731 LPTPORT:                generating next interrupt...
00000024  5.38863822 LPTPORT: In Isr procedure, ISR_Irql=8
00000025  5.38864465 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 2
00000026  5.38865135 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 2F
                              ReadByte = 05
00000027  5.38865442 LPTPORT:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
00000091  5.38991464 LPTPORT: DoNextTransfer:
00000092  5.38991939 LPTPORT:                Sending 0x0F to port 378
00000093  5.38992442 LPTPORT:                generating next interrupt...
00000094  5.39003533 LPTPORT: In Isr procedure, ISR_Irql=8
00000095  5.39004175 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount = 12
00000096  5.39004874 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = BF
                              ReadByte = 0F
00000097  5.39005181 LPTPORT:
00000098  5.39005516 LPTPORT: DoNextTransfer:
00000099  5.39005963 LPTPORT:                Sending 0x00 to port 378
00000100  5.39006494 LPTPORT:                generating next interrupt...
00000101  5.39017557 LPTPORT: In Isr procedure, ISR_Irql=8
00000102  5.39018199 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount = 13
00000103  5.39018870 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 07
                              ReadByte = 00
00000104  5.39019177 LPTPORT:
00000105  5.39019512 LPTPORT: DoNextTransfer:
00000106  5.39019959 LPTPORT:                Sending 0x01 to port 378
00000107  5.39020434 LPTPORT:                generating next interrupt...
00000108  5.39031469 LPTPORT: In Isr procedure, ISR_Irql=8
00000109  5.39032112 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount = 14
00000110  5.39032754 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 0F
                              ReadByte = 01
00000111  5.39033061 LPTPORT:
00000112  5.39033425 LPTPORT: DoNextTransfer:
00000113  5.39033872 LPTPORT:                Sending 0x02 to port 378
00000114  5.39034374 LPTPORT:                generating next interrupt...
00000115  5.39045493 LPTPORT: In Isr procedure, ISR_Irql=8
00000116  5.39046136 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount = 15
00000117  5.39046778 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 17
                              ReadByte = 02
00000118  5.39047058 LPTPORT:
00000119  5.39047393 LPTPORT: DoNextTransfer:
00000120  5.39047840 LPTPORT:                Sending 0x03 to port 378
00000121  5.39048315 LPTPORT:                generating next interrupt...
00000122  5.39059350 LPTPORT: In Isr procedure, ISR_Irql=8
00000123  5.39059992 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount = 16
00000124  5.39060663 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 1F
                              ReadByte = 03
00000125  5.39060942 LPTPORT:
00000126  5.39061333 LPTPORT: We are now in DpcForIsr, all data transmitted.
00000127  5.39104691 LPTPORT: in DispatchRead now
00000128  5.39105166 LPTPORT: DispatchRead: 17 byted transferred.
00000129  5.39139639 LPTPORT: in DispatchClose now
00000130  10.93612205 LPTPORT: in DriverUnload now
00000131  10.93615557 LPTPORT: SymLink \DosDevices\LPTPORT0 deleted

Дополнительный тест на скорость переноса

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

for(i=0; i<100000; i++)
{
    int result = ReadWrite(devHandle);
    if(result) break;
}

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

Рис. 11.3
График числа прерываний в секунду при интенсивном использовании драйвера, обслуживающего заглушку CheckIt

Если обратится к Диспетчеру задач Windows и посмотреть на график разделения времени процессора, расходуемого на обслуживание кода режима ядра и пользовательского режима, то увидим, что доля кода ядра в процентном отношении непривычно велика, см. рис. 11.4. Можно сразу же предположить, что все пользовательские процессы будут притормаживаться. И в самом деле, пока работает тестовое приложение в указанном интенсивном режиме "жизнь" в системе замирает — очень долго открываются и перемещаются окошки (время отклика на процессоре с тактовой частотой ЗГГц до 10 секунд), слегка "зависает" курсор мышки и т.п.

Рис. 11.4
Окно Диспетчера задач в момент запуска тестирующего приложения в интенсивном режиме (работа в режиме ядра занимает более 50% времени процессора)

Проанализировав приведенную информацию, несложно сделать заключение, что описанный драйвер, вполне пригодный для изучения прерываний — поскольку показывает прерывания от момента их зарождения до момента вызова программного кода, отвечающего за их обработку, оказывается не столь хорошим для реальной жизни. "Расход" прерываний в отношении 1 прерывание на перенос 1/2 байта данных является непозволительной роскошью, которая приводит к существенной деградации системы. Разумеется, лучшим вариантом использования прерываний является их генерация устройством при готовности к переносу как можно большей порции данных. Это является общей закономерностью использования прерываний.


Глава 11. Обработка аппаратных прерываний

Вариант 2. Модификация драйвера для работы с прерываниями

И хотя второй вариант драйвера не улучшает соотношения "перенос/прерывание", в нем рассматриваются несколько приемов, которые часто встречаются в драйверах Windows, исходные тексты которых доступны для анализа.

Прежде всего, это — использование системных очередей IRP пакетов для обеспечения сериализации при доступе к обслуживаемому устройству. Во-вторых, в приведенном ниже примере демонстрируется способ синхронизации работы драйвера и вызывающего его приложения по совместно используемому событию. В-третьих, используется более традиционная схема использования DPC процедур для завершения работы по поводу прерывания, которая связывает только одну DPC процедуру с объектом устройства и использует вызовы IoInitializeDpcRequest и IoRequestDpc. Наконец, практически показано, как можно использовать для хранения временной информации незадействованные поля IRP пакетов, здесь — Parameters.DeviceIoControl.Type3InputBuffer в составе текущей (на момент обработки) ячейки стека IRP пакета.

Помимо упомянутых отличий, в драйвере используется механизм IOCTL запросов от приложения для операций чтения и записи в устройство, тогда как обработчиков запросов от Win32 вызовов WriteFile и ReadFile в данной реализации драйвера нет вовсе.

Условия компиляции и сборки драйвера и тестирующего приложения не изменились.


Глава 11. Обработка аппаратных прерываний

Заголовочный файл Driver.h

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

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

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

//=============================================================================
// Driver.h - заголовочный файл для драйвера обслуживания заглушки CheckIt
// ( Вариант 2 )
// By SVP, 21 June 2004
//=============================================================================
#pragma once
extern "C" {
#include <NTDDK.h>
}
#define MAX_BUFFER_SIZE (32)
#define IOCTL_SEND_TO_PORT          CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_SEND_TO_USER          CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_TAKE_EVENT            CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CLOSE_EVENT            CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef struct _DEVICE_EXTENSION
{
    PDEVICE_OBJECT  pDevice;
    UNICODE_STRING  ustrDeviceName;	 // внутреннее имя устройства
    UNICODE_STRING  ustrSymLinkName; // внешнее имя (символьная ссылка)
    UCHAR byteToBeOutToPort,         // следующий передаваемый байт данных
          deviceInBuffer[MAX_BUFFER_SIZE]; // данные, полученные из порта

    ULONG  xferCount; // получено (байт) из порта и не передано клиенту
    //=============================================
    // Только для применения в операциях передачи данных клиенту драйвера:
    PUCHAR pUserBuffer;
    ULONG  xferSize;
    //=============================================
    PUCHAR portBase;// адрес порта ввода/вывода
    ULONG  Irq;     // Irq в терминах шины ISA для параллельного порта
    //=============================================
    PKINTERRUPT pIntObj;  // interrupt object
    //=============================================
    PKEVENT pEvent; // event object - объект события
    HANDLE  hEvent; // представления объекта события через дескриптор
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

// Маски для выделения бит в регистре управления:
#define CR_NOT_RST  0x04   // CR.2 - 0 Reset printer
#define CR_INT_ENB  0x10   // CR.4 - 1 Interrupt enable
#define CR_DEFAULT  0xC0   // неиспользуемые биты

// Макроопределения для записи и чтения в параллельный порт
#define DATA_REG          0
#define STATUS_REG        1
#define CONTROL_REG       2

#define WriteControlRegister( pDeviceExtension, byte )   \
     ( WRITE_PORT_UCHAR( pDeviceExtension->portBase + CONTROL_REG, byte ) )
#define WriteDataRegister( pDeviceExtension, byte )      \
     ( WRITE_PORT_UCHAR( pDeviceExtension->portBase + DATA_REG, byte ) )
#define ReadStatusRegister( pDeviceExtension )           \
     ( READ_PORT_UCHAR( pDeviceExtension->portBase + STATUS_REG ) )


Глава 11. Обработка аппаратных прерываний

Исполняемый код драйвера

Как и в первом варианте драйвера, исходный текст всех функций драйвера представлен файлом Driver.cpp.

Часто повторяющийся код по завершению обработки IRP пакетов собран в функции CompleteIrp.

В процедуре DriverEntry дополнительно (по сравнению с первым вариантом) выполняется регистрация функции StartIo, которая будет использоваться при старте процесса обработки IRP пакетов, откладываемых в системную очередь обработчиком IOCTL запросов с IOCTL кодом IOCTL_SEND_TO_PORT.

Функция CreateDevice по-прежнему выполняет создание объекта устройства, регистрацию символьной ссылки и подключение драйвера к прерыванию. Однако теперь иначе выполняется регистрация DPC процедуры: при помощи вызова IoInitializeDpcRequest (таблица 8.14) с передачей ссылки на объект устройства создается DPC объект в "недрах" системы. И тот код, который пожелает запланировать вызов функции DpcForIsr (регистрируемой таким образом), должен выполнить вызов IoRequestDpc. Еще раз следует отметить, что в WDM драйвере реального PnP устройства эти операции следовало бы выполнять в процедуре AddDevice и обработчике IR_MJ_PNP + IRP_MN_START_DEVICE, так как загрузка драйвера является только частью старта настоящего PnP устройства.

Процедура DriverUnload (она отвечает за завершающие операции при выгрузке драйвера) более не занимается DPC объектами (считается, что драйвер теперь не имеет прямого доступа ни к одному такому объекту). Однако в обязанности этой функции теперь входит выполнение вызова ObDereferenceObject для уменьшения числа ссылок на используемый объект события, чтобы разрешить операционной системе его удаление, если эта операция будет сочтена целесообразной.

Обработчики DispatchCreate (при получении клиентом дескриптора для доступа к драйверу) и DispatchClose (при закрытии дескриптора, полученного в DispatchCreate) не претерпели изменений. Обработчики DispatchWrite и DispatchRead теперь в драйвере отсутствуют, а обработка запросов от клиента на чтение и запись данных в параллельный порт переложена на плечи функции DeviceControlRoutine (обработчик IRP пакетов, поступающих вследствие клиентских вызовов от Win32 функции DeviceIoControl).

Функционально работу модифицированного драйвера можно описать следующим образом.

Драйвер получает IOCTL запрос IOCTL_SEND_TO_PORT от клиента (тестирующего приложения), откладывает запрос в системную очередь (см. описание механизма System Queuing в 6 главе) и завершает обработку IRP пакета, помечая его как завершенный в состоянии STATUS_PENDING, указывая на всякий случай процедуру CancelRoutine драйвера. Эта процедура получит управление, если клиент решит отменить запрос. Заметим, что такая ситуация возникает, когда тестирующее приложение запускается в отсутствие заглушки CheckIt в параллельном порту. Пакет не может быть завершен, и приложение простаивает в ожидании окончания синхронного вызова DeviceIoControl — снятие приложения приводит к вызову CancelRoutine.

При извлечении IRP пакета из очереди Диспетчер ввода/вывода вызывает зарегистрированную в DriverEntry функцию StartIo, которая проверяет условия своего вызова, записывает в pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer ноль и планирует работу DPC функции DpcForIsr вызовом IoRequestDpc. Заметим, что это проходит вполне корректно (несмотря на то, что IoRequestDpc необходимо вызывать на уровнях IRQL не ниже DISPATCH_LEVEL), поскольку сама функция StartIo работает на IRQL равном DISPATCH_LEVEL.

Работа DpcForIsr при первом вызове сводится к выводу первого байта (точнее половины байта) в порт и запуску первого прерывания, после чего срабатывает цепная реакция Isr-DpcForIsr-перенос/прерывание-Isr до полного переноса всей порции данных, поступивших по IOCTL запросу IOCTL_SEND_TO_PORT от клиента (тестирующего приложения). При выполнении переноса и при условии, что предварительно клиент запросил использование объекта события, процедура DpcForIsr выполняет установку объекта события в сигнальное состояние и таким образом сигнализирует клиенту, что перенос завершен. Тут следует обратить внимание, что, при условии регистрации объекта события, когда клиент решил отменить свой запрос на запись в порт и управление получает функция CancelRoutine, то ее код должен что-то сделать с объектом события. В нашем случае он переводится в сигнальное состояние, поскольку прекращение жизни IRP пакета — это тоже (хотя и своеобразное) завершение работы над ним.

//=========================================================================
// Файл driver.cpp (Вариант 2)
// Драйвер обслуживания заглушки CheckIt (параллельный порт 378h)
// By SVP, 21 June 2004
//=========================================================================
#include "driver.h"
// Предварительные объявления функций
NTSTATUS CreateDevice   ( IN PDRIVER_OBJECT pDriverObject,
                          IN ULONG          portBase,
                          IN ULONG          Irq  );
NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
NTSTATUS DispatchClose  ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
VOID     DriverUnload   ( IN PDRIVER_OBJECT pDriverObject );
NTSTATUS DeviceControlRoutine ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
VOID     StartIo        ( IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp );
BOOLEAN  Isr            ( IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext );
VOID     DpcForIsr      ( IN PKDPC pDpc,
                          IN PVOID DeferredContext,
                          IN PVOID pArg1, IN PVOID pArg2 );
//=========================================================================
NTSTATUS CompleteIrp( PIRP pIrp, NTSTATUS status, ULONG size)
{
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = size;
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
return status;
}
//=========================================================================
// Функция:     DriverEntry
// Назначение:  Инициализирует драйвер, подключает объект устройства для
//              получения прерываний.
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
//              pRegistryPath - указатель на Юникод-строку,
//              обозначающую раздел Системного Реестра, созданный
//              для данного драйвера.
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
extern "C" NTSTATUS DriverEntry (  IN PDRIVER_OBJECT  pDriverObject,
                                   IN PUNICODE_STRING pRegistryPath )
{
    NTSTATUS status;
    #if DBG==1
        DbgPrint("LPTPORT: in DriverEntry, RegistryPath is:\n     %ws. \n",
                           pRegistryPath->Buffer);
    #endif
    // Регистрируем рабочие процедуры драйвера:
    pDriverObject->DriverUnload = DriverUnload;
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
    pDriverObject->MajorFunction[IRP_MJ_CLOSE]  = DispatchClose;
    pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =
                                                 DeviceControlRoutine;
    pDriverObject->DriverStartIo = StartIo;
    // Работа по созданию объекта устройства, подключению
    // ресурсов, прерывания, созданию символьной ссылки:
    status = CreateDevice(pDriverObject, 0x378, 0x7);
return status;
}
//=========================================================================
// Функция:     CreateDevice
// Назначение:  Создание устройства с точки зрения системы
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
//              portBase - адрес базового регистра параллельного порта (378h)
//              Irq - прерывание (в терминах шины ISA) для обслуживания порта
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS CreateDevice ( IN PDRIVER_OBJECT   pDriverObject,
                        IN ULONG            portBase,
                        IN ULONG            Irq   )
{
    NTSTATUS status;
    PDEVICE_OBJECT pDevObj;
    PDEVICE_EXTENSION pDevExt;
    // Создаем внутреннее имя устройства
    UNICODE_STRING devName;
    RtlInitUnicodeString( &devName, L"\\Device\\LPTPORT" );
    // Создаем объект устройства
    status= IoCreateDevice( pDriverObject,
                sizeof(DEVICE_EXTENSION),
                &devName,
                FILE_DEVICE_UNKNOWN,
                0,
                FALSE, // TRUE - for exclusive access
                &pDevObj
                );
    if (!NT_SUCCESS(status))  return status;

    // Указывать, что будем использовать метод буферизации
    // BUFFERED_IO при отсутствии обработчиков Read/Write
    // уже не актуально, поэтому можем закоментировать:
    // pDevObj->Flags |= DO_BUFFERED_IO;
    // Заполняем данными структуру Device Extension
    pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    pDevExt->pDevice        = pDevObj;   // сохраняем - это пригодится
    pDevExt->ustrDeviceName = devName;
    pDevExt->Irq            = Irq;
    pDevExt->portBase       = (PUCHAR)portBase;
    pDevExt->pIntObj        = NULL;
    pDevExt->xferCount      = 0; // сейчас нет полученных данных
    pDevExt->pIntObj        = NULL;
    pDevExt->pEvent         = NULL;
    pDevExt->hEvent         = NULL;
    //================================================
    // Теперь не инициализируем объект DPC,
    // а закрепляем за объектом устройства (а значит и за обслуживаемым
    // им прерыванием) единственную DpcForIsr функцию:
    IoInitializeDpcRequest( pDevObj, DpcForIsr );
    //================================================
    // На всякий случай блокируем поступление прерываний:
    WriteControlRegister ( pDevExt, CR_DEFAULT );
    //================================================
    // Создаем и подключаем объект прерываний:
    KIRQL     kIrql;
    KAFFINITY kAffinity;
    ULONG kVector =
        HalGetInterruptVector(Isa, 0, pDevExt->Irq, pDevExt->Irq,
                              &kIrql,    &kAffinity);
    // Замечание. Для Isa шины второй параметр (номер шины) обычно
    // равен 0, а третий и четвертый параметры равны.
    #if DBG==1
        DbgPrint( "LPTPORT: Interrupt %d converted to kIrql = %d, "
        "kAffinity = %d, kVector = %X(hex)\n",
        pDevExt->Irq, kIrql, kAffinity, kVector);
    #endif

    status =
       IoConnectInterrupt (
        &pDevExt->pIntObj, // Здесь будет создан Interrupt Object
        Isr,       // Наша функция ISR
        pDevExt,   // Этот указатель ISR функция будет
                   // получать при вызове (контекстный указатель)
        NULL,      // Не будем использовать spin-блокировку для
                   // безопасного доступа к совместно используемым
                   // данным
        kVector,   // транслированное значение прерывания
        kIrql,	   // DIRQL
        kIrql,     // DIRQL
        Latched,   // Прерывание по перепаду
        TRUE,      // Совместно используемое (Shared) прерывание
        kAffinity, // Поцессоров в мультипроцессорной системе
        FALSE );   // Не сохранять значения регистров сопроцессора
    if (!NT_SUCCESS(status))
    {
        // В случае неудачи удаляем объект устройства
        IoDeleteDevice( pDevObj );
        return status;
    }
    #if DBG==1
        DbgPrint("LPTPORT: Interrupt successfully connected.\n");
    #endif
    //================================================
    // Создаем символьную ссылку:
    UNICODE_STRING symLinkName;
    // Сформировать символьное имя:
    //#define	SYM_LINK_NAME	L"\\??\\LPTPORT0"
    //                              ^^ проходит только в NT
    // Для того, чтобы работало в Windows 98 & XP :
    #define SYM_LINK_NAME L"\\DosDevices\\LPTPORT0"
    RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME );

    // Создать символьную ссылку:
    status = IoCreateSymbolicLink( &symLinkName, &devName );
    if (!NT_SUCCESS(status))
    {	// При неудаче - отключаемся от прерывания и
        // удаляем объект устройства:
        IoDisconnectInterrupt( pDevExt->pIntObj );
        IoDeleteDevice( pDevObj );
        return status;
    }
    pDevExt->ustrSymLinkName = symLinkName;
    #if DBG==1
        DbgPrint("LPTPORT: Symbolic Link is created: %ws. \n",
                           pDevExt->ustrSymLinkName.Buffer);
    #endif
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:     DriverUnload
// Назначение:  Останавливает и удаляет объекты устройств, отключает
//              прерывания, подготавливает драйвер к выгрузке.
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID DriverUnload ( IN PDRIVER_OBJECT pDriverObject )
{
    #if DBG==1
        DbgPrint("LPTPORT: in DriverUnload now\n");
    #endif
    PDEVICE_OBJECT pNextObj = pDriverObject->DeviceObject;
    // Проход по всем устройствам, контролируемым драйвером
    for( ; pNextObj!=NULL; )
    {
        PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pNextObj->DeviceExtension;
        // Удаляем объект прерываний:
        if (pDevExt->pIntObj)
        {
            // На всякий случай блокируем поступление прерываний
            WriteControlRegister( pDevExt, CR_DEFAULT);
            IoDisconnectInterrupt( pDevExt->pIntObj );
        }
        // Удаляем символьную ссылку:
        IoDeleteSymbolicLink(&pDevExt->ustrSymLinkName);
        #if DBG==1
            DbgPrint("LPTPORT: SymLink %ws deleted\n",
                               pDevExt->ustrSymLinkName.Buffer);
        #endif
        // Сообщаем о прекращении использования объекта события
        if(pDevExt->pEvent!=NULL)
        {
            ObDereferenceObject(pDevExt->pEvent);
            #if DBG==1
                DbgPrint("LPTPORT: Event object dereferenced.\n");
            #endif
        }
        // Сохраняем ссылку на следующее устройство и удаляем
        // текущий объект устройства:
        pNextObj = pNextObj->NextDevice;
        IoDeleteDevice( pDevExt->pDevice );
    }
    // Замечание. Поскольку мы использовали ресурс (параллельный порт)
    // объявленные не нами, то освобождение этого ресурса можно опустить.
}
//=========================================================================
// Функция:    DispatchCreate
// Назначение: Обрабатывает запрос по поводу Win32 вызова CreateFile
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: STATUS_SUCCESS
//
NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    #if DBG==1
        DbgPrint("LPTPORT: in DispatchCreate now\n");
    #endif
    CompleteIrp( pIrp, STATUS_SUCCESS, 0 ); // ничего не передано
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:    DispatchClose
// Назначение: Обрабатывает запрос по поводу Win32 вызова CloseHandle
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: STATUS_SUCCESS
//
NTSTATUS DispatchClose ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    PDEVICE_EXTENSION  pDevExt =
             (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
    #if DBG==1
        DbgPrint("LPTPORT: in DispatchClose now\n");
    #endif
    // Сообщаем о прекращении использования объекта события
    if(pDevExt->pEvent!=NULL)
    {
        // Удаляем объект события, поскольку клиент заканчивает
        // работу с драйвером и не сможет больше отдать команду
        // о прекращении работы с событием:
        #if DBG==1
            DbgPrint("LPTPORT: DispatchClose, attempt to close handle %04X.\n",
                               pDevExt->hEvent );
        #endif
        NTSTATUS sts = ZwClose(pDevExt->hEvent);
        pDevExt->pEvent = NULL;
        pDevExt->hEvent = NULL;
        #if DBG==1
            DbgPrint("LPTPORT: DispatchClose, event handle closed (status=%0X).\n",
                               sts );
        // sts == 0 <==> sts == STATUS_SUCCESS
        #endif
    }
    CompleteIrp( pIrp, STATUS_SUCCESS, 0 ); // ничего не передано
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:    CancelRoutine
// Назначение: Вызывается в случае отказа клиента от запроса на обработку
//             IRP пакета, который был отложен (для работы со StartIo)
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp    - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID CancelRoutine ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    PDEVICE_EXTENSION pDevExt =
            (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
    #if DBG==1
        KIRQL  currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: in CancelRoutine now, Irql=%d, IRP Canceled.\n",
                           currentIrql );
    #endif
    if(pIrp == pDevObj->CurrentIrp)
    {
        IoReleaseCancelSpinLock(pIrp->CancelIrql);
        CompleteIrp( pIrp, STATUS_CANCELLED, 0 );
        //^^^^^^^^^ Что бы ни утверждалось в DDK, с этим
        // работает лучше! (Иначе - "глючит" Monitor)
    }
    else // Пакет - не текущий, удаляем его из системной очереди,
    {    // если он там есть:
        KeRemoveEntryDeviceQueue(
            &pDevObj->DeviceQueue,
            &pIrp->Tail.Overlay.DeviceQueueEntry);
        IoReleaseCancelSpinLock(pIrp->CancelIrql);
        CompleteIrp( pIrp, STATUS_CANCELLED, 0 );
    }
    // Во всяком случае, обработка данного пакета завершена
    if(pDevExt->pEvent!=NULL)
    {
       KeSetEvent(pDevExt->pEvent, IO_NO_INCREMENT , FALSE );
    }
    // Не повредит...
    IoStartNextPacket( pDevObj, TRUE );
}
//=================================================================================
// Функция:    StartIo
// Назначение: Вызывается для обработки отложенных IRP пакетов.
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp    - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID StartIo( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: We are now in StartIo , currentIrql=%d\n", currentIrql );
    #endif
    PDEVICE_EXTENSION   pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    PIO_STACK_LOCATION  pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
    PUCHAR userBuffer;
    ULONG  xferSize;
    switch( pIrpStack->MajorFunction )
    {
       case IRP_MJ_DEVICE_CONTROL:
       {
        ULONG controlCode =
            pIrpStack->Parameters.DeviceIoControl.IoControlCode;
        if( controlCode != IOCTL_SEND_TO_PORT )
        {  // Сюда мы не должны попадать:
           #if DBG==1
               DbgPrint("LPTPORT: StartIo: bad IOCTL.");
           #endif
           CompleteIrp( pIrp, STATUS_NOT_SUPPORTED, 0 );
           IoStartNextPacket( pDevObj, FALSE );
           break;
        }
        // Очищаем место, где будем хранить номер следующего байта в
        // клиентском буфере, который следует передать:
        pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer=(PVOID)0;
        //=============================================================
        // Можно было сделать как и в варианте 1 для запуска переноса:
        // KeSynchronizeExecution( pDevExt->pIntObj,
        //                         DoNextTransfer,
        //                         pDevExt );
        //
        // Но пойдем усложненным путем: внесем изменения в DpcForIsr
        // и DoNextTransfer, затем запланируем вызов DpcForIsr.
        // Замечание. Функцию IoRequestDpc для планирования DpcForIsr
        // нужно вызывать с уровня IRQL не ниже DISPATCH_LEVEL,
        // а StartIo работает как раз на этом уровне IRQL:
        #if DBG==1
            DbgPrint("LPTPORT: StartIo: IoRequestDpc will be called now.");
        #endif
        IoRequestDpc( pDevObj, NULL, NULL );
        //=============================================================
           break;
       }
       default:
       {    // Сюда мы не должны попадать:
        #if DBG==1
            DbgPrint("LPTPORT: StartIo: bad IOCTL (in 'switch-default').");
        #endif
        CompleteIrp( pIrp, STATUS_NOT_SUPPORTED, 0 );
        IoStartNextPacket( pDevObj, FALSE );
        break;
       }
    }
}

Как было сказано, работа с процедурой StartIo и системной очередью незавершенных IRP пакетов без особых усилий со стороны разработчика драйвера дает сериализацию (последовательную обработку) пакетов, отражающих поступившие запросы от клиентов драйвера. Если драйвер зарегистрировал процедуру StartIo и затем в обработчиках запросов от клиента ставит IRP пакеты в очередь на обработку соответствующим образом (вызовом IoStartPacket и т.д., что выполняет обработчик DeviceControlRoutine в отношении пакетов с IOCTL кодом, равным IOCTL_SEND_TO_PORT), то Диспетчер ввода/вывода в каждый момент времени будет "выдавать" драйверу (точнее, объекту устройства) не более чем по одному пакету. Эти порции так и будут попадать в процедуру StartIo в связке "объект устройства"-"IRP пакет". Не нужно организовывать особых "синхронизационных" мероприятий по правильному и безопасному совместному использованию устройства. Процедура StartIo должна только запустить процесс (последовательность событий, а не программный!), в результате которого когда-то завершится обработка IRP пакета (успешно или нет, не имеет принципиального значения). Это может быть запуск устройства (прерывание, извещающее о готовности устройства, может вызвать к работе ISR процедуру и целую цепочку событий) или тривиальное программирование последующего запуска отложенной процедуры, как в данном случае. До момента объявления IRP пакета "завершенным" Диспетчер ввода/вывода не вызовет StartIo снова, несмотря на то, что сама процедура StartIo могла уже давно завершить свою работу.

Однако если устройство занято, то появляется желание и далее эксплуатировать механизм системной очереди незавершенных запросов (System Queuing), и поставить запрос снова в эту же очередь, в надежде на то, что очень скоро устройство будет все-таки готово воспринять запрос. В тех случаях, когда в этом действительно имеется необходимость и драйвер собственными силами ведет очередь незавершенных пакетов, ничто не мешает это сделать. Однако в системной очереди это не проходит — повторный вызов IoStartPacket с только что извлеченным из системной очереди IRP пакетом надежно "заклинивает" системную очередь. В этом имеется своя логика — если устройство не смогло обработать запрос сейчас, то зачем его снова вставлять в очередь IRP пакетов с такими же параметрами? Они все равно не смогут быть обработаны. Лучшее решение состоит в том, чтобы вернуть управление с соответствующим кодом завершения клиенту, который сам решит, как лучше поступить.

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

В коде данного драйвера часто встречаются фрагменты, когда указатель на текущий IRP пакет извлекается из поля CurrentIrp в структуре объекта устройства. Возникает вопрос: а всегда ли так можно поступать? Ведь это достаточно простой и удобный способ получения ссылки на IRP пакет в любом месте драйвера, которым так хочется пользоваться! Ответ: к сожалению, далеко не всегда и не везде. Диспетчер ввода/вывода устанавливает в поле CurrentIrp в структуре объекта устройства значение указателя на IRP пакет только для текущего пакета, который участвует в работе через системную очередь (System Queuing). Этот IRP пакет сейчас один из всех, которые возможно были занесены в эту очередь данным драйвером или, иными словами, данным объектом устройства, поступил на обработку в функцию StartIo и до окончания его обработки (с каким либо кодом завершения в одной из функций драйвера) другого адреса в поле CurrentIrp не появится. Соответственно, если разработчик так написал драйвер, что адрес такого IRP пакета где-то "оседает" (задерживается, например, в параллельно работающем системном потоке данного драйвера), то это может быть источником краха системы, поскольку обращение к области хранения полностью завершенных IRP пакетов совершенно некорректно.
//=========================================================================
// Процедура обслуживания прерывания:
//
BOOLEAN Isr (IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pServiceContext;
    PDEVICE_OBJECT    pDevObj = pDevExt->pDevice;
    #if DBG==1
        KIRQL         currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: In Isr procedure, ISR_Irql=%d\n", currentIrql);
    #endif
    //===============================================================
    // Строго говоря,  следовало бы проверить, имело ли место
    // прерывание и наше ли это прерывание:
    // UCHAR status = ReadStatusRegister( pDevExt );
    // if( status & 0x04 ) return FALSE; // прерывания не было
    // Однако в силу упомянутых накладок с использованием бита SR.2,
    // не проверяем это вовсе, делая допущение, что если Isr получила
    // управление, то прерывание - наше.
    //===============================================================
    // Общей практикой является блокирование поступления прерываний
    // в этом месте:

    // WriteControlRegister( pDevExt, CR_DEFAULT);

    // Однако, мы не будем этого делать, чтобы не испортить данные,
    // находящиеся сейчас в Status Register. Полагаем, что кроме
    // нашего драйвера такие прерывания никто не генерирует, а драйвер
    // защищен тем, что Write запросы отвергаются до полного переноса
    // данных.
    //================================================================
    // Планируем вызов DPC процедуры для обработки прерывания позднее
    //
    IoRequestDpc( pDevObj,  // <- указание на объект устройства здесь
                    // однозначно определяет, какая процедура
                // DPC (здесь - DpcForIsr) будет вызвана
              NULL,     // <- Arg1 in DpcForIsr
              NULL );   // <- Arg2 in DpcForIsr

return TRUE;    // нормальное завершение обработки прерывания
}
//=========================================================================
// Код, который посылает в порт данные, вызывающие (при наличии
// CheckIt заглушки) сигнал прерывания:
//
VOID ForceInterrupt( PDEVICE_EXTENSION pDevExt, UCHAR bits )
{
    // Генерируем сигнал прерывания
    WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT );
    KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс
    // Импульс ACK#
    WriteControlRegister(
        pDevExt,
        bits | CR_INT_ENB | CR_NOT_RST | CR_DEFAULT  );
    KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс
    // Удерживая информационные биты, снимаем импульс ACK#
    WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT );
}
//=========================================================================
// Функция DoNextTransfer безопасно (от вмешательства кода прерывания, ISR
// функции) записывает данные в параллельный порт:
//
BOOLEAN DoNextTransfer ( IN PVOID pContext )
{

    PDEVICE_EXTENSION pDevExt  = (PDEVICE_EXTENSION)pContext;
    UCHAR             nextByte = 0x0F & (pDevExt->byteToBeOutToPort);

    #if DBG==1
        DbgPrint("LPTPORT: DoNextTransfer:\n");
        DbgPrint("LPTPORT: Sending 0x%02X to port %X\n",
                           nextByte, pDevExt->portBase);
    #endif
    // Отправка полубайта данных.
    //= 1 ===============================================================
    // Заглушка CheckIt работает не самым простым образом.
    // Бит 0 отсылаемого полубайта нужно отправить как бит 0
    // в Data Register
    WriteDataRegister ( pDevExt, nextByte & 0x01);
    //
    // Это бит будет считан как бит 3 из Status Register

    //= 2 ===============================================================
    // Биты 1-3 отсылаемого полубайта нужно отправить как
    // биты 0, 1 и 3 в Control Register
    UCHAR bits = (nextByte & 0x8) + ((nextByte & 0x6)>> 1);
    // Таким образом бит 2 всегда равен 0

    bits ^= 0x3; // Инвертируем биты (0 & 1) перед
             // записью в Control Register
    // Эти биты будут считаны в Status Register как биты 4,5 и 7
    #if DBG==1
        DbgPrint("LPTPORT:         generating next interrupt...\n");
    #endif
    // Собственно отправляем данные вместе с генерацией
    // сигнала прерывания:
    ForceInterrupt( pDevExt, bits );
return TRUE;
}
//=========================================================================
// Функция ReadDataSafely выполняет чтение данных из устройства
// без опасения быть прерванной кодом ISR функции:
//
BOOLEAN ReadDataSafely ( IN PVOID pContext )
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pContext;
    UCHAR             status  = ReadStatusRegister( pDevExt );

    // Преобазуем полученные через Status register биты
    // в понятную форму
    UCHAR readByte =
        ((status & 0x8)<< 1) | ((status & 0x30)<< 1) | (status & 0x80);
    readByte >>= 4;
    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: ReadDataSafely, currentIrql=%d ReadStatus = %02X"
                 " ReadByte = %02X\n",     currentIrql,    status,    readByte );
        DbgPrint( "LPTPORT: \n");
    #endif
    if(pDevExt->xferCount < MAX_BUFFER_SIZE)
    {
       // Помещаем принимаемые во внутренний буфер драйвера,
       // только если его размер не исчерпан:
       pDevExt->deviceInBuffer[pDevExt->xferCount++] = readByte;
    }
return TRUE;
}
//=========================================================================
// Функция TransferToUserSafely выполняет перенос данных из внутреннего
// буфера драйвера без опасения быть прерванной кодом ISR функции:
//
BOOLEAN TransferToUserSafely ( IN PVOID pContext )
{
    PDEVICE_EXTENSION pDevExt  = (PDEVICE_EXTENSION)pContext;
    ULONG             xferReq  = pDevExt->xferSize;
    PUCHAR            inBuffer = pDevExt->deviceInBuffer;

    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: TransferToUserSafely, currentIrql=%d\n",currentIrql );
        DbgPrint("         requested %d bytes, while ready %d bytes.\n",
                 xferReq,  pDevExt->xferCount);
    #endif

    if(  pDevExt->xferCount< 1  ||  xferReq< 1  )
    {	// Нет никаких полученных данных или нулевой запрос
        pDevExt->xferSize = 0;
        return FALSE;
    }
    if( xferReq > MAX_BUFFER_SIZE ) xferReq = MAX_BUFFER_SIZE;
    // Передаем не более данных, чем все, полученные из LPT порта,
    // оказавшиеся во внутреннем буфере драйвера:
    if( xferReq > pDevExt->xferCount ) xferReq = pDevExt->xferCount;

    // Собственно перенос запрошенных данных в буфер клиента:
    RtlCopyMemory( pDevExt->pUserBuffer, inBuffer, xferReq );

    if( xferReq < pDevExt->xferCount)
    {   // Перемещаем оставшиеся данные к началу буфера:
        ULONG i=0,j=xferReq;
        for(; j<pDevExt->xferCount; ) inBuffer[i++]=inBuffer[j++];
    }
    pDevExt->xferCount -= xferReq;
    pDevExt->xferSize   = xferReq;
    #if DBG==1
        DbgPrint("         Transferred %d, the rest %d bytes.\n",
                 xferReq, pDevExt->xferCount);
    #endif
return TRUE;
}
//=========================================================================
// Функция:    DpcForIsr
// Назначение: Данная функция начинает работу по "заказу" ISR функции
//             и выполняет ввод/вывод путем безопасного  вызова функций
//             ReadDataSafely и DoNextTransfer, то есть реализует
//             низкоуровневый ввод/вывод
// Аргументы:  Указатель на текущий DPC объект (не используется)
//             pDeferredContext - контекстный указатель - так
//             передается указатель на структуру объекта устройства
// Возвращаемое значение: нет
//
VOID
DpcForIsr( IN PKDPC pDpc,              // не используется
           IN PVOID pDeferredContext,  // Внимание! Теперь здесь
                                       // находится указатель pDevObj !
           IN PVOID pArg1,             // не используется
           IN PVOID pArg2              // не используется
           )
{
    PDEVICE_OBJECT     pDevObj   = (PDEVICE_OBJECT)pDeferredContext;
    PDEVICE_EXTENSION  pDevExt   =
                (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    PIRP               pIrp      = pDevObj->CurrentIrp;
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
    ULONG 	total =
        pIrpStack->Parameters.DeviceIoControl.InputBufferLength,
            currentByteNumToWriteToPort =
        (ULONG)pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;

    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: We are now in DpcForIsr, currentIrql=%d xferCount = %d\n",
                           currentIrql, pDevExt->xferCount );
    #endif

    if( currentByteNumToWriteToPort > 0 ) // <- т.е. уже были передачи
    {   // Безопасно читаем следующий байт из устройства:
        KeSynchronizeExecution( pDevExt->pIntObj,
                      ReadDataSafely,
                      pDevExt  );
    }
    if ( currentByteNumToWriteToPort < total )
    {  // Если остались данные, то записываем следующий байт
       // данных в порт и запускаем прерывание:
       PUCHAR userBuffer = (PUCHAR)pIrp->AssociatedIrp.SystemBuffer;
       pDevExt->byteToBeOutToPort =
                 userBuffer[currentByteNumToWriteToPort];
       #if DBG==1
           DbgPrint("LPTPORT: currentByteNo = %d, byteToBeOutToPort=%02X(hex)\n",
                           currentByteNumToWriteToPort, pDevExt->byteToBeOutToPort );
       #endif
       // Заранее корректируем номер следующего байта для записи в порт
       // и сохраняем его там же:
       pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer =
                   (PVOID)( currentByteNumToWriteToPort+1 );
       KeSynchronizeExecution( pDevExt->pIntObj,
                   DoNextTransfer,
                   pDevExt );
    }
    else
    {
       #if DBG==1
           DbgPrint("LPTPORT: We are now in DpcForIsr, all data transmitted.\n");
       #endif
       if(pDevExt->pEvent!=NULL)
       {
        // Устанавливаем объект события в сигнальное состояние,
        // что является сигналом клиету о полностью завершенном
        // переносе данных в параллельный порт
        KeSetEvent(pDevExt->pEvent, IO_NO_INCREMENT , FALSE );
        //^^^^^^^^^^^^^^^ Замечание А.
       }
       // Завершаем обработку текущего IRP пакета.
       // Поскольку это была обработка пакета от Win32 вызова
       // DeviceIoControl, который передавал данные в драйвер,
       // а назад ничего не ожидал,
       // то указываем, что число байт, возвращаемых клиенту
       // (третий аргумент вызова CompleteIrp), равно 0:
       PIRP pIrp = pDevObj->CurrentIrp;
       CompleteIrp( pIrp, STATUS_SUCCESS, 0 );

       // Сообщаем Диспетчеру ввода/вывода, что готовы обработать
       // следующий пакет:
       IoStartNextPacket( pDevObj, FALSE );
    }
return;
}
//=========================================================================
// Функция:     DeviceControlRoutine
// Назначение:  Обрабатывает запросы от Win32 вызова DeviceIoControl.
// Аргументы:   pDevObj - поступает от Диспетчера ввода/вывода,
//              pIrp - поступает от Диспетчера ввода/вывода.
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
    NTSTATUS           status = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIrpStack=IoGetCurrentIrpStackLocation(pIrp);
    PDEVICE_EXTENSION  pDevExt =
               (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    //-------------------------------
    #if DBG==1
        KIRQL currentIrql = KeGetCurrentIrql();
        DbgPrint("LPTPORT: We are now in DeviceControlRoutine, currentIrql=%d\n",
                           currentIrql );
    #endif
    // Диспетчеризация по IOCTL кодам:
    switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode )
    {
    case IOCTL_SEND_TO_PORT:
    {
       // Размер данных, поступивших от клиента драйвера
       // (см. таблицу 8.9):
       ULONG xferSize =
        pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
       if( xferSize < 1 )
       {    // Нет данных для отправки в порт:
        #if DBG==1
            DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,\n",
                     "         no bytes to transfer.\n");
        #endif
        // Завершение без переноса данных клиенту:
         return CompleteIrp( pIrp, STATUS_SUCCESS, 0 );
       }
       // if( xferSize > MAX_BUFFER_SIZE ) ? Но размер входящих данных
       // уже не актуален, поскольку будет использоваться буфер,
       // поступивший от клиента (точнее, от Диспетчера ввода/вывода).
       //
       // Теперь мы ничего не копируем, а просто инициируем процесс
       // программируемого вывода, откладывая пакет в очередь
       // необработанных IRP пакетов (не забываем сообщить адрес
       // процедуры CancelRoutine, которая должна получить управление,
       // если клиент решит отозвать свой запрос):
       #if DBG==1
           DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,\n"
                    "         xfer size is %d Irp is pending.\n", xferSize );
       #endif
       IoMarkIrpPending( pIrp );
       IoStartPacket( pDevObj, pIrp, 0, CancelRoutine);
       return STATUS_PENDING;
    }
    case IOCTL_SEND_TO_USER:
    {
       // Размер данных, ожидаемых пользователем
       pDevExt->xferSize =
        pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
       if( pDevExt->xferSize> 0 )
       {
        // Согласно таблице 8.9, адрес клиентского буфера при
        // методе METHOD_BUFFERED в IOCTL запросе находится там
        // же, что и при обработке обычного запроса Read (см.
        // первый вариант драйвера, функцию DispatchRead):
        pDevExt->pUserBuffer =
                (PUCHAR)pIrp->AssociatedIrp.SystemBuffer;
        // Пытаемся безопасно перенести данные:
        KeSynchronizeExecution( pDevExt->pIntObj,
                                TransferToUserSafely,
                                pDevExt    );
       }
       // Завершаем обработку IRP пакета:
       #if DBG==1
           DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_USER,\n"
                    "         %d bytes transferred to user.\n", pDevExt->xferSize);
       #endif
       return CompleteIrp( pIrp, STATUS_SUCCESS, pDevExt->xferSize);
    }
    case IOCTL_TAKE_EVENT:
    {
       // Размер данных, поступивших от клиента драйвера
       ULONG xferFromDriverSize =
         pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
       if( xferFromDriverSize < sizeof(HANDLE))
       {
        #if DBG==1
            DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n         "
                     "event info can not be transferred due to 0 buffer size.\n");
        #endif
        status = STATUS_INVALID_PARAMETER;
        // Завершение без переноса данных клиенту:
         return CompleteIrp( pIrp, status, 0 ); // 0 - Нет переноса
       }
       if(pDevExt->pEvent==NULL) // Объект события еще не был создан
       {                         // для данного клиента.
        // Создаем объект события типа
        // SynchronizationEvent, с автоматическим переходом в
        // несигнальное состояние:
        #define  EVENT_NAME L"\\BaseNamedObjects\\LPTPORT_EVENT"
        UNICODE_STRING eventName;
        RtlInitUnicodeString( &eventName, EVENT_NAME );

        HANDLE  hEvent;  // Объект события - без имени:
        PKEVENT pEvent =
                IoCreateSynchronizationEvent( &eventName, &hEvent );
        if(pEvent==NULL)
        {
            // Объект события не был создан
            #if DBG==1
                DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n"
                         "         error - event wasn't created.\n");
            #endif
            // Завершение без переноса данных клиенту:
            return CompleteIrp( pIrp, STATUS_UNSUCCESSFUL, 0 );
        }
        #if DBG==1
            DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n"
                     "      event named %ws successfully created.\n",
                     eventName.Buffer);
        #endif
        pDevExt->pEvent = pEvent;
        pDevExt->hEvent = hEvent;
        // Предустанавливаем объект события в несигнальное состояние:
        KeClearEvent(pDevExt->pEvent);
       }
       // Cообщаем об объекте события клиенту - передаем дескриптор
       RtlCopyMemory( pIrp->AssociatedIrp.SystemBuffer,
                      &pDevExt->hEvent,
                      sizeof(HANDLE) );
       #if DBG==1
           DbgPrint( "LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n"
                  "         event handle = %04X(hex) is sent to user.\n",
                  pDevExt->hEvent);
       #endif
       return CompleteIrp( pIrp, STATUS_SUCCESS, sizeof(HANDLE) );
    }
    case IOCTL_CLOSE_EVENT:
    {
       if(pDevExt->pEvent!=NULL) // объект события был создан
       {
        NTSTATUS sts = ZwClose(pDevExt->hEvent);
        #if DBG==1
            if(sts==STATUS_SUCCESS)
                DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,\n"
                         "         event handle closed with STATUS_SUCCESS.\n");
            DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,\n"
                     "         event (handle %04Xhex) closing status = %d.\n",
                     pDevExt->hEvent, sts );
        #endif
        // Во всяком случае, эти событием пользоваться не будем:
        pDevExt->pEvent = NULL;
        pDevExt->hEvent = NULL;
       }
       return CompleteIrp( pIrp, STATUS_SUCCESS, 0 );
    }
    default:
    {
        #if DBG==1
            DbgPrint("LPTPORT: DeviceControlRoutine: bad IOCTL code.");
        #endif
        // Завершение без переноса данных клиенту:
        status = STATUS_INVALID_DEVICE_REQUEST;
        CompleteIrp( pIrp, status, 0 );
    }
    } // <- конец оператора "switch"
return status;
}
//=================================================================================

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

Драйвер обрабатывает четыре IOCTL кода, которые может задать клиент при своем обращении к драйверу (из приложения пользовательского режима это делается через Win32 вызов DeviceIoControl):

  • IOCTL_SEND_TO_PORT обеспечивает отправку данных драйверу. При обработке этого запроса драйвер "откладывает" IRP пакет в системную очередь, передавая работу функции StartIo.
  • IOCTL_SEND_TO_USER обеспечивает передачу данных, накопившихся на текущий момент в рабочем буфере deviceInBuffer (см. описание структуры расширения объекта устройства для второго варианта драйвера в файле Driver.h), то есть полученных из параллельного порта через механизм прерываний.
  • IOCTL_TAKE_EVENT передает клиенту дескриптор объекта события, если оно было создано или создает его, сохраняя данные в полях pEvent и hEvent структуры расширения объекта устройства (см. описание структуры расширения объекта устройства для второго варианта драйвера в файле Driver.h).
  • IOCTL_CLOSE_EVENT закрывает дескриптор текущего используемого объекта события.

Если речь идет о взаимодействии приложения пользовательского режима с драйвером, то, как правило, событие создается именно в приложении пользовательского режима. Операционная система по Win32 вызову CreateEvent создает событие как объект режима ядра, возвращая приложению открытый дескриптор. Этот дескриптор передается в драйвер (через IOCTL запрос), а драйвер при помощи вызова ObReferenceObjectByHandle получает ссылку на созданный объект события по указателю на существующий объект (ссылка на объект является в режиме ядра более привычным способом обращения с событием, мьютексом и т.д.). Описание подобного способа совместного использования события в режиме ядра и пользовательском режиме несложно отыскать в литературе или в интернете, например, на Интернет сайте codeguru.com. Особенностью же данного драйвера является то, что событие изначально создается в драйвере (в режиме ядра), и лишь после этого соответствующий ему дескриптор передается клиенту. Для этого используется практически не применяемый разработчиками вызов IoCreateSynchronizationEvent (не применяемый по причине плохого описания в документации пакета DDK всех имеющихся на сегодня версий). Между тем этот удобный вызов, вся "особая" специфика использования которого состоит в правильном задании имени создаваемого объекта (это же относится и к вызову режима ядра IoCreateNotificationEvent), возвращает не только указатель на создаваемый объект, но и его дескриптор одновременно.

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

Данный драйвер (и первый, и второй варианты) в том виде как они приведены, не позволяют одновременно получить доступ к нему из нескольких приложений, поскольку объект устройства создается для эксклюзивного доступа. По этой причине Win32 вызов CreateFile, который должен получить дескриптор доступа к драйверу, завершается с ошибкой 5 (отказано в доступе). Для того чтобы исправить ситуацию необходимо предпоследний параметр вызова IoCreateDevice в драйверной функции CreateDevice задать равным FALSE (вместо TRUE, как указано изначально).

Еще один вопрос-опасение, который может возникнуть у внимательного читателя: как обстоят дела с доступом ко входным буферам с данными и для данных в смысле корректности уровня IRQL, на которых к ним обращается драйвер? Например, копирование в пользовательский буфер полученных данных по IOCTL запросу IOCTL_SEND_TO_USER производится функцией TransferToUserSafely, защищенной KeSynchronizeExecution, то есть работающей на уровне IRQL, равном IRQL процедуры обслуживания прерывания Isr. Почему не происходит сбоя, поскольку достоверно известно, что пользовательские области находятся в странично организованной памяти?

Ответ состоит в том, что в обоих драйверах используется буферизованный метод передачи данных при обращениях клиента к драйверу:

  • в первом варианте драйвера было указано, что объект устройства имеет метод буферизации при запросах на запись/чтение pDevObj->Flags |= DO_BUFFERED_IO;
  • во втором варианте аналогичное указание Диспетчер ввод/вывода получает в каждом IOCTL коде, когда при его определении явно указывается METHOD_BUFFERED.

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


Глава 11. Обработка аппаратных прерываний

Модификация приложения для тестирования драйвера

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

//=======================================================================
// Файл тестовой программы test.cpp
//=======================================================================

#include <windows.h>
#include <stdio.h>
#include "winioctl.h"

#define IOCTL_SEND_TO_PORT          CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_SEND_TO_USER          CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_TAKE_EVENT            CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CLOSE_EVENT            CTL_CODE( \
    FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)
int ReadWrite(HANDLE devHandle);
#define BUFFSIZE (17)
static unsigned char outBuffer[BUFFSIZE], inBuffer[BUFFSIZE*2];
static HANDLE devHandle, hEvent;

int __cdecl main()
{
    printf("\n\n\n\n\nParallel Port CheckIt Loopback Device Test Program.\n" );
    devHandle = CreateFile(  "\\\\.\\LPTPORT0",
                             GENERIC_READ | GENERIC_WRITE,
                             0,           // share mode none
                             NULL,	      // no security
                             OPEN_EXISTING,
                             FILE_ATTRIBUTE_NORMAL,
                             NULL );    // no template

    if ( devHandle == INVALID_HANDLE_VALUE )
    {
        printf("Error: can not open device PLPTPORT0. Win32 errno %d\n",
               GetLastError() );
        return -1;
    }
    printf("Congratulation. LPTPORT0 device is open.\n\n");
    //==========================================
    // Получение доступа к объекту события,
    // создаваемому в драйвере
    DWORD bytesRead;
    if ( !DeviceIoControl(devHandle,
                          IOCTL_TAKE_EVENT,
                          NULL, 0,                 // отправляем в драйвер
                          &hEvent, sizeof(HANDLE), // получаем из драйвера
                          &bytesRead,
                          NULL ) )
    {
        printf("Error during IOCTL_TAKE_EVENT: errno %d.\n",
               GetLastError() );
        CloseHandle(devHandle);
        return -1;
    }
    printf("\nEvent handle =  %04X(hex)\n", hEvent);
    //==========================================
    // Заполнение буфера данными:
    DWORD i=3,j=0;
    for ( ; j<sizeof(outBuffer); ) outBuffer[j++] = (unsigned char)i++;
    //==========================================
    ReadWrite(devHandle);
    //==========================================
    // Завершение работы
    if ( !DeviceIoControl(devHandle,
                          IOCTL_CLOSE_EVENT,
                          NULL, 0, // отправляем в драйвер
                          NULL, 0, // получаем из драйвера
                          &bytesRead,
                          NULL ) )
    {
        printf("\nError during IOCTL_CLOSE_EVENT: errno %d.\n", GetLastError() );
    }
    else printf("\nEvent handle is normally closed.\n");

    if ( ! CloseHandle(devHandle) )
    {
        printf("\n Error during CloseHandle: errno %d.\n", GetLastError() );
        return -1;
    }
    printf("\n\n\n Device LPTPORT0 successfully closed. Normal exit.\n");
return 0;
}
//==========================================================================
// передача и получение данных из CheckIt заглушки:
int ReadWrite(HANDLE devHandle)
{
    //==========================================
    // Передача данных драйверу
    printf("Writing to LPTPORT0 device...\n");

    DWORD bytesReturned, outCount = sizeof(outBuffer);
    if ( !DeviceIoControl(devHandle,
                          IOCTL_SEND_TO_PORT,
                          outBuffer, outCount, // отправляем в драйвер
                          NULL, 0,             // получаем из драйвера
                          &bytesReturned,
                          NULL ) )
    {
        printf("Error during IOCTL_SEND_TO_PORT: errno %d.\n", GetLastError() );
        return 11;
    }
    printf( "Successfully transferred %d bytes.\n"
            "Buffer content was: \n",  outCount);
    for (DWORD i=0; i<outCount; i++ ) printf("%02X ",outBuffer[i]);
    //==========================================
    // Использование созданного события
    DWORD result = WaitForSingleObject(hEvent,10);

    switch(result) {
    case WAIT_TIMEOUT:   printf("\nWait timeout.\n");break;
    case WAIT_ABANDONED: printf("\nWait WAIT_ABANDONED.\n"); break;
    default:             printf("\nWait default case.\n");
    }
    //==========================================
    // Получение данных из драйвера
    printf("\n\nReading from device LPTPORT0...\n");
    DWORD bytesRead, inCount = sizeof(inBuffer);
    if ( !DeviceIoControl(devHandle,
                          IOCTL_SEND_TO_USER,
                          NULL, 0,           // отправляем в драйвер
                          inBuffer, inCount, // получаем из драйвера
                          &bytesRead,
                          NULL ) )
    {
        printf("Error during OCTL_SEND_TO_USER: errno %d.\n", GetLastError() );
        return 12;
    }
    if ( bytesRead != outCount )
    {   // размер записанных и прочитанных данных не совпадает
        printf("Error: is to read %d bytes,\n"
               "but IOCTL_SEND_TO_USER reported %d bytes.\n",
               outCount, inCount);
        return 13;
    }
    printf("Succesfully read %d bytes.\n Buffer content is: \n", bytesRead);
    for ( i=0; i<bytesRead; i++ ) printf( "%02X ", (UCHAR)inBuffer[i] );

return 0; // Нормальное завершение
}

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

Рис. 11.5
Именованный объект события "LPTPORT_EVENT", созданный драйвером, в рабочем окне программы WinObj

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

Parallel Port CheckIt Loopback Device Test Program.
Congratulation. LPTPORT0 device is open.

Event handle = 07CC(hex)
Writing to LPTPORT0 device...

Successfully transferred 17 bytes.
 Buffer content was:
03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13
Wait default case.

Reading from device LPTPORT0...
Successfully read 17 bytes.
 Buffer content is:
03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00 01 02 03
Event handle is normally closed.

Device LPTPORT0 successfully closed. Normal exit.

Ниже приводится информация из отладочного вывода, перехваченного программой DebugView (log-файл этой программы). Средняя часть этого файла (сообщения с 35 по 137) опущена, как и в первом случае, поскольку в этих строках содержится однообразная и малоинтересная информация.

00000000  0.00000000 LPTPORT: in DriverEntry, RegistryPath is:
00000001  0.00000223          \REGISTRY\MACHINE\SYSTEM\ControlSet001\ Services\LPTPort.
00000002  0.00003101 LPTPORT: Interrupt 7 converted to kIrql = 8,
                              kAffinity = 1, kVector = 191(hex)
00000003  0.00004023 LPTPORT: Interrupt successfully connected.
00000004  0.00006761 LPTPORT: Symbolic Link is created: \DosDevices\LPTPORT0.
00000005  3.62195949 LPTPORT: in DispatchCreate now
00000006  3.62206817 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0
00000007  3.62210393 LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,
00000008  3.62210616          event named \BaseNamedObjects\LPTPORT_EVENT
                              successfully created.
00000009  3.62211231 LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,
00000010  3.62211426          event handle = 07CC(hex) is sent to user.
00000011  3.62228496 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0
00000012  3.62229082 LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,
00000013  3.62229250          xfer size is 17 Irp is pending.
00000014  3.62229864 LPTPORT: We are now in StartIo , currentIrql=2
00000015  3.62230256 LPTPORT: StartIo: IoRequestDpc will be called now.
00000016  3.62230982 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 0
00000017  3.62231485 LPTPORT: currentByteNo = 0, byteToBeOutToPort=03(hex)
00000018  3.62231848 LPTPORT: DoNextTransfer:
00000019  3.62232323 LPTPORT:                Sending 0x03 to port 378
00000020  3.62232826 LPTPORT:                generating next interrupt...
00000021  3.62243972 LPTPORT: In Isr procedure, ISR_Irql=8
00000022  3.62244671 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 0
00000023  3.62245341 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 1F
                              ReadByte = 03
00000024  3.62245621 LPTPORT:
00000025  3.62246096 LPTPORT: currentByteNo = 1, byteToBeOutToPort=04(hex)
00000026  3.62246459 LPTPORT: DoNextTransfer:
00000027  3.62246906 LPTPORT:                Sending 0x04 to port 378
00000028  3.62247409 LPTPORT:                generating next interrupt...
00000029  3.62258471 LPTPORT: In Isr procedure, ISR_Irql=8
00000030  3.62259170 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 1
00000031  3.62259812 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus= 27
                              ReadByte= 04
00000032  3.62260120 LPTPORT:
00000033  3.62260595 LPTPORT: currentByteNo =2, byteToBeOutToPort=05(hex)
00000034  3.62260958 LPTPORT: DoNextTransfer:
. . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . .
00000138  3.62448468 LPTPORT: DoNextTransfer:
00000139  3.62448915 LPTPORT:                Sending 0x02 to port 378
00000140  3.62449417 LPTPORT:                generating next interrupt...
00000141  3.62460480 LPTPORT: In Isr procedure, ISR_Irql=8
00000142  3.62461123 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount = 15
00000143  3.62461765 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus= 17
                              ReadByte= 02
00000144  3.62462073 LPTPORT:
00000145  3.62462548 LPTPORT: currentByteNo = 16, byteToBeOutToPort=13(hex)
00000146  3.62462883 LPTPORT: DoNextTransfer:
00000147  3.62463330 LPTPORT:                Sending 0x03 to port 378
00000148  3.62463833 LPTPORT:                generating next interrupt...
00000149  3.62474868 LPTPORT: In Isr procedure, ISR_Irql=8
00000150  3.62475566 LPTPORT: We are now in DpcForIsr, currentIrql=2
                              xferCount= 16
00000151  3.62476209 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus= 1F
                              ReadByte= 03
00000152  3.62476488 LPTPORT:
00000153  3.62476879 LPTPORT: We are now in DpcForIsr, all data transmitted.
00000154  3.62529372 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0
00000155  3.62529875 LPTPORT: TransferToUserSafely, currentIrql=8
00000156  3.62530322          requested 34 bytes, while ready 17 bytes.
00000157  3.62530769          Transferred 17, the rest 0 bytes.
00000158  3.62531271 LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_USER,
00000159  3.62531439          17 bytes transferred to user.
00000160  3.62566360 LPTPORT: We are now in DeviceControlRoutine, currentIrql=0
00000161  3.62567449 LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,
00000162  3.62567645          event handle closed with STATUS_SUCCESS.
00000163  3.62568231 LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,
00000164  3.62568427          event (handle 07CChex) closing status = 0.
00000165  3.62577506 LPTPORT: in DispatchClose now
00000166  6.71423547 LPTPORT: in DriverUnload now
00000167  6.71426704 LPTPORT: SymLink \DosDevices\LPTPORT0 deleted


Глава 11. Обработка аппаратных прерываний

Заключение

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

Драйвер обладает определенными недостатками. Обслуживая не-PnP устройство, он опять-таки построен по методике Legacy (как драйвер "в стиле NT"), a не как современный WDM драйвер. Кроме того, для простоты реализации, он использует ресурсы, обозначенные другим, системным драйвером, не уделив должного внимания собственному "захвату" аппаратных ресурсов с последующей регистрацией их в операционной системе.

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

Первая, упрощенная версия драйвера LPTPort.sys, знакомит с общей методикой обработки прерываний в Windows NT 5.x. Вторая, усложненная версия вводит в использование механизмов системных очередей IRP пакетов как метода обеспечения последовательного доступа к устройству, а также знакомит с приемами совместного использования объектов события приложениями пользовательского режима и драйверами.

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

Следующая глава будет посвящена рассмотрению вопросов установки драйверов с использованием inf-файлов.


Глава 12. Инсталляция драйверов при помощи INF файлов

Глава 12

Инсталляция драйверов при помощи INF файлов

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

Пример, приведенный в главе 3, предлагал два способа установки — при помощи SCM Менеджера и при помощи inf-файла.

Первый способ запускает драйвер непосредственно из приложения, которое также может выполнить как остановку, так и выгрузку драйвера. (Следует, однако, признать, что не все драйверы поддаются работе через сервисы SCM Менеджера). Этот удобный (особенно при экспериментах по изучению системных вызовов режима ядра) способ практически полностью описан в главе 3, поэтому рассмотрим подробнее инсталляцию с использованием inf-файлов.

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

Еще одним, третьим, способом инсталляции можно считать создание специального приложения пользовательского режима, которое выполняет ту же работу (копирование файлов и внесение новой информации в Системный Реестр). Однако такая методика не может быть признана приемлемой при работе с самостоятельно идентифицирующимися (PnP) устройствами: в этом случае запустившийся в ответ на обнаружение нового устройства Мастер Установки работает с inf-файлом, который одной из своих записей должен подтверждать свою "ответственность" за обнаруженное устройство. Данная глава описывает правила построения INF файлов и работы с ними.


Глава 12. Инсталляция драйверов при помощи INF файлов

Структура INF файла

Инсталляционный inf-файл является текстовым файлом, поставляемым вместе с драйверным программным обеспечением и аппаратным обеспечением, соответственно.

В операционных системах Windows 9x/Me размер inf-файл не может превосходить 64 килобайта. Для NT систем ограничений нет. Если не указано иначе для конкретного типа ОС, то максимальная длина любого поля в inf-файле составляет 512 символов.

Секции inf-файла и основные общие правила ввода записей

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

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

Весь текст, введенный в inf-файле, не различается в смысле регистра символов — все имена секций и записи могут быть введены и в верхнем, и в нижнем регистре. Поэтому слова version, VERSION и Version являются идентичными для процесса установки. Текст не должен содержать символов табуляции и других невидимых управляющих символов.

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

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

Порядок следования секций в файле не играет роли, важно лишь, чтобы секции носили корректные имена и были правильно соотнесены в перекрестных ссылках. (Правда, сами разработчики придерживаются некоторых правил, например, секция [Strings] вводится обычно последней.) Секция продолжается до объявления начала следующей секции, либо до обнаружения конца файла. Имя секции должно быть уникальным для данного inf-файла. Хотя в некоторых источниках указывается, что содержимое секций, имеющие одинаковые имена, системное программное обеспечение, занимающееся интерпретацией inf-файла, объединяет, тем не менее, практика показывает, что это не так. Например, из двух секций [Strings] принимается во внимание только содержимое первой (по тексту inf-файла).

Имя секции не должно содержать более 28 символов для Windows 9x и более 255 символов для Windows NT. Ссылки на секции могут содержать в своем составе пробелы, но только если имя в целом заключено в кавычки (то же относится и к символу точки с запятой). Допустимы точка и символ подчеркивания. Например, как указывает документация DDK, строка в кавычках ";; Std Mfg " является приемлемой ссылкой на имя секции, если указываемая секция имеет имя, в точности совпадающее с содержимым внутри кавычек, а именно [;; Std Mfg ].

В теле секции информация представляется в форме записей. Общий формат элемента секции для inf-файлов, применяемых в Windows:

entry = value[,value[,value...]]

где entry является ключевым словом (начало директивы) либо маркером (ссылкой на значение — такая ссылка обособляется символами %...%). Параметры value являются значениями, соотносимыми с полем entry (именно, соотносимыми, а не присваиваемыми). В редких типах секций в роли поля entry могут выступать имена файлов (например, в секции [SourceDiskFiles]) или имена других секциях (например, в секции [DestinationDirs]).

В inf-файлах для нескольких систем в секциях для Windows 9x все запятые должны присутствовать в указанном в документации количестве, а в секциях для NT замыкающие перечисления запятые (если значения опущены) можно опускать (то есть в тех секциях, имена которых которые оформлены суффиксами .nt, .ntx86, и т.п.). Например, запись в секции SourceDisksFiles в общей нотации описывается следующим образом:

filename = diskid[, [subdir] [, size] ]

Пропуская значение subdir, указываем значение size и оставляем две запятых в середине:

filename = diskid,,size

Если пропускаем два значения (subdir и size) в inf-файле для NT, то запись выглядит так:

filename = diskid

Для Windows9x это требуется ввести иначе:

filename = diskid,,

Секция описания версии [Version]

Корректно составленный inf-файл начинается с секции [Version], которая является заголовком и меткой для всего драйверного inf-файла. Допустимые и необходимые записи внутри секции [Version] перечисляются в таблице 12.1.

Таблица 12.1. Элементы секции [Version]

Записи | Значения
Signature | Обязательная запись. Одно из указанных ниже значений "$Windows NT" — для ОС ряда Windows NT "$Windows 95" — для ОС Windows 9x/Me "$Chicago$" — для всех версий ОС, поддерживающих WDM драйвера
Class | Имя класса для целого семейства драйверов. Некоторые имена, например, Net, Display или Unknown зарезервированы (предопределены). В секции [Version] должна быть либо директива Class, соответствующая типу устройства, обслуживаемого устанавливаемым драйвером, либо ClassGuid, либо обе сразу.
ClassGuid | Уникальный GUID идентификатор для класса устройства, которое обслуживает данный набор драйверного программного обеспечения (см. таблицу 12.2).
Provider | Поставщик INF файла, наименование организации и т.п.
LayoutFile | Используется только в INF файлах, поставляемых с операционной системой. Файлы, поставляемые OEM (Original Equipment Manufacturer), то есть "при аппаратуре", должны вместо этого элемента использовать SourceDiskNames и SourceDiskFiles
CatalogFile | Указывает на cat-файл (с расширением .CAT), содержащий набор драйверных файлов. Этот набор формируется лабораторией Microsoft HW Quality Lab и содержит зашифрованную цифровую подпись проверенного драйверного программного обеспечения. Данный файл не должен подвергаться каким-либо формам архивации.
DriverVer | Обязательная запись. Независимо от локализации версии ОС имеет формат mm/dd/yyyy[,x.y.v.z]; Здесь версия драйвера может быть введена через запятую после указания даты.

В таблице 12.2 приводятся некоторые из инсталляционных классов, которые можно указывать в директивах Class и ClassGuid. Наиболее полный и верный на текущий момент набор классов можно найти в разделе Системного Реестра HKLM\CurrentControlSet\Control\Class\{...}, где операционная система хранит все поддерживаемые на текущий момент классы устройств. Указанный раздел разбит на подразделы в соответствии с GUID идентификаторами классов, причем в каждом подразделе имеется параметр Class, в котором хранится наименование соответствующего класса в текстовой форме.

Таблица 12.2. Инсталляционные классы: названия и глобально-уникальные идентификаторы

Наименование | Описание | GUID идентификаторы
1394 | Хост-контроллер шины 1394 | {6BDD1FC1-810F-11D0-BEC7-08002BE2092F}
Battery | Аккумуляторные устройства питания | {72631E54-78A4-11D0-BCF7-00AA00B7B32A}
CDROM | Устройства CD ROM | {4D36E965-E325-11CE-BFC1-08002BE10318}
Display | Дисплейные адаптеры | {4D36E968-E325-11CE-BFC1-08002BE10318}
HIDClass | HID устройства | {745A17A0-74D3-11D0-B6FE-00A0C90F57DA}
Infrared | Устройства ИК-связи (IrDA) | {6BDD1FC5-810F-11D0-BEC7-08002BE2092F}
Keyboard | Клавиатура | {4D36E96B-E325-11CE-BFC1-08002BE10318}
Media | Устройства мультимедиа | {4D36E96C-E325-11CE-BFC1-08002BE10318}
Modem | Модем | {4D36E96D-E325-11CE-BFC1-08002BE10318}
Monitor | Монитор | {4D36E96E-E325-11CE-BFC1-08002BE10318}
Mouse | Манипулятор "мышь" | {4D36E96F-E325-11CE-BFC1-08002BE10318}
MultiPortSerial | Многопортовые последовательные адаптеры | {50906CB8-BA12-11D1-BF5D-0000F805F530}
Network | Сетевой адаптер | {4D36E972-E325-11CE-BFC1-08002bE10318}
NetClient | Сетевой клиент | {4D36E973-E325-11CE-BFC1-08002BE10318}
NetService | Сетевой сервис | {4D36E974-E325-11CE-BFC1-08002BE10318}
PCMCIA | Адаптеры PCMCIA | {4D36E977-E325-11CE-BFC1-08002BE10318}
Ports | Порты (COM&LPT) | {4D36E978-E325-11CE-BFC1-08002BE10318}
Printer | Принтер | {4D36E979-E325-11CE-BFC1-08002BE10318}
System | Системные устройства | {4D36E97D-E325-11CE-BFC1-08002BE10318}
TapeDrive | Устройства работы с магнитной лентой | {6D807884-7D21-11CF-801C-08002BE10318}
Unknown | Другие устройства | {4D36E97E-E325-11CE-BFC1-08002BE10318}
USB | USB устройства | {36FC9E60-C465-11CF-8056-444553540000}

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

Секция описания поставщика [Manufacturer]

Второй по важности секцией любого inf-файла является секция поставщиков оборудования. В ней указываются ссылки на секции описания моделей [Models] устанавливаемого оборудования.

Первая из возможных форм записей в данной секции:

%token% = model_section_name

В секции может быть много поставщиков и, соответственно, ссылок на секции описания моделей. Вот один из примеров DDK:

[Manufacturer]
%ATAPI_CHGR%      = atapi_chgr
%CHINON%          = chinon_cdrom
%DENON%           = denon_cdrom
%FUJITSU%         = fujitsu_cdrom
%HITACHI%         = hitachi_cdrom
%HP%              = hp_cdrom
%MITSUMI%         = mitsumi_cdrom
%NEC%             = nec_cdrom
%OTI%             = oti_cdrom
%PIONEER%         = pioneer_cdrom
%WEARNES%         = wearnes_cdrom
%GenManufacturer% = cdrom_device

Данная секция описания поставщиков содержит 12 записей о поставщиках. Слева указаны маркеры (token — идентификаторы, обособленные двумя знаками процента, см. ниже), которые позже, в том же inf-файле в секции [Strings], соотнесены со строками-названиями производителей в полной текстовой форме. Справа от знака равенства указаны имена секций (например, секции [atapi_chgr]), описывающих установку программного обеспечения для моделей аппаратуры данного производителя (ссылки на секции моделей), которые были в этом файле рассмотрены позже.

Другая форма возможна для операционных систем, начиная с Windows XP, где можно указывать разные секции моделей в связи с версией операционной системы. В результате строки в секции [Manufacturer] приобретают вид:

%token%= model_section_name[,TargetOS] [,TargetOS]

Например, для Windows Server 2003:

[Manufacturer]
%MSFT%=Microsoft_Model_Section, NT.5.2

Соответственно, секции моделей будет начинаться так:

[Microsoft_Model_Section] ; допустимо для ОС систем до и включая
<тело секции моделей>     ; Windows XP

[Microsoft_Model_Section.NT.5.2]  ; для Windows Server 2003
<тело секции моделей>

Значение маркера следует раскрыть в секции [Strings].

Для секции описания производителя [Manufacturer] возможна и другая форма (содержащая только идентификатор производителя, являющийся одновременно и ссылкой на секцию моделей), например:

[Manufacturer]
"Microsoft"

Тогда и секция описания модели будет выглядеть иначе, например:

[Microsoft]
<тело секции моделей>

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

На рисунке 12.1 показана взаимосвязь между секциями inf-файла. Каждая запись в секции описания поставщиков [Manufacturer] указывает на секцию описания моделей для данного поставщика. В этой секции моделей вводятся уже записи, указывающие на секции, описывающие собственно установку программного обеспечения, то есть копирование файлов (откуда/куда), изменения в Системном Реестре и т.п.

Рис. 12.1
Взаимодействие между секциями INF файла

Могут быть также и секции, стоящие несколько в стороне от основной работы по установке, в частности, секция [ClassInstall32] и ей подчиненные секции, которые служат для внесения в Системный Реестр нового класса аппаратуры.

Секция описания моделей аппаратуры [Models]

Для каждого поставщика, указанного в секции [Manufacturer], должна быть представлена соответствующая секция описания моделей его аппаратуры [Models]. Имя данного типа секций не может быть жестко регламентировано, потому что разработчик сам задает его в секции [Manufacturer].

В каждой такой секции [Models] записи представляются по следующей форме:

device_description = install_section_name,hw_id[,compatible_id...]

где device_description представляет собой уникальный набор видимых символов либо маркер, обязательный для определения в секции [Strings]. Данная строка будет предъявляться пользователю во время инсталляционного диалога, так что имеет смысл позаботиться о поддержке нескольких языков.

Значение install_section_name представляет собой ссылку на секцию, описывающую собственно действия по инсталляции для данной модели (в документации DDK такого типа секции обозначены как [DDInstall]).

Значение hw_id является PnP идентификатором, возвращаемым аппаратным устройством во время опроса PnP-совместимой шины. Например, USB\VID_04B4&PID_1002 определяет плату тестового набора фирмы Cypress (так называемый EZUSB Kit). Любое количество значений compatible_id может быть приведено для обозначения того, что та же самая инсталляционная запись должна быть использована для указанного в этом списке устройства.

Применение одной и той же группы символов может сбить с толку начинающего разработчика inf-файлов. Рассмотрим показательный пример из DDK для Windows XP.

[Version]
Signature = "$Windows NT$"  ; inf-файл для установки только под NT
Class=System
ClassGUID={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%MSFT%
DriverVer= 5/1/2001

[Manufacturer]
%MSFT%=MSFT  ; со знаками процента - маркер

[MSFT]
%_MCADesc%=_MCA_Inst,_MCA0000

[_MCA_Inst.ntx86]
CopyFiles = _MCA.Files.x86_12

[Strings]
MSFT= "Microsoft"  ; раскрываем маркер
_MCADesc= "Microsoft MCA Driver"

В секции [Manufacturer] видим, что маркер %MSFT% "приравнивается" ссылке MSFT. Это означает, что в данной секции описан поставщик Microsoft (именно так раскрывается маркер %MSFT% в секции [Strings]), a модели аппаратуры представлены в секции описания моделей [MSFT].

Переходя к рассмотрению секции моделей, видим маркер %_MCADesc% (раскрываемый как "Microsoft MCA Driver" — для диалога с пользователем при установке), который ассоциирован со ссылкой на секцию установки драйвера [_MCA_Inst]. Правда, эта секция снабжена суффиксом '.NTx86' (регистр не имеет значения), сообщающим о том, что данная секция применима только для установки в операционной системе NT на платформе Intel x86 (в документации DDK такое присоединение суффиксов называется декорированием имен секций).

Начиная с версии операционной системы Windows XP, имя секции моделей может декорироваться идентификатором версии, например:

[Manufacturer]
%MSFT%=MSFT_model
[MSFT_model.NT.5.1]   ; Windows XP
[MSFT_model.NT.5.2]   ; Windows Server 2003

Замечания по декорированию имен

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

Прежде всего, маркер, который является идентификатором, взятым в два знака процента, есть не что иное, как формальная переменная ('икс' в школьной задаче). Его внешнее сходство с чем-либо еще в записях inf-файла обманчиво. Во всех тех местах, где встречается маркер, следует подставлять его значение, соотнесенное с ним в секции [Strings]. Причем, это значение, может быть не обязательно текстовым, например:

[Example.Service]
DisplayName  = %Example.ServiceName%
ServiceType  = %SERVICE_KERNEL_DRIVER%
StartType    = %SERVICE_DEMAND_START%

[Strings]
Example.ServiceName="Example NTDDK driver (V.001)"
SERVICE_KERNEL_DRIVER=1
SERVICE_DEMAND_START=3

Здесь с маркером %Example.ServiceName% соотнесено строковое значение "Example NTDDK driver (V.001)", а с маркером %SERVICE_KERNEL_DRIVER% соотнесено значение 1. Текстовые значения (здесь Example.ServiceName) называются локализуемыми значениями (их можно задавать разными для разных языковых версий операционной системы, локализаций). Нетекстовые значения считаются нелокализуемыми.

В том случае, если значение маркера не раскрывается, то будет полагаться, что поле, в котором введен маркер, просто имеет текстовое значение, совпадающее с маркером, например, "%Example.ServiceName%" в примере выше.

Во-вторых, двусмысленна роль точки в формировании имен секций и маркеров.

Если точка встречается в написании маркера, то следует считать ее обычным символом, который разработчик употребил ради большей выразительности.

В случае если точка встретилась в имени секции (или в ссылке на имя секции), то возможно два варианта:

  • Присоединяемый после точки суффикс — один из предопределенных суффиксов NT, NTx86, x86, alpha, ia64, NTia64, NT.5.1, NT.5.2 либо Services, NT.Services, NTx86.Services и некоторые другие. В данном случае, это модификация свойств секции. В частности, секция [ModelList.NT.5.1] будет приниматься к рассмотрению только в операционной системе Windows XP.
  • Присоединяемый суффикс не является модификатором свойств секции. В данном случае точку следует рассматривать как рядовой видимый символ, как и в упомянутом выше случае, когда точка встречается в идентификаторе маркера.

Секция [CopyFiles]

Секции [CopyFiles] имеют уникальные для INF файла названия, ссылки на них исходят из директив CopyFiles секций [DDInstall]. Соответственно, конкретные имена этих секций определяет сам разработчик inf-файла.

Каждая запись внутри секции [CopyFiles] имеет вид

destination-filename[, source-filename][, temp-filename][, flag]

где destination-filename является целевым (то есть новым, конечным) именем файла после копирования. Предполагается, что и исходный файл имеет такое же имя. В том случае, если исходный файл все-таки называется иначе, необходимо указать source-filename. Требование указывать temp-filename все еще требуется для Windows 98/Me, и это поле вводит промежуточное имя для нового файла до момента первой перезагрузки системы. В Windows 2000/XP/2003 это значение игнорируется.

Таблица 12.5. Определение значения flag в записях секции [CopyFiles]

Значение | Символьное имя | Описание
0x0400 | COPYFLG_REPLACEONLY | Копировать исходный файл только в том случае, если в целевой директории есть файл с таким именем
0x0800 | COPYFLG_NODECOMP | Копировать без разархивации (если файл обработан архиватором)
0x0008 | COPYFLG_FORCE_FILE_IN_USE | Если файл с целевым именем в целевой директории сейчас открыт, то следует копировать исходный файл в файл с временным именем, форсировать перезагрузку, после чего переименовать временный файл
0x0010 | COPYFLG_NO_OVERWRITE | Не переписывать существующие одноименные файлы в целевой директории
0x1000 | COPYFLG_REPLACE_BOOT_FILE | Файл является частью системной загрузки, форсировать перезагрузку системы
0x2000 | COPYFLG_NOPRUNE | Осуществить копирование, даже если инсталлятор не считает эту операцию целесообразной
0x0020 | COPYFLG_NO_VERSION_DIALOG | Не переписывать одноименные существующие файлы, которые датированы как более новые, нежели предназначенные к записи (игнорируется, если инсталлируемый пакет имеет цифровую подпись)
0x0004 | COPYFLG_NOVERSIONCHECK | Всегда переписывать целевые файлы (флаг игнорируется, если инсталлируемый пакет имеет цифровую подпись)
0x0040 | COPYFLG_OVERWRITE_OLDER_ONLY | Переписывать только те существующие файлы, которые являются более старыми, чем имеющиеся в пакете (данный флаг игнорируется, если инсталлируемый пакет имеет цифровую подпись)
0x0001 | COPYFLG_WARN_IF_SKIP | Предупреждать пользователя о возникшей необходимости пропустить переписывание файл (игнорируется, если инсталлируемый пакет имеет цифровую подпись)
0x0002 | COPYFLG_NOSKIP | Запретить пользователю выбор возможности пропуска каких-либо файлов при копировании (всегда применяется, если инсталлируемый пакет имеет цифровую подпись)

Значение flag определяет управление новым целевым файлом, что подробнее отражено в таблице 12.5. Для описания сложного управления необходимо выполнять ИЛИ над операндами — для получения одновременного воздействия указываемых вариантов. Некоторые варианты взаимно исключают друг друга (например, COPYFLG_WARN_IF_SKIP и COPYFLG_NOSKIP), поэтому следует в сомнительных ситуациях обратиться к документации.

Так как секции [CopyFiles] не имеют синтаксических средств указывать диск или полный путь к исходному файлу, то следует использовать другие секции, такие как [SourceDisksNames] и [SourceDisksFiles]. Место (конкретные файловые каталоги), куда файлы будут помещены в результате установки, определяется другой секцией, называемой [DestinatonDirs].

Следует отметить, что здесь секция [CopyFiles] описывается, как присутствующая в inf-файле по той причине, что на нее ссылалась директива CopyFiles из секции [DDInstall]. На самом деле, директива CopyFiles может присутствовать и в секции [ClassInstall32], которая посвящена инсталляции нового класса устройств в системе (будет рассмотрена ниже). Вводимая таким образом секция [CopyFiles] должна быть построена по таким же правилам, как указано здесь.

Секции [ServiceInstall]

Секции типа [ServiceInstall] предназначены для заполнения или модификации подраздела Системного Реестра, описывающего загрузку драйвера в сервисном подразделе для данного драйвера, а именно — в подразделе HKLM\System\CurrentControlSet\Services\<service-name>. Здесь <service-name> — это значение поля service-name, указанное в директиве AddService в секции [DDInstall.Xxx.Services]. Конкретное имя секции типа [ServiceInstall] выбирается разработчиком inf-файла. Декорирование имен секций данного типа (с целью отразить ее предназначение для конкретной версии системы) уже не имеет смысла и не воспринимается, поскольку эта принадлежность должна была быть введена раньше — на уровне секций [DDInstall.Xxx.Services]. Описание директив для секций типа [ServiceInstall] приводится в таблице 12.10, причем директивы ServiceType, StartType, ErrorControl и ServiceBinary являются обязательными. Эти директивы однозначно определяют информацию (значения одноименных параметров), которая появится в Системном Реестре в сервисном подразделе для данного драйвера — пример, касающийся драйвера Example.sys, рассмотрен в Приложении В.

Таблица 12.10. Записи секции [ServiceInstall]

Запись | Значение поля
DisplayName | Развернутое наименование драйвера, выводится на экран Мастером Установки Оборудования
Description | Краткое описание назначения драйвера или сервиса, выводится Мастером Установки Оборудования
ServiceType | Для драйвера режима ядра 0x01 (см. также Приложение В)
StartType | Определяет момент загрузки драйвера 0 — SERVICE_BOOT_START — во время загрузки системы (WDM драйверы, опирающиеся на системные драйверы не должны использовать такой тип запуска) 1 — SERVICE_SYSTEM_START — во время инициализации системы (WDM драйверы, опирающиеся на системные драйверы должны использовать такой тип запуска с осторожностью) 2 — SERVICE_AUTO_START — автостарт после запуска системы средствами SCM Менеджера (WDM драйверы и драйверы РпР устройств не должны указывать этот код запуска) 3 — SERVICE_DEMAND_START — старт по требованию: либо по запросу РnР Менеджера при обнаружении РnР устройства, либо по явному запросу приложения при помощи вызовов SCM Менеджера 4 — SERVICE_DISABLED — не может стартовать
ErrorControl | Распоряжение относительно возникающих ошибок: 0 — игнорировать все ошибки при загрузке драйвера 1 — показывать сообщения об ошибках пользователю 2 — выполнить рестарт с набором параметров, обеспечившим последнюю удачную загрузку (LastKnownGood), игнорировать дальнейшие ошибки 3 — выполнить рестарт с набором параметров, обеспечившим последнюю удачную загрузку (LastKnownGood), контроль ошибок если таковые возникнут со стороны пользователя
ServiceBinary | Путь к файлу драйвера (может включать коды dirid, таблица 12.6)
AddReg | Вводит (через запятую) ссылки на секции типа [AddReg], в которых описываются действия над Реестром, которые следует выполнить дополнительно к описанным в данной секции
LoadOrderGroup | Идентифицирует группу, в которой должен загружаться драйвер (возможные группы можно увидеть в разделе Системного Реестра HKLM\System\CurrentControlSet\Control\GroupOrderList)
Dependencies | Указывает сервисы (драйверы) или группы загрузки, которые должны быть загружены к моменту загрузки драйвера. Имена групп выделяются при вводе предшествующим им знаком '+'.

Директивы LoadOrderGroup и Dependencies широко используются при установке драйверов SCSI устройств и фильтр-драйверов.

Директива DelReg, которая также может быть в составе [ServiceInstall], вводит ссылки на секции, описывающие удаление из Системного Реестра информации для уже установленных программных продуктов. Используется эта директива редко.

Остальные директивы, возможные для ввода в секциях типа [ServiceInstall], а именно, StartName и BitReg практически не используются.

Секция [ClassInstall32]

Разработчик драйвера может создать собственный класс устройств (с собственным GUID, созданным при помощи программы GuidGen) и использовать его при установке своего драйвера. Данная операция не является сложной и выполняется при помощи секции [ClassInstall32], например:

[Version]
Signature="$Chicago$"
Class=ExampleDrvClass
ClassGuid={DC16BE99-C06B-4801-A144-43A98BB99052}
. . .
[ClassInstall32]
Addreg=ExampleClassReg

[ExampleClassReg]    ; секция изменений в Реестре
HKR,,,0,%ClassName%  ; имя класса вводится через маркер %ClassName%

[Strings] ; Дополняем секцию значением маркера
ClassName="Example's Driver Class"
. . .
Рис. 12.2
Новый класс в окне Диспетчера устройств

Внесем приведенные выше дополнения в inf-файл, предназначенный для установки драйвера Example.sys, см. главу 3. В результате установки Мастером установки появится новый класс с указанным GUID в разделе HKLM\System\CurrentControlSet\Control\Class Системного Реестра, см. рисунок 12.3. В его подразделе будет указан параметр Class, содержащий значение имени "ExampleDrvClass", и один вложенный подраздел \0000, описывающий установленный драйвер Example.Sys, см. рисунок 12.4.

Рис. 12.3.
Новый класс в окне Редактора Системного Реестра

Рис. 12.4.
Вновь установленный драйвер класса ExampleClassReg

Открывая описания классов в Системном Реестре при помощи Редактора Реестра, можно увидеть, что другие классы имеют существенно больше параметров, чем создано для нового класса ExampleDrvClass при помощи указанных выше записей. Все недостающие параметры можно ввести в секции описания изменений в Реестре, в данном случае — [ExampleClassReg].

Имя секции [ClassInstall32] может быть декорировано при помощи суффиксов .nt, .ntx86 и .ntia64 для того, чтобы ограничить применимость данной секции.

Помимо директивы AddReg, обязательной для секции [ClassInstall32], в данной секции могут присутствовать некоторые другие директивы (следует обратиться к документации DDK), из которых самой важной является директива CopyFiles.

Синтаксис директивы CopyFiles совершенно аналогичен тому, который используется в секции [DDInstall], и вводит информацию о копировании файлов, если таковые необходимы для завершения установки нового класса.

В том случае, если при установке нового класса устройств еще требуется установить и некоторые драйверы, предусмотрена возможность их установки с использованием секции [ClassInstall32.Services] ([ClassInstall32.Xxx.Services]), использование которой аналогично [DDInstall.Services].

Созданный класс легко и безболезненно удаляется из Системного Реестра (в интерактивном Редакторе), если только в системе не осталось устройств данного класса.

Секции [DefaultInstall32.Xxx] и [DefaultInstall32.Xxx.Services]

В Windows 98 была возможность установки драйвера по нажатию правой кнопки мышки в программе Проводник на inf-файле с последующим выборе в открывшемся меню пункта "Установить". В Windows 2000/XP/2003 для такой установки необходимо наличие в inf-файле секций [DefaultInstall32.Xxx] и [DefaultInstall32.Xxx.Services], где "Ххх" обозначает суффиксы декорирования имен nt, ntx86, ntia64.

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


Глава 12. Инсталляция драйверов при помощи INF файлов

Секции [DDInstall]

Для каждой модели, указанной в секции описания моделей аппаратуры данного поставщика, следует сделать ссылку на секцию описания собственно установки программного обеспечения драйвера — секции [DDInstall]. Конкретное название этой секции устанавливает разработчиком драйвера и, в общем случае, должно быть уникальным для каждой модели каждого производителя из тела каждой секции описания моделей. Однако бывают случаи, когда одному драйверу удается обслуживать сразу несколько моделей PnP устройств, предоставляющих при подключении разные идентификаторы. В таких случаях возможна ситуация, когда одна секция типа [DDInstall] соответствует сразу нескольким ссылкам из секции описания моделей.

Основные директивы секции [DDInstall] перечисляются в таблице 12.3. По поводу использования остальных следует обратиться к документации пакета DDK.

Таблица 12.3. Элементы секции [DDInstall]

Записи | Значения
DriverVer | mm/dd/yyyy[,x.y.v.z] Здесь версия драйвера может быть введена через запятую после указания даты
CopyFiles | Любое имя секции, указывающей имена файлов для инсталляции, либо конкретное имя файла, предваряемое префиксом @
CopyInf | Директива, определяющая копирование inf-файлов на целевой диск. Введена только в Windows XP.
AddReg | Обязательна для ввода. Перечисляет имена секций, где содержится информация, предназначенная для занесения в Системный Реестр во время инсталляции.
Include | Указатель на другие INF файлы, необходимые для данной инсталляции
Needs | Подмножество/а записи Include (выше), перечисляющее имена всех необходимых секций (считая все включаемые INF файлы).
DelFiles | Указывает имена других секций, которые перечисляют файлы, подлежащих удалению в целевой директории (обычно, в процессе обновления, upgrade).
RenFiles | Указывает имена других секций, которые перечисляют файлы, подлежащих переименованию перед инсталляцией (обычно, чтобы сохранить состояние предыдущей инсталляции). Об организации секций, описывающих переименование см. подробнее в документации DDK.
DelReg | Указывает имена других секций, которые содержат информацию, что именно следует удалить из Системного Реестра при инсталляции

В то время, как AddReg требуется только с точки зрения синтаксиса, директива CopyFiles является весьма значимой директивой секции [DDInstall]. Директива CopyFiles имеет форму

CopyFiles = file_list_section[,file_list_section...]

либо

CopyFiles=@filename

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

Когда имя секции [DDInstall] упоминается в ссылке из секции описания модели, то суффиксы, задающие версию системы, применять не следует. В момент ссылки в директивах секций описания моделей имя секции [DDInstall] задается универсально, одинаково для всех типов операционных систем (без стандартных суффиксов, типа .NT или .NTx86). Зато, в начале собственно тела секции, имя [DDInstall] секции может быть декорировано одним из суффиксов типа .nt, .ntx86 или ntia64, что означает принятие к исполнению данной секции только в соответствующей операционной системе. Пример ниже демонстрирует, что конкретизация происходит в момент описания собственно секции [DDInstall].

[Manufacturer]
%MSFT%=MSFT

[MSFT]
%_MCADesc%=_MCA_Inst, _MCA0000

[_MCA_Inst.ntx86]
CopyFiles = _MCA.Files.x86_12

Здесь в секции моделей [MSFT] введена ссылка на [DDInstall] секцию с конкретным именем _MCA_Inst, и эта секция была введена только для использования в Windows NT. Поэтому имя было декорировано суффиксом .NTx86, что в результате выглядит как [_MCA_Inst.ntx86].

Секция [DDInstall.Services]

Чтобы скопированные в положенное место файлы действительно заработали как настоящий драйвер, необходимо надлежащим образом уведомить SCM Менеджер. Соответственно, для этого необходимо иметь записи в системном Реестре в разделе HKLM\System\CurrentControlSet\Services по поводу каждого драйвера.

В Windows 2000/XP/2003 работа по регистрации драйвера вынесена в отдельный тип секций [DDInstall.Services].

Можно считать, что данная секция является придатком секции [DDInstall] и всего лишь конкретизирует, какие секции inf-файла (service-install-section) определяют, что именно будет внесено/удалено в Системный Реестр в рамках установки драйвера. В Windows 98 следовало пользоваться директивой AddReg в секции [DDInstall]. В Windows 2000 inf-файл стал сложнее, и работа с Системным Реестром была выделена в еще один "промежуточный" тип секций [DDInstall.Services].

В том случае, если имя [DDInstall] секции декорировано одним из суффиксов .nt, .ntx86 или ntia64 (что означает принятие к исполнению данной секции только в соответствующей операционной системе), то суффикс .Services следует присоединять именно к получившемуся декорированному имени [DDInstall.Xxx].

Среди записей секции [DDInstall.Xxx.Services] имеется обязательная директива AddService (помимо еще трех необязательных), которая имеет вид:

AddService=service-name,[flags],service-install-section[ ,eventlog-install-section[,event-log[,event-name]]]

где service-name представляет название сервиса, что обычно совпадает с названием Драйвера (если отбросить расширение .sys). Подраздел с таким именем будет создан в результате инсталляции в разделе Системного Реестра HKLM\System\CurrentControlSet\Services.

Директив AddService в секциях [DDInstall.Xxx.Services] может быть несколько.

Возможные значения поля flags приводятся в таблице 12.4. Можно вводить значения, которые представляют "побитовое ИЛИ" указанных в таблице значений.

Значение поля service-install-section и необязательное для ввода значение поля eventlog-install-section (ссылка на секцию, где описывается установка сервисов протоколирования) объявляют имена секций INF файла типа [ServiceInstall], например:

[Manufacturer] ; секция описания поставщиков
%ThisMfg%=DeviceList

[DeviceList]   ; секция описания моделей
%Model1%= Model1_Inst, _MXX0001 ; <- идентификатор

[Model1_Inst.ntx86] ; [DDInstall] секция инсталляции для NT
. . . .
[Model1_Inst.ntx86.Services]   ; установка драйвера как сервиса Mxx0001
; Поле flags имеет значение 0x0002 (см. таблицу 12.4)
; ссылка на секцию [ServiceInstall]
AddService= Mxx0001, 0x0002, MODEL1_ADDSERVICE_SECTION

[MODEL1_ADDSERVICE_SECTION] ; секция [service-install-section]
DisplayName  = %Model1.ServiceDesc%
ServiceType  = 1 ; = SERVICE_KERNEL_DRIVER, см. ntddk.h, wdm.h
StartType    = 3 ; = SERVICE_DEMAND_START, см. ntddk.h, wdm.h
ErrorControl = 1 ; = SERVICE_ERROR_NORMAL, см. ntddk.h, wdm.h
ServiceBinary= %12%\SpecialDrv.sys

[Strings]  ; Расшифровка значений
ThisMfg= "This Manufacturer"
Model1= "Model1 (made by This Manufacturer)"
Model1.ServiceDesc= "Model1 Special Driver v.1.000"

Таблица 12.4. Значения flags директивы AddService

Значение | Символьное имя SPSVCINST_Xxx | Описание
0x0002 | SPSVCINST_ASSOCSERVICE | Драйвер является функциональным драйвером или драйвером в "стиле-NT"
0x0008 | ...NOCLOBBER_DISPLAYNAME | Не переписывать существовавшее в Системном Реестре значение DisplayName, если такой сервис уже был установлен ранее
0x0100 | ...NOCLOBBER_DESCRIPTION | Не переписывать описание
0x0010 | ...NOCLOBBER_STARTTYPE | Не переписывать StartType
0x0020 | ...NOCLOBBER_ERRORCONTROL | Не переписывать ErrorControl
0x0040 | ...NOCLOBBER_LOADORDERGROUP | Не переписывать LoadOrderGroup
0x0080 | ...NOCLOBBER_DEPENDENCIES | Не переписывать Dependencies

Более подробно организация секций типа [ServiceInstall] (включающих директивы DisplayName, ServiceType, StartType и т.п.) будет рассмотрена далее, после описания секций типа [AddReg].

В секциях типа [DDInstall.Services] могут быть введены директивы DelService (удаления ранее установленных сервисов), Include (включения текста внешних inf-файлов) и директива Needs. Для получения информации по данным необязательным директивам следует обратиться к соответствующей документации, например, документации DDK.


Глава 12. Инсталляция драйверов при помощи INF файлов

Другие секции, определяющие копирование файлов

Как было сказано выше, дополнительную информацию о том, где взять исходные файлы и куда их скопировать предоставляют секции [SourceDisksNames], [SourceDisksFiles] и [DestinatonDirs] (имена данных секций не подлежат изменению разработчиком).

Секция [SourceDiskNames]

В случае, если файлы, относящиеся к установке драйвера и управляемые данным INF файлом, размещены более чем на одном диске (CD или дискете), то INF файл должен содержать секцию [SourceDisksNames]. Эта секция содержит по одной записи на каждый диск из набора, предлагаемого для установки. Запись имеет вид:

diskid=disk_description[,[tagfile],[неиспользуемое_поле,path][,flags]]

где diskid это уникальное в пределах набора дисков неотрицательное целое число. Как правило, нумерация дисков начинается с 1, хотя возможна и шестнадцатеричная нумерация, например 0x0, 0x1 и т.п.

Поле disk_description является понятной человеку строкой (выделенной кавычками), которая может быть использована для того, чтобы проинформировать пользователя о том, какой диск требуется установить в привод. Здесь можно применять маркер (вместо строки в кавычках), который, соответственно, раскрывается строкой в секции [Strings].

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

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

Поле flags обычно не используется, как и еще одно поле (tag_file), введенное только в Windows XP.

Имя секции [SourceDisksNames] может декорироваться суффиксами версий операционной системы x86 и ia64, так что в inf-файле может быть несколько секций данного типа, отличающихся суффиксами, например:

[SourceDisksNames.x86]
[SourceDisksNames.ia64]

Секция [SourceDiskFiles]

Инсталляционный inf-файл должен содержать секцию [SourceDiskFiles], в которой перечисляются имена файлов, составляющих предмет инсталляции. Каждый файл представлен одной записью в этой секции в форме:

filename = diskid [, [subdir] [,size] ]

Соответственно, значение diskid указывает диск, введенный в секции [SourceDisksNames], где находится файл filename.

Необязательное для ввода значение subdir указывает путь к этому файлу относительно директории, указанной полем path в соответствующей (по diskid) записи секции [SourceDisksNames]. Если значение path не было там указано, то подразумевается инсталляционная директория (там, откуда взят inf-файл).

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

 Практически, значение diskid следует рассматривать всего лишь как идентификатор для построения связки "запись в секции [SourceDiskFiles]" — "запись в секции [SourceDisksNames]", что необходимо системному программному обеспечению, выполняющему установку, для уяснения, откуда следует брать исходные файлы.

Имя секции [SourceDiskFiles] также может декорироваться суффиксами версий операционной системы x86 и ia64.

Секция [DestinationDirs]

Данная обязательная секция INF файла описывает файловый каталог (директорию), в который будет производиться копирование исходных файлов. Без этой секции программа, выполняющая установку драйвера, не будет знать, куда копировать файлы исходных носителей. Записи в секции [DestinationDirs] имеют вид:

file_list_section=dirid[,subdir]

либо

DefaultDestDir=dirid[,subdir]

где file_list_section как раз указывает на секцию типа [CopyFiles], на которую была объявлена ссылка также и в директиве CopyFiles из секции типа [DDInstall] (или [DDInstall.Xxx]).

Это означает, что все файлы, перечисленные в этой "перекрестно ссылаемой" секции типа [CopyFiles], будут скопированы в файловый каталог (директорию), которому присвоен идентификатор dirid. Значение dirid указывает численное значение, за которым стоит достаточно конкретный файловый каталог (директория) назначения. Таблица 12.6 описывает возможные числовые коды dirid и ассоциированные с ними файловые каталоги.

Запись DefaultDestDir=... указывает место, куда будут скопированы файлы, перечисленные в остальных секциях типа [CopyFiles], для которых не было указано в явной форме (записями вида file_list_section=dirid[,subdir]) то место, куда переносить эти файлы.

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

Таблица 12.6. Значения кодов dirid в контексте секции DestinationDirs

Значения | Описание
12 | %windir%\system32\drivers для Windows 2000/XP/2003 %windir%\system\IoSubsys для Windows 98
10 | %windir%\
11 | %windir%\system32 для Windows 2000/XP/2003 %windir%\system для Windows 98
30 | Корневая директория загрузочного диска
54 | Загрузочная (boot) директория Windows 2000/XP/2003
01 | Директория данного INF файла
17 | Директория INF файлов
20 | Директория шрифтов
51 | Spool-директория
52 | Директория spool-драйверов
55 | Директория программ поддержки печати (print processors)
23 | Color (ICM)
-1 | Полное имя файлового каталога (абсолютный путь)
21 | Директория вьюверов (программ просмотра файлов различных форматов — графики, текстов, файлов БД и т.п.)
53 | Директория пользовательского лицевого счета (User Profile)
24 | Директория размещения приложений
25 | Совместно используемая директория
18 | Директория файлов-справок (help directory)
16406 | All Users\Start Menu
16407 | All Users\Start Menu\Programs
16408 | All Users\Start Menu\Programs\Startup
16409 | All Users\Desktop
16415 | All Users\Favorites
16419 | All Users\Application Data
16422 | Program Files
16427 | Program Files\Common
16429 | All Users\Templates
16430 | All Users\Documents

В секции [DestinationDirs] может быть только одна запись DefaultDestDir=... и много ссылок на секции вида file_list_section=dirid[,subdir].

Примеры описания процедуры копирования файлов

Рассмотрим простейший пример взаимодействия информации, вводимой в секциях, управляющих копированием файлов [SourceDisksNames], [SourceDisksFiles], [DestinatonDirs] и [CopyFiles].

[Manufacturer]
%ThisMfg%=  ModelList ; ссылка на секцию моделей

[ModelList]
"ISA Hammer"=InstallHammer, ISA\Hammer

[InstallHammer]            ; секция инсталляции конкретной модели
CopyFiles=CopyHammerFiles  ; секция CopyFiles
CopyFiles=CopyHammerHelp   ; еще одна секция CopyFiles
AddReg=HammerRegSection    ; ссылка на секцию AddReg

[DestinationDirs]     ; Куда следует выполнять копирование:
DefaultDestDir=12     ; по умолчанию -> %windir%\system32\drivers
CopyHammerHelp=18     ; стандартная директория для help файлов

[CopyHammerFiles]
Hammer.sys      ; <- будет скопирован в директорию dirid=12
[Copy Hammer Help]
Hammer.hlp      ; <- будет скопирован в директорию dirid=18

[SourceDisksNames]
; Подразумевается, что устанавливаемые файлы
; находятся в том же файловом каталоге, что и данный inf-файл.
l="Hammer Driver Files"

[SourceDisksFiles]
Hammer.sys=1; Ссылается на единственную запись в [SourceDisksNames]
Hammer.hlp=1; Ссылается на единственную запись в [SourceDisksNames]

[Strings]
ThisMfg="Big Hammer Manufacturer"

Файл hammer.sys будет скопирован в директорию Windows\Help (Windows XP) или WinNT\Help (Windows 2000) . Немного модифицируем пример: используем в имени секции точку и изменяем направление

[InstallHammer]
CopyFiles=CopyLaunchHelp.Section
. . .
[DestinationDirs]
CopyHammerHelp.Section = -1, C:\Hammer

[CopyHammerHelp.Section]
Hammer.hlp

От введения суффикса .Section взаимодействие секций не изменяется. Поскольку в секции [DestinationDirs] теперь указано '-1' (абсолютный путь), то файл hammer.hlp будет скопирован в каталог C:\Hammer. В случае, если такой каталог не существует, он будет создан.


Глава 12. Инсталляция драйверов при помощи INF файлов

Секция [AddReg]

Секции типа [AddReg] содержат описание действий по внесению новых подразделов и/или параметров и их значений в Системный Реестр, а также действия по модификации значений уже существующих параметров.

Ссылки на секции данного типа могут присутствовать в секциях [DDInstall], [ClassInstall32] и секциях [ServiceInstall] (обозначенных ссылками из директив AddService в секциях [DDInstall.Services]). Кроме того, ссылка на [AddReg] может быть введена и из секций, описывающих установку интерфейса (для организации доступа к объекту устройства по идентификатору интерфейса), что в данной книге не рассматривается. В перечисленных типах секций ссылки на [AddReg] вводятся директивами AddReg. Конкретное имя секций типа [AddReg] зависит от разработчика inf-файла. Каждая запись внутри секции [AddReg] имеет вид

reg-root, [subkey], [value-name], [flag] , [value]

Здесь в поле reg-root следует ввести аббревиатуру одного из корневых разделов Системного Реестра, возможные значения которых перечислены в таблице 12.7. Эти значения указывают на корневые разделы, в чьих подразделах будут сделаны изменения. Поле subkey представляет наименование подраздела внутри указанного корневого раздела. Значении HKR не имеет конкретного, раз и навсегда определенного, значения. Его конечное значение в записях секции [AddReg] зависит от того, из какой секции была сделана ссылка на [AddReg].

Таблица 12.7. Аббревиатуры корневых разделов Системного Реестра

Значения | Описание
HKCR | HKEY_CLASSES_ROOT
HKCU | HKEY_CURRENT_USER
HKLM | HKEY_LOCAL_MACHINE
HKU | HKEY_USERS
HKR | Контекстный раздел Системного Реестра (то есть какой конкретно раздел Реестра будет модифицирован, зависит от того, в какой секции была сделана ссылка на секцию типа [AddReg])

Значение value-name обозначает имя параметра в модифицируемом подразделе subkey, который (параметр подраздела, то есть) будет добавлен или модифицирован.

Значение flag описывает тип данных, который должен быть сохранен в поле значения параметра данного модифицируемого подраздела Системного Реестра. Возможные значения (из числа применимых в Windows 2000, XP и 2003), которые может принимать поле flag, перечисляются в таблице 12.8.

Таблица 12.8. Основные значение поля flag в записях секции [AddReg]

Значение | Символьное имя | Описание
0x00000 | FLG_ADDREG_TYPE_SZ | Строка символов, завершающаяся нулем (значение flag по умолчанию, если это поле в записи опущено)
0x00001 | FLG_ADDREG_BINVALUETYPE | Бинарные данные
0x00002 | FLG_ADDREG_NOCLOBBER | Не замещать существующее значение
0x00004 | FLG_ADDREG_DELVALUE | Стереть подраздел или параметр
0x00010 | FLG_ADDREG_KEYONLY | Создать подраздел, игнорировать параметр и его значение
0x00020 | FLG_ADDREG_OVERWRITEONLY | Если параметр существует, заменить, иначе ничего не предпринимать
0x10000 | FLG_ADDREG_TYPE_MULTI_SZ | Данные REG_MULTI_SZ (массив строк)
0x00008 | FLG_ADDREG_APPEND | Присоединить к существующему массиву строк REG_MULTI_SZ. Применим только совместно с FLG_ADDREG_TYPE_MULTI_SZ
0x20000 | FLG_ADDREG_TYPE_EXPAND_SZ | Данные типа REG_EXPAND_SZ
0x10001 | FLG_ADDREG_TYPE_DWORD | Данные типа REG_DWORD
0x20001 | FLG_ADDREG_TYPE_NONE | Данные REG_NONE

Значения flag в inf-файлах должны указываться собственно в числовом значении (символьные имена, указанные в таблице 12.8 не применяются). Однако при желании, для улучшения читаемости inf-файла, можно применять маркеры, например:

[DeviceInstallSection]
AddReg = DeviceAddRegSection

[DeviceAddRegSection]
HKR,, ThisDriver, %REG_DWORD%, 1
HKR,, InstalledDrivers, %REG_MULTI_SZ%, Device0001

[Strings] ; расшифровка значений маркеров
REG_SZ          = 0x00000000
REG_MULTI_SZ    = 0x00010000
REG_EXPAND_SZ   = 0x00020000
REG_BINARY      = 0x00000001
REG_DWORD       = 0x00010001

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

[MyDriver.Install]
CopyFiles=. . .
AddReg=MyDriver.AddReg

[MyDriver.AddReg]
HKR,,DevLoader,,*ntkern
HKR,,NTMPDriver,, MyDriver.sys     ; (flag = FLG_ADDREG_TYPE_SZ)

что равносильно

HKR,,"NTMPDriver",,"MyDriver.sys"

Значения HKR

Аббревиатурой HKR в inf-файлах обозначаются подходящие по контексту подразделы Системного Реестра, применимые для данной операции.

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

  • Подраздел экземпляра аппаратуры, Hardware Instance Key. Такие подразделы описывают экземпляр устройства в процессе перечисления и видны в разделе HKLM\System\CurrentControlSet\Enum, например, HKLM\System\CurrentControlSet\Enum\USB\Vid_0458&Pid_000e\5&1e1f5333&0&1, мышь "Genius".
  • Подраздел класса, Class Key, описывает зарегистрированный класс драйвера. Его можно найти в разделе Системного Реестра HKLM\System\CurrentControlSet\Control\Class (обязательно с добавлением GUID класса), например, для описанной выше мышки это будет ...\Class\{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}, то есть HIDClass.
  • Драйверный подраздел, Driver Key, описывает установленный драйвер в подразделе для всех устройств данного класса (подразделе класса), например, .. .\Class\{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}\0000.
  • Сервисный подраздел, Service Кеу (или Software Кеу), описывает, где находится загружаемый файл драйвера, когда его следует загружать, как обрабатывать ошибки и т.п. Такие подразделы видны в разделе HKLM\System\CurrentControlSet\Services, для примера с USB мышкой это будет HKLM\System\CurrentControlSet\Services\HidUsb.

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

Таблица 12.9. Значение параметра HKR в секциях [AddReg]

Откуда исходит ссылка на [AddReg] | Куда указывает HKR
Секция [DDInstall], директива AddReg | Драйверный подраздел
Секция [DDInstall.Xxx.Hw], директива AddReg | Подраздел экземпляра аппаратуры
Директива AddReg в секции [ServiceInstall], на которую указывает директива AddService секции [DDInstall.Xxx.Services] | Сервисный подраздел
Секция [ClassInstall32] или [ClassInstall], директива AddReg | Подраздел класса
Секция [DDInstall.Xxx.Coinstallers], директива AddReg | Драйверный подраздел


Глава 12. Инсталляция драйверов при помощи INF файлов

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

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

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

chkinf.bat path\Example.inf /b

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

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

Summary of "k:\Ex\Example.inf"
Total Errors:  2
Total Warnings: 6
________________________________________________________________
Errors:
  Line 5: (E22.1.1081) Directive: CatalogFile required (and must not be
blank) in section [Version] for WHQL digital signature.
  Line 11: (E22.1.1310) Class ExampleDrvClass (ClassGUID
{DC16BE99-C06B-4801-A144-43A98BB99052}) is unrecognized.
________________________________________________________________
Warnings:
  Line 0:  (W22.1.2212) No Copyright information found.
  Line 34: (W22.1.2023) Use a string token, and put localizable text in the
[Strings] section.
  Line 45: (W22.1.2208) NT-specific section(s) found. Ignoring general section.
  Line 45: (W22.1.2083) Section [EXAMPLE.INSTALL] not referenced
  Line 49: (W22.1.2083) Section [EXAMPLE.ADDREG] not referenced
  Line 53: (W22.1.2083) Section [EXAMPLE.FILES.DRIVER] not referenced

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

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

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

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

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

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

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

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

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

[SourceDisksNames]
1=%ExamDir%,,,

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

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

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

Рис. 12.5.
Результаты работы CHKINF (после исправлений)

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


Глава 12. Инсталляция драйверов при помощи INF файлов

Использование INF файлов

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

Рассмотрим два варианта запуска установки — при помощи явно вызываемого Мастера Установки (метод для не-PnP устройств и драйверов "в-стиле-NT") и автоматическую установку, запускаемую системой автоматически при обнаружении новых PnP устройств.


Глава 12. Инсталляция драйверов при помощи INF файлов

Мастер Установки/удаления новой аппаратуры

В главе 3 рассматривался вариант инсталляции Example.sys — драйвера "в-стиле-NT" (legacy driver) при помощи Мастера Установки нового оборудования и inf-файла.

Процесс установки драйвера для PnP устройства, которое предъявляет системе идентификационные коды, отличается тем, что Мастер Установки нового оборудования самостоятельно находит драйвер (если для данного типа оборудования драйвер уже устанавливался ранее хотя бы раз) либо предлагает пользователю выбрать более подходящий драйвер, который заявляет соответствующие коды в соответствующей установочной секции inf-файла.

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

Установка PnP устройств

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

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

  1. PnP Менеджер режима ядра (см. документацию DDK по указателю на ключевую фразу "Device Installation Components") уведомляет PnP Менеджер пользовательского режима о том, что в системе обнаружено новое устройство со специфическими кодами PnP идентификации (кодами производителя, модели, версии и т.п.). PnP Менеджер пользовательского режима конструирует список возможных драйверов для нового устройства, в частности, проверяется системный файловый каталог inf-файлов на наличие подходящего inf-файла (по полученной от нового устройства информации). Инсталляционные inf-файлы для дополнительно доставляемых драйверов чаще всего попадают туда под новым именем oemXxx.inf, где Xxx — это целое число, начиная с 0.
  2. Если подходящий inf-файл не обнаружен, система откладывает все последующие действия до момента, пока в систему войдет пользователь с достаточным уровнем привилегий. Этому пользователю и предлагается диалог с Мастером Установки Оборудования (Add Hardware Wizard). Пользователь должен указать место (чаще всего, CD), где размещены файлы нового драйвера и его inf-файл.
  3. Как только выявлен приемлемый inf-файл, начинается его обработка при помощи библиотеки вызовов Configuration Manager API (CfgMgr API, см. документацию DDK по указателю на ключевую фразу "Device Installation Components"). Выполняется копирование файлов драйвера и модификация информации Системного Реестра. Эта работу делает, главным образом, PnP Менеджер режима ядра.
  4. На основе директив inf-файла PnP Менеджер режима ядра загружает все фильтр-драйверы нижнего уровня, затем функциональный драйвер и, наконец, верхние фильтр-драйверы, предназначенные для обслуживания нового устройства. Драйверу, который находится на вершине стека, затем направляются соответствующие PnP запросы (IRP пакеты с кодом IRP_MJ_PNP), включая IRP_MN_START_DEVICE.

Идентификаторы PnP устройств

Процесс автоматической установки PnP устройств сильно зависит от способности устанавливающего программного обеспечения локализовать нужный inf-файл и секции в нем, относящиеся к драйверу. Особое значение имеет в данном случае то, как указываются в inf-файле наименования классов и идентификаторы устройств.

Каждое устройство, спроектированное по спецификации PnP, должно иметь идентификатор, который однозначно определяет модель данного устройства. Этот идентификатор должен быть предоставлен шинному аппаратному обеспечению (а следовательно, и шинному драйверу) по поступлении запроса. Разумеется, шинный драйвер подает запрос сразу же, как только новое устройство подключено. Секция [Models] в inf-файле содержит значение hw_id, играющее роль идентификатора модели.

В примере инсталляционного inf-файла для драйвера Example.sys (глава 3) в роли такого идентификатора выступила строка "*svpBook\Example", что было приемлемо для не-PnP устройства. Значение, вводимое в поле hw_id для PnP устройств, должно придерживаться определенного формата, изменяющегося в зависимости от типа шины, к которой устройство подключается, но обычно идентификатор поступает в устанавливающий программный код в виде:

тип_шины\идентификатор_модели

например:

PGI\VEN_1011&DEV_002&SUBSYS_00000000&REV_02

Устанавливающие системные сервисы весьма просто могут проследить, согласуется ли запись в inf-файле с возвращаемым вновь подключенным устройством идентификатором. В той же записи inf-файла допускается описание списка совместимого аппаратного обеспечения (разумеется, если совместимость существует) в форме задания дополнительных идентификаторов устройств. В случае, если точное совпадение с полученным ID устройства не обнаружено в данном inf-файле, делается попытка найти совпадения по совместимым идентификаторам.

PnP идентификаторы PCI устройств

Полный идентификатор для PnP PCI устройств имеет форму

PCI\Ven_vvvv&Dev_dddd&SubSys_ssssssss&Rev_rr

Здесь vvvv является идентификатором поставщика (производителя), зарегистрированным в группе PCI Special Interest Group, dddd — идентификатор, присвоенный производителем данной PCI карте, ssssssss — идентификатор конструкции (subsystem ID), rr — номер версии разработки. Все упомянутые поля вводятся как шестнадцатеричные числа. Поле ssssssss обычно вводится как нулевое.

Кроме того, допустимо в inf-файлах представлять усеченные варианты идентификационной информации, например:

PCI\Ven_vvvv&Dev_dddd&SubSys_ssssssss
PCI\Ven_vvvv&Dev_dddd&Rev_rr
PCI\Ven_vvvv&Dev_dddd
PCI\Ven_vvvv&Dev_dddd&Rev_rr&CC_ccss
PCI\Ven_vvvv&Dev_dddd&CC_ccsspp
PCI\Ven_vvvv&Dev_dddd&CC_ccss
PCI\Ven_vvvv&CC_ccsspp
PCI\Ven_vvvv&CC_ccss
PCI\Ven_vvvv
PCI\CC_ccsspp
PCI\CC_ccss

Здесь cc является кодом базового класса из конфигурационного пространства PCI устройства, ss — код подкласса, pp — идентификатор программного интерфейса.

PnP идентификаторы SCSI устройств

Полный идентификатор для PnP SCSI устройств имеет форму

SCSI\ttttvvvvvvvvpppppppppppppppprrrr

Здесь tttt является типо-кодом устройства, vvvvvvvvявляется 8-символьным идентификатором поставщика (производителя), pppppppppppppppp— 16 символьный идентификатор устройства, rrrr — номер версии разработки.

Кроме того, допустимо в inf-файлах представлять усеченные варианты идентификационной информации, например:

SCSI\ttttvvvvvvvvpppppppppppppppp
SСSI\ttttvvvvvvvv
SCSI\vvvvvvvvppppppppppppppppr
vvvvvvvvppppppppppppppppr
gggg

Здесь gggg является одним из групповых типов (generic type) классов, приведенных в таблице 12.11.

Таблица 12.11. Типы SCSI u IDE устройств

SCSI код | Устройство | Тип | Групповой тип
DIRECT_ACCESS_DEVICE (0) | Дисковое | Disk | GenDisk
SEQUENTIAL_ACCESS_DEVICE (1) | Последовательное | Sequential |
PRINTER_DEVICE (2) | Принтер | Printer | GenPrinter
PROCESSOR_DEVICE (3) | Сканнеры, принтеры и т.п. | Processor |
WRITE_ONCE_READ_MULTIPLE_DEVICE (4) | Worm | Worm | GenWorm
READ_ONLY_DIRECT_ACCESS_DEVICE (5) | CD ROM | CdRom | GencdRom
SCANNER_DEVICE (6) | Сканирующее | Scanner | GenScanner
OPTICAL_DEVICE (7) | Оптические диски | Optical | GenOptical
MEDIUM_CHANGER (8) | Устройство со сменными носителями | Changer | ScsiChanger либо GenChanger (для IDE)
COMMUNICATION_DEVICE (9) | Сетевое | Net | ScsiNet

Для SCSI диска, имеющего полный установочный PnP идентификатор SCSI\DiskSEAGATE_ST39102LW____0004, шинный драйвер сконструирует также и следующий список идентификаторов:

SCSI\DiskSEAGATE_ST39102LW____0004
SCSI\DiskSEAGATE_
SCSI\DiskSEAGATE_ST39102LW____0
DiskSEAGATE_ST39102LW____0004
GenDisk

PnP идентификаторы IDE устройств

Идентификаторы IDE устройств схожи с идентификаторами для SCSI устройств. Для IDE допустимо в inf-файлах представлять следующие варианты идентификационной информации, например:

IDE\ttttv_vrrrrrrrr
IDE\v_vrrrrrrrr
IDE\ttttv_v
v_vrrrrrrrr
gggg

Здесь tttt является типо-кодом устройства (см. таблицу 12.11), v_v является 40-символьным идентификатором поставщика (производителя), rrrrrrrr — 8-сим-вольный номер версии разработки. В случае, если идентификатор поставщика короче 40 символов, то он дополняется символами подчеркивания. Пример для третьего варианта представления PnP идентификационной информации в inf-файле:

IDE\CdRomALPS_DC544______________________________

PnP идентификаторы USB устройств

Полный идентификатор для PnP USB устройств имеет форму

USB\Vid_vvvv&Pid_dddd&Rev_rr

Здесь vvvv является идентификатором поставщика (производителя), зарегистрированным в Комитете USB производителей, dddd — идентификатор, присвоенный производителем данной модели устройства, rr — номер версии разработки. Все упомянутые поля вводятся как шестнадцатеричные числа.

Кроме того, допустимо в inf-файлах представлять усеченные варианты идентификационной информации, например:

USB\Vid_vvvv_&Pid_dddd
USB\Class_cc&SubClass_ss&Prot_pp
USB\Class_cc&SubClass_ss
USB\Class_cc

Здесь cc является кодом базового класса из полученного дескриптора устройства или дескриптора интерфейса данного USB устройства, ss — код подкласса, pp — идентификатор протокола.

PnP идентификаторы устройств IEEE-1394 (FireWire)

Полный идентификатор для PnP USB устройств имеет форму

1394\VendorName&ModelName
1394\UnitSpecId&UnitSwVersion

Здесь VendorName является наименованием поставщика (производителя), ModelName — идентификатор, присвоенный производителем данной модели устройства, UnitSpecId и UnitSwVersion — идентификаторы программных спецификаций, получаемые из конфигурационных ПЗУ подключаемых устройств, например:

1394\MICROSOFT&1394_DIAGNOSTIC_DEVICE
1394\031887&040892


Глава 12. Инсталляция драйверов при помощи INF файлов

Заключение

Установка драйвера при помощи inf-файлов является приемлемым и общепринятым решением для проведения установки драйвера в системе, особенно если драйвер следует передать для использования другим людям, которые не могут и вовсе не обязаны быть в курсе тонкостей установки разработанного драйвера.

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

В следующей главе будут рассмотрены общие вопросы отладки драйверного кода.


Глава 13. Тестирование и отладка

Глава 13

Тестирование и отладка

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

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

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

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

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

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

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

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

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


Глава 13. Тестирование и отладка

Что следует проверять?

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

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

Фирма Microsoft предлагает тесты на аппаратурную совместимость (HCT, Hardware Compatibility Tests), которые являются официальными тестами для аппаратуры по поводу возможности ее работы под Windows 2000/XP/2003. Набор включает следующие тесты:

  • Общие системные тесты, которые экзаменуют центральный процессор, последовательные и параллельные порты материнской платы, клавиатуру и средства поддержки слоя аппаратный абстракций HAL.
  • Тесты по проверке специфических типов аппаратного обеспечения, а именно — видеоадаптеров, мультимедийных устройств, сетевых интерфейсов, накопителей на магнитной ленте, SCSI устройств и т.п.
  • Общие тесты по тестированию работы системы под действием высоких нагрузок на системные ресурсы и устройства ввода/вывода.

Тестирование проходит под управлением тест-менеджера с графическим интерфейсом, который автоматизирует прохождение тестов и сбор результатов. Даже если класс аппаратуры, для которой разрабатывается драйвер, не прошел серию испытаний HCT, то все равно этот набор тестов может послужить средством для выяснения того, как будет работать новый драйвер в условиях повышенной нагрузки на систему.

Цифровое подписание драйвера

Огромное количество драйверов сторонних поставщиков (третьей стороны, third-party companies) поставляется в составе CD дистрибутива Microsoft Windows. Для того чтобы участвовать в этой программе, поставщик драйвера должен выполнить некоторые требования, сформулированные Microsoft. B интересах Microsoft обеспечить достижение двух взаимоисключающих целей:

  • обеспечить работу под управлением Windows максимально разнообразной и многочисленной аппаратуры при обычном способе распространения дистрибутива;
  • обеспечить стабильность работы новых драйверов без нарушения каких-либо свойств или целостности системы.

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

Разумеется, совместимость Windows 2000/XP/2003 с большим количеством аппаратуры, нежели другие операционные системы, является сильным фактором стимулирования продаж. Следовательно, Microsoft непрерывно контактирует с производителями аппаратного обеспечения на предмет получения своевременных и протестированных конечных версий драйверов. Вплоть до того, что конфигурация Windows Server 2003 Datacenter Edition поставляется только OEM поставщиками совместно с компьютером.

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

  • использовании логотипа Windows как знака соответствующей ступени сертифицированности поставляемой аппаратуры и программного обеспечения к нему;
  • включении в официальный перечень поддерживаемого и сертифицированного оборудования для различных операционных систем разработки Microsoft (см. интернет-страницу microsoft.com/hcl);
  • автоматического распространение драйверного обеспечения вместе с будущими поставками новых под-версий Windows;
  • получении цифровой подписи.

Подробнее ознакомиться с условиями участия в этой программе (процедурой и ценами на работы) можно на интернет-странице microsoft.com/hwtest.

Как часть результата участия в программе WHQL, драйвер получает цифровую подпись (digital signature), защищенную криптографическими методами, которая позволяет Windows 2000/XP/2003 осуществлять инсталляцию драйвера без выдачи обескураживающего предупреждения о "надвигающейся" опасности. Цифровая подпись является, по мнению Microsoft, средством отсеивания ненадежного кода и устройств и гарантией того, что устанавливаемый код является оригинальным драйверным кодом от поставщика. Применение собственно цифровой подписи касается двух моментов:

  • Файла-каталога (с расширением CAT), который включается в распространяемый пакет драйверного программного обеспечения. Он и содержит цифровую подпись, предоставленную Microsoft.
  • Запись в INF файле в составе секции [Version], где указывается ссылка на CAT файл.

Сама цифровая подпись ни в коей степени не производит модификацию кода драйверного программного обеспечения.


Глава 13. Тестирование и отладка

Драйвер отказывается работать?

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

Аппаратные проблемы

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

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

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

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

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

Утечка ресурсов

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

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

Торможение программных потоков

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

Самая простая ошибка состоит в том, что драйвер по какой-то причине не выполняет вызов IoCompleteRequest. В результате, присланный IRP пакет никогда не возвращается Диспетчеру ввода/вывода. Иногда не столь очевидна необходимость сделать вызов IoStartNextPacket (при использовании очередей IRP пакетов). Однако даже если не существует запросов, ожидающих обработки, драйвер должен выполнить этот вызов, поскольку только таким образом объект устройства будет "помечен" как простаивающий. Без этого вызова новые IRP пакеты будут помещаться в очередь ожидания обработки, так и не поступая в процедуру StartIo драйвера.

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

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

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

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

Проблема приоритетов времени выполнения

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

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

Показательным случаем неправильного использования приоритетов является вызов другого драйвера при помощи IoCallDriver, который формально может быть выполнен на уровнях IRQL APC_LEVEL и DISPATCH_LEVEL. Повысив текущий приоритет потока перед вызовом IoCallDriver до значения, например, DISPATCH_LEVEL, драйвер, скорее всего, организует блокировку: если вызываемый драйвер должен работать на уровне PASSIVE_LEVEL, то он не может сделать нужные ему вызовы, пока работает вызвавший его поток, который ждет возвращения управления из IoCallDriver. Система приходит в неработоспособное состояние ("замерзает").

Отслеживание ошибок

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

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


Глава 13. Тестирование и отладка

Отладчики

Отладчик WinDbg

Одним из отладчиков, который может использовать разработчик драйверов, является отладчик WinDbg, поставляющийся в составе пакета DDK. Отладчик WinDbg представляет собой гибрид отладчика уровня ядра и пользовательского режима. При помощи WinDbg можно проводить анализ файлов "crash dump files" (отображение физической памяти в момент сбоя), отлаживать драйверный код путем трассирования (исполнения), анализировать "dump file" приложений (пользовательского режима).

Помимо того, что WinDbg выполняет свои задачи и в режиме ядра, и в пользовательском режиме, он также предоставляет рабочий интерфейс и в графической форме, и в форме командной строки, характерной для старых отладчиков.

Одной из самых значительных возможностей, которой обладает отладчик WinDbg, является его способность поддерживать отладку компонентов уровня ядра в исходных текстах отлаживаемых модулей. При этом отладчику должны быть доступны и исходные тексты, и файлы с отладочными символами (далее они будут называться "файлы идентификаторов").

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

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

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

Сумма этих затруднений может перевесить чашу весов в пользу других отладчиков, в особенности, отладчика SoftIce фирмы CompuWare Corporation (разумеется, если не принимать в рассмотрение цену полной версии пакета Driver Studio).

Директории идентификаторов

Файлы идентификаторов (symbol files) создаются по дополнительно установленным флагам компиляции при компиляции и сборке проекта. В них находятся имена локальных и глобальных переменных, адреса функций и информация об определениях типов данных. Приводится также информация о номерах строк, поэтому машинные инструкции могут быть легко соотнесены с исходным текстом. Программные средства Microsoft могут предоставлять эти файлы в нескольких форматах: более старом (но более типичном) COFF (Common Object File Format) и формате PDB (Program Database), что определяется настроечными флагами компиляции и сборки.

Сама операционная система Windows тоже поставляется с файлами идентификаторов, которые могут быть и на дополнительном диске дистрибутива, и в составе DDK (разумеется, речь не идет об исходных текстах). Для операционной системы Windows 2000 такие файлы, возможно, до сих пор можно загрузить с Интернет сервера Microsoft. Для остальных версий они распространяются Microsoft в рамках подписки MSDN либо в составе набора CD дисков Driver Suite. Поскольку файлы идентификаторов меняются при каждой новой сборке (build), то обновление системы (service pack) требует также и обновления файлов идентификаторов, относящихся к системным модулям.

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

Когда установлены необходимые файлы идентификаторов, отладчику WinDbg необходимо сообщить об их местонахождении. Это выполняется с использованием в пункте Symbol File Path пункта меню File. Идентификаторы операционной системы обычно устанавливаются в директории %SystemDir%\Symbols. Файлы идентификаторов драйвера (расширения .DBD или .PDB) обычно хранятся вместе с бинарным файлом драйвера (.SYS, .WDM).

Директории исходных текстов

Помимо файлов идентификаторов отладчику понадобятся файлы исходных текстов драйвера (.c, .cpp, .h). Путь к ним необходимо указать в пункте Source File Path пункта меню File.

Запуск и окончание отладочной сессии

Основное предназначение отладчика WinDbg — интерактивная отладка. Для этого необходимо наличие двух компьютеров: целевого (на нем будет запущен тестовый код) и хост-компьютер (на котором производится разработка и работает WinDbg). Существенно здесь то, что для управления и мониторинга активности на целевом компьютере используется интерфейс последовательных портов. На рисунке 13.1 показано соединение хост и целевого компьютеров и размещение файлов на них.

Для успешной отладки необходимо, чтобы все нужные файлы оказались на своих местах. Последовательность действий такова.

Рис. 13.1
Интерактивная отладка с использованием WinDbg

  1. Инсталлировать бинарный файл драйвера на целевой машине, причем там не нужно устанавливать файлы идентификаторов или исходного текста.
  2. На хост-компьютере необходимо иметь файлы идентификаторов драйвера и файлы идентификаторов операционной системы, установленной на целевом компьютере. Неплохо было бы, чтобы на двух компьютерах была бы установлена одна и та же версия системы (и одинаковые обновления, Service pack), однако это не является жестким условием.
  3. На хост-компьютере запускается WinDbg. Происходит установка путей к исходному коду и к файлам идентификаторов. Эти файлы должны соответствовать бинарному файлу отлаживаемого драйвера, который находится на целевом компьютере.
  4. Необходимо выбрать отладку в режиме ядра в меню View закладки Options Kernel Debugger в отладчике WinDbg.
  5. Необходимо установить приемлемые значения номера СОМ порта и скорости передачи из упомянутого диалогового окна.
  6. Следует выбрать кнопку GO или ввести команду g в окне командной строки отладчика для того, чтобы перевести WinDbg в режим ожидания соединения с целевым компьютером.
  7. Необходимо выполнить перезагрузку целевого компьютера, разрешив использование клиента отладки. Когда завершится перезагрузка системы на целевом компьютере, соединение будет установлено. В командном окне WinDbg будет выведено соответствующее сообщение.

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

Чтобы разорвать отладочную сессию, необходимо выполнить следующее.

  1. Сделать паузу, нажав Ctrl-C в командном окне WinDbg.
  2. Следует выбрать Edit — Breakpoints в отладчике WinDbg, затем выполнить Clear All. Важно выполнить очистку точек останова до прекращения отладочной сессии.
  3. Следует выбрать Run — Go для того, чтобы разрешить дальнейшую работу на целевом компьютере.
  4. Выйти из WinDbg (Alt-F4).

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

Отладчик SoftIce

Отладку драйвера режима ядра на одном компьютере обеспечивает мощный отладчик SoftIce, разработанный фирмой CompuWare Corporation и входящий в состав пакета DriverStudio. Внешний вид интерфейса отладчика сохранился еще со времен MS DOS, однако, при некоторой тренировке с использованием мнемонических команд, вряд ли это доставит много неудобств, тем более что SoftIce управляется мышкой, включая мышку с колесиком.

Параметры запуска устанавливаются конфигуратором DSConfig, который определяет способ запуска SoftIce (при загрузке, по СОМ, по IP адресу, запуск по требованию на данном компьютере), настройки размера окна, настройки мышки.

При настройке загрузки по требованию следует явно запустить из меню команд Пуск -... - Start SoftIce. Данная команда загружает отладчик, а собственно его активация производится комбинацией Ctrl-D.

В том случае, если нужно выполнить отладку драйвера, после загрузки SoftIce следует загрузить файл драйвера (для отладки в исходных текстах — обязательно отладочную версию) программой loader32 командой Пуск — ... — Symbol Loader. Лишь после этого следует активировать отладчик нажатием Ctrl-D.

При помощи команды h в окне активированного отладчика можно ознакомиться с полным набором и форматом команд. Это же можно узнать и из прилагаемой документации.

При помощи определенных команд несложно отыскать драйвер в памяти, если известно имя драйвера или объекта устройства, после чего можно устанавливать точки прерывания в коде драйвера. Затем следует деактивировать отладчик повторным нажатием Ctrl-D. В момент, когда приложение пользовательского режима вызовет драйвер и, соответственно, достигнет указанной точки прерывания, отладчик активизируется самостоятельно. В окне отладчика будет виден нужный фрагмент драйвера.

В то время, когда активно окно отладчика SoftIce, активность операционной системы замирает: не обновляются показания часов, не работает обмен через clipboard и т.п.

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

Более детально ознакомиться с отладчиком SoftIce можно, загрузив trial-версию с Интернет сайта CompuWare Corporation.


Глава 13. Тестирование и отладка

Чтение crash-экранов

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

Кто инициирует объявление системного сбоя?

Во-первых, некоторые компоненты уровня ядра, которые "по долгу службы" обнаружили некорректное состояния, могут решить, что пришла пора "сказать стоп". Например, если Диспетчер ввода/вывода обнаруживает, что драйвер передал в вызов IoCompleteRequest завершенный ранее пакет IRP, то Диспетчер ввода/вывода непременно вызовет этот самый сигнал фатального системного сбоя.

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

Независимо от того, какие подсистемы ядра выступают инициатором системного останова, реально выполняется вызов одной из двух функций:

VOID KeBugCheck( BugChCode );
VOID KeBugCheckEx( BugChCode, Arg1, Arg2, Arg3, Arg4 );

Любая из этих функций вызывает останов системы и (необязательно) сохраняет файл со снимком памяти (crash dump file) на жестком диске. В зависимости от выбора системного администратора, операционная система после этого прекращает работу, перезапускается или запускает отладку ядра (kernel's debug client).

Аргумент BugChCode в вызове KeBugCheck указывает причину сбоя в виде значения типа DWORD. Этот аргумент известен также под именем "bugcheck code". Дополнительные 4 аргумента в функции KeBugCheckEx дают возможность видеть в диагностическом сообщении дополнительную информацию (что, возможно, позволит удачнее локализовать источник сбоя).

Коды "bugcheck codes" могут быть использованы из числа определенных Microsoft (см. файл BUGCODES.h), либо можно определить в драйвере дополнительно.

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

Голубой экран смерти (BSOD)

Системное сообщение об останове системы — это появляющееся на голубом фоне сообщение, которое носит закрепившееся за ним название "Blue Screen Of Death" (BSOD). В разных версиях NT вид BSOD сообщений сильно менялся. В русифицированных версиях зачастую на экране видна абракадабра вместо текста (из-за неверной кодировки), что, однако, не должно сильно обременять разработчика. Главной информацией являются две строки, содержащие код ошибки и некоторые параметры сбоя (шестнадцатеричные числа), например:

DRIVER_IRQL_NOT_LESS_OR_EQUAL
*** STOP: 000000Dl (00000000 00000002 00000000 F8B57624)
*** Example.sys - address F8B57624 base at F8B57000 Datestamp 3e6da099

Сообщение код "bugcheck code", указанный в текстовой форме (DRIVER_IRQL_NOT_LESS_OR_EQUAL), его шестнадцатеричное значение (здесь 000000D1, после слова STOP) и четыре аргумента, переданные в вызове KeBugCheckEx. B зависимости от кода интерпретируется значение остальных 4-х аргументов (см. Приложение А).

Коду 0xD1 соответствует DRIVER_IRQL_NOT_LESS_OR_EQUAL. Приложение А указывает, что код 0xD1 означает ошибку, связанную с обращением к отсутствующей в физической памяти странице (page fault) на уровне IRQL равном DISPATCH_LEVEL или выше. Кроме того, при этом коде ошибки можно сказать, что четыре аргумента KeBugCheckEx означают следующее:

Arg1: Ссылочный адрес (обращение к которому вызвало сбой) 00000000
Arg2: Текущий IRQL уровень 2
Arg3: Тип доступа 0 (что означает "чтение")
Arg4: Адрес инструкции, которая вызвала сбой 0xF8B57624

Анализ информации Crash Dump файлов

Когда происходит фатальный сбой системы, операционная система может (при определенных настройках системы) сохранить состояние системы в момент сбоя в файле на жестком диске. Чтобы это выполнялось, в апплете Пуск - Настройка - Панель Управления - Система - Дополнительно - Загрузка и восстановление - Параметры (рисунок 13.3) необходимо ввести приемлемые данные. Фиксирование состояние системы в момент сбоя позволит затем вернуться к анализу причин сбоя.

Рис. 13.3
Окно системного апплета для установки параметров crash dump файла

При помощи отладчика WinDbg можно проанализировать состояние системы в момент сбоя по содержимому crash dump файла. Там можно обнаружить всю информацию, которая была бы доступна отладчику, если бы он оказался включенным в момент сбоя. Это тип экспертизы позволяет добывать убедительные доказательства причин, приведших к фатальному сбою. Во время анализа необходимо установить:

  • Какой драйвер выполнялся в момент сбоя?
  • Какую операцию пытался выполнить драйвер в момент сбоя?
  • Каково содержимое структуры расширения объекта устройства в момент сбоя?
  • Какой объект устройства работал?

После запуска WinDbg необходимо открыть crash dump файл (полученный с целевого компьютера) для анализа, выбрав пункт меню File и затем Open Crash Dump. После выбора и загрузки необходимого файла, на экране появится информация следующего (например) типа:

Windows XP Kernel Version 2600 UP Free x86 compatible
Kernel base = 0x804d4000 PsLoadedModuleList = 0x8054be30
Bufcheck 000000Dl : 00000000 00000002 00000000 F8B57624
. . .

Как видим, начальная информация показывает те же данные, что и сообщение экрана BSOD. Не должно вводить в заблуждение сообщение о не перехваченном исключении с кодом 0x80000003. Это точка прерывания, используемая собственно KeBugCheck для останова системы, поэтому не следует придавать ей значения.

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

Loading Dump File [E:\SystemCrashDump\MEMORY.DMP]
Kernel Dump File: Full address space is available

Microsoft (R) Windows Kernel Debugger Version 3.0.0020.0
Copyright (c) Microsoft Corporation. All rights reserved.

Loaded dbghelp extension DLL
Loaded ext extension DLL
Loaded exts extension DLL
Loaded kext extension DLL
Loaded kdexts extension DLL
Symbol search path is: *** Invalid *** : Verify _NT_SYMBOL_PATH setting
Executable search path is:
*********************************************************************
* Symbols can not be loaded because symbol path is not initialized. *
*                                                                   *
* The Symbol Path can be set by:                                    *
*   using the _NT_SYMBOL_PATH environment variable.                 *
*   using the -y <symbol_path> argument when starting the debugger. *
*   using .sympath and .sympath+                                    *
*********************************************************************
*** ERROR: Symbol file could not be found. Defaulted to export symbols for
ntoskrnl.exe -
Windows XP Kernel Version 2600 (Service Pack 1) UP Free x86 compatible
Built by: 2600.xpspl.020828-1920
Kernel base = 0x804d4000 PsLoadedModuleList = 0x8054be30
Debug session time: Fri Jun 13 02:20:17 2003
System Uptime: 0 days 3:15:59
*********************************************************************
* Symbols can not be loaded because symbol path is not initialized. *
*                                                                   *
* The Symbol Path can be set by:                                    *
*   using the _NT_SYMBOL_PATH environment variable.                 *
*   using the -y <symbol_path> argument when starting the debugger. *
*   using .sympath and .sympath+                                    *
*********************************************************************

WaitForEvent failed
WARNING: Stack unwind information not available. Following frames may be wrong.
WARNING: Stack unwind information not available. Following frames may be wrong.
*******************************************************************************
*                                                                             *
*                         Bugcheck Analysis                                   *
*                                                                             *
*******************************************************************************

***** Kernel symbols are WRONG. Please fix symbols to do bugcheck analysis.
*******************************************************************************
*                                                                             *
*                         Bugcheck Analysis                                   *
*                                                                             *
*******************************************************************************
Bugcheck code 000000D1
Arguments 00000000 00000002 00000000 f8b57624

ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
f2270b54 804dce53 0000000a 00000000 00000002 ntoskrnl!KeBugCheckEx+0xl9
f2270b70 00000000 00000000 00000000 00000000 ntoskrnl!Kei386EoiHelper+0x251c

eax=ffdff13c ebx=0000000a ecx=00000000 edx=40000000 esi=f8b57624 edi=00000000
eip=805266db esp=f2270b3c ebp=f2270b54 iopl=0         nv up ei ng nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000286
ntoskrnl!KeBugCheckEx+19:
805266db 5d               pop     ebp

Самой важной информацией, которую можно извлечь из приведенного выше набора строк — это код ошибочной ситуации (bugcheck code), равный 0x00D1. Остальные сведения, связанные с раскруткой стека вызовов в данной ситуации недоступны. Хотя именно данная информация особенно ценна в crash dump файлах.


Глава 13. Тестирование и отладка

Общие приемы отладки

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

Установка фиксированных точек прерывания

При использовании интерактивных отладчиков не найдется много причин устанавливать фиксированные точки прерывания внутри кода драйвера (hard breakpoints). Тем не менее, это можно сделать, если воспользоваться двумя функциями:

VOID DbgBreakPoint();
VOID KdBreakPoint();

Вызов KdBreakPoint представляет из себя макроопределение, которое определяет условную компиляцию с целью выполнить вызов DbgBreakPoint. Это макроопределение не выполняет данного вызова, если выполнена релизная (без отладочных инструкций) сборка драйвера (free build).

Будьте внимательны: Windows дает фатальный сбой с сообщением KMODE_EXCEPTION_NOT_HANDLED в том случае, если драйвер применил фиксированную точку прерывания (через упомянутые вызовы), но в этот момент клиент отладки (см. рисунок 13.1) был недоступен. Если драйвер добрался до такой точки прерывания, и не оказалось отладчика, подключенного к последовательному порту, то драйвер "виснет". В некоторых случаях, ситуация может быть выправлена запуском отладчика на хост-компьютере.

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

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

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

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

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

Сохранение отладочного кода в исходном тексте драйвера

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

Утилита BUILD использует символ времени компиляции DBG, который может быть использован при составлении условно компилируемых фрагментов. В отладочной версии (checked build) этому символу присвоено значение 1, в версии free значение DBG равно 0. При внесении отладочного кода в драйвер следует ограничивать его рамками директив условной компиляции типа #if DGB и #endif.

Перехват некорректных условий

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

Для того чтобы перехватить эти непредусмотренные отклонения, следует применять несложный прием: такие допущения должны проверяться во время выполнения, хотя бы в отладочной сборке драйвера. Макроопределения ASSERT и ASSERTMSG помогут решить эту задачу:

ASSERT( Expression );
ASSERTMSG( Message, Expression );

В случае если выражение Expression, рассмотренное как логическое, будет равно FALSE, макроопределение ASSERT произведет запись сообщения в командное окно WinDbg, подключенного на хост-компьютере для проведения отладки. Это сообщение содержит исходный код выражения, значение которого оказалось равным FALSE, имя файла (в котором был исходный текст данного фрагмента) и номер строки, где было вызвано макроопределение ASSERT. Затем предлагается сделать выбор, сделать ли прерывание в месте данного макроопределения ASSERT, игнорируя причину остановки, либо прервать процесс или поток, в котором "сработал" ASSERT (что, впрочем, совершенно аналогично применению ASSERT в Visual Studio).

Макроопределение ASSERTMSG демонстрирует точно такое же поведение, с той лишь разницей, что в сообщение включается содержимое Message (являющееся строкой). В отличие от debug print функций, описанных ранее, ASSERTMSG не допускает форматного вывода данных в стиле printf.

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

Кроме того, в основе упомянутых выше макроопределений лежит использование функции RtlAssert, которая во "free" версиях Windows 2000/XP/2003 превращается в пустышку. Следовательно, чтобы наблюдать последствия ошибок в макроопределениях ASSERT и ASSERTMSG следует тестировать драйвер под отладочной (checked) версией Windows.

Использование диагностических callback-функций

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

  1. В процедуре DriverEntry необходимо выполнить вызов функции KeInitializeCallbackRecord для выполнения настройки структуры KBUGCHECK_CALLBACK_RECORD. Место для хранения этой закрытой структуры должно быть выделено в нестраничном пуле (причем должно содержаться в не прикосновенности до момента ее де-регистрации, выполняемой при помощи функции KeDeregisterBugCheckCallback в процедуре Unload данного драйвера).
  2. В DriverEntry необходимо выполнить вызов KeRegisterBugCheckCallback для подключения драйвера к механизму уведомления об ошибочных ситуациях. Аргументами этого вызова будет указатель на структуру типа KBUGCHECK_CALLBACK_RECORD, адрес предоставляемой драйвером callback-функции, адрес и размер предоставляемого драйвером буфера и строка, которая будет использована для идентификации буфера. Место для буфера должно быть выделено в нестраничном пуле (причем также должно содержаться в неприкосновенности до момента вызова функции KeDeregisterBugCheckCallback).
  3. В том случае, если была обнаружена ошибка, система выполняет вызов зарегистрированной в п. 2 callback-функции, которой будет передан адрес буфера и его размер. Работа вызванной функции состоит в том, что она должна заполнить предоставленный буфер информацией, которую сочтет необходимой, и которая не смогла бы иным образом найти свое отражение в crash dump файле, например, внутреннее содержимое регистров обслуживаемого устройства.
  4. При анализе crash dump файла при помощи WinDbg эту информацию можно вывести на экран, если воспользоваться командой !bugdump.

Существуют некоторые ограничения на действия, разрешенные регистрируемой callback-функции. Во время работы она не может получать какие-либо системные ресурсы (например, память). Она также не должна использовать объекты синхронизации. Однако разрешено делать вызовы процедур ядра, которые не нарушают указанных ограничений, и вызовы процедур HAL уровня, например, для того, чтобы получить доступ к регистрам устройства.

Обнаружение утечек памяти

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

  1. Необходимо заменить вызовы ExAllocatePool на вызовы ExAllocatePoolWithTag. Дополнительный аргумент, представляющий собой четырехбайтную величину (4 символа), используется для того, чтобы пометить вновь выделенный блок этим значением (тегом).
  2. Необходимо запустить драйвер под отладочной версией (checked build) Windows. Поддержка трассировки страниц пула является дорогостоящим "удовольствием", поэтому доступно оно только в отладочных версиях Windows.
  3. Когда выполняется анализ crash dump файла или в ситуации, когда достигну та точка прерывания при отладке "живого" драйвера, следует воспользоваться командами !poolused или !poolfind для того, чтобы ознакомиться с состоянием пулов памяти. Эти команды сортируют области пулов по значению тегов и высвечивают различные статистические данные об использовании памяти. Следует помнить, что в отсутствии файлов отладочных символов указанные команды WinDbg не работают.

Легкий способ повсеместной замены вызовов ExAllocatePool на вызовы ExAllocatePoolEx состоит в том, чтобы изначально использовать фрагменты условной компиляции, например:

#if DBG==1
    #define ALLOCATE_POOL(type,size) \
            ExAllocatePoolWithTag((type),(size),'1234')
#else
    #define ALLOCATE_POOL(type,size) ExAllocatePool((type),(size))
#endif

Аргумент тега в вызове ExAllocatePoolWithTag состоит из четырех букв (в верхнем регистре), которые на экране отладчика предстанут в обратном порядке, то есть '4321'.

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

Поставляемая в составе DDK утилита PoolTag позволяет наблюдать теговое выделение памяти и без привлечения отладчика WinDbg. Эта программа непрерывно выводит на экран обновляемые данные о страничных тегах.

Установка параметров загрузки в файле boot.ini

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

Его можно редактировать при помощи редактора notepad (если убрать атрибут "только для чтения", например, в программе WinCommander-TotalCommander), но более правильно это делать в окне системного апплета, показанного на рисунке 13.3.

В документации DDK несложно отыскать описание задаваемых параметров загрузки — по ключу "boot.ini" - "Parameters for boot.ini". Помимо параметра /sos, подробно рассмотренного в приложении Б, разработчику драйверов может быть также полезен параметр /maxmem=Xxx, который ограничивает размер используемой физической памяти, что позволит протестировать систему и драйвер в условиях недостатка физической памяти.


Глава 13. Тестирование и отладка

Частные приемы восстановления системы

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

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

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

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

  • загрузка последней удачной конфигурации;
  • загрузка в безопасном режиме.

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

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

При более серьезных проблемах можно воспользоваться загрузкой с внешнего носителя. Способ создания загрузочных дискет для Windows XP описан на Интернет-сайте Microsoft (статья Базы Знаний номер Q30559) по адресу support.microsoft.com/default.aspx?scid=kb;ru;30559. Загрузочная дискета позволит получить доступ к логическим дискам с установленной системой Windows. Логично пользоваться этим способом на компьютерах, не поддерживающих загрузки с CD ROM. (Имеется в виду то, что дискета обеспечивает доступ к загрузочному CD диску, который также необходим в этом процессе.)

В том случае, если в распоряжении имеется компакт-диск для установки Windows и компьютер может быть загружен с CD ROM (практически все современные компьютеры на платформе Intel позволяют это сделать), то следует выполнить именно такой способ загрузки и войти в Консоль Восстановления системы (Recovery Console). B этом режиме будут доступны системные консольные команды, список которых можно получить по команде help. Наиболее актуальной является команда принудительной проверки диска (например, логического диска D)

CHKDSK D: /p

В данном случае ключ /p обеспечивает принудительную проверку.

В том случае, если оказалась испорчена загрузочная запись (MBR) или следует избавиться от загрузочной записи нестандартных загрузчиков LILO, MILO и т.п., следует воспользоваться программой fixmbr. В этом же режиме можно выполнить поверку диска. Перезаписать загрузочный (boot) сектор на логическом диске можно при помощи команды fixboot.

При ненарушенной файловой системе затруднения в загрузке могут быть обусловлены плохим состоянием информации в Системном Реестре. Некоторые источники предлагают (помимо специальных программ) такой метод восстановления как простое копирование ранее сохраненных файлов Системного Реестра на их место в директории %systemroot%\config. В нормальном рабочем состоянии операционная система не разрешает доступа к ним (для просмотра, копирования или перезаписи), однако, при загрузке с установочного CD диска, это становится допустимой операцией. Файлы Реестра, которые всегда имеют солидные размеры, для этого случая можно сохранять на других логических дисках компьютера.

Заметим также, что все затруднения, связанные со спецификой доступа к разделам файловой системы в формате NTFS, можно обойти, если установить операционную систему на логический диск с форматом FAT32. Утратив некоторые преимущества NTFS (такие, как запись файлов более 4Гбайт и более высокая надежность), можно будет получать доступ к таким разделам с дискеты MS-DOS или при загрузке Windows 98 с другого логического диска. Совершенно аналогичный и официальный совет дает Microsoft, предлагая установить на компьютере еще один экземпляр Windows NT на другом логическом диске, что, безусловно, предоставит возможность доступа ко всем NTFS разделам, в том числе — к тому, на котором испытывается драйвер и, возможно, произошел сбой.

Наконец, еще одним средством доступа к файлам операционной системы Windows NT 5, размещенным на диске с файловой системой NTFS, является программа NTFSDOS, разработанная фирмой SysInternals. При загрузке MS DOS либо при загрузке с загрузочной дискеты, сделанной под Windows 98, эта программа (если она записана в не-NTFS разделах) может быть запущена и позволяет получить доступ к NTFS разделам жесткого диска, включая запуск программы CHKDSK. (Интересно, что в свой загрузочный набор программа NTFSDOS при установке включает еще файлы Ntoskrnl.exe, Ntfs.sys и Ntdll.dll из установленной на компьютере версии Windows NT.) Соответственно, это позволяет модифицировать данные в NTFS разделах — при наличии полной версии NTFSDOS Professional — или копировать их в другие не-NTFS разделы (демо-версия), если они присутствуют в системе. Возможен вариант, при котором подготавливается комплект из 2-х дискет, с которых и выполняется загрузка.

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


Глава 13. Тестирование и отладка

Заключение

Данная глава была посвящена рассмотрению общих вопросов тестирования драйверов.

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

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

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

Операционная система Windows NT 5 (версии 2000, XP и Server 2003) отличается богатым набором инструментальных средств, которыми разработчик быстро и эффективно может локализовать ошибку, особенно, если она является в большей степени программной (в отличие от аппаратных ошибок обслуживаемого устройства). Остается лишь научиться это применять на практике.


Приложение А. Коды ошибочных ситуаций

Приложение А

Коды ошибочных ситуаций

При выводе системных сообщений о прекращении работы (известные как bug-checks), выводятся также коды, по которым можно определить, что побудило систему запаниковать. В зависимости от ошибки, система сообщает до 4-х дополнительных параметров, которые дают дополнительную информацию о возникшей проблеме. Хотя полный перечень кодов можно найти в заголовочном файле bug-codes.h, входящий в пакет DDK, расшифровки значений там не приводится. По этой причине ниже приводятся наиболее часто встречающиеся коды, основные причины данных ситуаций и расшифровка дополнительных параметров. Данному вопросу посвящена статья Microsoft Knowledge Base Q103059.

0x08 | IRQL_NOT_DISPATCH_LEVEL
Был выполнен функцией (предназначенной для вызова с уровня IRQL, равного DISРATCH_LEVEL) из кода, работающего на более высоких уровнях, например DIRQL
Параметры | Описание
1-4 | Зарезервировано

0x0A | IRQL_NOT_LESS_OR_EQUAL
Драйвер пытается получить доступ к странично организованной памяти при работе на уровне привилегий IRQL равном DISPATCH_LEVEL или выше, причем эта страница в момент обращения отсутствует в оперативной памяти (ситуация не может быть перехвачена обработчиком PAGE FAULT)
Параметры | Описание
1 | Адрес в страничной памяти, обращение по которому вызвало сбой
2 | Значение уровня IRQL в момент обращения
3 | Тип доступа при этом обращении 0 — чтение, 1 — запись
4 | Адрес инструкции, которая выполняла некорректное обращение

0x10 | SPIN_LOCK_NOT_OWNED
Программный поток пытается освободить объект спин-блокировки, которым не владеет
Параметры | Описание
1-4 | Зарезервировано

0x19 | MEMORY_MANAGEMENT
Структура "кучи" (heap — область, из которой программному коду динамически выделяется память) нарушена
Параметры | Описание
1-4 | Зарезервировано

0x1E | KMODE_ESCEPTION_NOT_HANDLED
Программный код режима ядра вызвал исключение, для которого нет обработчика (в частности, его не предоставил сам программный код)
Параметры | Описание
1 | Код исключения (как правило, это значения, которые можно найти в файле winerror.h), в частности: • 0xC0000005 — нарушение прав доступа (access violation): ошибочный указатель или запись в область памяти "только для чтения" • 0xC000001D — ошибочная инструкция. Возможно, при выполнении вызова по указателю произошло неправильное декодирование инструкции, поскольку адрес был ошибочен. Возможно, что стек был испорчен, в результате чего не сохранился адрес возврата из текущей функции • 0xC0000094 — целочисленное деление на ноль • 0xC00000FD — переполнение стека, например, в результате многочисленных рекурсивных вызовов
2 | Адрес инструкции, которая вызвала исключение
3 | Первый параметр исключения
4 | Второй параметр исключения

0x24 | NTFS_FILE_SYSTEM
Выявлена проблема в ntfs.sys
Параметры | Описание
1 | Исходный файл и номер строки
2 | Адрес записи исключения (необязательно)
3 | Адрес записи контекста (необязательно)
4 | Адрес инструкции, где было вызвано исключение (необязательно)

0x2A | INCONSISTENT_IRP
Проблема в структуре IRP пакета: параметры конфликтуют. Возможно, в результате того, что указатель на IRP пакет ошибочно использовался в качестве указателя на элемент данных другого типа
Параметры | Описание
1 | Адрес IRP пакета
2-4 | Зарезервировано

0x2E | DATA_BUS_ERROR
Данная ошибка обычно бывает обусловлена сбоем в контроле четности в системной памяти — аппаратная проблема. (Данная ошибка может быть также вызвана обращением драйвера по несуществующему виртуальному адресу 0x8xxxxxxx в системном адресном пространстве.)
Параметры | Описание
1 | Виртуальный адрес, который вызвал сбой
2 | Физический адрес, который вызвал сбой
3 | Содержимое PSR (Process Status Register, регистра состояния процессора)
4 | Faulting instruction register (FIR)

0x35 | NO_MORE_IRP_STACK_LOCATIONS
Был создан IRP пакет, в котором оказалось недостаточно ячеек стека (stack locations) для того, чтобы передать его в IoCallDriver — для обработки нижними слоями драйверов
Параметры | Описание
1 | Адрес IRP пакета
2-4 | Зарезервировано

0x36 | DEVICE_REFERENCE_COUNT_NOT_ZERO
Был выполнен системный вызов IoDeleteDevice, в процессе которого обнаружилось, что число ссылок на объект устройства все еще не равно нулю, то есть операция удаления объекта некорректна
Параметры | Описание
1 | Адрес объекта устройства (Device Object)
2-4 | Зарезервировано

0x3F | NO_MORE_SYSTEM_PTES
Системная таблица страниц заполнена. Наиболее вероятная причина состоит в том, что драйвер не выполняет освобождение страниц памяти. Возможно, слишком мала таблица страниц (следует обратиться к методам ее увеличения)
Параметры | Описание
1-4 | Зарезервировано

0x44 | MULTIPLE_IRP_COMPLETE_REQUESTS
Был выполнен системный вызов IoCompleteRequest, в процессе которого обнаружилось, что относительно данного IRP пакета это действие уже выполнялось ранее
Параметры | Описание
1 | Адрес IRP пакета
2-4 | Зарезервировано

0x50 | PAGE_FAULT_IN_NONPAGED_AREA
Ошибочное обращение к области памяти в системном адресном пространстве
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 | Зарезервировано

0x51 | REGISTRY_ERROR
Структура Системного Реестра нарушена, возможно, в процессе предыдущего системного сбоя. К сожалению, переустановка Windows самый вероятный способ преодоления этой проблемы. (Разумеется, если не выполнялось резервное копирование системных файлов.)
Параметры | Описание
1-4 | Зарезервировано

0x58 | FT_DISK_INTERNAL_ERROR
Система была загружена из восстановленного основного раздела (primary partition). (Поддерево реестра говорит — все хорошо, но на самом деле это не так.)
Параметры | Описание
1-4 | Зарезервировано

0x76 | PROCESS_HAS_LOCKED_PAGES
Работа завершена, но остались зафиксированные в памяти (locked) страницы. Возможно, драйвер пытался освободить заблокированные страницы после операции ввода/вывода, в частности, в процедуре Unload или обработчике запроса Shutdown, но это ему не удалось
Параметры | Описание
1 | 0
2 | Адрес процесса
3 | Число заблокированных страниц
4 | 0 или указатель на стек драйвера

0x77 | KERNEL_STACK_INPAGE_ERROR
Запрошенная страница системного адресного пространства, размещенная в страничном файле, не может быть считана в оперативную память
Параметры | Описание
1 | Код состояния или 0
2 | Код состояния ввода/вывода или значение, найденное в стеке, где должна размещаться сигнатура
3 | 0 или номер страничного файла
4 | Адрес сигнатуры в стеке или смещение в страничном файле

0x79 | MISMATCHED_HAL
Версии (revision level) программного кода ядра и программного кода HAL не согласуются. Это может происходить из-за неправильного использования ключей /kernel и /hal в файле BOOT.INI. (См. Microsoft Knowledge Base, Q103059.)
Параметры | Описание
1 | 1: Рассогласование редакций PRCB 2: Рассогласование версий сборки 3: Рассогласование Micro Channel
2 | 1: Редакция (release level) ntoskrnl.exe 2: Тип сборки (build type) ntoskrnl.exe 3: Тип компьютера, обнаруженный во время загрузки
3 | 1: Редакция hal.dll 2: Тип сборки hal.dll 3: Тип компьютера, поддерживаемый HAL
4 | Зарезервировано

0x7A | KERNEL_DATA_INPAGE_ERROR
Запрошенная страница системного адресного пространства, размещенная в страничном файле, не может быть считана в оперативную память
Параметры | Описание
1 | Тип блокировки (значения 1,2,3 или адрес записи в таблице страниц)
2 | Код статуса операции ввода/вывода (во время которой произошла ошибка)
3 | Если тип блокировки 1 или 2 — текущий процесс Если тип блокировки 3 — виртуальный адрес
4 | Виртуальный адрес, соответствующая которому страница не может быть считана в оперативную память

0x7B | INACCESSIBLE_BOOT_DEVICE
Операционная система Window NT потеряла доступ к системному разделу (system partition) во время запуска. Это может происходить из-за ошибок в драйвере, обслуживающем устройство, с которого должна осуществляться загрузка, из-за наличия программного обеспечения, несовместимого с NT, вирусов, неверного конфигурирования CD-ROM, ошибок в разбиении или незавершенного разбиения жесткого диска (partition table). (См. также Microsoft Knowledge Base Q103069, Q105026, Q115339, Q120744, Q124307, Q126423, Q131337, Q131712, Q136074, Q137860, Q153296, Q156168, Q161960.)
Параметры | Описание
1 | Адрес объекта устройства, которое не может быть смонтировано
2-4 | 0

0x7F | UNEXPECTED_KERNEL_MODE_TRAP
Только для платформы INTEL. Процессор сгенерировал системное прерывание (trap), которое не может быть обработано ядром
Параметры | Описание
1 | Код прерывания
2-4 | Зарезервировано

0x81 | SPIN_LOCK_INIT_FAILURE
Ошибка при инициализации спин-блокировки. Условия возникновения данной ошибки четко не определены, однако ее природа всегда — программная
Параметры | Описание
1-4 | Зарезервировано

0x9F | DRIVER_POWER_STATE_FAILURE
Драйвер находится в ошибочном состоянии с точки зрения участия в схеме энергопотребления
Параметры | Описание
1 | 1: Освобождаемый драйверный объект имеет незавершенный запрос по схеме энергопотребления, который ожидает обработки 2: Драйверный объект завершил обработку системного IRP запроса по энергопотреблению, но не выполнил вызов PoStartNextPowerIrp 3: Драйвер не оформил должным образом IRP запрос как ожидающий обработки или как обработанный 100: Объекты устройств в узле (devnode) несостоятельны в своем использовании DO_POWER_PAGABLE 101: Родительский объект устройства обнаружил, что дочерний объект устройства не установил бит DO_POWER_PAGABLE
2 | 1: Указатель на объект устройства 2: Указатель на целевой объект устройства 3: Указатель на целевой объект устройства 100: Указатель на объект устройства в нестраничной памяти 101: Дочерний объект устройства (FDO)
3 | 1: Зарезервировано 2: Указатель на объект устройства 3: Указатель на объект устройства 100: Указатель на целевой объект устройства 101: Дочерний объект устройства (FDO)
4 | 1: Зарезервировано 2: Зарезервировано 3: IRP пакет 100: Указатель на уведомляемый объект устройства 101: Родительский объект устройства

0xBE | ATTEMPTED_WRITE_TO_READONLY_MEMORY
Драйвер пытается выполнить запись в область памяти, предназначенную только для чтения
Параметры | Описание
1 | Виртуальный адрес, по которому производится попытка записи
2 | Содержимое записи в таблице страниц
3-4 | Зарезервировано

0xC1 | SPECIAL_POOL_DETECTED_MEMORY_CORRUPTION
Драйвер произвел запись в ошибочную секцию специального пула памяти
Параметры | Описание
Попытка освободить область памяти, которая не была ранее получена (выделена, allocated)
1 | Адрес, к которому предпринята попытка "освободить" его
2 | Зарезервировано
3 | 0
4 | 0x20
Попытка освободить область по ошибочному адресу
1 | Адрес, к которому предпринята попытка "освободить"
2 | Запрошенное число байт
3 | Рассчитанное число байт
4 | 0x21, 0x22
Попытка освободить область по адресу, в то время как близлежащие байты на странице испорчены
1 | Адрес, к которому предпринята попытка "освободить"
2 | Адрес, где размещается испорченная область
3 | Зарезервировано
4 | 0x23
Попытка освободить область по адресу, в то время как байты после данной выделенной области перезаписаны
1 | Адрес, к которому предпринята попытка "освободить"
2 | Адрес, где размещается испорченная область
3 | Зарезервировано
4 | 0x24
Попытка получить область в пуле памяти при ненадлежащем уровне IRQL
1 | Текущий уровень IRQL
2 | Тип пула памяти
3 | Число байт
4 | 0x30
Попытка освободить область в пуле памяти при ненадлежащем уровне IRQL
1 | Текущий уровень IRQL
2 | Тип пула памяти
3 | Адрес, к которому предпринята попытка "освободить"
4 | 0x31

0xC2 | BAD_POOL_CALLER
Текущий программный поток сделал ошибочный запрос к области памяти
Параметры | Описание
Заголовок пула памяти испорчен
1 | 0x01, 0x02 или 0x04
2 | Указатель на заголовок пула
3 | Первая часть содержимого заголовка пула
4 | 0
Попытка освободить область в пуле памяти, которая уже освобождена
1 | 0x06
2 | Зарезервировано
3 | Указатель на заголовок пула
4 | Содержимое заголовка пула
Попытка освободить область в пуле памяти, которая уже освобождена
1 | 0x07
2 | Зарезервировано
3 | Указатель на заголовок пула
4 | 0
Попытка получить область в пуле памяти при ненадлежащем уровне IRQL
1 | 0x08
2 | Текущий уровень IRQL
3 | Тип пула памяти
4 | Размер запрошенной области
Попытка освободить область в пуле памяти при ненадлежащем уровне IRQL
1 | 0x09
2 | Текущий уровень IRQL
3 | Тип пула памяти
4 | Адрес пула
Попытка освободить область в пуле памяти уровня ядра по указателю, который имеет вид адреса пользовательского режима
1 | 0x40
2 | Начальный адрес области
3 | Начало системного адресного пространства
4 | 0
Попытка освободить область в пуле нестраничной памяти, которая не была ранее получена (выделена, allocated)
1 | 0x41
2 | Начальный адрес области
3 | Страничный блок физической памяти
4 | Максимальный страничный блок физической памяти
Попытка освободить область в нуле страничной памяти, которая не была выделена
1 | 0x50
2 | Начальный адрес области
3 | Смещение (в страницах) от начала страничного пула
4 | Размер области страничного пула в байтах
Попытка освободить область в пуле памяти по ошибочному адресу либо с разрушенным заголовком
1 | 0x99
2 | Адрес, к которому предпринята попытка "освободить"
3 | 0
4 | 0

0xC5 | DRIVER_CORRUPTED_EXPOOL
Драйвер, возможно, разрушил пул в системном адресном пространстве
Параметры | Описание
1 | Указатель на область памяти, обращение к которой вызвало сбой
2 | Уровень IRQL в момент обращения
3 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
4 | Адрес инструкции, обратившейся к области памяти

0xC6 | DRIVER_CAUGHT_MODIFYING_FREED_POOL
Драйвер обращается к области пула памяти, которая освобождена
Параметры | Описание
1 | Указатель на область памяти, обращение к которой вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | 0: в режиме ядра 1: в пользовательском режиме
4 | 4

0xC7 | TIMER_OR_DPC_INVALID
Области памяти под объектами уровня ядра — таймером или DPC — были освобождены, хотя они еще находятся в очереди, ожидая активации
Параметры | Описание
1 | 0: объект таймера 1: объект DPC 2: DPC процедура
2 | Адрес объекта
3 | Начало области, обращение к которой вызвало ошибку
4 | Конец области, обращение к которой вызвало ошибку

0xCA | PNP_FATAL_ERROR
РnР Менеджер обнаружил критическую ошибку, вероятно, в результате ошибки в функционировании РnР драйвера
Параметры | Описание
Обнаружены двойники-PDO (объекты физического устройства) — отдельные фрагменты драйвера создали несколько PDO объектов с одинаковыми идентификатором устройства
1 | 0x01
2 | Адрес вновь созданного PDO объекта
3 | Адрес ранее созданного PDO объекта, который теперь "повторен"
4 | Зарезервировано
Ошибка в объекте физического устройства. Программный поток, которых запросил объект PDO, не выполнил инициализацию объектов PDO или FDO
1 | 0x02, 0x03
2 | Адрес подразумеваемого PDO объекта
3 | Зарезервировано
4 | Зарезервировано
Ошибка в идентификаторе (ID). Модуль ядра, который выполнял перечисление (enumeration) возвратил идентификатор, который содержит ошибочные символы или неверно завершен. Идентификаторы должны содержать только символы ASCII 0x20..0x2B и 0x2D..0x7F
1 | 0x04
2 | Адрес PDO объекта с установленным DO_DELETE_PENDING
3 | Адрес буфера, содержащего ID
4 | 1: DeviceID 2: UniqueID 3: Аппаратные идентификаторы 4: Совместимые идентификаторы
Объект PDO был освобожден (то есть Менеджер объектов уменьшил число ссылок на него до нуля), но сам объект еще участвует в дереве объектов устройств (devnode tree). Как правило, это означает, что драйвер не добавил ссылку (соответствующим системным вызовом) в тот момент, когда возвратил PDO объект в ответ на поступивший соответствующий IRP запрос
1 | 0x05
2 | Адрес PDO объекта
3 | Зарезервировано
4 | Зарезервировано

0xCB | DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS
Драйверу не удалось выполнить освобождение зафиксированных в оперативной памяти (locked) страниц после операции ввода/вывода
Параметры | Описание
1 | Адрес инициатора (в драйвере) фиксации данных страниц
2 | Инициатор вызова инициатора фиксации
3 | Указатель на MDL список, содержащий зафиксированные в оперативной памяти страницы
4 | Указатель на имя "виновного" драйвера (в формате Unicode)

0xCC | PAGE_FAULT_IN_FREED_SPECIAL_POOL
Система обратилась к ранее освобожденной области памяти
Параметры | Описание
1 | Адрес, обращение к которому, вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 | Зарезервировано

0xCD | PAGE_FAULT_BEYOND_END_OF_ALLOCATION
Система обратилась к памяти за пределами выделенной драйвером области (в некотором пуле)
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 | Зарезервировано

0xCE | DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATION
Драйвер не отменил незавершенные (pending) операции перед своей выгрузкой
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 | Зарезервировано

0xD0 | DRIVER_CORRUPTED_MMPOOL
Драйвер испортил системный пул памяти
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Уровень IRQL в момент обращения
3 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
4 | Адрес инструкции, из которой был осуществлен ошибочный доступ

0xD1 | DRIVER_IRQL_NOT_LESS_OR_EQUAL
Драйвер пытался получить доступ к страничной памяти при работе на высоком уровне IRQL
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Уровень IRQL в момент обращения
3 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
4 | Адрес инструкции, из которой был осуществлен ошибочный доступ

0xD3 | DRIVER_PORTION_MUST_BE_NONPAGED
Драйвер поступил некорректно, пометив сегмент своего кода или своих данных как размещаемый в страничной памяти
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Уровень IRQL в момент обращения
3 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
4 | Адрес инструкции, из которой был осуществлен ошибочный доступ

0xD4 | SYSTEM_SCAN_AT_RAISED_IRQL_CAUGHT_IMPROPER_DRIVER_UNLOAD
Драйвер не отменил незавершенные (pending) операции перед своей выгрузкой
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Уровень IRQL в момент обращения
3 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
4 | Адрес инструкции, из которой был осуществлен ошибочный доступ

0xD5 | DRIVER_PAGE_FAULT_IN_FREED_SPECIAL_POOL
Драйвер обратился к ранее освобожденной области памяти
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 | Зарезервировано

0xD6 | DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION
Драйвер обратился к памяти за пределами выделенного пула памяти
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
3 | Адрес инструкции, из которой был осуществлен ошибочный доступ (если он известен)
4 | Зарезервировано

0xD7 | DRIVER_UNMAPPING_INVALID_VIEW
Драйвер пытается получить физический адрес для виртуального, который не существует
Параметры | Описание
1 | Виртуальный адрес
2 | 0: Система не является терминальным сервером 1: Система является терминальным сервером
3 | 0
4 | 0

0xD8 | DRIVER_USED_EXCESSIVE_PTES
Не осталось свободных записей в системной таблице страниц виртуальной памяти
Параметры | Описание
1 | Указатель на имя "виновного" драйвера (Unicode) либо 0
2 | Число записей в системной таблице страниц, используемых "виновным" драйвером (если первый параметр не равен 0)
3 | Общее количество свободных записей в системной таблице страниц
4 | Общее количество записей в системной таблице страниц

0xDB | DRIVER_CORRUPTED_SYSPTES
Была сделана попытка обращения к памяти при ненадлежащем уровне IQRL, вероятно, из-за разрушения записей в системных страничных таблицах виртуальной памяти
Параметры | Описание
1 | Адрес, обращение по которому вызвало сбой
2 | Уровень IRQL в момент обращения
3 | Код доступа при возникновении ошибки 0 — чтение, 1 — запись
4 | Адрес инструкции, из которой был осуществлен ошибочный доступ

0xDC | DRIVER_INVALID_STACK_ACCESS
Драйвер пытался получить доступ к пространству стека, которое находится ниже указателя границы стека (stack pointer) текущего рабочего потока
Параметры | Описание
1-4 | Зарезервировано

0xDE | POOL_CORRUPTED_IN_FILE_AREA
Драйвер разрушил пул памяти, используемый для хранения страниц, предназначенных для работы с диском
Параметры | Описание
1-4 | Зарезервировано

0xE1 | WORKER_THREAD_RETURNED_AT_BAD_IRQL
Рабочий поток, созданный драйвером, завершился и вернул управление на IRQL уровне равном DISPATCH_LEVEL или выше
Параметры | Описание
1 | Адрес рабочей процедуры
2 | Уровень IRQL (должен быть 0)
3 | Параметры рабочей единицы
4 | Адрес рабочей единицы

0xE2 | MANUALLY_INITIATED_CRASH
Пользователь сознательно вызвал сбой из отладчика либо с клавиатуры
Параметры | Описание
1-4 | Зарезервировано

0xE3 | RESOURCE_NOT_OWNED
Программный поток пытается освободить ресурс, который ему не принадлежит
Параметры | Описание
1 | Адрес объекта ресурса
2 | Адрес объекта потока
3 | Адрес таблицы владельца (если существует)
4 | Зарезервировано

0xE4 | WORKER_INVALID
Запись рабочего процесса подмножества Executive была обнаружена в области памяти, которая не должна была содержать такую запись
Параметры | Описание
1 | Индикатор положения кода
2 | Адрес записи рабочего процесса
3 | Начало блока памяти
4 | Конец блока памяти


Приложение Б. Загрузка операционной системы

Приложение Б

Загрузка операционной системы

При загрузке 32-разрядных версий операционной системы Windows NT 5 (2000, XP или Server 2003) используются следующие файлы:

C:\NTLDR — стадии подготовки к загрузке и загрузка
C:\BOOT.INI — стадия загрузки ядра
C:\BOOTSECT.DOS — стадия загрузки ядра (опционально)
C:\NTDETECT.COM — стадия загрузки ядра
%systemroot%\System32\NTOSKRNL.EXE — стадия загрузки ядра (или NTKRNLPA.EXE)
%systemroot%\System32\HAL.DLL — стадия загрузки ядра
Раздел реестра HKLM\SYSTEM — стадия инициализации ядра
%systemroot%\System32\*.sys — стадия инициализации ядра

Здесь под %systemroot% подразумевается директория, где размещены основные файлы операционной системы, например, D:\Windows — для Windows XP, установленной на логический диск D. Для системы Windows 2000, например, установленной на логическом диске С, это будет директория C:\WINNT.

Подготовка к загрузке

  1. Компьютер тестирует себя (стадия POST, Power On Self Test), оперативную память, физические устройства. Если BIOS поддерживает спецификацию PnP, то происходит определение и настройка такого типа устройств.
  2. BIOS обнаруживает загрузочное устройство (жесткий диск, привод CD ROM), загружает и запускает на выполнение основную загрузочную запись (MBR).
  3. MBR просматривает таблицу разделов (partition table), чтобы найти активный, загружает загрузочный вектор активного раздела в память и запускает его на выполнение.
  4. Загружает и инициализирует файл NTLDR, который представляет собой загрузчик ОС.

Начальная стадия загрузки

Загрузчик NTLDR переключает процессор из реального режима в 32-разрядный защищенный, что необходимо для получения возможности выполнить любую дополнительную функцию. Файловая мини-система, встроенная в NTLDR позволяет загружать Windows NT 5 с носителей FAT, FAT32 и NTFS.

Стадия загрузки

Загрузчик NTLDR считывает файл BOOT.INI, использует его для отображения экрана загрузчика, предоставляет пользователю возможность выбора ОС, выбирает профиль оборудования и загружает ядро, но не инициализирует его. Файл BOOT.INI содержит 2 раздела: [Boot Loader] и [Operating System]

Распознавание оборудования

NTDETECT.COM запускается после надписи "Выберите операционную систему для загрузки", составляет список установленного на данный момент оборудования, и возвращает этот список в NTLDR для последующего включения его в раздел системного реестра HKLM\HARDWARE. NTDETECT.COM выполняет определение характеристик устройств:

  • Тип шины.
  • Последовательные порты.
  • Математический сопроцессор.
  • Гибкие диски.
  • Клавиатура и указывающие устройства.
  • Параллельные порты.
  • Адаптеры SCSI.
  • Видео адаптеры.

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

Выбор конфигурации

После того как NTLDR начинает загрузку и получит информацию об оборудовании, будет отображен экран с диалогом "Выбор профиля оборудования/Восстановление системы" (Hardware profile/Configuration recovery). B случае если профиль оборудования является единственным, то диалога представлено не будет.

Загрузка ядра

На этапе загрузки ядра NTLDR выполняет следующие действия:

  • Загружает код ядра из файла NTOSKRNL.EXE (NTKRNLPA.EXE при наличии опции /РАЕ в файле boot.ini), но не инициализирует его.
  • Загружает код слоя аппаратных абстракций из файла HAL.DLL.
  • Загружает раздел HKLM\SYSTEM из %systemroot%\System32\Config\System.
  • Выбирает набор параметров для конфигурации (список драйверов, устройств, устройств и служб, которые необходимо запустить).
  • Загружает драйверы (обычно это низкоуровневые драйвера, как, например, драйвера дисков) со значением параметра Start равным 0x0.

Значение параметра List в HKLM\SYSTEM\CurrentContorlSet\Control\ServiceGroupOrder определяет порядок загрузки их загрузчиком NTLDR. Драйверы, регулирующие свою загрузку таким способом, должны иметь соответствующие значения параметра Group в своих подразделах раздела Системного Реестра HKLM\System\CurrentControlSet\Services.

Инициализация ядра

По завершении загрузки, ядро инициализируется и ему передается управление от загрузчика NTLDR.

  1. Создается раздел HKLM\Hardware по результатам распознавания аппаратуры, куда заносится информация о системной плате, устройствах и прерываниях.
  2. Создается набор параметров Clone путем копирования управляющих параметров; информация о которых содержится в параметре Current в разделе HKLM\System\Select. Набор Clone никогда не модифицируется.
  3. Загружаются драйверы, указанные в разделе системного реестра HKLM\System\CurrentControlSet\Services, в параметрах которых присутствует значение Start равное 0x01 , порядок загрузки которых так же, как и было указано выше, определяется в параметре Group. Драйверы инициализируются сразу же после их загрузки. Значения параметра ErrorControl в описании драйвера (то есть в его параметре, указанном в Системном Реестре) определяет реакцию системы в том случае, если при загрузке и инициализации данного драйвера произошла ошибка. Подробнее возможные значения параметра ErrorControl и соответствующие способы реакции операционной системы представлены в Приложении В.
  4. Запускаются сервисы (например, Служба Журнала Событий) и драйверы.

Вывод на экран информации о процессе загрузки

Указав параметр /sos в соответствующей строке файла boot.ini, например,

multi(0)disk(0)rdisk(0)partition(2)\Windows="Комментарий для пользователя" /sos

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

multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\ntoskrnl.exe
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\hal.dll
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\KDCOM.DLL
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\BOOTVID.DLL
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\config\system
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\c_1251.nls
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\c_866.nls
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\l_intl.nls
multi(0)disk(0)rdisk(0)partition(1)\Windows\FONTS\vga866.fon
multi(0)disk(0)rdisk(0)partition(1)\Windows\AppPatch\drvmain.sdb
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\ACPI.SYS
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\WMILIB.SYS
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\pci.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\isapnp.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\viaide.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\PCIIDEX.SYS
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\MountMgr.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\ftdisk.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\dmload.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\dmio.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\PartMgr.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\VolSnap.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\atapi.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\disk.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\CLASSPNP.SYS
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\sr.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\Fastfat.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\KSecDD.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\NDIS.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\viaagp.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\nmfilter.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\Simvid.sys
multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\Mup.sys

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

В том случае, если в строке описания параметров загрузки после опции /sos ввести /PAE (поддержка расширенной физической адресации, PAE, см. главу 4), то первая строка примет вид: multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\ntkrnlpa.exe

Убедиться программным способом в том, поддерживает ли операционная система в настоящий момент работу с РАЕ (то есть с каким ключом она загружена), несложно. Достаточно в текст драйвера включить следующий фрагмент:
if(*Mm64bitPhysicalAddress==TRUE)
{
#if DBG
DbgPrint("System supports IO operations over 4GB");
#endif
}
else
{
#if DBG
DbgPrint("System doesn't support IO ioerations over 4GB");
#endif
}

Переменная Mm64BitPhysicalAddress объявляется в файлах wdm.h и ntddk.h.

Другое подтверждение работы операционной системы с поддержкой РАЕ (Physical Address Extension) можно обнаружить в Системном Реестре: в таком случае параметр PhysicalAddressExtension в разделе HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management примет значение 1 (в отличие от значения 0 в отсутствие поддержки РАЕ).


Приложение В. Некоторые стандартные параметры описания драйвера в Системном Реестре

[Previous]

Приложение В

Некоторые стандартные параметры описания драйвера в Системном Реестре

В Системном Реестре в разделе HKLM\System\CurrentControlSet\Services сосредоточены описания загружаемых драйверов, представленные набором параметров и/или вложенных подразделов.

После загрузки и запуска драйвера Example.sys, рассмотренного в главе 3, при помощи программы monitor, ранее описанной во 2 главе, можно наблюдать следующие изменения в Системном Реестре (рисунок B-1).

Программа monitor при загрузке драйвера создает в Системном Реестре соответствующий подраздел в HKLM\System\CurrentControlSet\Services с новым именем Example, в который помещает параметры DisplayName, ErrorControl, ImagePath, Start и Туре. Все перечисленные параметры достаточно типичны для описания драйверов в Системном Реестре, поэтому рассмотрим их подробнее.

Рис. В-1
Записи Системного Реестра для драйвера Example.sys

Параметр DisplayName

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

Параметр ErrorControl

Параметр ErrorControl (тип REG_DWORD) предписывает операционной системе способ поведения в той ситуации, когда при загрузке и инициализации драйвера (отработке процедуры DriverEntry) произошла ошибка.

Таблица B-1. Значения параметра ErrorControl

Значение | Символьное имя (см. wdm.h, ntddk.h, winnt.h) | Поведение операционной системы
0x0 | SERVICE_ERROR_IGNORE | В процессе загрузки ошибки игнорируются, загрузка продолжается без уведомления об ошибках в данном драйвере
0x1 | SERVICE_ERROR_NORMAL | В процессе загрузки ошибки игнорируются, но выводятся сообщения о них, при этом загрузка продолжается
0x2 | SERVICE_ERROR_SEVERE | Порядок загрузки нарушается и начинается заново с использованием набора параметров LastKnownGood, а если он уже используется, то ошибка игнорируется
0x3 | SERVICE_ERROR_CRITICAL | Порядок загрузки нарушается и начинается заново с использованием набора параметров LastKnownGood, а если он уже используется, то загрузка прерывается и выводится сообщение об ошибке

Параметр ImagePath

Значение этого параметра описывает полный путь к файлу, содержащему исполняемый код драйвера. В данном примере, загрузка драйвера была выполнена программой monitor из файла K:\Ex\Example.sys, что и было отражено в значении параметра ImagePath в данном подразделе Реестра. В случае, если данный параметр не указан, то используется значение %system%\Drivers\drivername.sys.

Параметр Start

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

Таблица B-2. Значения параметра Start

Значение | Символьное имя (см. wdm.h, ntddk.h, winnt.h) | Время загрузки драйвера
0x0 | SERVICE_BOOT_START | Драйвер запускается загрузчиком NTLDR
0x1 | SERVICE_SYSTEM_START | Драйвер запускается на стадии загрузки компонентов ядра ОС
0x2 | SERVICE_AUTO_START | Драйвер будет запущен средствами SCM после загрузки компонентов ядра ОС
0x3 | SERVICE_DEMAND_START | Драйвер запущен "вручную", то есть по запросу пользовательского приложения при помощи средств SCM (после загрузки ОС)
0x4 | SERVICE_DISABLED | Драйвер не может быть запущен

Параметр Туре

Значение этого параметра описывает тип драйвера, некоторые из значений приведены в таблице B-3.

Таблица B-3. Значения параметра Туре

Значение | Символьное имя (см. wdm.h, ntddk.h, winnt.h) | Тип драйвера
0x1 | SERVICE_KERNEL_DRIVER | Драйвер режима ядра
0x2 | SERVICE_FILE_SYSTEM_DRIVER | Драйвер файловой системы
0x4 | SERVICE_ADAPTER | Драйвер адаптера

Помимо описанных выше часто используемых параметров, используемых для описания драйверов в Системном Реестре, могут встречаться параметры DependOnGroup, DependOnService, Tag, Group, определяющие порядок загрузки, и параметр Parameters, официально предназначенный для хранения частных настроечных параметров.

Параметры подраздела \Enum

Подраздел HKLM\System\CurrentControlSet\Services\drivername\Enum (здесь drivername — имя драйвера) в Системном Реестре присутствует постоянно для драйверов, установленных при помощи Мастера Установки Оборудования. Для драйверов, загружаемых при помощи сервиса SCM, он появляется только после их удачного старта. В этом подразделе присутствуют параметры Count (число обслуживаемых устройств), NextInstance, параметры 0, 1 и т.п.

Рис. В-3
Подраздел \Enum после удачного старта драйвера Example.sys

Параметры 0, 1 и т.п. появляются только для удачно стартовавших драйверов, а их значения указывают на подраздел в HKLM\System\CurrentControlSet\Enum (где отражаются все когда-либо удачно стартовавшие драйвера). Если пойти по указанной в параметре 0 (или 1 и т.п.) ссылке, то можно увидеть в соответствующем подразделе HKLM\System\CurrentControlSet\Enum\...\Control параметр ActiveService, дающий обратную ссылку HKLM\System\CurrentControlSet\Services\drivername. Если одна из этих ссылок в Реестре не существует (прямая или обратная), то это признак того, что драйвер не запустился.