39770

Драйверы режима ядра

Реферат

Информатика, кибернетика и программирование

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

Русский

2013-10-08

1.04 MB

30 чел.

Драйверы режима ядра: Часть 1: Основные понятия

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

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

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

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

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

  •  Процессы поддержки системы (System Support Processes) - например, процесс входа в систему Winlogon (реализован в \%SystemRoot%\System32\Winlogon.exe);
  •  Процессы сервисов (Service Processes) - например, спулер печати;
  •  Пользовательские приложения (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).

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

Исполнительная система (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 2000

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

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

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

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

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

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

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

Драйверы Windows 2000 

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

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

  •  Драйверы пользовательского режима (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 2000.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

"Голубой экран смерти" 

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

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

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

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

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

Driver Development Kit 

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

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

Включаемые файлы 

Также нам понадобятся включаемые (*.inc) файлы с определениями прототипов функций. Их нам (точнее мне :-)) тоже придется делать самим. Я перепробовал много разных утилит, конвертирующих *.lib -> *.inc, как входящих в пакет masm32 by hutch, так и слитых мной в разное время с бескрайних просторов Internet. Из всех что имеются у меня в наличии, только protoize.exe by f0dder справилась со своей задачей, и мне практически ничего не пришлось править руками. Эта замечательная тулза будет лежать в каталоге \tools\protoize. Сайт автора: http://f0dder.didjitalyphrozen.com/. Только вы ее там не найдете. f0dder несколько раз постил эту утилиту в конференции http://board.win32asmcommunity.net/. Инклуды будут лежать в каталоге \include\w2k. Их следует поместить в каталог \masm32\include\w2k. Для конвертации использовались *.lib для свободного выпуска Windows 2000, т. к. у меня стоит именно этот вариант (и у вас, наверняка, тоже).

Следующая проблема более серьезна. Это практически полное отсутствие включаемых файлов с определениями необходимых структур, символьных констант и макросов. Найти что-либо путное в сети вы вряд ли сможете - уж слишком экзотическое это занятие - писать драйверы режима ядра на ассемблере. Кое-что можно найти у EliCZ http://www.anticracking.sk/EliCZ/. Кое-что у Y0da http://mitglied.lycos.de/yoda2k/index.htm (частично сделаное им самим, частично взятое у того же EliCZ). Но сделано это из рук вон плохо, (при всем моем глубоком уважении к нашим словакскому и немецкому коллегам): имена членов многих структур отличаются от определенных в оригинальных заголовочных файлах из DDK; вложенные структуры и обьединения не имеют имен; хотя в оригинале они именованы. И вообще, все находится в некотором беспорядке, и при просмотре вызывает удручающее впечатление. Неплохо сделан только ntstatus.inc. Частично это объясняется тем, что EliCZ начал создавать свои инклуды еще в отсутствие у него DDK (как он сам говорит). В любом случае, я не советую вам их использовать, по крайней мере без тщательной проверки. Кое-что, в свое время, мелькало в конференции http://board.win32asmcommunity.net/, но качество тоже не особо впечатляет. Короче, единственно правильное решение в данной ситуации - делать все самим, причем вручную, т. к. какие-либо тулзы, позволяющие автоматизировать этот процесс, мне не известны. Если вы, вдруг, наткнетесь на что-то стоящее, не сочтите за труд - дайте мне знать.

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

Также нам потребуется отладчик, причем, поскольку отлаживать придется код режима ядра, то и отладчик нужен соответствующий. Самым лучшим выбором будет SoftICE. Или можно воспользоваться Kernel Debugger входящим в состав DDK. Этот отладчик требует двух компьютеров - ведущего и ведомого, что не каждый может себе позволить. Марк Руссинович (Mark Russinovich, http://www.sysinternals.com/) написал утилиту LiveKd, которая позволяет использовать Kernel Debugger без подключения второго компьютера. Не знаю есть ли она на сайте (не проверял), но на диске к книжке "Внутреннее устройство Microsoft Windows 2000" имеется. Также этот отладчик чрезвычайно полезен для исследования внутреннего устройства системы, при условии что у вас установлены отладочные символы, которые можно (или было можно) свободно скачать с сайта Microsoft.

Что почитать 

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

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

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

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

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

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

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

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

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

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

Драйверы режима ядра: Часть 2: Службы

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

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

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

Службы 

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

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

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

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

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

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

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

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

На конечном этапе загрузки системы, перед появлением диалога регистрации пользователя, запускается 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).

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

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

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

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

DisplayName 

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

ErrorControl 

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

SERVICE_ERROR_IGNORE (0)

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

SERVICE_ERROR_NORMAL (1)

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

Журнал событий системы можно просмотреть стандартными средствами Windows 2000: Администрирование > Просмотр событий (Administrative Tools > Event Viewer).

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

ImagePath 

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

Start 

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

SERVICE_AUTO_START (2)

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

SERVICE_DEMAND_START (3)

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

Все драйверы устройств, у которых параметр Start равен SERVICE_AUTO_START (2), автоматически запускаются диспетчером управления службами при загрузке системы. Это автоматически запускаемые службы (auto-start services). При этом запускаются и все драйверы устройств, от которых данный драйвер зависит, даже если параметр Start у них не указывает на автоматическую загрузку. (Для определения порядка своей загрузки драйверы устройств могут использовать параметры Group и Tag, а сервисы DependOnGroup и 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 можно извлечь следующую информацию: драйвер режима ядра beep.sys, находящийся в каталоге C:\masm32\mProgs\Ring0\Kmd\Article2\beep и имеющий экранное имя "Nice Melody Beeper", запускается по требованию, возможные ошибки игнорируются и не заносятся в журнал событий системы.

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

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

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

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

Вот код простейшей SCP, которая будет управлять драйвером beep.sys. Находится в файле scp.asm.

 .386

.model flat, stdcall

option casemap:none

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

;                                  I N C L U D E   F I L E S                                        

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

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

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

;                                         C O D E                                                   

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

.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("beep.sys"), sizeof acDriverPath, addr acDriverPath, esp

        pop eax

        invoke CreateService, hSCManager, $CTA0("beep"), $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

Первое, что нам необходимо сделать - это, пользуясь терминологией 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. Это можно сделать так:

 SERVICES_ACTIVE_DATABASE equ $CTA0("ServicesActive")

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

Т.к. мы не собираемся открывать никакую другую базу данных 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 для регистрации драйвера, не говоря уже обо всем остальном, естественно надо иметь некоторые привилегии. Так что будем считать, что они у нас есть.

Получив доступ к базе SCM, мы регистрируем в ней свой драйвер beep.sys, с помощью функции 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

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("beep.sys"), sizeof acDriverPath, addr acDriverPath, esp

pop eax

invoke CreateService, hSCManager, $CTA0("beep"), $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

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

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

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

Если драйвер, который мы пытаемся зарегистрировать, уже существует, то вызов CreateService, естественно, завершится неудачей. Последующий вызов функции GetLastError вернет ERROR_SERVICE_EXISTS. Если же все ОК, то мы получим описатель вновь созданной службы, и поместим его в переменную hService. Он понадобится для дальнейших манипуляций, первой из которых будет запуск драйвера функцией 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 (тут почти полная аналогия с DllMain в обычной DLL). Принципиальная разница, не считая уровня привилегий, в том, что код процедуры DriverEntry всегда выполняется одним из потоков процесса System, и, естественно, в контексте этого процесса.

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

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

    invoke DeleteService, hService

    invoke CloseServiceHandle, hService

 .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 закрывает описатель hSCManager самого SCM.

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

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

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

Ну, и самое последнее на сегодня. Я, конечно, не могу заставить вас мучиться ожиданием следующей статьи. Поэтому, в архиве к этой статье, помимо исходных кодов SCP, содержится также и откомпилированный драйвер beep.sys. Несмотря на наличие числа 2000 в заголовке статьи, этот драйвер прекрасно работает и под XP. Думаю будет работать и под NT4.0, но возможности проверить это у меня нет.

Драйверы режима ядра: Часть 3: Простейшие драйверы

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

;@echo off

;goto make

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

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

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

 :make

\masm32\bin\ml /nologo /c /coff driver.bat

\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:driver.sys /subsystem:native driver.obj

del driver.obj

echo.

 pause

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

Параметры компоновки имеют следующий смысл:

/driver 

- Указывает компоновщику, что нужно сформировать файл драйвера режима ядра Windows NT;

/base:0x10000 

- Устанавливает предопределенный адрес загрузки образа драйвера равным 10000h. Я уже говорил про это в предыдущей статье;

/align:32 

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

/out:driver.sys 

- По умолчанию компоновщик производит файлы с расширением .exe. При наличии ключа /dll файл будет иметь расширение .dll. Нам нужно получить файл с расшрением .sys;

/subsystem:native 

- В PE-заголовке имеется поле, указывающее загрузчику образа исполняемого файла, для какой подсистемы этот файл предназначен: Win32, POSIX или OS/2. Это нужно для того, чтобы поместить образ в необходимое ему окружение. Подсистема Win32 автоматически запускается при загрузке системы. Если же запускается файл, предназначенный для функционирования, например, в подсистеме POSIX, то сначала операционная система запускает саму подсистему POSIX. Таким образом, с помощью этого ключа можно указать компоновщику, какая подсистема необходима. Когда мы компилируем *.exe или *.dll, то указываем под этим ключем значение windows, которое означает, что файлу требуется подсистема Win32. Драйверу вообще не нужна ни одна из подсистем, т.к. он работает в естественной (native) для самой операционной системы среде.


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

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

;@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 proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR

    ret

DriverEntry endp

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

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

end DriverEntry

:make

\masm32\bin\ml /nologo /c /coff simplest.bat

\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:simplest.sys /subsystem:native simplest.obj

 del simplest.obj

echo.

pause

Как и у любого другого выполнимого модуля, у драйвера должна быть точка входа, на которую система передаст управление после загрузки драйвера в память. Как и полагается в программе на ассемблере, точкой входа является первая инструкция, обозначенная меткой указанной в директиве 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

    woLength        WORD    ?  ; длина строки в байтах (не символах)

    MaximumLength   WORD    ?  ; длина буфера содержащего строку в байтах (не символах)

    Buffer          PWSTR   ?  ; указатель на буфер содержащий строку

UNICODE_STRING ENDS

woLength 

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

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

Теперь перейдем к рассмотрению драйвера, программу управления которым, мы писали в прошлый раз. Мне пришлось переименовать его из beep.sys в beeper.sys, потому что, как оказалось, в NT4 и в некоторых версиях XP уже существует драйвер beep.sys. Вобще говоря, beep.sys есть во всех версиях NT (\%SystemRoot%\System32\Drivers\beep.sys), но он еще должен быть зарегистрирован в реестре. Как бы там ни было, надеюсь beeper.sys будет уникальным. Вот его исходный текст:

;@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

\masm32\bin\ml /nologo /c /coff beeper.bat

\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:beeper.sys /subsystem:native beeper.obj

 del beeper.obj

echo.

pause

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

На материнской плате находится устройство системный таймер, который является перепрограммируемым. Таймер содержит несколько каналов, 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.sys можно считать законченной. Он возвращает системе код ошибки и благополучно удаляется из памяти. На всякий случай повторяю: код ошибки нужно вернуть, только для того, чтобы система удалила драйвер из памяти. Все что мог, он уже сделал. Когда мы доберемся до полнофункциональных драйверов, то, естественно, будем возвращать STATUS_SUCCESS.

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

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

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

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

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

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

;

;  DateTime.asm

;

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

 ;

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

.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

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

 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.

На материнской плате компьютера имеется специальная микросхема, выполненная по технологии 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.sys, к рассмотрению исходного текста которого мы и переходим.

 ;@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

\masm32\bin\ml /nologo /c /coff giveio.bat

\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:giveio.sys /subsystem:native giveio.obj

 del giveio.obj

echo.

pause

Код драйвера основан на хорошо известных изысканиях Дейла Робертса, восходящих аж к 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 байт. Для манипулирования 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 XP недокументированные структуры сильно отличаются. Так что, использовать этот файлик при компиляции драйвера предназначенного для XP, не самая лучшая идея). Код функции Ke386IoSetAccessProcess устанавливает член IopmOffset структуры KPROCESS в соответствующее значение.

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

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

 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

Макрос InitializeObjectAttributes находится еще на стадии разработки, так что не советую использовать его способом отличным от приведенного выше. Если что не так - я не виноват ;-)

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 (все там же - \Macros\Strings.mac). Обратите внимание на то, что я указываю выравнивание строки по границе двойного слова (выравнивание самой структуры 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, мы резервируем на стеке для него место, а после вызова извлекаем значение. Я постоянно пользуюсь таким приемом - очень удобно.

.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

            mov status, STATUS_INSUFFICIENT_RESOURCES

        .endif

    .endif

    invoke ZwClose, hKey

.endif

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

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

В этом примере я обратился к памяти CMOS, просто, для разнообразия. Можно было, как в предыдущем драйвере beeper.sys, попищать системным динамиком. Оставляю это вам, в качестве домашнего задания. Надо будет открыть доступ к соответствующим портам ввода-вывода. Вызвать процедуру 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, достаточно безобидное занятие. Но обращение к каким-то другим портам может быть небезопасно, т.к. в режиме пользователя его невозможно синхронизировать.

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

Сейчас мы уже более предметно можем поговорить об этом увлекательном процессе. Как я уже говорил в первой части, удобнее всего использовать в качестве отладчика 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://sysinternals.com/ntw2k/utilities.shtml

Что в архиве 

В архиве к этой статье, помимо исходных кодов примеров и макросов, вы обнаружите:

\tools\protoize

- утилита конвертации библиотечных .lib файлов во включаемые .inc файлы сделанная f0dder;

Некоторые inc-файлы в каталоге \include\w2k\ изготовлены с ее помощью. Правда, все __cdecl-функции мне пришлось фиксить руками :-(

\tools\KmdManager

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

\include\w2k

- необходимые включаемые файлы;

\lib\w2k

- необходимые библиотечные файлы.

В связи с тем, что Microsoft прекратила свободное распространение DDK, у вас могут возникнуть некоторые проблемы при компиляции драйверов. Прежде всего - это отсутствие .lib файлов. В этом каталоге находятся файлы от свободного выпуска Windows 2000, но подойдут без проблем и для Windows XP, и, думаю, для Windows NT4.0 тоже. Надеюсь, Microsoft на меня за это не очень обидится ;-)

Что почитать 

Документацию DDK, помимо сайта http://www.microsoft.com/, можно посмотреть тут: "Windows XP SP1 DDK Documentation On-line".

Все Zw* функции и некоторые структуры описаны подробно в книге Гэри Неббета "Справочник по базовым функциям API Windows NT/2000", Издательский дом "Вильямс", 2002. В сети можно найти электронную версию этой книги: Gary Nebbett, "Windows NT-2000 Native API Reference".

Вобщем, на первых порах, можно обойтись и без DDK. Если чувствуете, что чего-то не хватает - ищите в сети. При желании найти можно многое.

Все драйверы я тестировал под Windows 2000 Pro и Windows XP Pro. Но все должно работать и на более ранних выпусках Windows NT. До встречи в следующей статье, где мы поговорим о подсистеме ввода-вывода вообще, и о диспетчере ввода-вывода в частности.

Драйверы режима ядра: Часть 4: Подсистема ввода-вывода

· версия для печати 

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

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

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

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

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

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

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

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

;

; 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, pacBuf: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, pacBuf, 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. Эти три функции являются ключевыми. Причем, ни в одном параметре передающимся в эти функции вы не найдете и намека, как я уже сказал, на какой-либо драйвер. Все операции ввода-вывода здесь происходят с виртуальным устройством.

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

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

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

Имена объектов попадают в пространство имен диспетчера объектов. Пространство имен имеет иерархическую структуру аналогичную структуре каталогов файловой системы. Для просмотра базы данных диспетчера объектов можно использовать разные утилиты, самой удобной из которых, на мой взгляд, является WinObj уже знакомого вам Марка Руссиновича http://www.sysinternals.com/.

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

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

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

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

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

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

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

Для того чтобы объект "устройство" стал доступен коду режима пользователя, драйвер должен создать в доступном ему (коду режима пользователя) каталоге "\??" еще один объект - символьную ссылку (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".

Когда будете просматривать эти объекты на своем компьютере, имейте ввиду, что WinObj сортирует имена с учетом регистра.

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

Перейдем теперь к разбору исходного кода. При успешном завершении функции 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 все равно возвращал описатель устройства. Не смотря на это, в дальнейшем, мы все равно будем считать, что этот параметр работает так, как написано в документации.

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, что влечет за собой переход в режим ядра. Все адреса таких функций сведены в единый массив 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 - счетчик ссылок, второй - описателей.

                .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.

В самом начале я сказал, что код драйвера, по сути, является набором функций, позволяющих решать задачи недоступные коду режима пользователя. Поскольку драйвер устройства может выполнять массу таких задач, необходимо как то дифференцировать запросы. Для этого и предназначен второй параметр 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)

- доступ на запись, т.е. передачу данных на устройство;

- комбинация последних двух значений.

Function 

- собственно и определяет операцию, которую должно выполнить устройство (12 бит).

Может принимать значение в диапазоне 0-FFFh, который также разбит на две равные половины. Диапазон 0-7FFh зарезервирован Microsoft, а диапазон 800h-0FFFh доступен.

Method 

- определяет метод ввода-вывода.

Размер поля 2 бита - 4 значения, и все они могут быть использованы для наших целей:

METHOD_BUFFERED (0)

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

METHOD_IN_DIRECT (1)

METHOD_OUT_DIRECT (2)

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

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, т.к. эти значения требуются как в исходном тексте программы управления, так и в исходном тексте драйвера. Т.о. все изменения в этом файле отразятся на исходных текстах программы управления и драйвера.

 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)

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

                    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

                    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". Исходные же коды программы управления вместе с откомпилированным драйвером и файлом winioctl.inc в архиве.

Драйверы режима ядра: Часть 5: Полнофункциональный драйвер

· версия для печати 

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

 ;@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

        .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [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

        .else

            mov status, STATUS_BUFFER_TOO_SMALL

        .endif

    .else

        mov status, STATUS_INVALID_DEVICE_REQUEST

    .endif

    assume edi:nothing

    m2m [esi].IoStatus.Status, status

    m2m [esi].IoStatus.Information, dwBytesReturned

    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


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

Начнем с определения структур 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", значением которой является строка "\??"

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

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

.code INIT

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

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

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

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

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

                                             0, FALSE, addr pDeviceObject

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

IoCreateDevice proto std call 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. А при выгрузке драйвера мы извлечем этот указатель прямо из объекта "драйвер".

    .if eax == STATUS_SUCCESS

        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName

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

        .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, коих в Windows 2000 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.

        .else

            invoke IoDeleteDevice, pDeviceObject

        .endif

    .endif

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

    mov eax, status

    ret

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

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

  •  

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

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

  •  

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

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

  •  

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

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

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

  •  

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

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

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

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


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

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

 DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

pDeviceObject 

- указатель на объект "устройство" (структура DEVICE_OBJECT).

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

pIrp 

- указатель на текущий пакет запроса ввода-вывода (структура _IRP).

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

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

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

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

Если же предполагается, при обработке запроса типа 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-функций.

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

В функциях 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 такого макроса, естественно, нет.

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

Напоследок, хочу еще немножко помучить вас теорией ;-)

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

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

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

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

Именно этот вид управления используется в драйвере 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. Поэтому, может обращаться даже к памяти сброшенной в файл подкачки - диспетчер памяти сам решит все вопросы по возвращению ее в физическую память. Правда, временные издержки будут.

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

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

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

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

Я не буду подробно рассматривать этот вид управления, т.к. в контексте данных статей ему нет применения.

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

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

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

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

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

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

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

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

Процедура обработки 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. Трансляция происходит, естественно, не один к одному, т.к. в режимах пользователя и ядра разные наборы кодов ошибок.

    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 и возвращаем системе текущий код состояния.

Процедура трансляции адресов 

Код режима ядра, в отличие от кода режима пользователя, может транслировать виртуальные адреса в физические и наоборот. Для этого необходим доступ к каталогу страниц (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_X86 в \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). Для 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_X86 в \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_X86 в \include\w2k\w2kundoc.inc), выделяем биты 22-31, получая PFN, который, в случае больших страниц, и является физическим адресом страницы.

            and ecx, 00000000001111111111111111111111y

            add eax, ecx

        .endif

    .else

        xor eax, eax

    .endif

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

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

Работа с записями не часто встречается в практике программирования на ассемблере. Если не знакомы с директивой mask, то вместо

mask pde4kValid

mask pde4kLargePage

 mask pteValid

mask ptePageFrameNumber

mask pde4mPageFrameNumber

используйте

01y

010000000y

01y

11111111111111111111000000000000y

11111111110000000000000000000000y


Процедура выгрузки драйвера 

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

    invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

    mov eax, pDriverObject

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

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

Таблица 5-1 подытоживает рассмотреный материал. В ней показано, к вызову каких процедур в драйвере приводит обращение кода режима пользователя к той или иной функции ввода-вывода, с указанием контекста и уровня запроса прерывания.

Таблица 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


Компиляция 

: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)

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

Добавляем ресурсы 

Каждый уважающий своего создателя драйвер должен сообщать его имя, а также кое-какие дополнительные сведения о себе. Это можно сделать используя файл ресурсов (см. 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

Тут ничего принципиально нового. Стандартный ресурсный скрипт. Компилируется и линкуется как обычно. В свойствах файла драйвера можно посмотреть что получилось.

Еще немного об отладке 

Вытащить всю (или почти всю) "подноготную" объектов "драйвер" и "устройство" можно с помощью команд driver <имя драйвера> и device <имя устройства> отладчика SoftICE. Вот как это выглядит на моей машине:

Рис. 5-2. Результат выполнения команды driver VirtToPhys

Рис. 5-3. Результат выполнения команды device devVirtToPhys

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

Заключение 

Вот мы, наконец, и разобрали основы программирования драйверов режима ядра на языке ассемблера. Надеюсь, эти статьи помогли вам легко "въехать в тему". За дополнительной информацией обращайтесь, прежде всего, к DDK, книге Walter Oney " Programming the Microsoft Windows Driver Model" и журналу "The NT Insider" http://www.osr.com/.

В архиве к этой статье вы обнаружите:

\include\

- некоторые обновленные включаемые файлы;

\src\Article4-5\NtBuild

- код драйвера возвращающего номер сборки (build number) и выпуск системы. Демонстрирует использование функции ReadFile, вида управления буферами neither и использование SEH;

\src\Article4-5\Skeleton

- каркас драйвера режима ядра;

\src\Article4-5\VirtToPhys

- код описанного в данной статье драйвера.

Драйверы режима ядра: Часть 6: Базовая техника: Работа с памятью. Использование системных куч

· версия для печати 

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

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

Пользовательским процессам система, точнее диспетчер памяти (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. Поэтому мы обойдемся подкачиваемой памятью.), что-нибудь полезное в нее запишем, освободим и заставим систему выгрузить драйвер.

Для экономии места я привожу только исходный текст процедуры DriverEntry. Это собственно и есть весь драйвер.

;@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 (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. Is it possible?\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 в драйвере. Пользуясь этим нехитрым приемом, можно сэкономить немалое число перезагрузок.

Исходный код драйвера в архиве. Для компиляции требуется последняя версия KmdKit - берите на сайте.

Драйверы режима ядра: Часть 7: Работа с памятью. Использование ассоциативных списков

· версия для печати 

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            &