Разработка и отладка простейшего драйвера

April 2, 2012 by admin Комментировать »

Для того чтобы начать разрабатывать драйверы устройств Windows на компьютере дол жен быть установлен пакет Windows DDK (Driver Development Kit) фирмы Microsoft, желатель но последней версии (на момент написания книги таковой являлась версия 2003  SP1). Этот пакет включает все необходимые средства для разработки  и отладки драйверов.  Кроме того, нужно иметь под рукой хороший компилятор  C/C++  (подойдет бесплатная версия Microsoft Visual Studio Express Edition) для разработки приложения, используемого при тестировании драйвера.  Поскольку  для драйвера  потребуется отладчик, то в качестве такого можно вы брать DebugView (www.microsoft.com),  который, несмотря на свою простоту, довольно удобен в работе.

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

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

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

Вот исходный текст нашего простейшего драйвера:

#include  <ntddk.h>

#define  NT_DEVICE_NAME   L"\\Device\\Test"

#define  DOS_DEVICE_NAME   L"\\DosDevices\\Test"

VOID

Test_Unload  (IN  PDRIVER_OBJECT  DriverObject)

{

UNICODE_STRING  DosDeviceName;

DbgPrint("%s","Test:  UNLOADING!\n"); RtlInitUnicodeString(&DosDeviceName,  DOS_DEVICE_NAME); IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DriverObject->DeviceObject);

}

NTSTATUS

Test_Create(IN  PDEVICE_OBJECT  DeviceObject, IN PIRP Irp)

{

DbgPrint("%s", "Test:  CREATED!\n");

Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp,  IO_NO_INCREMENT); return STATUS_SUCCESS;

}

NTSTATUS

Test_Close(IN   PDEVICE_OBJECT  DeviceObject, IN PIRP Irp)

{

DbgPrint("%s","Test:  CLOSED!\n");

Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp,  IO_NO_INCREMENT); return STATUS_SUCCESS;

}

NTSTATUS

DriverEntry(IN  PDRIVER_OBJECT  DriverObject, IN PUNICODE_STRING  RegistryPath)

{

PDEVICE_OBJECT  DeviceObject; UNICODE_STRING  NtDeviceName; UNICODE_STRING  DosDeviceName; NTSTATUS  status;

RtlInitUnicodeString(&NtDeviceName,  NT_DEVICE_NAME);

status = IoCreateDevice(DriverObject,

0,

&NtDeviceName, FILE_DEVICE_UNKNOWN,

0,

FALSE,

&DeviceObject);

if (!NT_SUCCESS(status))

{

IoDeleteDevice(DeviceObject);

return status;

}

RtlInitUnicodeString(&DosDeviceName,  DOS_DEVICE_NAME);

status = IoCreateSymbolicLink(&DosDeviceName, &NtDeviceName);

if (!NT_SUCCESS(status))

{

IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DeviceObject);

return status;

}

DeviceObject->Flags  &=   ~DO_DEVICE_INITIALIZING; DeviceObject->Flags  |=  DO_BUFFERED_IO;

DriverObject->MajorFunction[IRP_MJ_CREATE]  = Test_Create;

DriverObject->MajorFunction[IRP_MJ_CLOSE]     = Test_Close; DriverObject->DriverUnload  = Test_Unload;

return status;

}

В начало листинга нужно включить файл заголовка  ntddk.h,  в котором  определены все не обходимые функции. Далее мы определяем две строковые  константы в  формате UNICODE. Здесь мы должны уточнить некоторые нюансы. Строка NT_DEVICE_NAME указывает имя устрой ства, к которому будет обращаться наш драйвер, таким, каким  оно должно быть в пространстве имен операционной системы. Здесь возникает  проблема: приложение пользователя не может использовать функцию CreateFile() с данным именем, поскольку, в силу исторических обстоя тельств, нужно указывать другой путь, определенный в константной строке DOS_DEVICE_NAME. Ничего трагического  в этом нет, и мы может просто связать, или по другому, сделать ссылку из DOS_DEVICE_NAME на NT_DEVICE_NAME, после чего все будет работать  нормально.

Вспомним, что каждый драйвер обрабатывает только те пакеты запросов, которые про

граммист для него определил. В нашем случае мы создали драйвер «виртуального» устрой

ства Test, который обрабатывает только запросы IRP_MJ_CREATE, IRP_MJ_CLOSE и отдельно Unload. Функция Unload выгружает драйвер из системы и обрабатывается несколько иным способом, чем пакеты запросов (в WDM драйверах эта функция вообще не используется).

Проанализируем исходный текст функции обработчика IRP_MJ_CREATE:

NTSTATUS

Test_Create(IN  PDEVICE_OBJECT  DeviceObject, IN PIRP Irp)

{

DbgPrint("%s",  "Test: CREATED!\n");

Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp,  IO_NO_INCREMENT); return STATUS_SUCCESS;

}

Первое – все функции обработчики пакетов запросов принимают два параметра – адрес структуры  DEVICE_OBJECT устройства  и адрес пакета  запроса  PIRP. Оба параметра содержат поля, которые функция может использовать при обработке запроса.

Функция DbgPrint – это специальная функция, предназначенная для вывода информации  в отладчик.  Эта функция используется  только для трассировки программного кода и может быть перехвачена только в отладчике, таком, например, как  DebugView. В функции Testdrv_Create также ничего существенного не делается. В поле Status пакета запроса заносится значение STATUS_SUCCESS, которое затем будет возвращено  приложению  как  свидетельство успешно выполненной операции. Функция IoCompleteRequest завершает обработку пакета запроса. Сама функция возвращает  значение  STATUS_SUCCESS.

Точно так же работает и функция Testdrv_Close, которая, по запросу приложения (функ

ция WINAPI CloseHandle()) закрывает дескриптор устройства.

Функция Testdrv_Unload удаляет символическую ссылку на устройство, о которой мы упо

минали ранее (IoDeleteSymbolicLink()) и удаляет устройство из системы (IoDeleteDevice()).

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

1)   инициализируется константная строка с именем устройства в пространстве имен си

стемы (функция RtlInitUnicodeString(&NtDeviceName,  NT_DEVICE_NAME);

2)   создается программный  объект описания устройства при помощи функции IoCrea teDevice(). Если объект устройства создать не удалось, функция заканчивает работу с ошибкой,  код которой  передается в переменной status, а сам объект устройства удаляется;

3)   инициализируется константная строка с именем устройства в пространстве имен,

доступном пользовательской  программе  (функция RtlInitUnicodeString(&DosDevice Name, DOS_DEVICE_NAME));

4)   создается символическая ссылка DOS имени на имя из пространства имен операци

онной системы для доступа к устройству из приложения пользователя (функция status

= IoCreateSymbolicLink(&DosDeviceName, &NtDeviceName). В случае неудачи симво лическая ссылка и объект устройства удаляются, а функция DriverEntry заканчивается с ошибкой;

5)   устанавливаются флаги устройства;

6)   определяются функции обработчики пакетов запроса.

Поместим исходный текст драйвера в файл test.c и сохраним его. Теперь в каталог, где находится файл, поместим еще два файла:

makefile sources

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

# The sources for   the  test device   driver: TARGETNAME=test

TARGETPATH=obj TARGETTYPE=DRIVER INCLUDES=..\

TARGETLIBS=     $(DDK_LIB_PATH)\wdmsec.lib

SOURCES=test.c

После этого выберите  консоль  для запуска  компилятора  в среде Windows DDK, например, Windows XP Checked Build Environment и перейдите в каталог,  где находятся ваши рабочие файлы. Затем наберите команду

build –ceZ

Если в исходном тексте файла test.c нет ошибок, то файл будет откомпилирован успешно

(рис. 7.7):

Рис. 7.7

Рис. 7.10

Вид окна отладчика DebugView

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

щиеся в Интернете.

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

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

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

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

typedef struct  _MY_DEVICE_EXTENSION

{

LONG   L1;

}  MY_DEVICE_EXTENSION,   *PMY_DEVICE_EXTENSION;

Здесь определена структура с одной 32 битовой целочисленной переменной L1. Посколь ку требуется всего 4 байта памяти, то система (гипотетически) могла бы выделить эти 4 байта, но, поскольку память выделяется блоками с учетом выравнивания  для увеличения быстро действия, то, скорее  всего,  выделенный объем будет больше. Кроме  того, при создании объекта устройства в функции IoCreateDevice в качестве второго параметра должен зада ваться размер области расширения, например:

status =  IoCreateDevice(DriverObject, sizeof(MY_DEVICE_EXTENSION),

&NtDeviceName, FILE_DEVICE_UNKNOWN,

0,

FALSE,

&DeviceObject);

Здесь   вторым    параметром    функции   IoCreateDevice   является   sizeof   (MY_DEVICE_ EXTENSION).

Какие  функции используются для операций записи чтения? Что касается приложения пользователя, то мы уже рассматривали этот вопрос. Напомню, что приложение может запи сать данные в дескриптор  устройства с помощью  функции WINAPI WriteFile(), а прочитать дан ные с помощью функции ReadFile(). Есть и универсальная  функция DeviceIoControl(), позволя ющая выполнять как чтение, так и запись данных.

Для манипуляций с данными в драйвере устройства можно использовать  одну из функций ядра вида RtlXXX. Очень часто используется функция RtlMoveMemory(), которая перемещает данные из системной области памяти в область памяти драйвера (при записи данных в уст ройство) и наоборот, из области памяти драйвера в системный буфер (при чтении данных из устройства).

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

IRP_MJ_READ,  IRP_MJ_WRITE  и  IRP_MJ_DEVICE_CONTROL. При  этом  для  драйвера  должен быть  указан  тип  обмена  данными  (буферизованный  или  небуферизованный)  в  функции DriverEntry. Во всех наших примерах мы будем использовать буферизованный обмен данны

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

DeviceObject->Flags  |=   DO_BUFFERED_IO;

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

И, наконец, последнее перед тем, как перейти к практике – в наших последующих приме рах мы будем для обмена данными использовать функцию DeviceIoControl, для которой Ме неджер ввода вывода операционной системы формирует пакет IRP_MJ_DEVICE_CONTROL.

Источник:  Магда Ю. С. Компьютер  в домашней лаборатории.  – М.: ДМК Пресс, 2008. – 200 с.: ил.

Оставить комментарий

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