ДонНТУ   Портал магістрів

[изображение]

Реферат за темою випускної роботи

Зміст

Введення

В останній час техніка зробила великий ривок у розвитку й продовжує інтенсивно розвиватися. З’являються все нові пристрої, які вимагають програмне забезпечення для роботи з ними - драйвера. Всі пристрої мають свої специфічні завдання й протоколи обміну даними для їхнього рішення, драйвера виконують абстрагування від цього рівня й надають користувальницьким програмам необхідний інтерфейс для роботи з їхніми пристроями. Наприклад, відеоадаптери розвиваються, поступово містять у собі нові можливості, при цьому можливо навіть міняючи внутрішні протоколи, але додатки користувача повинні залишатися працездатними, таким чином зовнішній інтерфейс драйверів повинен тільки доповнюватися

Мета й задачі дослідження, плановані результати

Метою дослідження є розробка методики написання драйверів для операційних систем Windows NT. Основні задачі дослідження:

Структура й функціонування драйверів

Драйвера мають ряд стандартних функцій, які обов’язково повинні в них бути присутніми, і мати певні параметри. Перша з них - функція завантаження (точка входу) драйвера:
NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING RegistryPath)
Значення, що повертається нею, має тип NTSTATUS, що буде постійно фігурувати далі. Саме ім’я DriverEntry є незмінним, і відшукується компілятором при зборці драйвера, також як для Windows додатка функція WinMain. В DriverEntry системою передається покажчик на об’єкт драйвера й покажчик на Unicode-рядок, що містить шлях у реєстрі до драйвера, звернемо увагу, що шлях передається у форматі Unicode, далі в драйвері увесь час буде використовуватися цей формат. Тут ми повинні провести всю ініціалізацію, а саме:

У наведених вище пунктах були виконані мінімальні необхідні дії. Було створено один пристрій, яке є обов’язковим, але може бути не єдиним

Функція вивантаження драйвера

VOID myDriverUnload(IN PDRIVER_OBJECT pDriverObject)
Не має значення, що повертається, приймає в параметрах покажчик на об’єкт драйвера. Цю функцію, як і попередню, викликає сама операційна система, а виконати тут потрібно, відповідно протилежні дії - зруйнувати символьний зв’язок імен і видалити об’єкт пристрою:
PDEVICE_OBJECT deviceObject = pDriverObject->DeviceObject;
UNICODE_STRING uniWin32NameString;
RtlInitUnicodeString( &uniWin32NameString, L"\\DosDevices\\MyDriver");
IoDeleteSymbolicLink( &uniWin32NameString ); // зв’язок зруйнований
IoDeleteDevice( deviceObject ); // об’єкт пристрою видалений

Функція створення/відкриття пристрою користувачем

NTSTATUS myCreate(IN PDEVICE_OBJECT pDeviceObject,IN PIRP Irp)
До неї передається покажчик на об’єкт пристрою й покажчик на пакет запиту вводу-виводу IRP (Input/output Request Packet). У найпростішому випадку потрібно тільки успішно завершити запит:
Irp->IoStatus.Status=STATUS_SUCCESS; // статус запиту
Irp->IoStatus.Information=0; // кількість байт переданої інформації
IoCompleteRequest(Irp,IO_NO_INCREMENT); // завершення запиту
return STATUS_SUCCESS; // функція виконана успішно

Функція закриття пристрою

NTSTATUS myClose(IN PDEVICE_OBJECT pDeviceObject,IN PIRP Irp)
Має аналогічний вигляд, і виконаємо ми в ній точно такі ж дії, як в попередній.

Функція інформаційного обміну драйвера з користувачем

NTSTATUS myDispatch(IN PDEVICE_OBJECT pDeviceObject,IN PIRP Irp)
До неї теж передається покажчик на об’єкт пристрою й покажчик на IRP. Тут, через буфери даних, буде проходити вся основна робота, наприклад:
PIO_STACK_LOCATION pIrpStack; // покажчик на стек IRP
PUSHORT pIBuffer, pOBuffer; // покажчики на вхідний і вихідний буфери
USHORT val;
pIrpStack = IoGetCurrentIrpStackLocation(Irp); // одержання покажчика на стек
pIBuffer = (PUSHORT)(Irp->AssociatedIrp.SystemBuffer);
// одержання покажчика на вхідний буфер
pOBuffer = (PUSHORT)(Irp->UserBuffer); // і вихідний
switch (pIrpStack->Parameters.DeviceIoControl.IoControlCode) // код операції
{
case IOCTL_MYCODE1: // коди визначає розроблювач
    val = *pIBuffer; // забираємо слово із вхідного буфера
    *pOBuffer = val*val; // зводимо в квадрат й поміщаємо у вихідний
    Irp->IoStatus.Information=0; // кількість переданих користувачеві байт (прокоментований нижче)
    break;
}
Irp->IoStatus.Status=STATUS_SUCCESS; // запит виконаний успішно
IoCompleteRequest(Irp,IO_NO_INCREMENT); // завершення запиту
return STATUS_SUCCESS; // функція завершена успішно

У цьому прикладі драйвер одержує від користувача число типу USHORT і повертає йому квадрат цього числа в тім же форматі. Кількість переданих користувачеві байт задається рівним нулю, це значення одержить користувач, тут є важлива особливість - перед передачею керування програмі користувача це число байт буде скопійовано із вхідного буфера (pIBuffer) у вихідний (pOBuffer) автоматично. У такий спосіб при заданому методі обміну METHOD_BUFFERED (описано нижче) можна навіть не одержувати адресу вихідного буфера, а працювати тільки із вхідним, котрий попередньо буде скопійований в системний (і наші дані в програмі користувача не затруться) і вказати кількість байт для копіювання, розмір виділеного системного буфера визначається як максимальний з розмірів вхідного й вихідного, заданих в користувальницькому запиті. Якщо при звертанні до буфера вийти за його границі - тут уже не буде звичайної помилки Windows, ми одержимо BSOD (Blue Screen Of Death - синій екран смерті) і перезапуск Windows. Таким чином, для варіанта роботи із системним буфером важливо не тільки виділити достатні буфери користувача, але й правильно вказати їх розмір
Коди операцій - визначаються програмістом, формуються за допомогою макросу CTL_CODE( DeviceType, Function, Method, Access ) [7], що повертає 32-розрядне число, цей макрос описаний у файлі WinIoCtl.h (досить підключити windows.h). Компонент, що найбільш інтересує нас тут - сам код функції Function, під нього виділено 12 біт і він повинен бути в діапазоні від 0x800 до 0xFFF, значення нижче 0x800 зарезервовані для Windows [4]. Наш код операції сформований у такий спосіб:
#define IOCTL_MYCODE1 CTL_CODE (FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
Всі необхідні визначення типів і констант драйвера - у єдиному файлі, що підключається:
#include "ntddk.h"

Зв’язок з програмою користувача

У загальному виді робота буде складатися з послідовності:
[Відкриття пристрою] [обмін] ... [обмін] [закриття пристрою]. На рис.1 показана відповідність функцій користувача й драйвера при викликах

Рисунок 1 — Зв’язок драйвера з програмою користувача
Почнемо з відкриття пристрою - функція CreateFile. Основний параметр - ім’я драйвера в просторі імен WIN32 у форматі \\.\ім’я, функція повертає дескриптор пристрою у випадку успіху, або INVALID_HANDLE_VALUE у випадку невдачі. Для нашого приклада:
HANDLE hDrv;
hDrv = CreateFile("\\\\.\\MyDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
Останній параметр є покажчиком на Шаблон яким для драйвера був би додатковий опис пристрою. У нас він заданий NULL, тобто не використовується, тому що в нас створене всього один пристрій
Функція обміну [6]:
BOOL WINAPI DeviceIoControl(
    HANDLE hDevice, // дескриптор нашого пристрою
    DWORD dwIoControlCode, // код операції
    LPVOID lpInBuffer, // покажчик на буфер даних що відправляються драйверу
    DWORD nInBufferSize, // розмір цього буфера
    LPVOID lpOutBuffer, // покажчик на буфер для одержуваних даних
    DWORD nOutBufferSize, // його розмір
    LPDWORD lpBytesReturned, // покажчик на змінну, у яку запишеться кількість отриманих байт
    LPOVERLAPPED lpOverlapped); // додатковий параметр, розглядати не будемо

Для нашого приклада обоє буфери повинні мати розмір 2 байти (одна змінна типу USHORT), виконаємо обмін:
USHORT toDriver = 5,fromDriver;
DWORD cbRet;
DeviceIoControl(hDrv,IOCTL_MYCODE1, &toDriver,2,&fromDriver,2,&cbRet,NULL);
Тепер змінна fromDriver містить число 25.
Функція закриття пристрою приймає єдиний параметр - дескриптор пристрою:
CloseHandle(hDrv);
Тепер пристрій закритий, і ми не можемо звертатися до нього по дескриптору hDrv.

Зборка

Для зборки драйверів необхідно встановити пакет Windows Driver Development Kit (WinDDK). Пакет запускається як консольний додаток (рис. 2).

Рисунок 2 — Запуск WinDDK
Як видно з рисунка, для різних версій Windows свої збирачі. Checked Environment означає налагоджувальну зборку, Free - релізову. Після запуску ми перебуваємо в кореневій папці WinDDK, звідти ми повинні добратися до підготовленої папки нашого драйвера що розроблюється, за допомогою команд cd <папка>, і ввести команду build [5]. У випадку успіху одержимо наприкінці повідомлення 1 executable built, і в одній з підпапок, створених уже самим WinDDK, ми одержимо наш виконавчий файл драйвера, що має розширення *.sys (у даному прикладі - у папці i386, що витікає з рядка Linking Executable - i386\driv.sys). У випадку невдачі будуть наведені помилки із вказівкою номерів рядків у вихідному файлі. Перейдемо до папки драйвера - вона обов’язково повинна містити два файли: makefile і sources (без розширення). Вміст makefile незмінний, його потрібно скопіювати з будь-якого приклада, що додається до пакета (у даній версії, наприклад у папці WinDDK\7600.16385.1\src\input\HBtnKey\). Файл sources містить інформацію про вхідні й вихідні файли проекту, наприклад [3]:
TARGETNAME=driv - ім’я вихідного файлу (додасться .sys)
TARGETPATH=C:\WinDDK\7600.16385.1\MY_TESTS - директорія
TARGETTYPE=DRIVER - тип об’єкта що створюється - драйвер
SOURCES=driv.c - файл із початковим кодом драйвера.
Приклад зборки (скриншот) наведений на рис. 3.

Рисунок 3 — Зборка драйвера

Установка

Установка може бути статичною й динамічною. Статична - відбувається при старті Windows і більше підходить для завершених драйверів. Динамічна - установка й деінсталяція відбувається в будь-який момент часу, цей спосіб зручний для налагодження.

Для статичної установки файл драйвера потрібно помістити в папку windows\system32\drivers, створити в реєстрі по адресі HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\ папку з ім’ям файлу драйвера, а в ній чотири описаних нижче ключі. Це можна зробити вручну, за допомогою regedit або ж за допомогою *.reg файлу, у якому потрібно прописати наступне:
REGEDIT4

[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\driv]
"Type"=dword:00000001
"Start"=dword:00000002
"ErrorControl"=dword:00000001
"Group"="Extended Base"
Де driv було ім’ям файлу нашого драйвера. Пробіл після першого рядка обов’язковий. Зазначений шлях до файлу драйвера є стандартним, можна задати інший, створивши додатковий строковий параметр ImagePath, що містить повний шлях. Отриманий файл потрібно виконати за допомогою подвійного клацання лівою клавішею миші, при цьому буде виданий запит підтвердження додавання інформації до реєстру, а після - буде видане повідомлення про успішне внесення інформації в реєстр.

Для динамічної установки потрібно створити програму - установник (або виконувати її в самому додатку що тестує). У реєстрі драйвери описувалися в папці сервісів, а даний метод заснований на відкритті менеджера сервісів і установці вручну. Розглянемо установку й деінсталяцію драйвера відразу на прикладі [1].
Першою справою потрібно відкрити менеджер сервісів:
SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
if(scm == NULL)
    return -1; // невдача - виходимо

Далі установка драйвера - створюємо сервіс:
SC_HANDLE Service = CreateService(scm, // відкритий дескриптор до SCManager
    "Driv", // ім’я сервісу - Driv
    "Driv", // ім’я для виводу на екран
    SERVICE_ALL_ACCESS, // бажаний доступ - повний
    SERVICE_KERNEL_DRIVER, // тип сервісу - драйвер ядра
    SERVICE_DEMAND_START, // тип запуску - викликаний
    SERVICE_ERROR_NORMAL, // як обробляється помилка
    "C:\\driv.sys", // шлях до файлу драйвера
    // Інші параметри не використовуються - укажемо NULL
    NULL, // не визначаємо групу завантаження
    NULL, NULL, NULL, NULL);

if(Service == NULL) // невдача
{
    DWORD err = GetLastError();
    if(err == ERROR_SERVICE_EXISTS)
        {/* драйвер уже встановлений */}
    else
        return -1; // інша помилка - вихід
}
Далі два випадки - або сервіс був успішно створений і отриманий його дескриптор, або сервіс уже був установлений і дескриптор не був отриманий. Для другого випадку його потрібно відкрити, у загальному виді можна просто закрити й відкрити:
CloseServiceHandle(Service); // закриваємо сервіс
Service = OpenService(scm, "Driv", SERVICE_ALL_ACCESS); // відкриваємо сервіс
if(Service == NULL)
    return -1; // помилка відкриття - вихід
Тепер запускаємо сервіс:
BOOL ret = StartService(Service, // дескриптор сервісу
    0, // число аргументів (у нашім випадку їх немає)
    NULL); // покажчик на аргументи (не використовуємо)
if(! ret) // невдача
{
    DWORD err = GetLastError();
    if(err == ERROR_SERVICE_ALREADY_RUNNING)
        { /* драйвер уже був запущений */ }
    else
    {
        ret = DeleteService(Service); // видаляємо (вивантажуємо) драйвер
        if(!ret)
        { /* невдача при видаленні драйвера */ }
        return -1; // помилка запуску - вихід
    }
}
Тепер ми можемо працювати із драйвером. По закінченню його потрібно зупинити і вивантажити. Зупиняємо драйвер:
SERVICE_STATUS serviceStatus;
ret = ControlService(Service, SERVICE_CONTROL_STOP, &serviceStatus);
if(! ret) // помилка
{
    DWORD err = GetLastError();
    // тут можна виконати додаткову діагностику
}
Вивантажуємо драйвер:
ret = DeleteService(Service);
if(! ret)
    { /* невдача при видаленні драйвера */ }
CloseServiceHandle(Service); // закриваємо сервіс
Тепер драйвер повністю вилучений з Windows.

Вступ у переривання

Процедури обробки переривання звуться ISR (Interrupt Service Routine). В Windows всі завдання мають пріоритети, у їхньому ланцюжку Додатки користувача займають нижче місце, за ними ідуть програми режиму ядра (драйвери й т.д.), і самі пріоритетні - переривання [2]. Внаслідок цього самі оброблювачі переривань не дозволяють виконуватися всім іншим, що може приводити до так званої деградації системи, для цього були введені процедури відкладеного виклику DPC (Deferred Procedure Call), які відносяться до рівня програм ядра. Внаслідок цього якщо обробка переривання може забирати значний час, то вона переноситься в спеціальну DPC процедуру, а оброблювач переривання лише залишає запит на її виклик. DPC процедури прив’язуються до об’єктів пристроїв, і запити на їхні виклики заносяться саме через ідентифікатори пристроїв. Для переривань уведені спеціальні об’єкти, через які виробляється їх відв’язка від ISR, відв’язку потрібно робити вручну. Якщо, наприклад, прив’язати до переривання ISR і, не відв’язуючи вивантажити драйвер - по наступному відповідному перериванню буде виконане звертання до недійсної області пам’яті, і, скоріше всього, перезапуск системи

Об’єкт переривання

Об’єкти переривання описуються структурами KINTERRUPT, які створюються функцією підключення переривання до оброблювача - IoConnectInterrupt, у функцію потрібно передати апаратний вектор переривання й рівень переривання. Розглянемо на прикладі. Ми перебуваємо в процедурі DriverEntry, зареєстрували пристрій, підключаємо оброблювач переривання:
KAFFINITY Affinity; // структура, відповідальна за прив’язку до логічного процесора
ULONG MappedVector; // апаратний вектор переривання
PKINTERRUPT pInterruptObject; // покажчик на об’єкт переривання
ULONG InterruptLevel = 7; // IRQ7
KIRQL irql = InterruptLevel; // апаратний рівень використовуваних переривань
MappedVector = HalGetInterruptVector(Isa, // тип системної шини - ISA
    0, // номер шини
    InterruptLevel, // шинний рівень переривання
    InterruptLevel, // шинний вектор переривання (для Isa - однакові)
    &irql, // рівень переривання - вихідний параметр
    &Affinity); // прив’язка до процесора - вихідний параметр

// апаратний вектор переривання отриманий, реєструємо оброблювач
NTSTATUS status; // статус виконання прив’язки
status = IoConnectInterrupt(&pInterruptObject, // одержимо об’єкт переривання
    IsrRoutine, // наш оброблювач переривання
    pDeviceObject, // додатковий параметр, переданий при виклику ISR
    NULL, // спин-блокування - не використаємо
    MappedVector, // апаратний вектор
    irql, // рівень переривання
    irql,
    Latched, // тип переривання - по фронту
    FALSE, // не загальнодоступний вектор
    Affinity, // прив’язка до процесора
    FALSE); // не зберігаємо FPU, MMX регістри при виклику
if(status != STATUS_SUCCESS)
{ // помилка
}
else
    // успіх, тепер переривання підключене до обробника.
Тут ми підключили переривання на IRQ7 шини ISA(0), що, в основному, відповідає портам LPT1, LPT2.
По завершенню роботи потрібно відключити переривання:
IoDisconnectInterrupt( pInterruptObject ); // функція нічого не повертає
Функція оброблювача переривання (ISR) повинна мати вигляд:
BOOLEAN IsrRoutine(IN PKINTERRUPT pInterrupt,IN PVOID pContext);
Вона приймає покажчик на об’єкт самого переривання (чим дає можливість зробити відв’язку в самій обробці), і контекст пристрою - покажчик без типу, це той самий додатковий параметр, переданий при виклику який був зазначений в IoConnectInterrupt, у цьому випадку це буде pDeviceObject.

Процедура відкладеної обробки (DPC)

DPC, як було сказано, прив’язується до об’єкта пристрою:
IoInitializeDpcRequest(pDeviceObject, // покажчик на об’єкт пристрою
    DpcRoutine); // процедура відкладеного виклику
Функція нічого не повертає. Сама DPC повинна мати вигляд:
VOID DpcRoutine(IN PKDPC Dpc,IN PDEVICE_OBJECT pDeviceObject,IN PIRP pIrp,IN PVOID pContext);
Вона одержує структуру, що містить інформацію про відкладений виклик, її об’єкт пристрою, покажчик на пакет вводу/виводу (IRP), додатковий параметр на розсуд програміста
Тепер зареєструвати виклик можна наступним викликом:
IoRequestDpc(pDeviceObject, // покажчик на об’єкт пристрою
    NULL, // покажчик на IRP пакет - не використовуємо
    NULL); // наш додатковий параметр - не використовуємо
Після цього DPC поставлена в чергу на виконання. Як видно - єдиний обов’язковий параметр це посилання на об’єкт пристрою, властиво без нього й невідомо що викликати. Зберігаючи потрібні дані в розширенні пристрою можна практично не використати інші параметри

Експерименти

Був створений пристрій, що генерує випадкові числа по запитах від комп’ютера. Воно підключається через порт LPT1 і використовує переривання, наведене в прикладі вище, для ефективності роботи системи, тому що LPT порт працює на частоті до 100 кгц, а пристрій - до 15 кгц, драйвер залишає запит на нове число й віддає керування системі, не очікуючи даремно готовності пристрою. По готовності пристрій виставляє дані й генерує переривання, що обробляється по описаній вище схемі. Схема зв’язків і взаємодій наведена на рис. 4.

Рисунок 4 — Схема зв’язків і взаємодій (анімація, 17 кадрів, 85.8 кБ, інтервал між кадрами 0.4 с)

Циклічний буфер використовується для зручного одночасного читання й запису в нього, на випадок, якщо в момент читання програмою прийде переривання. Таким чином, у буфера є не тільки довжина, а і його початок (хвіст) - читання відбувається з хвоста, а запис із голови, що після останнього зсуву в буфері переходить у його початок (нульовий зсув) - аналогічно тому, як зроблений буфер клавіатури В DOS.

Споконвічно в регістрі даних LPT-порту передбачався запис із боку комп’ютера й відповідно читання з боку зовнішнього пристрою, тому тут необхідно використати двонаправлений режим передачі даних - установлюється режим роботи EPP в bios. Фото пристрою наведені на рис. 5,6.

Рисунок 5 — Фото пристрою (верхня сторона)
Рисунок 6 — Фото пристрою (нижня сторона)
Скриншоти програми тестування пристрою наведені на рис. 7-9.
Рисунок 7 — Тестування пристрою із включеним живленням (перші запити)
Рисунок 8 — Тестування пристрою (наступні запити)
Рисунок 9 — Тестування пристрою після вимикання живлення

Програма що тестує виводить отримані випадкові числа у вигляді графіка, з’єднуючи лініями крапки, відповідним значенням отриманих чисел і відображені з інтервалом у три пікселя. При старті роботи пристрою завжди спостерігається деяка ініціалізація (маленький розкид значень) тривалістю близько 10-15 чисел, що відповідає приблизно однієї мілісекунді після першого запиту до пристрою. Після відключення живлення контролер продовжує працювати, одержуючи +5В від LPT-порту, але інша частина схеми вже не може працювати коректно, АЦП видає на виході послідовність даних, схожих на прямокутні імпульси. Судячи із цього в деяких випадках можна не підключати додаткового живлення до малопотужних пристроїв, залежно від схем. Проміжні читання з порту даних показали, що між запитами значення в ньому не міняється, як і повинне бути - буфер швидко поповнюється, пристрій чекає запитів від драйвера, драйвер чекає запитів від програми користувача

Висновок

На даний момент був проведений аналіз структури драйверів Windows NT, було розроблено й зібрано пристрій, що підключається через LPT-порт, написаний його драйвер, і над ними були проведені тести, що підтвердили працездатність обох. При написанні реферату магістерська робота ще не завершена, планований час завершення — грудень 2012 року. Далі планується розробити драйвер USB пристрою, і розширити наявну методику написання драйверів

Перелык посилань

  1. Солдатов В.П. Программирование драйверов WINDOWS, Изд. 2-е, перераб. и доп. – М.: ООО Бином-Пресс, 2004 г. – 480 с: ил.
  2. Рудаков П.И, Финогенов К.Г. Язык ассемблера: уроки программирования. – М.:Диалог-МИФИ, 2001. – 640 с.
  3. Пишем первый драйвер. Часть 1. Интернет ресурс. Режим доступу: http://www.pcports.ru/articles/ddk2.php
  4. Пишем первый драйвер. Часть 2. Интернет ресурс. Режим доступу: http://www.pcports.ru/articles/ddk3.php
  5. Пишем первый драйвер. Часть 3. Интернет ресурс. Режим доступу: http://www.pcports.ru/articles/ddk4.php
  6. Тестируем драйвер на практике. Интернет ресурс. Режим доступу: http://www.pcports.ru/articles/ddk5.php
  7. Общая архитектура Windows NT. Интернет ресурс. Режим доступу: http://www.tisbi.org/resource/lib/Eltext/BookPrSistSecurity/Glava%202/GL2.htm