В нашому першому прикладі прочитаємо дані з пристрою і виведемо їх значення на екран. Для цього нам знадобиться розробити драйвер пристрою і програму, що буде читати дані. Почнемо з програми. Наше додаток буде встановлювати і запус кать драйвер, потім читати з нього дані, після чого буде зупиняти драйвер і вигр жати його з системи. Незважаючи на гадану складність такого додатка, більша його частина вже аналізувалася нами раніше (установка і видалення драйвера), тому нам залишається нется додати програмний код для читання даних з пристрою. Ось початковий текст цієї програми:

#include  <windows.h>

#include  <stdio.h>

#define  IOCTL_READ   CTL_CODE(FILE_DEVICE_UNKNOWN,\

0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)

SC_HANDLE  scm,  svc; SERVICE_STATUS   ServiceStatus;

void  main(void)

{

HANDLE  fh;

int i1;

DWORD   bytes;

scm =  OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

if (!scm)

{

printf("Cannot open SCM!\n");

return;

}

svc  =  CreateService(scm, "Testr", "Testr",

SERVICE_ALL_ACCESS,

SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, "i:\\testr.sys",

NULL, NULL, NULL, NULL, NULL);

if (!svc)

{

printf("Cannot  open service!\n"); CloseHandle(scm);

return;

}

StartService(svc, 0, NULL); CloseServiceHandle(svc); CloseServiceHandle(scm);

fh  = CreateFile("\\\\.\\Testr",

GENERIC_READ  | GENERIC_WRITE,

0, NULL, OPEN_EXISTING,

0,

NULL);

if (fh   !=  INVALID_HANDLE_VALUE)

{

DeviceIoControl(fh, IOCTL_READ, NULL,

0,

&i1, sizeof(i1),

&bytes, NULL);

printf("Testr: IOCTL_READ  = %d\n",  i1);

}

CloseHandle(fh);

scm =  OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

if (!scm)

{

printf("Cannot open SCM!\n");

return;

}

svc  = OpenService(scm,   "Testr", SERVICE_ALL_ACCESS); ControlService(svc,  SERVICE_CONTROL_STOP,  &ServiceStatus); DeleteService(svc);

CloseServiceHandle(svc); CloseServiceHandle(scm);

}

Наша програма буде взаємодіяти з «віртуальним» пристроєм testr, драй вер для якого ми розробимо пізніше, а зараз зупинимося більш детально на функ ції WINAPI DeviceIoControl (), з допомогою якої ми прочитаємо дані з нашого влаштування.

Перш за все, для функції DeviceIoControl () потрібно визначити коди команд, які

повинні виконуватися. В нашому випадку буде виконуватися тільки читання даних з устрій

ства, тому використовується одна команда, якій можемо присвоїти ім’я IOCTL_READ:

#define  IOCTL_READ   CTL_CODE(FILE_DEVICE_UNKNOWN,\

0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)

Команда формується за допомогою макросу CTL_CODE і її другий параметр повинен пре перевищувати число 0x800 (користувальницький діапазон). Коди IOCTL команд, а також їх атрибути в додатку і в драйвері повинні збігатися.

Функція DeviceIoControl в якості параметрів, крім усього іншого, бере адресу та розмір вхідного і вихідного буферів даних. При цьому «вхідний» буфер функції містить дані для запису в пристрій, а «вихідний» буфер – дані, отримані від пристрою. У нашій програмі дані будуть тільки читатися, тому параметри вхідного буфера приймаються рівними NULL і 0, а у вихідний буфер i1 буде записано значення целочіслен ної змінної, ліченої з пристрою. Значення цієї змінної потім буде виведено на екран дисплея.

Решта фрагменти програмного коду використовуються завантаження вивантаження драйвера

пристрої і нами проаналізовані раніше.

Перейдемо тепер до драйвера пристрою. Наш пристрій називається testr і з нього можна тільки читати дані. Оригінальний текст драйвера цього пристрою показаний далі:

#include  <ntddk.h>

#define  IOCTL_READ   CTL_CODE(FILE_DEVICE_UNKNOWN,\

0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)

#define  NT_DEVICE_NAME   L"\\Device\\Testr"

#define  DOS_DEVICE_NAME   L"\\DosDevices\\Testr"

typedef struct  _MY_DEVICE_EXTENSION

{

LONG  L1;

}  MY_DEVICE_EXTENSION,   *PMY_DEVICE_EXTENSION;

VOID

Testr_Unload   (IN  PDRIVER_OBJECT  DriverObject)

{

UNICODE_STRING  DosDeviceName;

RtlInitUnicodeString(&DosDeviceName,  DOS_DEVICE_NAME); IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DriverObject->DeviceObject);

}

NTSTATUS

Testr_Create(IN  PDEVICE_OBJECT  DeviceObject, IN PIRP Irp)

{

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

}

NTSTATUS

Testr_Close(IN  PDEVICE_OBJECT  DeviceObject, IN PIRP Irp)

{

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

}

NTSTATUS

Testr_IoctlR(IN  PDEVICE_OBJECT  DeviceObject, IN PIRP Irp)

{

PMY_DEVICE_EXTENSION   dx=(PMY_DEVICE_EXTENSION)DeviceObject-

>DeviceExtension;

PIO_STACK_LOCATION  pIoStack   = IoGetCurrentIrpStackLocation(Irp);

ULONG   ctlCode   = pIoStack->Parameters.DeviceIoControl.IoControlCode; ULONG   OutputLength  = pIoStack-

>Parameters.DeviceIoControl.OutputBufferLength;

PUCHAR   buf  = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;

dx->L1 = 337; OutputLength  = 4;

if (ctlCode == IOCTL_READ)

{

RtlMoveMemory(buf,

(PUCHAR)&dx->L1, OutputLength);

}

Irp->IoStatus.Information  = OutputLength; 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, sizeof(MY_DEVICE_EXTENSION),

&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]  = Testr_Create; DriverObject->MajorFunction[IRP_MJ_CLOSE]     = Testr_Close; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = Testr_IoctlR; DriverObject->DriverUnload  = Testr_Unload;

return status;

}

Аналіз вихідного тексту драйвера (назвемо його testr.sys) почнемо з розділу декларацій на початку лістингу. Як і в додатку користувача, щойно нами розглянутому, тут присутній оголошення команди IOCTL_READ:

#define  IOCTL_READ   CTL_CODE(FILE_DEVICE_UNKNOWN,\

0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)

Як видно, це оголошення збігається з тим, що зазначено в користувальницькому додатку. Для зберігання даних нам потрібна область пам’яті, для чого скористаємося областю, ви ділить під розширення драйвера пристрою (DeviceExtension). зчитуватися буде всього одна целочисленная мінлива, тому область розширення оголошується наступним об

разом:

typedef struct  _MY_DEVICE_EXTENSION

{

LONG  L1;

}  MY_DEVICE_EXTENSION,   *PMY_DEVICE_EXTENSION;

У цій структурі вказана 32 бітова целочисленная мінлива L1, значення якої буде прочитано додатком.

В нашому драйвері буде виконуватися читання даних по IOCTL команді, тому повинен бути присутнім обробник запиту IRP_MJ_DEVICE_CONTROL, що і вказується в функції DriverEntry наступним рядком:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  =  Testr_IoctlR;

Як обробника запиту виступає функція Test_IoctlR. Проаналізуємо еe ис Ходна текст. Перш за все, створюємо змінну покажчик dx на область розширення, в якій зберігається значення змінної L1. Потім для зручності роботи створюємо змін

ні ctlCode і InputLength, які будуть містити відповідно код команди IOCTL і раз заходів переданих у додаток даних. Для передачі даних система створює буфер пам’я ти, адреса якого ми присвоюємо змінної buf.

Змінній L1, що знаходиться в області розширення, присвоюємо довільне значення ня (в даному випадку, 337), яке і буде передано в додаток користувача. Крім того, нам буде потрібно вказати точну розмір (в байтах) переданої області даних. Ці дії виконуються двома операторами:

dx->L1 = 337; OutputLength  = 4;

Далі аналізуємо код команди (оператор if) і, якщо це IOCTL_READ, то виконуємо пересилання даних з області розширення драйвера в системний буфер за допомогою функції RtlMoveMemory:

RtlMoveMemory(buf,

(PUCHAR)&dx->L1, OutputLength);

У цій функції перший параметр – адреса області пам’яті, куди будуть переміщені дан ні, другий параметр – адреса області пам’яті, звідки будуть переміщатися дані, і, нако нец, третій параметр показує розмір даних, що пересилаються в байтах.

Перед завершенням виконання запиту функція повинна передати Менеджеру введення

виведення інформацію про розмір виведених даних, що виконує опреаторамі

Irp->IoStatus.Information  = OutputLength;

Потім запит завершується, як звичайно, установкою статусу операції і викликом функ

ції IoCompleteRequest:

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

Ось, власне, і всі про це драйвері. Перед компіляцією вихідного тексту в Windows DDK не забудьте вказати коректне ім’я в файлі sources. Нагадаю, що збірки драйвера потрібні два файли – makefile і sources. Обидва можна взяти із прикладів, потім відкоригую вать sources, після чого виконати компіляцію драйвера.

Скопіюємо файл драйвера testr.sys в кореневий каталог i: і запустимо користувальницький додаток. На екрані дисплея побачимо результат роботи (рис. 7.11):

У розглянутому нами прикладі додаток користувача виконало читання даних з

«віртуального» пристрою testr. З таким же успіхом можна і записувати дані в влаштуй ство. Зараз ми розглянемо більш складний приклад, в якому користувальницьке додатку ня записує цілочисельне значення в «віртуальне» пристрій введення виведення, а потім читає з нього значення, помножене на 3. Ось вихідний текст програми:

#include  <windows.h>

#include  <stdio.h>

Рис. 7.11

Вид вікна працюючої програми читання даних

з пристрою testr

Рис. 7.12

Вид вікна працюючої програми

Таким чином, ми розглянули базові концепції функціонування драйверів пристроїв операційних систем Windows. Перед тим як рухатися далі, хочу звернути увагу читачів на ще один аспект програмування драйверів. У багатьох випадках, особливо при розробці драйверів для електронних аматорських пристроїв, керованих від комп’ютера, окремі фрагменти програмного коду можна розробляти на вбудований ном асемблері. Наприклад, фрагмент програмного коду, в якому виконується множення ня числа на 3 в драйвері testw.sys (оператор dx> L1 = dx> L1 * 3 😉 може бути замінений сле дме послідовністю команд:

. . .

PUCHAR   Lbuf = (PUCHAR)&dx->L1;

switch   (ctlCode)

{

case  IOCTL_READ:

asm {

. . .

mov     ECX, DWORD PTR   Lbuf mov    EAX,  DWORD  PTR [ECX] imul  EAX,  3

mov      DWORD PTR   [ECX], EAX

}

На перший погляд може здатися, що оператор

dx->L1 = dx->L1 * 3;

виконається швидше, але при дизассемблирования ми отримаємо досить велику групу команд асемблера, що представляє код цього оператора, причому частина операцій будуть з биточнимі, або не цілком оптимальними. Це не означає, що потрібно використовувати Ассем Блер скрізь і всюди, але при роботі з портами вводу виводу, особливо якщо важливо бистродей ствие електроніки пристрою, асемблер може дуже навіть бути корисним.

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

Джерело: Магда Ю. С. Комп’ютер в домашній лабораторії. – М.: ДМК Пресс, 2008. – 200 с.: Іл.