О KmdTut

01.08.2004

KmdTut - это сборник уроков о разработке драйверов режима ядра на ассемблере.

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

Скачать KmdTut можно с сайта http://www.wasm.ru.

Для плодотворной работы понадобится KmdKit. Раз вы читаете эти строки, то надо полагать, что он у вас уже есть.


О KmdKit

KmdKit - это комплект разработки драйверов режима ядра на ассемблере.

Этот пакет содержит все необходимое (по крайней мере на первых порах) для программирования драйверов режима ядра на языке ассемблера с использованием компилятора masm. В нем вы найдете включаемые (*.inc) файлы с определениями прототипов функций, необходимых структур, символьных констант и макросов; библиотечные (*.lib) файлы; примеры драйверов и кое-какие полезные инструменты.

Скачать KmdKit можно с сайта http://www.wasm.ru.


Основные понятия

1.1 Обзор архитектуры
1.1.1 Основные компоненты системы
1.1.2 Режим пользователя и режим ядра
1.2 Драйверы устройств
1.2.1 Типы драйверов устройств
1.2.2 Одно- и многоуровневые драйверы
1.3 Контекст потока
1.4 Уровни запросов прерываний
1.5 Крах системы
1.6 Driver Development Kit
1.7 Отладка драйверов
1.8 Что почитать

Здесь и далее рассматриваются только драйверы режима ядра Windows NT4/2000/XP/2003.

1.1 Обзор архитектуры

1.1.1 Основные компоненты системы

Внутренний мир Windows NT (здесь и далее под Windows NT имеется в виду вся линейка - NT4, 2000, XP, 2003) разделен на две части с четко обозначенными границами, как в плане адресного пространства, так и в плане прав и обязанностей кода в этом адресном пространстве выполняющегося.

С разделением адресного пространства все на удивление просто. Все четыре, доступных в 32-х разрядной архитектуре, гигабайта виртуальной памяти разделены на две равные части (4GT RAM Tuning и Physical Address Extension я опускаю как зкзотические). Нижняя половина отдана процессам пользовательского режима, верхняя принадлежит ядру.

С разделением прав и обязанностей немного сложнее.

К пользовательским относятся следующие процессы:

  • Процессы поддержки системы (System Support Processes) - например, процесс входа в систему Winlogon (реализован в \%SystemRoot%\System32\Winlogon.exe);

  • Процессы сервисов (Service Processes) - например, спулер печати (реализован в \%SystemRoot%\System32\spoolsv.exe);

  • Пользовательские приложения (User Applications) - бывают пяти типов: Win32, Windows 3.1, MS-DOS, POSIX и OS/2;

  • Подсистемы окружения (Environment Subsystems) - поддерживается три подсистемы окружения: Win32 (реализована в \%SystemRoot%\System32\Csrss.exe), POSIX (реализована в \%SystemRoot%\System32\Psxss.exe), OS/2 (реализована в \%SystemRoot%\System32\os2ss.exe).

Начиная с Windows XP поддержка POSIX и OS/2 прекращена.

Ядро состоит из следующих компонентов:

  • Исполнительная система (Executive) - управление памятью, процессами и потоками и др.;

  • Ядро (Kernel) - планирование потоков, диспетчеризация прерываний и исключений и др. (реализовано в \%SystemRoot%\System32\Ntoskrnl.exe);

  • Драйверы устройств (Device Drivers) - драйверы аппаратных устройств, сетевые драйверы, драйверы файловых систем;

  • Уровень абстрагирования от оборудования (Hardware Abstraction Layer, HAL) - изолирует три вышеперечисленных компонента от различий между аппаратными архитектурами (реализован в \%SystemRoot%\System32\Hal.dll);

  • Подсистема поддержки окон и графики (Windowing And Graphics System) - функции графического пользовательского интерфейса (Graphic User Interface, GUI) (реализована в \%SystemRoot%\System32\Win32k.sys).

Рис. 1-1. Упрощенная схема архитектуры Windows NT

1.1.2 Режим пользователя и режим ядра

Хотя процессоры семейства Intel x86 поддерживают четыре уровня привилегий (называемых кольцами защиты), в Windows используются только два: 0-ой для режима ядра и 3-ий для режима пользователя. Это связано с поддержкой других процессоров (alpha, mips), в которых реализовано только два уровня привилегий. Windows NT4.0 поддерживала эти архитектуры, но начиная с Windows 2000 осталась поддержка только x86.

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

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

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

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

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

1.2 Драйверы устройств

Как следует из самого названия, драйвер устройства (device driver) - это программа предназначенная для управления каким-то устройством, причем устройство это не обязательно должно быть физическим. Оно может быть и виртуальным. Именно драйверам для управления такими виртуальными устройствами и посвящено всё дальнейшее повествование. Однако знания почерпнутые из этого документа могут быть в полной мере применены и при написании драйвера для физического устройства.

1.2.1 Типы драйверов устройств

Windows NT поддерживает множество типов драйверов устройств.

Существует два базовых, которые имеют своих представителей:

  • Драйверы пользовательского режима (User-Mode Drivers):

    • Драйверы виртуальных устройств (Virtual Device Drivers, VDD) - используются для поддержки программ MS-DOS (не путать с VxD драйверами в Windows 95/98 - это совсем разные вещи, хотя и имеют одно название);

    • Драйверы принтеров (Printer Drivers).

  • Драйверы режима ядра (Kernel-Mode Drivers):

    • Драйверы файловых систем (File System Drivers) - реализуют ввод-вывод на локальные и сетевые диски;

    • Унаследованные драйверы (Legacy Drivers) - написаны для предыдущих версий Windows NT;

    • Драйверы видеоадаптеров (Video Drivers) - реализуют графические операции;

    • Драйверы потоковых устройств (Streaming Drivers) - реализуют ввод-вывод видео и звука

    • WDM-драйверы (Windows Driver Model, WDM) - поддерживают технологию Plag and Play и управления электропитанием. Их отличительной особенностью является совместимость на уровне исходного кода между Windows 98, Windows ME и Windows NT.

В разных источниках вы можете встретить классификацию немного отличную от приведенной выше, это не суть важно. Важно то, что драйверы, которые мы будем писать, не подпадают ни под один из пунктов этой классификации. Это ни драйверы файловой системы, ни унаследованные драйверы, ни драйверы видеоадаптеров или звуковых карт, ни WDM-драйверы, т.к. не поддерживают Plag'n'Play и управление электропитанием. Это не драйверы пользовательского режима (это вообще не интересно). На самом деле это просто черт знает что такое, т.к. система сама позволяет легко и просто добавить в саму себя код непонятно для какого устройства, и делать с ней все что угодно!

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

По своей структуре драйвер устройства является ни чем иным как файлом PE-формата (Portable Executable, PE). Таким же как обычные exe и dll. Только загружается и работает по другим правилам. Драйверы можно рассматривать как DLL режима ядра, предназначенные для выполнения задач не решаемых из пользовательского режима. Принципиальная разница здесь (не считая уровня привилегий) в том, что мы не сможем напрямую обращаться к драйверу, ни к его коду, ни к его данным, а будем пользоваться специальным механизмом предоставляемым диспетчером ввода-вывода (Input/Output Manager). Диспетчер ввода-вывода обеспечивает среду для функционирования драйверов, а также предоставляет механизмы для их загрузки, выгрузки и управления ими.

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

1.2.2 Одно- и многоуровневые драйверы

Большинство драйверов управляющих физическими устройствами являются многоуровневыми (layered drivers). Обработка запроса ввода-вывода разделяется между несколькими драйверами. Каждый выполняет свою часть работы. Например, запрос на чтение файла передается драйверу файловой системы, который, выполнив некоторые операции (например, разбиение запроса на несколько частей), передает его "ниже" - драйверу диска, а тот, в свою очередь, отправляет запрос драйверу шины. Кроме того между этими драйверами можно добавить любое количество драйверов-фильтров (например, шифрующих/дешифрующих данные). Выполнив запрос, нижестоящий драйвер (lower-level driver) передает его результаты "наверх" - вышестоящему (higher-level driver). Но, к счастью, у нас все будет значительно проще. Практически все драйверы, которые мы будем создавать, будут одноуровневыми (monolithic drivers), что сильно упростит весь процесс их написания и отладки. Возможно, одна или несколько статей будут посвящены драйверам фильтрам. В этом случае мы будем подключать свой драйвер в цепочку уже функционирующих драйверов, и отслеживать передачу им запросов ввода-вывода.

1.3 Контекст потока

Поскольку, в большинстве случаев, мы имеем всего один процессор, а приложений, которые нужно выполнять много, то естественно, что для создания иллюзии одновременного их выполнения надо последовательно подключать эти приложения к процессору, причем очень быстро. Эта процедура называется переключением контекста потока (thread context switching). Если система переключает контекст потоков принадлежащих одному процессу, то необходимо сохранить значение регистров процессора отключаемого потока, и загрузить, предварительно сохраненные значения регистров процессора подключаемого потока, и обновить кое-какие структуры данных. Если же подключаемый поток принадлежит другому процессу, то необходимо еще в регистр CR3 процессора загрузить указатель на каталог страниц (page directory) процесса. Так как каждому пользовательскому процессу предоставлено закрытое адресное пространство, то у разных процессов разные проекции адресных пространств, а значит, и разные каталоги страниц и наборы таблиц страниц по которым процессор транслирует виртуальные адреса в физические. Все это не имеет прямого отношения к программированию драйверов. Но я напоминаю об этом в связи вот с чем. Так как переключение контекста операция не самая быстрая, то драйверы, по соображениям лучшей производительности, как правило, не создают своих потоков. Но код драйвера все же нужно выполнять. Поэтому, для экономии времени на переключение контекстов, драйверы выполняются в режиме ядра в одном из трех контекстов:

  • в контексте пользовательского потока инициировавшего запрос ввода-вывода;

  • в контексте системного потока режима ядра (эти потоки принадлежат процессу System);

  • как результат прерывания (а значит, в контексте потока, который был текущим на момент прерывания).

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

1.4 Уровни запросов прерываний

Прерывание - неотъемлемая часть любой операционной системы. Прерывание требует обработки, поэтому выполнение текущего кода прекращается и управление передается обработчику прерывания. Существуют как аппаратные, так и программные прерывания. Прерывания обслуживаются в соответствии с их приоритетом. Windows NT использует схему приоритетов прерываний, известную под названием уровни запросов прерываний (interrupt request levels, IRQL). Всего существует 32 уровня, с 0 (passive), имеющего самый низкий приоритет, по 31 (high), имеющего самый высокий. Причем, прерывания с IRQL=0 по IRQL=2 (DPC\dispatch) являются программными, а прерывания с IRQL=3 (device 1) по IRQL=31 (high) являются аппаратными. Не путайте уровни приоритета прерываний с уровнями приоритетов потоков - это совсем разные вещи. Прерывание с уровнем IRQL=0, строго говоря, прерыванием не является, т.к. оно не может прервать работу никакого кода (ведь для этого этот код должен выполняться на еще более низком уровне прерывания, а такого уровня нет). На этом IRQL выполняются потоки пользовательского режима. И код наших драйверов тоже будет выполняться на этом IRQL. Это отнюдь не означает, что код любого драйвера всегда выполняется на уровне "passive". Просто мы не будем обрабатывать ни программные, ни тем более аппаратные прерывания. А отсюда следуют, по крайней мере, два очень важных вывода.

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

Второй важный момент: на уровне прерывания passive можно вызывать любые функции ядра (в DDK в описании каждой функции обязательно указано, на каком уровне прерывания ее можно вызывать), а также обращаться к страницам памяти сброшенным в файл подкачки. На более высоких уровнях прерывания (DPC/dispath и выше), попытка обращения к странице отсутствующей в физической памяти приводит к краху системы, т.к. диспетчер памяти (Memory Manager) не может обрабатывать ошибки страниц.

1.5 Крах системы

Думаю каждый, хотя бы один раз, видел волнующую картину под названием "голубой экран смерти" (Blue Screen Of Death, BSOD). Наверное, нет необходимости объяснять, что это такое и почему возникает. Важно то, что взявшись за разработку драйверов режима ядра, приготовьтесь к тому, что BSOD довольно часто будет появляться на экране вашего монитора.

В третьем кольце все было просто: набросал примерный код, расставил int3 где надо, запустил и... в отладчике уже разбираешься что к чему. Если что-то не так - прибил, исправил ошибки, перекомпилировал... и так далее, до тех пор пока код не заработает как надо. При программировании драйверов об этой технике можно забыть. Тут "сапер ошибается один раз". Одно неверное движение... и можно откинуться на спинку кресла и минутку расслабиться.

Для того чтобы видеть BSOD как можно реже следует придерживаться одного очень простого правила: "Семь раз отмерь - один отрежь"... в смысле "Семь раз проверь - один запусти". Это конечно просто сказать, но значительно труднее сделать. Но как правило, учитывая то, что структура драйверов, которые вы будете писать (после прочтения этих статей), относительно проста, можно разобраться с ошибками и до появления BSOD. Если же он упорно появляется перед вашими глазами, и вы никак не можете понять причину, возможным способом прояснить ситуацию является анализ аварийного дампа (crash dump). О том, что это такое, как его сделать и анализировать можно почитать в статье Марка Руссиновича "Анализ аварийных дампов памяти" http://www.osp.ru/win2000/2001/03/025.htm. Дело это (анализ) очень непростое, но надеюсь, что до этого не дойдет.

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

Теперь осветим кое-какие более практические вещи. А именно, что нам понадобится, чтобы теорию превратить в практику.

1.6 Driver Development Kit

Первое это конечно Комплект разработки драйверов (Driver Development Kit, http://www.microsoft.com/ddk/). К сожалению, Microsoft прекратила свободное распространение DDK, но при очень большом желании его все же можно найти. В этот пакет входит документация, которая является богатым, а иногда и единственным, источником информации о внутренних структурах данных и внутрисистемных функциях используемых драйверами устройств.

Помимо документации в DDK входит набор библиотечных файлов (*.lib), которые будут совершенно необходимы при компоновке. В DDK входит два комплекта этих файлов: для окончательной версии Windows (называемой свободным выпуском (free build)); и для отладочной (называемой проверочным выпуском (checked build)). Находятся эти файлы в каталогах %ddk%\libfre\i386 и %ddk%\libchk\i386 соответственно. Отладочная версия отличается более строгой проверкой ошибок. Использовать нужно файлы соответствующие вашей версии системы.

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

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

1.7 Отладка драйверов

Для отладки драйверов, естественно, потребуется отладчик, причем, поскольку отлаживать придется код режима ядра, то и отладчик нужен соответствующий. Самым лучшим выбором будет SoftICE. Либо можно воспользоваться отладчиками, входящими в комплект Debugging Tools от Microsoft. Правда, для отладки кода ядра с помощью этих отладчиков потребуется два компьютера, соединенные нуль-модемным кабелем. Отладчик Kernel Debugger, кстати, чрезвычайно полезен для исследования внутреннего устройства системы, при условии, что у вас установлены отладочные символы, которые можно скачать с сайта Microsoft. Марк Руссинович (Mark Russinovich, http://www.sysinternals.com/) написал утилиту LiveKd, которая позволяет использовать Kernel Debugger без подключения второго компьютера.

1.8 Что почитать

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

  • Дэвид Соломон, Марк Руссинович, "Внутреннее устройство Microsoft Windows 2000", изд. "Русская редакция", 2001.

    • Хотя в этой книге нет ни одной строчки исходного кода, она прежде всего для программистов драйверов.

  • Свен Шрайбер, "Недокументированные возможности Windows 2000", изд. "Питер", 2002.

    • Сугубо практическая книга, в которой раскрыто множество тайн Windows 2000.

  • Walter Oney, "Programming the Microsoft Windows Driver Model", Microsoft Press, 1999

  • Walter Oney, "Programming the Microsoft Windows Driver Model. 2nd edition", Microsoft Press, 2003

    • В этих книгах упор сделан на Plag'n'Play драйверы, но это нисколько не умоляет их достоинств, т.к. базовые принципы разработки драйверов универсальны.

  • Art Baker и Jerry Lozano, "The Windows 2000 Device Driver Book, A Guide for Programmers, Second Edition", Prentice Hall, 2000

    • Достойная книжка на книжной полке драйверописателя.

  • Rajeev Nagar, "Windows NT File System Internals. A Developer's Guide", O'Reilly.

    • Эта книга, как следует из самого названия, посвещена разработке драйверов файловых систем.

  • Prasad Dabak, Sandeep Phadke, and Milind Borate, "Undocumented Windows NT", M&T Books, 1999

    • Очень актуальная книжка, не смотря на год издания.

  • Гэри Неббет, "Справочник по базовым функциям API Windows NT/2000", Издательский дом "Вильямс", 2002

    • Если вы не собираетесь ограничивать себя использованием только документированных функций, вам без этой книги не обойтись. Подробно описаны все Zw* функции и используемые ими структуры данных.

    В сети можно найти электронную версию этой книги: Gary Nebbett, "Windows NT-2000 Native API Reference".

  • Джеффри Рихтер, "Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows", изд. "Русская редакция", 2000

    • Эта книга не имеет никакого непосредственного отношения к программированию драйверов, но тоже очень интересная ;-)

Этот список ни в коем случае не претендует на полноту. Многое, особенно по-английски, можно найти в Internet (кроме книги Свена Шрайбера, всё есть в электронном варианте). Все эти книги из разряда "must have". Увидите - покупайте/скачивайте не глядя/думая. Некоторые переведены на наш "великий и могучий".

Copyright © 2002-2004 Four-F, four-f@mail.ru


Службы

2.1 Службы
2.2 Диспетчер управления службами
2.3 Программа управления службой
2.3.1 Установка канала связи с SCM
2.3.2 Регистрация драйвера
2.3.3 Запуск драйвера
2.3.4 Приводим систему в исходное состояние
2.4 Макросы для определения строк
Исходный текст: KmdKit\examples\simple\Beeper

"Ну, вот! Начали за здравие, кончили за упокой. При чем тут службы?" - спросите вы... и будете неправы. Очень даже при чем.

Я, было, начал эту статью с описания простейшего драйвера, но по ходу дела, был вынужден отвлекаться на то, чтобы объяснять, как его зарегистрировать, запустить и т.д. и т.п. Тогда я решил, что будет логичнее, сначала поведать о том, как драйверы регистрируются, запускаются… Но, тут возникла похожая проблема. Мне пришлось говорить о том, к каким действиям, еще не написанного драйвера, приведет тот или иной вызов диспетчера управления службами или диспетчера ввода-вывода. Тут уж ничего не поделаешь. Слишком тесно эти компоненты связаны друг с другом. Как бы там ни было, но я остановился на втором варианте: сначала я глаголю о диспетчере управления службами, потом о простейшем драйвере, затем о диспетчере ввода-вывода, и наконец, разберем полнофункциональный драйвер. После прочтения последней части будет неплохо вернуться к предыдущим - тогда многое встанет на свои места. Так что, запаситесь терпением.

Поскольку у меня нет ни малейшего желания выступать в качестве переводчика официальной документации Microsoft, по крайней мере безвозмездно, то информацию о функциях, которыми мы будем пользоваться, принимаемых ими параметрах и их значениях, я буду давать лишь в объеме, необходимом для реализации наших целей. За подробностями обращайтесь к MSDN, API Reference и DDK.

2.1 Службы

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

Кстати сказать, это понятие часто переводят и как "сервис" (если это можно назвать переводом), но я буду употреблять именно термин "служба".

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

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

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

  • Диспетчер управления службами (Service Control Manager, SCM). Именно благодаря ему мы будем иметь возможность легко и просто загружать драйверы;

  • Программа управления службой (Service Control Program, SCP). Работает в тесной связке с SCM;

  • Собственно сам драйвер.

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

2.2 Диспетчер управления службами

На конечном этапе загрузки системы, перед появлением диалога регистрации пользователя, запускается SCM (\%SystemRoot%\System32\Services.exe), который, просматривая раздел реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\, создает свою внутреннюю базу данных (ServicesActive database или SCM database). Далее SCM находит в созданной базе все драйверы устройств и службы, помеченные для автоматического запуска, и загружает их.

Чтобы получить кое-какое представление об этом, запустите редактор реестра (\%SystemRoot%\regedit.exe), откройте раздел HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ и изучите его содержимое.

Теперь запустите оснастку Администрирование > Службы (Administrative Tools > Services). вы увидите список установленных служб (именно служб, а не драйверов).

Чтобы просмотреть список загруженных драйверов, запустите Администрирование > Управление компьютером (Administrative Tools > Computer Management) и в левом окне откройте ветвь Служебные программы > Сведения о системе > Программная среда > Драйверы (System Tools > System Information > Software Environment > Drivers).

Проанализировав содержимое этих трех окон, вы заметите, что они во многом совпадают.

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

Рассмотрим минимально возможный набор параметров, необходимых для запуска драйвера. Более подробно можно почитать: DDK > Setup, Plug Play, Power Management > Design Guide > Reference > Part3: Setup > 1.0 INF File Sections and Directives > INF AddService Directive. В качестве примера, возьмем простейший драйвер режима ядра beeper (о нем самом мы поговорим в следующий раз). Подраздел реестра соответствующий этому драйверу и его содержимое представлен на рис 2-1.

Рис. 2-1. Подраздел реестра драйвера beeper

Параметр Описание

DisplayName

- Так называемое экранное имя, отображаемое служебными приложениями на экране.

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

ErrorControl

- Определяет, каким образом реагировать на ошибки.

Для нас могут представлять интерес два значения:

SERVICE_ERROR_IGNORE (0)

- возвращаемый драйвером код ошибки игнорируется. Предупреждения не выводятся и не регистрируются;

SERVICE_ERROR_NORMAL (1)

- если драйвер, в ответ на команду запуска SCM, сообщает об ошибке, выводится предупреждение (которое я, кстати, так ни разу и не увидел) и ошибка регистрируется в журнале событий системы (system event log) с указанием причины сбоя.

Журнал событий системы можно просмотреть стандартными средствами Windows:

Администрирование > Просмотр событий (Administrative Tools > Event Viewer).

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

ImagePath

- Путь к исполняемому файлу драйвера.

В отличие от служб, для драйверов не обязательно указывать значение этого параметра, но тогда файл драйвера должен находиться в каталоге \%SystemRoot%\System32\Drivers. Если значение следующего параметра равно SERVICE_BOOT_START или SERVICE_SYSTEM_START, то файл драйвера также должен находиться в каталоге \%SystemRoot%\System32\Drivers, т.к. на ранних этапах загрузки системы обращение к другим каталогам невозможно.

Start

- Указывает как нужно запускать драйвер.

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

SERVICE_AUTO_START (2)

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

SERVICE_DEMAND_START (3)

- драйвер запускается по требованию. Т.е. в любой момент после полной загрузки системы.

Все драйверы устройств, у которых параметр Start равен SERVICE_AUTO_START (2), автоматически запускаются диспетчером управления службами при загрузке системы. Это автоматически запускаемые службы (auto-start services). При этом запускаются и все драйверы устройств, от которых данный драйвер зависит, даже если параметр Start у них не указывает на автоматическую загрузку. (Для определения порядка своей загрузки драйверы устройств могут использовать параметры Group, Tag и DependOnGroup, а сервисы Group и DependOnService, но нам они не понадобятся.)

Есть и другие значения, указывающие на автоматический запуск, например, SERVICE_BOOT_START (0). Это значение могут использовать только драйверы. В этом случае драйвер будет загружен загрузчиком операционной системы, еще до того, как будет запущен SCM, и даже диспетчер ввода-вывода (I/O Manager). Но это нам тоже пока не интересно.

Автоматический запуск драйвера иногда называют статической загрузкой, по аналогии со статически загружаемыми VxD в Windows 9x. Это не правильно, т.к. автоматически загруженный драйвер можно выгрузить и удалить из базы данных SCM в любой момент.

Драйверы, которые имеют параметр Start равным SERVICE_DEMAND_START (3), попадают в базу данных SCM, и считаются зарегистрированными (installed services). Это службы запускаемые по требованию (demand-start services). Их можно запустить в любой момент, вызвав функцию ServiceStart.

Type

- Определяет тип службы.

Нас интересует только значение SERVICE_KERNEL_DRIVER (1).

В подразделе Security хранится контекст безопасности выполнения службы. По умолчанию контекст безопасности соответствует LocalSystem. Содержимое подраздела Enum используется при перечислении драйверов и устройств. Эти подразделы создаются автоматически, и нам они не интересны.

Таким образом, из рис 2-1 можно извлечь следующую информацию: драйвер режима ядра beeper, находящийся в каталоге C:\masm32\Ring0\Kmd\Article2\beeper и имеющий экранное имя "Nice Melody Beeper", запускается по требованию, возможные ошибки игнорируются и не заносятся в журнал событий системы.

Что такое \??, в начале пути к файлу драйвера, я расскажу в следующих частях.

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

2.3 Программа управления службой

Как следует из самого названия, программа управления службой (далее SCP) призвана выполнять некие действия по отношению к драйверу. Делает она это под наблюдением SCM, вызывая соответствующие функции. Все они экспортируются модулем \%SystemRoot%\System32\advapi.dll (Advanced API).

Вот код простейшей SCP (содержится в файле scp.asm), которая будет управлять драйвером beeper.

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  Программа управления драйвером beeper
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

start proc

local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("beeper.sys"), sizeof acDriverPath, addr acDriverPath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax
            invoke StartService, hService, 0, NULL
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
                            NULL, MB_ICONSTOP
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

2.3.1 Установка канала связи с SCM

Первое, что нам необходимо сделать - это, пользуясь терминологией Microsoft, установить канал связи с SCM, используя функцию OpenSCManager, прототип которой выглядит так:

OpenSCManager proto lpMachineName:LPSTR, lpDatabaseName:LPSTR, dwDesiredAccess:DWORD
Параметр Описание

lpMachineName

- указатель на завершающуюся нулем строку, содержащую имя компьютера.

Этот параметр мы сразу устанавливаем в NULL, т. к. будем открывать канал связи с SCM только на локальном компьютере.

lpDatabaseName

- указатель на завершающуюся нулем строку с именем открываемой базы данных.

Может быть либо SERVICES_ACTIVE_DATABASE, либо NULL, что то же самое, т. к. в этом случае будет открыта база SERVICES_ACTIVE_DATABASE.

Символьная константа SERVICES_ACTIVE_DATABASE не определена в файле masm32\include\windows.inc. Это можно сделать так:

.const
szActiveDatabase db "ServicesActive", 0
SERVICES_ACTIVE_DATABASE equ offset szActiveDatabase

Т.к. мы не собираемся открывать никакую другую базу данных SCM, кроме активной в данный момент, просто, установим этот параметр в NULL.

dwDesiredAccess

- запрашиваемые права доступа.

Сообщает SCM, что мы собственно намереваемся делать с его базой данных.

Нам могут быть полезны три значения:

SC_MANAGER_CONNECT

- доступ на установку канала связи с SCM.

По умолчанию (т.е. если просто передать в этом параметре 0), устанавливается именно это значение. Очень странно, но в документации ничего не говорится, что же конкретно мы можем делать с этим уровнем доступа. А делать можно многое: запускать драйвер, останавливать, и что совсем не понятно, даже удалять сведения о нем из базы данных SCM и из реестра;

SC_MANAGER_CREATE_SERVICE

- доступ на занесение в базу данных SCM нового драйвера.

Т. к. мы собираемся занести туда своего представителя, то именно это значение и используем. Можно подумать, что никаких других прав, кроме регистрации нового драйвера, этот флаг не дает. Это не так. Т.к. флаг SC_MANAGER_CONNECT считается установленным по умолчанию, то и соответствующие права нам тоже предоставляются. Что тоже совсем не очевидно;

SC_MANAGER_ALL_ACCESS

- позволяет получить максимальный доступ.

Мы устанавливаем канал связи с SCM таким образом:

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

Если канал связи с SCM успешно установлен, функция OpenSCManager вернет описатель (handle), предоставляющий доступ к активной базе данных SCM, который мы сохраняем в переменной hSCManager для дальнейшего использования.

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

2.3.2 Регистрация драйвера

Получив доступ к базе SCM, мы регистрируем в ней свой драйвер beeper, с помощью функции CreateService. Вот прототип этой функции.

CreateService proto hSCManager:HANDLE, lpServiceName:LPSTR, lpDisplayName:LPSTR, \
                    dwDesiredAccess:DWORD, dwServiceType:DWORD, dwStartType:DWORD, \
                    dwErrorControl:DWORD, lpBinaryPathName:LPSTR, lpLoadOrderGroup:LPSTR, \
                    lpdwTagId:LPDWORD, lpDependencies:LPSTR, lpServiceStartName:LPSTR, \
                    lpPassword:LPSTR

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

Параметр Описание

hSCManager

- описатель базы данных SCM.

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

lpServiceName

- указатель на завершающуюся нулем строку, содержащую внутреннее имя.

Максимальная длина строки 256 символов.Не допускаются символы "/" и "\". Соответствует имени подраздела в реестре.

lpDisplayName

- указатель на завершающуюся нулем строку, содержащую экранное имя.

Максимальная длина строки 256 символов. Соответствует параметру DisplayName в реестре.

dwDesiredAccess

- тип доступа.

Определяет, какие права по манипулированию вновь созданным сервисом нам будут предоставлены.

Для нас могут оказаться полезными следующие значения:

SERVICE_ALL_ACCESS

- позволяет получить максимальный доступ;

SERVICE_START

- доступ на запуск драйвера вызовом функции StartService;

SERVICE_STOP

- доступ на останов драйвера вызовом функции ControlService с параметром SERVICE_CONTROL_STOP;

DELETE

- доступ на удаление сведений о драйвере из базы данных SCM вызовом функции DeleteService.

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

dwServiceType

- тип службы (SERVICE_KERNEL_DRIVER).

Соответствует параметру Type в реестре.

dwStartType

- тип запуска (SERVICE_DEMAND_START).

Соответствует параметру Start в реестре.

dwErrorControl

- степень контроля ошибок (SERVICE_ERROR_IGNORE).

Соответствует параметру ErrorControl в реестре.

lpBinaryPathName

- указатель на завершающуюся нулем строку, содержащую полный путь к исполняемому файлу драйвера.

Соответствует параметру ImagePath в реестре.

lpLoadOrderGroup

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

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

lpdwTagId

- указатель на переменную, в которую будет записано уникальное значение тэга, которое идентифицирует группу, указанную в параметре lpLoadOrderGroup.

Можно указать в этом параметре NULL. Что мы и делаем.

lpDependencies

- указатель на завершающийся двумя нулями массив разделённых нулями имён драйверов или групп драйверов, которые система должна запустить до запуска этого драйвера.

Если драйвер не зависит от других драйверов, то в этом параметре можно указать NULL или указатель на пустую строку. Ставим в NULL.

lpServiceStartName

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

Если тип службы SERVICE_KERNEL_DRIVER, то этот параметр должен содержать имя объекта драйвера, которое используется системой для загрузки. Если используется имя объекта "драйвер" созданное подсистемой ввода-вывода, то этот параметр устанавливается равным NULL. Так и сделаем. А что такое имя объекта "драйвер", нам еще предстоит узнать.

lpPassword

- указатель на завершающуюся нулем строку, которая содержит пароль к аккаунту, указанному в параметре lpServiceStartName.

Если тип службы SERVICE_KERNEL_DRIVER, то этот параметр игнорируется. Ну и хорошо - нам меньше проблем. Раз игнорируется, то тоже передаем NULL.

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

Последние пять параметров мы сразу устанавливаем в NULL, и напрочь забываем об их существовании. Первым параметром является описатель базы данных SCM, полученный на предыдущем этапе. С четвертым параметром dwDesiredAccess, тоже все просто. Для чего нужны остальные параметры, я думаю, вы уже догадались... Правильно. Они соответствуют параметрам в подразделе реестра, которые мы разобрали выше. Для наглядности я свел их в таблицу.

CreateService | Реестр
lpServiceName | Имя подраздела
lpDisplayName | DisplayName
dwServiceType | Type
dwStartType | Start
dwErrorControl | ErrorControl
lpBinaryPathName | ImagePath

Таблица 2-1. Соответствие некоторых параметров функции CreateService, ключам реестра.

Как видите, все не так уж сложно. Вернемся к исходному коду.

        push eax
        invoke GetFullPathName, $CTA0("beeper.sys"), sizeof acDriverPath, addr acDriverPath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax

О том, что такое $CTA0 и что находится в файле Strings.mac поговорим позже.

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

CreateService регистрирует в базе данных SCM новый драйвер, и заполняет соответствующий подраздел реестра. Посмотрев еще раз на рис. 2-1, вы увидите там результаты работы CreateService. Если вы закомментарите вызов функции DeleteService, перекомпилируете csp.asm и запустите, то можно даже будет посмотреть на этот раздел реестра вживую, на вашей машине.

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

Если драйвер, который мы пытаемся зарегистрировать, уже существует, то вызов CreateService, естественно, завершится неудачей. Последующий вызов функции GetLastError вернет ERROR_SERVICE_EXISTS. Если же все ОК, то мы получим описатель вновь созданной службы, и поместим его в переменную hService. Он понадобится для дальнейших манипуляций.

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

Первой такой манипуляцией будет запуск драйвера функцией StartService, прототип которой выглядит следующим образом:

StartService proto hService:HANDLE, dwNumServiceArgs:DWORD, lpServiceArgVectors:LPSTR

Параметр | Описание
hService | - описатель службы. Этот описатель также можно получить с помощью функции OpenService.
dwNumServiceArgs | - количество строк-аргументов в массиве lpServiceArgVectors. Если lpServiceArgVectors равен NULL, то этот параметр может быть нулевым. Для драйверов это всегда так. И именно так у нас и будет.
lpServiceArgVectors | - указатель на массив указателей на завершающиеся нулём строки, которые будут переданы службе как аргументы. Драйверы не получают этих аргументов, поэтому мы устанавливаем этот параметр в NULL.

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

            invoke StartService, hService, 0, NULL

Функция StartService заставляет систему произвести действия, очень сильно напоминающие загрузку обыкновенной DLL. Образ файла драйвера проецируется на системное адресное пространство. При этом, возможности управлять адресом загрузки нет никакой. Да это и не нужно. Предопределенный адрес загрузки (preferred base address) у всех наших драйверов будет равен 10000h, что значительно ниже начала системного диапазона адресов. Пытаться установить его в какое-то другое значение не имеет смысла, т.к. система, все равно, будет загружать драйвер по случайному (для нас) адресу. Поскольку фактический адрес загрузки не совпадает с предопределенным, система производит настройку адресов пользуясь таблицей перемещений (relocation table), находящейся в секции .reloc файла драйвера. Затем производится связывание (fix-up) импорта. Кстати, импорт в файле драйвера раскинут в секции INIT и .idata. В .idata находится таблица адресов импорта (import address table, IAT). В ней содержатся адреса функций во внешних модулях. Она нужна драйверу постоянно. А в секции INIT содержится остальная часть импорта, необходимая только на этапе загрузки (имена внешних модулей и имена импортируемых функций), после которой, память занимаемая этой секцией освобождается. Когда образ драйвера подготовлен, управление передается на точку входа (entry point), которая находится в процедуре DriverEntry. Принципиальная разница, не считая уровня привилегий, в том, что код процедуры DriverEntry всегда выполняется одним из потоков процесса System, и, естественно, в адресном контексте этого процесса.

Вызов StartService синхронный. Это значит, что она не вернет управление до тех пор, пока не отработает процедура DriverEntry в драйвере. Если инициализация драйвера прошла успешно, DriverEntry должна вернуть STATUS_SUCCESS, а функция StartService вернет значение отличное от нуля. И мы вновь окажемся в контексте потока вызвавшего StartService, т.е. в контексте нашей SCP.

Вызов StartService может завершиться неудачей, если база данных SCM заблокирована. Последующий вызов функции GetLastError вернет ERROR_SERVICE_DATABASE_LOCKED. Как написано в документации, в этом случае, следует подождать несколько секунд, и повторить попытку, но мы этого делать не будем, т.к. такая ситуация крайне маловероятна. И вообще, в данном случае, нас не интересует возвращаемое функцией StartService значение, т.к. драйвер beeper уже проиграл свою дивную мелодию и вернул код ошибки. Т.е. мы заранее знаем, что вызов StartService даст ошибку.

2.3.4 Приводим систему в исходное состояние

Осталось привести систему в исходное состояние. Надеюсь, что впечатление от столь виртуозного исполнения на системном динамике, останется с вами навсегда ;-)

            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager

Вызовом функции DeleteService мы удаляем сведения о драйвере из базы данных SCM и из реестра. Странно, но передавать описатель самой базы данных SCM в функцию DeleteService не нужно. Прототип функции DeleteService прост:

DeleteService proto hService:HANDLE

Параметр | Описание
hService | - описатель службы, подлежащей удалению. Естественно нужно иметь право доступа на удаление. Мы его имеем.

На самом деле, функция DeleteService ничего ниоткуда не удаляет. Она только сообщает системе, что это можно сделать, когда наступит благоприятный момент. А он наступит тогда, когда все описатели службы будут закрыты. Т.к. мы все еще держим описатель hService открытым, то удаления не происходит. Если попытаться вызвать DeleteService повторно, то он завершится неудачей, а последующий вызов функции GetLastError вернет ERROR_SERVICE_MARKED_FOR_DELETE.

Вызовом функции CloseServiceHandle мы закрываем описатель hService. Прототип функции CloseServiceHandle также тривиален:

CloseServiceHandle proto hSCObject:HANDLE

Параметр | Описание
hSCObject | - описатель SCM или службы, подлежащий закрытию.

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

2.4 Макросы для определения строк

Теперь разберемся, что такое $CTA0. Это макро-функция, позволяющая определять строки. Masm обладает мощным препроцессором, возможностями которого, грех не воспользоваться. Этот макрос не единственный. В файле Strings.mac (\Macros\Strings.mac) находится целая коллекция подобных макросов, на все случаи жизни. Ну..., почти на все. Поскольку это не имеет непосредственного отношения к драйверам, я не буду особо растекаться мыслью по древу. В самом начале Strings.mac находятся достаточно подробные инструкции, как пользоваться макросами, правда, на английском языке. Если вы с ним не знакомы, то большое количество примеров позволит вам ухватить суть.

Я начал создавать эти макросы довольно давно. Прототипом послужили идеи Свена Шрайбера (когда то давно, и он кодил на асме, но потом пересел на с). Постепенно мои макросы обрастали разного рода усовершенствованиями, пока не приобрели достаточно законченный вид. Приглядитесь к ним. Они действительно очень удобны. И чтобы доказать это, я буду ими усиленно пользоваться.


Простейшие драйверы

3.1 Компиляция и компоновка драйвера
3.2 Самый простой драйвер режима ядра
3.2.1 Исходный текст драйвера simplest
3.2.2 Процедура инициализации драйвера
3.3 Драйвер режима ядра beeper
3.3.1 Исходный текст драйвера beeper
3.3.2 Управление системным таймером
3.3.3 Автоматическая загрузка драйвера
3.4 Программа управления драйвером giveio
3.4.1 Исходный текст программы управления драйвером giveio
3.4.2 Использование реестра для передачи параметров драйверу
3.4.3 Обращение к памяти CMOS
3.5 Драйвер режима ядра giveio
3.5.1 Исходный текст драйвера giveio
3.5.2 Карта разрешения ввода-вывода
3.5.3 Чтение данных из реестра
3.5.4 Откраваем коду режима пользователя доступ к портам ввода-вывода
3.6 Пара слов об отладке
Исходный текст: KmdKit\examples\simple\Beeper
Исходный текст: KmdKit\examples\simple\DateTime

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

3.1 Компиляция и компоновка драйвера

Все исходные тексты драйверов я буду оформлять в виде *.bat файла, который, на самом деле, является комбинацией *.bat и *.asm файлов, но имеет расширение .bat.

;@echo off
;goto make

.386                      ; начало исходного текста драйвера

  ;:::::::::::::::::::::::::
  ; остальной код драйвера ;
  ;:::::::::::::::::::::::::

end DriverEntry            ; конец исходного текста драйвера

:make

set drv=drvname

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

Если такой "самокомпилирующийся" файл запустить, то произойдет следущее. Первые две команды закомментарены, поэтому, они игнорируются компилятором masm, но принимаются командным процессором, который, в свою очередь, игнорирует символ "точка с запятой". Управление передается на метку :make, за которой находятся инструкции для компилятора и компоновщика. Все, что находится за директивой ассемблера end, игнорируется компилятором masm. Таким образом, весь текст между командой goto make и меткой :make, игнорируется командным процессором, но принимается компилятором masm. А все, что вне (включая команду goto make и метку :make), игнорируется компилятором masm, но принимается командным процессором.

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

set drv=drvname

Определяем переменную окружения которую можно будет подставить в необходимых местах.

Ключ | Описание
/driver | - Указывает компоновщику, что нужно сформировать файл драйвера; Этот ключ заставляет компоновщик разместить часть таблицы импорта в новой секции PE-файла с именем "INIT". Туда попадают имена импортируемых драйвером функций и их модулей, нужные только на этопе загрузки драйвера. После того как драйвер окажется в памяти эта секция может быть выгружена системой за ненадобностью.
/base:0x10000 | - Устанавливает предопределенный адрес загрузки образа драйвера равным 10000h. Я уже говорил про это в предыдущей статье;
/align:32 | - Память режима ядра - драгоценный ресурс. Поэтому, файлы драйверов имеют более "мелкое" выравнивание секций;
/out:%dvr%.sys | - По умолчанию компоновщик производит файлы с расширением .exe. При наличии ключа /dll файл будет иметь расширение .dll. Нам нужно получить файл с расшрением .sys;
/subsystem:native | - В PE-заголовке имеется поле, указывающее загрузчику образа исполняемого файла, для какой подсистемы этот файл предназначен: Win32, POSIX или OS/2. Это нужно для того, чтобы поместить образ в необходимое ему окружение. Подсистема Win32 автоматически запускается при загрузке системы. Если же запускается файл, предназначенный для функционирования, например, в подсистеме POSIX, то сначала операционная система запускает саму подсистему POSIX. Таким образом, с помощью этого ключа можно указать компоновщику, какая подсистема необходима. Когда мы компилируем *.exe или *.dll, то указываем под этим ключем значение windows, которое означает, что файлу требуется подсистема Win32. Драйверу вообще не нужна ни одна из подсистем, т.к. он работает в естественной (native) для самой операционной системы среде.

3.2 Самый простой драйвер режима ядра

3.2.1 Исходный текст драйвера simplest

Вот исходный текст простейшего драйвера режима ядра.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; simplest - Самый простой драйвер режима ядра
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                  С Б О Р К А    Д Р А Й В Е Р А
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:make

set drv=simplest

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

3.2.2 Процедура инициализации драйвера

Как и у любого другого выполнимого модуля, у драйвера должна быть точка входа, на которую система передаст управление после загрузки драйвера в память. Как и полагается, в программе на ассемблере точкой входа является первая инструкция, обозначенная меткой указанной в директиве end. У нас, как и в текстах на с, это DriverEntry, которая оформлена в виде процедуры, принимающей два параметра. Имя процедуры, естественно, может быть любым. Прототип DriverEntry выглядит так:

DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

К сожалению, Microsoft отошла от принципа "венгерской нотации" при составлении заголовочных файлов и документации DDK. Возможно, это связано с большим количеством специфических типов данных, используемых в DDK. Хотя в обозначении типов кое-что осталось. В исходных текстах я буду придерживаться этого принципа везде, где только возможно, т.к. настолько привык им пользоваться, что исходники не использующие "венгерскую нотацию" мне кажутся совершенно нечитабельными. Поэтому, легким движением руки, DriverObject превращается в pDriverObject, а RegistryPath в pusRegistryPath.

Типы данных PDRIVER_OBJECT и PUNICODE_STRING определены в файлах \include\w2k\ntddk.inc и \include\w2k\ntdef.inc соответственно.

PDRIVER_OBJECT  typedef PTR DRIVER_OBJECT
PUNICODE_STRING typedef PTR UNICODE_STRING
Параметр Описание

pDriverObject

- указатель на объект только что созданного драйвера.

Windows является объектно-ориентированной системой. Поэтому, понятие объект распространяется на все, что только можно, и что нельзя тоже. И драйверы не являются исключением. Загружая драйвер, система создает объект "драйвер" (driver object), представляющий для нее образ драйвера в памяти. Через этот объект система управляет драйвером. Звучит красиво, но не дает никакого представления о том, что же в действительности происходит. Если отбросить всю эту объектно-ориентированную мишуру, то станет очевидно, что объект "драйвер" представляет собой обыкновенную структуру данных типа DRIVER_OBJECT (определена в \include\w2k\ntddk.inc). Некоторые поля этой структуры заполняет система, некоторые придется заполнять нам самим. Обращаясь к этой структуре, система и управляет драйвером. Итак, как вы наверное уже поняли, первым параметром, передающимся в функцию DriverEntry, как раз и является указатель на эту самую структуру (или пользуясь объектно-ориентированной терминологией - объект "драйвер"). Используя этот указатель, мы можем (и будем, но позже) заполнить соответствующие поля структуры DRIVER_OBJECT. Но, в рассматриваемых в этой части драйверах этого не требуется, поэтому мы, пока, оставим pDriverObject без внимания.

pusRegistryPath

- указатель на раздел реестра, содержащий параметры инициализации драйвера.

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

О формате данных UNICODE_STRING следует сказать особо. В отличие от режима пользователя, режим ядра оперирует строками в формате UNICODE_STRING. Эта структура определена в файле \include\w2k\ntdef.inc следующим образом:

UNICODE_STRING STRUCT
    _Length         WORD    ?  ; длина строки в байтах (не символах)
    MaximumLength   WORD    ?  ; длина буфера содержащего строку в байтах (не символах)
    Buffer          PWSTR   ?  ; указатель на буфер содержащий строку
UNICODE_STRING ENDS

Параметр | Описание
_Length | - содержит текущую длину строки в байтах (не в символах!), не считая завершающего нуля. Мне пришлось изменить оригинальное имя Length, т.к. оно является зарезервированным словом
MaximumLength | - максимальный размер буфера (также в байтах), в котором эта строка содержится.
Buffer | - указатель на саму Unicode-строку.

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

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

xor eax, eax
xchg [eax], eax

Это приведет к остановке системы и появлению BSOD (Blue Screen Of Death). А выполнение такого кода приведет к перезагрузке компьютера:

mov al, 0FEh
out 64h, al

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

В этих двух случаях, процедура DriverEntry никогда не вернет управление. Поэтому, возвращаемое ею значение не важно. Если же действия выполняемые DriverEntry будут более конструктивными, как, например, в драйвере beeper, то надо вернуть системе некое значение, указывающее на то, как прошла инициализация драйвера. Если вернуть STATUS_SUCCESS, то инициализация считается успешной, и драйвер остается в памяти. Любое другое значение STATUS_* указывает на ошибку, и в этом случае драйвер выгружается системой. Вышеприведенный драйвер simplest является самым простым, какой только можно себе представить. Единственное что он делает, это позволяет себя загрузить. Т.к. ничего кроме этого он сделать больше не может, то возвращает код ошибки STATUS_DEVICE_CONFIGURATION_ERROR. Я просто подобрал подходящее по смыслу значение (полный список можно посмотреть в файле \include\w2k\ntstatus.inc). Если возвратить STATUS_SUCCESS, то драйвер так и останется болтаться в памяти без дела, и выгрузить его средствами SCM будет невозможно, т.к. мы не определили процедуру отвечающую за выгрузку драйвера. Эта процедура должна находиться в самом драйвере. Она выполняет действия, зеркальные по отношению к DriverEntry. Если драйвер выделил себе какие-то ресурсы, например, память, то в процедуре выгрузки эта память должна быть возвращена системе. И только сам драйвер знает об этом. Но, тут я немного забежал вперед.

3.3 Драйвер режима ядра beeper

3.3.1 Исходный текст драйвера beeper

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

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  beeper - Драйвер режима ядра
;  Пищит системным динамиком
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\hal.inc

includelib \masm32\lib\w2k\hal.lib

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

TIMER_FREQUENCY        equ 1193167                   ; 1,193,167 Гц
OCTAVE                 equ 2                         ; множитель октавы

PITCH_C                equ 523                       ; До        -  523,25 Гц
PITCH_Cs               equ 554                       ; До диез   -  554,37 Гц
PITCH_D                equ 587                       ; Ре        -  587,33 Гц
PITCH_Ds               equ 622                       ; Ре диез   -  622,25 Гц
PITCH_E                equ 659                       ; Ми        -  659,25 Гц
PITCH_F                equ 698                       ; Фа        -  698,46 Гц
PITCH_Fs               equ 740                       ; Фа диез   -  739,99 Гц
PITCH_G                equ 784                       ; Соль      -  783,99 Гц
PITCH_Gs               equ 831                       ; Соль диез -  830,61 Гц
PITCH_A                equ 880                       ; Ля        -  880,00 Гц
PITCH_As               equ 988                       ; Ля диез   -  987,77 Гц
PITCH_H                equ 1047                      ; Си        - 1046,50 Гц

; Нам нужны три звука для до-мажорного арпеджио (до, ми, соль)

TONE_1                 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
TONE_2                 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
TONE_3                 equ (PITCH_G*OCTAVE)           ; для HalMakeBeep

DELAY                  equ 1800000h                   ; для моей ~800mHz машины

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         М А К Р О С Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DO_DELAY MACRO
    mov eax, DELAY
    .while eax
        dec eax
    .endw
ENDM

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                              К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            MakeBeep1
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

MakeBeep1 proc dwPitch:DWORD

    ; Прямой доступ к оборудованию через порты ввода-вывода

    cli

    mov al, 10110110y
    out 43h, al

    mov eax, dwPitch
    out 42h, al

    mov al, ah
    out 42h, al

    ; включить динамик

    in al, 61h
    or  al, 11y
    out 61h, al

    sti

    DO_DELAY

    cli

    ; выключить динамик

    in al, 61h
    and al, 11111100y
    out 61h, al

    sti

    ret

MakeBeep1 endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            MakeBeep2
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

MakeBeep2 proc dwPitch:DWORD

    ; Прямой доступ к оборудованию используя функции
    ; WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll

    cli

    invoke WRITE_PORT_UCHAR, 43h, 10110110y

    mov eax, dwPitch
    invoke WRITE_PORT_UCHAR, 42h, al
    mov eax, dwPitch
    invoke WRITE_PORT_UCHAR, 42h, ah

    ; включить динамик

    invoke READ_PORT_UCHAR, 61h
    or  al, 11y
    invoke WRITE_PORT_UCHAR, 61h, al

    sti

    DO_DELAY

    cli

    ; выключить динамик

    invoke READ_PORT_UCHAR, 61h
    and al, 11111100y
    invoke WRITE_PORT_UCHAR, 61h, al

    sti

    ret

MakeBeep2 endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke MakeBeep1, TONE_1
    invoke MakeBeep2, TONE_2

    ; Прямой доступ к оборудованию используя функцию HalMakeBeep из модуля hal.dll

    invoke HalMakeBeep, TONE_3
    DO_DELAY
    invoke HalMakeBeep, 0

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                  С Б О Р К А    Д Р А Й В Е Р А
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:make

set drv=beeper

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

Задача этого драйвера, исполнять на системном динамике восходящее до-мажорное арпеджио. Что это такое, вы, наверное уже послушали. Для этого драйвер использует инструкции процессора in и out, обращаясь к соответствующим портам ввода-вывода. Общеизвестно, что доступ к портам ввода-вывода - это свято охраняемый Windows NT системный ресурс. Попытка обращения к любому из них, как на ввод, так и на вывод, из режима пользователя, неизбежно приводит к завершению приложения. Но, на самом деле, есть способ обойти и это ограничение, т.е. обращаться к портам ввода-вывода прямо из третьего кольца. В этом вы убедитесь ниже. Правда, для этого, опять таки, нужен драйвер.

3.3.2 Управление системным таймером

На материнской плате находится устройство - системный таймер (system timer), который является перепрограммируемым. Таймер содержит несколько каналов, 2-ой управляет системным динамиком компьютера, генерируя прямоугольные импульсы с частотой 1193180/<начальное значение счетчика> герц. Начальное значение счетчика является 16-битным, и устанавливается через порт 42h. 1193180 Гц - частота тактового генератора таймера. Тут есть одна тонкость, которую я не совсем понимаю. Функция QueryPerformanceFrequency из kernel32.dll действительно возвращает значение 1193180. Оно просто жестко зашито в тело функции. Но дизассемблировав hal.dll, в функции HalMakeBeep я обнаружил несколько другое значение, равное 1193167. Его я и использую. Возможно, здесь учтена какая-то временная задержка, или что-то подобное. В любом случае, пищать системным динамиком нам это никак не помешает. Я не буду подробно останавливаться на описании системного таймера. Эту тему очень любят мусолить почти в каждой книжке по программированию на ассемблере. Достаточно подробную информацию можно найти в сети.

Итак, первый звук до-мажорного арпеджио мы воспроизводим пользуясь процедурой MakeBeep1.

    mov al, 10110110y
    out 43h, al

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

    mov eax, dwPitch
    out 42h, al

    mov al, ah
    out 42h, al

Затем, в порт 42h выводим 16-битное начальное значение счетчика. Сначала младший байт, затем старший.

    in al, 61h
    or  al, 11y
    out 61h, al

И, наконец, посредством вывода в порт 61h значения, с установленными 0-ым и 1-ым битами, включаем динамик.

DO_DELAY MACRO
    mov eax, DELAY
    .while eax
        dec eax
    .endw
ENDM

Даем данамику позвучать некоторое время, пользуясь макросом DO_DELAY. Да - примитивно, но - эффективно ;-)

    in al, 61h
    and al, 11111100y
    out 61h, al

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

Второй звук (ми) мы воспроизводим посредством процедуры MakeBeep2, тем же самым образом, но используя для обращения к портам ввода-вывода функции WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll. Помимо этих двух, в модуле hal.dll имеется целый набор подобных функций. Они призваны скрыть межплатформенные различия. Вспомните, что я говорил про HAL в первой части статьи. Для процессора alpha, например, внутренняя реализация этих функций будет совершенно другой, но для драйвера ничего не изменится. Я использовал эти функции для разнообразия. Просто, чтобы показать, что такие функции есть.

Третий звук (соль) мы воспроизводим пользуясь функцией HalMakeBeep, находящейся в модуле hal.dll. Внутри этой функции происходят события, полностью аналогичные двум предыдущим случаям. Опять же, имеется в виду модуль hal.dll для платформы x86. При этом, в качестве параметра, нужно использовать не частное частоты тактового генератора таймера и начального значения счетчика, а само значение частоты, которую мы хотим воспроизвести. В начале файла beeper.bat определены все 12 нот. Я использую только до, ми и соль. Остальные оставлены для вашего будущего супер-пуппер синтезатора ;-). Для выключения динамика, надо вызвать HalMakeBeep еще раз, передав в качестве аргумента 0.

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

3.3.3 Автоматическая загрузка драйвера

Программа scp.exe производит загрузку драйвера beeper по требованию. Для того, чтобы закончить с этим вопросом, думаю, будет уместно попробовать загрузить его автоматически, раз уж мы так подробно разобрали этот вопрос в прошлый раз. Проще всего это сделать так: закомментарьте вызов функции DeleteService, в вызове функции CreateService замените SERVICE_DEMAND_START на SERVICE_AUTO_START, а SERVICE_ERROR_IGNORE на SERVICE_ERROR_NORMAL, перекомпилируйте csp.asm и запустите. В реестре останется соответствующая запись. Теперь можете забыть об этом до следующей перезагрузки системы. Драйвер beeper сам напомнит о себе, а в журнале событий системы останется запись о произошедшей ошибке. Посмотреть на нее можно с помощью оснастки Администрирование > Просмотр событий (Administrative Tools > Event Viewer).

Рис. 3-1. Сообщение об ошибке

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

3.4 Программа управления драйвером giveio

3.4.1 Исходный текст программы управления драйвером giveio

Теперь рассмотрим программу управления другим драйвером - giveio.

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  DateTime - Программа управления драйвером giveio
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option  casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         М А К Р О С Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

CMOS MACRO by:REQ
    mov al, by
    out 70h, al
    in al, 71h

    mov ah, al
    shr al, 4
    add al, '0'

    and ah, 0Fh
    add ah, '0'
    stosw
ENDM

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          DateTime
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DateTime proc uses edi

local acDate[16]:CHAR
local acTime[16]:CHAR
local acOut[64]:CHAR

    ; Подробнее смотри Ralf Brown's Interrupt List

    ;:::::::::::::::::: Установим формат таймера ::::::::::::::::::

    mov al, 0Bh               ; Управляющий регистр B
    out 70h, al
    in al, 71h

    push eax                  ; Сохраним старый фармат таймера
    and al, 11111011y         ; Бит 2: Формат - 0: упакованный двоично-десятичный, 1: двоичный
    or al, 010y               ; Бит 1: 24/12 формат часа - 1 включает 24-часовой режим
    out 71h, al

    ;:::::::::::::::::::: Получим текущую дату ::::::::::::::::::::

    lea edi, acDate

    CMOS 07h                  ; Число месяца
    mov al, '.'
    stosb

    CMOS 08h                  ; Месяц
    mov al, '.'
    stosb

    CMOS 32h                  ; Две старшие цифры года
    CMOS 09h                  ; Две младшие цифры года

    xor eax, eax              ; Завершим строку нулем
    stosb

    ;:::::::::::::::::::: Получим текущее время :::::::::::::::::::

    lea edi, acTime

    CMOS 04h                  ; Часы
    mov al, ':'
    stosb

    CMOS 02h                  ; Минуты
    mov al, ':'
    stosb

    CMOS 0h                   ; Секунды

    xor eax, eax              ; Завершим строку нулем
    stosb

    ;:::::::::::::: Восстановим старый формат таймера :::::::::::::

    mov al, 0Bh
    out 70h, al
    pop eax
    out 71h, al

    ;::::::::::::::::: Покажем текущие дату и время :::::::::::::::

    invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
    invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

    ret

DateTime endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

start proc

local fOK:BOOL
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR

local hKey:HANDLE
local dwProcessId:DWORD

    and fOK, 0        ; Предположим, что произойдет ошибка

    ; Открываем базу данных SCM

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("giveio.sys"), sizeof acDriverPath, addr acDriverPath, esp
        pop eax

        ; Регистрируем драйвер

        invoke CreateService, hSCManager, $CTA0("giveio"), $CTA0("Current Date and Time fetcher."), \
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL

        .if eax != NULL
            mov hService, eax

            invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
                                    $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
                                    0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey

            .if eax == ERROR_SUCCESS

                ; Добавляем в реестр идентификатор текущего процесса

                invoke GetCurrentProcessId
                mov dwProcessId, eax
                invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
                                        addr dwProcessId, sizeof DWORD

                .if eax == ERROR_SUCCESS
                    invoke StartService, hService, 0, NULL
                    inc fOK                ; Устанавливаем флаг
                    invoke RegDeleteValue, hKey, addr szProcessId
                .else
                    invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
                                        NULL, MB_ICONSTOP
                .endif

                invoke RegCloseKey, hKey

            .else
                invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP
            .endif

            ; Удаляем драйвер из базы данных SCM

            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
                           NULL, MB_ICONSTOP
    .endif

    ; Если все ОК, получаем и показываем текущие дату и время

    .if fOK
        invoke DateTime
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

3.4.2 Использование реестра для передачи параметров драйверу

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

            invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
                                    $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
                                    0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey

            .if eax == ERROR_SUCCESS

                ; Добавляем в реестр идентификатор текущего процесса

                invoke GetCurrentProcessId
                mov dwProcessId, eax
                invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
                                        addr dwProcessId, sizeof DWORD

                .if eax == ERROR_SUCCESS
                    invoke StartService, hService, 0, NULL

Перед запуском драйвера, мы создаем в подразделе реестра, соответствующем драйверу, дополнительный параметр ProcessId, и устанавливаем его значение равным идентификатору текущего процесса, т.е. процесса программы управления. Обратите внимание на то, что вызывая макрос $CTA0, я указываю метку szProcessId, которой будет помечен текст "ProcessId", для того, чтобы позже к нему обратиться. Если добавление параметра прошло без ошибок, то запускаем драйвер. Зачем нужен этот дополнительный параметр вы узнаете позже, когда мы будем разбирать текст драйвера.

                    inc fOK
                    invoke RegDeleteValue, hKey, addr szProcessId
                .else
                    invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
                                        NULL, MB_ICONSTOP
                .endif

                invoke RegCloseKey, hKey

Получив управление от функции StartService, мы считаем, что драйвер успешно отработал и устанавливаем флаг fOK. Вызов функции RegDeleteValue делать не обязательно. Все равно, весь раздел реестра будет удален последующим вызовом DeleteService. Просто, я стараюсь придерживаться в программировании правила "хорошего тона": нагадил - подотри ;-)

    .if fOK
        invoke DateTime
    .endif

Удалив драйвер из базы данных SCM и закрыв все открытые описатели, мы вызывает процедуру DateTime, предварительно проверив флаг fOK.

3.4.3 Обращение к памяти CMOS

На материнской плате компьютера имеется специальная микросхема, выполненная по технологии CMOS (Complementary Metal-Oxide Semiconductor, Металл-Окисел-Полупроводник с Комплементарной структурой, КМОП), и питающаяся от батарейки. В этой микросхеме реализован еще один таймер, называемый часами реального времени (Real Time Clock, RTC), который работает постоянно, даже при выключенном питании компьютера. Помимо таймера, в этой микросхеме имеется небольшой блок памяти, в котором хранится собственно текущее время, а также кое-какая информация о физических параметрах компьютера. Достаточно подробно об этом можно узнать в справочнике "Ralf Brown's Interrupt List". Получить содержимое памяти CMOS можно обратившись к портам ввода-вывода 70h и 71h.

    mov al, 0Bh               ; Управляющий регистр B
    out 70h, al
    in al, 71h

    push eax                  ; Сохраним старый фармат таймера
    and al, 11111011y         ; Бит 2: Формат - 0: упакованный двоично-десятичный, 1: двоичный
    or al, 010y               ; Бит 1: 24/12 формат часа - 1 включает 24-часовой режим
    out 71h, al

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

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

    invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
    invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

Получив текущие дату и время, составляем из них единую строку и выводим ее на экран. Управляющая последовательность \t вставляет символ горизонтальной табуляции, а \n перевода строки (подробнее см. \Macros\Strings.mac). И на экране мы должны увидеть:

Рис. 3-2. Результат работы программы DateTime.exe

Самым странным, в вышеприведенном тексте, является обращение к портам ввода-вывода прямо из режима пользователя. Как я уже упомянул выше, доступ к портам ввода-вывода свято охраняется Windows NT. И тем не менее, мы к ним обратились. Это стало возможно благодаря драйверу giveio, к рассмотрению исходного текста которого мы и переходим.

3.5 Драйвер режима ядра giveio

3.5.1 Исходный текст драйвера giveio

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  giveio - Драйвер режима ядра
;
;  Предостваляет прямой доступ к портам ввода-вывода из режима пользователя
;   Основан на исходном тексте Дейла Робертса
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

IOPM_SIZE equ 2000h     ; размер карты разрешения ввода-вывода

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local kvpi:KEY_VALUE_PARTIAL_INFORMATION
local pIopm:PVOID
local pProcess:LPVOID

    invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    lea ecx, oa
    InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

    invoke ZwOpenKey, addr hKey, KEY_READ, ecx
    .if eax == STATUS_SUCCESS

        push eax
        invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
                               KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
        pop ecx

        .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )

            invoke DbgPrint, $CTA0("giveio: Process ID: %X"), \
                                dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data

            ; выделяем буфер для карты разрешения ввода-вывода

            invoke MmAllocateNonCachedMemory, IOPM_SIZE
            .if eax != NULL
                mov pIopm, eax

                lea ecx, kvpi
                invoke PsLookupProcessByProcessId, \
                        dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
                .if eax == STATUS_SUCCESS

                    invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess

                    invoke Ke386QueryIoAccessMap, 0, pIopm
                    .if al != 0

                        ; Открываем доступ к порту 70h

                        mov ecx, pIopm
                        add ecx, 70h / 8
                        mov eax, [ecx]
                        btr eax, 70h MOD 8
                        mov [ecx], eax

                       ; Открываем доступ к порту 71h

                        mov ecx, pIopm
                        add ecx, 71h / 8
                        mov eax, [ecx]
                        btr eax, 71h MOD 8
                        mov [ecx], eax

                        invoke Ke386SetIoAccessMap, 1, pIopm
                        .if al != 0
                            invoke Ke386IoSetAccessProcess, pProcess, 1
                            .if al != 0
                                invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")
                            .else
                                invoke DbgPrint, $CTA0("giveio: I/O permission is failed")
                                mov status, STATUS_IO_PRIVILEGE_FAILED
                            .endif
                        .else
                            mov status, STATUS_IO_PRIVILEGE_FAILED
                        .endif
                    .else
                        mov status, STATUS_IO_PRIVILEGE_FAILED
                    .endif
                    invoke ObDereferenceObject, pProcess
                .else
                    mov status, STATUS_OBJECT_TYPE_MISMATCH
                .endif
                invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
            .else
                invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
                mov status, STATUS_INSUFFICIENT_RESOURCES
            .endif
        .endif
        invoke ZwClose, hKey
    .endif

    invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")

    mov eax, status
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                  С Б О Р К А    Д Р А Й В Е Р А
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:make

set drv=giveio

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

3.5.2 Карта разрешения ввода-вывода

Код драйвера основан на хорошо известных изысканиях Дейла Робертса, восходящих аж к 96 году прошлого века, в области предоставления процессу режима пользователя доступа к портам ввода-вывода на платформе Windows NT. Я решил, что здесь это будет очень кстати. Перевод статьи Дейла Робертса "Прямой ввод-вывод в среде Windows NT" можно почитать http://void.ru/?do=printable&id=701.

Я не буду подробно останавливаться на теории, т.к. достаточно подробно это описано в вышеупомянутой статье. Если очень коротко, то процессор поддерживает гибкий механизм защиты, позволяющий операционной системе предоставлять доступ к любому подмножеству портов ввода-вывода для каждого отдельно взятого процесса. Это возможно благодаря карте разрешения ввода-вывода (I/O Permission Map, IOPM). Немного подробнее про эту карту здесь: http://www.sasm.narod.ru/docs/pm/pm_tss/chap_5.htm. Про сегмент состояния задачи (Task State Segment, TSS), также активно принимающий в этом участие, можно почитать там же: http://www.sasm.narod.ru/docs/pm/pm_tss/chap_3.htm. Каждый процесс может иметь свою собственную IOPM. Каждый бит в этой карте соответствует байтовому порту ввода-вывода. Если он (бит) установлен, то доступ к соответствующему порту запрещен, если сброшен - разрешен. Поскольку, пространство портов ввода-вывода в архитектуре x86 составляет 65535, то максимальный размер IOPM равен 2000h байт.

Всё, что сказано выше о I/O Permission Map верно, но не для операционных систем Windows NT+. Разработчики этих систем отказались от использования отдельного TSS для каждого процесса, по причине худшей производительности, а фирма Intel задумывала именно так и процессоры этой фирмы такую возможность поддерживают. Операционные систем Windows NT+ используют один TSS на все процессы. Поскольку TSS глобален, то и IOPM тоже. Это значит, что любые манипуляции с ней отражаются на все выполняющиеся, а также те, которые будут выполняться процессы.

Для манипулирования IOPM в модуле ntoskrnl.exe имеются две полностью недокументированные функции: Ke386QueryIoAccessMap и Ke386SetIoAccessMap. Приведу их описание составленное стараниями Дейла Робертса и моими тоже.

Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

Копирует текущую IOPM размером 2000h из TSS в буфер, указатель на который содержится в параметре pIopm.

Параметр | Описание
dwFlag | 0 - заполнить буфер единичными битами (т.е запретить доступ ко всем портам); 1 - скопировать текущую IOPM из TSS в буфер.
pIopm | - указатель на блок памяти для приема IOPM, размером не менее 2000h байт.

При успешном завершении, возвращает в регистре al ненулевое значение. Если произошла ошибка, то al равен нулю.

Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

Копирует переданную IOPM длинной 2000h из буфера, указатель на который содержится в параметре pIopm, в TSS.

Параметр | Описание
dwFlag | только 1 - разрешает копирование. При любом другом значении функция возвращает ошибку.
pIopm | - указатель на блок памяти содержащий IOPM, размером не менее 2000h байт.

При успешном завершении, возвращает в регистре al ненулевое значение. Если произошла ошибка, то al равен нулю.

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

Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD

Разрешает/запрещает использование IOPM для процесса.

Параметр | Описание
pProcess | - указатель на структуру KPROCESS (чуть подробней ниже).
dwFlag | 0 - запретить доступ к портам ввода-вывода, установкой смещения IOPM за границу сегмента TSS; 1 - разрешить доступ к портам ввода-вывода, устанавливая смещение IOPM в пределах TSS равным 88h.

При успешном завершении, возвращает в регистре al ненулевое значение. Если произошла ошибка, то al равен нулю.

Кстати, по префиксу в имени функции можно определить к какому компоненту она относится: Ke - ядро, Ob - диспетчер объектов, Ps - поддержка процессов, Mm - диспетчер памяти и т.д.

Для доступа к объектам код режима пользователя использует описатели (handles), которые являются ни чем иным как индексами в таблице описателей, в которой содержится сами указатели на объекты. Ну а что такое, на самом деле, объект мы уже немного поговорили выше. Таким образом, посредством описателей система отрезает код режима пользователя от прямого доступа к объекту. Код режима ядра, напротив, пользуется именно указателями, т.к. он и есть сама система и имеет право делать с объектами что хочет. Функция Ke386IoSetAccessProcess требует, в качестве первого параметра, указатель на объект "процесс" (process object), т.е. на структуру KPROCESS (см. \include\w2k\w2kundoc.inc. Я специально поставил префикс "w2k", т.к. в разных поколениях Windows NT недокументированные структуры сильно отличаются. Так что, использовать этот файлик при компиляции драйвера предназначенного для XP или Server 2003, не самая лучшая идея). Код функции Ke386IoSetAccessProcess устанавливает член IopmOffset структуры KPROCESS в соответствующее значение.

3.5.3 Чтение данных из реестра

Раз мы будем вызывать функцию Ke386IoSetAccessProcess, нам потребуется указатель на объект "процесс". Его можно получить разными способами. Я выбрал наиболее простой - по идентификатору. Именно поэтому в модуле DateTime мы получаем идентификатор текущего процесса и помещаем его в реестр. В данном случае мы используем реестр просто для передачи данных в драйвер. Т.к. процедура DriverEntry выполняется в контексте процесса System, нет возможности узнать, какой процесс на самом деле запустил драйвер. Вторым параметром, pusRegistryPath, в процедуре DriverEntry мы имеем указатель на раздел реестра, содержащий параметры инициализации драйвера. Мы воспользуемся им, чтобы извлечь из реестра идентификатор процесса.

Теперь можно перейти к разбору кода драйвера giveio.

    lea ecx, oa
    InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

Для последующего вызова функции ZwOpenKey нам потребуется указатель на заполненную структуру OBJECT_ATTRIBUTES (\include\w2k\ntdef.inc). Для ее заполнения я использую макрос InitializeObjectAttributes. Можно заполнить и "вручную":

lea ecx, oa
xor eax, eax
assume ecx:ptr OBJECT_ATTRIBUTES
mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
mov [ecx].RootDirectory, eax                       ; NULL
push pusRegistryPath
pop [ecx].ObjectName
mov [ecx].Attributes, eax                          ; 0
mov [ecx].SecurityDescriptor, eax                  ; NULL
mov [ecx].SecurityQualityOfService, eax            ; NULL
assume ecx:nothing

    invoke ZwOpenKey, addr hKey, KEY_READ, ecx
    .if eax == STATUS_SUCCESS

        push eax
        invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
                               KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
        pop ecx

Вызовом функции ZwOpenKey получаем описатель раздела реестра в переменной hKey. Вторым параметром в эту функцию передаются права доступа, третьим - указатель на структуру OBJECT_ATTRIBUTES, заполненную на предыдущем этапе. С помощью функции ZwQueryValueKey получаем значение идентификатора процесса, записанное в параметре реестра ProcessId. Вторым параметром в эту функцию передается указатель на инициализированную структуру UNICODE_STRING, содержащую имя параметра реестра, значение которого мы хотим получить. Я стараюсь использовать возможности препроцессора masm на "полную катушку", поэтому, и тут использую самописный макрос $CCOUNTED_UNICODE_STRING. Обратите внимание на то, что я указываю выравнивание строки по границе двойного слова (выравнивание самой структуры UNICODE_STRING жестко прописано в макросе и равно двойному слову). Какой-то особой необходимости в этом тут нет, просто, я даю вам возможность оценить гибкость и удобство моих макросов. Рекламная пауза ;-) Если органически не перевариваете макросы, то можно использовать традиционный способ определения Unicode-строки, и структуры UNICODE_STRING ее содержащей:

 usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
 us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}

Меня этот способ никогда не вдохновлял, поэтому, я и написал для этой цели макросы COUNTED_UNICODE_STRING, $COUNTED_UNICODE_STRING, CCOUNTED_UNICODE_STRING, $CCOUNTED_UNICODE_STRING (см. \Macros\Strings.mac).

Третий параметр функции ZwQueryValueKey определяет тип запрашиваемой информации. KeyValuePartialInformation - символьная константа равная 2 (\include\w2k\ntddk.inc). Четвертый и пятый параметры - указатель на структуру KEY_VALUE_PARTIAL_INFORMATION и ее размер соответственно. В члене Data этой структуры мы и получим значение идентификатора процесса. Последний параметр - указатель на переменную, размером DWORD, в которую будет записано количество скопированных из реестра байт. Перед самым вызовом ZwQueryValueKey, мы резервируем на стеке для него место, а после вызова извлекаем значение. Я постоянно пользуюсь таким приемом - очень удобно.

3.5.4 Откраваем коду режима пользователя доступ к портам ввода-вывода

        .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
            invoke MmAllocateNonCachedMemory, IOPM_SIZE
            .if eax != NULL
                mov pIopm, eax

Если вызов ZwQueryValueKey прошел успешно, выделяем с помощью функции MmAllocateNonCachedMemory кусочек памяти в пуле неподкачиваемой памяти (такая память никогда не сбрасывается на диск), размером 2000h байт - максимальный размер карты разрешения ввода-вывода. Сохраняем указатель в переменной pIopm.

                lea ecx, kvpi
                invoke PsLookupProcessByProcessId, \
                        dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
                .if eax == STATUS_SUCCESS
                    invoke Ke386QueryIoAccessMap, 0, pIopm

Передавая в функцию PsLookupProcessByProcessId полученный ранее идентификатор процесса, получаем указатель на KPROCESS в переменной pProcess. Вызовом функции Ke386QueryIoAccessMap, копируем IOPM в буфер.

                    .if al != 0

                        mov ecx, pIopm
                        add ecx, 70h / 8
                        mov eax, [ecx]
                        btr eax, 70h MOD 8
                        mov [ecx], eax

                        mov ecx, pIopm
                        add ecx, 71h / 8
                        mov eax, [ecx]
                        btr eax, 71h MOD 8
                        mov [ecx], eax

                        invoke Ke386SetIoAccessMap, 1, pIopm
                        .if al != 0
                            invoke Ke386IoSetAccessProcess, pProcess, 1
                            .if al != 0
                                ; доступ получен
                            .else
                                mov status, STATUS_IO_PRIVILEGE_FAILED
                            .endif
                        .else
                            mov status, STATUS_IO_PRIVILEGE_FAILED
                        .endif
                    .else
                        mov status, STATUS_IO_PRIVILEGE_FAILED
                    .endif

Сбрасываем биты соответствующие портам ввода-вывода 70h и 71h, и записываем модифицированную IOPM. Вызовом функции Ke386IoSetAccessProcess разрешаем доступ. Обратите внимание, что Microsoft предусмотрела специальный код ошибки STATUS_IO_PRIVILEGE_FAILED. В принципе, здесь совершенно не важно, какой код ошибки мы вернем системе при выходе из DriverEntry.

                    invoke ObDereferenceObject, pProcess
                .else
                    mov status, STATUS_OBJECT_TYPE_MISMATCH
                .endif

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

                invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
            .else
                invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
                mov status, STATUS_INSUFFICIENT_RESOURCES
            .endif
        .endif
        invoke ZwClose, hKey
    .endif

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

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

В этом примере я обратился к памяти CMOS, просто, для разнообразия. Можно было, как в предыдущем драйвере beeper, попищать системным динамиком. Оставляю это вам, в качестве домашнего задания. Надо будет открыть доступ к соответствующим портам ввода-вывода. Вызвать процедуру MakeBeep1, предварительно убрав из ее тела каманды cli и sti, т.к. выполнять привилегированные команды процессора в режиме пользователя, вам никто не разрешит. Вызывать функции из модуля hal.dll, естественно, тоже нельзя, т.к. они находятся в адресном пространстве ядра. Максимум, что вы можете себе позволить - это предоставить доступ ко всем 65535 портам, одним махом, использовав такой код:

invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
    mov pIopm, eax
    invoke RtlZeroMemory, pIopm, IOPM_SIZE
    lea ecx, kvpi
    invoke PsLookupProcessByProcessId, \
                dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
    .if eax == STATUS_SUCCESS
        invoke Ke386SetIoAccessMap, 1, pIopm
        .if al != 0
            invoke Ke386IoSetAccessProcess, pProcess, 1
        .endif
        invoke ObDereferenceObject, pProcess
    .endif
    invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
    mov status, STATUS_INSUFFICIENT_RESOURCES
.endif

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

3.6 Пара слов об отладке

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

Базовой техникой является расстановка в нужных местах исходного текста отладочного прерывания int 3. При этом нужно убедиться, что в SoftICE включено отслеживание этого прерывания. В более поздних версиях SoftICE, для адресов режима ядра (>80000000h), это сделано автоматически. Проверить это можно с помощью команды i3here. Если отлов int 3 не включен, сделать это можно с помощью той же команды i3here on (выключается - i3here off). Очень советую прописать эту команду прямо в параметры инициализации SoftICE. Если вы забудите это сделать при следующей загрузке системы, и запустите драйвер с таким прерыванием, то BSOD не заставит себя ждать. Есть еще одна команда приводящая к тому же результату - bpint 3. Разница в том, что в первом случае, вы окажетесь в SoftICE на инструкции следующей за int 3, а во втором, прямо на int 3. Можно сделать и так: bpint 3 do "r eip eip+1", но это менее удобно.

В коде драйвера giveio я неоднократно вызывал функцию DbgPrint. Эта функция выводит на консоль отладчика форматированные сообщения. SoftICE прекрасно их понимает. Можно использовать утилиту DebugView Марка Руссиновича http://www.sysinternals.com.

Все драйверы я тестировал под Windows 2000 Pro, Windows XP Pro и Windows Server 2003 Standart. Но все должно работать и на более ранних (и поздних, когда таковые появятся) выпусках Windows NT.

В следующей части мы поговорим о подсистеме ввода-вывода вообще, и о диспетчере ввода-вывода в частности.


Подсистема ввода-вывода

4.1 Диспетчер ввода-вывода
4.2 Программа управления драйвером VirtToPhys
4.2.1 Исходный текст программы управления
4.2.2 Объект "устройство"
4.2.3 Объект "драйвер"
4.2.4 Объект "символьная ссылка"
4.2.5 Объект "файл"
4.2.6 Управление виртуальным устройством
4.2.7 Управляющий код ввода-вывода
4.2.8 Обмен данными с устройством
4.2.9 Очистка ресурсов
Исходный текст: KmdKit\examples\simple\VirtToPhys

4.1 Диспетчер ввода-вывода

В первой части я сказал, что разрабатываемые нами драйверы можно считать DLL режима ядра. С определенной долей условности это действительно так. Зачем еще нужен драйвер, который не управляет каким-либо реальным устройством? Только для того, чтобы служить проводником в режим ядра. При этом код драйвера, по сути, является набором функций, позволяющих решать задачи недоступные коду режима пользователя. Когда нужно решить одну из таких задач, вызывается соответствующая функция. Принципиальная разница (не считая уровня привилегий) в том, что, в случае обычной DLL, мы получаем (явно или неявно) адрес интересующей нас функции и передаем туда управление. В случае драйвера режима ядра, такой сценарий был бы крайне опасен с точки зрения безопасности системы. Поэтому, система предоставляет посредника в лице диспетчера ввода-вывода (I/O manager), который является одним из компонентов подсистемы ввода-вывода (I/O subsystem). Диспетчер ввода-вывода отвечает за формирование пакета запроса ввода-вывода (I/O request packet, IRP) и посылку его драйверу для дальнейшей обработки. Весьма упрощенная схема взаимодействия диспетчера ввода-вывода с приложениями пользовательского режима и драйверами устройств приведена на рис 4-1.

Рис. 4-1. Упрощенная схема ввода-вывода

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

Код режима пользователя вынужден заказывать операцию ввода-вывода на устройство. Именно на устройство. Дело в том, что в процедуре DriverEntry драйвер должен создать устройство (или несколько), которым он будет управлять. В нашем случае это устройство виртуальное. Создание устройства, конечно же, не следует понимать буквально, даже если драйвер призван управлять реальным устройством. Это означает создание в памяти соответствующих структур данных, олицетворяющих для системы само устройство. Создавая устройство, драйвер как бы говорит диспетчеру ввода-вывода: "Вот устройство, которым я буду управлять. Если к тебе придет запрос ввода-вывода на это устройство, ты отправляй его ко мне, а я уж разберусь, что с ним делать." Только драйвер знает как работать со своим устройством. Диспетчер ввода-вывода просто занимается формированием пакетов запроса ввода-вывода и направлением их нужным драйверам. А код режима пользователя вообще не знает, и не должен знать, какой драйвер или драйверы какое устройство обслуживают.

4.2 Программа управления драйвером VirtToPhys

4.2.1 Исходный текст программы управления

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

Вот этот исходный код.

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; VirtToPhys.asm - Программа управления драйвером VirtToPhys
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\include\winioctl.inc

include \masm32\Macros\Strings.mac

include common.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      BigNumToString
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

BigNumToString proc uNum:UINT, pszBuf:LPSTR

; переводит число в строку разделяя разряды пробелами

local acNum[32]:CHAR
local nf:NUMBERFMT

    invoke wsprintf, addr acNum, $CTA0("%u"), uNum

    and nf.NumDigits, 0
    and nf.LeadingZero, FALSE
    mov nf.Grouping, 3
    mov nf.lpDecimalSep, $CTA0(".")
    mov nf.lpThousandSep, $CTA0(" ")
    and nf.NegativeOrder, 0
    invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pszBuf, 32

    ret

BigNumToString endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

start proc uses esi edi

local hSCManager:HANDLE
local hService:HANDLE
local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local hDevice:HANDLE

local adwInBuffer[NUM_DATA_ENTRY]:DWORD
local adwOutBuffer[NUM_DATA_ENTRY]:DWORD
local dwBytesReturned:DWORD

local acBuffer[256+64]:CHAR
local acThis[64]:CHAR
local acKernel[64]:CHAR
local acUser[64]:CHAR
local acAdvapi[64]:CHAR

local acNumber[32]:CHAR

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \
                                sizeof acModulePath, addr acModulePath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \
                              $CTA0("Virtual To Physical Address Converter"), \
                              SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER, \
                              SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath, \
                              NULL, NULL, NULL, NULL, NULL

        .if eax != NULL
            mov hService, eax

            ; в драйвере будет вызвана функция DriverEntry
            invoke StartService, hService, 0, NULL
            .if eax != 0

                ; драйверу будет направлен IRP типа IRP_MJ_CREATE
                invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
                                0, NULL, OPEN_EXISTING, 0, NULL

                .if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax

                    lea esi, adwInBuffer
                    assume esi:ptr DWORD
                    invoke GetModuleHandle, NULL
                    mov [esi][0*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
                    mov [esi][1*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
                    mov [esi][2*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
                    mov [esi][3*(sizeof DWORD)], eax

                    lea edi, adwOutBuffer
                    assume edi:ptr DWORD
                    ; драйверу будет направлен IRP типа IRP_MJ_DEVICE_CONTROL
                    invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
                                            esi, sizeof adwInBuffer, \
                                            edi, sizeof adwOutBuffer, \
                                            addr dwBytesReturned, NULL

                    .if ( eax != 0 ) && ( dwBytesReturned != 0 )

                        invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
                                                  addr acModulePath, sizeof acModulePath

                        lea ecx, acModulePath[eax-5]
                        .repeat
                            dec ecx
                            mov al, [ecx]
                        .until al == '\'
                        inc ecx
                        push ecx

                        CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod

                        invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
                        pop ecx
                        invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
                                         [esi][0*(sizeof DWORD)], \
                                         [edi][0*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
                                         [esi][1*(sizeof DWORD)], \
                                         [edi][1*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
                                         [esi][2*(sizeof DWORD)], \
                                         [edi][2*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
                                         [esi][3*(sizeof DWORD)], \
                                         [edi][3*(sizeof DWORD)], addr acNumber

                        invoke wsprintf, addr acBuffer, \
                                         $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
                                         addr acThis, addr acKernel, addr acUser, addr acAdvapi

                        assume esi:nothing
                        assume edi:nothing
                        invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \
                                           MB_OK + MB_ICONINFORMATION
                    .else
                        invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
                                           MB_OK + MB_ICONSTOP
                    .endif
                    ; драйверу будет направлен IRP типа IRP_MJ_CLOSE
                    invoke CloseHandle, hDevice
                .else
                    invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
                .endif
                ; в драйвере будет вызвана функция DriverUnload
                invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
            .else
                invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
            .endif
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \
                           MB_OK + MB_ICONSTOP
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

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

4.2.2 Объект "устройство"

Создает устройство и присваивает ему имя драйвер. Для драйверов создающих виртуальные устройства это происходит обычно в процедуре инициализации DriverEntry. Драйвер VirtToPhys создает устройство под именем "devVirtToPhys" (префикс "dev" я добавил сознательно - зачем, скажу ниже).

Итак, драйвер регистрируется и запускается обычным образом. Обращение к функции StartService приводит к тому, что в драйвере вызывается процедура DriverEntry, в которой и создается виртуальное устройство под именем "devVirtToPhys". Именуется устройство для того, чтобы его можно было найти (по имени) и получить к нему доступ.

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

Имена объектов попадают в пространство имен диспетчера объектов. Пространство имен имеет иерархическую структуру аналогичную структуре каталогов файловой системы. Для просмотра базы данных диспетчера объектов можно использовать разные утилиты. Я лично предпочитаю использовать свою - WinObjEx (взять можно на http://www.wasm.ru/).

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

В каталог "\Device", по соглашению, помещаются объекты "устройство". Наше устройство devVirtToPhys помещается драйвером именно в этот каталог.

Рис. 4-2. Объект "устройство" devVirtToPhys в пространстве имен диспетчера объектов

Рис. 4-3. Свойства объекта "устройство" devVirtToPhys

4.2.3 Объект "драйвер"

В каталог "\Driver" попадают объекты "драйвер". В этот каталог система и помещает наш драйвер VirtToPhys (никаких префиксов в имени я не использовал).

Рис. 4-4. Объект "драйвер" VirtToPhys в пространстве имен диспетчера объектов

Все каталоги в пространстве имен диспетчера объектов, кроме двух - "\BaseNamedObjects" и "\??", невидимы для кода режима пользователя. Поэтому, обратиться ни к одному объекту, кроме находящихся в этих двух каталогах, код режима пользователя не может. Это сделано все по тем же соображениям безопасности. Таким образом, обратиться ни к объекту "драйвер" VirtToPhys в каталоге "\Driver", ни к объекту "устройство" devVirtToPhys в каталоге "\Device", код режима пользователя не может.

4.2.4 Объект "символьная ссылка"

Для того чтобы объект "устройство" стал доступен коду режима пользователя, драйвер должен создать в доступном ему (коду режима пользователя) каталоге "\??" еще один объект - символьную ссылку (symbolic link). Да, да именно символьную, а не символическую, как пишут иногда в иных изданиях. И не надо говорить: "А вот в моем большом - на 386 тысяч слов - словаре...". ;-)

Кстати, создать символьную ссылку можно и из режима пользователя функцией DefineDosDevice.

Открыв каталог "\??" вы увидите, что он буквально кишит символьными ссылками. Такое странное имя этот каталог получил потому, что при сортировке по алфавиту он будет первым, что увеличивает скорость поиска объектов. До Windows NT4 этот каталог назывался "\DosDevices" и являлся наиболее часто используемым при поиске объектов. Поэтому и был переименован в "\??".

Для совместимости с драйверами предыдущих версий Windows, в корневом каталоге пространства имен диспетчера объектов имеется символьная ссылка "\DosDevices", значением которой является строка "\??"

Драйвер VirtToPhys создает символьную ссылку "slVirtToPhys" на свое устройство "devVirtToPhys" в каталоге "\??", значением которой является строка "\Device\devVirtToPhys" (Что, совсем запутались? ;-) ). В имени символьной ссылки я также специально использовал префикс "sl".

Рис. 4-5. Объект "символьная ссылка" slVirtToPhys в пространстве имен диспетчера объектов

Рис. 4-6. Свойства объекта "символьная ссылка" slVirtToPhys

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

Таким образом, на момент выхода из функции StartService мы имеем три объекта: драйвер "\Driver\VirtToPhys", устройство "\Device\devVirtToPhys" и символьную ссылку на устройство "\??\slVirtToPhys".

Если вы еще помните, во второй части, где мы разбирали параметр реестра ImagePath, я обещал рассказать о том, что такое "\??" в самом начале пути "\??\C:\masm32\..." к файлу драйвера. Ну, что такое "\??" вы уже узнали, а вот "\??\C:" это не что иное, как символьная ссылка на устройство "Device\Harddisk\Volume1" или, в моем случае, на первый том первого жесткого диска. При отображении системой образа файла драйвера в память, именно это устройство получит запрос ввода-вывода.

4.2.5 Объект "файл"

Перейдем теперь к разбору исходного кода. При успешном завершении функции StartService мы вызываем стандартную Win32-функцию ввода-вывода CreateFile. Батюшки-святы! А файлы то тут при чем?!... Согласен, пока все кажется довольно запутанным. Надеюсь, скоро многое прояснится.

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

CreateFile proto stdcall     lpFileName:LPCSTR,            dwDesiredAccess:DWORD, \
                             dwShareMode:DWORD,            lpSecurityAttributes:LPVOID, \
                             dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \
                             hTemplateFile:HANDLE

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

Параметр | Описание
lpFileName | - указатель на завершающуюся нулем строку, с именем открываемого устройства, точнее, именем символьной ссылки указывающей на устройство. На эту тему мы поговорим достаточно подробно ниже после описания всех параметров.
dwDesiredAccess | - определяет права доступа к открываемому устройству. Нам потребуются два значения:
 | GENERIC_READ | - доступ на чтение данных с устройства;
 | GENERIC_WRITE | - доступ на запись данных в устройство.
 | Можно использовать и комбинацию этих двух значений.
dwShareMode | - определяет, может ли устройство быть совместо используемым с другим кодом. Т.е. можно ли еще раз получить описатель устройства вызовом функции CreateFile. Нам могут пригодиться три значения:
 | 0 | - в большинстве случаев нужно именно это значение, т.к. обычно устройства выполняющие такие специфические задачи, как наши, открываются для монопольного использования.
 | Если нужно использовать устройство совместно с каким-либо кодом, то используем:
 | FILE_SHARE_READ | - другой код тоже может получить описатель устройства с доступом на чтение;
 | FILE_SHARE_WRITE | - другой код тоже может получить описатель устройства с доступом на запись.
 | Хотя в документации написано именно так, но сколько я не экспериментировал с этим параметром, мне так и не удалось получить монопольный доступ к устройству. Какое бы значения я не указывал, последующий вызов CreateFile все равно возвращал описатель устройства. Не смотря на это, в дальнейшем, мы все равно будем считать, что этот параметр работает так, как написано в документации. Позже, при описании функции IoCreateDevice, я расскажу, как сделать устройство доступным только для монопольного использования.
lpSecurityAttributes | - указатель на структуру SECURITY_ATTRIBUTES. Т.к. никакой особой защиты нам не требуется, а также мы не собираемся наследовать описатель устройства каким-то другим процессам, то просто устанавливаем этот параметр в NULL.
dwCreationDistribution | - определяет действия в случае если устройство с заданным именем не найдено или уже существует. Для устройств используется только значение OPEN_EXISTING. Т.е. на момент вызова CreateFile устройство должно уже существовать.
dwFlagsAndAttributes | - определяет атрибуты и специальные флаги. У нас этот параметр всегда будет равен 0.
hTemplateFile | - описатель файла-прототипа. И этот параметр у нас тоже всегда будет равен NULL.

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

Если вызов функции CreateFile потерпит неудачу, то, в отличие от многих других функций, возвращающих NULL в подобных случаях, функция CreateFile вернет INVALID_HANDLE_VALUE (-1).

С учетом всего вышесказанного, мы вызываем CreateFile следующим образом.

                invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
                                0, NULL, OPEN_EXISTING, 0, NULL

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

А вот с первым параметром, в качестве которого мы передаем указатель на строку "\\.\slVirtToPhys", надо разобраться. Префикс "\\.\" является псевдонимом локального компьютера. Функция CreateFile является оболочкой вокруг другой функции NtCreateFile (реализованной в \%SystemRoot%\System32\ntdll.dll), которая, в свою очередь, обращается к одноименному системному сервису (не путать со службами).

Системный сервис - это функция, вызов которой осуществляется через прерывание int 2Eh (до Windows XP) или команду sysenter (начиная с Windows XP), что влечет за собой переход в режим ядра. Все адреса таких функций сведены в единый массив KiServiceTable (недокументирован и адрес не экспортируется, префикс Ki означает Kernel Internal), доступ к которому можно получить через переменную ядра KeServiceDescriptorTable.

Системные сервисы не имеют никакого прямого отношения к нашей сегодняшней теме. Может быть, в будущем, мы к этому еще и вернемся. Недокументированую и не экспортируемую информацию (имена и адреса функций и переменных, структуры и идентификаторы и т.п.) можно получить при наличии пакета отладочных символов (symbol package), который пока еще - слава Microsoft! - можно скачать бесплатно.

NtCreateFile заменяет псевдоним локального компьютера "\\.\" на имя каталога "\??" в пространстве имен диспетчера объектов (т.о. "\\.\slVirtToPhys" превращается в "\??\slVirtToPhys") и вызывает функцию ядра ObOpenObjectByName. Через символьную ссылку ObOpenObjectByName находит объект "\Device\devVirtToPhys" и возвращает указатель на него (таким образом символьная ссылка, видимая коду режима пользователя, используется диспетчером объектов для трансляции во внутреннее имя устройства). Используя этот указатель NtCreateFile создает новый объект "файл" представляющий устройство и возвращает его описатель.

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

Прежде чем функция CreateFile вернет управление, в драйвере будет вызвана функция которую определяет сам драйвер (точнее, диспетчер ввода-вывода сформирует IRP типа IRP_MJ_CREATE и направит его драйверу обслуживающему наше устройство). При этом, эта процедура будет выполнена в контексте потока вызвавшего CreateFile при IRQL = PASSIVE_LEVEL (помните еще что это такое? ;-) ). Если она (процедура) вернет код успеха, то новый описатель будет создан и возвращен коду вызвавшему CreateFile. Если драйвер вернет код ошибки, то соответственно никаких новых описателей не возникнет.

Созданый CreateFile новый объект "файл" является объектом исполнительной системы (executive) и не попадает в пространство имен диспетчера объектов. Просмотреть описатели принадлежащие процессу можно с помощью утилиты Process Explorer (http://www.sysinternals.com).

Рис. 4-7. Объект "файл"

Рис. 4-8. Свойства объекта "файл"

Чтобы охватить одним взглядом, только что описанный и весьма непростой процесс, позволю себе подытожить. Итак, "\\.\slVirtToPhys" превращается в символьную ссылку "\??\slVirtToPhys", через которую, уже на стороне режима ядра, осуществляется доступ к устройству "\Device\devVirtToPhys". Из структуры объекта "устройство" DEVICE_OBJECT (см. \include\w2k\ntddk.inc) извлекаются сведения об обслуживающем его драйвере. Диспетчер ввода-вывода формирует пакет запроса ввода-вывода и направляет его драйверу. Так драйвер узнает о том, что код режима пользователя пытается получить доступ к его устройству. Если драйвер не имеет ничего против, то он возвращает код успеха, что является сигналом диспетчеру объектов о создании виртуального файла. При этом в таблице описателей (handle table) процесса создается новый элемент с указателем на объект "файл", и коду режима пользователя возвращается новый описатель. Таким образом, символьная ссылка и описатель служат косвенными указателями на системные ресурсы, что позволяет системе оградить прикладные программы от прямого взаимодействия с системными структурами данных.

Я был неприятно удивлен, когда сравнил значения счетчиков ссылок и открытых описателей, которые показывает Process Explorer v5.25 в свойствах описателя, с реальными значениями. Мы уже немного касались темы указателей и описателей объекта в прошлой статье. Описатель является косвенной ссылкой на объект, предоставляемой ядром коду режима пользователя, а указателями пользуется ядро. Оказалось, что значение счетчика ссылок References постоянно завышается на единицу. Поставив прерывание на область памяти содержащую счетчик ссылок я обнаружил, что при просмотре свойств описателя, значение счетчика динамически повышается на несколько единиц и тут же понижается до прежнего значения, но на вкладке свойств все равно отображается неверное - на единицу больше - значение. Так что в действительности значение References на рис. 4-8 равно 1, а не 2. И на старуху бывает проруха. Реальное значение счетчиков можно посмотреть в SoftICE. Для этого нужно вывести список описателей процесса командой proc -o VirtToPhys. В столбце Handle найти нужный описатель и взяв из столбца Ob Hdr * указатель на заголовок объекта (структура OBJECT_HEADER) вывести в окно дампа содержимое заголовка dd <адрес заголовка>. Первый DWORD - счетчик ссылок, второй - описателей.

4.2.6 Управление виртуальным устройством

                .if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax

Если все ОК, то мы сохраняем описатель устройства, возвращенный CreateFile, в переменной hDevice, и имеем возможность осуществлять операции ввода-вывода с этим устройством, посредством вызова функций DeviceIoControl, ReadFile и WriteFile. DeviceIoControl является универсальной функцией ввода-вывода - ее мы и будем использовать.

DeviceIoControl proto stdcall hDevice:HANDLE,         dwIoControlCode:DWORD, \
                              lpInBuffer:LPVOID,      nInBufferSize:DWORD, \
                              lpOutBuffer:LPVOID,     nOutBufferSize:DWORD, \
                              lpBytesReturned:LPVOID, lpOverlapped:LPVOID

Не смотря на то, что функция DeviceIoControl принимает даже больше параметров чем CreateFile, тут все достаточно просто.

Параметр | Описание
hDevice | - описатель устройства, которому адресуется запрос ввода-вывода;
dwIoControlCode | - управляющий код ввода-вывода; Подробнее об этом параметре ниже.
lpInBuffer | - указатель на буфер с входными (относительно устройства) данными или NULL, если для выполнения операции никаких дополнительных данных устройству не требуется;
nInBufferSize | - размер входного буфера в байтах;
lpOutBuffer | - указатель на буфер с выходными (относительно устройства) данными или NULL, если устройство не возвращает никаких дополнительных данных;
nOutBufferSize | - размер выходного буфера в байтах;
lpBytesReturned | - количество байт, скопированных в выходной буфер при выходе из DeviceIoControl;
lpOverlapped | - указатель на структуру OVERLAPPED. Т.к. мы собираемся осуществлять только синхронный ввод-вывод (процедура DeviceIoControl не вернет управление до тех пор, пока не отработает вызванная в драйвере процедура), то этот параметр у нас всегда будет равен NULL.

4.2.7 Управляющий код ввода-вывода

Я уже говорил, что код драйвера, по сути, является набором функций, позволяющих решать задачи недоступные коду режима пользователя. Поскольку драйвер устройства может выполнять массу таких задач, необходимо как-то дифференцировать запросы. Для этого и предназначен второй параметр dwIoControlCode, называемый управляющим кодом ввода-вывода (I/O control code), который строится по определенным правилам.

Рис. 4-9. Поля управляющего кода ввода-вывода

Поле | Описание
DeviceType | - идентификатор типа устройства (16 бит). Может принимать значение в диапазоне 0-0FFFFh, который разбит на две равные половины. Диапазон 0-7FFFh зарезервирован Microsoft, а диапазон 8000h-0FFFFh доступен всем желающим. В файле \include\w2k\ntddk.inc можно найти набор констант FILE_DEVICE_* со значениями из зарезервированного диапазона. Мы будем использовать FILE_DEVICE_UNKNOWN. Можно определить и свой собственный идентификатор.
Access | - запрашиваемые права доступа к устройству. Т.к. это поле размером 2 бита, то возможны четыре значения:
 | FILE_ANY_ACCESS (0) | - максимальные права доступа;
 | FILE_READ_ACCESS (1) | - доступ на чтение, т.е. получение данных от устройства;
 | FILE_WRITE_ACCESS (2) | - доступ на запись, т.е. передачу данных на устройство;
 | FILE_READ_ACCESS or FILE_WRITE_ACCESS (3) | - комбинация последних двух значений.
Function | - собственно и определяет операцию, которую должно выполнить устройство (12 бит). Может принимать значение в диапазоне 0-FFFh, который также разбит на две равные половины. Диапазон 0-7FFh зарезервирован Microsoft, а диапазон 800h-0FFFh доступен.
Method | - определяет метод ввода-вывода. Размер поля 2 бита - 4 значения, и все они могут быть использованы для наших целей:
 | METHOD_BUFFERED (0) | - буферизованный ввод-вывод (buffered I/O);
 | METHOD_IN_DIRECT (1) | - прямой ввод-вывод (direct I/O);
 | METHOD_OUT_DIRECT (2)
 | METHOD_NEITHER (3) | - ввод-вывод без управления (neither I/O).

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

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

CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0>
    EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method))
ENDM

Этот макрос определен дважды - в файле \include\winioctl.inc (отсутствует в пакете masm32), который необходимо включить в исходный текст программы управления, и в файле ntddk.inc, включаемом в исходный текст драйвера.

Определение значения управляющего кода ввода-вывода и двух констант вынесено в отдельный файл common.inc, т.к. эти значения требуются как в исходном тексте программы управления, так и в исходном тексте драйвера. Т.о. все изменения в этом файле отразятся на исходных текстах программы управления и драйвера. Содержимое файла common.inc выглядит так:

NUM_DATA_ENTRY         equ 4
DATA_SIZE              equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)

4.2.8 Обмен данными с устройством

Вернемся к исходному тексту.

                    lea esi, adwInBuffer
                    assume esi:ptr DWORD
                    invoke GetModuleHandle, NULL
                    mov [esi][0*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
                    mov [esi][1*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
                    mov [esi][2*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
                    mov [esi][3*(sizeof DWORD)], eax

Подготавливаем буфер adwInBuffer для передачи устройству. Буфер содержит виртуальные базовые адреса загрузки самой программы управления VirtToPhys.exe, и трех системных модулей отображенных на адресное пространство нашего процесса.

                    lea edi, adwOutBuffer
                    assume edi:ptr DWORD
                    invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
                                            esi, sizeof adwInBuffer, \
                                            edi, sizeof adwOutBuffer, \
                                            addr dwBytesReturned, NULL

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

Используя описатель устройства диспетчер ввода-вывода извлечет сведения об обслуживающем его драйвере, сформирует пакет запроса ввода-вывода типа IRP_MJ_DEVICE_CONTROL и направит его драйверу. В драйвере будет вызвана соответствующая процедура. Эта процедура также будет выполнена в контексте потока вызвавшего DeviceIoControl при IRQL = PASSIVE_LEVEL. По значению управляющего кода ввода-вывода IOCTL_GET_PHYS_ADDRESS драйвер определит что конкретно от него требуется, извлечет из входного буфера четыре виртуальных адреса, преобразует их в физические и поместит в выходной буфер.

                    .if ( eax != 0 ) && ( dwBytesReturned != 0 )

                        invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
                                                  addr acModulePath, sizeof acModulePath

                        lea ecx, acModulePath[eax-5]
                        .repeat
                            dec ecx
                            mov al, [ecx]
                        .until al == '\'
                        inc ecx
                        push ecx

                        CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod

                        invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
                        pop ecx
                        invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
                                         [esi][0*(sizeof DWORD)], \
                                         [edi][0*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
                                         [esi][1*(sizeof DWORD)], \
                                         [edi][1*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
                                         [esi][2*(sizeof DWORD)], \
                                         [edi][2*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
                                         [esi][3*(sizeof DWORD)], \
                                         [edi][3*(sizeof DWORD)], addr acNumber

                        invoke wsprintf, addr acBuffer, \
                                         $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
                                         addr acThis, addr acKernel, addr acUser, addr acAdvapi

                        assume esi:nothing
                        assume edi:nothing
                        invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \
                                           MB_OK + MB_ICONINFORMATION
                    .else
                        invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
                                           MB_OK + MB_ICONSTOP
                    .endif

Если все пройдет удачно, то DeviceIoControl вернет ненулевое значение, а в dwBytesReturned окажется количество байт возвращенных устройством в буфере adwOutBuffer. Теперь наша задача - отформатировать эти данные и вывести их на экран. Я не буду подробно описывать этот процесс - тут все достаточно тривиально. В текстовых макросах $CTA0 интенсивно используются стандартные эскейп-последовательности (подробное описание см. \Macros\Strings.mac). Процедура BigNumToString разбивает адрес по три разряда. Закончив фарматирование, выводим информацию на экран.

Рис. 4-10. Результат работы программы VirtToPhys.exe

4.2.9 Очистка ресурсов

                    invoke CloseHandle, hDevice

Как и полагается поступать с описателями которые больше не нужны, вызовом функции CloseHandle, закрываем описатель устройства. И все повторяется вновь. Используя описатель устройства диспетчер ввода-вывода извлекает указатель на драйвер, формирует пакет запроса ввода-вывода типа IRP_MJ_CLEANUP и направляет его драйверу. В драйвере будет вызвана процедура зарегистрированная на обработку этого типа IRP, если это необходимо драйверу. Эта процедура также будет выполнена в контексте потока вызвавшего CloseHandle при IRQL = PASSIVE_LEVEL. Таким образом драйвер узнает, что описатель его устройства собираются закрыть. Затем счетчик открытых описателей объекта "файл" уменьшается на единицу и становится равным нулю. Счетчик ссылок также уменьшается до нуля, что приводит к удалению объекта из памяти. Кстати, значение счетчика ссылок всегда больше или равно значению счетчика открытых описателей, т.к. каждому описателю соответствует, по крайней мере одна, ссылка. Когда счетчик ссылок уменьшается до нуля диспетчер ввода-вывода формирует пакет запроса ввода-вывода типа IRP_MJ_CLOSE и направляет его драйверу. В драйвере будет вызвана процедура зарегистрированная на обработку этого типа IRP. И опять эта процедура будет выполнена в контексте потока вызвавшего CloseHandle при IRQL = PASSIVE_LEVEL. Таким образом драйвер узнает, что описатель его устройства уже закрыт.

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

Останов и удаление сведений о драйвере из базы данных SCM происходит обычным образом и подробно описано в предыдущих статьях.

В исходном коде VirtToPhys.exe я использовал ANSI версии API функций, что не совсем правильно, т.к. Windows NT по своей природе использует UNICODE. Все ANSI функций являются просто оболочками вокруг своих UNICODE-аналогов. Стандартные включаемые файлы пакета masm32 не поддерживают UNICODE. Поэтому, я и не стал усложнять и без того не простой для начинающих код. С помощью утилиты masm32\bin\l2incu.exe можно самостоятельно изготовить UNICODE-включаемые файлы.

И последнее. Я тестировал VirtToPhys, как обычно, под Windows 2000 Pro и Windows XP Pro. На более ранних выпусках могут возникнуть проблемы из-за того, что в пространстве имен диспетчера объектов отсутствует каталог "\??". В этом случае вам придется перекомпилировать драйвер заменив "\??" на "\DosDevices".


Полнофункциональный драйвер

5.1 Исходный код полнофункционального драйвера
5.2 Имя устройства и символьной ссылки
5.3 Процедура инициализации драйвера
5.3.1 Создание виртуального устройства
5.3.2 Создание символьной ссылки
5.3.3 Заполнение массива MajorFunction
5.3.4 Очистка ресурсов
5.3.5 Объекты создаваемые в процедуре DriverEntry и их взаимосвязи
5.4 Процедуры диспетчеризации запросов ввода-вывода
5.5 Процедура обработки IRP типов IRP_MJ_CREATE и IRP_MJ_CLOSE
5.6 Соглашение о передаче параметров
5.7 Виды управления буферами при обработке IRP
5.7.1 буферизованный ввод-вывод
5.7.2 прямой ввод-вывод
5.7.3 ввод-вывод без управления
5.8 Процедура обработки IRP типа IRP_MJ_DEVICE_CONTROL
5.9 Процедура трансляции адресов
5.10 Процедура выгрузки драйвера
5.11 Компиляция драйвера
5.12 Добавляем ресурсы
5.13 Еще немного об отладке
Исходный текст: KmdKit\examples\simple\VirtToPhys

5.1 Исходный код полнофункционального драйвера

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

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; VirtToPhys - Драйвер режима ядра
;
;  Переводит виртуальный адрес в физический
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\w2kundoc.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

include ..\common.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.const
CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                    GetPhysicalAddress
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

GetPhysicalAddress proc dwAddress:DWORD

    mov eax, dwAddress
    mov ecx, eax

    shr eax, 22
    shl eax, 2

    mov eax, [0C0300000h][eax]

    .if ( eax & (mask pde4kValid) )
        .if !( eax & (mask pde4kLargePage) )
            mov eax, ecx
            shr eax, 10
            and eax, 1111111111111111111100y
            add eax, 0C0000000h
            mov eax, [eax]

            .if eax & (mask pteValid)
                and eax, mask ptePageFrameNumber

                and ecx, 00000000000000000000111111111111y
                add eax, ecx
            .else
                xor eax, eax
            .endif
        .else
            and eax, mask pde4mPageFrameNumber
            and ecx, 00000000001111111111111111111111y
            add eax, ecx
        .endif
    .else
        xor eax, eax
    .endif

    ret

GetPhysicalAddress endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                   DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    assume eax:ptr _IRP
    mov [eax].IoStatus.Status, STATUS_SUCCESS
    and [eax].IoStatus.Information, 0
    assume eax:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchControl proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

local status:NTSTATUS
local dwBytesReturned:DWORD

    and dwBytesReturned, 0

    mov esi, pIrp
    assume esi:ptr _IRP

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS

        mov status, STATUS_BUFFER_TOO_SMALL
        .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE )
        .if ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )

            mov edi, [esi].AssociatedIrp.SystemBuffer
            assume edi:ptr DWORD

            xor ebx, ebx
            .while ebx < NUM_DATA_ENTRY

                invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]

                mov [edi][ebx*(sizeof DWORD)], eax
                inc ebx
            .endw

            mov dwBytesReturned, DATA_SIZE
            mov status, STATUS_SUCCESS

        .endif
        .endif

    .else
        mov status, STATUS_INVALID_DEVICE_REQUEST
    .endif

    assume edi:nothing

    push status
    pop [esi].IoStatus.Status

    push dwBytesReturned
    pop [esi].IoStatus.Information

    assume esi:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, status
    ret

DispatchControl endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverUnload proc pDriverObject:PDRIVER_OBJECT

        invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

        mov eax, pDriverObject
        invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

    ret

DriverUnload endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code INIT

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local pDeviceObject:PVOID

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                             0, FALSE, addr pDeviceObject

    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:PTR DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],            offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],   offset DispatchControl
            mov [eax].DriverUnload,                                          offset DriverUnload
            assume eax:nothing
            mov status, STATUS_SUCCESS
        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

    mov eax, status
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=VirtToPhys

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

5.2 Имя устройства и символьной ссылки

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

.const
uszDeviceName          dw "\", "D", "e", "v", "i", "c", "e", "\", "D", "e", "v", "N", "a", "m", "e", 0
uszSymbolicLinkName    dw "\", "?", "?", "\", "D", "e", "v", "N", "a", "m", "e", 0

.code
DriverEntry proc . . .
. . .
local usDeviceName:UNICODE_STRING
local usSymbolicLinkName:UNICODE_STRING
. . .
    invoke RtlInitUnicodeString, addr usDeviceName, offset uszDeviceName
    invoke RtlInitUnicodeString, addr usSymbolicLinkName, offset uszSymbolicLinkName

Задача функции RtlInitUnicodeString измерить unicode-строку и заполнить структуру UNICODE_STRING (см. Часть 3). Т.к. сами unicode-строки в этом коде определены статически, т.е. никогда не меняются, то можно, еще на этапе компиляции, заполнить структуру UNICODE_STRING. Это проще, нагляднее и требует меньшего количества байт (8 байт на структуру UNICODE_STRING + максимум 3 байта на выравнивание. Против минимум 14 байт на вызов функции RtlInitUnicodeString + временные издержки). Именно так и поступим, а макрос CCOUNTED_UNICODE_STRING еще больше облегчит нам жизнь, и весь вышеприведенный код (мы добавим еще выравнивание) превратится в две элегантные строки.

CCOUNTED_UNICODE_STRING "\\Device\\DevName", usDeviceName,       4
CCOUNTED_UNICODE_STRING "\\??\\DevName",     usSymbolicLinkName, 4

Не знаю как вам, а мне этот вариант правится значительно больше. Функция же RtlInitUnicodeString требуется когда длина строки заранее неизвестна. В ntoskrnl.exe, кстати, имеется целый набор функций, как для заполнения структур *_STRING, так и для работы со строками вообще.

.const
CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4

Таким образом, мы имеем две глобальные переменные g_usDeviceName и g_usSymbolicLinkName типа UNICODE_STRING с именем устройства и именем символьной ссылки соответственно. Как я уже говорил в прошлый раз, префиксы "dev" и "sl" я добавил только для наглядности.

На более ранних выпусках Windows NT в пространстве имен диспетчера объектов отсутствует каталог "\??". В этом случае надо заменить в имени символьной ссылки "\??" на "\DosDevices". Причем, в более поздних выпусках это тоже будет работать, т.к. для обратной совместимости в корневом каталоге пространства имен диспетчера объектов имеется символьная ссылка "\DosDevices", значением которой является строка "\??"

5.3 Процедура инициализации драйвера

Кое-что о процедуре DriverEntry мы уже узнали в третьей части. Прежде чем мы пойдем дальше, обратите внимание на строку:

.code INIT

Весь код, помеченный таким образом, помещается компоновщиком в отдельную секцию PE-файла с атрибутом Discardable. Благодаря этому, система автоматически выгрузит такой код при необходимости. Это позволяет экономно использовать ресурсы системной памяти. Весь код в процедуре DriverEntry нужен только один раз - во время инициализации драйвера. После этого он будет бесполезно занимать память. Но, так как он помещен в секцию "INIT", этого не произойдет. Но только в том случае, если кроме секции INIT на странице (страницах) больше ничего нет, т.к. частично выгружать страницы система не умеет. О секции "INIT" мы уже говорили в третьей части при описании ключей компоновщика.

Поскольку, процедура DriverEntry просто крошечная, и весь остальной код и данные тоже занимают очень мало места, а в параметре компоновщика /align:32 мы определили выравнивание секций по границе 32 байта, то весь драйвер, со всеми своими потрохами, умещается всего на одной странице памяти (4кБ). А как известно, минимальной единицей выделения памяти, на данный момент, является страница. Т.е. можно было и не использовать отдельную секцию, т.к. в данном случае это ничего не дает. В драйверах для более ранних выпусков Windows NT процедура DriverEntry занимала довольно приличное место, поэтому собственно Microsoft так и поступила.

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

Предположим, что во время инициализации драйвера произойдет ошибка. Если это случится, то значение STATUS_DEVICE_CONFIGURATION_ERROR и вернется системе, и на стороне режима пользователя вызов функции StartService завершится ошибкой.

5.3.1 Создание виртуального устройства

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                             0, FALSE, addr pDeviceObject

Поскольку, драйвер призван управлять виртуальным устройством, то первым делом мы и пытаемся это устройство создать. Функция IoCreateDevice выделяет память под необходимые структуры, в частности DEVICE_OBJECT, и инициализирует их. Ее прототип выглядит следующим образом:

IoCreateDevice proto stdcall DriverObject:PDRIVER_OBJECT, DeviceExtensionSize:DWORD, \
                             DeviceName:PUNICODE_STRING,  DeviceType:DEVICE_TYPE, \
                             DeviceCharacteristics:DWORD, Exclusive:BOOL, \
                             DeviceObject: PDEVICE_OBJECT
Параметр Описание

DriverObject

- указатель на объект "драйвер" (структура DRIVER_OBJECT), созданный системой;

DeviceExtensionSize

- произвольный размер области дополнительной памяти устройства (device extension), которую можно выделить в каждом объекте "устройство".

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

DeviceName

- необязательный указатель на имя устройства в формате UNICODE_STRING.

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

DeviceType

- определяет уникальный идентификатор типа устройства.

Этого момента мы коснулись в прошлый раз, при описании управляющего кода ввода-вывода. У нас этот параметр всегда будет FILE_DEVICE_UNKNOWN;

DeviceCharacteristics

- дополнительная информация об устройстве.

У нас ничего такого нет, поэтому ставим в 0;

Exclusive

- определяет монопольный доступ к устройству.

Опять же в прошлый раз, при описании функции CreateFile, я говорил, что мне не удалось получить монопольный доступ к устройству используя параметр dwShareMode. Это можно сделать с помощью Exclusive. Трудно сказать нужно нам это или нет, поэтому, мы разрешаем одновременное использование устройства несколькими приложениями устанавливая этот параметр в FALSE;

DeviceObject

- этот параметр является возвращаемым и будет указывать, при успешном завершении функции IoCreateDevice, на высокопарно называемый Microsoft объект "устройство", являющийся на самом деле структурой DEVICE_OBJECT. Через эту структуру система и будет управлять устройством.

Если создать символьную ссылку не удастся, то указатель на объект "устройство" потребуется для его удаления. Поэтому, мы передаем в этом параметре адрес локальной переменной pDeviceObject. Можно сохранить указатель на объект "устройство" и в глобальной переменной и использовать его в процедуре выгрузки драйвера, но я этого делать не стал, чтобы не создавать лишнюю секцию ?data. А при выгрузке драйвера мы извлечем этот указатель прямо из объекта "драйвер".

5.3.2 Создание символьной ссылки

    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName

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

5.3.3 Заполнение массива MajorFunction

        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:PTR DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],            offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],   offset DispatchControl

Если создание символьной ссылки прошло успешно, переходим к следующему этапу.

В состав структуры DRIVER_OBJECT входит массив MajorFunction. В этот масив помещаются указатели на процедуры диспетчеризации (dispatch routines), предназначенные для обработки разных типов пакетов запроса ввода-вывода (IRP). Каждый элемент этого массива соответствует своему типу IRP. Если, например, драйверу необходимо обрабатывать запрос типа IRP_MJ_SHUTDOWN, уведомляющий о завершении работы системы, то он должен поместить в соответствующую позицию массива MajorFunction указатель на функцию, которой запросы этого типа и будут направляться. Если такая функциональность драйверу не нужна, как в нашем случае, то и заполнять этот элемент массива MajorFunction не требуется. Т.о. мы совсем не обязаны заполнять все элементы массива MajorFunction, коих в DDK определено аж целых 28 штук (IRP_MJ_MAXIMUM_FUNCTION+1). Все зависит от задач стоящих перед драйвером. Например, драйвер beeper.sys и так прекрасно справлялся со своей работой, вообще не устанавливая никаких процедур диспетчеризации. В элементы массива MajorFunction не заполненные драйвером диспетчер ввода-вывода заносит указатель на внутреннюю функцию IopInvalidDeviceRequest. Эта функция уведомляет о попытке обращения к неподдерживаемой данным драйвером функции.

Для того, чтобы код режима пользователя смог успешно вызывать функции CreateFile, DeviceIoControl и CloseHandle, мы должны обрабатывать как минимум три типа запросов:

Тип запроса | Описание
IRP_MJ_CREATE | - формируется диспетчером ввода-вывода при вызове кодом режима пользователя функции CreateFile;
IRP_MJ_DEVICE_CONTROL | - формируется диспетчером ввода-вывода при вызове кодом режима пользователя функции DeviceIoControl;
IRP_MJ_CLOSE | - формируется диспетчером ввода-вывода при вызове кодом режима пользователя функции CloseHandle.

Запросы типов IRP_MJ_CREATE и IRP_MJ_CLOSE будет обрабатывать одна функция DispatchCreateClose. Почему узнаем позже. В ntddk.inc, среди прочих, определены константы могущие представлять для нас интерес:

IRP_MJ_CREATE               equ 0
. . .
IRP_MJ_CLOSE                equ 2
IRP_MJ_READ                 equ 3
IRP_MJ_WRITE                equ 4
. . .
IRP_MJ_DEVICE_CONTROL       equ 0Eh
. . .
IRP_MJ_CLEANUP              equ 12h

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

            mov [eax].DriverUnload,                                          offset DriverUnload

Еще одно ключевое поле структуры DRIVER_OBJECT - это DriverUnload. Туда мы должны поместить, если хотим иметь возможность динамически выгружать драйвер, указатель на процедуру, которую система будет вызывать при обращении кода режима пользователя к функции ControlService с параметром SERVICE_CONTROL_STOP.

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

            assume eax:nothing
            mov status, STATUS_SUCCESS

Если мы благополучно дошли до этой точки, то инициализация драйвера прошла успешно, о чем мы и сообщаем системе, возвращая код успеха STATUS_SUCCESS.

5.3.4 Очистка ресурсов

        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

Если же создать символьную ссылку не удалось, то мы должны удалить объект "устройство", созданный на предыдущем этапе функцией IoCreateDevice, чтобы привести систему в прежнее состояние. Именно должны! Это вам не режим пользователя, где после завершения процесса все объекты ему принадлежащие автоматически уничтожаются системой. В режиме ядра никто за нас делать грязную работу не будет. Это общее правило. Выделили память - освободите. Создали объект - удалите. Получили ссылку на объект - "отдайте" назад. И т.д. и т.п. Запомните это.

    mov eax, status
    ret

Возвращаем системе текущий код состояния. Если это STATUS_SUCCESS, то драйвер остается в памяти и диспетчер ввода-вывода будет формировать и направлять его устройствам пакеты запроса ввода-вывода. Если это какое-либо другое значение, то драйвер удаляется.

5.3.5 Объекты создаваемые в процедуре DriverEntry и их взаимосвязи

Таким образом, после удачного завершения DriverEntry в системе появится три новых объекта:

  • Объект "драйвер", представляющий отдельный драйвер в системе.

    • Из этого объекта (точнее из массива MajorFunction структуры DRIVER_OBJECT) диспетчер ввода-вывода получает адреса процедур диспетчеризации;

  • Объект "устройство", представляющий в системе отдельное устройство.

    • Из этого объекта (точнее из поля DriverObject структуры DEVICE_OBJECT) диспетчер ввода-вывода получает указатель на объект "драйвер" это устройство обслуживающий;

  • Объект "файл", представляющий для кода режима пользователя объект "устройство".

    • Из этого объекта (точнее из поля DeviceObject структуры FILE_OBJECT) диспетчер ввода-вывода получает указатель на объект "устройство";

    Этот объект прозрачен для нас. О том, как объект "файл" возникает, мы говорили в прошлый раз;

  • Объект "символьная ссылка", видимая коду режима пользователя.

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

На рис. 5-1 показаны основные взаимосвязи этих объектов. Эта схема поможет вам глубже понять дальнейший материал.

Рис. 5-1. Взаимосвязи объектов "файл", "устройство" и "драйвер" с пакетом запроса ввода-вывода, и кодом драйвера

5.4 Процедуры диспетчеризации запросов ввода-вывода

Все процедуры диспетчеризации имеют одинаковый прототип.

DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

Параметр | Описание
pDeviceObject | - указатель на объект "устройство" (структура DEVICE_OBJECT). Если драйвер обслуживает несколько устройств, то по значению этого параметра можно определить, к какому именно устройству пришел запрос. Или обратиться к области дополнительной памяти устройства. В нашем случае, в процедурах диспетчеризации, использовать указатель на объект "устройство" не требуется;
pIrp | - указатель на текущий пакет запроса ввода-вывода (структура _IRP). Диспетчер ввода-вывода создает IRP, представляющий операцию ввода-вывода. Через параметр pIrp драйверу передается указатель на IRP. Получив IRP, драйвер выполняет указанную в пакете операцию и возвращает его диспетчеру ввода-вывода, чтобы тот, либо завершил эту операцию, либо передал пакет другому драйверу для дальнейшей обработки. Завершать IRP или передавать его дальше решает сам драйвер. Наш драйвер одноуровневый, поэтому никакой дальнейшей обработки не требуется. По завершении операции ввода-вывода, результаты возвратятся коду режима пользователя, а IRP будет удален.

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

5.5 Процедура обработки IRP типов IRP_MJ_CREATE и IRP_MJ_CLOSE

Для начала о том, почему процедура DispatchCreateClose обрабатывает у нас запросы такого разного типа. Запросы типов IRP_MJ_CREATE и IRP_MJ_CLOSE направляются драйверу, когда код режима пользователя вызывает функции CreateFile и CloseHandle, соответственно. Единственное, что нам надо сделать для обработки обоих запросов, это заполнить блок статуса ввода-вывода (I/O status block) - вложенную в _IRP структуру IO_STATUS_BLOCK - и вернуть код успеха. Поэтому, чтобы не плодить лишние процедуры-близнецы, я их и объединил.

Если быть совсем точным, то при вызове CloseHandle сначала приходит запрос типа IRP_MJ_CLEAUP, но его обработка нам не требуется, а затем IRP_MJ_CLOSE.

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

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    assume eax:ptr _IRP
    mov [eax].IoStatus.Status, STATUS_SUCCESS
    and [eax].IoStatus.Information, 0
    assume eax:nothing

Заполняем блок статуса ввода-вывода, определяющий состояние запроса ввода-вывода.

Значение поля Status определяет, как завершится на стороне режима пользователя вызов функций CreateFile и CloseHandle. Мы хотим, чтобы пользовательский процесс открывал и закрывал описатель устройства без ошибок. Поэтому, помещаем в поле Status код успеха STATUS_SUCCESS.

Смысл поля Information зависит от типа запроса. Это может быть значение количества байт переданных в пользовательское приложение или указатель на структуру. В данном случае мы поместим в это поле 0. При обработке запроса типа IRP_MJ_DEVICE_CONTROL (в процедуре DispatchControl), мы должны будем поместить туда количество байт, которое необходимо скопировать в пользовательский буфер.

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

Вызов функции IofCompleteRequest инициирует операцию завершения ввода-вывода (I/O completion).

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

Если драйвер обслуживает физическое устройство, то операция ввода-вывода может длиться ощутимое время. Пока поток ждет завершения операции, система не предоставляет ему процессорное время (при синхронных операциях ввода-вывода, как в нашем случае). После окончания операции ожидавший поток вправе немедленно возобновить выполнение и обработать полученные данные. Именно через второй параметр функции IofCompleteRequest драйвер и сообщает системе на какую величину повысить приоритет ожидавшего потока. Для устройств "тугодумов" предусмотрены бОльшие значения повышения приоритета. Например, для звуковых карт DDK рекомендует использовать константу IO_SOUND_INCREMENT, равную 8. В нашем случае, повышения приоритета потока не требуется, и мы передаем IO_NO_INCREMENT равную нулю.

Функция IofCompleteRequest является fastcall-функцией (см. ниже). В префиксе имени функции присутствует символ 'f'. Существует, кстати сказать, и stdcall вариант - IoCompleteRequest. Обратите внимание на отсутствие символа 'f' в префиксе. Но, в образовательных целях, мы будем использовать быструю версию. Эта не единственная fastcall-функция - есть и другие. И у них также есть свои stdcall аналоги, которые, как правило, являются оболочками вокруг соответствующих fastcall-функций.

5.6 Соглашение о передаче параметров

В функциях API ядра Windows NT используется три типа соглашений о передаче параметров (calling convention): stdcall, cdecl и fastcall. Последний тип не поддерживается компилятором masm.

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

Декорированные имена функций fastcall начинаются с символа "@", после имени добавляется символ "@" с десятичным числом, обозначающим суммарный размер аргументов в байтах. Например, функция IofCompleteRequest декорируется следующим образом:

@IofCompleteRequest@8

Это значит, что это fastcall-функция, экспортируемое имя IofCompleteRequest, принимает два аргумента размером DWORD каждый. В файле \include\w2k\ntoskrnl.inc она определена следующим образом (на ключевое слово SYSCALL можете не обращать внимание):

EXTERNDEF SYSCALL @IofCompleteRequest@8:PROC
IofCompleteRequest TEXTEQU <@IofCompleteRequest@8>

Для упрощения вызова таких функций я написал макрос fastcall:

fastcall MACRO api:REQ, p1, p2, px:VARARG

local arg

    ifnb <px>
        % for arg, @ArgRev( <px> )
            push arg
        endm
    endif

    ifnb <p1>

        ifdifi <p1>, <ecx>
            mov ecx, p1
        endif

        ifnb <p2>
            ifdifi <p2>, <edx>
                mov edx, p2
            endif
        endif

    endif

    call api

ENDM

Здесь, для экономии места, я привожу упрощенную версию этого макроса. Чтобы не плодить лишние файлы с макросами, мне пришлось поместить этот макрос в \include\w2k\ntddk.inc, где и можно посмотреть полный вариант. В оригинальном ntddk.h такого макроса, естественно, нет.

5.7 Виды управления буферами при обработке IRP

Диспетчер ввода-вывода поддерживает три вида управления буферами:

  • буферизованный ввод-вывод (buffered I/O);

  • прямой ввод-вывод (direct I/O);

  • ввод-вывод без управления (neither I/O).

Этого момента мы чуть-чуть касались в прошлой статье. Теперь поговорим более обстоятельно. Здесь, мы будем рассматривать только случай применения функции DeviceIoControl для организации ввода-вывода. Использование ReadFile и WriteFile не рассматривается. Пример использования ReadFile смотрите в \src\Article4-5\NtBuild.

5.7.1 буферизованный ввод-вывод

Именно этот вид управления используется в драйвере VirtToPhys.

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

Создавая IRP, при операции записи, диспетчер ввода-вывода копирует данные из пользовательского входного буфера в выделенный системный буфер и передает драйверу его адрес в поле AssociatedIrp.SystemBuffer структуры _IRP. Размер скопированных данных помещается в поле Parameters.DeviceIoControl.InputBufferLength структуры IO_STACK_LOCATION (на эту структуру указывает поле Tail.Overlay.CurrentStackLocation структуры _IRP, значение которого можно получить используя макрос IoGetCurrentIrpStackLocation).

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

Завершая IRP, при операции чтения, диспетчер ввода-вывода копирует данные из системного буфера в пользовательский выходной буфер. Размер копируемых данных извлекается из поля IoStatus.Information структуры _IRP. Это поле должен заполнить драйвет - только он знает сколько байт поместил в системный буфер.

Таким образом, диспетчер ввода-вывода дважды копирует буферы. Именно поэтому, этот вид управления самый медленный, но при сравнительно малых размерах буферов издержки незначительны.

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

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

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

Таким образом, этот вид управления буферами следует использовать при относительно небольших размерах буферов. Или если опереция ввода-вывода выполняется не часто.

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

5.7.2 прямой ввод-вывод

Используется для организации прямого доступа к памяти (direct memory access, DMA).

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

Диспетчер ввода-вывода блокирует выходной буфер в памяти, делая его неподкачиваемым, и создает MDL (Memory Descriptor List), описывающий физические страницы занимаемые буфером. Указатель на MDL помещается в поле MdlAddress структуры _IRP. Подробнее см. DDK.

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

5.7.3 ввод-вывод без управления

Диспетчер ввода-вывода помещает в поле DeviceIoControl.Type3InputBuffer структуры IO_STACK_LOCATION указатель на пользовательский входной буфер, а в поле UserBuffer структуры _IRP указатель на пользовательский выходной буфер и оставляет драйверу возможность управлять ими самостоятельно. Т.о. вся ответственность за управление пользовательскими буферами ложится на драйвер. Он может блокировать их в памяти, отображать на системное адресное пространство или обращаться к ним напрямую и т.д.

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

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

Остается одна проблема - пользовательский поток может передать заведомо неверный адрес буфера, например, попадающий в системное адресное пространство или адрес невыделенной области памяти и т.п. Или пользовательский процесс может быть многопоточным, и один из потоков может освободить память занимаемую буфером во время обработки драйвером запроса ввода-вывода. Такие ситуации надо просто предвидеть и корректно обрабатывать. Использование структурной обработки исключений (Structured Exception Handling, SEH) при этом обязательно (пример см. \src\Article4-5\NtBuild). Учтите только, что хотя структурная обработка исключений в режиме ядра принципиально ничем не отличается от таковой в режиме пользователя, обрабатывать таким образом все исключения не удастся. Например, попытка деления на ноль приведет к появлению BSOD даже при установленном обработчике SEH.

5.8 Процедура обработки IRP типа IRP_MJ_DEVICE_CONTROL

Запрос типа IRP_MJ_DEVICE_CONTROL направляется драйверу, когда код режима пользователя вызывает функцию DeviceIoControl.

    and dwBytesReturned, 0

Если произойдет ошибка, то диспетчер ввода-вывода не должен ничего копировать в пользовательский буфер.

    mov esi, pIrp
    assume esi:ptr _IRP

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

Макрос IoGetCurrentIrpStackLocation извлекает из структуры _IRP указатель на текущий блок стека ввода-вывода (I/O stack). Это структура IO_STACK_LOCATION. Таких блоков в пакете запроса ввода-вывода может быть несколько, в зависимости от того, сколько драйверов обслуживают запрос. У нас драйвер один и блок стека тоже будет один.

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS

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

Определение значения управляющего кода ввода-вывода IOCTL_GET_PHYS_ADDRESS и констант NUM_DATA_ENTRY и DATA_SIZE вынесено в отдельный файл common.inc

NUM_DATA_ENTRY         equ 4
DATA_SIZE              equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)

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

        .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )

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

Поля OutputBufferLength и InputBufferLength вложенной в IO_STACK_LOCATION структуры DeviceIoControl соответствуют параметрам nOutBufferSize и nInBufferSize функции режима пользователя DeviceIoControl.

            mov edi, [esi].AssociatedIrp.SystemBuffer

Извлекаем указатель на системный буфер. Т.к., в определении управляющего кода ввода-вывода, мы указали, в качестве вида управления буферами, значение METHOD_BUFFERED, то формируя IRP диспетчер ввода-вывода выделяет в неподкачиваемом системном пуле блок памяти, равный по размеру наибольшему из буферов. Копирует туда содержимое входного буфера, адрес которого передается в параметре lpInBuffer функции DeviceIoControl. В нашем случае, этот буфер содержит четыре виртуальных адреса подлежащих трансляции.

            assume edi:ptr DWORD

Т.к. адрес имеет размер двойного слова, даем понять компилятору, что регистр edi указывает на ячейку памяти размером DWORD.

            xor ebx, ebx
            .while ebx < NUM_DATA_ENTRY

                invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]

                mov [edi][ebx*(sizeof DWORD)], eax
                inc ebx
            .endw

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

Получив преобразованное значение, помещаем его обратно в системный буфер, на то же самое место.

            mov dwBytesReturned, DATA_SIZE
            mov status, STATUS_SUCCESS

Если мы добрались до этой точки, то обработка запроса закончена. Помещаем в переменные dwBytesReturned и status соответствующие значения.

        .else
            mov status, STATUS_BUFFER_TOO_SMALL
        .endif
    .else
        mov status, STATUS_INVALID_DEVICE_REQUEST
    .endif

Если происходит какая-нибудь ошибка, то переменная status получает соответствующий код ошибки.

    assume edi:nothing

    push status
    pop [esi].IoStatus.Status

Завершая операцию ввода-вывода, помещаем в поле Status блока статуса ввода-вывода текущее значение переменной status. На стороне режима пользователя код состояния транслируется в код ошибки, который можно получить, вызвав функцию GetLastError. Трансляция происходит, естественно, не один к одному, т.к. в режимах пользователя и ядра разные наборы кодов ошибок. Вот, например, как транслируются используемые нами в данном примере коды ошибок.

Nt Status | Win32 Error
STATUS_SUCCESS | NO_ERROR
STATUS_BUFFER_TOO_SMALL | ERROR_INSUFFICIENT_BUFFER
STATUS_INVALID_DEVICE_REQUEST | ERROR_INVALID_FUNCTION

Можно воспользоваться функцией NtStatusToDosError из модуля ntdll.dll для определения соответствия кодов ошибок в режиме ядра и в режиме пользователя. Именно это делает утилита Tools\StatusToError, транслируя код статуса в код ошибки подсистемы Win32.

    push dwBytesReturned
    pop [esi].IoStatus.Information

В поле Information блока статуса ввода-вывода помещаем текущее значение переменной dwBytesReturned. Завершая IRP, диспетчер ввода-вывода извлекает из этого поля значение количества байт, которое он должен скопировать из системного буфера в пользовательский, указатель на который передается в параметре lpOutBuffer функции DeviceIoControl на стороне режима пользователя. Это же значение вернется коду режима пользователя в двойном слове, указатель на которое помещен в параметр lpBytesReturned функции DeviceIoControl.

Если произойдет какая-либо ошибка, то переменная dwBytesReturned уже инициализирована нулевым значением. В этом случае диспетчер ввода-вывода ничего никуда копировать не станет. Если же все пойдет по плану, то в переменной dwBytesReturned окажется значение DATA_SIZE. Именно это количество байт диспетчер ввода-вывода и скопирует в пользовательский буфер.

    assume esi:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, status
    ret

Даем указание диспетчеру ввода-вывода завершить обработку IPR и возвращаем системе текущий код состояния.

5.9 Процедура трансляции адресов

Код режима ядра, в отличие от кода режима пользователя, может транслировать виртуальные адреса в физические и наоборот. Для этого необходим доступ к каталогу страниц (page directory) и самим таблицам страниц (page tables). В Windows NT для x86-систем каталог страниц процесса расположен по виртуальному адресу 0C0300000h (режим проецирования памяти Physical Address Extension мы не рассматриваем), а таблицы страниц процесса начинаются с виртуального адреса 0C0000000h.

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

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

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

    mov eax, dwAddress
    mov ecx, eax

Сохраняем виртуальный адрес в регистре ecx для дальнейшего использования.

    shr eax, 22

Выделяем старшие 10 бит, которые являются индексом каталога страниц (page directory index).

    shl eax, 2

Умножаем индекс на размер элемента каталога страниц (page directory entry, PDE), равный 4 байтам. Т.о. в регистре eax получаем смещение нужного PDE от начала каталога.

    mov eax, [0C0300000h][eax]

Извлекаем содержимое PDE.

    .if ( eax & (mask pde4kValid) )

12 младших бит PDE (см. запись HARDWARE_PDE4K в \include\w2k\w2kundoc.inc) содержат атрибуты PDE. Если флаг Valid (бит 0) установлен, то PDE действителен.

        .if !( eax & (mask pde4kLargePage) )

Флаг Large Page (бит 7) указывает, что PDE относится к 4-мегабайтной странице (на данный момент, такие страницы используются ядром (по крайней мере в Windows 2000 это так) для проецирования ntoskrnl.exe и hal.dll в диапазон виртуальных адресов 80000000h - 9FFFFFFFh. Но только если в системе установлено более 128Мб памяти). Для PDE соответствующих 4-килобайтным и 4-мегабайтным страницам трансляция адреса отличается.

            mov eax, ecx
            shr eax, 10
            and eax, 1111111111111111111100y

Если PDE соответствует 4-килобайтной странице, выделяем биты 12-21, которые являются индексом таблицы страниц (page table index), одновременно "умножая" индекс на размер элемента таблицы страниц (page table entry, PTE) равный 4 байтам. Т.о. в регистре eax получаем смещение нужного PTE от начала всего массива таблиц страниц.

            add eax, 0C0000000h
            mov eax, [eax]

Прибавив базовый адрес массива таблиц страниц процесса, извлекаем PTE.

            .if eax & (mask pteValid)

12 младших бит PТE (см. запись HARDWARE_PTE в \include\w2k\w2kundoc.inc) содержат атрибуты PTE. Если флаг Valid (бит 0) установлен, то PTE соответствует странице в физической памяти.

                and eax, mask ptePageFrameNumber

Если PTE действителен, то его старшие 20 бит содержат номер фрейма страницы (page frame number, PFN) являющийся номером страницы в пространстве физических адресов. Этой командой мы одним махом выделяем PFN и "умножаем" его на размер страницы (4кб) и получаем физический адрес страницы.

                and ecx, 00000000000000000000111111111111y
                add eax, ecx
            .else
                xor eax, eax
            .endif

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

        .else
            and eax, mask pde4mPageFrameNumber

Если PDE соответствует 4-мегабайтной странице (см. запись HARDWARE_PDE4M в \include\w2k\w2kundoc.inc), выделяем биты 22-31, получая PFN, который, в случае больших страниц, и является физическим адресом страницы.

            and ecx, 00000000001111111111111111111111y
            add eax, ecx
        .endif
    .else
        xor eax, eax
    .endif

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

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

Работа с записями не часто встречается в практике программирования на ассемблере. Если не знакомы с директивой mask, то вместо

mask pde4kValid
mask pde4kLargePage
mask pteValid
mask ptePageFrameNumber
mask pde4mPageFrameNumber

используйте

01y
010000000y
01y
11111111111111111111000000000000y
11111111110000000000000000000000y

5.10 Процедура выгрузки драйвера

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

        invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

        mov eax, pDriverObject
        invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

Тут код говорит сам за себя. В процедуре выгрузки драйвера мы должны произвести действия прямо противоположные действиям в процедуре инициализации, т.е. удалить все созданные нами объекты. Как я уже говорил, в режиме ядра никто за нас "подтирать" не будет.

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

Режим пользователя | Режим ядра | Контекст | IRQL
StartService | DriverEntry | Процесса System | PASSIVE_LEVEL
CreateFile | IRP_MJ_CREATE | Вызывающего процесса | PASSIVE_LEVEL
DeviceIoControl | IRP_MJ_DEVICE_CONTROL | Вызывающего процесса | PASSIVE_LEVEL
ReadFile | IRP_MJ_READ | Вызывающего процесса | PASSIVE_LEVEL
WriteFile | IRP_MJ_WRITE | Вызывающего процесса | PASSIVE_LEVEL
CloseHandle | IRP_MJ_CLEANUP, IRP_MJ_CLOSE | Вызывающего процесса | PASSIVE_LEVEL
ControlService,,SERVICE_CONTROL_STOP | DriverUnload | Процесса System | PASSIVE_LEVEL

Таблица 5-1. Соответствие пользовательских функций процедурам драйвера

5.11 Компиляция драйвера

:make

set drv=skeleton

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

Все это мы с вами уже разобрали в части 3. Я добавил ключ /ignore:4078, т.к. из-за того, что у нас появилась вторая секция кода "INIT", с отличающимися атрибутами, компоновщик выдает предупреждение:

LINK : warning LNK4078: multiple "INIT" sections found with different attributes (E2000020)

5.12 Добавляем ресурсы

Каждый уважающий своего создателя драйвер должен сообщать его имя, а также кое-какие дополнительные сведения о себе. Это можно сделать используя файл ресурсов (см. rsrc.rc).

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,0
 PRODUCTVERSION 1,0,0,0
 FILEFLAGSMASK 0x3fL
 FILEFLAGS 0x0L
 FILEOS 0x40004L
 FILETYPE 0x1L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904E4"
        BEGIN
            VALUE "Comments", "Written by Four-F\0"
            VALUE "CompanyName", "Four-F Software\0"
            VALUE "FileDescription", "Kernel-Mode Driver VirtToPhys v1.00\0"
            VALUE "FileVersion", "1, 0, 0, 0\0"
            VALUE "InternalName", "VirtualToPhysical\0"
            VALUE "LegalCopyright", "Copyright © 2003, Four-F\0"
            VALUE "OriginalFilename", "VirtToPhys.sys\0"
            VALUE "ProductName", "Kernel-Mode Driver Virtual To Physical Address Converter\0"
            VALUE "ProductVersion", "1, 0, 0, 0\0"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1200
    END
END

Тут ничего принципиально нового. Стандартный ресурсный скрипт. Компилируется и линкуется как обычно. В свойствах файла драйвера можно посмотреть что получилось.

5.13 Еще немного об отладке

Вытащить всю (или почти всю) "подноготную" объектов "драйвер" и "устройство" можно с помощью команд driver <имя драйвера> и device <имя устройства> отладчика SoftICE. Вот как это выглядит на моей машине:

Рис. 5-2. Результат выполнения команды driver VirtToPhys

Рис. 5-3. Результат выполнения команды device devVirtToPhys

Как вы понимаете, информация отображаемая SoftICE, в основном, извлекается из структур DRIVER_OBJECT и DEVICE_OBJECT соответственно. Используя эту информацию можно легко найти эти объекты в памяти, поставить точки прерывания на процедуры диспетчеризации и выгрузки драйвера и т.п.


Использование системных куч

6.1 Системные кучи
6.2 Выделяем память из системного пула
Исходный текст: KmdKit\examples\basic\MemoryWorks\SystemModules

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

Система, точнее диспетчер памяти (Memory Manager), предоставляет пользовательским процессам довольно богатый API для работы с памятью, в который входят три группы функций: операции со страницами виртуальной памяти, проецирование файлов в память и управление кучами (динамически распределяемыми областями памяти).

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

6.1 Системные кучи

Системные кучи (к пользовательским кучам не имеют никакого отношения) представлены двумя так называемыми пулами памяти, которые, естественно, располагаются в системном адресном пространстве:

  • Пул неподкачиваемой памяти (Nonpaged Pool). Назван так потому, что его страницы никогда не сбрасываются в файл подкачки, а значит, никогда и не подкачиваются назад. Т.е. этот пул всегда присутствует в физической памяти и доступен при любом IRQL. Одна из причин его существования в том, что обращение к такой памяти не может вызвать ошибку страницы (Page Fault). Такие ошибки приводят к краху системы, если происходят при IRQL >= DISPATCH_LEVEL;

  • Пул подкачиваемой памяти (Paged Pool). Назван так потому, что его страницы могут быть сброшены в файл подкачки, а значит должны быть подкачаны назад при последующем к ним обращении. Эту память можно использовать только при IRQL строго меньше DISPATCH_LEVEL.

Оба пула находятся в системном адресном пространстве, а значит, доступны из контекста любого процесса. Для выделения памяти в системных пулах существует набор функций ExAllocatePoolXxx, а для возвращения выделенной памяти всего одна - ExFreePool.

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

  1. Обращение к памяти сброшенной в файл подкачки при IRQL >= DISPATCH_LEVEL, как я уже говорил, приводят к краху системы.

    Имейте в виду, что если в момент обращения к подкачиваемой памяти она физически присутствует, то краха не будет, даже при IRQL >= DISPATCH_LEVEL. Но можете быть уверены, что рано или поздно ее не окажется на месте и тогда BSOD обеспечен.

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

  3. Если вы выделили память из любого системного пула, то вне зависимости от того, что дальше случится с вашим драйвером, эта память не будет возвращена системе назад до тех пор, пока вы явно не вызовите ExFreePool. Т.е. если драйвер не освободит выделенную ему память явно, то она так и останется бесполезно болтаться в системе, даже если драйвер будет выгружен. Я уже неоднократно говорил, что все выделяемые драйвером ресурсы должны быть явно возвращены назад.

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

Определить какой тип памяти (подкачиваемая или неподкачиваемая) нужен вам, очень просто. Если какой-либо код должен обращаться к памяти при IRQL >= DISPATCH_LEVEL, то нужно использовать только неподкачиваемую память. Причем, как сам код, так и данные должны располагаться в неподкачиваемой памяти. По умолчанию весь драйвер загружается в неподкачиваемую память, кроме секции с именем "INIT" и секций, имена которых начинаются с "PAGE". Если кроме этого вы не предпринимали никаких действий по изменению атрибутов памяти принадлежащих драйверу, например, не вызывали функцию MmPageEntireDriver, делающую весь образ драйвера подкачиваемым, то о самом драйвере беспокоится не стоит - он всегда будет присутствовать в физической памяти.

В предыдущих статьях мы достаточно подробно разобрали, при каком IRQL вызываются стандартные процедуры (DriverEntry, DriverUnload, DispatchXxx) драйвера.

Кроме того, в DDK в описании каждой функции указано при каком IRQL ее можно вызывать или при каком IRQL она вызывается системой, если это функция обратного вызова (callback). Например, в одной из следующих статей мы будем использовать функцию IoInitializeTimer. В описании этой функции сказано, что процедура, вызываемая системой при срабатывании таймера, выполняется при IRQL = DISPATCH_LEVEL. Это недвусмысленно говорит нам о том, что эта процедура и любая память, к которой она будет обращаться должны быть неподкачиваемыми.

На худой конец, если уж вы совсем не уверены в текущем IRQL, его можно определить функцией KeGetCurrentIrql таким образом:

invoke KeGetCurrentIrql
.if eax < DISPATCH_LEVEL
    ; используем любую память
.else
    ; используем только неподкачиваемую память
.endif

6.2 Выделяем память из системного пула

В качестве примера рассмотрим очень простой драйвер SystemModules. Все действо будет происходить в процедуре DriverEntry. Мы быстренько выделим немного подкачиваемой памяти (вы, несомненно, помните, что DriverEntry работает на IRQL = PASSIVE_LEVEL. Поэтому мы обойдемся подкачиваемой памятью.), что-нибудь полезное в нее запишем, освободим и заставим систему выгрузить драйвер.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  SystemModules - Выделяем память из системного пула и используем её.
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\native.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code INIT

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc uses esi edi ebx pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local cb:DWORD
local p:PVOID
local dwNumModules:DWORD
local pMessage:LPSTR
local buffer[256+40]:CHAR

    invoke DbgPrint, $CTA0("\nSystemModules: Entering DriverEntry\n")

    and cb, 0
    invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb
    .if cb != 0

        invoke ExAllocatePool, PagedPool, cb
        .if eax != NULL
            mov p, eax

            invoke DbgPrint, \
                   $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), cb, p

            invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb
            .if eax == STATUS_SUCCESS
                mov esi, p

                push dword ptr [esi]
                pop dwNumModules

                mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2

                invoke ExAllocatePool, PagedPool, cb
                .if eax != NULL
                    mov pMessage, eax

                    invoke DbgPrint, \
                           $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), \
                           cb, pMessage

                    invoke memset, pMessage, 0, cb

                    add esi, sizeof DWORD
                    assume esi:ptr SYSTEM_MODULE_INFORMATION

                    xor ebx, ebx
                    .while ebx < dwNumModules

                        lea edi, [esi].ImageName
                        movzx ecx, [esi].ModuleNameOffset
                        add edi, ecx

                        invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1
                        push eax
                        invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1
                        pop ecx

                        and eax, ecx
                        .if ZERO?
                            invoke _snprintf, addr buffer, sizeof buffer, \
                                    $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \
                                    edi, [esi].Base, [esi]._Size
                            invoke strcat, pMessage, addr buffer
                        .endif

                        add esi, sizeof SYSTEM_MODULE_INFORMATION
                        inc ebx

                    .endw
                    assume esi:nothing

                    mov eax, pMessage
                    .if byte ptr [eax] != 0
                        invoke DbgPrint, pMessage
                    .else
                        invoke DbgPrint, \
                               $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n")
                    .endif

                    invoke ExFreePool, pMessage
                    invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), pMessage

                .endif
            .endif

            invoke ExFreePool, p
            invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), p

        .endif
    .endif

    invoke DbgPrint, $CTA0("SystemModules: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=SystemModules

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj

del %drv%.obj

echo.
pause

В качестве чего-нибудь полезного мы возьмем список модулей загруженных в системное адресное пространство (в этот список войдут модули самой системы: ntoskrnl.exe, hal.dll и т.п. и драйверы устройств) и попытаемся найти в нем два модуля: ntoskrnl.exe и ntice.sys. Список системных модулей можно получить, вызвав функцию ZwQuerySystemInformation с информационным классом SystemModuleInformation. Описание этой функции можно найти в книге Гэри Неббета "Справочник по базовым функциям API Windows NT/2000". Кстати, ZwQuerySystemInformation уникальная функция. С её помощью можно получить просто огромное количество самой различной информации о системе.

Программы управления драйвером не будет. Используйте KmdManager (входит в пакет KmdKit) или аналогичную утилиту, а отладочные сообщения, выдаваемые драйвером, контролируйте с помощью утилиты DebugView ( http://www.sysinternals.com ) или консоли SoftICE.

    and cb, 0
    invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb

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

    .if cb != 0
        invoke ExAllocatePool, PagedPool, cb

Если размер требуемого буфера не равен нулю, мы выделяем необходимое количество памяти из подкачиваемого пула (об этом говорит первый параметр - PagedPool. Значение NonPagedPool будет означать запрос неподкачиваемой памяти). Функция ExAllocatePool даже проще чем ее аналог режима пользователя HeapAlloc. Всего два параметра. Первый определяет пул: подкачиваемый или неподкачиваемый, второй - размер требуемой памяти. Проще не бывает.

        .if eax != NULL

Если ExAllocatePool вернет ненулевое значение, то это указатель на выделенный буфер.

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

            mov p, eax
            invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb

Сохраняем указатель на выделенный буфер в переменной p и вызываем ZwQuerySystemInformation еще раз, передавая ей указатель на буфер и его размер.

            .if eax == STATUS_SUCCESS
                mov esi, p

Если ZwQuerySystemInformation возвращает STATUS_SUCCESS, то её удовлетворили параметры нашего буфера и теперь он содержит список системных модулей в виде массива структур SYSTEM_MODULE_INFORMATION (определена в файле include\w2k\native.inc).

SYSTEM_MODULE_INFORMATION STRUCT        ;Information Class 11
    Reserved            DWORD   2 dup(?)
    Base                PVOID   ?
    _Size               DWORD   ?
    Flags               DWORD   ?
    Index               WORD    ?
    Unknown             WORD    ?
    LoadCount           WORD    ?
    ModuleNameOffset    WORD    ?
    ImageName           CHAR 256 dup(?)
SYSTEM_MODULE_INFORMATION ENDS

Действительное количество байт скопированных в буфер вернется в переменной cb, но нам оно не нужно.

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

                push dword ptr [esi]
                pop dwNumModules

В самом первом двойном слове буфера, заполненного ZwQuerySystemInformation, содержится количество структур SYSTEM_MODULE_INFORMATION равное количеству модулей и сразу за ним (двойным словом) начинается их массив. Запоминаем количество модулей в переменной dwNumModules.

                mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2
                invoke ExAllocatePool, PagedPool, cb
                .if eax != NULL
                    mov pMessage, eax

Для дальнейшей плодотворной работы нам потребуется еще один буфер, в который будут помещаться имена двух искомых модулей и кое-какая дополнительная информация. Мы предполагаем, что (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2 должно как раз хватить.

Обратите внимание на адрес буфера - он не будет кратен размеру страницы, т.к. его размер меньше страницы.

                    invoke memset, pMessage, 0, cb

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

                    add esi, sizeof DWORD
                    assume esi:ptr SYSTEM_MODULE_INFORMATION

Пропускаем DWORD содержащий число модулей и регистр esi теперь указывает на первую структуру SYSTEM_MODULE_INFORMATION.

                    xor ebx, ebx
                    .while ebx < dwNumModules

Организуем цикл, повторяющийся dwNumModules раз. В цикле перебираем массив структур SYSTEM_MODULE_INFORMATION и ищем там структуры соответствующие модулям ntoskrnl.exe и ntice.sys.

В многопроцессорной системе модуль ntoskrnl.exe будет иметь имя ntkrnlmp.exe. А в системе с поддержкой PAE - ntkrnlpa.exe и ntkrpamp.exe, соответственно. Здесь я предполагаю, что вы не являетесь счастливым обладателем подобной системы.

                        lea edi, [esi].ImageName
                        movzx ecx, [esi].ModuleNameOffset
                        add edi, ecx

Поля ImageName и ModuleNameOffset содержат полный путь к модулю и относительное смещение имени модуля в пути, соответственно.

                        invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1
                        push eax
                        invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1
                        pop ecx

Поиск осуществляем простым сравнением имен модулей. Функция _strnicmp сравнивает две ANSI-строки независимо от регистра букв. Сравнивает она только то количество символов, которое передано в третьем параметре. В данном случае это не обязательно, т.к. имена модулей в структурах SYSTEM_MODULE_INFORMATION завершаются нулями и можно было бы использовать _stricmp. Я использую _strnicmp для пущей безопастности.

Кстати, ntoskrnl.exe экспортирует большое количество стандартных функций по работе со строками: strcmp, strcpy, strlen и т.п.

                        and eax, ecx
                        .if ZERO?
                            invoke _snprintf, addr buffer, sizeof buffer, \
                                    $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \
                                    edi, [esi].Base, [esi]._Size
                            invoke strcat, pMessage, addr buffer
                        .endif

                        add esi, sizeof SYSTEM_MODULE_INFORMATION
                        inc ebx

                    .endw
                    assume esi:nothing

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

Цифра 4 в макросах $CTA0 означает, что определяемая ими строка выравнивается по границе DWORD (здесь этого можно и не делать). А метки szNtoskrnl и szNtIce нужны для того, чтобы передать их в директиву sizeof. Кстати, вы можете менять местами метку и выравнивание в моих строковых макросах - они распознаются автоматически. Либо можете использовать только метку или только выравнивание (подробнее см. macros\Strings.mac).

                    mov eax, pMessage
                    .if byte ptr [eax] != 0
                        invoke DbgPrint, pMessage
                    .else
                        invoke DbgPrint, \
                               $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n")
                    .endif

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

                    invoke ExFreePool, pMessage
                .endif
            .endif
            invoke ExFreePool, p
        .endif
    .endif

Возвращаем выделенную из системных пулов память.

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

Заставляем систему выгрузить драйвер.

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

Многие ZwXxx функции экспортируются также библиотекой ntdll.dll режима пользователя и являются простыми переходниками в режим ядра, где и происходит вызов соответствующей функции, которая и проделывает всю полезную работу. Примечательно то, что количество параметров и их смысл полностью совпадают. Из этой ситуации можно извлечь большую выгоду. Поскольку ошибки в ядре приводят к краху системы, можно сначала отладить код в режиме пользователя, а потом перенести его в драйвер с минимальными изменениями, а иногда даже и без изменений. Например, в нашем случае, будучи вызвана из ntdll.dll в режиме пользователя, функция ZwQuerySystemInformation вернет ту же самую информацию, что и одноименная функция, вызванная из ntoskrnl.exe в драйвере. Пользуясь этим нехитрым приемом, можно сэкономить немалое число перезагрузок.


Использование ассоциативных списков

7.1 Ассоциативные списки
7.2 Исходный текст драйвера LookasideList
7.3 Работаем с ассоциативным списком
7.4 Процедура AddEntry
7.5 Процедура RemoveEntry
Исходный текст: KmdKit\examples\basic\MemoryWorks\LookasideList

7.1 Ассоциативные списки

Диспетчер куч (heap manager) управляет кучами, как системными, так и пользовательскими, разбивая пространство кучи на блоки и организуя списки блоков одинакового размера. Если приходит запрос на выделение блока памяти из кучи, то диспетчер куч пытается подобрать свободный блок подходящего размера. На это, естественно, требуется какое-то время. Если же заранее известно, что потребуются блоки памяти фиксированного размера, но количество этих блоков и частота их использования не известны, то следует использовать, по соображениям лучшей производительности, так называемые ассоциативные списки (look-aside lists), которые существуют только в ядре. Главное отличие ассоциативных списков от пулов в том, что из ассоциативных списков можно выделять блоки памяти только фиксированного и заранее определенного размера, а из пулов любого, причем память из ассоциативных списков выделяется быстрее, так как нет необходимости подбирать подходящую по размеру область.

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

  • Односвязный список (singly linked list);

  • S-список (S-list, sequenced singly-linked list.), являющийся развитием односвязного списка;

  • Двусвязный список (doubly linked list).

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

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

Хотя обе эти конструкции называются списками, по сути, это совершенно разные вещи. Перевод английского термина look-aside list на русский язык крайне абстрактен и плохо отражает суть. Look-aside дословно можно перевести как: "смотреть по сторонам". Смысл в том, что look-aside list представляет собой набор или список заранее выделенных системой блоков памяти. В каждый момент времени какие-то блоки могут быть свободны, а какие-то заняты. При запросе на выделение очередного блока задача системы пройти по списку ("посмотреть по сторонам ") и найти близлежащий свободный блок. В русском же переводе, look-aside превращается в "ассоциативный", и становится совершенно непонятно, что с чем здесь ассоциируется. Тем не менее, перевод этот устоявшийся - хочешь, не хочешь, придется применять. Т.о. ассоциативный список - это фактически особая системная куча, работающая по определенным правилам.

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

7.2 Исходный текст драйвера LookasideList

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

Программы управления драйвером не будет. Используйте KmdManager (входит в пакет KmdKit) или аналогичную утилиту, а отладочные сообщения, выдаваемые драйвером, контролируйте с помощью утилиты DebugView ( http://www.sysinternals.com ) или консоли SoftICE.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  LookasideList - Пример использования и управления ассоциативным списком.
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.486
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       С Т Р У К Т У Р Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

SOME_STRUCTURE STRUCT
    SomeField1    DWORD        ?
    SomeField2    DWORD        ?
    ; . . .                          ; Любое кол-во дополнительно необходимых членов

    ListEntry    LIST_ENTRY    <>    ; Двусвязный список для управления структурами
                                     ; Зтот член удобнее располагать первым в структуре,
                                     ; но здесь используется более общее решение

    ; . . .                          ; Любое кол-во дополнительно необходимых членов
    SomeFieldX    DWORD        ?
SOME_STRUCTURE ENDS

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                       И Н И Ц И А Л И З И Р О В А Н Н Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.data?

g_pPagedLookasideList    PPAGED_LOOKASIDE_LIST    ?
g_ListHead               LIST_ENTRY               <>
g_dwIndex                DWORD                    ?

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         AddEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

AddEntry proc uses esi

    invoke ExAllocateFromPagedLookasideList, g_pPagedLookasideList
    .if eax != NULL
        mov esi, eax

        invoke DbgPrint, \
               $CTA0("LookasideList: + Memory block allocated from lookaside list at address %08X\n"), esi

        invoke memset, esi, 0, sizeof SOME_STRUCTURE

        assume esi:ptr SOME_STRUCTURE

        lea eax, g_ListHead
        lea ecx, [esi].ListEntry
        InsertHeadList eax, ecx

        inc g_dwIndex
        mov eax, g_dwIndex
        mov [esi].SomeField1, eax

        invoke DbgPrint, $CTA0("LookasideList: + Entry #%d added\n"), [esi].SomeField1

        assume esi:nothing

    .else
        invoke DbgPrint, $CTA0("LookasideList: Very bad. Couldn't allocate from lookaside list\n")
    .endif

    ret

AddEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       RemoveEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

RemoveEntry proc uses esi

    IsListEmpty addr g_ListHead
    .if eax != TRUE

        lea eax, g_ListHead
        RemoveHeadList eax
        sub eax, SOME_STRUCTURE.ListEntry
        mov esi, eax

        invoke DbgPrint, $CTA0("LookasideList: - Entry #%d removed\n"), \
                            (SOME_STRUCTURE PTR [esi]).SomeField1

        invoke ExFreeToPagedLookasideList, g_pPagedLookasideList, esi

        invoke DbgPrint, \
            $CTA0("LookasideList: - Memory block at address %08X returned to lookaside list\n"), esi
    .else
        invoke DbgPrint, \
            $CTA0("LookasideList: An attempt was made to remove entry from empty lookaside list\n")
    .endif

    ret

RemoveEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc uses ebx pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke DbgPrint, $CTA0("\nLookasideList: Entering DriverEntry\n")

    invoke ExAllocatePool, NonPagedPool, sizeof PAGED_LOOKASIDE_LIST
    .if eax != NULL

        mov g_pPagedLookasideList, eax

        invoke DbgPrint, \
        $CTA0("LookasideList: Nonpaged memory for lookaside list allocated at address %08X\n"), \
        g_pPagedLookasideList

        invoke ExInitializePagedLookasideList, g_pPagedLookasideList, NULL, NULL, \
                                               0, sizeof SOME_STRUCTURE, ' msaW', 0

        invoke DbgPrint, $CTA0("LookasideList: Lookaside list initialized\n")

        lea eax, g_ListHead
        InitializeListHead eax

        invoke DbgPrint, $CTA0("LookasideList: Doubly linked list head initialized\n")

        invoke DbgPrint, $CTA0("\nLookasideList: Start to allocate/free from/to lookaside list\n")

        and g_dwIndex, 0

        xor ebx, ebx
        .while ebx < 5

            invoke AddEntry
            invoke AddEntry

            invoke RemoveEntry

            inc ebx
        .endw

        invoke DbgPrint, $CTA0("\nLookasideList: Free the rest to lookaside list\n")

        .while TRUE

            invoke RemoveEntry

            lea eax, g_ListHead
            IsListEmpty eax
            .if eax == TRUE
                invoke DbgPrint, $CTA0("LookasideList: Doubly linked list is empty\n\n")
                .break
            .endif

        .endw

        invoke ExDeletePagedLookasideList, g_pPagedLookasideList

        invoke DbgPrint, $CTA0("LookasideList: Lookaside list deleted\n")

        invoke ExFreePool, g_pPagedLookasideList

        invoke DbgPrint, \
        $CTA0("LookasideList: Nonpaged memory for lookaside list at address %08X released\n"), \
        g_pPagedLookasideList

    .else
        invoke DbgPrint, \
         $CTA0("LookasideList: Couldn't allocate nonpaged memory for lookaside list control structure")
    .endif

    invoke DbgPrint, $CTA0("LookasideList: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=LookasideList

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

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

    invoke ExAllocatePool, NonPagedPool, sizeof PAGED_LOOKASIDE_LIST
    .if eax != NULL

        mov g_pPagedLookasideList, eax

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

        invoke ExInitializePagedLookasideList, g_pPagedLookasideList, NULL, NULL, \
                                                0, sizeof SOME_STRUCTURE, ' msaW', 0

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

Обратите внимание на то, что при инициализации ассоциативного списка мы нигде не указываем, сколько всего блоков памяти нам может потребоваться. Откуда же система знает, сколько памяти она должна заблаговременно выделить? Ведь если этого не сделать заранее, то ассоциативный список не будет работать быстрее, чем системный пул. Дело в том, что изначально система выделяет всего несколько блоков, количество которых определяется самой системой. Если теперь мы начнем выделять память из ассоциативного списка, то будут возвращаться указатели именно на эти заранее выделенные блоки. Один раз в секунду система проводит настройку всех ассоциативных списков в системе, вызывая функцию ExAdjustLookasideDepth (В книге Дэвида Соломона и Марка Руссиновича "Внутреннее устройство Microsoft Windows 2000" в соответствующем разделе говорится, что эта функция называется KiAdjustLookasideDepth. Скорее всего, это ошибка - такой функции я не нашел - и имелась ввиду ExAdjustLookasideDepth.). Если при настройке ассоциативного списка система увидит, что резерв свободных блоков уменьшился, то выделит новые. Количество вновь выделяемых блоков зависит от нагрузки на ассоциативный список, т.е. от частоты с которой мы выделяем из него память. Система пытается настроить ассоциативный список так, чтобы он работал наиболее эффективно. Если в интервале времени между двумя настройками мы исчерпали лимит заранее выделенных блоков, то система начинает использовать системный пул до следующей настройки, когда она увидит, что резерв равен нулю и выделит необходимый лимит. Здесь важно понять, что если скорость выделения блоков уж слишком велика, то можно и не получить выигрыша в производительности по сравнению с системным пулом. В этом случае придется организовывать собственную кучу. Оценить насколько эффективно работает ассоциативный список можно с помощью команды !lookaside отладчика MS Kernel Debugger либо вручную, просмотрев с помощью SoftICE дамп структуры PAGED_LOOKASIDE_LIST. Информацию, извлекаемую именно из этой структуры, система использует для выбора стратегии управления ассоциативным списком.

kd> !lookaside ed374840

Lookaside "" @ ed374840 "Regm"
    Type     =     0001 PagedPool
    Current Depth  =        2   Max Depth  =        4
    Size           =     1024   Max Alloc  =     4096
    AllocateMisses =        4   FreeMisses =        0
    TotalAllocates =  1319722   TotalFrees =  1319720
    Hit Rate       =       99%  Hit Rate   =      100%

Это пример того, насколько эффективно работает ассоциативный список утилиты RegMon ( http://www.sysinternals.com ). Как видите, при огромном (более одного миллиона!) количестве операций выделения/освобождения эффективность стремится к 100%. Причина такой высокой эффективность в том, что RegMon не держит долго выделенный блок, а тут же использует и возвращает назад.

        lea eax, g_ListHead
        InitializeListHead eax

Вызовом макроса InitializeListHead, инициализируем голову двусвязного списка. Теперь оба поля структуры LIST_ENTRY содержат указатели на саму эту структуру. Это означает, что двусвязный список пуст (см. рис 7-1, иллюстрация 1).

Рис. 7-1. Этот рисунок позволит нагляднее увидеть работу двусвязного списка.

        and g_dwIndex, 0

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

        xor ebx, ebx
        .while ebx < 5

            invoke AddEntry
            invoke AddEntry

            invoke RemoveEntry

            inc ebx
        .endw

Пять раз прогоняем цикл, который добавляет два элемента и удаляет один. Каждый элемент представляет собой структуру SOME_STRUCTURE. Все выделенные структуры связаны посредством двусвязного списка. Функции AddEntry и RemoveEntry рассмотрим позже.

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

        .while TRUE

            invoke RemoveEntry

            lea eax, g_ListHead
            IsListEmpty eax
            .if eax == TRUE
                .break
            .endif

        .endw

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

В бесконечном цикле вызываем процедуру RemoveEntry, которая удаляет один элемент. Цикл работает до тех пор, пока двусвязный список не окажется пустым. Макрос IsListEmpty проверяет, пуст ли двусвязный список. Признаком пустоты является ситуация, когда оба поля головы двусвязного списка - структуры LIST_ENTRY - указывают на саму эту структуру, т.е. голова указывает на саму себя. В этот момент мы приходим к тому же состоянию, которое мы имели сразу после вызова макроса InitializeListHead (см. рис 7-1, иллюстрация 1).

        invoke ExDeletePagedLookasideList, g_pPagedLookasideList

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

        invoke ExFreePool, g_pPagedLookasideList

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

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

Мы освободили все выделенные нам ресурсы - драйвер можно выгружать.

7.4 Процедура AddEntry

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

    invoke ExAllocateFromPagedLookasideList, g_pPagedLookasideList
    .if eax != NULL
        mov esi, eax

Вызовом функции ExAllocateFromPagedLookasideList, получаем указатель на новый блок памяти и сохраняем его в регистре esi. Обратите внимание на то, что мы не сообщаем системе размер блока, т.к. этот размер определен ранее в вызове функции ExInitializePagedLookasideList и равен размеру структуры SOME_STRUCTURE.

        invoke memset, esi, 0, sizeof SOME_STRUCTURE

Обнуляем выделенный блок, хотя здесь этого и не требуется.

        assume esi:ptr SOME_STRUCTURE

Теперь мы имеем новый экземпляр структуры SOME_STRUCTURE. Но пока он существует сам по себе. Мы должны "привязать" его к нашему двусвязному списку, который при первом вызове AddEnrty еще пуст.

        lea eax, g_ListHead
        lea ecx, [esi].ListEntry
        InsertHeadList eax, ecx

После первого вызова макроса InsertHeadList мы имеем ситуацию изображенную на рис. 7-1, иллюстрация 2. Обратите внимание на то, что вторым параметром в макрос InsertHeadList мы передаем адрес не самой структуры SOME_STRUCTURE, а адрес её поля ListEntry. Т.е. макрос InsertHeadList получает указатель на две структуры LIST_ENTRY, первая из которых является головой двусвязного списка, а вторая элементом структуры, которую необходимо к этому двусвязному списку привязать. Причем макрос InsertHeadList привязывает новую структуру в голову двусвязного списка (справа, если смотреть на рис 7-1). Можно привязать и в хвост (слева по рисунку), если воспользоваться макросом InsertTailList.

При добавлении первой структуры в двусвязный список оба макроса произведут одинаковый эффект и двусвязный список будет выглядеть так, как изображено на рис 7-1, иллюстрация 2.

Если же двусвязный список не пустой, то макрос InsertHeadList раздвинет его между головой и идущей справа от нее структурой и вставит на это место новую структуру. Мы получим ситуацию изображенную на рис 7-1, иллюстрация 3. Если бы мы воспользовались макросом InsertTailList, то он сделал бы то же самое, но с хвоста двусвязного списка (см. рис 7-1, иллюстрация 4).

Надеюсь, здесь всё ясно.

        inc g_dwIndex
        mov eax, g_dwIndex
        mov [esi].SomeField1, eax

Запишем в поле SomeField1, добавленной только что структуры, её порядковый номер. Порядок, в котором структуры добавляются/удаляются можно будет посмотреть в окне DbgView.

7.5 Процедура RemoveEntry

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

    IsListEmpty addr g_ListHead
    .if eax != TRUE

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

        lea eax, g_ListHead
        RemoveHeadList eax

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

        sub eax, SOME_STRUCTURE.ListEntry
        mov esi, eax

Обратите внимание на этот момент. Макросы RemoveTailList/RemoveHeadList/RemoveEntryList возвращают указатель на связующий элемент (вложенную структуру LIST_ENTRY) отвязанной структуры, а не указатель на саму отвязанную структуру. Это вполне естественно, ведь эти макросы не знают, в каком месте располагается связующий элемент. И узнать это они никак не могут. Это знаем только мы. Поэтому мы должны вычесть из полученного указателя смещение поля ListEntry в структуре SOME_STRUCTURE и получить указатель на саму структуру (в DDK это делает макрос CONTAINING_RECORD). Вот теперь этот блок памяти можно возвратить в ассоциативный список.

        invoke ExFreeToPagedLookasideList, g_pPagedLookasideList, esi

Передаем функции ExFreeToPagedLookasideList адрес блока, который мы хотим вернуть в ассоциативный список.


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

8.1 Структурная обработка исключений
8.1.1 Исходный текст драйвера seh
8.1.2 Устанавливаем SEH-фрейм
8.1.3 Обрабатываем исключение
8.1.4 Удаляем SEH-фрейм
8.1.5 Используем макросы для установки/удаления SEH-фрейма
8.2 Использование раздела для обмена данными
8.2.1 Исходный текст программы управления драйвером SharedSection
8.2.2 Исходный текст драйвера SharedSection
Исходный текст: KmdKit\examples\basic\MemoryWorks\SharedSection

Прежде чем обратиться непосредственно к теме этой статьи, вкратце рассмотрим структурную обработку исключений (Structured Exception Handling, SEH), т.к. она нам потребуется.

8.1 Структурная обработка исключений

Я не буду подробно рассказывать о том, что это такое. Если дружите с английским языком, настоятельно рекомендую найти и прочитать статью “Win32 Exception handling for assembler programmers” by Jeremy Gordon. Она написана применительно к режиму пользователя, но SEH – это универсальный механизм обработки исключений, как в режиме пользователя, так и в режиме ядра. Существует, однако, одно, но очень существенное отличие: в ядре с помощью SEH можно обработать не все исключения! Например, попытка деления на ноль приведет к краху системы даже при установленном обработчике SEH. Самое ужасное, что обращение к невыделенной памяти ядра также приводит к появлению BSOD. А обращение к невыделенной памяти режима пользователя легко обрабатывается SEH. Так что единственная возможность избежать краха – писать код так, чтобы он не вызывал необрабатываемых исключений.

8.1.1 Исходный текст драйвера seh

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  SEH – Структурная обработка исключений.
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

include seh0.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       С Т Р У К Т У Р Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

SEH STRUCT
    SafeEip         dd  ?   ; Точка, с которой будет продолжено выполнение потока
    PrevEsp         dd  ?   ; Предыдущее значение esp
    PrevEbp         dd  ?   ; Предыдущее значение ebp
SEH ENDS

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                    Н Е И Н И Ц И А Л И З И Р О В А Н Н Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.data?

seh SEH <>

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       BuggyReader
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

BuggyReader proc

    xor eax, eax
    mov eax, [eax]              ; !!! Без SEH - BSOD !!!

    ret

BuggyReader endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       BuggyWriter
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

BuggyWriter proc

    mov eax, MmUserProbeAddress
    mov eax, [eax]
    mov eax, [eax]

    mov byte ptr [eax], 0       ; !!! Без SEH - BSOD !!!

    ret

BuggyWriter endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      ExceptionHandler
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

ExceptionHandler proc uses esi pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD

    mov esi, pExcept

    invoke DbgPrint, $CTA0("\nSEH: An exception %08X has occured\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionCode]

    .if [esi][EXCEPTION_RECORD.ExceptionCode] == 0C0000005h

        ; Если произошло исключение типа EXCEPTION_ACCESS_VIOLATION,
        ; то выводим дополнительную информацию.

        invoke DbgPrint, $CTA0("     Access violation at address: %08X\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionAddress]

        .if [esi][EXCEPTION_RECORD.ExceptionInformation][0]         ; Попытка чтения или записи ?

            invoke DbgPrint, $CTA0("     The code tried to write to address %08X\n\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionInformation][4]
        .else
            invoke DbgPrint, $CTA0("     The code tried to read from address %08X\n\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionInformation][4]
        .endif
    .endif

    lea eax, seh
    push (SEH PTR [eax]).SafeEip
    push (SEH PTR [eax]).PrevEsp
    push (SEH PTR [eax]).PrevEbp

    mov eax, pContext
    pop (CONTEXT PTR [eax]).regEbp
    pop (CONTEXT PTR [eax]).regEsp
    pop (CONTEXT PTR [eax]).regEip

    xor eax, eax
    ret

ExceptionHandler endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke DbgPrint, $CTA0("\nSEH: Entering DriverEntry\n")

    ;::::::::::::::::::::::::::::::::
    ; Ставим SEH “вручную”
    ;::::::::::::::::::::::::::::::::

    assume fs:nothing
    push offset ExceptionHandler
    push fs:[0]
    mov fs:[0], esp
    assume fs:error

    mov seh.SafeEip, offset SafePlace
    mov seh.PrevEbp, ebp
    mov seh.PrevEsp, esp

    invoke BuggyReader

SafePlace:

    assume fs:nothing
    pop fs:[0]
    add esp, sizeof DWORD
    assume fs:error

    ;:::::::::::::::::::::::::::::::::::::::::::::::
    ; Используем макросы. Это чуть-чуть проще :-)
    ;:::::::::::::::::::::::::::::::::::::::::::::::

    _try

    invoke BuggyWriter

    _finally

    invoke DbgPrint, $CTA0("\nSEH: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=seh

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

8.1.2 Устанавливаем SEH-фрейм

    assume fs:nothing

По умолчанию masm запрещает использование регистра fs. Снимаем это ограничение с помощью директивы assume.

    push offset ExceptionHandler
    push fs:[0]
    mov fs:[0], esp

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

    assume fs:error

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

    mov seh.SafeEip, offset SafePlace
    mov seh.PrevEbp, ebp
    mov seh.PrevEsp, esp

Для того чтобы после обработки исключения наш обработчик смог возобновить выполнение потока мы должны сохранить значения регистров esp, ebp и указать на точку, с которой поток продолжит выполнение. Сохраняем эти три значения в структуре seh и вызываем процедуру BuggyReader, которая попытается прочитать двойное слово по адресу 00000000.

BuggyReader proc

    xor eax, eax
    mov eax, [eax]

    ret

BuggyReader endp

Обращение по нулевому указателю являлось столь частой ошибкой, что Microsoft пришлось отвести 64 килобайта памяти в диапазоне 00000000-0000FFFFh и сделать этот регион недоступным. Обращение к любому байту в этом диапазоне приводит к исключению типа EXCEPTION_ACCESS_VIOLATION.

8.1.3 Обрабатываем исключение

При попытке чтения с адреса 00000000 в процедуре BuggyReader происходит исключение, и мы попадаем в установленный нами обработчик.

    mov esi, pExcept

    invoke DbgPrint, $CTA0("\nSEH: An exception %08X has occured\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionCode]

    .if [esi][EXCEPTION_RECORD.ExceptionCode] == 0C0000005h

        invoke DbgPrint, $CTA0("     Access violation at address: %08X\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionAddress]

        .if [esi][EXCEPTION_RECORD.ExceptionInformation][0]

            invoke DbgPrint, $CTA0("     The code tried to write to address %08X\n\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionInformation][4]
        .else
            invoke DbgPrint, $CTA0("     The code tried to read from address %08X\n\n"), \
                        [esi][EXCEPTION_RECORD.ExceptionInformation][4]
        .endif
    .endif

Первым делом наш обработчик выводит соответствующее отладочное сообщение и, если произошло исключение типа EXCEPTION_ACCESS_VIOLATION, кое-какую дополнительную информацию. Затем приступаем к собственно обработке исключения.

    lea eax, seh
    push (SEH PTR [eax]).SafeEip
    push (SEH PTR [eax]).PrevEsp
    push (SEH PTR [eax]).PrevEbp

    mov eax, pContext
    pop (CONTEXT PTR [eax]).regEbp
    pop (CONTEXT PTR [eax]).regEsp
    pop (CONTEXT PTR [eax]).regEip

В данном примере обработка исключения заключается в простом восстановлении значений в регистрах esp, ebp и помещении в регистр eip адреса, с которого безопасно продолжить выполнение потока. Эти данные обработчик извлекает из предварительно заполненной нами структуры seh и помещает их в структуру CONTEXT, указатель на которую передается системой.

    xor eax, eax
    ret

Возвращаем значение ExceptionContinueExecution равное нулю, которое сообщает системе, что она должна перезагрузить контекст потока и продолжить его выполнение. Т.к. значение eip равно адресу метки SafePlace, а значения в регистрах esp и ebp восстановлены, то поток продолжит своё выполнение с метки SafePlace, как ни в чем не бывало.

8.1.4 Удаляем SEH-фрейм

SafePlace:

    assume fs:nothing
    pop fs:[0]
    add esp, sizeof DWORD
    assume fs:error

Восстанавливаем старое значение в fs:[0] и корректируем указатель стека (подробности см. в вышеупомянутой статье).

8.1.5 Используем макросы для установки/удаления SEH-фрейма

Проделаем ту же самую операцию с помощью макросов, которые я написал для упрощения работы с SEH. Макросы определены в файле seh0.inc. Описывать их я не буду, т.к. делают они то же самое, что мы только что разобрали. Если в обработчике исключений не требуется какой-то особой обработки, то при использовании макросов _try/_finally весь вышеприведенный код сведется к трем строчкам. Точкой SafePlace, в данном случае, будет строка, в которой стоит макрос _finally.

    _try

    invoke BuggyWriter

    _finally

На этот раз вызовем процедуру BuggyWriter, которая попытается записать двойное слово по адресу MmUserProbeAddress (7FFF0000h).

BuggyWriter proc

    mov eax, MmUserProbeAddress
    mov eax, [eax]
    mov eax, [eax]

    mov byte ptr [eax], 0
    ret

BuggyWriter endp

64Кб памяти в диапазоне 7FFF0000h – 7FFFFFFFh являются недоступным регионом, зарезервированным для предотвращения передачи буферов через границу пользовательского и системного пространства. Стартовый адрес этого региона содержится в экспортируемой ядром переменной MmUserProbeAddress.

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

8.2 Использование раздела для обмена данными

Windows предоставляет богатейший набор механизмов для обмена данными между двумя и более процессами (Interprocess Communications, IPC): буфер обмена, DDE, оконные сообщения (в частности WM_COPYDATA), почтовые ящики (mailslot), сокеты (sockets) и др. Все эти механизмы, так или иначе, базируются на объекте “проекция файла” (file-mapping object), представляющем собой блок памяти доступный двум и более процессам для совместного использования. В терминологии DDK проецируемые файлы являются объектами “раздел” (section object), в просторечии называемыми секциями (не путать с секциями PE-файла).

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

Совместное использование данных с помощью объекта “раздел” происходит следующим образом: один процесс создает отображаемый в память файл, вызывая функцию CreateFileMapping. Затем, вызовом функции MapViewOfFile (если спуститься на один уровень глубже, то вызывается NtMapViewOfSection), отображает его представление (view) на свое адресное пространство, а другой процесс через OpenFileMapping открывает тот же самый отображаемый файл и так же отображает его, но уже в свое адресное пространство. В результате одни и те же физические страницы памяти становятся доступными в обоих процессах, что позволяет им легко передавать через эту область большие порции данных, т.к. любые изменения на этих страницах в одном процессе отражаются на представление в другом процессе.

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

8.2.1 Исходный текст программы управления драйвером SharedSection

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  SharedSection – Клиент драйвера SharedSection.sys
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\w2k\native.inc
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\winioctl.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
include \masm32\include\w2k\ntdll.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
includelib \masm32\lib\w2k\ntdll.lib

include \masm32\Macros\Strings.mac

include ..\common.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                    CallDriver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

CallDriver proc

local fOk:BOOL

local hSCManager:HANDLE
local hService:HANDLE
local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local hDevice:HANDLE

local abyOutBuffer[4]:BYTE
local dwBytesReturned:DWORD

    and fOk, FALSE

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("SharedSection.sys"), sizeof acModulePath, addr acModulePath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("SharedSection"), $CTA0("One way to share section"), \
            SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
            SERVICE_ERROR_IGNORE, addr acModulePath, NULL, NULL, NULL, NULL, NULL

        .if eax != NULL
            mov hService, eax

            invoke StartService, hService, 0, NULL
            .if eax != 0

                invoke CreateFile, $CTA0("\\\\.\\SharedSection"), 0, \
                                        0, NULL, OPEN_EXISTING, 0, NULL

                .if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax

                    ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

                    invoke DeviceIoControl, hDevice, IOCTL_SHARE_MY_SECTION, NULL, 0, NULL, 0, \
                                                addr dwBytesReturned, NULL
                    .if eax != 0
                        inc fOk
                    .else
                        invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
                                                    MB_OK + MB_ICONSTOP
                    .endif

                    ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

                    invoke CloseHandle, hDevice
                .else
                    invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
                .endif
                invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
            .else
                invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
            .endif
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
                                NULL, MB_OK + MB_ICONSTOP
    .endif

    mov eax, fOk
    ret

CallDriver endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

start proc

local hSection:HANDLE
local liSectionSize:_LARGE_INTEGER
local oa:OBJECT_ATTRIBUTES
local pSectionBaseAddress:PVOID
local liViewSize:_LARGE_INTEGER

    and liSectionSize.HighPart, 0
    mov liSectionSize.LowPart, SECTION_SIZE

    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usSectionName, OBJ_CASE_INSENSITIVE, NULL, NULL

    invoke ZwCreateSection, addr hSection, SECTION_MAP_WRITE + SECTION_MAP_READ, addr oa, \
                            addr liSectionSize, PAGE_READWRITE, SEC_COMMIT, NULL
    .if eax == STATUS_SUCCESS

        and pSectionBaseAddress, NULL
        and liViewSize.HighPart, 0
        and liViewSize.LowPart, 0
        invoke ZwMapViewOfSection, hSection, NtCurrentProcess, addr pSectionBaseAddress, 0, \
                          SECTION_SIZE, NULL, addr liViewSize, ViewShare, 0, PAGE_READWRITE
        .if eax == STATUS_SUCCESS

            CTA ".revird ecived a dna sessecorp resu neewteb yromem ", g_szStrToReverse
            CTA "erahs ot euqinhcet emas eht esu nac uoy ,revewoH "
            CTA ".sessecorp resu gnoma yromem gnirahs rof desu euqinhcet "
            CTA0 "nommoc a si elif gnigap eht yb dekcab elif deppam-yromem A"

            invoke strcpy, pSectionBaseAddress, addr g_szStrToReverse

            invoke CallDriver
            .if eax == TRUE
                invoke MessageBox, NULL, pSectionBaseAddress, \
                                $CTA0("HOWTO: Share Memory Between User Mode and Kernel Mode"), \
                                MB_OK + MB_ICONINFORMATION
            .endif

            invoke ZwUnmapViewOfSection, NtCurrentProcess, pSectionBaseAddress
        .else
            invoke MessageBox, NULL, $CTA0("Can't map section."), NULL, MB_OK + MB_ICONSTOP
        .endif

        invoke ZwClose, hSection
    .else
        invoke MessageBox, NULL, $CTA0("Can't create section."), NULL, MB_OK + MB_ICONSTOP
    .endif

    invoke ExitProcess, 0
    ret

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

:make

set exe=SharedSection

if exist ..\%exe%.exe del ..\%exe%.exe

\masm32\bin\ml /nologo /c /coff %exe%.bat
\masm32\bin\link /nologo /subsystem:windows %exe%.obj

del %exe%.obj
move %exe%.exe ..
if exist %exe%.exe del %exe%.exe

echo.
pause

Разберем только ключевые моменты, все остальное должно быть понятно без комментариев.

    and liSectionSize.HighPart, 0
    mov liSectionSize.LowPart, SECTION_SIZE

При создании раздела придется указать его размер. Т.к. этот размер может превышать 4Гб, то для задания размера используется переменная типа LARGE_INTEGER, которую мы инициализируем значением SECTION_SIZE равным размеру одной страницы (4Кб). Константа SECTION_SIZE определена в файле common.inc.

    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usSectionName, OBJ_CASE_INSENSITIVE, NULL, NULL

С макросом InitializeObjectAttributes мы уже знакомы. Поскольку для последующего вызова ZwCreateSection требуется заполненная соответствующим образом структура OBJECT_ATTRIBUTES, то мы это и делаем, пользуясь этим макросом.

Раздел, который мы собираемся совместно использовать должен быть именованным, для того чтобы его можно было открыть по имени. Имя раздела определено в файле common.inc таким образом:

.const
CCOUNTED_UNICODE_STRING	"\\BaseNamedObjects\\UserKernelSharedSection", g_usSectionName, 4

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

    invoke ZwCreateSection, addr hSection, SECTION_MAP_WRITE + SECTION_MAP_READ, addr oa, \
                            addr liSectionSize, PAGE_READWRITE, SEC_COMMIT, NULL

Вызовом функции ZwCreateSection, создаем именованный объект “раздел” размером SECTION_SIZE доступный для чтения и записи. Если раздел будет создан, то в переменной hSection получим его описатель.

        and pSectionBaseAddress, NULL
        and liViewSize.HighPart, 0
        and liViewSize.LowPart, 0
        invoke ZwMapViewOfSection, hSection, NtCurrentProcess, addr pSectionBaseAddress, 0, SECTION_SIZE, \
                                    NULL, addr liViewSize, ViewShare, 0, PAGE_READWRITE

Отображаем представление всего раздела в память. Здесь довольно много параметров – все они подробно описаны в DDK. Т.к. переменная pSectionBaseAddress инициализирована нулевым значением, система сама определит по какому виртуальному адресу ей удобнее отобразить раздел и вернет этот адрес в этой же переменной. Инициализированная нулем переменная liViewSize определяет, что раздел будет отображен полностью.

            CTA ".revird ecived a dna sessecorp resu neewteb yromem ", g_szStrToReverse
            CTA "erahs ot euqinhcet emas eht esu nac uoy ,revewoH "
            CTA ".sessecorp resu gnoma yromem gnirahs rof desu euqinhcet "
            CTA0 "nommoc a si elif gnigap eht yb dekcab elif deppam-yromem A"

            invoke strcpy, pSectionBaseAddress, addr g_szStrToReverse

Копируем в полученное представление перевернутую строку. Задачей драйвера будет привести эту строку к читабельному виду.

            invoke CallDriver
            .if eax == TRUE
                invoke MessageBox, NULL, pSectionBaseAddress, \
                                $CTA0("HOWTO: Share Memory Between User Mode and Kernel Mode"), \
                                MB_OK + MB_ICONINFORMATION
            .endif

Значение TRUE возвращенное процедурой CallDriver означает, что драйвер справился со своей задачей. Показываем результаты его работы. В процедуре CallDriver мы проводим обычные операции регистрации и запуска драйвера и посылаем ему управляющий код IOCTL_SHARE_MY_SECTION.

            invoke ZwUnmapViewOfSection, NtCurrentProcess, pSectionBaseAddress
        .endif
        invoke ZwClose, hSection

Приводим систему в исходное состояние.

8.2.2 Исходный текст драйвера SharedSection

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  SharedSection – использует раздел, созданный программой управления.
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\native.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

include ..\common.inc
include seh0.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.const

CCOUNTED_UNICODE_STRING "\\Device\\SharedSection", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\DosDevices\\SharedSection", g_usSymbolicLinkName, 4

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                              К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                   DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    mov (_IRP PTR [eax]).IoStatus.Status, STATUS_SUCCESS
    and (_IRP PTR [eax]).IoStatus.Information, 0

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchControl proc uses esi edi pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

local hSection:HANDLE
local oa:OBJECT_ATTRIBUTES
local pSectionBaseAddress:PVOID
local liViewSize:LARGE_INTEGER

    invoke DbgPrint, $CTA0("\nSharedSection: Entering DispatchControl\n")

    mov esi, pIrp
    assume esi:ptr _IRP

    mov [esi].IoStatus.Status, STATUS_UNSUCCESSFUL
    and [esi].IoStatus.Information, 0

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_SHARE_MY_SECTION

        invoke DbgPrint, $CTA0("SharedSection: Opening section object\n")

        lea ecx, oa
        InitializeObjectAttributes ecx, offset g_usSectionName, OBJ_CASE_INSENSITIVE, NULL, NULL
        invoke ZwOpenSection, addr hSection, SECTION_MAP_WRITE + SECTION_MAP_READ, addr oa
        .if eax == STATUS_SUCCESS

            invoke DbgPrint, $CTA0("SharedSection: Section object opened\n")

            and pSectionBaseAddress, NULL
            and liViewSize.HighPart, 0
            and liViewSize.LowPart, 0
            invoke ZwMapViewOfSection, hSection, NtCurrentProcess, addr pSectionBaseAddress, 0, \
                         SECTION_SIZE, NULL, addr liViewSize, ViewShare, 0, PAGE_READWRITE
            .if eax == STATUS_SUCCESS

                invoke DbgPrint, \
                   $CTA0("SharedSection: Section mapped at address %08X\n"), pSectionBaseAddress

                _try

                invoke _strrev, pSectionBaseAddress
                mov [esi].IoStatus.Status, STATUS_SUCCESS

                invoke DbgPrint, $CTA0("SharedSection: String reversed\n")

                _finally

                invoke ZwUnmapViewOfSection, NtCurrentProcess, pSectionBaseAddress

                invoke DbgPrint, $CTA0("SharedSection: Section at address %08X unmapped \n"), \
                                           pSectionBaseAddress

            .else
                invoke DbgPrint, \
                       $CTA0("SharedSection: Couldn't map view of section. Status: %08X\n"), eax
            .endif
            invoke ZwClose, hSection
            invoke DbgPrint, $CTA0("SharedSection: Section object handle closed\n")
        .else
            invoke DbgPrint, $CTA0("SharedSection: Couldn't open section. Status: %08X\n"), eax
        .endif

    .else
        mov [esi].IoStatus.Status, STATUS_INVALID_DEVICE_REQUEST
    .endif

    push [esi].IoStatus.Status

    assume edi:nothing
    assume esi:nothing

    fastcall IofCompleteRequest, esi, IO_NO_INCREMENT

    invoke DbgPrint, $CTA0("SharedSection: Leaving DispatchControl\n")

    pop eax
    ret

DispatchControl endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverUnload proc pDriverObject:PDRIVER_OBJECT

    invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

    mov eax, pDriverObject
    invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

    ret

DriverUnload endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code INIT

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local pDeviceObject:PDEVICE_OBJECT

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                     0, TRUE, addr pDeviceObject
    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:ptr DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],          offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],  offset DispatchControl
            mov [eax].DriverUnload,                                         offset DriverUnload
            assume eax:nothing
            mov status, STATUS_SUCCESS
        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

    mov eax, status
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=SharedSection

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

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

        lea ecx, oa
        InitializeObjectAttributes ecx, offset g_usSectionName, OBJ_CASE_INSENSITIVE, NULL, NULL
        invoke ZwOpenSection, addr hSection, SECTION_MAP_WRITE + SECTION_MAP_READ, addr oa

Получив управляющий код IOCTL_SHARE_MY_SECTION, драйвер пытается открыть объект раздел с именем определенным в переменной g_usSectionName (см. common.inc).

        .if eax == STATUS_SUCCESS
            and pSectionBaseAddress, NULL
            and liViewSize.HighPart, 0
            and liViewSize.LowPart, 0
            invoke ZwMapViewOfSection, hSection, NtCurrentProcess, addr pSectionBaseAddress, 0, SECTION_SIZE, \
                                   NULL, addr liViewSize, ViewShare, 0, PAGE_READWRITE
            .if eax == STATUS_SUCCESS

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

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

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

                _try

                invoke _strrev, pSectionBaseAddress
                mov [esi].IoStatus.Status, STATUS_SUCCESS

                _finally

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

                invoke ZwUnmapViewOfSection, NtCurrentProcess, pSectionBaseAddress
            .endif
            invoke ZwClose, hSection
        .endif

Освобождаем занятые ресурсы. В программе управления функция DeviceIoControl вернет код успеха и на экране отобразится восстановленная строка.

Рис. 8-1. Результат работы программы SharedSection.exe


Разделяемая память

9.1 Исходный текст драйвера SharingMemory
9.1.1 Процедура DriverEntry
9.1.2 Процедура DispatchControl
9.1.3 Memory Descriptor List
9.1.4 Процедура Cleanup
9.2 Исходный текст программы управления драйвером SharingMemory
Исходный текст: KmdKit\examples\basic\MemoryWorks\SharingMemory

В предыдущем примере SharedSection, где мы совместно использовали раздел, драйвер был жестко привязан к адресному контексту конкретного процесса, т.к. виртуальный адрес имевшийся у драйвера указывал в адресное пространство этого процесса. Метод, который мы используем в этом примере лишен этого недостатка. Для драйверов этот метод намного более естественен.

9.1 Исходный текст драйвера SharingMemory

Сначала разберёмся с тем, что происходит в драйвере.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; SharingMemory - Пример того, как драйвер может передать в пользовательский процесс
;                 используемую им область памяти
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc

include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\hal.inc

includelib \masm32\lib\w2k\ntoskrnl.lib
includelib \masm32\lib\w2k\hal.lib

include \masm32\Macros\Strings.mac

include ..\common.inc
include seh0.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.const
CCOUNTED_UNICODE_STRING "\\Device\\SharingMemory", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\DosDevices\\SharingMemory", g_usSymbolicLinkName, 4

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                    Н Е И Н И Ц И А Л И З И Р О В А Н Н Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.data?
g_pSharedMemory     PVOID   ?
g_pMdl              PVOID   ?
g_pUserAddress      PVOID   ?

g_fTimerStarted     BOOL    ?

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        UpdateTime
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

UpdateTime proc

local SysTime:LARGE_INTEGER

    invoke KeQuerySystemTime, addr SysTime
    invoke ExSystemTimeToLocalTime, addr SysTime, g_pSharedMemory

    ret

UpdateTime endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       TimerRoutine
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

TimerRoutine proc pDeviceObject:PDEVICE_OBJECT, pContext:PVOID

    invoke UpdateTime

    ret

TimerRoutine endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          Cleanup
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

Cleanup proc pDeviceObject:PDEVICE_OBJECT

    .if g_fTimerStarted
        invoke IoStopTimer, pDeviceObject
        invoke DbgPrint, $CTA0("SharingMemory: Timer stopped\n")
    .endif

    .if ( g_pUserAddress != NULL ) && ( g_pMdl != NULL )
        invoke MmUnmapLockedPages, g_pUserAddress, g_pMdl
        invoke DbgPrint, $CTA0("SharingMemory: Memory at address %08X unmapped\n"), g_pUserAddress
        and g_pUserAddress, NULL
    .endif

    .if g_pMdl != NULL
        invoke IoFreeMdl, g_pMdl
        invoke DbgPrint, $CTA0("SharingMemory: MDL at address %08X freed\n"), g_pMdl
        and g_pMdl, NULL
    .endif

    .if g_pSharedMemory != NULL
        invoke ExFreePool, g_pSharedMemory
        invoke DbgPrint, $CTA0("SharingMemory: Memory at address %08X released\n"), g_pSharedMemory
        and g_pSharedMemory, NULL
    .endif

    ret

Cleanup endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     DispatchCleanup
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchCleanup proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    invoke DbgPrint, $CTA0("\nSharingMemory: Entering DispatchCleanup\n")

    invoke Cleanup, pDeviceObject

    mov eax, pIrp
    mov (_IRP PTR [eax]).IoStatus.Status, STATUS_SUCCESS
    and (_IRP PTR [eax]).IoStatus.Information, 0

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    invoke DbgPrint, $CTA0("SharingMemory: Leaving DispatchCleanup\n")

    mov eax, STATUS_SUCCESS
    ret

DispatchCleanup endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                   DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    mov (_IRP PTR [eax]).IoStatus.Status, STATUS_SUCCESS
    and (_IRP PTR [eax]).IoStatus.Information, 0

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchControl proc uses esi edi pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

local dwContext:DWORD

    invoke DbgPrint, $CTA0("\nSharingMemory: Entering DispatchControl\n")

    mov esi, pIrp
    assume esi:ptr _IRP

    mov [esi].IoStatus.Status, STATUS_UNSUCCESSFUL
    and [esi].IoStatus.Information, 0

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GIVE_ME_YOUR_MEMORY
        .if [edi].Parameters.DeviceIoControl.OutputBufferLength >= sizeof PVOID

            invoke ExAllocatePool, NonPagedPool, PAGE_SIZE
            .if eax != NULL
                mov g_pSharedMemory, eax

                invoke DbgPrint, \
                $CTA0("SharingMemory: %X bytes of nonpaged memory allocated at address %08X\n"), \
                PAGE_SIZE, g_pSharedMemory

                invoke IoAllocateMdl, g_pSharedMemory, PAGE_SIZE, FALSE, FALSE, NULL
                .if eax != NULL
                    mov g_pMdl, eax

                    invoke DbgPrint, \
                            $CTA0("SharingMemory: MDL allocated at address %08X\n"), g_pMdl

                    invoke MmBuildMdlForNonPagedPool, g_pMdl

                    _try

                    invoke MmMapLockedPagesSpecifyCache, g_pMdl, UserMode, MmCached, \
                                        NULL, FALSE, NormalPagePriority
                    .if eax != NULL

                        mov g_pUserAddress, eax

                        invoke DbgPrint, \
                        $CTA0("SharingMemory: Memory mapped into user space at address %08X\n"), \
                        g_pUserAddress

                        mov eax, [esi].AssociatedIrp.SystemBuffer
                        push g_pUserAddress
                        pop dword ptr [eax]

                        invoke UpdateTime

                        invoke IoInitializeTimer, pDeviceObject, TimerRoutine, addr dwContext
                        .if eax == STATUS_SUCCESS

                            invoke IoStartTimer, pDeviceObject
                            inc g_fTimerStarted

                            invoke DbgPrint, $CTA0("SharingMemory: Timer started\n")

                            mov [esi].IoStatus.Information, sizeof PVOID
                            mov [esi].IoStatus.Status, STATUS_SUCCESS

                        .endif
                    .endif

                    _finally

                .endif
            .endif

        .else
            mov [esi].IoStatus.Status, STATUS_BUFFER_TOO_SMALL
        .endif
    .else
        mov [esi].IoStatus.Status, STATUS_INVALID_DEVICE_REQUEST
    .endif

    assume edi:nothing

    .if [esi].IoStatus.Status != STATUS_SUCCESS

        invoke DbgPrint, $CTA0("SharingMemory: Something went wrong\:\n")

        invoke Cleanup, pDeviceObject

    .endif

    push [esi].IoStatus.Status

    assume esi:nothing

    fastcall IofCompleteRequest, esi, IO_NO_INCREMENT

    invoke DbgPrint, $CTA0("SharingMemory: Leaving DispatchControl\n")

    pop eax
    ret

DispatchControl endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverUnload proc pDriverObject:PDRIVER_OBJECT

    invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

    mov eax, pDriverObject
    invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

    ret

DriverUnload endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code INIT

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local pDeviceObject:PDEVICE_OBJECT

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    and g_pSharedMemory, NULL
    and g_pMdl, NULL
    and g_pUserAddress, NULL
    and g_fTimerStarted, FALSE

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                     0, TRUE, addr pDeviceObject
    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:ptr DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],          offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLEANUP*(sizeof PVOID)],         offset DispatchCleanup
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],  offset DispatchControl
            mov [eax].DriverUnload,                                         offset DriverUnload
            assume eax:nothing
            mov status, STATUS_SUCCESS
        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

    mov eax, status
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=SharingMemory

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

9.1.1 Процедура DriverEntry

            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],          offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLEANUP*(sizeof PVOID)],         offset DispatchCleanup
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],  offset DispatchControl

Помимо обычных запросов IRP_MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_DEVICE_CONTROL будем обрабатывать также IRP_MJ_CLEANUP. Когда код режима пользователя вызывает CloseHandle, драйверу сначала посылается IRP_MJ_CLEANUP, сигнализируя о том, что описатель устройства драйвера сейчас будет закрыт. После того как описатель будет фактически закрыт, драйвер получает IRP_MJ_CLOSE. В данном примере желательно освободить ресурсы как можно раньше. Поэтому и потребовалась обработка IRP_MJ_CLEANUP.

9.1.2 Процедура DispatchControl

            invoke ExAllocatePool, NonPagedPool, PAGE_SIZE
            .if eax != NULL
                mov g_pSharedMemory, eax

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

ExAllocatePool вернёт адрес из системного диапазона, т.е. обращаться по нему драйвер может независимо от текущего контекста. Теперь необходимо отобразить эту память в адресное пространство того процесса, с которым мы хотим эту память совместно использовать. Наш драйвер одноуровневый, поэтому при обработке IRP_MJ_DEVICE_CONTROL мы находимся в адресном контексте нашей программы управления. Прежде чем мы отобразим в её адресное пространство выделенную страницу памяти необходимо сформировать MDL (Memory Descriptor List. Перевод этого термина на русский язык я не знаю).

9.1.3 Memory Descriptor List

MDL представляется одноименной структурой и содержит описание физических страниц региона памяти.

MDL STRUCT
    Next            PVOID       ?
    _Size           SWORD       ?
    MdlFlags        SWORD       ?
    Process         PVOID       ?
    MappedSystemVa  PVOID       ?
    StartVa         PVOID       ?
    ByteCount       DWORD       ?
    ByteOffset      DWORD       ?
MDL ENDS
PMDL typedef PTR MDL

Точнее говоря, структура MDL это заголовок. Сразу за заголовком идет массив двойных слов Pages, каждое из которых представляет собой номер физической страницы (page frame number, PFN). Хотя регион памяти описываемый MDL является непрерывным в виртуальном пространстве адресов, занимаемые им физические страницы могут располагаться в физической памяти в произвольном порядке. Именно поэтому и нужен массив Pages, содержащий список всех физических страниц занимаемых регионом памяти. Также он требуется для организации прямого доступа к памяти (Direct Memory Access, DMA). Т.о. MDL содержит всю необходимую диспетчеру памяти информацию. В нашем случае страница всего одна.

                invoke IoAllocateMdl, g_pSharedMemory, PAGE_SIZE, FALSE, FALSE, NULL
                .if eax != NULL
                    mov g_pMdl, eax

Первые два параметра функции IoAllocateMdl определяют виртуальный адрес и размер блока памяти, для которого надо сформировать MDL. Если MDL не ассоциируется с IRP (именно так и есть в нашем случае), то третий параметр равен FALSE. Четвертый параметр определяет, нужно ли уменьшить квоту процесса, и применим только для драйверов находящихся на самом верхнем уровне в цепочке драйверов или для одноуровневых драйверов (именно таковым наш драйвер и является). Каждый процесс получает от системы квоты на ресурсы. Когда процесс выделяет себе ресурс, квота уменьшается. Если квота кончается, то и соответствующий ресурс больше не выделяется. Мы не хотим уменьшать квоту процесса на выделяемую память, поэтому четвертый параметр будет равен FALSE. Последний параметр определяет необязательный указатель на IRP, с которым ассоциируется MDL. Например, при прямом вводе-выводе диспетчер ввода-вывода создает MDL для пользовательского буфера и передает его адрес в IRP.MdlAddress. Мы формируем MDL не для операции ввода-вывода, поэтому никакого IRP у нас нет, и последний параметр будет равен NULL.

Т.о. функция IoAllocateMdl выделяет память для MDL и инициализирует его заголовок.

                    invoke MmBuildMdlForNonPagedPool, g_pMdl

MmBuildMdlForNonPagedPool заполняет массив номеров физических страниц и обновляет некоторые поля заголовка MDL.

                    _try

Если параметр AccessMode функции MmMapLockedPagesSpecifyCache, которую мы сейчас будем вызывать, равен UserMode и её вызов окончится неудачей, система вбрасывает исключение (это явно указано в DDK), которое мы сможем обработать, поставив SEH-фрейм.

                    invoke MmMapLockedPagesSpecifyCache, g_pMdl, UserMode, MmCached, \
                                        NULL, FALSE, NormalPagePriority

Отображаем память, описываемую MDL в адресное пространство нашей программы управления.

Первый параметр указывает на MDL, описывающий регион памяти подлежащий отображению. Второй параметр определяет, можно ли будет обращаться к этой памяти из режима пользователя. Третий определяет политику кэширования этой памяти процессором. Если четвертый параметр равен NULL, то система сама выберет виртуальный адрес в пользовательском пространстве. Пятый параметр определяет, появится ли BSOD, если вдруг система не сможет удовлетворить запрос, но только если второй параметр равен KernelMode. Тем не менее, передаем в этом параметре FALSE, т.к. не хотим рушить систему ни при каких обстоятельствах. Последний параметр определяет насколько важно, чтобы вызов MmMapLockedPagesSpecifyCache прошёл успешно.

В Windows NT4 функция MmMapLockedPagesSpecifyCache не реализована. Вместо неё используйте MmMapLockedPages таким образом:

invoke MmMapLockedPages, g_pMdl, UserMode

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

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

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

                    .if eax != NULL
                        mov g_pUserAddress, eax

                        mov eax, [esi].AssociatedIrp.SystemBuffer
                        push g_pUserAddress
                        pop dword ptr [eax]

MmMapLockedPagesSpecifyCache вернет адрес из пользовательского диапазона, по которому она отобразила нашу страницу. Передаем его в программу управления. Т.о. с этого момента страница памяти становится совместно используемой, причём драйвер сможет обращаться к ней независимо от текущего адресного контекста, а пользовательский процесс будет обращаться к ней по доступному ему адресу.

                        invoke UpdateTime

Для наглядности процедура UpdateTime будет помещать на разделяемую страницу текущее системное время.

UpdateTime proc

local SysTime:LARGE_INTEGER

    invoke KeQuerySystemTime, addr SysTime
    invoke ExSystemTimeToLocalTime, addr SysTime, g_pSharedMemory

    ret

UpdateTime endp

KeQuerySystemTime сообщает системное время по Гринвичу. ExSystemTimeToLocalTime корректирует его с учётом часового пояса.

                        invoke IoInitializeTimer, pDeviceObject, TimerRoutine, addr dwContext

Инициализируем таймер, который будет ассоциирован с управляемым драйвером объектом "устройство". Для этого в структуре DEVICE_OBJECT имеется поле Timer, являющееся указателем на структуру IO_TIMER. Первый параметр функции IoInitializeTimer определяет, с каким объектом "устройство" надо связать таймер. Второй - указатель на процедуру вызываемую системой при срабатывании таймера. Процедура TimerRoutine будет обновлять системное время на разделяемой странице, вызывая UpdateTime. TimerRoutine выполняется при IRQL = DISPATCH_LEVEL (об этом явно написано в DDK). Это вторая и главная причина, по которой нам требуется неподкачиваемая память. Последний параметр функции IoInitializeTimer - указатель на произвольные данные. Этот указатель будет передан в TimerRoutine. Нам никакие дополнительные данные не нужны, поэтому dwContext у нас просто фиктивная переменная.

                        .if eax == STATUS_SUCCESS

                            invoke IoStartTimer, pDeviceObject
                            inc g_fTimerStarted

Запускаем таймер. Теперь процедура TimerRoutine будет вызываться примерно раз в секунду. Изменить этот интервал нельзя.

    .if [esi].IoStatus.Status != STATUS_SUCCESS
        invoke Cleanup, pDeviceObject
    .endif

Если на одном из предыдущих этапов возникли проблемы, проводим очистку ресурсов.

9.1.4 Процедура Cleanup

Cleanup proc pDeviceObject:PDEVICE_OBJECT

    .if g_fTimerStarted
        invoke IoStopTimer, pDeviceObject
    .endif

    .if ( g_pUserAddress != NULL ) && ( g_pMdl != NULL )
        invoke MmUnmapLockedPages, g_pUserAddress, g_pMdl
        and g_pUserAddress, NULL
    .endif

    .if g_pMdl != NULL
        invoke IoFreeMdl, g_pMdl
        and g_pMdl, NULL
    .endif

    .if g_pSharedMemory != NULL
        invoke ExFreePool, g_pSharedMemory
        and g_pSharedMemory, NULL
    .endif

    ret

Cleanup endp

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

9.2 Исходный текст программы управления драйвером SharingMemory

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; Клиент драйвера SharingMemory
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\include\winioctl.inc

include \masm32\Macros\Strings.mac

include ..\common.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

IDD_MAIN            equ 1000
IDC_TIME            equ 1001
IDI_ICON            equ 1002

TIMER_ID            equ     100

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                    Н Е И Н И Ц И А Л И З И Р О В А Н Н Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.data?
g_hDevice           HANDLE      ?
g_hInstance         HINSTANCE   ?
g_hDlg              HWND        ?
g_pSharedMemory     LPVOID      ?

g_hSCManager        HANDLE      ?
g_hService          HANDLE      ?

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             MyUnhandledExceptionFilter
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

MyUnhandledExceptionFilter proc lpExceptionInfo:PTR EXCEPTION_POINTERS

local _ss:SERVICE_STATUS

    invoke KillTimer, g_hDlg, TIMER_ID
    invoke CloseHandle, g_hDevice
    invoke ControlService, g_hService, SERVICE_CONTROL_STOP, addr _ss
    invoke DeleteService, g_hService
    invoke CloseServiceHandle, g_hService
    invoke CloseServiceHandle, g_hSCManager

    mov eax, EXCEPTION_EXECUTE_HANDLER
    ret

MyUnhandledExceptionFilter endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                              UpdateTime
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

UpdateTime proc

local stime:SYSTEMTIME
local buffer[64]:CHAR

    .if g_pSharedMemory != NULL
        invoke FileTimeToSystemTime, g_pSharedMemory, addr stime
        movzx eax, stime.wHour
        movzx ecx, stime.wMinute
        movzx edx, stime.wSecond

        invoke wsprintf, addr buffer, $CTA0("%02d:%02d:%02d"), eax, ecx, edx

        invoke SetDlgItemText, g_hDlg, IDC_TIME, addr buffer
    .endif

    ret

UpdateTime endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                               D I A L O G     P R O C E D U R E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DlgProc proc uses esi edi hDlg:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    mov eax, uMsg
    .if eax == WM_TIMER

        invoke UpdateTime

    .elseif eax == WM_INITDIALOG

        push hDlg
        pop g_hDlg

        invoke LoadIcon, g_hInstance, IDI_ICON
        invoke SendMessage, hDlg, WM_SETICON, ICON_BIG, eax

        invoke SetWindowText, hDlg, $CTA0("Kernel Timer")

        invoke UpdateTime

        invoke SetTimer, hDlg, TIMER_ID, 1000, NULL

    .elseif eax == WM_COMMAND

        mov eax, wParam
        .if ax == IDCANCEL
            invoke EndDialog, hDlg, 0
        .endif

    .elseif eax == WM_DESTROY

        invoke KillTimer, hDlg, TIMER_ID

    .else

        xor eax, eax
        ret

    .endif

    xor eax, eax
    inc eax
    ret

DlgProc endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

start proc uses esi edi

local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local dwBytesReturned:DWORD

    and g_pSharedMemory, NULL

    invoke SetUnhandledExceptionFilter, MyUnhandledExceptionFilter

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
    .if eax != NULL
        mov g_hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("SharingMemory.sys"), sizeof acModulePath, addr acModulePath, esp
        pop eax

        invoke CreateService, g_hSCManager, $CTA0("SharingMemory"), \
            $CTA0("Another way how to share memory"), SERVICE_START + SERVICE_STOP + DELETE, \
            SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath, \
            NULL, NULL, NULL, NULL, NULL

        .if eax != NULL
            mov g_hService, eax

            invoke StartService, g_hService, 0, NULL
            .if eax != 0

                invoke CreateFile, $CTA0("\\\\.\\SharingMemory"), GENERIC_READ, \
                                0, NULL, OPEN_EXISTING, 0, NULL

                .if eax != INVALID_HANDLE_VALUE
                    mov g_hDevice, eax

                    ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

                    invoke DeviceIoControl, g_hDevice, IOCTL_GIVE_ME_YOUR_MEMORY, NULL, 0, \
                                addr g_pSharedMemory, sizeof g_pSharedMemory, \
                                addr dwBytesReturned, NULL

                    .if ( eax != 0 ) && ( dwBytesReturned == sizeof g_pSharedMemory )

                        invoke GetModuleHandle, NULL
                        mov g_hInstance, eax
                        invoke DialogBoxParam, g_hInstance, IDD_MAIN, NULL, addr DlgProc, 0

                    .else
                        invoke MessageBox, NULL, $CTA0("Can't send control code to device."), \
                                                    NULL, MB_OK + MB_ICONSTOP
                    .endif

                    ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

                    invoke CloseHandle, g_hDevice
                .else
                    invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
                .endif
                invoke ControlService, g_hService, SERVICE_CONTROL_STOP, addr _ss
            .else
                invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
            .endif
            invoke DeleteService, g_hService
            invoke CloseServiceHandle, g_hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, g_hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, MB_ICONSTOP
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

:make

set exe=SharingMemory

if exist ..\%scp%.exe del ..\%scp%.exe

if exist rsrc.obj goto final
    \masm32\bin\rc /v rsrc.rc
    \masm32\bin\cvtres /machine:ix86 rsrc.res
    if errorlevel 0 goto final
        pause
        exit

:final
if exist rsrc.res del rsrc.res

\masm32\bin\ml /nologo /c /coff %exe%.bat
\masm32\bin\link /nologo /subsystem:windows %exe%.obj rsrc.obj

del %exe%.obj
move %exe%.exe ..
if exist %exe%.exe del %exe%.exe

echo.
pause

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

    invoke SetUnhandledExceptionFilter, MyUnhandledExceptionFilter

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

                    invoke DeviceIoControl, g_hDevice, IOCTL_GIVE_ME_YOUR_MEMORY, NULL, 0, \
                                addr g_pSharedMemory, sizeof g_pSharedMemory, \
                                addr dwBytesReturned, NULL

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

                    .if ( eax != 0 ) && ( dwBytesReturned == sizeof g_pSharedMemory )

                        invoke GetModuleHandle, NULL
                        mov g_hInstance, eax
                        invoke DialogBoxParam, g_hInstance, IDD_MAIN, NULL, addr DlgProc, 0

Если все прошло удачно, запускаем диалог. Дальше всё элементарно.

    .elseif eax == WM_INITDIALOG
        . . .
        invoke UpdateTime
        invoke SetTimer, hDlg, TIMER_ID, 1000, NULL

При обработке сообщения WM_INITDIALOG вызываем процедуру UpdateTime. Это нужно для того, чтобы сразу после появления диалога отобразить текущее время. Затем запускаем таймер, который будет срабатывать один раз в секунду.

    .if eax == WM_TIMER
        invoke UpdateTime

При обработке сообщения WM_TIMER также вызываем UpdateTime для обновления времени.

UpdateTime proc

local stime:SYSTEMTIME
local buffer[64]:CHAR

    .if g_pSharedMemory != NULL
        invoke FileTimeToSystemTime, g_pSharedMemory, addr stime
        movzx eax, stime.wHour
        movzx ecx, stime.wMinute
        movzx edx, stime.wSecond

        invoke wsprintf, addr buffer, $CTA0("%02d:%02d:%02d"), eax, ecx, edx

        invoke SetDlgItemText, g_hDlg, IDC_TIME, addr buffer
    .endif

    ret

UpdateTime endp

Задачей процедуры UpdateTime является форматирование и вывод текущего времени в общепринятом формате Часы:Минуты:Секунды.

Рис. 8-1. Результат работы программы SharingMemory.exe

Т.о. раз в секунду драйвер помещает на разделяемую страницу текущее время, обращаясь при этом по виртуальному адресу в системном адресном пространстве, а программа управления, также раз в секунду, забирает эту информацию, обращаясь при этом по виртуальному адресу в пользовательском адресном пространстве. Но физически разделяется одна страница памяти. Т.о. часы "тикают" каждую секунду. Кстати, функция KeQuerySystemTime получает текущее время также обращаясь к разделяемой между ядром и режимом пользователя странице, которая в режиме ядра спроецирована по адресу 0FFDF0000h, а в режиме пользователя по адресу 7FFE0000h (пользовательская функция GetSystemTime читает те же самые байты что и функция ядра KeQuerySystemTime) и описывается структурой KUSER_SHARED_DATA (см. ntddk.inc). Даже по названию этой структуры видно, что она разделяется ядром и режимом пользователя.

MyUnhandledExceptionFilter proc lpExceptionInfo:PTR EXCEPTION_POINTERS

local _ss:SERVICE_STATUS

    invoke KillTimer, g_hDlg, TIMER_ID
    invoke CloseHandle, g_hDevice
    invoke ControlService, g_hService, SERVICE_CONTROL_STOP, addr _ss
    invoke DeleteService, g_hService
    invoke CloseServiceHandle, g_hService
    invoke CloseServiceHandle, g_hSCManager

    mov eax, EXCEPTION_EXECUTE_HANDLER
    ret

MyUnhandledExceptionFilter endp

Если в любой точке программы управления возникнет исключение, система вызовет наш обработчик MyUnhandledExceptionFilter. Всё что мы можем сделать - это освободить все выделенные ресурсы. Самое главное закрыть описатель устройства. Тогда драйвер получит IRP_MJ_CLEANUP, а затем IRP_MJ_CLOSE и также проведет очистку, самой главной из которых является отключение (unmap) региона памяти от пользовательского адресного пространства. На самом деле, можно даже обойтись без обработчика исключений. Если программа управления и рухнет, система сама закроет все открытые описатели, в том числе и описатель устройства. Мы отключаем нашу разделяемую страницу при обработке IRP_MJ_CLEANUP просто из желания сделать очистку ресурсов как можно раньше. В данном случае можно сделать это и при обрабтке IRP_MJ_CLOSE. В любом случае MmUnmapLockedPages должна быть вызвана до того, как пользовательский процесс прекратит своё существование.

В отличие от предыдущего примера с разделяемым разделом, здесь у нас уже два потока обращающихся к разделяемому ресурсу памяти. Т.е. необходимо подумать о синхронизации. Читающий поток работает в режиме пользователя, а значит всегда выполняется при IRQL = PASSIVE_LEVEL. Пишущий поток принадлежит системному процессу и выполняет процедуру TimerRoutine, адрес которой мы определили в вызове IoInitializeTimer. Процедура TimerRoutine вызывается системой при IRQL = DISPATCH_LEVEL (об этом недвусмысленно написано в DDK) и выполняется потоком процесса простоя (idle process), во всяком случае, в моих экспериментах работал именно этот поток. Т.к. его приоритет ниже, чем приоритет пользовательского потока, то он не может прервать программу управления драйвером в момент считывания данных с разделяемой страницы. Т.к. при IRQL = DISPATCH_LEVEL планирования потоков не происходит, пользовательский поток не может прервать системный в момент записи текущего времени на разделяемую страницу. Т.о. на однопроцессорной машине никаких проблем с синхронизацией возникнуть не должно. На многопроцессорной машине возможна одновременная работа этих потоков. Поэтому в подобных ситуациях требуется подумать о синхронизации. В данном случае мы не предпринимаем никаких усилий в этом направлении, т.к. это тема для одной из следующих статей. При стечении самых неблагоприятных обстоятельств в одну из секунд в диалоге отобразится неверное значение времени. В данном случае, больше нам это ничем не грозит.


Реестр

10.1 Структура реестра
10.2 Доступ к реестру из драйвера
10.3 Исходный текст драйвера RegistryWorks
10.3.1 Создаём и открываем подраздел реестра
10.3.2 Создаём параметр реестра
10.3.3 Получаем значение параметра реестра
10.3.4 Удаляем подраздел реестра
10.3.5 Перебираем содержимое подраздела реестра
Исходный текст: KmdKit\examples\basic\RegistryWorks

10.1 Структура реестра

Реестр (Registry) - это централизованная база данных, играющая ключевую роль в конфигурировании и управлении системой. Структура реестра аналогична структуре логического диска, но содержимое реестра не является статической совокупностью хранящихся на жестком диске данных, а динамически изменяется в процессе работы системы. Реестр состоит из разделов (keys), напоминающих дисковые каталоги. Разделы самого верхнего уровня называются корневыми (root keys). Разделы представляют собой контейнер, содержащий другие разделы, называемые подразделами (subkeys), и/или параметры (values), которые можно сравнить с файлами на диске. Параметры хранят собственно данные. За реализацию и управление реестром отвечает Диспетчер конфигурации (Configuration Manager).

Существует шесть корневых разделов:

HKEY_USER | содержит информацию обо всех учётных записях;
HKEY_CURRENT_USER | хранит профиль текущего зарегистрированного пользователя;
HKEY_LOCAL_MACHINE | хранит информацию о конфигурации системы: описание аппаратного обеспечения, политики безопасности, пароли пользователей, настройки приложений, а также конфигурацию сервисов и драйверов;
HKEY_CURRENT_CONFIG | содержит текущий профиль оборудования;
HKEY_CLASSES_ROOT | хранит сопоставления расширений файлов и регистрационные данные COM-классов;
HKEY_PERFORMANCE_DATA | содержит значения счетчиков производительности.

HKEY_PERFORMANCE_DATA особый раздел, обращаться к которому напрямую вряд ли потребуется. В пользовательском режиме информация о счетчиках производительности доступна через библиотеку Performance Data Helper, реализованную в модуле pdh.dll. Стандартная оснастка "Производительность" (Performance Monitor) использует именно эту библиотеку. Помимо счетчиков производительности этот раздел содержит массу дополнительной информации. Например, функции Process Status (реализованы в psapi.dll), перечисляющие процессы, потоки, модули и т.п., черпают информацию именно из раздела HKEY_PERFORMANCE_DATA. Редакторы реестра Regedit и Regedt32 не показывают содержимое этого раздела, т.к. редактировать его нет смысла.

Корневые разделы HKEY_CURRENT_USER, HKEY_CURRENT_CONFIG и HKEY_CLASSES_ROOT не содержат никакой информации. На самом деле это ссылки на другие подразделы реестра.

HKEY_CURRENT_USER | ссылка на подраздел HKEY_USER\<SID_текущего_пользователя>, соответствующий текущему зарегистрированному в системе пользователю.
HKEY_CURRENT_CONFIG | ссылка на подраздел HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current.
HKEY_CLASSES_ROOT | ссылка на подразделы HKEY_LOCAL_MACHINE\SOFTWARE\Classes и HKEY_CURRENT_USER\SOFTWARE\Classes.

10.2 Доступ к реестру из драйвера

Как же добраться до реестра из режима ядра? Доступ к реестру осуществляется точно также, как и к любому другому именованному объекту, т.е. через пространство имен диспетчера объектов (подробнее см. 3 часть). Для интеграции пространства имен реестра и диспетчера объектов Диспетчер конфигурации (Configuration Manager) создает объект типа "раздел реестра" (key object) под именем "Registry" и помещает его в корневой каталог пространства имен диспетчера объектов. Для компонентов режима ядра это и есть точка входа в реестр.

Рис. 10-1. Объект "раздел реестра" Registry в пространстве имен диспетчера объектов

Все функции ядра, осуществляющие доступ к именованным объектам получают их имена в составе структуры OBJECT_ATRIBUTES - это нам уже давно известно. В случае объекта типа "раздел реестра" это имя должно начинаться с "\Registry". Остаётся добавить имена корневых разделов. Сразу скажу, что я не знаю, какое имя используется для открытия коневого раздела HKEY_PERFORMANCE_DATA и вообще, используется ли для этого какое-то имя. Возможно, здесь применяется другой механизм. Я предпринял кое-какие усилия в этом направлении, но они оказались напрасны. Если вам что-то известно по этому вопросу - просьба поделиться со мной этими знаниями. Для оставшихся двух корневых разделов всё просто.

Раздел | Имя
HKEY_USER | "\Registry\User"
HKEY_LOCAL_MACHINE | "\Registry\Machine"

Для обращения к трём разделам-ссылкам потребуются дополнительные манипуляции. Например, чтобы обратиться к разделу HKEY_CURRENT_CONFIG, надо знать, что он является ссылкой на подраздел HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current и подставить имя этого корневого раздела. Т.о. получится такое имя \Registry\Machine\SYSTEM\CurrentControlSet\Hardware Profiles\Current. К сожалению, определить такую длинную unicode-строку, используя макросы CTW0, $CTW0 и т.п. не удастся, т.к. эта строка превышает лимит в 47 символов. Можете воспользоваться любым доступным вам способом.

10.3 Исходный текст драйвера RegistryWorks

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

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  RegistryWorks - Пример работы с реестром
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.const

CCOUNTED_UNICODE_STRING "\\Registry\\Machine\\Software\\CoolApp", g_usMachineKeyName, 4
CCOUNTED_UNICODE_STRING "SomeData", g_usValueName, 4

CTW0 "It's just a string", g_wszStringData, 4

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         CreateKey
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

CreateKey proc

local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local dwDisposition:DWORD

    invoke DbgPrint, $CTA0("\nRegistryWorks: *** Creating registry key\n")

    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usMachineKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL
    invoke ZwCreateKey, addr hKey, KEY_WRITE, addr oa, 0, NULL, \
                                      REG_OPTION_VOLATILE, addr dwDisposition
    .if eax == STATUS_SUCCESS

        .if dwDisposition == REG_CREATED_NEW_KEY
            invoke DbgPrint, \
                $CTA0("RegistryWorks: Registry key \\Registry\\Machine\\Software\\CoolApp created\n")
        .elseif dwDisposition == REG_OPENED_EXISTING_KEY
            invoke DbgPrint, \
                $CTA0("RegistryWorks: Registry key \\Registry\\Machine\\Software\\CoolApp opened\n")
        .endif

        invoke ZwClose, hKey
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key handle closed\n")
    .else
        invoke DbgPrint, $CTA0("RegistryWorks: Can't create registry key. Status: %08X\n"), eax
    .endif

    ret

CreateKey endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       SetValueKey
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

SetValueKey proc

local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE

    invoke DbgPrint, $CTA0("\nRegistryWorks: *** Opening registry key to set new value\n")
    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usMachineKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL
    invoke ZwOpenKey, addr hKey, KEY_SET_VALUE, ecx

    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key openeded\n")

        invoke ZwSetValueKey, hKey, addr g_usValueName, 0, REG_SZ, \
                                addr g_wszStringData, sizeof g_wszStringData
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("RegistryWorks: Registry key value added\n")
        .else
            invoke DbgPrint, \
                    $CTA0("RegistryWorks: Can't set registry key value. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hKey
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key handle closed\n")
    .else
        invoke DbgPrint, $CTA0("RegistryWorks: Can't open registry key. Status: %08X\n"), eax
    .endif

    ret

SetValueKey endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      QueryValueKey
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

QueryValueKey proc

local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local cb:DWORD
local ppi:PKEY_VALUE_PARTIAL_INFORMATION
local as:ANSI_STRING
local us:UNICODE_STRING

    invoke DbgPrint, $CTA0("\nRegistryWorks: *** Opening registry key to read value\n")
    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usMachineKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL
    invoke ZwOpenKey, addr hKey, KEY_QUERY_VALUE, ecx

    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key openeded\n")

        invoke ZwQueryValueKey, hKey, addr g_usValueName, \
                                KeyValuePartialInformation, NULL, 0, addr cb
        .if cb != 0
            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL
                mov ppi, eax

                invoke ZwQueryValueKey, hKey, addr g_usValueName, \
                                    KeyValuePartialInformation, ppi, cb, addr cb
                .if ( eax == STATUS_SUCCESS ) && ( cb != 0 )

                    mov eax, ppi
                    .if [KEY_VALUE_PARTIAL_INFORMATION PTR [eax]]._Type == REG_SZ
                        lea eax, (KEY_VALUE_PARTIAL_INFORMATION PTR [eax]).Data
                        invoke RtlInitUnicodeString, addr us, eax
                        invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE
                        .if eax == STATUS_SUCCESS
                            invoke DbgPrint, \
                                $CTA0("RegistryWorks: Registry key value is: \=%s\=\n"), as.Buffer
                            invoke RtlFreeAnsiString, addr as
                        .endif
                    .endif
                .else
                    invoke DbgPrint, \
                            $CTA0("RegistryWorks: Can't query registry key value. Status: %08X\n"), eax
                .endif
                invoke ExFreePool, ppi
            .else
                invoke DbgPrint, $CTA0("RegistryWorks: Can't allocate memory. Status: %08X\n"), eax
            .endif
        .else
            invoke DbgPrint, \
            $CTA0("RegistryWorks: Can't get bytes count needed for key partial information. Status: %08X\n"), eax
        .endif
        invoke ZwClose, hKey
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key handle closed\n")
    .else
        invoke DbgPrint, $CTA0("RegistryWorks: Can't open registry key. Status: %08X\n"), eax
    .endif

    ret

QueryValueKey endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        DeleteKey
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DeleteKey proc

local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE

    invoke DbgPrint, $CTA0("\nRegistryWorks: *** Deleting registry key\n")

    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usMachineKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL
    invoke ZwOpenKey, addr hKey, KEY_ALL_ACCESS, ecx

    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key opened\n")
        invoke ZwDeleteKey, hKey
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("RegistryWorks: Registry key deleted\n")
        .else
            invoke DbgPrint, $CTA0("RegistryWorks: Can't delete registry key. Status: %08X\n"), eax
        .endif
        invoke ZwClose, hKey
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key handle closed\n")
    .else
        invoke DbgPrint, $CTA0("RegistryWorks: Can't open registry key. Status: %08X\n"), eax
    .endif

    ret

DeleteKey endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      EnumerateKey
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

EnumerateKey proc

local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local cb:DWORD
local pbi:PKEY_BASIC_INFORMATION
local pfi:PKEY_FULL_INFORMATION
local as:ANSI_STRING
local us:UNICODE_STRING
local dwSubKeys:DWORD
local pwszKeyName:PWCHAR

    invoke DbgPrint, $CTA0("\nRegistryWorks: *** Opening \\Registry\\User key to enumerate\n")

    CCOUNTED_UNICODE_STRING "\\Registry\\User", g_usUserKeyName, 4

    lea ecx, oa
    InitializeObjectAttributes ecx, offset g_usUserKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL
    invoke ZwOpenKey, addr hKey, KEY_ENUMERATE_SUB_KEYS, ecx

    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key openeded\n")

        invoke ZwQueryKey, hKey, KeyFullInformation, NULL, 0, addr cb
        .if cb != 0

            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL
                mov pfi, eax

                invoke ZwQueryKey, hKey, KeyFullInformation, pfi, cb, addr cb
                .if ( eax == STATUS_SUCCESS ) && ( cb != 0 )

                    mov eax, pfi
                    push (KEY_FULL_INFORMATION PTR [eax]).SubKeys
                    pop dwSubKeys

                    invoke DbgPrint, \
                        $CTA0("RegistryWorks: ---------- Starting enumerate subkeys ----------\n")

                    push ebx
                    xor ebx, ebx
                    .while ebx < dwSubKeys
                        invoke ZwEnumerateKey, hKey, ebx, KeyBasicInformation, NULL, 0, addr cb
                        .if cb != 0
                            invoke ExAllocatePool, PagedPool, cb
                            .if eax != NULL
                                mov pbi, eax

                                invoke ZwEnumerateKey, hKey, ebx, KeyBasicInformation, pbi, cb, addr cb
                                .if ( eax == STATUS_SUCCESS ) && ( cb != 0 )

                                    mov eax, pbi
                                    mov eax, (KEY_BASIC_INFORMATION PTR [eax]).NameLength
                                    add eax, sizeof WCHAR
                                    mov cb, eax
                                    invoke ExAllocatePool, PagedPool, cb
                                    .if eax != NULL
                                        mov pwszKeyName, eax

                                        invoke memset, pwszKeyName, 0, cb

                                        mov ecx, pbi
                                        mov eax, (KEY_BASIC_INFORMATION PTR [ecx]).NameLength
                                        shr eax, 1
                                        lea ecx, (KEY_BASIC_INFORMATION PTR [ecx])._Name
                                        invoke wcsncpy, pwszKeyName, ecx, eax

                                        invoke RtlInitUnicodeString, addr us, pwszKeyName
                                        invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE
                                        .if eax == STATUS_SUCCESS
                                            invoke DbgPrint, $CTA0("RegistryWorks: \=%s\=\n"), as.Buffer
                                            invoke RtlFreeAnsiString, addr as
                                        .endif

                                        invoke ExFreePool, pwszKeyName
                                    .endif
                                .else
                                    invoke DbgPrint, \
                                        $CTA0("RegistryWorks: Can't enumerate registry keys. Status: %08X\n"), eax
                                .endif
                                invoke ExFreePool, pbi
                            .endif
                        .endif
                        inc ebx
                    .endw
                    pop ebx

                    invoke DbgPrint, \
                        $CTA0("RegistryWorks: ------------------------------------------------\n")

                .else
                    invoke DbgPrint, \
                        $CTA0("RegistryWorks: Can't query registry key information. Status: %08X\n"), eax
                .endif
                invoke ExFreePool, pfi
            .else
                invoke DbgPrint, $CTA0("RegistryWorks: Can't allocate memory. Status: %08X\n"), eax
            .endif
        .endif

        invoke ZwClose, hKey
        invoke DbgPrint, $CTA0("RegistryWorks: Registry key handle closed\n")

    .else
        invoke DbgPrint, $CTA0("RegistryWorks: Can't open registry key. Status: %08X\n"), eax
    .endif

    ret

EnumerateKey endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke DbgPrint, $CTA0("\nRegistryWorks: Entering DriverEntry\n")

    ;:::::::::::::::::::::::::::::::::::::::
    ; Создаём новый подраздел              ;
    ;:::::::::::::::::::::::::::::::::::::::

    invoke CreateKey

    ;:::::::::::::::::::::::::::::::::::::::
    ; Создаем в этом подразделе параметр   ;
    ;:::::::::::::::::::::::::::::::::::::::

    invoke SetValueKey

    ;:::::::::::::::::::::::::::::::::::::::
    ; Получаем значение параметра          ;
    ;:::::::::::::::::::::::::::::::::::::::

    invoke QueryValueKey

    ;:::::::::::::::::::::::::::::::::::::::
    ; Удаляем подраздел                    ;
    ;:::::::::::::::::::::::::::::::::::::::

    invoke DeleteKey

    ;:::::::::::::::::::::::::::::::::::::::
    ; Перечисляем содержимое раздела       ;
    ;:::::::::::::::::::::::::::::::::::::::

    invoke EnumerateKey

    invoke DbgPrint, $CTA0("\nRegistryWorks: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=RegistryWorks

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

Код драйвера состоит из нескольких автономных процедур: CreateKey, SetValueKey, QueryValueKey, DeleteKey и EnumerateKey, каждая из которых работает с реестром «с нуля» - для учебного примера так нагляднее.

10.3.1 Создаём и открываем подраздел реестра

В процедуре CreateKey, вызовом функции ZwCreateKey, создаем новый подраздел Registry\Machine\Software\CoolApp.

    invoke ZwCreateKey, addr hKey, KEY_WRITE, addr oa, 0, NULL, REG_OPTION_VOLATILE, addr dwDisposition

Флаг REG_OPTION_VOLATILE запрещает запись созданного подраздела в куст реестра (hiev) - один из файлов реестра на диске. Т.е. этот подраздел будет существовать только до следующей перезагрузки системы. В данном случае, использовать этот флаг не обязательно, т.к. мы всё равно удалим весь подраздел. Если вы захотите прописать подраздел в реестр на постоянное место жительства, то использовать этот флаг не следует.

    .if eax == STATUS_SUCCESS
        .if dwDisposition == REG_CREATED_NEW_KEY
        .elseif dwDisposition == REG_OPENED_EXISTING_KEY
        .endif

После успешного вызова ZwCreateKey по значению переменной dwDisposition можно определить, был ли создан новый подраздел (REG_CREATED_NEW_KEY) или такой подраздел уже существовал в реестре (REG_OPENED_EXISTING_KEY) и поэтому был открыт.

    invoke ZwOpenKey, addr hKey, KEY_SET_VALUE, ecx
    invoke ZwOpenKey, addr hKey, KEY_QUERY_VALUE, ecx
    invoke ZwOpenKey, addr hKey, KEY_ALL_ACCESS, ecx
    invoke ZwOpenKey, addr hKey, KEY_ENUMERATE_SUB_KEYS, ecx

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

10.3.2 Создаём параметр реестра

Теперь, создадим в нашем подразделе строковый параметр с именем "SomeData".

. . .
CCOUNTED_UNICODE_STRING "SomeData", g_usValueName, 4
CTW0 "It's just a string", g_wszStringData, 4
. . .
        invoke ZwSetValueKey, hKey, addr g_usValueName, 0, REG_SZ, \
                                addr g_wszStringData, sizeof g_wszStringData

Константа REG_SZ определяет тип создаваемого параметра: unicode-строка завершающаяся нулём. Существует масса других типов - все они описаны в DDK.

10.3.3 Получаем значение параметра реестра

Получим значение нашего параметра SomeData.

        invoke ZwQueryValueKey, hKey, addr g_usValueName, \
                                KeyValuePartialInformation, NULL, 0, addr cb

Третий параметр функции ZwQueryValueKey определяет тип запрашиваемой информации. В DDK определено три значения: KeyValueBasicInformation, KeyValueFullInformation и KeyValuePartialInformation, каждому из которых соответствует своя структура: KEY_VALUE_BASIC_INFORMATION, KEY_VALUE_FULL_INFORMATION и KEY_VALUE_PARTIAL_INFORMATION. В данном случае, мы хотим получить содержимое параметра. Для этого вполне годится KeyValuePartialInformation.

Т.к. объём информации нам заранее не известен, вызываем ZwQueryValueKey, передавая NULL в четвертом параметре (указатель на буфер) и 0 - в пятом (размер буфера). Функция ZwQueryValueKey подсчитает необходимый ей размер буфера и вернет это значение в переменной cb (многие, но не все, Zw* функции работают подобным образом).

        .if cb != 0
            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL
                mov ppi, eax

                invoke ZwQueryValueKey, hKey, addr g_usValueName, \
                                    KeyValuePartialInformation, ppi, cb, addr cb

Выделив необходимое пространство памяти, вызываем ZwQueryValueKey ещё раз - теперь уже с указателем на буфер.

                .if ( eax == STATUS_SUCCESS ) && ( cb != 0 )
                    mov eax, ppi
                    .if [KEY_VALUE_PARTIAL_INFORMATION PTR [eax]]._Type == REG_SZ

На всякий случай, проверяем тип параметра.

                        lea eax, (KEY_VALUE_PARTIAL_INFORMATION PTR [eax]).Data

Если тип параметра REG_SZ, то поле Data структуры KEY_VALUE_PARTIAL_INFORMATION содержит завершенную нулем unicode-строку. Нам надо вывести эту строку в отладочном сообщении, поэтому необходимо преобразовать её в ansi-строку.

                        invoke RtlInitUnicodeString, addr us, eax

Функция RtlInitUnicodeString измеряет unicode-строку, адрес которой передан во втором параметре и заполняет структуру UNICODE_STRING, адрес которой передан в первом параметре.

                        invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE

Функция RtlUnicodeStringToAnsiString конвертирует unicode-строку в ansi. Если последний параметр установлен в TRUE, функция сама выделит буфер, запишет туда преобразованную строку и заполнит структуру ANSI_STRING (переменную as, в нашем случае). Поле Buffer структуры ANSI_STRING будет указывать на выделенный буфер с преобразованной ansi-строкой. Если последний параметр установить в FALSE, то буфер для ansi-строки надо выделить заранее и поместить указатель на него в поле Buffer структуры ANSI_STRING. В данном случае, мы просим функцию RtlUnicodeStringToAnsiString выделить буфер за нас.

                        .if eax == STATUS_SUCCESS
                            invoke DbgPrint, \
                                $CTA0("RegistryWorks: Registry key value is: \=%s\=\n"), as.Buffer
                            invoke RtlFreeAnsiString, addr as
                        .endif

В DDK (по крайней мере, в 2000 DDK) описание функции RtlUnicodeStringToAnsiString, весьма туманно, я бы даже сказал двусмысленно. IFS DDK описывает работу этой функции значительно лучше. Чтобы расставить «точки над i» рассмотрим простой пример.

wsz db 'a', 0, 'b', 0, 'c', 0, 0, 0
us  UNICODE_STRING <>
as  ANSI_STRING    <>

Изначально переменные us и as неопределены. wsz - unicode-строка, подлежащая переводу в формат ANSI.

us._Length        = ?
us.MaximumLength  = ?
us.Buffer         = ?

as._Length        = ?
as.MaximumLength  = ?
as.Buffer         = ?

RtlInitUnicodeString заполняет переменную us на основании размера строки wsz.

invoke RtlInitUnicodeString, addr us, addr wsz

us._Length        = 6
us.MaximumLength  = 8
us.Buffer         = offset wsz

as._Length        = ?
as.MaximumLength  = ?
as.Buffer         = ?

По последнему аргументу RtlUnicodeStringToAnsiString видит, что она должна выделить буфер размера us.MaximumLength / sizeof WCHAR. Выделив буфер и поместив указатель на него в поле as.Buffer, функция RtlUnicodeStringToAnsiString приступает к собственно конвертации строки. Если эта операция проходит успешно, то переменная as будет содержать полное описание конвертированной строки.

invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE

us._Length        = 6
us.MaximumLength  = 8
us.Buffer         = offset wsz

as._Length        = 3
as.MaximumLength  = 4
as.Buffer         = -> 'a', 'b', 'c', 0  ; указатель на выделенный функцией RtlUnicodeStringToAnsiString,,,TRUE
                                         ; буфер, который содержит преобразованную строку wsz в формате ANSI.

После использования, буфер, выделенные функцией RtlUnicodeStringToAnsiString, надо освободить, вызвав RtlFreeAnsiString, причем в качестве аргумента ей передается не указатель на сам буфер, а указатель на структуру ANSI_STRING. RtlFreeAnsiString освобождает буфер as.Buffer и обнуляет всю переменную as.

invoke RtlFreeAnsiString, addr as

us._Length        = 6
us.MaximumLength  = 8
us.Buffer         = offset wsz

as._Length        = 0
as.MaximumLength  = 0
as.Buffer         = NULL

Надеюсь, теперь всё понятно.

10.3.4 Удаляем подраздел реестра

Думаю, тут вы разберетесь и без меня.

10.3.5 Перебираем содержимое подраздела реестра

Теперь посмотрим, что лежит в разделе \Registry\User.

        invoke ZwQueryKey, hKey, KeyFullInformation, NULL, 0, addr cb
        .if cb != 0

            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL
                mov pfi, eax

                invoke ZwQueryKey, hKey, KeyFullInformation, pfi, cb, addr cb
                .if ( eax == STATUS_SUCCESS ) && ( cb != 0 )

                    mov eax, pfi
                    push (KEY_FULL_INFORMATION PTR [eax]).SubKeys
                    pop dwSubKeys

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

                    push ebx
                    xor ebx, ebx
                    .while ebx < dwSubKeys
                        invoke ZwEnumerateKey, hKey, ebx, KeyBasicInformation, NULL, 0, addr cb
                        .if cb != 0

                            invoke ExAllocatePool, PagedPool, cb
                            .if eax != NULL
                                mov pbi, eax

                                invoke ZwEnumerateKey, hKey, ebx, KeyBasicInformation, pbi, cb, addr cb
                                .if ( eax == STATUS_SUCCESS ) && ( cb != 0 )

Ползуясь функцией ZwEnumerateKey с информацмонным классом KeyBasicInformation, организуем цикл перебора. Как и в предыдущих случаях, вызываем её два раза: первый раз для того, чтобы узнать размер информации, второй - для того, чтобы эту информацию получить.

                                    mov eax, pbi
                                    mov eax, (KEY_BASIC_INFORMATION PTR [eax]).NameLength
                                    add eax, sizeof WCHAR
                                    mov cb, eax
                                    invoke ExAllocatePool, PagedPool, cb

В поле _Name структуры KEY_BASIC_INFORMATION вернётся имя подраздела/параметра в виде unicode-строки, но строка эта не завершена нулём. Для последующего преобразования её в ansi-строку (для вывода в отладочном сообщении) нам нужна завершенная нулем строка - выделим для неё временный буфер.

                                    .if eax != NULL
                                        mov pwszKeyName, eax

                                        invoke memset, pwszKeyName, 0, cb

                                        mov ecx, pbi
                                        mov eax, (KEY_BASIC_INFORMATION PTR [ecx]).NameLength
                                        shr eax, 1
                                        lea ecx, (KEY_BASIC_INFORMATION PTR [ecx])._Name
                                        invoke wcsncpy, pwszKeyName, ecx, eax

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

                                        invoke RtlInitUnicodeString, addr us, pwszKeyName
                                        invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE
                                        .if eax == STATUS_SUCCESS
                                            invoke DbgPrint, $CTA0("RegistryWorks: \=%s\=\n"), as.Buffer
                                            invoke RtlFreeAnsiString, addr as
                                        .endif

Делаем необходимые преобразования, подробно рассмотренные выше и выводим имя подраздела/параметра в отладочном сообщении.

                                        invoke ExFreePool, pwszKeyName
                                    .endif
                                .endif
                                invoke ExFreePool, pbi
                            .endif
                        .endif
                        inc ebx
                    .endw
                    pop ebx
                .endif
                invoke ExFreePool, pfi
            .endif
        .endif

Проводим необходимую очистку ресурсов.


Каталоги и файлы

11.1 Таблица описателей ядра
11.2 Исходный текст драйвера FileWorks
11.3 Создание каталога и файла
11.4 Объект "файл"
11.5 Запись в файл
11.6 Изменение свойств файла
11.7 Чтение из файла
11.8 Добавление данных в файл
11.9 Усечение файла
11.10 Удаление файла и каталога
11.11 Перечисление содержимого каталога

Исходный текст: KmdKit\examples\basic\FileWorks

Обеспечение возможности работы с файлами является важнейшей задачей любой операционной системы. Посмотрим, какие возможности предоставляют нам операционные системы семейства NT.

11.1 Таблица описателей ядра

Прежде чем мы перейдем непосредственно к теме статьи, обсудим один важный момент, которому прежде не было уделено должного внимания. Для получения описателя объекта требуется заполнить структуру OBJECT_ATTRIBUTES - это мы делали уже много раз примерно таким образом:

InitializeObjectAttributes addr oa, addr g_usName, OBJ_CASE_INSENSITIVE, NULL, NULL

Инициализировав структуру OBJECT_ATTRIBUTES, мы вызывали функцию создания/открытия объекта и получали его описатель (hande). Но описатель этот попадал в таблицу описателей того процесса, в контексте которого был получен. Т.к. таблица описателей специфична для процесса, то использовать такой описатель можно только в контексте того же самого процесса. Например, попытка закрыть этот описатель в контексте другого процесса, в лучшем случае, завершится неудачно. А в худшем, если в таблице описателей этого процесса окажется описатель с таким же значением, ведь описатель это просто 32-битное число (точнее битовая структура), возможно, будет закрыт описатель совершенно другого объекта. Даже если описатель получен драйвером, но в контексте пользовательского процесса, он попадет в таблицу описателей этого процесса и по нему можно будет обратиться к объекту из режима пользователя, преднамеренно или случайно. Бывают ситуации, когда именно это и требуется, но в общем случае это не то, что вы хотите. Именно поэтому компоненты ядра, и драйверы в частности, не "любят" пользоваться описателями, а предпочитают использовать ссылки на объекты (reference to object), которые являются простыми указателями на структуру объекта в памяти ядра. Для учета предоставленных ссылок на объект в заголовке каждого объекта хранится счетчик ссылок (reference count). Если же для доступа к объекту нужен именно его описатель, как в этом и предыдущем примере, и вы планируете обращаться к нему из разных контекстов, нужно заставить систему поместить описатель в так называемую таблицу описателей ядра (kernel handle table).

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

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

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

InitializeObjectAttributes addr oa, addr g_usName, OBJ_KERNEL_HANDLE, NULL, NULL

11.2 Исходный текст драйвера FileWorks

Также как и в предыдущем примере, код драйвера состоит из нескольких автономных процедур: CreateDirectory, CreateFile, WriteFile, MarkAsReadOnly, ReadFile, UnmarkAsReadOnly, AppendFile, TruncateFile, DeleteFile, DeleteDirectory и EnumerateFiles. Все они роботают почти независимо друг от друга.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  FileWorks - Пример различных операций с файлами.
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                               В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntifs.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.const

CCOUNTED_UNICODE_STRING "\\??\\c:\\FileWorks\\test.txt", g_usFileName, 4
CCOUNTED_UNICODE_STRING "\\??\\c:\\FileWorks", g_usDirName, 4

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           К О Д
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      CreateDirectory
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

CreateDirectory proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hDirectory:HANDLE

    ; Помни, что коды форматирования Unicode (%C, %S, %lc, %ls, %wc, %ws, %wZ), передаваемые в
    ; функцию DbgPrint могут быть использованы только на IRQL = PASSIVE_LEVEL!

    invoke DbgPrint, $CTA0("\nFileWorks: Creating %ws directory\n"), g_usDirName.Buffer

    InitializeObjectAttributes addr oa, addr g_usDirName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwCreateFile, addr hDirectory, SYNCHRONIZE, addr oa, addr iosb, 0, FILE_ATTRIBUTE_NORMAL, \
                        0, FILE_OPEN_IF, FILE_DIRECTORY_FILE + FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0
    .if eax == STATUS_SUCCESS
        .if iosb.Information == FILE_CREATED
            invoke DbgPrint, $CTA0("FileWorks: Directory created\n")
        .elseif iosb.Information == FILE_OPENED
            invoke DbgPrint, $CTA0("FileWorks: Directory exists and was opened\n")
        .endif
        invoke ZwClose, hDirectory
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't create directory. Status: %08X\n"), eax
    .endif

    ret

CreateDirectory endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        CreateFile
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

CreateFile proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE

    invoke DbgPrint, $CTA0("\nFileWorks: Creating %ws file\n"), g_usFileName.Buffer

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwCreateFile, addr hFile, SYNCHRONIZE, addr oa, addr iosb, 0, FILE_ATTRIBUTE_NORMAL, \
                        0, FILE_CREATE, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File created\n")
        invoke ZwClose, hFile
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't create file. Status: %08X\n"), eax
    .endif

    ret

CreateFile endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            WriteFile
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

WriteFile proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file for writing\n")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwCreateFile, addr hFile, FILE_WRITE_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                        0, 0, FILE_SHARE_READ, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        CTA0 "Data can be written to an open file", g_szData, 4

        invoke ZwWriteFile, hFile, 0, NULL, NULL, addr iosb, \
                        addr g_szData, sizeof g_szData - 1, NULL, NULL
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("FileWorks: File was written\n")
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't write to the file. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

WriteFile endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        MarkAsReadOnly
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

MarkAsReadOnly proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE
local fbi:FILE_BASIC_INFORMATION

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file for changing attributes\n")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwCreateFile, addr hFile, FILE_READ_ATTRIBUTES + FILE_WRITE_ATTRIBUTES + SYNCHRONIZE, \
                        addr oa, addr iosb, 0, 0, FILE_SHARE_READ, \
                        FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        invoke ZwQueryInformationFile, hFile, addr iosb, addr fbi, sizeof fbi, FileBasicInformation
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("FileWorks: File attributes were: %08X\n"), fbi.FileAttributes
            or fbi.FileAttributes, FILE_ATTRIBUTE_READONLY
            invoke ZwSetInformationFile, hFile, addr iosb, addr fbi, sizeof fbi, FileBasicInformation
            .if eax == STATUS_SUCCESS
                invoke DbgPrint, $CTA0("FileWorks: Now file marked as read-only\n")
            .else
                invoke DbgPrint, $CTA0("FileWorks: Can't change file attributes. Status: %08X\n"), eax
            .endif
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't query file attributes. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

MarkAsReadOnly endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          ReadFile
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

ReadFile proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE
local p:PVOID
local cb:DWORD
local fsi:FILE_STANDARD_INFORMATION

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file for reading\n")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwOpenFile, addr hFile, FILE_READ_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE, FILE_SYNCHRONOUS_IO_NONALERT
    .if eax == STATUS_SUCCESS

        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        invoke ZwQueryInformationFile, hFile, addr iosb, addr fsi, sizeof fsi, FileStandardInformation
        .if eax == STATUS_SUCCESS

            mov eax, fsi.EndOfFile.LowPart
            inc eax
            mov cb, eax

            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL
                mov p, eax

                invoke RtlZeroMemory, p, cb

                invoke ZwReadFile, hFile, 0, NULL, NULL, addr iosb, p, cb, 0, NULL
                .if eax == STATUS_SUCCESS
                    invoke DbgPrint, $CTA0("FileWorks: File content: \=%s\=\n"), p
                .else
                    invoke DbgPrint, $CTA0("FileWorks: Can't read from the file. Status: %08X\n"), eax
                .endif

                invoke ExFreePool, p

            .else
                invoke DbgPrint, $CTA0("FileWorks: Can't allocate memory. Status: %08X\n"), eax
            .endif
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't query file size. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile

    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

ReadFile endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        UnmarkAsReadOnly
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

UnmarkAsReadOnly proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE
local fbi:FILE_BASIC_INFORMATION

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file for changing attributes\n")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwCreateFile, addr hFile, FILE_READ_ATTRIBUTES + FILE_WRITE_ATTRIBUTES + SYNCHRONIZE, \
                        addr oa, addr iosb, 0, 0, FILE_SHARE_READ, FILE_OPEN, \
                        FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        invoke ZwQueryInformationFile, hFile, addr iosb, addr fbi, sizeof fbi, FileBasicInformation
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("FileWorks: File attributes were: %08X\n"), fbi.FileAttributes
            and fbi.FileAttributes, not FILE_ATTRIBUTE_READONLY
            invoke ZwSetInformationFile, hFile, addr iosb, addr fbi, sizeof fbi, FileBasicInformation
            .if eax == STATUS_SUCCESS
                invoke DbgPrint, $CTA0("FileWorks: Now file can be written or deleted\n")
            .else
                invoke DbgPrint, $CTA0("FileWorks: Can't change file attributes. Status: %08X\n"), eax
            .endif
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't query file attributes. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

UnmarkAsReadOnly endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         AppendFile
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

AppendFile proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file to append data\n")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwOpenFile, addr hFile, FILE_APPEND_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                                    FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        CTA0 " using ZwWriteFile", g_szDataToAppend, 4

        invoke ZwWriteFile, hFile, 0, NULL, NULL, addr iosb, \
                        addr g_szDataToAppend, sizeof g_szDataToAppend - 1, NULL, NULL
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("FileWorks: Data appended to the file\n")
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't append data to file. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile
   .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

AppendFile endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        TruncateFile
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

TruncateFile proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE
local fsi:FILE_STANDARD_INFORMATION
local feofi:FILE_END_OF_FILE_INFORMATION

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file to truncate\n")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwOpenFile, addr hFile, FILE_WRITE_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                        FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        invoke ZwQueryInformationFile, hFile, addr iosb, \
                        addr fsi, sizeof fsi, FileStandardInformation
        .if eax == STATUS_SUCCESS

            invoke DbgPrint, $CTA0("FileWorks: EOF was: %08X\n"), fsi.EndOfFile.LowPart

            and feofi.EndOfFile.HighPart, 0
            mov eax, fsi.EndOfFile.LowPart
            shr eax, 1
            mov feofi.EndOfFile.LowPart, eax
            invoke ZwSetInformationFile, hFile, addr iosb, \
                        addr feofi, sizeof feofi, FileEndOfFileInformation
            .if eax == STATUS_SUCCESS
                invoke DbgPrint, $CTA0("FileWorks: File truncated to its half size\n")
            .else
                invoke DbgPrint, $CTA0("FileWorks: Can't truncate file. Status: %08X\n"), eax
            .endif

        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't query file info. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

TruncateFile endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         DeleteFile
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DeleteFile proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hFile:HANDLE
local fdi:FILE_DISPOSITION_INFORMATION

    invoke DbgPrint, $CTA0("\nFileWorks: Opening file for deletion")

    InitializeObjectAttributes addr oa, addr g_usFileName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwCreateFile, addr hFile, DELETE + SYNCHRONIZE, addr oa, addr iosb, \
                        0, 0, FILE_SHARE_DELETE, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("FileWorks: File openeded\n")

        mov fdi.DeleteFile, TRUE
        invoke ZwSetInformationFile, hFile, addr iosb, addr fdi, sizeof fdi, FileDispositionInformation
        .if eax == STATUS_SUCCESS
            invoke DbgPrint, $CTA0("FileWorks: File has been marked for deletion\n")
            invoke DbgPrint, $CTA0("FileWorks: It should be deleted when the last open handle is closed\n")
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't mark file for deletion. Status: %08X\n"), eax
        .endif

        invoke ZwClose, hFile
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open file. Status: %08X\n"), eax
    .endif

    ret

DeleteFile endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DeleteDirectory
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DeleteDirectory proc

local oa:OBJECT_ATTRIBUTES
local iosb:IO_STATUS_BLOCK
local hDirectory:HANDLE

    InitializeObjectAttributes addr oa, addr g_usDirName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwDeleteFile, addr oa
    .if eax == STATUS_SUCCESS
        invoke DbgPrint, $CTA0("\nFileWorks: Directory should be deleted\n")
    .else
        invoke DbgPrint, $CTA0("\nFileWorks: Can't delete directory. Status: %08X\n"), eax
    .endif

    ret

DeleteDirectory endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      EnumerateFiles
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

EnumerateFiles proc uses esi

local status:NTSTATUS
local oa:OBJECT_ATTRIBUTES
local hSystemRootDirectory:HANDLE
local hDriversDirectory:HANDLE
local as:ANSI_STRING
local us:UNICODE_STRING
local iosb:IO_STATUS_BLOCK
local tf:TIME_FIELDS
local cb:DWORD
local pfdi:PFILE_DIRECTORY_INFORMATION

    invoke DbgPrint, $CTA0("\nFileWorks: Opening directory to enumerate files\n")

    InitializeObjectAttributes addr oa, $CCOUNTED_UNICODE_STRING("\\SystemRoot"), \
                                OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

    invoke ZwOpenFile, addr hSystemRootDirectory, FILE_LIST_DIRECTORY + SYNCHRONIZE, addr oa, \
                        addr iosb, FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE, \
                        FILE_DIRECTORY_FILE + FILE_SYNCHRONOUS_IO_NONALERT
    .if eax == STATUS_SUCCESS

        InitializeObjectAttributes addr oa, $CCOUNTED_UNICODE_STRING("system32\\drivers"), \
                            OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, hSystemRootDirectory, NULL

        invoke ZwOpenFile, addr hDriversDirectory, FILE_LIST_DIRECTORY + SYNCHRONIZE, addr oa, \
                            addr iosb, FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE, \
                            FILE_DIRECTORY_FILE + FILE_SYNCHRONOUS_IO_NONALERT
        .if eax == STATUS_SUCCESS

            mov cb, sizeof FILE_DIRECTORY_INFORMATION + 256

            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL

                mov pfdi, eax
                mov esi, eax
                assume esi:ptr FILE_DIRECTORY_INFORMATION

                invoke DbgPrint, \
                        $CTA0("\nFileWorks: ---------- Starting enumerate files ----------\n")

                invoke ZwQueryDirectoryFile, hDriversDirectory, NULL, NULL, NULL, addr iosb, \
                            esi, cb, FileDirectoryInformation, \
                            TRUE, $CCOUNTED_UNICODE_STRING("c*"), TRUE

                .while eax != STATUS_NO_MORE_FILES

                    .if ( eax == STATUS_SUCCESS )

                        mov eax, [esi].FileNameLength
                        mov us._Length, ax
                        mov us.MaximumLength, ax
                        lea eax, [esi].FileName
                        mov us.Buffer, eax
                        invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE
                        .if eax == STATUS_SUCCESS

                            invoke RtlTimeToTimeFields, addr [esi].CreationTime, addr tf
                            movzx eax, tf.Day
                            movzx ecx, tf.Month
                            movzx edx, tf.Year

                            invoke DbgPrint, $CTA0("    %s   size=%d   created on %d.%02d.%04d\n"), \
                                        as.Buffer, [esi].EndOfFile.LowPart, eax, ecx, edx

                            invoke RtlFreeAnsiString, addr as
                        .endif

                    .endif

                    invoke ZwQueryDirectoryFile, hDriversDirectory, NULL, NULL, NULL, addr iosb, \
                                esi, cb, FileDirectoryInformation, \
                                TRUE, NULL, FALSE
                .endw
                invoke DbgPrint, \
                    $CTA0("FileWorks: ------------------------------------------------\n")

                assume esi:nothing
                invoke ExFreePool, pfdi
            .endif
            invoke ZwClose, hDriversDirectory
        .else
            invoke DbgPrint, $CTA0("FileWorks: Can't open drivers directory. Status: %08X\n"), eax
        .endif
        invoke ZwClose, hSystemRootDirectory
    .else
        invoke DbgPrint, $CTA0("FileWorks: Can't open system root directory. Status: %08X\n"), eax
    .endif

    ret

EnumerateFiles endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke DbgPrint, $CTA0("\nFileWorks: Entering DriverEntry\n")

    invoke CreateDirectory
    invoke CreateFile
    invoke WriteFile
    invoke MarkAsReadOnly
    invoke ReadFile
    invoke UnmarkAsReadOnly
    invoke AppendFile
    invoke ReadFile
    invoke TruncateFile
    invoke ReadFile
    invoke DeleteFile
    invoke DeleteDirectory
    invoke EnumerateFiles

    invoke DbgPrint, $CTA0("\nFileWorks: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=FileWorks

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

В ntddk.inc не входят некоторые нужные нам константы и структуры.

include \masm32\include\w2k\ntifs.inc

В Installable File System (IFS) Kit, предназначенный для разработки драйверов файловых систем, входит заголовочный файл ntifs.h, аналогом которого и является включаемый файл ntifs.inc. Этот файл входит в состав KmdKit начиная с версии 1.5.

11.3 Создание каталога и файла

    InitializeObjectAttributes addr oa, addr g_usDirName, \
                        OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

Заполняем структуру OBJECT_ATTRIBUTES, не забывая про флаг OBJ_KERNEL_HANDLE. Хочу подчеркнуть, что в данном примере это необязательно, т.к. мы не собираемся использовать ни один из описателей в контексте какого-либо другого процесса. Но поскольку процедуры драйвера FileWorks легко могут быть использованы в других проектах в контексте любого процесса, я решил сделать именно так. Если в ваши задачи входит совместное использование описателя драйвером и пользовательским процессом, то флаг OBJ_KERNEL_HANDLE использовать не следует. Если обращения к объекту будут происходить в контексте одного и того же процесса, флаг OBJ_KERNEL_HANDLE не нужен.

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

    invoke ZwCreateFile, addr hDirectory, SYNCHRONIZE, addr oa, addr iosb, 0, FILE_ATTRIBUTE_NORMAL, \
                        0, FILE_OPEN_IF, FILE_DIRECTORY_FILE + FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0

У функции ZwCreateFile довольно много параметров. Поэтому, я приведу её протопип. И вынужден сделать это, используя синтаксис языка си, чтобы были видны входные (IN) выходные (OUT) и необязательные (OPTIONAL) параметры.

NTSTATUS
  ZwCreateFile(
    OUT PHANDLE             FileHandle,
    IN  ACCESS_MASK         DesiredAccess,
    IN  POBJECT_ATTRIBUTES  ObjectAttributes,
    OUT PIO_STATUS_BLOCK    IoStatusBlock,
    IN  PLARGE_INTEGER      AllocationSize  OPTIONAL,
    IN  ULONG               FileAttributes,
    IN  ULONG               ShareAccess,
    IN  ULONG               CreateDisposition,
    IN  ULONG               CreateOptions,
    IN  PVOID               EaBuffer        OPTIONAL,
    IN  ULONG               EaLength
    );

При успешном завершении функции, параметр FileHandle получает описатель созданного каталога. DesiredAccess определяет тип запрашиваемого доступа к создаваемому каталогу. Мы передаем только флаг SYNCHRONIZE, значение которого станет ясно чуть позже. Что такое ObjectAttributes вы уже знаете. После завершения функции из параметра IoStatusBlock, являющегося указателем на структуру IO_STATUS_BLOCK можно извлечь кое-какую дополнительную информацию. Параметр FileAttributes определяет атрибуты (только для чтения, скрытый и т.п.) создаваемого каталога. Мы используем FILE_ATTRIBUTE_NORMAL, т.к. придавать какие-то особые свойства каталогу, в данном случае, не требуется. FILE_ATTRIBUTE_NORMAL используется по умолчанию, поэтому, можно просто передать в этом параметре 0. Нулевое значение в необязательном параметре AllocationSize определяет, что будет создан файл нулевого размера. В случае каталога это вполне естественно. Параметр ShareAccess определяет, может ли какой-то другой код открыть описатель каталога и с какими правами доступа. В данном случае, мы запрещаем кому-либо доступ к каталогу, установив этот параметр в 0. Параметр CreateDisposition определяет действия системы, в случае если такой каталог уже существует или наоборот - если такого файла нет. Мы используем флаг FILE_OPEN_IF, который означает, что если такой каталог уже существует, он будет открыт. В параметре CreateOptions передаем комбинацию флагов FILE_DIRECTORY_FILE и FILE_SYNCHRONOUS_IO_NONALERT. Первый в особых пояснениях не нуждается и означает, что будет создан каталог, а не файл. FILE_SYNCHRONOUS_IO_NONALERT определяет (не уверен, кстати, что в случае каталога этот флаг имеет значение), что все операции с файлом будут проводиться синхронно, т.е. например, вызов ZwReadFile не вернет управление до тех пор, пока данные из файла не будут фактически прочитаны. При этом диспетчер ввода-вывода поддерживает для файла так называемую текущую позицию указателя файла (file position context). Если флаг FILE_SYNCHRONOUS_IO_NONALERT установлен, то в параметре DesiredAccess так же должен быть определен флаг SYNCHRONIZE. Последние два параметра не используются драйверами.

    .if eax == STATUS_SUCCESS
        .if iosb.Information == FILE_CREATED
        .elseif iosb.Information == FILE_OPENED
        .endif

Как я только что сказал, в структуре IO_STATUS_BLOCK будет находиться дополнительная информация.

Вспомните, как мы завершаем обработку запроса ввода-вывода (см. предыдущие части). Например, в драйвере SharingMemory (часть 9) обработка IRP_MJ_CREATE и IRP_MJ_CLOSE завершается так:

mov eax, pIrp
mov (_IRP PTR [eax]).IoStatus.Status, STATUS_SUCCESS
and (_IRP PTR [eax]).IoStatus.Information, 0
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

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

Т.к. мы определили флаг FILE_OPEN_IF, система может поступить двояко. По значению iosb.Information мы узнаем, что же произошло: был ли создан новый каталог или такой каталог уже существовал и поэтому был открыт.

        invoke ZwClose, hDirectory
    .endif

Лишний раз повторю, что в ядре необходимо явно закрывать все открытые описатели.

    invoke ZwCreateFile, addr hFile, SYNCHRONIZE, addr oa, addr iosb, 0, FILE_ATTRIBUTE_NORMAL, \
                        0, FILE_CREATE, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0

Как видите, файл создается практически аналогично, нужно только убрать флаг FILE_DIRECTORY_FILE. А флаг FILE_CREATE я использую для разнообразия. Он означает, что файл может быть только создан. Если такой файл уже существует, вызов ZwCreateFile окончится неудачей.

11.4 Объект "файл"

Каждому открытому описателю файла соответствует объект "файл" (file object), представляющийся в памяти ядра структурой FILE_OBJECT.

FILE_OBJECT STRUCT                              ; sizeof = 070h
    _Type                   SWORD       ?       ; 0000h  IO_TYPE_FILE
    _Size                   SWORD       ?       ; 0002h
    DeviceObject            PVOID       ?       ; 0004h  PTR DEVICE_OBJECT
    Vpb                     PVOID       ?       ; 0008h  PTR VPB
    FsContext               PVOID       ?       ; 000Ch
    FsContext2              PVOID       ?       ; 0010h
    SectionObjectPointer    PVOID       ?       ; 0014h  PTR SECTION_OBJECT_POINTERS
    PrivateCacheMap         PVOID       ?       ; 0018h
    FinalStatus             SDWORD      ?       ; 001Ch
    RelatedFileObject       PVOID       ?       ; 0020h  PTR FILE_OBJECT
    LockOperation           BYTE        ?       ; 0024h  BOOLEAN
    DeletePending           BYTE        ?       ; 0025h  BOOLEAN
    ReadAccess              BYTE        ?       ; 0026h  BOOLEAN
    WriteAccess             BYTE        ?       ; 0027h  BOOLEAN
    DeleteAccess            BYTE        ?       ; 0028h  BOOLEAN
    SharedRead              BYTE        ?       ; 0029h  BOOLEAN
    SharedWrite             BYTE        ?       ; 002Ah  BOOLEAN
    SharedDelete            BYTE        ?       ; 002Bh  BOOLEAN
    Flags                   DWORD       ?       ; 002Ch
    FileName                UNICODE_STRING  <>  ; 0030h
    CurrentByteOffset       LARGE_INTEGER   <>  ; 0038h
    Waiters                 DWORD       ?       ; 0040h
    Busy                    DWORD       ?       ; 0044h
    LastLock                PVOID       ?       ; 0048h
    _Lock                   KEVENT      <>      ; 004Ch
    Event                   KEVENT      <>      ; 005Ch
    CompletionContext       PVOID       ?       ; 006Ch  PTR IO_COMPLETION_CONTEXT
FILE_OBJECT ENDS
PFILE_OBJECT typedef ptr FILE_OBJECT

Например, мы можем два раза открыть один и тот же файл, но запросить разные права доступа: чтение (FILE_READ_DATA) в первом случае и запись (FILE_WRITE_DATA) - во втором. В результате в ядре будет создано две структуры FILE_OBJECT, каждая из которых будет соответствовать своему описателю файла. Но оба описателя и соответственно обе структуры соответствуют одному и тому же файлу на диске. В первой структуре FILE_OBJECT будет установлено поле ReadAccess, во второй - WriteAccess. Например, при попытке записи в файл по первому описателю, система увидит, что поле WriteAccess = FALSE и запрос будет отклонен.

Поле DeviceObject будет содержать указатель на объект "устройство" \Device\HarddiskVolume1, т.к. именно на это устройство ссылается символьная ссылка \??\c:, которую мы используем в имени создаваемого файла.

Можете после прочтения статьи поэкспериментировать с файлами и посмотреть, как система заполняет и управляет структурой FILE_OBJECT. Если возникнут проблемы с локализацией её в памяти, можно использовать функцию ObReferenceObjectByHandle примерно таким образом:

local pFileObject:PFILE_OBJECT
. . .
invoke ObReferenceObjectByHandle, hFile, FILE_READ_DATA, NULL, KernelMode, addr pFileObject, NULL
.if eax == STATUS_SUCCESS
    ; pFileObject указывает на структуру FILE_OBJECT соответствующую описателю hFile
    fastcall ObfDereferenceObject, pFileObject
.endif

ObReferenceObjectByHandle вернет в переменной pFileObject указатель на структуру FILE_OBJECT, соответствующую описателю файла. При этом счетчик ссылок будет увеличен на единицу. Вызовом ObfDereferenceObject необходимо вернуть ему старое значение.

11.5 Запись в файл

К этому моменту у нас уже есть каталог и файл нулевого размера. Пора в него что-нибудь записать - это будет строка символов.

    invoke ZwCreateFile, addr hFile, FILE_WRITE_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                        0, 0, FILE_SHARE_READ, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0

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

Для записи в файл нужен соответствующий доступ - используем флаг FILE_WRITE_DATA.

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

FILE_GENERIC_WRITE equ (STANDARD_RIGHTS_WRITE or FILE_WRITE_DATA or FILE_WRITE_ATTRIBUTES or FILE_WRITE_EA or FILE_APPEND_DATA or SYNCHRONIZE)

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

Очевидно, что пока мы будем писать в файл никто другой не должен делать того же. Используем флаг FILE_SHARE_READ - больше никто не сможет открыть наш файл ни для записи, ни для удаления, а сможет только получить доступ на чтение из файла.

    .if eax == STATUS_SUCCESS

        CTA0 "Data can be written to an open file", g_szData, 4

        invoke ZwWriteFile, hFile, 0, NULL, NULL, addr iosb, \
                        addr g_szData, sizeof g_szData - 1, NULL, NULL

Название функции ZwWriteFile говорит само за себя.

NTSTATUS
  ZwWriteFile(
    IN  HANDLE            FileHandle,
    IN  HANDLE            Event       OPTIONAL,
    IN  PIO_APC_ROUTINE   ApcRoutine  OPTIONAL,
    IN  PVOID             ApcContext  OPTIONAL,
    OUT PIO_STATUS_BLOCK  IoStatusBlock,
    IN  PVOID             Buffer,
    IN  ULONG             Length,
    IN  PLARGE_INTEGER    ByteOffset  OPTIONAL,
    IN  PULONG            Key  OPTIONAL
    );

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

11.6 Изменение свойств файла

Предположим, нам потребовалось предотвратить удаление файла.

    invoke ZwCreateFile, addr hFile, FILE_READ_ATTRIBUTES + FILE_WRITE_ATTRIBUTES + SYNCHRONIZE, \
                        addr oa, addr iosb, 0, 0, FILE_SHARE_READ, \
                        FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0

Флаги FILE_READ_ATTRIBUTES и FILE_WRITE_ATTRIBUTES требуются для получения и изменения свойств файла соответственно.

    .if eax == STATUS_SUCCESS
        invoke ZwQueryInformationFile, hFile, addr iosb, addr fbi, sizeof fbi, FileBasicInformation
        .if eax == STATUS_SUCCESS

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

            or fbi.FileAttributes, FILE_ATTRIBUTE_READONLY
            invoke ZwSetInformationFile, hFile, addr iosb, addr fbi, sizeof fbi, FileBasicInformation

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

            and fbi.FileAttributes, not FILE_ATTRIBUTE_READONLY

11.7 Чтение из файла

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

NTSTATUS
  ZwOpenFile(
    OUT PHANDLE             FileHandle,
    IN  ACCESS_MASK         DesiredAccess,
    IN  POBJECT_ATTRIBUTES  ObjectAttributes,
    OUT PIO_STATUS_BLOCK    IoStatusBlock,
    IN  ULONG               ShareAccess,
    IN  ULONG               OpenOptions
    );

    invoke ZwOpenFile, addr hFile, FILE_READ_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE, FILE_SYNCHRONOUS_IO_NONALERT

FILE_READ_DATA - в данный момент нам будет достаточно только доступа для чтения данных из файла. Комбинация флагов FILE_SHARE_READ, FILE_SHARE_WRITE и FILE_SHARE_DELETE позволит всем остальным получать полный доступ к файлу.

    .if eax == STATUS_SUCCESS
        invoke ZwQueryInformationFile, hFile, addr iosb, addr fsi, sizeof fsi, FileStandardInformation
        .if eax == STATUS_SUCCESS

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

            mov eax, fsi.EndOfFile.LowPart
            inc eax
            mov cb, eax

Добавим один байт на завершающий ноль.

            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL
                mov p, eax

                invoke RtlZeroMemory, p, cb

                invoke ZwReadFile, hFile, 0, NULL, NULL, addr iosb, p, cb, 0, NULL
                .if eax == STATUS_SUCCESS
                    invoke DbgPrint, $CTA0("FileWorks: File content: \=%s\=\n"), p
                .endif

                invoke ExFreePool, p
            .endif

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

11.8 Добавление данных в файл

Есть несколько путей добавления данных в файл. Можно открыть его с флагом FILE_WRITE_DATA, установить текущую позицию указателя файла на его конец, передав смещение в параметре ByteOffset функции ZwWriteFile, и писать данные. Текущая позиция указателя файла, кстати, храниться в FILE_OBJECT.CurrentByteOffset. А можно открыть файл в флагом FILE_APPEND_DATA и текущая позиция указателя файла будет автоматически установлена на его конец. Так мы и поступим.

    invoke ZwOpenFile, addr hFile, FILE_APPEND_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                                    FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT
    .if eax == STATUS_SUCCESS

        CTA0 " using ZwWriteFile", g_szDataToAppend, 4

        invoke ZwWriteFile, hFile, 0, NULL, NULL, addr iosb, \
                        addr g_szDataToAppend, sizeof g_szDataToAppend - 1, NULL, NULL

Размер файла будет автоматически увеличен в соответствии с размером добавленных данных.

11.9 Усечение файла

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

    invoke ZwOpenFile, addr hFile, FILE_WRITE_DATA + SYNCHRONIZE, addr oa, addr iosb, \
                        FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT

Открываем файл с доступом на запись данных.

    .if eax == STATUS_SUCCESS
        invoke ZwQueryInformationFile, hFile, addr iosb, \
                        addr fsi, sizeof fsi, FileStandardInformation
        .if eax == STATUS_SUCCESS

Получаем текущий размер файла в составе структуры FILE_STANDARD_INFORMATION.

            and feofi.EndOfFile.HighPart, 0
            mov eax, fsi.EndOfFile.LowPart
            shr eax, 1
            mov feofi.EndOfFile.LowPart, eax
            invoke ZwSetInformationFile, hFile, addr iosb, \
                        addr feofi, sizeof feofi, FileEndOfFileInformation

Устанавливаем новый размер, равный половине текущего, пользуясь информационным классом FileEndOfFileInformation.

11.10 Удаление файла и каталога

Дело за малым - привести диск c: в первоначальное состояние. Как это ни странно, но 2000 DDK умалчивает о существовании функции ZwDeleteFile. В XP DDK говорится, что эта функция все таки существует в системе, но начиная с Windows XP. Но это не так. ZwDeleteFile есть и Windows 2000 и даже в Windows NT4. Но для удаления файла мы воспользуемся несколько более сложным способом, а вот каталог удалим с помощью ZwDeleteFile.

    invoke ZwCreateFile, addr hFile, DELETE + SYNCHRONIZE, addr oa, addr iosb, \
                        0, 0, FILE_SHARE_DELETE, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0

Открываем файл для удаления. При этом всем остальным также разрешаем только удалять его.

    .if eax == STATUS_SUCCESS

        mov fdi.DeleteFile, TRUE
        invoke ZwSetInformationFile, hFile, addr iosb, addr fdi, sizeof fdi, FileDispositionInformation

Воспользуемся информационным классом FileDispositionInformation и установим признак удаления файла. При этом в структуре FILE_OBJECT поле DeletePending изменит свое значение с FALSE на TRUE. Это будет означать, что файл отмечен для удаления. До тех пор, пока хотя бы один открытый описатель такого файла существует, он не будет удален.

        invoke ZwClose, hFile

Теперь единственный описатель файла закрыт и файл удален.

    invoke ZwDeleteFile, addr oa

Удаление каталога с помощью ZwDeleteFile проходит несколько проще и пояснений не требует.

11.11 Перечисление содержимого каталога

Существует два альтернативных способа определения имени создаваемого/открываемого объекта вообще и файла в частности. Доступ к именованному объекту можно получить либо используя полный путь, либо относительный. До сих пор мы использовали только абсолютные пути. Например, путь \??\c:\FileWorks\test.txt является абсолютным. В этом случае заполнение структуры OBJECT_ATTRIBUTES с помощью макроса InitializeObjectAttributes выглядит примерно так:

InitializeObjectAttributes addr oa, $CCOUNTED_UNICODE_STRING("\??\c:\FileWorks\test.txt"), OBJ_CASE_INSENSITIVE, NULL, NULL

Предпоследний параметр RootDirectory равен NULL. Параметр RootDirectory макроса InitializeObjectAttributes и одноименное поле структуры OBJECT_ATTRIBUTES, которую этот макрос заполняет, определяет описатель каталога-контейнера объекта.

OBJECT_ATTRIBUTES STRUCT
. . .
    RootDirectory               HANDLE          ?
. . .
OBJECT_ATTRIBUTES ENDS

Если каталог-контейнер объекта уже открыт, т.е. имеется его описатель, то к объекту можно обратиться по относительному к каталогу-контейнеру пути. При этом описатель каталога-контейнера должен быть помещен в поле RootDirectory. Например, если мы уже открыли каталог \??\c:\FileWorks\ и поместили его описатель в переменную hDirectory, то можем использовать относительный путь файла test.txt таким образом:

InitializeObjectAttributes addr oa, $CCOUNTED_UNICODE_STRING("test.txt"), OBJ_CASE_INSENSITIVE, hDirectory, NULL

Под каталогом-контейнером подразумевается не только каталог на диске, но и каталог в пространстве имен диспетчера объектов.

Для перечисления содержимого системного каталога \%SystemRoot%\System32\Drivers\ мы как раз и будем использовать относительный путь.

    InitializeObjectAttributes addr oa, $CCOUNTED_UNICODE_STRING("\\SystemRoot"), \
                                OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, NULL, NULL

Заполняем структуру OBJECT_ATTRIBUTES, используя символьную ссылку \SystemRoot - этот путь абсолютный.

    invoke ZwOpenFile, addr hSystemRootDirectory, FILE_LIST_DIRECTORY + SYNCHRONIZE, addr oa, \
                        addr iosb, FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE, \
                        FILE_DIRECTORY_FILE + FILE_SYNCHRONOUS_IO_NONALERT

Открываем каталог \%SystemRoot%\. Флаг FILE_LIST_DIRECTORY позволит перечислять содержимое каталога.

    .if eax == STATUS_SUCCESS

        InitializeObjectAttributes addr oa, $CCOUNTED_UNICODE_STRING("system32\\drivers"), \
                            OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE, hSystemRootDirectory, NULL

Если каталог успешно открыт, то в переменной hSystemRootDirectory получим его описатель, который и будем использовать в качестве описателя каталога-контейнера. Заполняем структуру OBJECT_ATTRIBUTES, используя относительный путь "system32\drivers".

        invoke ZwOpenFile, addr hDriversDirectory, FILE_LIST_DIRECTORY + SYNCHRONIZE, addr oa, \
                            addr iosb, FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE, \
                            FILE_DIRECTORY_FILE + FILE_SYNCHRONOUS_IO_NONALERT
        .if eax == STATUS_SUCCESS

Открываем каталог \%SystemRoot%\System32\Drivers\ по относительному пути.

            mov cb, sizeof FILE_DIRECTORY_INFORMATION + 256

            invoke ExAllocatePool, PagedPool, cb
            .if eax != NULL

Выделяем небольшой буфер, в который должна поместиться структура FILE_DIRECTORY_INFORMATION и имя файла.

                mov pfdi, eax
                mov esi, eax
                assume esi:ptr FILE_DIRECTORY_INFORMATION

                invoke ZwQueryDirectoryFile, hDriversDirectory, NULL, NULL, NULL, addr iosb, \
                            esi, cb, FileDirectoryInformation, \
                            TRUE, $CCOUNTED_UNICODE_STRING("c*"), TRUE

Начинаем перечислять файлы каталога, используя информационный класс FileDirectoryInformation. Прототип функции ZwQueryDirectoryFile, пожалуй, будет не лишним.

NTSTATUS
  ZwQueryDirectoryFile(
    IN  HANDLE                  FileHandle,
    IN  HANDLE                  Event       OPTIONAL,
    IN  PIO_APC_ROUTINE         ApcRoutine  OPTIONAL,
    IN  PVOID                   ApcContext  OPTIONAL,
    OUT PIO_STATUS_BLOCK        IoStatusBlock,
    OUT PVOID                   FileInformation,
    IN  ULONG                   Length,
    IN  FILE_INFORMATION_CLASS  FileInformationClass,
    IN  BOOLEAN                 ReturnSingleEntry,
    IN  PUNICODE_STRING         FileName    OPTIONAL,
    IN  BOOLEAN                 RestartScan
    );

И о существовании этой функции 2000 DDK тоже умалчивает. В XP DDK говорится, что эта функция существует в системе, начиная с Windows XP. И это тоже неправда.

Параметр ReturnSingleEntry устанавливаем равным TRUE, что заставляет функцию ZwQueryDirectoryFile вернуть информацию только об одном файле, причем первом. Параметр FileName указывает на строку с условием поиска "c*", т.е. будут перечисляться только файлы, имена которых начинаются с символа "c". Это позволит сократить объем выводимой через отладочные сообщения информации. При первом вызове ZwQueryDirectoryFile мы должны установить параметр RestartScan в TRUE. Это заставит функцию ZwQueryDirectoryFile начать просмотр содержимого каталога.

                .while eax != STATUS_NO_MORE_FILES

Запускаем цикл, который крутится до тех пор, пока ZwQueryDirectoryFile не вернет STATUS_NO_MORE_FILES, т.е. пока все файлы в каталоге не будут перечислены.

                    .if ( eax == STATUS_SUCCESS )

Если вдруг в просматриваемом нами каталоге окажется файл, длина имени которого превысит 256 байт (что, кстати, практически невероятно, т.к. имена драйверов не превышают 8 символов), ZwQueryDirectoryFile вернет код ошибки отличный от STATUS_NO_MORE_FILES. В этом случае мы просто пропускаем такой файл.

                        mov eax, [esi].FileNameLength
                        mov us._Length, ax
                        mov us.MaximumLength, ax
                        lea eax, [esi].FileName
                        mov us.Buffer, eax
                        invoke RtlUnicodeStringToAnsiString, addr as, addr us, TRUE
                        .if eax == STATUS_SUCCESS

                            invoke RtlTimeToTimeFields, addr [esi].CreationTime, addr tf
                            movzx eax, tf.Day
                            movzx ecx, tf.Month
                            movzx edx, tf.Year

                            invoke DbgPrint, $CTA0("    %s   size=%d   created on %d.%02d.%04d\n"), \
                                        as.Buffer, [esi].EndOfFile.LowPart, eax, ecx, edx

                            invoke RtlFreeAnsiString, addr as
                        .endif

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

                    .endif

                    invoke ZwQueryDirectoryFile, hDriversDirectory, NULL, NULL, NULL, addr iosb, \
                                esi, cb, FileDirectoryInformation, \
                                TRUE, NULL, FALSE
                .endw

В цикле вызываем ZwQueryDirectoryFile, но параметры ReturnSingleEntry, FileName и RestartScan равны TRUE, NULL и FALSE соответственно. Это заставит функцию ZwQueryDirectoryFile продолжить перечисление файлов.

                invoke ExFreePool, pfdi
            .endif
            invoke ZwClose, hDriversDirectory
        .endif
        invoke ZwClose, hSystemRootDirectory
    .endif

Отдаем все занятые ресурсы назад.


Продолжение следует

KmdTut Logo

Продолжение следует...

Ещё несколько статей доступны на сайте http://www.wasm.ru/. По прошествии некоторого времени они будут добавлены в этот документ.