Для того щоб почати розробляти драйвери пристроїв 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 с.: Іл.