Пишем драйвер для видеокарты
Вот и пришло время второй статьи из цикла о написании драйверов под Windows. Сейчас я тебя немного расстрою: в предыдущей статье я обещала, что в этой мы приступим собственно к практике. Но данная статья это, скорее, "полупрактика". Глупо "с места в карьер" бросаться разрабатывать драйвера режима ядра
(так как, если ты не забыл, в данном цикле мы разбираем именно этот тип драйверов), хотя бы поверхностно не изучив особенности и приёмы программирования в режиме ядра, что мы и сделаем в первой части этой статьи. Ну а во второй мы наконец - то разберём структуру настоящего драйвера под Windows
(Legacy и немного WDM) : его основные функции, их взаимодействие, а также параметры, принимаемые ими. Так что к третьей статье этого цикла ты уже, надо думать, будешь основательно подготовлен к написанию своего первого драйвера. Начинаем.
Особенности и приёмы программирования в режиме ядра
Программирование в режиме ядра имеет массу особенностей, для прикладников очень непривычных, а для новичков довольно сложных. Во-первых, у режима ядра своё, отличное от такового в пользовательском режиме, API. Кроме того, для кода, выполняющегося в режиме ядра, имеет очень большое значение его уровень IRQL, так как приложениям, выполняющимся на высоких уровнях IRQL, недоступны многие функции, к которым имеют доступ приложения низких IRQL уровней, и наоборот. Всё это необходимо учитывать.
Во-вторых, в режиме ядра есть свои дополнительные описатели типов. Полный их список можно найти в заголовочном файле ntdef.h. Его содержание примерно таково:
typedef unsigned char USHAR
typedef unsigned short USHORT
typedef unsigned long ULONG
.
Зачем это нужно? Ну, во-первых, для красоты. тьфу, для унификации стиля классических C - типов данных и нововведённых - таких, как WCHAR
(двухбайтный Unicode символ), LARGE_INTEGER (который, на самом деле, является объединением) и т.д. А также для унификации исходников для 32 - разрядных платформ и надвигающихся 64 - разрядных.
В исходниках драйверов часто встречаются макроопределения
IN, OUT, OPTIONAL. Что они означают? А ровным счётом ничего, и введены они только для повышения удобочитаемости исходника. OPTIONAL обозначает необязательные параметры, IN - параметры, передаваемые внутрь функции, например, OUT - соответственно, наоборот. А вот IN OUT означает, что параметр передаётся внутрь функции, а затем возвращается из неё обратно.
Функции драйвера, за исключением DriverEntry (главная процедура драйвера, подробнее см. во второй части статьи), могут называться как угодно, тем не менее, существуют определённые "правила хорошего тона" при разработке драйверов, в том числе и для именования процедур: например, все функции, относящиеся к HAL, желательно предварять префиксом HAL и т.д. Сама Microsoft практически постоянно следует этому правилу. А имена типов данных и макроопределения в листингах DDK написаны сплошь заглавными буквами. Советую тебе поступать также при разработке своих драйверов. Это и в самом деле во много раз повышает удобство работы с листингом.
А теперь, чтобы тебе стали более или менее понятны основные различия между программированием в пользовательском и системном режимах, расскажу об основных функциях для работы с памятью и реестром в режиме ядра.
Начнём с функций для работы с памятью, а для начала поговорим собственно об устройстве и работе с памятью в Windows. Единое 4-х гигабайтное адресное пространство памяти Windows
(я имею в виду 32-х разрядные версии Windows) делится на две части: 2 гигабайта для пользовательского пространства и 2 гигабайта для системного. 2 гигабайта системного пространства доступны для всех потоков режима ядра. Системное адресное пространство делится на следующие части:
Видов адресов в режиме ядра три: физические
(реально указывающие на область физической памяти), виртуальные
(которые перед использованием транслируются в физические), и логические
(используемые HAL уровнем при общении с устройствами; он же и отвечает за работу с такими адресами). Функции режима ядра, отвечающие за выделение и освобождение виртуальной памяти, отличаются от таковых в пользовательском режиме. Также, находясь на уровне режима ядра, становится возможным использовать функции выделения и освобождения физически непрерывной памяти. Разберём все эти функции поподробнее.
1) PVOID ExAllocatePool (уровень IRQL, на котором может выполняться эта функция - < DISPATCH_LEVEL) - выделяет область памяти. Принимает два параметра: параметр
(POOL_TYPE) , в котором содержится значение, означающее, какого типа область памяти нужно выделить: PagedPool - страничная, NonPagedPool - нестраничная
(в этом случае функцию можно вызвать с любого IRQL уровня). Второй параметр
(ULONG) - размер запрашиваемой области памяти. Функция возвращает указатель на выделенную область памяти, и NULL, если выделить память не удалось.
2) VOID ExFreePool (IRQL
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 - в противном случае.
6) PHYSICAL_ADDRESS MmGetPhysicalAddress (IRQL - любой) - определяет физический адрес по виртуальному. Принимает параметр
(PVOID), содержащий анализируемый виртуальный адрес. Возвращаемое значение - полученный физический адрес.
Основные функции для работы с памятью рассмотрели, перейдём к таковым для работы с реестром. Сначала поговорим о функциях доступа к реестру, предоставляемых диспетчером ввода - вывода, потом о драйверных функциях прямого доступа к реестру, а затем о самом богатом по возможностям и удобству семействе функций для работы с реестром -
Zw~.
Драйверные функции, предоставляемые диспетчером ввода - вывода.
2) IoGetDeviceProperty - данная функция запрашивает из реестра установочную информацию об устройстве.
3) IoOpenDeviceRegistryKey - возвращает дескриптор доступа к подразделу реестра для драйвера или устройства по указателю на его объект.
Драйверные функции для прямого доступа к реестру.
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 - возвращает информацию о вложенных подразделах уже открытого ранее подраздела.
5) ZwEnumerateValueKey - возвращает информацию о параметрах и их значениях открытого ранее подраздела.
6) ZwQueryValueKey - возвращает информацию о значении параметра в открытом ранее разделе реестра. Полнота возвращаемой информации определяется третьим параметром, передаваемым функции, который может принимать следующие значения
(дополнительные разъяснения не требуются, так как они имеют "говорящие" имена):
KeyValueBasicInformation, KeyValuePartialInformation и KeyValueFullInformation.
7) ZwSetValueKey - создаёт или изменяет значение параметра в открытом ранее подразделе реестра.
8) ZwFlushKey - принудительно сохраняет на диск изменения, сделанные в открытых функциями ZwCreateKey и ZwSetValueKey подразделах.
9) ZwDeleteKey - удаляет открытый подраздел из реестра.
10) ZwClose - закрывает дескриптор открытого ранее подраздела реестра, предварительно сохранив сделанные изменения на диске.
Практически все вышеперечисленные функции для работы с реестром должны вызываться с уровня IRQL
PASSIVE_LEVEL.
Думаю, пока достаточно. Конечно, у всех вышеперечисленных функций есть масса нюансов в применении. Да и вообще функций режима ядра - великое множество, их ничуть не меньше, чем в пользовательском режиме. Но моя задача была не рассказать обо всех API - функциях режима ядра
(что даже в рамках цикла невозможно сделать), а продемонстрировать отличия функций режима ядра, от таковых в пользовательском режиме, и хоть немного рассказать о нюансах их применения
(взять, к примеру, то, что в пользовательском режиме не имеет значения, в потоке какого приоритета будет выполняться приложение: оно будет иметь такой же полный доступ ко всем API функциям пользовательского режима, как и любые другие приложения; на уровне ядра, как ты только что, убедился, это не так). Ну а за более или менее полным списком и описанием всех этих API - функций советую обратиться к библии Гарри Нэббета.
Ну вот и всё, теперь ты готов к разговору о структуре драйвера, который мы сейчас и начнём.
Структура драйвера
Я уже говорила, что драйвер фактически можно представить как довольно-таки обычную dll-ку уровня ядра. Таким образом, далее можно представить драйвер просто как набор процедур, периодически вызываемых внешними программами. Несмотря на то, что процедуры драйверов для разных устройств сильно отличаются, есть общая структура и общие функции для всех драйверов. Главные из них мы сейчас и рассмотрим.
1) Поле DriverUnload - для регистрации собственной функции Unload, вызываемой при выгрузке драйвера. Подробнее о ней.
Эта функция вызывается только при динамической выгрузке драйвера
(т.е. происшедшей не в результате завершения работы системы). Legacy драйвера в этой функции выполняют полное освобождение всех занятых драйвером системных ресурсов. WDM драйвера выполняют такое освобождение в функции RemoveDevice при удалении каждого устройства
(если драйвер обслуживает несколько устройств). Функция Unload вызывается с уровня IRQL PASSIVE_LEVEL, принимает единственный параметр
(PDRIVER_OBJECT) - указатель на объект драйвера, и возвращает
void.
2) Поле DriverStartIo - для регистрации собственной функции
StartIo. Вкратце, регистрация функции StartIo нужна для участия в System Queuing - создании очередей необработанных запросов системными средствами, в отличие от DriverQueuing - когда то же самое реализуется средствами самого драйвера.
3) Поле AddDevice в подструктуре DriverExtension - для регистрации WDM драйвером своей процедуры
AddDevice.
4) Поле MajorFunction - для регистрации драйвером точек входа в свои рабочие процедуры.
Если выгрузка драйвера происходит в результате завершения работы системы, то функция Unload не вызывается. Это понятно: при выключении системы можно не заботиться об освобождении памяти и т.д. Поэтому вызывается функция Shutdown, которая просто предоставляет драйверу возможность оставить устройство в приемлемом состоянии покоя.
Может случиться так, что твоему драйверу необходимо будет получить управление при крахе системы. В этом случае ему нужно зарегистрировать callback процедуру Bugcheck. Если драйвер правильно выполнил регистрацию этой функции, то он будет вызван во время исполнения crash процесса.
Я перечислила основные функции драйвера для его загрузки и выгрузки. Теперь перейдём к рабочим процедурам драйвера.
Поговорим о следующих рабочих процедурах: обслуживания ввода - вывода
(включающих в себя процедуры передачи данных и обслуживания прерываний), callback процедуры для синхронизации доступа к объектам и некоторые другие.
Все драйверы должны иметь обработчик CreateDispatch, обрабатывающий пользовательский запрос CreateFile. Если драйверу нужно обрабатывать пользовательский запрос CloseHandle, то он должен иметь обработчик
CloseDispatch.
Перейдём к процедурам передачи данных. Это обработчики пользовательских запросов ReadFile, WriteFile и
DeviceIoControl.
Процедуру StartIo я уже рассмотрела, поэтому перейдём к процедуре обслуживания прерываний
(ISR - Interrupt Service Routine, напоминаю на всякий случай). Данная процедура вызвается диспетчером прерываний ядра
(Kernel`s Interrupt Dispatcher) при каждой генерации прерывания устройством, и она обязана полностью обслужить это прерывание.
Теперь о callback процедурах сихронизации доступа к объектам. Для начала разберёмся, в чём различия принципов сихронизации доступа к объектам в пользовательском и ядерном режимах. Например, в пользовательском режиме, если какой - либо поток обратился к объекту, уже занятому другим потоком, то он
(первый поток) запросто может быть заблокирован до лучших времён. Как ты сам понимаешь, в режиме ядра такая внеплановая "заморозка" потоков неприемлема, поэтому и применяется другая технология сихронизации. И заключается она в следующем. Когда какой-либо поток обращается к объекту, уже занятому другим потоком, то он оставляет свой запрос в очереди запросов. Если драйвер предварительно зарегистрировал особую callback функцию, то диспетчер ввода - вывода при освобождении
требуемого ресурса, уведомит об этом драйвер, вызвав callback - функцию. Таким образом, обеспечивается гарантия ответа на любой запрос к ресурсу, даже если он
(ответ) будет состоять только в том, чтобы уведомить о задержке в обработке и помещении запроса в очередь. Функции, это реализующие: IoAllocateController
(использующаяся для синхронизации доступа к контроллеру), AdapterControl
(использующаяся для синхронизации доступа к DMA каналам
(чаще всего)) и SynchCritSection (использующаяся для корректного обращения к ресурсам; точнее - эта функция позволяет коду с низким уровнем IRQL сделать работу при уровне DIRQL устройства без опасения возникновения конфликтов с ISR).
Также можно упомянуть ещё таймерные процедуры
(нужные для драйверов, выполняющих точный отсчёт временных интервалов; обычно реализуется с использованием IoTimer
(но не всегда)), процедуру IoCompletion (позволяющую WDM драйверу, работающему внутри многослойной драйверной структуры, получать уведомление о завершении обработки IRP запроса, направленного к драйверу нижнего уровня) и CancelRoutine
(если драйвер зарегистрирует эту callback процедуру при помощи вызова IoSetCancelRoutine, то диспетчер ввода - вывода сможет уведомить его об удалении IRP запросов, находящихся в ожидании обработки, что может понадобиться диспетчеру, если пользовательское приложение, инициировавшее эти IRP запросы, неожиданно завершит свою работу после снятия задачи диспетчером задач
(прошу прощения за необходимую тавтологию)).
Ну вот и всё, мы закончили обзор структуры драйвера и основных его процедур. Примеры их практического применения
(а также некоторых других функций) ты увидишь в третьей, заключительной статье этого цикла, в которой мы напишем свой полноценный Legacy драйвер.
Уфф, наконец-то мы покончили со скучной теорией. В этой статье ты узнал про некоторые приёмы программирования в режиме ядра, изучил структуру драйвера и его основные функции. Теперь ты полностью подготовлен
(на сей раз уже окончательно) к написанию своего первого
(или двадцать первого - не знаю) полноценного Legacy драйвера под Windows. Это мы проделаем в заключительной части моего рассказа о программировании драйверов под Windows. А пока что слегка отдохнём. Да не облысеют твои пятки!
Многие считают что самому создать драйвер для Windows это что-то на грани фантастики. Но на самом деле это не так. Конечно, разработка драйвера для какого-то навороченного девайса бывает не простой задачей. Но ведь тоже самое можно сказать про создание сложных программ или игр. В разработке простого драйвера нет ничего сложного и я попытаюсь на примерах это показать.
Сперва нам нужно определится в чем мы же будем создавать наш первый драйвер. Поскольку материал ориентирован на новичков, то язык программирования был выбран один из простых, и это не Си или ассемблер, а бейсик. Будем использовать один из диалектов бейсика — PureBasic. Из коробки он не обучен создавать драйверы, но у него удачный набор файлов, используемых для компиляции и небольшое шаманство позволяет добавить эту возможность. Процесс компиляции состоит из нескольких этапов. Если кратко, то он происходит следующим образом: Сначала транслятор «перегоняет» basic-код в ассемблер, который отдается FASM'у (компилятор ассемблера), который создает объектный файл. Далее в дело вступает линкер polink, создающий исполняемый файл. Как компилятор ассемблера, так и линкер могут создавать драйверы и если немного изменить опции компиляции, то получим не исполняемый файл, типа EXE или DLL, а драйвер режима ядра (SYS).
Окно IDE с загруженным кодом драйвера показано на скрине.
Компиляция драйвера выполняется через меню «Компилятор» (это если кто не понял).
Теперь определимся что будет делать наш первый драйвер. Обычно при изучении программирования начинают с простых вещей, скажем, выполнения математических операций и вывода результата. Вот пусть наш драйвер делает тоже самое, ведь банальная математика производимая в режиме ядра это очень круто!
Может показаться что это куча бессмысленного кода, но это не так.
У каждого драйвера должна быть точка входа, обычно у нее имя DriverEntry() и выполнена она в виде процедуры или функции. Как видите, в этом драйвере есть такая процедура. Если посмотрите на начало кода, то в первых строках увидите как ей передается управление. В этой процедуре происходит инициализация драйвера. Там же назначается процедура завершения работы драйвера, которая в нашем случае имеет имя UnloadDriver(). Процедуры CreateDispatch() и CloseDispatch() назначаются обработчиками соединения и отсоединения проги из юзермода.
Процедура DeviceIoControl() будет обрабатывать запросы WinAPI функции DeviceIoControl(), являющейся в данном драйвере связью с юзермодом. В конце кода расположена так называемая ДатаСекция (DataSection), в которой находятся имена драйвера, сохраненные в формате юникода (для этого использована одна из фишек ассемблера FASM).
Видите сколько понадобилось кода для выполнения простейшей математической операции — сложения двух чисел?
А теперь рассмотрим программу, работающую с этим драйвером. Она написана на том же PureBasic.
Процедура Plus() осуществляет связь с драйвером. Ей передаются хэндл, доступа к драйверу и слагаемые числа, которые помещаются в структуру и указатель на указатель которой, передается драйверу. Результат сложения чисел будет в переменной «Result».
Далее следует код простейшего GUI калькулятора, скопированного из википедии.
Когда закроют окно, то перед завершением работы программы, закрывается связь с драйвером и производится его деинсталляция из системы.
Результат сложения чисел 8 и 2 на скриншоте.
Исходные коды драйвера и программы, можно найти в папке «Examples», PureBasic на файлопомойке, ссылку на который давал в начале статьи. Там так же найдете примеры драйвера прямого доступа к порам компа и пример работы с памятью ядра.
PS.
Помните, работа в ядре чревата мелкими неожиданностями аля, BSOD (синий экран смерти), поэтому экспериментируйте осторожно и обязательно всё сохраняйте перед запуском драйвера.
Итак, для начала приведем текст этой несложной программы.
Итак, теперь сначала разберемся, что делает каждая инструкция. Перво-наперво мы подключаем заголовочный файл ntddk.h. Это один из базовых подключаемых файлов во всех драйверах: в нем содержатся объявления типов NTSTATUS, PDRIVER_OBJECT, PUNICODE_STRING, а также функции DbgPrint.
Структура DriverObject содержит множество полей, которые определяют поведение будущего драйвера. Наиболее ключевые из них — это указатели на так называемые вызываемые (или callback) функции, то есть функции, которые будут вызываться при наступлении определенного события. Одну из таких функций мы определяем: это функция UnloadRoutine. Указатель на данную функцию помещается в поле DriverUnload. Таким образом при выгрузке драйвера сначала будет вызвана функция UnloadRoutine. Это очень удобно, когда драйвер имеет какие-то временные данные, которые следует очистить перед завершением работы. В нашем примере эта функция нужна только чтобы отследить сам факт завершения работы драйвера.
Вы можете задуматься, мол ну с первой-то директивой понятно, типа оптимизация и все такое, но зачем мы используем вторую директиву, зачем помечать код как возможный к выгрузке в файл подкачки? Поясню этот вопрос: каждый процесс в системе имеет такой параметр, как IRQL (подробнее читаем по ссылке Interrupt request level ибо это материал отдельной статьи), то есть некоторый параметр, отвечающий за возможность прерывания процесса: чем выше IRQL тем меньше шансов прервать выполнение процесса. Возможности процесса так же зависят от IRQL: чем выше IRQL тем меньше возможности процесса, это вполне логично, т.е. такой подход побуждает разработчиков выполнять только самые необходимые операции при высоком IRQL, а все остальные действия делать при низком. Вернемся к основной теме, о том, почему мы делаем для функции UnloadRoutine возможность выгрузки в файл подкачки: все опять же сводится к оптимизации: работа с файлом подкачки недоступна при высоком IRQL, а процедура выгрузки драйвера гарантированно выполняется при низком IRQL, поэтому мы специально указываем руками что код функции выгрузки драйвера можно поместить в своп.
Ух, вроде как с обсуждением кода этой, казалось бы небольшой программки закончено, теперь разберемся как скомпилировать и запустить наш драйвер.
Теперь последовательность действий: сначала мы пишем два файла, один называется MAKEFILE, с таким содержимым
а второй называется sources и содержит в себе следующее:
Эти файлы нужны для сборки драйвера. Да, забыл сказать, что в WDK нет встроенной среды разработки, поэтому и нужен текстовый редактор, чтобы набирать текст драйверов. Для этой цели можно использовать и Visual Studio (некоторые даже интегрируют возможность сборки драйверов из VS), и любой другой текстовый редактор.
Сохраняем код драйвера в файл TestDriver.c и кладем его в ту же директорию, что и файлы MAKEFILE и souces. После этого запускаем установленный build environment (это командная строка с заданными переменными окружения для компиляции драйвера; она входит в WDK, и запустить ее можно как-то так: «Пуск->Программы->Windows Driver Kits->. ->Build Environments->WindowsXP->Windows XP x86 Checked Build Environment»). Переходим в директорию, куда мы положили файл с драйвером (у меня это C:\Drivers\TestDriver) с помощью команды cd (у меня команда выглядит следующим образом: cd C:\Drivers\TestDriver) и набираем команду build.
Данная команда соберет нам драйвер TestDriver.sys и положит его в папку «objchk_wxp_x86\i386».
Итак, чего же мы достигли: мы написали, скомпилировали и запустили свой первый Windows-драйвер! Добавлю только, что при написании сложный драйверов для отладки используется двухмашинная конфигурация, когда на одном компьтере ведется написание драйвера, а на другом — запуск и тестирование. Это делается из-за того, что неправильно написанный драйвер может обрушить всю систему, а на ней может быть очень много ценных данных. Часто в качестве второго компьютера используется виртуальная машина.
Драйвер-это основа взаимодействия системы с устройством в ОС Windows.Это одновременно удобно и неудобно.
Про удобства я разъяснять не буду - это и так понятно,
а заострюсь я именно на неудобствах драйверов.
В сложившейся ситуации пользователь полностью подчинён воле производителя
- выпусти тот драйвер - хорошо, а не выпустит.
Только продвинутый пользователь, имеющий голову на плечах
(особенно, если он ешё и программер) не станет мириться с таким положением дел
- он просто возьмёт и сам напишет нужный драйвер.
Это нужно и взломщику: драйвер - это удобное окошко в ring0,
которое является раем для хакера. Но хоть написать драйвер и просто,
да не совсем - есть масса подводных камней. Да и документированность данного вопроса на русском языке оставляет желать лучшего.
Этот цикл статей поможет тебе во всём разобраться.
Приступим.
Хочу сразу же сделать несколько предупреждений.
Данная статья всё-таки подразумевает определённый уровень подготовки.
Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок.
Поэтому хорошее знание обоих языков весьма желательно (если не сказать - обязательно).
Если же ты пока не можешь этим похвастаться,
но желание писать драйвера есть - что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы,
ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду.
Может как-нибудь в другой раз. Согласен? Тогда поехали!
Скоро здесь, возможно, будет стоять твоё имя.
Практически в любом деле, как мне кажется, нужно начинать с теории.
Вот и начнём с неё. Для начала уясним себе поточнее основные понятия.
Первое: что есть драйвер? Драйвер - в сущности
кусок кода ОС, отвечающий за взаимодействие с аппаратурой.
Слово "аппаратура" в данном контексте следует понимать в самом широком смысле.
С момента своего появления как такого до сегодняшнего дня драйвер беспрерывно эволюционировал.
Вот, скажем, один из моментов его развития. Как отдельный и довольно независимый модуль драйвер сформировался не сразу.
Да и сейчас этот процесс до конца не завершён:
ты наверняка сталкивался с тем, что во многих
дистрибутивах никсов для установки/перестановки 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).
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 неощутимы.
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.
Посмотри на схему - по ней многое можно себе уяснить. Разберём её подробнее.
С пользовательским режимом всё понятно. В 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) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность приложений, разработанных, скажем, для подсистемы 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.
Напоследок нельзя не упомянуть такие хорошие проги, как PE
Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.
Ну вот и подошла к концу первая статья из цикла про написание драйверов под Windows. Теперь ты достаточно "подкован" по
теоретической части, так что в следующей статье мы перейдём к практике. Желаю тебе удачи в этом интереснейшем деле - написании драйверов! Да не облысеют твои пятки!
Вот и пришло время третьей статьи в цикле о написании драйверов режима ядра под Windows
(и не последней - решено включить в цикл ещё одну, четвёртую, статью). Мы разобрали особенности архитектуры Windows NT, поговорили об особенностях драйвера, как понятия, и об его структуре и познакомились с некоторыми приёмами программирования в режиме ядра. А теперь мы, наконец, вплотную подошли к, собственно, написанию своего первого
(или тридцать первого) драйвера. В данной статье мы это и осуществим. Мы напишем простейший legacy - драйвер
("драйвер в стиле NT"), скомпилируем и установим его. И в результате, в виндошном диспетчере устройств наконец - то появится "устройство", драйвер к которому будет написан тобой. Приступим к воплощению этой мечты в реальность!
Пишем код драйвера
Процесс работы над нашим драйвером, мы начнем, естественно, с написания кода
(писать будем, как я уже говорила, на сях). Весь код нашего драйвера
(который мы назовём "Primer") будет находиться в двух файлах: Driver.h и main.cpp. Я буду по порядку объяснять куски кода, поэтому, если тебе захочется скомпилировать этот драйвер, достаточно будет просто скопировать их
(куски кода) в один файл. Начнём с Driver.h.
// Далее - структура расширения устройства
typedef struct _PRIMER_DEVICE_EXTENSION
PDEVICE_OBJECT fdo; // указатель на
FDO.
UNICODE_STRING ustrSymLinkName; // L"\\DosDevices\\Primer" - имя символьной ссылки
> PRIMER_DEVICE_EXTENSION, *PPRIMER_DEVICE_EXTENSION;
Подробное описание макроса CTL_CODE можно найти в заголовочном DDK файле Winioctl.h. А мы перейдём к
main.cpp. В прошлой статье я уже говорила о главных процедурах драйвера. В нашем драйвере это будут DriverEntry - главная точка входа драйвера, UnloadRoutine - процедура выгрузки драйвера и DeviceControlRoutine - обработчик DeviceIoControl IRP пакетов. Но перед тем, как разобраться с реализацией этих процедур, необходимо сделать некоторые предварительные объявления, с которых и начнётся наш файл main.cpp.
// Предварительные объявления функций:
NTSTATUS DeviceControlRoutine(IN PDEVICE_OBJECT fdo, IN PIRP Irp);
VOID UnloadRoutine(IN PDRIVER_OBJECT DriverObject);
NTSTATUS
ReadWrite_IRPhandler(IN PDEVICE_OBJECT fdo, IN PIRP Irp);
NTSTATUS
Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);
NTSTATUS
Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);
Итак, мы подключили заголовочный файл Driver.h, сделали несколько предварительных объявлений важных функций, объявили глобальную переменную и определили начало секции INIT. А теперь - реализация функции загрузки драйвера.
extern "C"
NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_OBJECT fdo; // указатель на объект драйвера
UNICODE_STRING devName; // указатель на раздел реестра
// Так как функция - NTSTATUS, то и возвращает она -
STATUS_XXX.
// Экспортируем точки входа в драйвер. Поскольку наш драйвер - 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; // сохраняем обратный указатель
// Продолжаем работу по созданию символьной ссылки
(примечания такие же, что и в предыдущей части работы).
// И, наконец, создаем собственно символьную ссылку.
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
IoDeleteDevice(fdo);
return status;
>
// Инициализируем объект спин - блокировки, который мы будем использовать для обрушения системы во время // выполнения кода обработчика IOCTL - запросов.
KeInitializeSpinLock(&SpinLock);
Процедура 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, // указатель на объект нашего FDO
IN PIRP Irp) // указатель на структуру принятого от Диспетчера ввода/вывода IRP
ULONG BytesTxd = 0;
NTSTATUS status = STATUS_SUCCESS;
Теперь - две связанных функции: 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);
Теперь - огромная рабочая процедура 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();
// Диспетчеризация по IOCTL кодам:
switch(ControlCode)
case IOCTL_MAKE_SYSTEM_CRASH: // "роняем" систему
(падает только NT)
int errDetected=0;
char x = (char)0xFF;
default: status = STATUS_INVALID_DEVICE_REQUEST;
>
Небольшая Эрих Мария Ремарка: почему в обработке IOCTL_MAKE_SYSTEM_CRASH не происходит перехвата исключения? Да потому, что вызов KeAcquireSpinLock меняет уровень IRQL на 2, а ведь данный обработчик IOCTL был вызван драйвером с уровня IRQL == 0
(PASSIVE_LEVEL)! Таким образом, конструкция try/exception, корректно работающая на нулевом IRQL уровне, на IRQL == 2 исключение не перехватывает, и, следовательно, не обрабатывает. Так что во всём виноваты спин - блокировки! Также стоит обратить пристальное внимание на процедуру обработки IOCTL_TOUCH_PORT_378H, так как в момент нашего обращения к порту может случиться так, что с ним уже будут работать другие драйвера или устройства.
Ну а теперь, наконец, процедура UnloadRoutine, выполняющая выгрузку драйвера, высвобождающая занятые драйвером объекты, и принимающая единственный параметр - указатель на объект драйвера.
VOID UnloadRoutine(IN PDRIVER_OBJECT pDriverObject)
PDEVICE_OBJECT pNextDevObj;
int i;
for(i=0; pNextDevObj!=NULL; i++)
PPRIMER_DEVICE_EXTENSION dx =
(PPRIMER_DEVICE_EXTENSION)pNextDevObj->DeviceExtension;
// Удаляем символьную ссылку и уничтожаем
FDO:
UNICODE_STRING *pLinkName = & (dx->ustrSymLinkName);
// сохраняем указатель:
pNextDevObj = pNextDevObj->NextDevice;
Вот и всё! (правда, несложно?) Драйвер готов! Точнее, готов только его исходный код: его ещё нужно откомпилировать, установить в систему и запустить. Вот об этом и поговорим.
Компиляция драйвера
Файл source содержит в себе индивидуальные настройки процесса компиляции и сборки
драйвера. В нашем случае он будет выглядит так:
Теперь все необходимые для компиляции файлы
(в нашем случае - main.cpp, Makefile,sources) осталось только поместить в один каталог и запустить компиляцию отладочной
(checked) версии драйвера с помощью утилиты Build. Всё, компиляция и сборка драйвера завершены. Перейдём к инсталляции.
Инсталляция драйвера
Инсталлировать драйвер можно несколькими способами: с внесением записей в реестр, с использованием программы Monitor из пакета Driver Studio, с использованием INF - файла и с использованием SCM - менеджера
(программно) (к слову сказать, не всегда есть такое богатство выбора - WDM - драйвера, например, рекомендуется инсталлить только с помощью INF - файла и Мастера установки оборудования). Последние два способа мы рассмотрим в следующей статье цикла, а первые два - сейчас.
Наш драйвер без проблем инсталлируется и работает как под Windows 9x, так и под NT
(секрет этого заключается в Windows 9x драйвере ntkern.vxd, который помогает NT - драйверам "почувствовать себя, как дома"; но, естественно, возможности его не безграничны), но процесс записи в реестр
(и записываемые значения) немного отличаются. Разберём оба варианта.
Открывай нотпэд, набивай в нём следующие строки и сохраняй документ под любым именем в виде .reg файла:
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.
Вот и всё. Ребуться и наслаждайся результатом!
(Проверить корректность установки и интеграции в ОС нового драйвера можно с помощью DeviceTree, например).
Заключение
Читайте также: