Драйвер

39
Драйвер-это основа взаимодействия системы с устройством в ОС Windows.Это одновременно удобно и неудобно. Про удобства я разъяснять не буду - это и так понятно, а заострюсь я именно на неудобствах драйверов. В сложившейся ситуации пользователь полностью подчинён воле производителя - выпусти тот драйвер - хорошо, а не выпустит... Только продвинутый пользователь, имеющий голову на плечах (особенно, если он ешё и программер) не станет мириться с таким положением дел - он просто возьмёт и сам напишет нужный драйвер. Это нужно и взломщику: драйвер - это удобное окошко в ring0, которое является раем для хакера. Но хоть написать драйвер и просто, да не совсем - есть масса подводных камней. Да и документированность данного вопроса на русском языке оставляет желать лучшего. Этот цикл статей поможет тебе во всём разобраться. Приступим. Интра Хочу сразу же сделать несколько предупреждений. Данная статья всё-таки подразумевает определённый уровень подготовки. Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок. Поэтому хорошее знание обоих языков весьма желательно (если не сказать - обязательно). Если же ты пока не можешь этим похвастаться, но желание писать драйвера есть - что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы, ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду. Может как-нибудь в другой раз. Согласен? Тогда поехали! Теория

Upload: -

Post on 29-Nov-2014

87 views

Category:

Documents


5 download

TRANSCRIPT

Page 1: Драйвер

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

Интра

Хочу сразу же сделать несколько предупреждений. Данная статья всё-таки подразумевает определённый уровень подготовки. Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок. Поэтому хорошее знание обоих языков весьма желательно (если не сказать - обязательно). Если же ты пока не можешь этим похвастаться, но желание писать драйвера есть - что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы, ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду. Может как-нибудь в другой раз. Согласен? Тогда поехали!

Теория

Скоро здесь, возможно, будет стоять твоё имя.

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

Page 2: Драйвер

сейчас этот процесс до конца не завершён: ты наверняка сталкивался с тем, что во многих дистрибутивах никсов для установки/перестановки etc драйверов нужно перекомпилировать ядро, т.е. фактически заново пересобирать систему. Вот, кстати ещё один близкий моментец: разные принципы работы с драйверами в Windows 9x и NT. В первом процесс установки/переустановки драйверов проходит практически без проблем, во втором же случае это тяжёлое и неблагодарное дело, для "благополучного" завершения которого нередко приходится прибегать к полной переустановке ОС. А зато в Windows 9x... так,стоп,открывается широкая и волнующая тема, которая уведёт меня далеко от темы нынешней статьи, так что вернёмся к нашим баранам... ой,то есть к драйверам. В порядке общего развития интересно сравнить особенности драйверов в Windows и *nix(xBSD) системах: 

1) Способ работы с драйверами как файлами (подробнее см. ниже)2) Драйвер, как легко заменяемая честь ОС (учитывая уже сказанные выше примечания)3) Существование режима ядра

Теперь касательно первого пункта. Это значит, что функции, используемые при взаимодействии с файлами, как и с драйверами, практически идентичные (имеется в виду лексически): open, close, read и т.д. И напоследок стоит отметить идентичность механизма IOCTL (Input/Output Control Code-код управления вводом-выводом) -запросов. 

Драйвера под Windows делятся на два типа: Legacy (устаревший) и WDM (PnP). Legacy драйверы (иначе называемые "драйверы в стиле NT") чрезвычайно криво работают (если работают вообще) под Windows 98, не работают с PnP устройствами, но зато могут пользоваться старыми функциями HalGetBusData, HalGetInterruptVector etc, но при этом не имеют поддержки в лице шинных драйверов. Как видишь, весьма средненький драйвер. То ли дело WDM: главный плюс - поддержка PnP и приличненькая совместимость: Windows 98, Me, 2000, XP, 2003, Server 2003 и т.д. с вариациями; но он тоже вынужден за это расплачиваться: например, он не поддерживает некоторые устаревшие функции (которые всё таки могут быть полезны). В любом случае, не нужно ничего воспринимать как аксиому, везде бывают свои исключения. В некоторых случаях лучше написания Legacy драйвера ничего не придумать. 

Как ты наверняка знаешь, в Windows есть два мода работы: User Mode и Kernel Mode - пользовательский режим и режим ядра соответственно. Первый - непривилегированный, а второй - наоборот. Вот во втором чаще всего и сидят драйвера (тем более, что мы в данный момент говорим именно о драйверах режима ядра). Главные различия между ними:  это доступность всяких привилегированных команд процессора. Программировать (а уж тем более качественно) в Kernel mode посложнее будет, чем писать прикладные незамысловатые проги. А драйвера писать без хорошего знания Kernel mode - никак. Нужно попариться над назначением выполнения разнообразных работ отдельному подходящему уровню IRQL, желательно выучить новое API (так как в Kernel mode API отличается от прикладного)... в общем, предстоит много всяких радостей. Но тем не менее, это очень интересно, познавательно, и даёт тебе совершенно иной уровень власти над компьютером.

А раз уж я упомянула про IRQL, разьясню и это понятие. IRQL (Interrupt Request Level - уровень приоритета выполнения) - это приоритеты, назначаемые специально для кода, работающего в режиме ядра. Самый низкий уровень выполнения - PASSIVE_LEVEl. Работающий поток может быть прерван потоком только с более высоким IRQL. 

Ну и напоследок разъясним ещё несколько терминов: 

1) ISR (Interrupt Service Routine) - процедура обслуживания прерываний. Эта функция вызывается драйвером в тот момент, когда обслуживаемая им аппаратура посылает сигнал прерывания. Делает самые необходимые на первый момент вещи: регистрирует callback - функцию и т.д.

2) DpcForISR (Deferred Procedure Call for ISR) - процедура отложенного вызова для обслуживания прерываний. Эту функцию драйвер регистрирует в момент работы ISR для выполнения основной работы.

3) IRP (Input/Output Request Packet) - пакет запроса на ввод - вывод. Пакет IRP состоит из фиксированной и изменяющейся частей. Вторая носит название стека IRP или стека ввода - вывода (IO stack).

Page 3: Драйвер

4) IO stack location - стек ввода - вывода в пакете IRP.

5) Dispatch Routines (Рабочие процедуры) - эти функции регистрируются в самой первой (по вызову) процедуре драйвера.

6) Major IRP Code - старший код IRP пакета.

7) Minor IRP Code - соответственно, младший код IRP пакета.

8) DriverEntry - эта функция драйвера будет вызвана первой при его загрузке.

9) Layering (Многослойность) - данной возможностью обладают только WDM - драйвера. Она заключается в наличии реализации стекового соединения между драйверами. Что такое стековое соединение? Для этого необходимо знать про Device Stack (стек драйверов) - поэтому я обязательно вспомню про всё это чуточку ниже.

10) Device Stack, Driver Stack (стек устройств, стек драйверов) - всего лишь объемное дерево устройств. Его, кстати, можно рассмотреть во всех подробностях с помощью программы DeviceTree (из MS DDK), например.

11) Стековое соединение - как и обещала, объясняю. В стеке драйверов самый верхний драйвер - подключившийся позднее. Он имеет возможность посылать/переадресовывать IRP запросы другим драйверам, которые находятся ниже его. Воти всё. Правда,просто?

12) AddDevice - функция, которую обязательно должны поддерживать WDM драйверы. Её название говорит само за себя.

13) Device Object, PDO, FDO (Объект устройства, физический, функциональный) - при подключении устройства к шине она создаёт PDO. А уже к PDO будут подключаться FDO объекты WDM драйверов. Обьект FDO создаётся самим драйвером устройства при помощи функции IOCreateDevice. Обьект FDO также может иметь свою символическую ссылку, от которой он будет получать запросы от драйвера. Это что касается WDM драйверов. С драйверами "в стиле NT" ситуация несколько иная. Если он не обслуживает реальных/PnP устройств, то PDO не создаётся. Но для связи с внешним миром без FDO не обойтись. Поэтому он присутствует и тут.

14) Device Extension (Расширение обьекта устройства) - "авторская" структура, т.е. она полностью определяется разработчиком драйвера. Правилом хорошего тона считается, например, размещать в ней глобальные переменные.

15) Monolithic Driver (Монолитный драйвер) - это драйвер, который самостоятельно обрабатывает все поступающие IRP пакеты и сам работает с обслуживаемым им устройством (в стеке драйверов он не состоит). Данный тип драйверов используется только если обслуживается не PnР устройство или же всего лишь требуется окошко в ring0.

16) DIRQL (уровни аппаратных прерываний) - прерывания, поступающие от реальных устройств, имеют наивысший приоритет IRQL, поэтому для них решено было придумать специальное название (Device IRQL).

17) Mini Driver (Мини - драйвер) - чуть меньше "полного" драйвера. Обычно реализуется в виде DLL-ки и имеет оболочку в виде "полного" драйвера.

18) Class Driver (Классовый драйвер) - высокоуровневый драйвер, который предоставляет поддержку класса устройств.

19) РnP Manager (PnP менеджер) - один из главных компонентов операционной системы. Состоит из двух частей: PnP менеджера пользовательского и "ядерного" режимов. Первый в основном взаимодействует с пользователем; когда тому нужно, например, установить новые драйвера и т.д. А второй управляет работой, загрузкой и т.д. драйверов.

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

Page 4: Драйвер

21) Filter Device Object - объект устройства, создаваемый фильтр - драйвером.

22) HAL (Hardware Abstraction Layer) - слой аппаратных абстракций. Данный слой позволяет абстрагироваться компонентам операционной системы от особенностей конкретной платформы.

23) Synchronization Objects (Обьекты синхронизации) - с помощью этих объектов потоки корректируют и синхронизируют свою работу.

24) Device ID - идентификатор устройства.

25) DMA (Direct Memory Access) - метод обмена данными между устройством и памятью (оперативной) в котором центральный процессор не принимает участия.

25) Polling - это особый метод программирования, при котором не устройство посылает сигналы прерывания драйверу, а сам драйвер периодически опрашивает обслуживаемое им устройство.

26) Port Driver (Порт-драйвер) - низкоуровневый драйвер, принимающий системные запросы. Изолирует классовые драйверы устройств от аппаратной специфики последних.

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

Краткий экскурс в архитектуру Windows NT

Наш обзор архитектуры Windows NT мы начнём с разговора об уровнях разграничения привилегий. Я уже упоминала об user и kernel mode. Эти два понятия тесно связаны с так называемыми кольцами (не толкиеновскими ). Их ( колец) в виде всего четыре: Ring3,2,1 и 0. Ring3 - наименее привилегированное кольцо, в котором есть множество ограничений по работе с устройствами, памятью и т.д. Например, в третьем кольце нельзя видеть адресное пространство других приложений без особого на то разрешения. Естественно, трояну вирусу etc эти разрешения получить будет трудновато, так что хакеру в третьем кольце жизни никакой. В третьем кольце находится user mode. Kernel mode сидит в нулевом кольце - наивысшем уровне привилегий. В этом кольце можно всё:  смотреть адресные пространства чужих приложений без каких - либо ограничений и разрешений, по своему усмотрению поступать с любыми сетевыми пакетами, проходящими через машину, на всю жизнь скрыть какой-нибудь свой процесс или файл и т.д. и т.п. Естественно, просто так пролезть в нулевое кольцо не получиться:  для этого тоже нужны дополнительные телодвижения. У легального драйвера с этим проблем нет:  ему дадут все необходимые API - шки, доступ ко всем нужным системным таблицам и проч. Хакерской же нечисти опять приходиться туго:  все необходимые привилегии ему приходиться "выбивать" незаконным путём. Но это уже тема отдельной статьи, и мы к ней как-нибудь ещё вернёмся. А пока продолжим.

У тебя наверняка возник законный вопрос:  а что же сидит в первом и втором кольцах ? В том то всё и дело, что программисты из Microsoft почему - то обошли эти уровни своим вниманием. Пользовательское ПО сидит в user mode,а всё остальное (ядро, драйвера...) - в kernel mode. Почему они так сделали - загадка, но нам это только на руку. А теперь разберёмся с компонентами (или, иначе говоря, слоями ) операционной системы Windows NT.

Page 5: Драйвер

Посмотри на схему - по ней многое можно себе уяснить. Разберём её подробнее. С пользовательским режимом всё понятно. В kernel mode самый низкий уровень аппаратный. Дальше идёт HAL, выше - диспетчер ввода - вывода и драйвера устройств в одной связке, а также ядрышко вместе с исполнительными компонентами. О HAL я уже говорила, поэтому поподробнее поговорим об исполнительных компонентах. Что они дают? Прежде всего они приносят пользу ядру. Как ты уже наверняка уяснил себе по схеме, ядро отделено от исполнительных компонентов. Возникает вопрос:  почему ? Просто на ядре оставили только одну задачу:  просто управление потоками, а все остальные задачи (управление доступом, памятью для процессов и т.д.) берут на себя исполнительные компоненты (еxecutive). Они реализованы по модульной схеме, но несколько компонентов её (схему) не поддерживают . Такая концепция имеет свои преимущества:  таким образом облегчается расширяемость системы. Перечислю наиболее важные исполнительные компоненты: 

1) System Service Interface (Интерфейс системных служб )2) Configuration Manager (Менеджер конфигурирования)3) I/O Manager (Диспетчер ввода-вывода,ДВВ)4) Virtual Memory Manager,VMM (Менеджер виртуальной памяти)5) Local Procedure Call,LPC (Локальный процедурный вызов )6) Process Manager (Диспетчер процессов)7) Object Manager (Менеджер объектов)

Так как эта статья - первая в цикле, обзорная, подробнее на этом пока останавливаться не будем. В процессе практического обучения написанию драйверов, я буду разъяснять все неясные термины и понятия. А пока перейдём к API. 

API (Application Programming Interface) - это интерфейс прикладного программирования. Он позволяет обращаться прикладным программам к системным сервисам через их специальные абстракции. API-интерфейсов несколько, таким образом в Windows-системах присутствуют несколько подсистем. Перечислю: 

1) Подсистема Win32.2) Подсистема VDM (Virtual DOS Machine - виртуальная ДОС - машина)3) Подсистема POSIX (обеспечивает совместимость UNIX - программ)4) Подсистемиа WOW (Windows on Windows). WOW 16 обеспечивает совместимость 32-х разрядной системы с 16-битными приложениями. В 64-х разрядных системах есть подсистема WOW 32, которая обеспечивает аналогичную поддержку 32 - битных приложений.5) Подсистема OS/2. Обеспечивает совместимость с OS/2 приложениями.

Казалось бы, всё вышеперечисленное однозначно говорит в пользу WINDOWS NT систем! Но не всё так хорошо. Основа WINDOWS NT (имеются ввиду 32-х разрядные версии) - подсистема Win32. Приложения, заточенные под одну подсистему не могут вызывать функции другой. Все остальные (не Win32) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность

Page 6: Драйвер

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

Подсистема Win32 отвечает за графический интерфейс пользователя, за обеспечение работоспособности Win32 API и за консольный ввод - вывод. Каждой реализуемой задаче соответствуют и свои функции: функции, отвечающие за графический фейс, за консольный ввод - вывод (GDI - функции) и функции управления потоками, файлами и т.д. Типы драйверов, наличествующие в Windows, я уже упоминала в разделе терминов:  монолитный драйвер, фильтр - драйвер и т.д. А раз так, то пора закругляться. Наш краткий обзор архитектуры Windows NT можно считать завершённым. Этого тебе пока хватит для общего понимания концепций Windows NT, и концепций написания драйверов под эту ось - как следствие.

Инструменты

Описать и/или упомянуть обо всех утилитах, могущих понадобиться при разработке драйверов - немыслимо. Расскажу только об общих направлениях.

Без чего нельзя обойтись ни в коем случае - это Microsoft DDK (Driver Development Kit ). К этому грандиозному пакету прилагается и обширная документация. Её ценность - вопрос спорный. Но в любом случае, хотя бы ознакомиться с первоисточником информации по написанию драйверов для Windows - обязательно. В принципе, можно компилять драйвера и в Visual Studio, но это чревато долгим и нудным копанием в солюшенах и vcproj-ектах, дабы код твоего драйвера нормально откомпилировался. В любом случае, сорцы придётся набивать в визуальной студии, т.к. в DDK не входит нормальная IDE. Есть пакеты разработки драйверов и от третьих фирм: WinDriver или NuMega Driver Studio, например. Но у них есть отличия от майкрософтовского базиса функций (порой довольно большие ) и многие другие мелкие неудобства. Так что DDK - лучший вариант. Если же ты хочешь писать драйвера исключительно на ассемблере, тебе подойдёт KmdKit (KernelMode Driver DevelopmentKit) для MASM32. Правда, этот вариант только для Win2k/XP.

Теперь можно поговорить о сторонних утилитах. Некоторые уже включены в стандартную поставку Windows:  редактор реестра. Но их в любом случае не хватит. Надо будем инсталлить отдельно. Множество наиполезнейших утилит создали патриархи системного кодинга в Windows: Марк Руссинович, Гарри Нэббет, Свен Шрайбер... и т.д. Вот о них и поговорим. Марк Руссинович создал много полезных утилит: RegMon, FileMon (мониторы обращений к реестру и файлам соответственно), WinObj (средство просмотра директорий имен объектов), DebugView,DebugPrint (программы просмотра, сохранения и т.д. отладочных сообщения) и проч. и проч. Все эти утилиты и огромное количество других можно найти на знаменитом сайте Руссиновича http://www.sysinternals.com.

На диске, прилагающемся к знаменитой книге "Недокументированные возможности Windows 2000" Свена Шрайбера, есть замечательные утилиты w2k_svc, -_sym, -_mem, позволяющие просматривать установленные драйвера, приложения и службы, работающие в режиме ядра, делать дамп памяти и т.д. Все эти утилиты, а также другие программы с диска можно скачать с http://www.orgon.com/w2k_internals/cd.html. 

Напоследок нельзя не упомянуть такие хорошие проги, как PE Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.

Вот и пришло время второй статьи из цикла о написании драйверов под Windows. Сейчас я тебя немного расстрою: в предыдущей статье я обещала, что в этой мы приступим собственно к практике. Но данная статья это, скорее, "полупрактика". Глупо "с места в карьер" бросаться разрабатывать драйвера режима ядра (так как, если ты не забыл, в данном цикле мы разбираем именно этот тип драйверов), хотя бы поверхностно не изучив особенности и приёмы программирования в режиме ядра, что мы и сделаем в первой части этой статьи. Ну а во второй мы наконец - то разберём структуру настоящего драйвера под Windows (Legacy и немного WDM) : его основные функции, их взаимодействие, а также параметры, принимаемые ими. Так что к третьей статье этого цикла ты уже, надо думать, будешь основательно подготовлен к написанию своего первого драйвера. Начинаем.

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

Page 7: Драйвер

Программирование в режиме ядра имеет массу особенностей, для прикладников очень непривычных, а для новичков довольно сложных. Во-первых, у режима ядра своё, отличное от такового в пользовательском режиме, API. Кроме того, для кода, выполняющегося в режиме ядра, имеет очень большое значение его уровень IRQL, так как приложениям, выполняющимся на высоких уровнях IRQL, недоступны многие функции, к которым имеют доступ приложения низких IRQL уровней, и наоборот. Всё это необходимо учитывать. Во-вторых, в режиме ядра есть свои дополнительные описатели типов. Полный их список можно найти в заголовочном файле ntdef.h. Его содержание примерно таково:

typedef unsigned char USHARtypedef unsigned short USHORTtypedef unsigned long ULONG............

Зачем это нужно? Ну, во-первых, для красоты... тьфу, для унификации стиля классических C - типов данных и нововведённых - таких, как WCHAR (двухбайтный Unicode символ), LARGE_INTEGER (который, на самом деле, является объединением) и т.д. А также для унификации исходников для 32 - разрядных платформ и надвигающихся 64 - разрядных.

В исходниках драйверов часто встречаются макроопределения IN, OUT, OPTIONAL. Что они означают? А ровным счётом ничего, и введены они только для повышения удобочитаемости исходника. OPTIONAL обозначает необязательные параметры, IN - параметры, передаваемые внутрь функции, например, OUT - соответственно, наоборот. А вот IN OUT означает, что параметр передаётся внутрь функции, а затем возвращается из неё обратно.

Есть изменения и в типах возвращаемых значений функций. Ты наверняка знаешь, что C-ишные функции либо не возвращают значения (void), либо возвращают значение определённого типа (char, int etc). При программировании драйверов ты столкнёшься с ещё одним типом - NT_STATUS. Этот тип включает в себя информацию о коде завершения функции (определение этого типа можно посмотреть в файле ntdef.h). NT_STATUS является переопределённым типом long integer. Неотрицательные значения переменных этого типа соответствуют успешному завершению функции, отрицательные - наоборот (файл NTSTATUS содержит символьные обозначения всех кодов возврата). Сообщение об удачном завершении имеет код 0 и символьное обозначение STATUS_SUCCESS. Остальные коды возврата, соответствующие разнообразным вариантам ошибок, транслируются в системные коды ошибок и передаются вызывающей программе. Для работы с типом NT_STATUS существует несколько макроопредений (описанные в файле ntdef.h), например NT_SUCCESS(), проверяющий код возврата на успешность завершения.

Функции драйвера, за исключением DriverEntry (главная процедура драйвера, подробнее см. во второй части статьи), могут называться как угодно, тем не менее, существуют определённые "правила хорошего тона" при разработке драйверов, в том числе и для именования процедур: например, все функции, относящиеся к HAL, желательно предварять префиксом HAL и т.д. Сама Microsoft практически постоянно следует этому правилу. А имена типов данных и макроопределения в листингах DDK написаны сплошь заглавными буквами. Советую тебе поступать также при разработке своих драйверов. Это и в самом деле во много раз повышает удобство работы с листингом.

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

Начнём с функций для работы с памятью, а для начала поговорим собственно об устройстве и работе с памятью в Windows. Единое 4-х гигабайтное адресное пространство памяти Windows (я имею в виду 32-х разрядные версии Windows) делится на две части: 2 гигабайта для пользовательского пространства и 2 гигабайта для системного. 2 гигабайта системного пространства доступны для всех потоков режима ядра. Системное адресное пространство делится на следующие части:

Page 8: Драйвер

Видов адресов в режиме ядра три: физические (реально указывающие на область физической памяти), виртуальные (которые перед использованием транслируются в физические), и логические (используемые HAL уровнем при общении с устройствами; он же и отвечает за работу с такими адресами). Функции режима ядра, отвечающие за выделение и освобождение виртуальной памяти, отличаются от таковых в пользовательском режиме. Также, находясь на уровне режима ядра, становится возможным использовать функции выделения и освобождения физически непрерывной памяти. Разберём все эти функции поподробнее.

1) PVOID ExAllocatePool (уровень IRQL, на котором может выполняться эта функция - < DISPATCH_LEVEL) - выделяет область памяти. Принимает два параметра: параметр (POOL_TYPE) , в котором содержится значение, означающее, какого типа область памяти нужно выделить: PagedPool - страничная, NonPagedPool - нестраничная (в этом случае функцию можно вызвать с любого IRQL уровня). Второй параметр (ULONG) - размер запрашиваемой области памяти. Функция возвращает указатель на выделенную область памяти, и NULL, если выделить память не удалось.

2) VOID ExFreePool (IRQL<DISPATCH_LEVEL) - освобождает область памяти. Принимает параметр (PVOID) - указатель на освобождаемую область памяти. Если высвобождается нестраничная память, то данная функция может быть вызвана с DISPATCH_LEVEL. Возвращаемое значение - void.

3) PVOID MmAllocateContiguousMemory (IRQL==PASSIVE_LEVEL) - выделяет физически непрерывную область памяти. Принимает два параметра. Первый параметр (ULONG) - размер запрашиваемой области памяти, второй - параметр (PHYSICAL_ADDRESS), означающий верхний предел адресов для запрашиваемой области. Возвращаемое значение: виртуальный адрес выделенной области памяти или NULL (при неудаче).

4) VOID MmFreeContiguousMemory (IRQL==PASSIVE_LEVEL) - освобождает физически непрерывную область памяти. Принимает единственный параметр (PVOID) - указатель на область памяти, выделенную ранее с использованием функции MmAllocateContiguousMemory. Возвращаемое значение - void.

5) BOOLEAN MmIsAddressValid (IRQL<=DISPATCH_LEVEL) - делает проверку виртуального адреса. Принимает параметр (PVOID) - виртуальный адрес, нуждающийся в проверке. Функция возвращает TRUE, если адрес "валидный" (т.е. присутствует в виртуальной памяти), и FALSE - в противном случае.

Page 9: Драйвер

6) PHYSICAL_ADDRESS MmGetPhysicalAddress (IRQL - любой) - определяет физический адрес по виртуальному. Принимает параметр (PVOID), содержащий анализируемый виртуальный адрес. Возвращаемое значение - полученный физический адрес.

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

Драйверные функции, предоставляемые диспетчером ввода - вывода. 

1) IoRegisterDeviceInterface - данная функция регистрирует интерфейс устройства. Диспетчер ввода - вывода создаёт подразделы реестра для всех зарегистрированных интерфейсов. После этого можно создавать и хранить в этом подразделе нужные драйверу параметры с помощью вызова функции IoOpenDeviceInterfaceRegistryKey, которая возвращает дескриптор доступа к подразделу реестра для зарегистрированного интерфейса устройства.

2) IoGetDeviceProperty - данная функция запрашивает из реестра установочную информацию об устройстве.

3) IoOpenDeviceRegistryKey - возвращает дескриптор доступа к подразделу реестра для драйвера или устройства по указателю на его объект.

4) IoSetDeviceInterfaceState - с помощью данной функции можно разрешить или запретить доступ к зарегистрированному интерфейсу устройства. Компоненты системы могут получать доступ только к разрешённым интерфейсам.

Драйверные функции для прямого доступа к реестру.

1) RtlCheckRegistryKey - проверяет, существует ли указанный подраздел внутри подраздела, переданного первым параметром. Что и каким образом передавать в первом параметре - в рамках статьи всё не перечислить, отсылаю к ntddk.h и wdm.h. Если существует - возвращается STATUS_SUCCESS.

2) RtlCreateRegistryKey - создаёт подраздел внутри раздела реестра, указанного вторым параметром. Далее - всё то же самое, что и у RtlCheckRegistryKey.

3) RtlWriteRegistryValue - записывает значение параметра реестра. Первый параметр - куда пишем, второй - в какой подраздел (если его нет, то он будет создан), а третий - какой параметр создаём.

4) RtlDeleteRegistryValue - удаляет параметр из подраздела. Параметры те же самые, что и у RtlWriteRegistryValue (только с необходимыми поправками, конечно).

5) RtlQueryRegistryValues - данная функция позволяет за один вызов получить значения сразу нескольких параметров указанного подраздела.

И напоследок функции для работы с реестром семейства Zw~.

1) ZwCreateKey - открывает доступ к подразделу реестра. Если такового нет - создаёт новый. Возвращает дескриптор открытого объекта.

2) ZwOpenKey - открывает доступ к существующему подразделу реестра.

3) ZwQueryKey - возвращает информацию о подразделе.

4) ZwEnumerateKey - возвращает информацию о вложенных подразделах уже открытого ранее подраздела.

Page 10: Драйвер

5) ZwEnumerateValueKey - возвращает информацию о параметрах и их значениях открытого ранее подраздела.

6) ZwQueryValueKey - возвращает информацию о значении параметра в открытом ранее разделе реестра. Полнота возвращаемой информации определяется третьим параметром, передаваемым функции, который может принимать следующие значения (дополнительные разъяснения не требуются, так как они имеют "говорящие" имена): KeyValueBasicInformation, KeyValuePartialInformation и KeyValueFullInformation.

7) ZwSetValueKey - создаёт или изменяет значение параметра в открытом ранее подразделе реестра.

8) ZwFlushKey - принудительно сохраняет на диск изменения, сделанные в открытых функциями ZwCreateKey и ZwSetValueKey подразделах.

9) ZwDeleteKey - удаляет открытый подраздел из реестра.

10) ZwClose - закрывает дескриптор открытого ранее подраздела реестра, предварительно сохранив сделанные изменения на диске.

Практически все вышеперечисленные функции для работы с реестром должны вызываться с уровня IRQL PASSIVE_LEVEL.

Думаю, пока достаточно. Конечно, у всех вышеперечисленных функций есть масса нюансов в применении. Да и вообще функций режима ядра - великое множество, их ничуть не меньше, чем в пользовательском режиме. Но моя задача была не рассказать обо всех API - функциях режима ядра (что даже в рамках цикла невозможно сделать), а продемонстрировать отличия функций режима ядра, от таковых в пользовательском режиме, и хоть немного рассказать о нюансах их применения (взять, к примеру, то, что в пользовательском режиме не имеет значения, в потоке какого приоритета будет выполняться приложение: оно будет иметь такой же полный доступ ко всем API функциям пользовательского режима, как и любые другие приложения; на уровне ядра, как ты только что, убедился, это не так). Ну а за более или менее полным списком и описанием всех этих API - функций советую обратиться к библии Гарри Нэббета. Ну вот и всё, теперь ты готов к разговору о структуре драйвера, который мы сейчас и начнём. 

Структура драйвера

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

Входная точка любого драйвера - функция DriverEntry (по поводу названий вообще всех функций - смотри соглашение в первой части статьи), которая фактически играет ту же самую роль для драйвера, что и main для проги на C. Эта функция вызывается при загрузке драйвера (неважно, загружается ли он динамически или при запуске системы). Данная функция выполняет некоторые действия, нужные для нормальной работы драйвера (например, регистрирует в специальном массиве адреса всех остальных функций драйвера, чтобы диспетчер ввода - вывода мог вызывать их по этим адресам). Если это не WDM драйвер, то в этой функции происходит локализация обслуживаемого оборудования, выделение и/или подтверждение используемых аппаратных ресурсов, выдача видимых для системы имён всем найденным обслуживаемым устройствам и т.д. WDM драйвера эту работу перекладывают на функцию AddDevice. Функция DriverEntry может вызываться с уровня IRQL == PASSIVE_LEVEL. Функция возвращает значение типа NTSTATUS, и принимает два параметра: адрес объекта драйвера (PDRIVER_OBJECT) и путь в реестре к подразделу драйвера (PUNICODE_STRING). Получив от диспетчера ввода - вывода указатель на структуру DRIVER_OBJECT драйвер должен заполнить в ней некоторые поля:

1) Поле DriverUnload - для регистрации собственной функции Unload, вызываемой при выгрузке драйвера. Подробнее о ней. Эта функция вызывается только при динамической выгрузке драйвера (т.е. происшедшей не в результате завершения работы системы). Legacy драйвера в этой функции

Page 11: Драйвер

выполняют полное освобождение всех занятых драйвером системных ресурсов. WDM драйвера выполняют такое освобождение в функции RemoveDevice при удалении каждого устройства (если драйвер обслуживает несколько устройств). Функция Unload вызывается с уровня IRQL PASSIVE_LEVEL, принимает единственный параметр (PDRIVER_OBJECT) - указатель на объект драйвера, и возвращает void.

2) Поле DriverStartIo - для регистрации собственной функции StartIo. Вкратце, регистрация функции StartIo нужна для участия в System Queuing - создании очередей необработанных запросов системными средствами, в отличие от DriverQueuing - когда то же самое реализуется средствами самого драйвера.

3) Поле AddDevice в подструктуре DriverExtension - для регистрации WDM драйвером своей процедуры AddDevice.

4) Поле MajorFunction - для регистрации драйвером точек входа в свои рабочие процедуры.

Бывают ситуации, когда при первоначальной загрузке драйвер не может до конца окончить процедуру инициализации (например, если необходимы какие-либо системные объекты или другие драйвера, ещё не загруженные). В этом случае драйвер регистрирует свою процедуру для завершения инициализации позднее. Регистрация этой процедуры выполняется вызовом IoRegisterDriverReinitialization с уровня IRQL PASSIVE_LEVEL, принимающей следующие параметры: указатель на объект драйвера (PDRIVER_OBJECT), указатель на процедуру реинициализации, предоставляемую драйвером (PDRIVER_REINITIALIZE) и контекстный указатель, получаемый регистрируемой функцией при вызове, и возвращающей void.

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

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

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

Все драйверы должны иметь обработчик CreateDispatch, обрабатывающий пользовательский запрос CreateFile. Если драйверу нужно обрабатывать пользовательский запрос CloseHandle, то он должен иметь обработчик CloseDispatch.

Перейдём к процедурам передачи данных. Это обработчики пользовательских запросов ReadFile, WriteFile и DeviceIoControl.

Процедуру StartIo я уже рассмотрела, поэтому перейдём к процедуре обслуживания прерываний (ISR - Interrupt Service Routine, напоминаю на всякий случай). Данная процедура вызвается диспетчером прерываний ядра (Kernel`s Interrupt Dispatcher) при каждой генерации прерывания устройством, и она обязана полностью обслужить это прерывание.

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

Page 12: Драйвер

оставляет свой запрос в очереди запросов. Если драйвер предварительно зарегистрировал особую callback функцию, то диспетчер ввода - вывода при освобождении требуемого ресурса, уведомит об этом драйвер, вызвав callback - функцию. Таким образом, обеспечивается гарантия ответа на любой запрос к ресурсу, даже если он (ответ) будет состоять только в том, чтобы уведомить о задержке в обработке и помещении запроса в очередь. Функции, это реализующие: IoAllocateController (использующаяся для синхронизации доступа к контроллеру), AdapterControl (использующаяся для синхронизации доступа к DMA каналам (чаще всего)) и SynchCritSection (использующаяся для корректного обращения к ресурсам; точнее - эта функция позволяет коду с низким уровнем IRQL сделать работу при уровне DIRQL устройства без опасения возникновения конфликтов с ISR). 

Также можно упомянуть ещё таймерные процедуры (нужные для драйверов, выполняющих точный отсчёт временных интервалов; обычно реализуется с использованием IoTimer (но не всегда)), процедуру IoCompletion (позволяющую WDM драйверу, работающему внутри многослойной драйверной структуры, получать уведомление о завершении обработки IRP запроса, направленного к драйверу нижнего уровня) и CancelRoutine (если драйвер зарегистрирует эту callback процедуру при помощи вызова IoSetCancelRoutine, то диспетчер ввода - вывода сможет уведомить его об удалении IRP запросов, находящихся в ожидании обработки, что может понадобиться диспетчеру, если пользовательское приложение, инициировавшее эти IRP запросы, неожиданно завершит свою работу после снятия задачи диспетчером задач (прошу прощения за необходимую тавтологию)).

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

Вот и пришло время третьей статьи в цикле о написании драйверов режима ядра под Windows (и не последней - решено включить в цикл ещё одну, четвёртую, статью). Мы разобрали особенности архитектуры Windows NT, поговорили об особенностях драйвера, как понятия, и об его структуре и познакомились с некоторыми приёмами программирования в режиме ядра. А теперь мы, наконец, вплотную подошли к, собственно, написанию своего первого (или тридцать первого) драйвера. В данной статье мы это и осуществим. Мы напишем простейший legacy - драйвер ("драйвер в стиле NT"), скомпилируем и установим его. И в результате, в виндошном диспетчере устройств наконец - то появится "устройство", драйвер к которому будет написан тобой. Приступим к воплощению этой мечты в реальность! 

Пишем код драйвера

Процесс работы над нашим драйвером, мы начнем, естественно, с написания кода (писать будем, как я уже говорила, на сях). Весь код нашего драйвера (который мы назовём "Primer") будет находиться в двух файлах: Driver.h и main.cpp. Я буду по порядку объяснять куски кода, поэтому, если тебе захочется скомпилировать этот драйвер, достаточно будет просто скопировать их (куски кода) в один файл. Начнём с Driver.h.

#ifndef _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_ // эти строки запрещают повторный проход по тексту заголовочных #define _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_ // (*.h) файлов, что особенно актуально для больших проектов // и повышает скорость компиляции#ifdef __cplusplusextern "C"{#endif

#include "ntddk.h" // если заменить эту строку на #include "wdm.h", то компиляция драйвера (с использованием Build из DDK)// пройдёт успешно, но WDM он всё равно не станет 

#ifdef __cplusplus}

Page 13: Драйвер

#endif

// Далее - структура расширения устройства 

typedef struct _PRIMER_DEVICE_EXTENSION{PDEVICE_OBJECT fdo; // указатель на FDO.UNICODE_STRING ustrSymLinkName; // L"\\DosDevices\\Primer" - имя символьной ссылки} PRIMER_DEVICE_EXTENSION, *PPRIMER_DEVICE_EXTENSION;

// С помощью этих, определённых нами, кодов IOCTL, в будущем можно будет обращаться к драйверу через DeviceIoControl

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

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

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

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

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

#endif

Подробное описание макроса CTL_CODE можно найти в заголовочном DDK файле Winioctl.h. А мы перейдём к main.cpp. В прошлой статье я уже говорила о главных процедурах драйвера. В нашем драйвере это будут DriverEntry - главная точка входа драйвера, UnloadRoutine - процедура выгрузки драйвера и DeviceControlRoutine - обработчик DeviceIoControl IRP пакетов. Но перед тем, как разобраться с реализацией этих процедур, необходимо сделать некоторые предварительные объявления, с которых и начнётся наш файл main.cpp.

#include "Driver.h"

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

VOID UnloadRoutine(IN PDRIVER_OBJECT DriverObject);

NTSTATUSReadWrite_IRPhandler(IN PDEVICE_OBJECT fdo, IN PIRP Irp);

NTSTATUSCreate_File_IRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);

NTSTATUSClose_HandleIRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);

KSPIN_LOCK SpinLock;#pragma code_seg("INIT") // положим начало секции INIT

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

extern "C"NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) 

Page 14: Драйвер

{NTSTATUS status = STATUS_SUCCESS;PDEVICE_OBJECT fdo; // указатель на объект драйвераUNICODE_STRING devName; // указатель на раздел реестра// Так как функция - NTSTATUS, то и возвращает она - STATUS_XXX.

// Все строки, обрамлённые if`ами DBG - будут исполняться только в отладочной версии драйвера.#if DBGDbgPrint("=Primer= DriverEntry.");DbgPrint("=Primer= RegistryPath = %ws.", RegistryPath->Buffer);#endif

// Экспортируем точки входа в драйвер. Поскольку наш драйвер - legacy, процедуру AddDevice мы не экспортируем.DriverObject->DriverUnload = UnloadRoutine;DriverObject->MajorFunction[IRP_MJ_CREATE]= Create_File_IRPprocessing;DriverObject->MajorFunction[IRP_MJ_CLOSE] = Close_HandleIRPprocessing;DriverObject->MajorFunction[IRP_MJ_READ] = ReadWrite_IRPhandler;DriverObject->MajorFunction[IRP_MJ_WRITE] = ReadWrite_IRPhandler;DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=DeviceControlRoutine;

// Начнём создавать символьную ссылкуRtlInitUnicodeString(&devName, L"\\Device\\PRIMER"); // данная процедура тоже должна бы располагаться в AddDevice.

// Создаём свой FDO и получаем указатель на него в fdo. Размер структуры PRIMER_DEVICE_EXTENSION передаётся для // того, чтобы при создании FDO выделить под неё память.status = IoCreateDevice(DriverObject,sizeof(PRIMER_DEVICE_EXTENSION),&devName, FILE_DEVICE_UNKNOWN,0,FALSE, &fdo);if(!NT_SUCCESS(status)) return status; // данная процедура также должна бы располагаться в AddDevice.

// Получаем указатель на область, предназначенную для PRIMER_DEVICE_EXTENSION. PPRIMER_DEVICE_EXTENSION dx = (PPRIMER_DEVICE_EXTENSION)fdo->DeviceExtension;

dx->fdo = fdo; // сохраняем обратный указатель

#if DBGDbgPrint("=PRIMER= FDO %X, DevExt=%X.",fdo,dx);#endif

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

UNICODE_STRING symLinkName; 

#define SYM_LINK_NAME L"\\DosDevices\\Primer" // именно ТАКОЙ код предназначен для того, чтобы символьная ссылка // работала и в Windows 9x, и в NT.RtlInitUnicodeString(&symLinkName, SYM_LINK_NAME);dx->ustrSymLinkName = symLinkName;

// И, наконец, создаем собственно символьную ссылку.status = IoCreateSymbolicLink(&symLinkName, &devName);if (!NT_SUCCESS(status)){IoDeleteDevice(fdo);return status;} 

// Инициализируем объект спин - блокировки, который мы будем использовать для обрушения

Page 15: Драйвер

системы во время // выполнения кода обработчика IOCTL - запросов.KeInitializeSpinLock(&SpinLock);

#if DBGDbgPrint("=PRIMER= DriverEntry successfully completed.");#endifreturn status;}#pragma code_seg() // конец секции INIT

Процедура CompleteIrp, первый аргумент которой - указатель на объект нашего FDO, предназначена для завершения обработки IRP пакетов с кодом завершения status. Эту функцию не нужно нигде регистрировать, так как она предназначена для внутренних потребностей драйвера. 

NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS status, ULONG info){Irp->IoStatus.Status = status;Irp->IoStatus.Information = info; // если этот параметр не равен нулю, то он обычно содержит количество байт, // переданных клиенту.IoCompleteRequest(Irp,IO_NO_INCREMENT);return status;}

Функция ReadWrite_IRPhandler - это рабочая процедура обработки read/write запросов. Если подробнее - она выполняет обработку запросов Диспетчера ввода/вывода, сформированных им в виде IRP пакетов (с кодами IRP_MJ_READ/WRITE) в результате обращений к драйверу из пользовательских приложений с вызовами read/write (или же из кода режима ядра с вызовами ZwRead/WriteFile). В нашем случае функция ReadWrite_IRPhandler ничего особенного не делает, поэтому она реализована в виде заглушки. Определила же я эту процедуру для демонстрации её использования (что позднее ты сможешь применить в разработке своих, уже намного более продвинутых драйверов).

NTSTATUS ReadWrite_IRPhandler(IN PDEVICE_OBJECT fdo, // указатель на объект нашего FDOIN PIRP Irp) // указатель на структуру принятого от Диспетчера ввода/вывода IRP{ULONG BytesTxd = 0;NTSTATUS status = STATUS_SUCCESS;

#if DBGDbgPrint("-Primer- in ReadWrite_IRPhandler.");#endifreturn CompleteIrp(Irp,status,BytesTxd); // CompleteIrp завершает обработку IRP. BytesTxd (число переданных или // полученных байт) - равно нулю. }

Теперь - две связанных функции: Create_File_IRPprocessing и Close_File_IRPprocessing, предназначенные для обработки запросов открытия/закрытия драйвера (CreateFile/CloseHandle, ZwCreateFile/ZwClose). Create_File_IRPprocessing обрабатывает IRP_MJ_CREATE, а Close_File_IRPprocessing - IRP_MJ_CLOSE.

NTSTATUS Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo,IN PIRP Irp) // параметры такие же, что и у ReadWrite_IRPhandler{PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);

#if DBGDbgPrint("-Primer- Create File is %ws",&(IrpStack->FileObject->FileName.Buffer));#endif

return CompleteIrp(Irp,STATUS_SUCCESS,0);}

Page 16: Драйвер

NTSTATUS Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo,IN PIRP Irp) // параметры те же, что и у ReadWrite_IRPhandler и // Create_File_IRPprocessing {#if DBGDbgPrint("-Primer- In Close handler."); #endifreturn CompleteIrp(Irp,STATUS_SUCCESS,0);}

Теперь - огромная рабочая процедура DeviceControlRoutine, предназначенная для обработки IOCTL - запросов (точнее - для обработки IRP_MJ_DEVICE_CONTROL - запросов, возникающих в результате обращения пользовательских приложений к драйверу с вызовом DeviceIoControl). В нашем драйвере эта функция реализует обработку нескольких IOCTL запросов. Все необходимые комментарии я буду давать в коде (определения возможно незнакомых тебе типов данных (UCHAR, PUCHAR etc) можно посмотреть в Windef.h). 

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

// Получаем указатель на расширение устройстваPPRIMER_DEVICE_EXTENSION dx = (PPRIMER_DEVICE_EXTENSION)fdo->DeviceExtension;

ULONG ControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;ULONG method = ControlCode & 0x03;

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

#if DBGDbgPrint("-Primer- In DeviceControlRoutine (fdo= %X)\n",fdo);DbgPrint("-Primer- DeviceIoControl: IOCTL %x.", ControlCode);if(currentIrql==PASSIVE_LEVEL)DbgPrint("-Primer- PASSIVE_LEVEL (val=%d)",currentIrql);#endif

KeAcquireSpinLock(&SpinLock,&irql);

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

#ifndef SMALL_VERSIONcase IOCTL_PRINT_DEBUG_MESS: // выводит мессагу в дебаговую консоль{ #if DBGDbgPrint("-Primer- IOCTL_PRINT_DEBUG_MESS.");#endifbreak;}case IOCTL_CHANGE_IRQL: // играемся с уровнями IRQL {#if DBGDbgPrint("-Primer- IOCTL_CHANGE_IRQL.");KIRQL dl = DISPATCH_LEVEL, oldIrql,newIrql=25; 

Page 17: Драйвер

KeRaiseIrql(newIrql,&oldIrql);newIrql=KeGetCurrentIrql(); 

DbgPrint("-Primer- DISPATCH_LEVEL value =%d",dl);DbgPrint("-Primer- IRQLs are old=%d new=%d", oldIrql,newIrql);KeLowerIrql(oldIrql); #endifbreak;}#endif 

case IOCTL_MAKE_SYSTEM_CRASH: // "роняем" систему (падает только NT){int errDetected=0;char x = (char)0xFF;

#if DBG :DbgPrint("-Primer- IOCTL_MAKE_SYSTEM_CRASH.");#endif__try { x = *(char*)0x0L;}__except(EXCEPTION_EXECUTE_HANDLER){ // Перехват исключения не срабатывает

errDetected=1;};#if DBGDbgPrint("-Primer- Value of x is %X.",x);if(errDetected)DbgPrint("-Primer- Except detected in Primer driver.");#endifbreak;}

#ifndef SMALL_VERSIONcase IOCTL_TOUCH_PORT_378H: // пробуем поработать с аппаратными ресурсами системы{ unsigned short ECRegister = 0x378+0x402;#if DBGDbgPrint("-Primer- IOCTL_TOUCH_PORT_378H.");#endif_asm {mov dx,ECRegister ;xor al,al ;out dx,al ; Установить EPP mode 000mov al,095h ; Биты 7:5 = 100out dx,al ; Установить EPP mode 100}break;}

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

Page 18: Драйвер

if(OutputLength<1){status = STATUS_INVALID_PARAMETER;break;}UCHAR *buff; // if(method==METHOD_BUFFERED){buff = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;#if DBGDbgPrint("-Primer- Method : BUFFERED.");#endif}elseif (method==METHOD_NEITHER){buff=(unsigned char*)Irp->UserBuffer;#if DBGDbgPrint("-Primer- Method : NEITHER.");#endif}else {#if DBGDbgPrint("-Primer- Method : unsupported.");#endifstatus = STATUS_INVALID_DEVICE_REQUEST;break;}#if DBGDbgPrint("-Primer- Buffer address is %08X",buff);#endif*buff=33; BytesTxd = 1; break;}#endif 

default: status = STATUS_INVALID_DEVICE_REQUEST;}

KeReleaseSpinLock(&SpinLock,irql);

#if DBGDbgPrint("-Primer- DeviceIoControl: %d bytes written.", (int)BytesTxd);#endif

return CompleteIrp(Irp,status,BytesTxd); }

Небольшая Эрих Мария Ремарка: почему в обработке IOCTL_MAKE_SYSTEM_CRASH не происходит перехвата исключения? Да потому, что вызов KeAcquireSpinLock меняет уровень IRQL на 2, а ведь данный обработчик IOCTL был вызван драйвером с уровня IRQL == 0 (PASSIVE_LEVEL)! Таким образом, конструкция try/exception, корректно работающая на нулевом IRQL уровне, на IRQL == 2 исключение не перехватывает, и, следовательно, не обрабатывает. Так что во всём виноваты спин - блокировки! Также стоит обратить пристальное внимание на процедуру обработки IOCTL_TOUCH_PORT_378H, так как в момент нашего обращения к порту может случиться так, что с ним уже будут работать другие драйвера или устройства. 

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

Page 19: Драйвер

#pragma code_seg("PAGE") // начинает секцию PAGE

VOID UnloadRoutine(IN PDRIVER_OBJECT pDriverObject){PDEVICE_OBJECT pNextDevObj;int i;

#if DBGDbgPrint("-Primer- In Unload Routine.");#endif// Нижеприведённые операции в полноценном WDM драйвере следут поместить в обработчик IRP_MJ_PNP - запросов (с // субкодом IRP_MN_REMOVE_DEVICE, естественно).

pNextDevObj = pDriverObject->DeviceObject;

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

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

IoDeleteSymbolicLink(pLinkName);IoDeleteDevice(dx->fdo);}}#pragma code_seg() // завершаем секцию PAGE

Вот и всё! (правда, несложно?) Драйвер готов! Точнее, готов только его исходный код: его ещё нужно откомпилировать, установить в систему и запустить. Вот об этом и поговорим.

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

Скомпилировать драйвер можно двумя способами: в Visual Studio и в DDK. Первый способ хорош тем, что в Visual Studio можно набрать (при этом, как обычно, будет производиться автоматическая проверка синтаксиса кода) и скомпилировать там же код. Но для того, что бы происходила проверка и корректная компиляция кода необходимо, как я уже говорила, исправить солюшены (.sln - файлы проекта), что достаточно лениво (тем не менее, такой способ мы рассмотрим в следующей статье цикла). Способ же с использованием DDK более прост и надёжен, поэтому на данный момент разберём второй вариант. Для компиляции и сборки драйвера в DDK (с использованием Build) необходимо создать два файла: Makefile и source. Первый управляет работой Build и в нашем случае имеет следующий стандартный вид :

# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source# file to this component. This file merely indirects to the real make file# that is shared by all the driver components of the Windows NT DDK#!INCLUDE $ (NTMAKEENV) \ makefile.def#

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

TARGETNAME=PRIMER // имя компилируемого драйвераTARGETTYPE=DRIVER // тип компилируемого проекта

Page 20: Драйвер

#DRIVERTYPE=WDM // При компиляции WDM драйвера эту строку нужно раскомментировать, а в заголовочном файле Driver.h вместо // ntddk.h подключить wdm.hTARGETPATH=obj // директория, в которой будут размещены промежуточные файлыSOURCES=main.cpp // главный файл сорцов

Теперь все необходимые для компиляции файлы (в нашем случае - main.cpp, Makefile,sources) осталось только поместить в один каталог и запустить компиляцию отладочной (checked) версии драйвера с помощью утилиты Build. Всё, компиляция и сборка драйвера завершены. Перейдём к инсталляции.

Инсталляция драйвера

Инсталлировать драйвер можно несколькими способами: с внесением записей в реестр, с использованием программы Monitor из пакета Driver Studio, с использованием INF - файла и с использованием SCM - менеджера (программно) (к слову сказать, не всегда есть такое богатство выбора - WDM - драйвера, например, рекомендуется инсталлить только с помощью INF - файла и Мастера установки оборудования). Последние два способа мы рассмотрим в следующей статье цикла, а первые два - сейчас.

Наш драйвер без проблем инсталлируется и работает как под Windows 9x, так и под NT (секрет этого заключается в Windows 9x драйвере ntkern.vxd, который помогает NT - драйверам "почувствовать себя, как дома"; но, естественно, возможности его не безграничны), но процесс записи в реестр (и записываемые значения) немного отличаются. Разберём оба варианта.

Открывай нотпэд, набивай в нём следующие строки и сохраняй документ под любым именем в виде .reg файла:

# содержимое файла реестра, необходимого для инсталляции драйвера, под Windows 9x:

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

Название параметров говорят сами за себя, так что, думаю, дополнительных пояснений не требуется. Для инсталляции драйвера в Windows NT необходимо практически то же самое. Топаешь по тому же пути, который был указан в вышеприведённом .reg файле (не важно, вручную, или же создавая .reg - файл), создаёшь тот же раздел и те же параметры со значениями 1,1 и 2 соответственно. Понятно, что перед внесением изменений в реестр готовый драйвер нужно положить в директорию, указанную в параметре ImagePath.

Программа Monitor из пакета DriverStudio позволяет загрузить, запустить, остановить и удалить драйвер и имеет интуитивно понятный графический фейс, работе с которым, я думаю, обучать не нужно. Перед запуском драйвера из Monitor`а можно предварительно запустить прогу DebugView - тогда все отладочные сообщения драйвера будут выдаваться в её окно.

Вот и всё. Ребуться и наслаждайся результатом! (Проверить корректность установки и интеграции в ОС нового драйвера можно с помощью DeviceTree, например).

Цикл подошел к концу. В этой статье я расскажу об еще нескольких способах инсталляции драйверов (в прошлый раз мы рассмотрели инсталляцию с помощью простого изменения реесстра (вручную или с использованием .reg - файла) и с использованием программы Monitor из состава Driver Studio) - с помощью inf - файла и с использованием возможностей SCM - менеджера. А последний пункт вплотную подводит нас к написанию собственной консольной проги для тестирования возможностей нашего драйвера. 

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

Page 21: Драйвер

Инсталляция драйвера (продолжение)

В данной статье мы рассмотрим еще два способа инсталляции драйвера: с помощью inf - файла и с использованием возможностей SCM - менеджера. Начнем с первого.

Для того, чтобы мы могли корректно установить наш драйвер с использованием виндошного Мастера Установка, нам необходимо создать inf - файл с правильной структурой. Пример оного вместе с комментариями читай ниже:

; Primer.Inf[Version]Signature="$Hacker$"Class=UnknownProvider=%HackerMagazine%DriverVer=27/10/2005,0.0.0.1

[Manufacturer]%HackerMagazine%=Hacker.Magazine

[Hacker.Magazine]%Primer%=Primer.Install, *hackermagazine\Primer

[DestinationDirs]Primer.Files.Driver=10,System32\Drivers ; путь для копирования драйвера под Windows 98Primer.Files.Driver.NTx86=10,System32\Drivers ; путь для копирования драйвера под Windows NT

[SourceDisksNames]1="Primer build directory",,, 

[SourceDisksFiles]Primer.sys=1,drv\w98 ; путь, где находится версия драйвера под Windows 98

[SourceDisksFiles.x86]Primer.sys=1,drv\nt ; путь, где находится версия драйвера под Windows NT; в нашем случае обе версии драйверов, конечно, одинаковые

; Windows 98; секция, используемая в случае установки драйвера под Windows 98[Primer.Install]CopyFiles=Primer.Files.DriverAddReg=Primer.AddReg

[Primer.AddReg]HKR,,DevLoader,,*ntkernHKR,,NTMPDriver,,Primer.sys[Primer.Files.Driver]Primer.sys

; Windows 2000, XP, Server 2003; секция, используемая в случае установки драйвера под Windows NT[Primer.Install.NTx86]CopyFiles=Primer.Files.Driver.NTx86

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

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

[Primer.Service]DisplayName = %Primer.ServiceName%ServiceType = %SERVICE_KERNEL_DRIVER%StartType = %SERVICE_AUTO_START%

Page 22: Драйвер

ErrorControl = %SERVICE_ERROR_NORMAL%ServiceBinary = %10%\System32\Drivers\Primer.sys

; Строки[Strings]HackerMagazine="Napisanie draiverov v podrobnostjah №4"Primer="Primer driver: checked build"Primer.ServiceName="Primer NTDDK driver (V.001)"

SPSVCINST_ASSOCSERVICE=0x00000002COPYFLG_NOSKIP=2 SERVICE_KERNEL_DRIVER=1SERVICE_AUTO_START=2SERVICE_DEMAND_START=3 SERVICE_ERROR_NORMAL=1

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

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

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

Написание консольного приложения для тестирования драйвера

Во-первых, нужно создать файл Ioctl.h. Его содержимое (которое, надо думать, тебе знакомо по файлу Driver.h :)):

#ifndef _IOCTL_H_05703_ BASHBD_1UIWQ1_4763_ 1NJKDH256_801_#define _IOCTL_H_05703_ BASHBD_1UIWQ1_4763_ 1NJKDH256_801_#define IOCTL_PRINT_DEBUG_ MESS CTL_CODE( \FILE_DEVICE_UNKNOWN, 0x701, METHOD_BUFFERED, FILE_ANY_ACCESS)

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

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

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

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

#endif

Вот содержимое файла resource.h:

#ifdef APSTUDIO_INVOKED#ifndef APSTUDIO_ READONLY_SYMBOLS#define _APS_NEXT_ RESOURCE_VALUE 101#define _APS_NEXT_ COMMAND_VALUE 40001

Page 23: Драйвер

#define _APS_NEXT_ CONTROL_VALUE 1000#define _APS_NEXT_ SYMED_VALUE 101#endif#endif

... и файла ресурсов .rc:

//Microsoft Developer Studio generated resource script.//#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS///////////////////////////////////////////////////////////////////////////////// Generated from the TEXTINCLUDE 2 resource.//#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////// Russian resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)#ifdef _WIN32LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT#pragma code_page(1251)#endif //_WIN32

#ifndef _MAC///////////////////////////////////////////////////////////////////////////////// Version//

VS_VERSION_INFO VERSIONINFOFILEVERSION 1,0,0,0PRODUCTVERSION 1,0,0,0FILEFLAGSMASK 0x3fL#ifdef _DEBUGFILEFLAGS 0x1L#elseFILEFLAGS 0x0L#endifFILEOS 0x40004LFILETYPE 0x1LFILESUBTYPE 0x0LBEGINBLOCK "StringFileInfo"BEGINBLOCK "040904b0"BEGINVALUE "Comments", "Tester for Primer.sys driver\0"VALUE "CompanyName", "Hacker\0"VALUE "FileDescription", "TestPrim\0"VALUE "FileVersion", "1, 0, 0, 0\0"VALUE "InternalName", "TestPrim\0"VALUE "LegalCopyright", "Copyright © 2005 Hacker\0"VALUE "LegalTrademarks", "\0"VALUE "OriginalFilename", "TestPrim.exe\0"VALUE "PrivateBuild", "\0"VALUE "ProductName", "Primer.sys tester\0"

Page 24: Драйвер

VALUE "ProductVersion", "1, 0, 0, 0\0"VALUE "SpecialBuild", "\0"ENDENDBLOCK "VarFileInfo"BEGINVALUE "Translation", 0x409, 1200ENDEND

#endif // !_MAC

#endif // Russian resources/////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////// English (U.K.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENG)#ifdef _WIN32LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK#pragma code_page(1252)#endif //_WIN32

#ifdef APSTUDIO_INVOKED///////////////////////////////////////////////////////////////////////////////// TEXTINCLUDE//

1 TEXTINCLUDE MOVEABLE PURE BEGIN"resource.h\0"END

2 TEXTINCLUDE MOVEABLE PURE BEGIN"#include ""afxres.h""\r\n""\0"END

3 TEXTINCLUDE MOVEABLE PURE BEGIN"\r\n""\0"END

#endif // APSTUDIO_INVOKED

#endif // English (U.K.) resources/////////////////////////////////////////////////////////////////////////////

#ifndef APSTUDIO_INVOKED///////////////////////////////////////////////////////////////////////////////// Generated from the TEXTINCLUDE 3 resource.//

/////////////////////////////////////////////////////////////////////////////

Page 25: Драйвер

#endif // not APSTUDIO_INVOKED

Ну а теперь головной .cpp - шник программы с комментариями:

////////////////////////////////////////////////////////////////////// Файл PrimerTest.cpp// Консольное приложение для тестирования драйвера Primer.sys////////////////////////////////////////////////////////////////////

// Заголовочные файлы, которые необходимы данной проге:#include <windows.h>#include <stdio.h>#include <winioctl.h>#include <tchar.h>

#include "Ioctl.h"

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

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

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

CloseServiceHandle (Service);return ret;}

Page 26: Драйвер

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

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

}

CloseServiceHandle (Service);return ret;}

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

Page 27: Драйвер

res = StartDriver (scm, DRIVERNAME);if(res){// Здесь следует разместить функции работы с драйверомres = StopDriver (scm, DRIVERNAME);if(res) res = RemoveDriver (scm, DRIVERNAME);}CloseServiceHandle(scm);return;}*/

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

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

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

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

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

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

Page 28: Драйвер

// переданных байт// Приступаем к тестированию драйвера. Последовательно выполняем обращения к драйверу // с различными кодами IOCTL: 

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

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

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

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

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

// Выполнение следующего теста в Windows NT приведет к// краху ОС`и /*

Page 29: Драйвер

ioctlCode=IOCTL_MAKE_SYSTEM_CRASH;if(!DeviceIoControl(hHandle,ioctlCode,NULL, 0, NULL, 0, &BytesReturned,NULL)){printf("Error in IOCTL_MAKE_SYSTEM_CRASH!");return(-1);}*/// Закрываем дескриптор доступа к драйверу:CloseHandle(hHandle);

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

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

CloseServiceHandle(scm);#endif

return 0;}

Вот и все, компиль и тестируй. Напоминаю еще раз: запусти вьювер дебаг-информации из Monitor (например) и посмотри все дебаговые сообщения драйвера! 

Правка солюшенов визуальной студии

Теперь я расскажу тебе, как исправить солюшены визуальной студии, чтобы ты смог писать драйвера в ней. Тем не менее, код драйвера, откомпилированный с помощью DDK (а лучше всего использовать именно его), считается эталонным. Ниже я привожу текст файла Primer.sln (Visual Studio 7) (для компиляции не-WDM драйвера):

Microsoft Visual Studio Solution File, Format Version 7.00Project (" {8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942} ") = "Primer",EndProjectGlobalGlobalSection (SolutionConfiguration) = preSolutionConfigName.0 = CheckedEndGlobalSectionGlobalSection (ProjectDependencies) = postSolutionEndGlobalSectionGlobalSection (ProjectConfiguration) = postSolution{ E524BA09-7993-4528-91A9-7E27FAA3565F }.Checked.ActiveCfg = Checked | Win32{ E524BA09-7993-4528-91A9-7E27FAA3565F }.Checked.Build.0 = 

Page 30: Драйвер

Checked | Win32EndGlobalSectionGlobalSection (ExtensibilityGlobals) = postSolutionEndGlobalSectionGlobalSection (ExtensibilityAddIns) = postSolutionEndGlobalSectionEndGlobal

А теперь - .vcproj:

<?xml version = "1.0" encoding = "windows-1251" ?><VisualStudioProject ProjectType = "Visual C++"Version = "7.00"Name = "Primer"SccProjectName = " "SccLocalPath = " " ><Platforms><Platform Name = "Win32" /> </Platforms><Configurations> <Configuration Name = "Checked | Win32 "OutputDirectory = ".\checked"IntermeaditeDirectory = ".\checked"ConfigurationType = "2"UseOfMFC = "0"ATLMinimizesCRunTimeLibraryUsage = "FALSE"CharacterSet = "1" ><Tool Name = "VCCLCompilerTool"AdditionalOptions = "/Zel - cbstring /QIfdiv- /QIf /Gi- /Gm- /GX"Optimization = "0"EnableIntrinsicFunctions = "FALSE"OmitFramePointers = "TRUE"OptimizeForProcessor = "2"AdditionalIncludeDirectories = "C:\WinDDK\2600\inc\ddk\w2k;C:\WinDDK\2600\inc\w2k;C:\WinDDK\2600\inc\crt"PreprocessorDefinitions = "_X86_=1;i386=1;CONDITION_HANDLING=1;NT_UP=1;NT_INST=0; WIN32=100;_NT1X_= 100;WINNT=1;_WIN32_WINNT=0x0400; WIN32_LEAN_AND_MEAN=1;DEVL=1; DBG=1; FPO=0"IgnoreStandartIncludePath="TRUE"StringPooling = "TRUE"ExceptionHandling = "TRUE"RuntimeLibrary="0"StructMemberAlignment="4"BufferSecurityCheck = "FALSE"EnableFunctionLevelLinking = "TRUE"PrecompiledHeaderFile =  ".\checked/Primer.pch"AssemblerListingLocation = ".\checked/"ObjectFile=".\checked/"ProgramDataBaseFileName= ".\checked\Primer.pdb"WarningLevel="3"SuppressStartupBanner="TRUE"DebugInformationFormat="1"CallingConvention="2"CompileAs = "0"ForcedIncludeFiles= "warning.h"/><Tool Name = "VCCustomerBuildTool"/><Tool Name = "VCLinkerTool" AdditionalOptions = " "AdditionalDependencies = "hal.lib ntoskrnl.lib int64.libmsvcrt.lib"OutputFile = ".\checked\Primer.sys"Version = "5.0"LinkIncremental = "1'SuppressStartupBanner = "TRUE"AdditionalLibraryDirectories = "C:\WinDDK\2600\lib\w2k\i386"

Page 31: Драйвер

IgnoreAllDefaultsLibraries = "TRUE"ProgramDatabaseFile = ".\checked/Primer.pdb"GenerateMapFile = "TRUE"MapFileName = "Primer.map"StackReserveSize = "262144"StackCommitSize = "4096"OptimizeReferences = "2"EnableCOMDATFolding = "2"EntryPointSymbol = "DriverEntry"SetChecksum = "TRUE"BaseAddress = "0x10000"ImportLibrary = " "MergeSections = ".rdata = .text"TargetMachine = "1" /><Tool Name = "VCMIDILTool"MkTypeLibCompatible = "TRUE"SuppressStartupBanner = "TRUE"TargetEnvironment = "1"TypeLibraryName = ".\checked/Primer.tlb"/><Tool Name = "VCPostBuildEventTool" /><Tool Name = "VCPreBuildEventTool"/><Tool Name = "VCPreLinkEventTool" /><Tool Name = "VCResourceCompilerTool"/><Tool Name = "VCWebServiceProxyGeneratorTool" /><Tool Name = "VCWebDeploymentTool" /></Configuration></Configurations><Files> <Filter Name = "Header Files" Filter = ".h"><File RelativePath = ".\Driver.h"> </File></Filter><Filter Name = "Source Files" Filter = ".c;.cpp"><File RelativePath = ".\main.cpp"> </File></Filter></Files><Globals></Globals></VisualStudioProject>

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