Предыстория
Для очередного проекта возникла необходимость написать простенький софтверный драйвер под Windows, но так как опыта в написании драйверов у меня примерно столько же, сколько и в балете, я начал исследовать данную тему. В таких делах я предпочитаю начинать с основ, ибо если кидаться сразу на сложные вещи, то можно упустить многие базовые понятия и приёмы, что в дальнейшем только усложнит жизнь.
После 20 минут поисков по сети я наткнулся на Github Павла Иосифовича (zodiacon — Overview). Личность легендарная в своих кругах, достаточно посмотреть на его репозиторий, публикации и выступления на именитых конференциях. Помимо этого, Павел является автором/соавтором нескольких книг: «Windows Internals» (книга, имеющаяся у меня на полке, которая принесла немало пользы), и «Windows Kernel Programming» 2019 года выпуска (бегло пролистав 11 Глав или 390 страниц, я понял – это то, что нужно!).
Кстати, книгу вы можете купить прямо на сайте Павла
Ссылка скрыта от гостей
Книгу я приобрёл в бумажной версии, чтобы хоть и немного, но поддержать автора. Безупречное качество, несмотря на то, что она издается в мягком переплете. Хорошие плотные листы формата А4 и качественная краска. (книга без проблем пережила вылитую на нее кружку горячего кофе).
Пока я сидел на балконе и читал четвёртую главу книги, в голову пришла мысль: а почему бы не сделать ряд статей на тему «Программирования драйвера под Windows», так сказать, совместить полезное, с еще более полезным.
И вот я здесь, пишу предысторию.
Как я вижу этот цикл статей и что от него ожидать:
Это будут статьи, которые будут базироваться на вышеупомянутой книге, своеобразный вольный и сокращенный перевод, с дополнениями и примечаниями.
Базовые понятия о внутреннем устройстве Windows (Windows Internals)
Для того, чтобы начать разрабатывать Драйвер под Windows, то есть работать на уровне с ядром ОС, необходимо базовое понимание того, как эта ОС утроена. Так как я хочу сосредоточиться на написании драйвера, а не на теории об операционных системах, подробно описывать понятия я не буду, чтобы не растягивать статью, вместо этого прикреплю ссылки для самостоятельного изучения.
Следовательно, нам стоит ознакомиться с такими базовыми понятиями как:
Ссылка скрыта от гостей
Процесс – это объект, который управляет запущенной инстанцией программы.
Ссылка скрыта от гостей
Технология, позволяющая создавать закрытые пространства памяти для процессов. В своем роде — это песочница.
Ссылка скрыта от гостей
Это сущность, которая содержится внутри процесса и использует для работы ресурсы, выделенные процессом — такие, как виртуальная память. По сути, как раз таки потоки и запускают код.
Ссылка скрыта от гостей
В своем роде это прокладка, которая позволяет программе отправлять запросы в Ядро операционной системы, для выполнения нужных ей операций.
Ссылка скрыта от гостей
Это сложно описать словами коротко, проще один раз увидеть картинку.
В упрощённом виде это выглядит так:
Ссылка скрыта от гостей
Дескрипторы и объекты необходимы для регулирования доступа к системным ресурсам.
Объект — это структура данных, представляющая системный ресурс, например файл, поток или графическое изображение.
Дескриптор – это некая абстракция, которая позволяет скрыть реальный адрес памяти от Программы в пользовательском режиме.
Для более глубокого понимания Операционных систем могу посоветовать следующие материалы:
Книги:
- Таненбаум, Бос: Современные операционные системы
- Windows Internals 7th edition (Part 1)
Видео:
Настройка рабочего пространства
Для разработки драйвера, как и любого другого софта необходима подходящая среда.
Так как мы работаем в операционной системе Windows, её средствами мы и будем пользоваться.
Что нам понадобится:
1. Visual Studio 2017 и старше.
(Community Version хватает с головой) Также во вкладке „Individual components” необходимо установить
Код:
MSVC v142 - VS 2019 C++ ARM build tools (Latest)
MSVC v142 - VS 2019 C++ ARM Spectre-mitigated libs (Latest)
MSVC v142 - VS 2019 C++ ARM64 build tools (Latest)
MSVC v142 - VS 2019 C++ ARM64 Spectre-mitigated libs (Latest)
MSVC v142 - VS 2019 C++ ARM64EC build tools (Latest - experimental)
MSVC v142 - VS 2019 C++ ARM64EC Spectre-mitigated libs (Latest - experimental)
MSVC v142 - VS 2019 C++ x64/x86 build tools (Latest)
MSVC v142 - VS 2019 C++ x64/x86 Spectre-mitigated libs (Latest)
и далее по списку.
2. Windows 10/11 SDK (последней версии)
Ссылка скрыта от гостей
Тут все просто. Качаем iso файл, монтируем и запускаем установщик.
3. Windows 10/11 Driver Kit (WDK)
Ссылка скрыта от гостей
В конце установки вам будет предложено установить расширение для Visual Studio. Обязательно установите его!
После закрытия окна установки WDK появится установщик Расширения VisualStudio
4. Sysinternals Suite
Ссылка скрыта от гостей
Скачайте и распакуйте в удобное для вас место. Это набор полезных утилит, которые пригодятся для исследования Windows, дебага драйвера и прочего.
5. Виртуальная Машина с Windows для тестов.
Выбор ПО для виртуализации на ваше усмотрение. Я буду использовать «VMware Workstation 16 pro».
Написанные драйверы лучше тестировать именно в виртуальной машине, так как Ядро — ошибок не прощает, и вы будете часто улетать в синий экран смерти.
После того, как все было установлено, пора запускать Visual Studio и начинать писать драйвер.
Создание проекта
Запускаем Visual Studio и создаем новый проект. Создадим пустой проект „Empty WDM Driver“
Называем его как душе угодно.
И вот он, наш свеженький чистенький проект для нашего первого драйвера.
Теперь необходимо создать cpp файл, в котором мы будем писать сам драйвер.
Вот и все. Настройку системы и среды мы закончили.
Первый драйвер
Сначала импортируем ntddk.h
эта одна из базовых библиотек для работы с ядром. Больше информации
Ссылка скрыта от гостей
. Как и у любой программы, у драйвера должна быть точка входа DriverEntry
, как функция Main
в обычной программе. Готовый прототип этой функции выглядит так
C++:
#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
/*
In_ это часть SAL(Source Code Ananotation Language) Аннотации не видимы для компилятора,
но содержат метаданные которые, улучшают анализ и чтение кода.
*/
return STATUS_SUCCESS;
}
Если мы попробуем собрать наш проект, то получим следующие ошибки и предупреждения.
В данном случае пункт 1 является следствием пунктов 2 и 3. Дело в том, что по дефолту в Visual Studio некоторые “предупреждения” расцениваются как ошибки.
Чтобы решить эту проблему есть 2 пути.
- Отключить эту фичу в Visual Studio, что делать не рекомендуется. Так как сообщения об ошибках могут быть полезны и сэкономят вам время и нервы в дальнейшем.
- Более правильный и классический метод это использовать макросы в c++. Как видно из сообщения с кодом C4100 объекты RegistryPath и DriverObject не упомянуты в теле функции. Подробнее
Ссылка скрыта от гостей
.
Для того, чтобы избавиться от предупреждений, и заставить наш код работать, стоит поместить объекты в макрос UNREFERENCED_PARAMETER(ObjectName)
C++:
include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
Теперь, если пересобрать проект, то мы увидим, что ошибка С220 и предупреждение C4100 пропали, но к ним на смену пришли LNK2019 и LNK1120. Однако это уже не ошибки компиляции — это ошибки линкера. А кто говорил что будет легко?
О том, что такое линкер можно почитать
Ссылка скрыта от гостей
.
Дело в том, что наша функция не представлена в стандартном линкере С++ и вообще она девушка капризная и хочет Си-линкер. Удовлетворим желание дамы и дадим ей то, чего она хочет.
Делается это просто. Перед функцией надо добавить extern "C"
так наш линкер будет понимать, что эта функция должна линковаться С-линкером.
Собираем проект заново и вуаля — Драйвер собрался.
Что на данный момент умеет наш драйвер? Сейчас это по сути пустышка, которая после загрузки, в случае успеха, вернет нам сообщения об удачном запуске. Давайте заставим его нас поприветствовать и проверим его работоспособность. Выводить сообщения мы будем при помощи функции KdPrint(());
да именно в двойных кавычках.
Итоговый код драйвера будет выглядеть так:
C++:
#include <ntddk.h>
//Указываем линкеру, что DriverEntry должна линковаться С-линкером
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
//Убираем варнинг C4100 и связанную с ним ошибку C220
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
//Выводим сообщение
KdPrint(("Hi Codeby, this is our first driver! Yuhu!n"));
return STATUS_SUCCESS;
}
Собираем или пересобираем драйвер.
Важно! Сборка драйвера должна происходить в режиме Debug!!!
После чего в папке нашего проекта мы сможем найти результаты нашего труда. Вы только посмотрите на него, какой маленький и хорошенький.
Но что делать дальше? Как проверить его работоспособность?
Для этого нам и понадобится наша виртуальная машина с Windows, но перед запуском на ней драйвера, нам придется проделать пару манипуляций. Дело в том, что в Windows есть встроенная защита, и если драйвер не подписан «нужной» подписью ака сертификатом, то драйвер просто не загрузится.
Дальнейшие действия нужно проделать в Windows на виртуальной машине.
Чтобы отключить эту проверку подписи, а точенее перевести Windows в тестовый режим, запустите cmd.exe от имени администратора и введите следующую команду bcdedit /set testsigning on
.
Перезагрузите виртуальную машину.
Если все прошло удачно, в правом нижнем углу вы увидите следующую надпись (2 нижнее строчки могут отличиться в зависимости от версии Windows)
Возвращаемся в папку с драйвером и копируем его в виртуальную машину. Теперь нам надо создать службу для запуска драйвер. Открываем консоль от имени администратора и вводим следующую команду:
sc create Name type= kernel binPaht= PATH_TO_DRIVER
в моем случае это выглядит так:
Также проверить успешность создания можно через реестр.
В той же консоли мы можем попробовать запустить нашу службу.
sc start CodebyDriver
Отлично, драйвер запустился и мы даже не улетели в синьку, а это всегда приятно. Теперь давайте проверим, выводится ли сообщение от драйвера.
Для этого нам необходимо провести подготовительные работы.
Создадим новый ключ в реестре и назовем его Debug Print Filter
.
В качестве значения задаем DWORD
с именем DEFAULT
и определяем данные для значения как 8
.
Перезагружаем виртуальную машину.
После перезапуска запускаем DebugView данный инструмент находится в архиве Sysinternals, который мы ранее скачали. Ее можно смело скопировать в виртуальную машину.
Запускаем DebugView от имени Администратора и ставим галочку “Capture Kerner”
Capture Win32 и Capture Global Win32 можно снять, если летит много сообщений.
Затем запускаем консоль от имени администратора и запускаем службу загрузки драйвера.
Все отработало отлично, и мы видим приветствие от нашего драйвера!
На этой приятной ноте первая статья из цикла заканчивается. В дальнейших статьях мы добавим функционала нашему драйверу, научим его выгружаться и получать данные.
Спасибо за чтение!
P.S: Я сам только начал изучать тему работы с драйверами. Так что если у вас есть предложения или правки по технической части статьи, прошу отписать в комментарии, чтобы я мог внести изменения в статью.
P.P.S: Как вы могли заметить, писать мы будем преимущественно на С++, посему могу посоветовать отличный канал с уроками по С++ — The Cherno.
В один прекрасный день меня попросили написать Драйвер. На тот день мои познания в C/C++ ограничивались программой Hellow Word, и поэтому на вопрос: «Напишешь?» я самоуверенно ответил: «Конечно». На следующий день я узнал, что на свете существуют MSDN и DDK. Вскоре я понял, что не все Windows одинаковые, оказалось, что мой драйвер должен работать под Win2000/NT. У меня ушло больше месяца на то, чтобы скомпилировать и запустить свой первый Драйвер. По правде сказать, это был не совсем мой Драйвер, а точнее — это был genport из NTDDK. Но радовался я так, как будто минимум написал свою ОС.
Недавно мне пришлось опять вернуться к Драйверу. И вот вчера я, наконец-то, сдал работающий драйвер и решил написать эту небольшую статью, для того, чтобы как-то систематизировать то, что я узнал, и, чтобы когда мне снова придется взяться за драйвер, было от чего отталкиваться.
Люди, знающие что такое IOCTL, DEVICE_EXTENSION, MajorFunction и DriverEntry не найдут здесь ничего нового. Эта статья для тех, кто, возможно, никогда не слышал слово ДДК, и кто до сего дня никогда не заглядывал в исходники драйверов. И еще, я буду довольно-таки подробно описывать многие, даже очевидные вещи, поэтому напомню о том, что данная статья рассчитана на людей с очень малым опытом программирования, какой был у меня, когда я занялся написанием драйверов.
Первое, с чем сталкивается программист, решивший заняться драйверами, это почти полное отсутствие русскоязычной литературы по данной теме. За год поисков мне удалось найти всего три книги на русском, в которых хотя бы немного говорится о драйверах:
————————
1. Свен Шрайбер «Недокументированные возможности Windows 2000». Издательство «Питер» 2002 год.
Здесь очень хорошо описан механизм динамической загрузки драйвера.
——————
2. П. И. Рудаков, К. Г. Финогенов «Язык ассемблера: уроки программирования» Диалог МИФИ 2001 год.
Очень полезная книга для того, что бы понять, как писать драйвера без всякий Wizard-ов.
——————————
3. Светлана Сорокина, Андрей Тихонов, Андрей Щербаков «Программирование драйверов и систем безопасности». Издательство «БХВ-Петербург» 2002 год.
Здесь хорошо описывается многоуровневая модель драйверов.
Я ни в коей мере не претендую как на полноту освещения темы написания драйверов, так и на 100% правильность и достоверность того, что здесь написано (но все приведенные здесь исходники проверены и являются работоспособными). Буду благодарен всем, приславшим мне (01_artem@mail.ru) или высказавшим в форуме свои замечания.
Итак, я обращаюсь к человеку, решившему написать Драйвер уровня ядра под Win2000/NT. Надеюсь, эти заметки помогут сэкономить кучу времени и сил.
Прежде всего, я бы не рекомендовал (исходя из собственного опыта) пользоваться различными библиотеками (типа NuMega и всякими другими визардами). В основном из-за того, что даже для написания простейшего драйвера необходимо хотя бы поверхностное представление о том, как он функционирует. И самый простой способ получить представление об этом — написать драйвер самому. Мне, например, не хватило терпения разобраться с NuMega, и даже оболочки функций динамической загрузки/выгрузки драйвера, предложенные Свен Шрайбером в своей книге, я предпочел переписать.
Итак, начнем. Для начала надо установить на компьютер Visul C++ 6.0, MSDN и NTDDK установку проводить желательно именно в этом порядке. Лично я пользуюсь редактором UltraEdit для работы с текстами драйверов, но, в принципе, исходный код драйвера можно набирать в любом текстовом редакторе, хоть в NotePad.
Создадим папку, в которой мы будем работать с драйвером (пусть это будет C:myDriver). В этой папке создадим 5 файлов:
1. myDrv.c
2. myDrv.h
3. myDrv.rc
4. MAKEFILE
5. SOURCES
Начнем с последнего файла. В SOURCES скопируйте следующее:
TARGETNAME=myDrv
TARGETPATH=obj
TARGETTYPE=DRIVER
SOURCES=myDrv.c MyDrv.rc
В MAKEFILE скопируйте следующее:
!INCLUDE $(NTMAKEENV)makefile.def
В myDrv.rc скопируйте следующее:
#include <windows.h>
#include <ntverp.h>
#define VER_FILETYPE VFT_DRV
#define VER_FILESUBTYPE VFT2_DRV_SYSTEM
#define VER_FILEDESCRIPTION_STR «Generic Port I/O»
#define VER_INTERNALNAME_STR «myDrv.sys»
#include «common.ver»
А вот так должен выглядеть myDrv.h:
#define FIRST_IOCTL_INDEX 0x800
#define FILE_DEVICE_myDRV 0x00008000
#define TEST_SMTH CTL_CODE(FILE_DEVICE_myDRV,
FIRST_IOCTL_INDEX + 101,
METHOD_BUFFERED,
FILE_ANY_ACCESS)
Теперь обратимся к тексту самого драйвера myDrv.c:
#include «ntddk.h»
#include «myDrv.h»
#include «parallel.h»
#define NT_DEVICE_NAME L»\Device\myDrv»
#define DOS_DEVICE_NAME L»\DosDevices\myDrv»
//структура расширения устройства
typedef struct _DEVICE_EXTENSION
{
PDRIVER_OBJECT DriverObject;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
HANDLE Handle;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
//прототипы функций
NTSTATUS
DriverDeviceControl(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp);
VOID
DriverUnload(IN PDRIVER_OBJECT DriverObject);
NTSTATUS
DriverOpen(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp);
NTSTATUS
DriverClose(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp);
/////////////////////////////////////////////////////////////////////////////////реализация функций
NTSTATUS
DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
PDEVICE_OBJECT deviceObject;
UNICODE_STRING deviceNameUnicodeString;
UNICODE_STRING deviceLinkUnicodeString;
PDEVICE_EXTENSION extension;
NTSTATUS ntStatus;
RtlInitUnicodeString(&deviceNameUnicodeString, NT_DEVICE_NAME);
ntStatus = IoCreateDevice(DriverObject,
sizeof (DEVICE_EXTENSION),
&deviceNameUnicodeString,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&deviceObject);
if (!NT_SUCCESS(ntStatus)) return ntStatus;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDeviceControl;
DriverObject->DriverUnload = DriverUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverOpen;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverClose;
extension = (PDEVICE_EXTENSION) deviceObject->DeviceExtension;
extension->DeviceObject = deviceObject;
extension->DriverObject = DriverObject;
// Create counted string version of our Win32 device name.
RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);
// Create a link from our device name to a name in the Win32 namespace.
ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString, &deviceNameUnicodeString);
if (!NT_SUCCESS(ntStatus))
{
IoDeleteDevice(deviceObject);
return ntStatus;
}
return STATUS_SUCCESS;
}
//———————————————————————————————-
VOID
DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING deviceLinkUnicodeString;
PDEVICE_EXTENSION extension;
PIRP pNewIrp = NULL;
ULONG m_size;
NTSTATUS ntStatus;
extension = DriverObject->DeviceObject->DeviceExtension;
// Create counted string version of our Win32 device name.
RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);
// Delete the link from our device name to a name in the Win32 namespace.
IoDeleteSymbolicLink(&deviceLinkUnicodeString);
// Finally delete our device object
IoDeleteDevice(DriverObject->DeviceObject);
}
//————————————————————————————————
NTSTATUS
DriverOpen(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
//————————————————————————————————
NTSTATUS
DriverClose(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
//————————————————————————————————
NTSTATUS
DriverDeviceControl(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
NTSTATUS ntStatus;
PIO_STACK_LOCATION irpStack;
PDEVICE_EXTENSION extension;
PULONG ioBuffer;
ULONG ioControlCode;
ULONG port = 0;
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
irpStack = IoGetCurrentIrpStackLocation(Irp);
extension = DeviceObject->DeviceExtension;
ioBuffer = Irp->AssociatedIrp.SystemBuffer;
ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
switch (ioControlCode)
{
case TEST_SMTH:
ioBuffer[0] =(ULONG)DriverEntry;//В буфер обмена адрес функции DriverEntry
Irp->IoStatus.Information = 4;
break;
default:
Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
break;
}
ntStatus = Irp->IoStatus.Status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return ntStatus;
}
Немного поясню содержимое файла myDrv.c. Итак, по порядку:
#define NT_DEVICE_NAME L»DevicemyDrv»
#define DOS_DEVICE_NAME L»DosDevicesmyDrv»- Эти строки служат для задания символических имен текстовыми строками с именами объекта устройства, который будет создан нашим драйвером.
Далее приведены прототипы используемых функций. Этим функциям можно дать произвольные имена, НО их сигнатура (состав параметров с типом возвращаемого значения) жестко заданы системой. [2]:Программная часть драйвера начинается с обязательной функции с именем DriverEntry(), которая автоматически вызывается системой на этапе загрузки драйвера. Эта функция должна содержать все действия по его инициализации. В качестве первого параметра наша функция получает указатель на объект драйвера типа PDRIVER_OBJECT.
При загрузке драйвера системы создается объект драйвера (driver object), олицетворяющий образ драйвера в памяти. С другой стороны, объект драйвера представляет собой структуру, содержащую необходимые для функционирования драйвера данные и адреса функций. В процессе инициализации драйвера (в DriverEntry()) создаются один или несколько объектов устройств (device object), олицетворяющие те устройства, с которыми будет работать драйвер. Этот самый device object необходим для правильного функционирования драйвера и создается (как и в нашем случае) даже тогда, когда драйвер не имеет отношения к какому-либо реальному устройству.
Далее, в первых строках DriverEntry мы определяем используемые в ней данные, в т.ч. указатель на device object, и две символьные строки UNICODE_STRING с именами устройств. Системные программы взаимодействуют с объектом устройства, созданным драйвером, посредством указателя на него. Однако для прикладных программ объект устройства представляется одним из файловых объектов, и обращение к нему осуществляется по имени (в приложении мы будем использовать функцию CreateFile()).
Надо иметь в виду, что объект устройства должен иметь два имени, одно — в пространстве имен NT, другое — в пространстве имен Win32. Эти имена должны представлять собой структуры UNICODE_STRING. Имена объектов устройств составляются по определенным правилам. NT-имя предваряется префиксом Device, а Win32-имя — префиксом ?? (или DosDevice). При указании имен в Си-программе знак обратной косой черты удваивается. Для того, чтобы указанное в программе драйвера имя можно было использовать в приложении для открытия устройства, следует создать символическую связь между обоими заданными именами устройств. Для этого используем функцию IoCreateSymbolicLink(). Следующая обязательная операция — создание объекта устройства — осуществляется вызовом функции IoCreateDevice(). Первый параметр этой функции — это указатель на объект драйвера, поступающий в DriverEntry(). Второй параметр определяет размер расширения устройства — структуры, которая служит для передачи данных между функциями драйвера (состав этой структуры произволен и полностью определяется разработчиком). Третий параметр — созданное ранее NT-имя устройства. Далее идут: тип устройства (FILE_DEVICE_UNKNOWN), специальные характеристики (0), FALSE означает, что у нас однопоточное устройство. Наконец, последний параметр является выходным — через него функция возвращает указатель на созданный объект устройства.
Далее необходимо занести в объект драйвера адреса основных функций, включенных программистом в текст драйвера. Массив MajorFunction является одним из элементов структурной переменной. В этот массив мы и заносим адреса основных функций (т.е. функций, которые вызываются системой автоматически в ответ на определенные действия приложения или устройства). Завершается функция оператором return с указанием кода успешного завершения.
Функция DriverUnload() вызывается при выгрузке драйвера. Здесь мы должны выполнить действия по удалению объекта устройства, созданного в DriverEntry().
Функции DriverOpen и DriverClose в нашем случае ничего не делают и возвращают просто STATUSS_SUCCESS. Кстати, все эти функции тоже могут иметь произвольные имена, но передаваемые им параметры строго фиксированы.
[2]:Вот мы и добрались до самой содержательной, с точки зрения прикладного программирования, функции DriverDeviceControl(). Эта функция вызывается каждый раз, когда драйверу приходит IRP-пакет с каким либо IOCTL_ кодом. Грубо говоря, IRP-пакет — это структура, передавая указатель на которую, приложение может общаться с драйвером (как, впрочем, и драйвер может общаться с другим драйвером). Более подробное описание того, что такое IRP-пакет, можно найти здесь http://www.lcard.ru/~gorinov/texts/irp99.html.
IRP-пакет содержит так называемый системный буфер, служащий для обмена информацией (переменная SystemBuffer). Таким образом, нам надо получить доступ к IRP-пакету, а через него к SystemBuffer. Для этого объявляем переменную irpStack типа указателя на стековую область ввода-вывода PIO_STACK_LOCATION, и, кроме того, переменная ioBuffer, в которую будет помещен адрес системного буфера обмена. В нашем случае тип этой переменной — PULONG, в действительности тип передаваемых данных может быть каким угодно. С помощью функции IoGetCurrentIrpStackLocation() в переменную irpStack помещается адрес стековой области ввода-вывода, а в переменную ioBuffer заносится адрес системного буфера из структуры IRP. Системный буфер входит в объединение (union) с именем AssociatedIrp, поэтому мы используем конструкцию Irp->AssociatedIrp.SystemBuffer. Конструкция switch-case анализирует содержимое ячейки IoControlCode и в зависимости от значения кода выполняет те или иные действия. В нашей программе только один код действия TEST_SMTH. Засылка в буфер обмена адреса функции DriverEntry() осуществляется через указатель на этот буфер. В переменную Irp->IoStatus.Information заносим количество (4) пересылаемых байт. Для завершения IRP-пакета вызываем IoCompleteRequest().
Итак, драйвер мы написали. Теперь надо его скомпилировать. Т.к. процесс компиляции идет из командной строки, то для этой цели гораздо удобнее пользоваться bat-файлом. Создадим небольшой bat-файл с именем, допустим, Crt.bat и со следующим содержанием:
%SystemRoot%system32cmd.exe /c «cd C:NTDDKbin&&setenv.bat C:NTDDK&&cd c:myDriver &&build -ceZ»
pause
NTDDK у меня установлено в корень С:, если у Вас по-другому, то вместо C:NTDDKbin и C:NTDDK пропишите полные пути к соответствующим папкам.
Итак, теперь запустим наш Crt.bat. После окончания компиляции в папке C:myDriverobjfrei386 находим готовый драйвер myDrv.sys. Наш драйвер пока умеет только лишь загружаться/выгружаться и по специальному запросу посылает приложению адрес одной из своих процедур.
Теперь займемся написанием приложения, работающего с нашим драйвером. Еще раз напоминаю, что мы работаем под Win2000. Эта ОС позволяет реализовать динамическую загрузку/выгрузку драйвера.
Точнее, динамическую загрузку/выгрузку служб (service), но т.к. в Win2000 в качестве службы можно рассматривать и драйвер, я буду использовать оба эти термина, подразумевая, в данной статье, наш драйвер.
Для загрузки и выгрузки драйверов используется диспетчер управления службами SC Manager (Service Control Manager). Прежде чем вы сможете работать с интерфейсом SC, вы должны получить дескриптор диспетчера служб. Для этого необходимо обратиться к функции OpenSCManager(). Дескриптор диспетчера служб необходимо использовать при обращении к функциям CreateServise() и OpenService(). Дескрипторы, возвращаемые этими функциями, необходимо использовать при обращении к вызовам, имеющим отношение к конкретной службе. К подобным вызовам относятся функции ControlService(), DeleteService() и StartService(). Для освобождения дескрипторов обоих типов используется вызов CloseServiceHandle().
Загрузка и запуск службы подразумевает выполнение следующих действий:
- Обращение к функции OpenSCManager() для получения дескриптора диспетчера
- Обращение к CreateServise() для того, чтобы добавить службу в систему. Если такой сервис уже существует, то CreateServise() выдаст ошибку с кодом 1073 (код ошибки можно прочитать GetLastError()) Данная ошибка означает, что сервис уже существует и надо вместо CreateServise() использовать OpenService().
- Обращение к StartService() для того, чтобы перевести службу в состояние функционирования.
- Если служба запустилась успешно, то можно вызвать CreateFile() для получения дескриптора, который мы будем использовать уже непосредственно при обращении к драйверу.
- И по окончании работы не забудьте дважды обратиться к CloseServiceHandle() для того, чтобы освободить дескрипторы диспетчера и службы.
Если на каком-то шаге этой последовательности возникла ошибка, нужно выполнить действия, обратные тем, которые вы успели выполнить до ошибки. Надо помнить о том, что при обращении к функциям, подобным CreateServise(), необходимо указывать полное имя исполняемого файла службы (в нашем случае полный путь и имя myDrv.sys).
Посмотрим на исходный текст простого консольного приложения, написанного на Visual C++ 6.0:
#include <conio.h>
#include «LoadDRV.h»
#include <string.h>
#include <stdio.h>
void main()
{
LPTSTR m_name = new char[20];
strcpy(m_name, «myDrv.sys»);
if (drvLoad(m_name)) TestSmth();
drvUnLoad(m_name);
delete m_name;
}
//——————————————————————————
//создаем и посылаем драйверу IRP-запрос
int TestSmth(void)//0x800 + 101
{
int test = 0;
DWORD ReturetLength = 0;
DeviceIoControl(hDevice, IOCTL_TEST_SMTH, NULL, 0,
&test, 4, &ReturetLength, NULL);
printf(«TestSmth= %in»,test);
return test;
}
///**************Функции динамической загрузки************************
bool drvLoad(char* name)
{
printf (name);
hSCManager=NULL;
hService=NULL;
bool status;
status=FALSE;
if(OpenManager())
{
if(drvCreateService(name))
{
if(drvStartService(name))
{
status=TRUE;
printf(«n Driver is now load…n»);
}
}
}
hDevice = CreateFile («//./myDrv», GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
return status;
}
//———————————————————————
bool OpenManager()
{
bool status;
status=FALSE;
if(hSCManager!=NULL)
{
CloseServiceHandle (hSCManager);
hSCManager=NULL;
}
hSCManager=OpenSCManager (NULL,NULL,SC_MANAGER_ALL_ACCESS);
if (hSCManager == NULL)
{
error(_OpenSCManager);
} else status=TRUE;
return status;
}
//———————————————————————
bool drvCreateService(PCHAR pDrvName)
{
LPTSTR lpBuffer;
lpBuffer = new char[256];
bool status = FALSE;
LPTSTR awPath; // путь к драйверу с именем pDrvName
// формируем путь к pDrvName, драйвер должен лежать рядом с exe-шником
GetCurrentDirectory(256, lpBuffer);
strcat(lpBuffer,»»);
strcat(lpBuffer,pDrvName);
awPath = lpBuffer;
hService = CreateService(hSCManager,pDrvName,pDrvName,SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,awPath,NULL,NULL,NULL,NULL,NULL);
if(!hService)
{
error(_CreateService);
status = drvOpenService(pDrvName);//Пытаемся открыть службу
}
else status=TRUE;
delete lpBuffer;
return status;
}
//———————————————————————
bool drvOpenService(PCHAR name)
{
bool status;
status=FALSE;
if(hService!=NULL) CloseService();
hService=OpenService(hSCManager,name,SERVICE_ALL_ACCESS);
if (!hService) error(_OpenService);
else status=TRUE;
return status;
}
//———————————————————————
bool drvStartService(PCHAR name)
{
bool status;
status=FALSE;
if(!StartService(hService,0,NULL))
{
error(_StartService);
printf(«Deleting service…»);
drvDeleteService(name)
}
else status=TRUE;
return status;
}
//———————————————————————
bool drvDeleteService(PCHAR name)
{
bool status;
status=FALSE;
CloseService();
if(!DeleteService(hService)) error(_DeleteService);
else status=TRUE;
return status;
}
//——————————————————————-
void CloseService()
{
CloseServiceHandle (hService);
hService=NULL;
}
//——————————————————————-
int drvUnLoad(PCHAR name)
{
int status;
status=FALSE;
if (hDevice!=INVALID_HANDLE_VALUE)
{
if(!CloseHandle(hDevice)) error(_CloseHandle);
hDevice=INVALID_HANDLE_VALUE;
}
if (hService)
{
status = ControlService(hService,SERVICE_CONTROL_STOP,&ServiceStatus);
if(!status) error(_SERVICE_CONTROL_STOP);
status = DeleteService(hService);
if(!status) error(_DeleteService);
status = CloseServiceHandle(hService);
if(!status) error(_CloseServiceHandle);
}
if(!CloseServiceHandle(hSCManager)) error(_CloseServiceHandle);
if (status) printf(«Driver Unload… SUCCESSn»);
return status;
}
//———————————————————————
void error(error_index erIndex)
{
DWORD err;
err=GetLastError();
switch(erIndex)
{
case _OpenSCManager:
printf(«OpenSCManager failed with Error=%in»,err);
break;
case _GetFullPathName:
printf(«GetFullPathName failed with Error=%in»,err);
break;
case _CreateService:
switch (err)
{
case 1073:
printf(«The specified service already exists.n»);
printf(«opening this service…»);
break;
default:
printf(«CreateService failed with Error=%in»,err);
}
break;
case _OpenService:
printf(«OpenService failed with Error=%in»,err);
break;
case _StartService:
printf(«StartService failed with Error=%in»,err);
break;
case _DeleteService:
printf(«DeleteService failed with Error=%in»,err);
break;
case _SERVICE_CONTROL_STOP:
printf(«SERVICE_CONTROL_STOP failed with Error=%in»,err);
break;
case _CreateFile:
printf(«CreateFile failed with Error=%in»,err);
break;
case _CloseHandle:
printf(«CloseHandle failed with Error=%in»,err);
break;
case _CloseServiceHandle:
printf(«CloseServiceHandle failed with Error=%in»,err);
break;
}
}
И содержимое h-файла этого приложения:
#define FIRST_IOCTL_INDEX 0x800
#define FILE_DEVICE_myDrv 0x00008000
#define TEST_SMTH CTL_CODE(FILE_DEVICE_myDrv,
0x800 + 101,
METHOD_BUFFERED,
FILE_ANY_ACCESS)
Это приложение загружает драйвер, файл которого лежит в одной папке, что и exe-файл данного приложения.
Посмотрим на исходный текст:
В main() мы создаем переменную с именем драйвера (myDrv.sys) и передаем это имя в функцию динамической загрузки драйвера drvLoad(), которая выполняет все необходимые действия по работе с менеджером служб, и в конце вызывает CreateFile(), которая возвращает дескриптор, нужный для работы с драйвером как файловым объектом. Этот дескриптор, в частности, используется при вызове функции DeviceIoControl.
Если драйвер загружен успешно, то вызываем функцию TestSmth(), внутри которой мы создаем и посылаем драйверу IRP-пакет (с помощью вызова DeviceIoControl()). Приняв этот пакет, наш драйвер возвращает адрес своей процедуры DriverEntry. После этого выгружаем драйвер. Все.
Итак, мы написали простейший (дальше некуда) драйвер и приложение, работающее с ним. В этой статье я активно цитировал материалы из книги П. И. Рудаков, К. Г. Финогенов, по возможности делая ссылки на нее в виде: [2].
Через некоторое время напишу о том, как на базе этого драйвера написать драйвер-фильтр для LPT-порта.
Артем.
написание драйвера для сканера (неполучается написать драйвер для primax colorado)
Модератор: Модераторы разделов
-
madtux
- Сообщения: 31
- ОС: Debian 5.2 Debian 6.1
написание драйвера для сканера
немогу написать программу для сканера primax colorado 1200p
пробовал готовые pxscan4.1 и primax_scan0.9beta
не хотят работать,сканер то lpt
занялся написанием сам,научил включать,выключать лампу.
выставлять разрешение,подтверждает(сканер)установку
передвигать лампу,тоже подтверждает
но при попытке сканировать выдает красную или серую страничку и усе
в зависимости от того какой байт порта читает.
Собственно вопрос:
Это я неправильно обращаюсь с epp или сканер дурит?
кто нибудь писал что нибуть для epp, подскажите
p.s выбросить сканер не подходит
пингвины размножаются делением на 2
-
Serg79
- Сообщения: 153
- Контактная информация:
Re: написание драйвера для сканера
Сообщение
Serg79 » 08.10.2007 08:49
В этом вопросе тебе сдесь врядли кто поможет, придется разбираться самому во всем.
Скорее всего, тебе для считывания данных надо использовать «DMA» а не читать данные непосредственно с порта ввода-вывода.
-
madtux
- Сообщения: 31
- ОС: Debian 5.2 Debian 6.1
Re: написание драйвера для сканера
Сообщение
madtux » 11.10.2007 18:53
поставил оффтоп98 драйвер сканера и softice
перехватил обмен порт драйвер,осталась одна проблема.
некоторые вычисления которые требует ccd
поэтому если кто знает кинте ссылку на библиотеку для работы с картинками на низком уровне
поставил оффтоп98 драйвер сканера и softice
перехватил обмен порт драйвер,осталась одна проблема.
некоторые вычисления которые требует ccd
поэтому если кто знает кинте ссылку на библиотеку для работы с картинками на низком уровне
пингвины размножаются делением на 2
-
vadiml
- Сообщения: 446
- ОС: fc12.x86_64
- Контактная информация:
Re: написание драйвера для сканера
Сообщение
vadiml » 21.10.2007 22:22
а возни с этим много? а то у меня тоже есть сканер с дровами только для w98 и w2k, я им в вмвари пользуюсь
просто если за неделю-другую вечеров это можно сделать, то я бы тоже взялся (у меня acer 602p, lpt версия, для scsi версии сканера дрова есть)
phenom x4 905e, asus m4a79 deluxe, 4 gb, ati x550, ati 4350, 2 x 17″ LCD
-
madtux
- Сообщения: 31
- ОС: Debian 5.2 Debian 6.1
Re: написание драйвера для сканера
Сообщение
madtux » 28.10.2007 12:58
Все зависит от версии CCD у меня главная проблема была какя ccd сидит в сканере,вообще цепляй lpt
варькин на файл.записываешь вывод в порт ,просишь сканировть .
потом приблуда на C выводит дамп у порт,сканер делает вид шо сканирует.
Вывод сканера конечно пропадает.
если сработало повторяешь lpt из под vmware в файл ,потом делаешь diff из нескольких.
постоянная часть это инициализция,переменная разрешение,область сканирования etc
времени доделать пока нет,но доделю для своего
Впринципе если уперется за неделю очень можно сделать
пингвины размножаются делением на 2
Ну вот мы и добрались до самого интересного — сейчас будем писать драйвер. Итак, приступим. Для удобства выполнения, всю последовательность действий по написанию драйвера я распишу по пунктам.
1. Создание директории проекта
Установив DDK, на Вашем компьютере в директории C:WINDDK2600.1106 должны появиться файлы DDK. В этой директории создадим папку, в которой будут храниться наши проекты. Назовем ее, например, MyDrivers.
В папке MyDrivers создадим папку FirstDriver — тут будет находится наш первый проект драйвера.
2. Подготовка файлов проекта
В папке FirstDriver создайте пустой текстовый файл и переименуйте его под именем FirstDriver.c
При попытке переименовки со сменой расширения файла, появляется следующее предупреждение:
Не обращаем внимания на это предупреждение, и нажимаем Да. При этом наш файл примет вид:
Если же никакого предупреждения не было и переименованный файл так и остался текстовым с именем FirstDriver.c и расширением .txt, то в настройках своийств папки, которые можно найти в Пуск-> Настройка-> Панель управления-> Свойства паки уберите галочку напротив пункта «Скрывать расширения для зарегестрированных типов файлов». Попробуйте еще раз и все должно быть в порядке.
Теперь нам надо добавить еще два очень важных файла в наш проект, без которых драйвер нам не сделать. Они называются makefile и sources (обратите внимание, у них нет расширения). Их можно создать самим, но мы сделаем проще: скопируем готовые из какого либо примера проекта драйвера из DDK. Например, возьмем их из C:WINDDK2600.1106srcgeneralcancelsys. Итак, копируем из указанной директории эти два файла и вставляем их в нашу папку проекта FirstDriver. Эти файлы управляют процессрм компиляции драйвера. Файл makefile оставляем без изменений, а вот sources надо подредактировать.
Открываем этот файл с помощью блокнота, и удаляем из него все что там есть и вставляем туда текст, представленный ниже. Сохранияем файл.
Первым параметром идет TARGETNAME, которому мы присвоили Port. Это значит, что когда DDK откомпилирует наш код и создаст драйвер, имя этого файла будет Port.sys Следующем параметром идет TARGETPATH, которому мы указали путь к папке нашего проекта. Если Вы устанавливали DDK в другое место, или создали пупку проекта в другой директории, здесь Вам надо это поправить на тот путь, который у Вас. Параметр TARGETTYPE пока оставлю без комментариев. В параметре SOURCES указываем, из каких файлов будет компилироваться драйвер. У нас это файл FirstDriver.c, вот мы его и указали.
Всю подготовительную работу мы сделали. Можно приступать к самой содержательной части — коду драйвера. Писать мы будем его на Си.
Еще раз напомню решаемую нами задачу: надо написать драйвер под Windows 2000, XP с помощью которого можно будет работать с портами компьютера (читать и писать данные) .
Рассмотрим код, представленный ниже. Серым цветом обозначены те участки кода, которые являются как бы шаблонными для большинства драйверов, а зеленым — код, который относится именно к нашему текущему драйверу.
Представленный код — есть код простейшего драйвера, который может только записать данные в порт и прочесть их от туда. Выглядит страшновато? Ни чего, в следующей статье будем разбираться с тем что в нем понаписано.
Вряд ли пользователь домашнего ПК заинтересуется тем, чтобы блокировать устройства на своем ПК. Но если дело касается корпоративной среды, то все становится иначе. Есть пользователи, которым можно доверять абсолютно во всем, есть такие, которым можно что-то делегировать, и есть те, кому доверять совсем нельзя. Например, вы заблокировали доступ к Интернету одному из пользователей, но не заблокировали устройства этого ПК. В таком случае пользователю достаточно просто принести USB-модем, и Интернет у него будет. Т.е. простым блокированием доступа к Интернету дело не ограничивается.
Однажды примерно такая задача и стояла передо мной. Времени на поиск каких-либо решений в Интернете не было, да и они, как правило, небесплатные. Поэтому мне было проще написать такой драйвер, и его реализация отняла у меня один день.
В этой статье я расскажу немного теоретическую часть, на основе которой все строится, и расскажу принцип самого решения.
Также полные исходные коды могут быть найдены в папке USBLock хранилища git по адресу: https://github.com/anatolymik/samples.git.
Структура DRIVER_OBJECT
Для каждого загруженного драйвера система формирует структуру DRIVER_OBJECT. Этой структурой система активно пользуется, когда отслеживает состояние драйвера. Также драйвер отвечает за ее инициализацию, в частности за инициализацию массива MajorFunction. Этот массив содержит адреса обработчиков для всех запросов, за которые драйвер может отвечать. Следовательно, когда система будет посылать запрос драйверу, она воспользуется этим массивом, чтобы определить, какая функция драйвера отвечает за конкретный запрос. Ниже представлен пример инициализации этой структуры.
Такая инициализация обычно выполняется при вызове системой точки входа драйвера, прототип которой изображен ниже.
Как видно из примера, сначала весь массив MajorFunction инициализируется одним и тем же обработчиком. В реальности типов запросов больше, чем в примере. Поэтому предварительно весь массив инициализируется так, чтобы запросы, которые не поддерживаются драйвером, обрабатывались корректно. Например, завершались с ошибкой. После инициализации массива обычно инициализируются обработчики для тех запросов, за которые драйвер отвечает.
Также инициализируется поле DriverUnload структуры, которое содержит адрес обработчика, отвечающего за завершение работы драйвера. Это поле может быть оставлено непроинициализированным, в таком случае драйвер становится невыгружаемым.
Обратите внимание на то, что в поле DriverExtension->AddDevice устанавливается адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство, за работу которого драйвер отвечает. Данное поле может быть оставлено непроинициализированным, в таком случае драйвер не сможет обрабатывать это событие.
Структура DEVICE_OBJECT
Структура DEVICE_OBJECT представляет ту или иную функциональность драйвера. Т.е. эта структура может представлять физическое устройство, логическое устройство, виртуальное устройство или просто некий функционал, предоставляемый драйвером. Поэтому когда система будет посылать запросы, то в самом запросе она будет указывать адрес этой структуры. Таким образом, драйвер сможет определить, какой функционал от него запрашивается. Если не использовать такую модель, тогда драйвер может обрабатывать только какую-нибудь одну функциональность, а в современном мире это недопустимо. Прототип функции, которая обрабатывает конкретный запрос, приведена ниже.
Массив MajorFunction ранее упомянутой структуры DRIVER_OBJECT содержит адреса обработчиков именно с таким прототипом.
Сама структура DEVICE_OBJECT всегда создается драйвером при помощи функции IoCreateDevice. Если система посылает запрос драйверу, то она всегда направляет его какому-либо DEVICE_OBJECT, как это следует из вышепредставленного прототипа. Также, прототип принимает второй параметр, который содержит адрес IRP-структуры. Эта структура описывает сам запрос, и она существует в памяти до тех пор, пока драйвер не завершит его. Запрос отправляется драйверу на обработку при помощи функции IoCallDriver как системой, так и другими драйверами.
Также со структурой DEVICE_OBJECT может быть связано имя. Таким образом, этот DEVICE_OBJECT может быть найден в системе.
Фильтрация
Фильтрация являет собой механизм, который позволяет перехватывать все запросы, направленные к конкретному DEVICE_OBJECT. Чтобы установить такой фильтр, необходимо создать другой экземпляр DEVICE_OBJECT и прикрепить его к DEVICE_OBJECT, запросы которого необходимо перехватывать. Прикрепление фильтра выполняется посредством функции IoAttachDeviceToDeviceStack. Все DEVICE_OBJECT, прикрепленные к перехватываемому DEVICE_OBJECT, вместе с ним формируют так называемый стек устройства, как это изображено ниже.
Стрелкой изображено продвижение запроса. Сначала запрос будет обрабатываться драйвером верхнего DEVICE_OBJECT, затем драйвером среднего и, в конце концов, управление на обработку запроса получит драйвер целевого DEVICE_OBJECT. Также нижний DEVICE_OBJECT называется дном стека, т.к. он ни к кому не прикреплен.
Наличие такого механизма позволяет добавлять функционал, которого нет изначально в драйверах. Например, таким образом, без доработки файловой системы FAT, поставляемой в Windows, можно добавить проверку прав доступа к файлам этой файловой системы.
PnP менеджер
PnP менеджер отвечает за диспетчеризацию устройств всей системы. В его задачи входит обнаружение устройств, сбор информации о них, загрузка их драйверов, вызов этих драйверов, управление аппаратными ресурсами, запуск и остановка устройств и их удаление.
Когда драйвер той или иной шины обнаруживает устройства на своих интерфейсах, то для каждого дочернего устройства он создает DEVICE_OBJECT. Этот DEVICE_OBJECT также называют Physical Device Object или PDO. Затем посредством функции IoInvalidateDeviceRelations он уведомляет PnP менеджер о том, что произошли изменения на шине. В ответ на это PnP менеджер посылает запрос с minor кодом IRP_MN_QUERY_DEVICE_RELATIONS с целью запросить список дочерних устройств. В ответ на этот запрос драйвер шины возвращает список PDO. Ниже изображен пример такой ситуации.
Как отражено на рисунке, в данном примере в качестве шины выступает драйвер USB-хаба. Конкретные DEVICE_OBJECT стека устройства этого хаба не изображены для краткости и с целью сохранения последовательности пояснений.
Как только PnP менеджер получит список всех PDO, он по отдельности соберет всю необходимую информацию об этих устройствах. Например, будет послан запрос с minor кодом IRP_MN_QUERY_ID. Посредством этого запроса PnP менеджер получит идентификаторы устройства, как аппаратные, так и совместимые. Также PnP менеджер соберет всю необходимую информацию о требуемых аппаратных ресурсах самим устройством. И так далее.
После того, как вся необходимая информация будет собрана, PnP менеджер создаст так называемую DevNode, которая будет отражать состояние устройства. Также PnP создаст ветку реестра для конкретного экземпляра устройства или откроет существующую, если устройство ранее подключалось к ПК.
Следующая задача PnP — это запуск драйвера устройства. Если драйвер не был ранее установлен, тогда PnP будет ожидать установки. Иначе, при необходимости, PnP загрузит его и передаст ему управление. Ранее упоминалось, что поле DriverExtension->AddDevice структуры DRIVER_OBJECT содержит адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство. Прототип этого обработчика изображен ниже.
Т.е. всякий раз, когда PnP обнаруживает устройство, управлением которого занимается тот или иной драйвер, вызывается зарегистрированный обработчик этого драйвера, где ему передается указатель на PDO. Информация об установленном драйвере также хранится в соответствующей ветке реестра.
В задачу обработчика входит создание DEVICE_OBJECT и его прикрепление к PDO. Прикрепленный DEVICE_OBJECT также называют Functional Device Object или FDO. Именно этот FDO и будет отвечать за работу устройства и представление его интерфейсов в системе. Ниже представлен пример, когда PnP завершил вызов драйвера, отвечающего за работу устройства.
Как отражено на примере, кроме драйвера самого устройства также могут быть зарегистрированы нижние и верхние фильтры класса устройства. Следовательно, если таковые имеются, PnP также загрузит их драйвера и вызовет их AddDevice обработчики. Т.е. порядок вызова драйверов следующий: сначала загружаются и вызываются зарегистрированные нижние фильтры, затем загружается и вызывается драйвер самого устройства, и в завершении загружаются и вызываются верхние фильтры. Нижние и верхние фильтры являются обычным DEVICE_OBJECT, которые создают драйвера и прикрепляют их к PDO в своих обработчиках AddDevice. Количество нижних и верхних фильтров не ограничено.
В этот момент стеки устройств полностью сформированы и готовы к работе. Поэтому PnP посылает запрос с minor кодом IRP_MN_START_DEVICE. В ответ на этот запрос все драйвера стека устройства должны подготовить устройство к работе. И если в этом процессе не возникло проблем, тогда запрос завершается успешно. В противном случае, если любой из драйверов не может запустить устройство, тогда он завершает запрос с ошибкой. Следовательно, устройство не будет запущено.
Также, когда драйвер шины определяет, что произошли изменения на шине, он посредством функции IoInvalidateDeviceRelations уведомляет PnP о том, что следует заново собрать информацию о подключенных устройствах. В этот момент драйвер не удаляет ранее созданный PDO. Просто при получении запроса с minor кодом IRP_MN_QUERY_DEVICE_RELATIONS он не включит этот PDO в список. Затем PnP на основании полученного списка опознает новые устройства и устройства, которые были отключены от шины. PDO отключенных устройств драйвер удалит тогда, когда PnP пошлет запрос с minor кодом IRP_MN_REMOVE_DEVICE. Для драйвера этот запрос означает, что устройство более никем не используется, и оно может быть безопасно удалено.
Суть решения
Суть самого решения заключается в создании верхнего фильтра класса USB-шины. Зарезервированные классы можно найти по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff553419(v=vs.85).aspx. Нас интересует класс USB с GUID равным 36fc9e60-c465-11cf-8056-444553540000. Как гласит MSDN, этот класс используется для USB хост контроллеров и хабов. Однако практически это не так, этот же класс используется, например, flash-накопителями. Это немного добавляет нам работы. Код обработчика AddDevice представлен ниже.
Как следует из примера, мы создаем DEVICE_OBJECT и прикрепляем его к PDO. Таким образом, мы будем перехватывать все запросы, направленные к USB-шине.
В нашу задачу входит перехватывать запросы с minor кодом IRP_MN_START_DEVICE. Код обработчика этого запроса изображен ниже.
Как изображено на рисунке, обработчик вызывает функцию UsbIsDeviceAllowedToWork. Эта функция выполняет все необходимые проверки, чтобы определить, разрешено ли устройству работать. В первую очередь функция позволяет всегда работать хабам и композитным устройствам, клавиатурам и мышам. А также тем устройствам, которые находятся в списке разрешенных. Если функция возвращает неуспешный код возврата, тогда запрос завершается с ошибкой. Таким образом, работа устройства будет заблокирована.
Обратите внимание: функция определяет, является ли устройство хабом или композитным устройством. Это необходимо потому, что, как уже было упомянуто, класс устройств, который используется для хабов и хост контроллеров, используется не только этими устройствами. А нам в первую очередь необходимо контролировать дочерние устройства только хабов, хост контроллеров и композитных устройств. Т.е. для хабов и композитных устройств дополнительно перехватывается запрос перечисления дочерних устройств, на этом этапе, важно также прикрепить ко всем дочерним устройствам фильтр, и этот фильтр будет нижним. В противном случае контроль над дочерними устройствами будет потерян.
Все упомянутые определения выполняются на основе идентификаторов устройств.
Заключение
Вы можете помочь и перевести немного средств на развитие сайта
Драйвер-это основа взаимодействия системы с устройством в ОС Windows.Это одновременно удобно и неудобно.
Про удобства я разъяснять не буду — это и так понятно,
а заострюсь я именно на неудобствах драйверов.
В сложившейся ситуации пользователь полностью подчинён воле производителя
— выпусти тот драйвер — хорошо, а не выпустит.
Только продвинутый пользователь, имеющий голову на плечах
(особенно, если он ешё и программер) не станет мириться с таким положением дел
— он просто возьмёт и сам напишет нужный драйвер.
Это нужно и взломщику: драйвер — это удобное окошко в ring0,
которое является раем для хакера. Но хоть написать драйвер и просто,
да не совсем — есть масса подводных камней. Да и документированность данного вопроса на русском языке оставляет желать лучшего.
Этот цикл статей поможет тебе во всём разобраться.
Приступим.
Хочу сразу же сделать несколько предупреждений.
Данная статья всё-таки подразумевает определённый уровень подготовки.
Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок.
Поэтому хорошее знание обоих языков весьма желательно (если не сказать — обязательно).
Если же ты пока не можешь этим похвастаться,
но желание писать драйвера есть — что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы,
ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду.
Может как-нибудь в другой раз. Согласен? Тогда поехали!
Скоро здесь, возможно, будет стоять твоё имя.
Практически в любом деле, как мне кажется, нужно начинать с теории.
Вот и начнём с неё. Для начала уясним себе поточнее основные понятия.
Первое: что есть драйвер? Драйвер — в сущности
кусок кода ОС, отвечающий за взаимодействие с аппаратурой.
Слово «аппаратура» в данном контексте следует понимать в самом широком смысле.
С момента своего появления как такого до сегодняшнего дня драйвер беспрерывно эволюционировал.
Вот, скажем, один из моментов его развития. Как отдельный и довольно независимый модуль драйвер сформировался не сразу.
Да и сейчас этот процесс до конца не завершён:
ты наверняка сталкивался с тем, что во многих
дистрибутивах никсов для установки/перестановки etc драйверов нужно перекомпилировать ядро,
т.е. фактически заново пересобирать систему.
Вот, кстати ещё один близкий моментец: разные принципы работы с драйверами в Windows 9x и NT.
В первом процесс установки/переустановки драйверов проходит практически без проблем,
во втором же случае это тяжёлое и неблагодарное дело,
для «благополучного» завершения которого нередко приходится прибегать к полной переустановке ОС.
А зато в Windows 9x. так,стоп,открывается широкая и волнующая тема,
которая уведёт меня далеко от темы нынешней статьи,
так что вернёмся к нашим баранам. ой,то есть к драйверам.
В порядке общего развития интересно сравнить особенности драйверов в Windows и *nix(xBSD) системах:
1) Способ работы с драйверами как файлами (подробнее см. ниже)
2) Драйвер, как легко заменяемая честь ОС (учитывая уже сказанные выше примечания)
3) Существование режима ядра
Теперь касательно первого пункта. Это значит,
что функции, используемые при взаимодействии с файлами,
как и с драйверами, практически идентичные (имеется в виду лексически):
open, close, read и т.д. И напоследок стоит отметить идентичность механизма
IOCTL (Input/Output Control Code-код управления вводом-выводом)
-запросов.
Драйвера под Windows делятся на два типа:
Legacy (устаревший) и WDM (PnP). Legacy драйверы (иначе называемые «драйверы в стиле
NT») чрезвычайно криво работают (если работают вообще)
под Windows 98, не работают с PnP устройствами, но зато могут пользоваться старыми функциями
HalGetBusData, HalGetInterruptVector etc, но при этом не имеют поддержки в лице шинных драйверов.
Как видишь, весьма средненький драйвер. То ли дело
WDM: главный плюс — поддержка PnP и приличненькая совместимость:
Windows 98, Me, 2000, XP, 2003, Server 2003 и т.д. с вариациями; но он тоже вынужден за это расплачиваться:
например, он не поддерживает некоторые устаревшие функции
(которые всё таки могут быть полезны). В любом случае,
не нужно ничего воспринимать как аксиому, везде бывают свои исключения.
В некоторых случаях лучше написания Legacy драйвера ничего не придумать.
Как ты наверняка знаешь, в Windows есть два мода работы:
User Mode и Kernel Mode — пользовательский режим и режим ядра соответственно.
Первый — непривилегированный, а второй — наоборот.
Вот во втором чаще всего и сидят драйвера (тем
более, что мы в данный момент говорим именно о драйверах режима ядра).
Главные различия между ними: это доступность всяких привилегированных команд процессора.
Программировать (а уж тем более качественно) в Kernel mode посложнее будет,
чем писать прикладные незамысловатые проги.
А драйвера писать без хорошего знания Kernel mode — никак.
Нужно попариться над назначением выполнения разнообразных работ отдельному подходящему уровню IRQL, желательно выучить новое API (так как в Kernel mode API отличается от прикладного).
в общем, предстоит много всяких радостей. Но тем не менее,
это очень интересно, познавательно, и даёт тебе совершенно иной уровень власти над компьютером.
А раз уж я упомянула про IRQL, разьясню и это понятие.
IRQL (Interrupt Request Level — уровень приоритета выполнения) — это приоритеты,
назначаемые специально для кода, работающего в режиме ядра.
Самый низкий уровень выполнения — PASSIVE_LEVEl. Работающий поток может быть прерван потоком только с более высоким
IRQL.
Ну и напоследок разъясним ещё несколько терминов:
1) ISR (Interrupt Service Routine) — процедура обслуживания прерываний.
Эта функция вызывается драйвером в тот момент,
когда обслуживаемая им аппаратура посылает сигнал прерывания.
Делает самые необходимые на первый момент вещи:
регистрирует callback — функцию и т.д.
2) DpcForISR (Deferred Procedure Call for ISR) — процедура отложенного вызова для обслуживания прерываний.
Эту функцию драйвер регистрирует в момент работы ISR для выполнения основной работы.
3) IRP (Input/Output Request Packet) — пакет запроса на ввод — вывод.
Пакет IRP состоит из фиксированной и изменяющейся частей.
Вторая носит название стека IRP или стека ввода — вывода (IO stack).
4) IO stack location — стек ввода — вывода в пакете IRP.
5) Dispatch Routines (Рабочие процедуры) — эти функции регистрируются в самой первой (по вызову) процедуре драйвера.
6) Major IRP Code — старший код IRP пакета.
7) Minor IRP Code — соответственно, младший код IRP пакета.
DriverEntry — эта функция драйвера будет вызвана первой при его загрузке.
9) Layering (Многослойность) — данной возможностью обладают только WDM — драйвера.
Она заключается в наличии реализации стекового соединения между драйверами.
Что такое стековое соединение? Для этого необходимо знать про Device
Stack (стек драйверов) — поэтому я обязательно вспомню про всё это чуточку ниже.
10) Device Stack, Driver Stack (стек устройств, стек драйверов) — всего лишь
объемное дерево устройств. Его, кстати, можно рассмотреть во всех подробностях с помощью программы
DeviceTree (из MS DDK), например.
11) Стековое соединение — как и обещала, объясняю. В стеке драйверов самый верхний драйвер — подключившийся позднее.
Он имеет возможность посылать/переадресовывать IRP запросы другим драйверам,
которые находятся ниже его. Воти всё. Правда,просто?
12) AddDevice — функция, которую обязательно должны поддерживать WDM драйверы.
Её название говорит само за себя.
13) Device Object, PDO, FDO (Объект устройства, физический,
функциональный) — при подключении устройства к шине она создаёт PDO.
А уже к PDO будут подключаться FDO объекты WDM драйверов.
Обьект FDO создаётся самим драйвером устройства при помощи функции IOCreateDevice.
Обьект FDO также может иметь свою символическую ссылку, от которой он будет получать запросы от драйвера.
Это что касается WDM драйверов. С драйверами «в стиле NT» ситуация несколько иная.
Если он не обслуживает реальных/PnP устройств,
то PDO не создаётся. Но для связи с внешним миром без FDO не обойтись.
Поэтому он присутствует и тут.
14) Device Extension (Расширение обьекта устройства) — «авторская» структура,
т.е. она полностью определяется разработчиком драйвера.
Правилом хорошего тона считается, например,
размещать в ней глобальные переменные.
15) Monolithic Driver (Монолитный драйвер) — это драйвер,
который самостоятельно обрабатывает все поступающие
IRP пакеты и сам работает с обслуживаемым им устройством
(в стеке драйверов он не состоит). Данный тип драйверов используется только если обслуживается не
PnР устройство или же всего лишь требуется окошко в ring0.
16) DIRQL (уровни аппаратных прерываний) —
прерывания, поступающие от реальных устройств, имеют наивысший приоритет IRQL,
поэтому для них решено было придумать специальное название
(Device IRQL).
17) Mini Driver (Мини — драйвер) — чуть меньше «полного» драйвера.
Обычно реализуется в виде DLL-ки и имеет оболочку в виде «полного» драйвера.
18) Class Driver (Классовый драйвер) — высокоуровневый драйвер,
который предоставляет поддержку класса устройств.
19) РnP Manager (PnP менеджер) — один из главных компонентов операционной системы.
Состоит из двух частей: PnP менеджера пользовательского и «ядерного» режимов.
Первый в основном взаимодействует с пользователем;
когда тому нужно, например, установить новые драйвера и т.д.
А второй управляет работой, загрузкой и т.д. драйверов.
20) Filter Driver (фильтр — драйвер) — драйверы, подключающиеся к основному драйверу либо сверху
(Upper), либо снизу (Lower). Фильтр драйверы (их может быть несколько) выполняют фильтрацию IRP пакетов.
Как правило, для основного драйвера Filter Drivers неощутимы.
21) Filter Device Object — объект устройства, создаваемый фильтр — драйвером.
22) HAL (Hardware Abstraction Layer) — слой аппаратных абстракций.
Данный слой позволяет абстрагироваться компонентам операционной системы от особенностей конкретной платформы.
23) Synchronization Objects (Обьекты синхронизации) — с помощью этих
объектов потоки корректируют и синхронизируют свою работу.
24) Device ID — идентификатор устройства.
25) DMA (Direct Memory Access) — метод обмена данными между устройством и памятью
(оперативной) в котором центральный процессор не принимает участия.
25) Polling — это особый метод программирования, при котором не устройство посылает сигналы прерывания драйверу,
а сам драйвер периодически опрашивает обслуживаемое им устройство.
26) Port Driver (Порт-драйвер) — низкоуровневый драйвер,
принимающий системные запросы. Изолирует классовые драйверы устройств от аппаратной специфики последних.
Ну вот, пожалуй, и хватит терминов. В будущем,
если нужны будут какие-нибудь уточнения по теме,
я обязательно их укажу. А теперь, раз уж эта статья
теоретическая, давай-ка взглянем на архитектуру Windows NT с высоты птичьего полёта.
Краткий экскурс в архитектуру Windows NT
Наш обзор архитектуры Windows NT мы начнём с разговора об уровнях разграничения привилегий. Я уже упоминала об user и kernel mode.
Эти два понятия тесно связаны с так называемыми кольцами (не толкиеновскими ).
Их ( колец) в виде всего четыре: Ring3,2,1 и 0. Ring3 — наименее привилегированное кольцо,
в котором есть множество ограничений по работе с устройствами,
памятью и т.д. Например, в третьем кольце нельзя видеть адресное пространство других приложений без особого на то разрешения. Естественно,
трояну вирусу etc эти разрешения получить будет трудновато, так что хакеру в третьем кольце жизни никакой. В третьем кольце находится user mode. Kernel mode сидит в нулевом кольце — наивысшем уровне привилегий. В этом кольце можно всё:
смотреть адресные пространства чужих приложений без каких — либо ограничений и разрешений, по своему усмотрению поступать с любыми сетевыми пакетами, проходящими через машину, на всю жизнь скрыть какой-нибудь свой процесс или файл и т.д. и т.п. Естественно,
просто так пролезть в нулевое кольцо не получиться:
для этого тоже нужны дополнительные телодвижения. У легального драйвера с этим проблем нет:
ему дадут все необходимые API — шки, доступ ко всем нужным системным таблицам и проч. Хакерской же нечисти опять приходиться туго:
все необходимые привилегии ему приходиться «выбивать»
незаконным путём. Но это уже тема отдельной статьи, и мы к ней как-нибудь ещё вернёмся. А пока продолжим.
У тебя наверняка возник законный вопрос:
а что же сидит в первом и втором кольцах ? В том то всё и дело,
что программисты из Microsoft почему — то обошли эти уровни своим вниманием. Пользовательское ПО сидит в user mode,а всё остальное (ядро,
драйвера. ) — в kernel mode. Почему они так сделали — загадка, но нам это только на руку. А теперь разберёмся с компонентами (или, иначе говоря, слоями ) операционной системы Windows
NT.
Посмотри на схему — по ней многое можно себе уяснить. Разберём её подробнее.
С пользовательским режимом всё понятно. В kernel mode самый низкий уровень аппаратный. Дальше идёт HAL, выше — диспетчер ввода — вывода и драйвера устройств в одной связке, а также ядрышко вместе с исполнительными компонентами. О HAL я уже говорила, поэтому поподробнее поговорим об исполнительных компонентах. Что они дают? Прежде всего они приносят пользу ядру. Как ты уже наверняка уяснил себе по схеме, ядро отделено от исполнительных компонентов. Возникает вопрос:
почему ? Просто на ядре оставили только одну задачу:
просто управление потоками, а все остальные задачи (управление доступом,
памятью для процессов и т.д.) берут на себя исполнительные компоненты (еxecutive). Они реализованы по модульной схеме, но несколько компонентов её (схему) не поддерживают . Такая концепция имеет свои преимущества:
таким образом облегчается расширяемость системы. Перечислю наиболее важные исполнительные компоненты:
1) System Service Interface (Интерфейс системных служб )
2) Configuration Manager (Менеджер конфигурирования)
3) I/O Manager (Диспетчер ввода-вывода,ДВВ)
4) Virtual Memory Manager,VMM (Менеджер виртуальной памяти)
5) Local Procedure Call,LPC (Локальный процедурный вызов )
6) Process Manager (Диспетчер процессов)
7) Object Manager (Менеджер объектов)
Так как эта статья — первая в цикле, обзорная, подробнее на этом пока останавливаться не будем. В процессе практического обучения написанию драйверов, я буду разъяснять все неясные термины и понятия. А пока перейдём к API.
API (Application Programming Interface) — это интерфейс прикладного программирования. Он позволяет обращаться прикладным программам к системным сервисам через их специальные абстракции. API-интерфейсов несколько, таким образом в Windows-системах присутствуют несколько подсистем. Перечислю:
1) Подсистема Win32.
2) Подсистема VDM (Virtual DOS Machine — виртуальная ДОС — машина)
3) Подсистема POSIX (обеспечивает совместимость UNIX — программ)
4) Подсистемиа WOW (Windows on Windows). WOW 16 обеспечивает совместимость 32-х разрядной системы с 16-битными приложениями. В 64-х разрядных системах есть подсистема WOW 32,
которая обеспечивает аналогичную поддержку 32 — битных приложений.
5) Подсистема OS/2. Обеспечивает совместимость с OS/2 приложениями.
Казалось бы, всё вышеперечисленное однозначно говорит в пользу WINDOWS NT систем!
Но не всё так хорошо. Основа WINDOWS NT (имеются ввиду 32-х разрядные версии) — подсистема Win32. Приложения, заточенные под одну подсистему не могут вызывать функции другой. Все остальные (не Win32) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность приложений, разработанных, скажем, для подсистемы POSIX и запущенных под винду — очевидны.
Увы.
Подсистема Win32 отвечает за графический интерфейс пользователя, за обеспечение работоспособности Win32 API и за консольный ввод — вывод. Каждой реализуемой задаче
соответствуют и свои функции: функции, отвечающие за графический фейс,
за консольный ввод — вывод (GDI — функции) и функции управления потоками,
файлами и т.д. Типы драйверов, наличествующие в Windows, я уже упоминала в разделе терминов:
монолитный драйвер, фильтр — драйвер и т.д. А раз так, то пора закругляться. Наш краткий обзор архитектуры Windows NT можно считать завершённым. Этого тебе пока хватит для общего понимания концепций Windows NT, и концепций написания драйверов под эту ось — как следствие.
Описать и/или упомянуть обо всех утилитах, могущих понадобиться при разработке драйверов — немыслимо. Расскажу только об общих направлениях.
Без чего нельзя обойтись ни в коем случае — это Microsoft DDK (Driver Development Kit ). К этому грандиозному пакету прилагается и обширная документация. Её ценность — вопрос спорный. Но в любом случае, хотя бы ознакомиться с первоисточником информации по написанию драйверов для Windows — обязательно. В принципе, можно компилять драйвера и в Visual Studio, но это чревато долгим и нудным копанием в солюшенах и vcproj-ектах, дабы код твоего драйвера нормально откомпилировался. В любом случае, сорцы придётся набивать в визуальной студии, т.к. в DDK не входит
нормальная IDE. Есть пакеты разработки драйверов и от третьих фирм:
WinDriver или NuMega Driver Studio, например. Но у них есть отличия от майкрософтовского базиса функций (порой довольно большие ) и многие другие мелкие неудобства. Так что DDK — лучший вариант. Если же ты хочешь писать драйвера исключительно на ассемблере, тебе подойдёт KmdKit (KernelMode Driver DevelopmentKit) для MASM32. Правда, этот вариант только для Win2k/XP.
Теперь можно поговорить о сторонних утилитах. Некоторые уже включены в стандартную поставку
Windows: редактор реестра. Но их в любом случае не хватит. Надо будем инсталлить отдельно.
Множество наиполезнейших утилит создали патриархи системного кодинга в
Windows: Марк Руссинович, Гарри Нэббет, Свен Шрайбер. и т.д. Вот о них и поговорим.
Марк Руссинович создал много полезных утилит:
RegMon, FileMon (мониторы обращений к реестру и файлам соответственно), WinObj (средство просмотра директорий имен
объектов), DebugView,DebugPrint (программы просмотра, сохранения и т.д. отладочных сообщения) и проч. и проч. Все эти утилиты и огромное количество других можно найти на знаменитом сайте Руссиновича
http://www.sysinternals.com.
На диске, прилагающемся к знаменитой книге «Недокументированные возможности Windows 2000» Свена Шрайбера,
есть замечательные утилиты w2k_svc, -_sym, -_mem, позволяющие просматривать установленные драйвера, приложения и службы, работающие в режиме ядра, делать дамп памяти и т.д. Все эти утилиты, а также другие программы с диска можно скачать с
http://www.orgon.com/w2k_internals/cd.html.
Напоследок нельзя не упомянуть такие хорошие проги, как PE
Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.
Ну вот и подошла к концу первая статья из цикла про написание драйверов под Windows. Теперь ты достаточно «подкован» по
теоретической части, так что в следующей статье мы перейдём к практике. Желаю тебе удачи в этом интереснейшем деле — написании драйверов! Да не облысеют твои пятки!
Урок 3. Сборка и запуск драйвера
Еще один, особый, вид программ – драйверы. В отличие от остальных – выполняются в особом режиме, где они имеют ряд возможностей, недоступных для обычных приложений Windows. Однако это осложняется тем, что в этом режиме практически отсутствуют средства для взаимодействия с пользователем. Для тестирования драйвера можно пользоваться функцией DbgPrint, выдающей отладочные сообщения отладчику. Но отладчика может не оказаться и этих сообщений никто в таком случае не увидит. Для обычного же взаимодействия с пользователем драйвер должен полагаться на прикладные приложения, обмениваясь с ними запросами ввода/вывода.
Сообщения, выдаваемые отладчику, проще всего увидеть при помощи программы DebugView, которую нужно запустить перед сеансом проверки.
Создание проекта будем вести следующим образом. Создаем папку DrvHello, где будем разрабатывать приложение-драйвер. Внутри нее создаем такие файлы:
• makefile
# # DO NOT EDIT THIS FILE!!! Edit .sources. if you want to add a new source # file to this component. This file merely indirects to the real make file # that is shared by all the driver components of the Windows NT DDK # !INCLUDE $(NTMAKEENV)makefile.def
Над текстом этого файла не следует задумываться, он просто должен присутствовать в таком вот неизменном виде. Хотя в чужих проектах этого файла может и не быть, либо он будет работать другим образом. В общем, если он есть и содержит это содержимое – то все в порядке, если нет – необходимы дополнительные исследования.
• sources
TARGETNAME=drvhello TARGETTYPE=DRIVER TARGETPATH=obj SOURCES=main.c
Задает параметры сборки. Здесь в параметре SOURCES необходимо перечислить через пробел все файлы с исходным кодом, которые нужно будет скомпилировать.
• main.c – это наш файл с исходным кодом. Может носить любое имя, их может быть много, а может быть очень много. Главное, не забыть их все перечислить в файле sources
#include "ntddk.h" #pragma code_seg("INIT") NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { DbgPrint("Hello, world!"); // Выдача отладочного сообщения return STATUS_DEVICE_CONFIGURATION_ERROR; // Выдача ошибки заставит систему сразу же выгрузить драйвер } #pragma code_seg()
Основная функция у нас здесь носит название DriverEntry. Запускаться она будет при попытке загрузки драйвера. А чтобы сделать такую попытку – нужна программа-загрузчик, которую напишем чуть поздней.
Откомпилируем драйвер. Чтобы мы могли что-то увидеть в отладчике, драйвер должен быть собран в отладочном режиме. Напомню, что отладочный режим у программистов драйверов называется checked, а финальный — free (прикладные программисты так не заморачиваются и называют их debug и release соответственно). В этом режиме программный код не оптимизируется и исполняемый файл содержит много избыточной информации, совершенно не нужной для работы, но очень полезной при изучении того, что будет делать этот код.
Для сборки открываем меню Start, затем Programs/Windows Driver Kits/WDK <номер версии>/Build Environment/Windows Server 2003 (или Windows XP)/ x86 Checked Build Environment. Щелкаем и попадаем в консольное окно. Если FAR был добавлен в PATH, то можно набрать far и перейти в директорию проекта обычным способом, иначе туда придется добираться используя команду cd (и может быть смену диска). Когда добрались до папки DrvHello (там где лежат три файла, которые были созданы выше) – набираем команду nmake. Если сборка прошла без ошибок, то будет созданы директории objchk_wnet_x86/i386, а в ней уже можно обнаружить файл drvhello.sys. Это и есть наш собранный драйвер.
Теперь вернемся к загрузчику. Это у нас будет консольное приложение (см. главу 1). Имя я ему дал DrvHelloloader и разместил, как всегда, в c:Projects. Имя файла с исходным кодом такое же – main.c, содержимое:
#include <windows.h> #include <shlwapi.h> #pragma comment(lib, "shlwapi.lib") void main(int argc, char* argv[]) { char serviceName[] = "drvhello"; char driverBinary[MAX_PATH] = ""; SC_HANDLE hSc; SC_HANDLE hService; // Чтобы запустить драйвер, потребуется полный путь к нему // Предполагаем, что он лежит в той же папке, что и экзешник strcpy(driverBinary, argv[0]); // argv[0] - здесь будет имя экзешника с полным путем PathRemoveFileSpec(driverBinary); // Выкидываем имя экзешника, остается только путь к папке strcat(driverBinary, "\drvhello.sys"); // Добавляем имя драйвера. // Бэкслэш в строках Си надо удваивать, из-за его специального значения. hSc = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); // Открываем SCM (Service Control Management) // Это такая штука, которая позволяет запускать драйверы // из пользовательского режима CreateService(hSc, serviceName, serviceName, SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, driverBinary, NULL, NULL, NULL, NULL, NULL); // Загрузка в 3 этапа - создаем службу hService = OpenService(hSc, serviceName, SERVICE_ALL_ACCESS); // Открываем ее StartService(hService, 0, NULL); // И запускаем. Вообще-то было бы неплохо // еще и закрыть ее... Как нибудь потом. }
Выбираем в меню cтудии – Build/Rebuild Solution. Произойдет пересборка проекта без запуска. В принципе, можно и запустить. Находим экзешник в папке проекта, докидываем в ту же папку drvhello.sys. Далее запускаем DebugView, включаем галочку Capture/Capture Kernel, как показано на рисунке:
Рис. 3.1 – Настройка DebugView
Теперь запускаем программу. Если все прошло успешно – видим следующую картину:
Рис. 3.2 – Проверка драйвера
Вот такой простой код позволяет уже очень много – работать на уровне ядра. Но следует помнить, что если допустить ошибку в прикладной программе – упадет она одна; если в драйвере – вся система. К примеру, если в код драйвера перед DbgPrint добавить строку:
int a = *((int *)0);
которая делает попытку прочесть содержимое памяти по нулевому адресу – то картинка будет уже иной:
Рис. 3.3 – Попытка чтения из нулевого адреса
Если бы это был не драйвер, а просто программа пользовательского режима, то при выполнении этого кода она бы просто закрылась, выдав ошибку:
Рис. 3.4 – Та же ошибка в программе пользовательского режима