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

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

Реферат по теме выпускной работы

Содержание

Введение

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

Цель и задачи исследования, планируемые результаты

Целью исследования является разработка методики написания драйверов для операционных систем 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