Пишем драйвер WDM на Ассемблере.
Источник: http://progrex.narod.ru/
Краткое содержание.
- Цель работы. Постановка задачи.
- API для WDM драйвера.
- Пишем рыбу.
- Детализация.
- Компиляция и сборка.
- Как правильно установить драйвер.
- Тестовая программа.
- Напоследок.
Часть 1. Цель работы. Постановка задачи.
Признаюсь честно: мне нравится Ассемблер. Вернее, даже не сам Ассемблер, а стиль общения с компьютером через него. В сети есть несколько примеров создания драйверов виртуальных устройств VxD на Ассемблере. Но нет ни одного аналогичного примера для драйвера WDM. Так исправим же эту досадную оплошность!
Создание несложного драйвера с использованием только лишь Ассемблера - довольно трудоёмкое занятие. По двум причинам:
- Отсутствие ассемблерных заголовочных файлов для использования драйверного API.
- Методически трудная отладка драйверов в системе Windows.
Первая причина может быть некритичной. Были бы руки да голова. Ведь известно, что значительная часть заголовков Win32 API была переведена энтузиастами на Ассемблер. И работа эта немалая.
Вторая причина более серьёзна и именно она является сдерживающей. Практически, самым доступным способом отладки является отладочный вывод из самого драйвера. При этом код осторожно дописывается небольшими кусочками.
Однако, теперь у вас будет хороший кусок работающего кода. Мы его сейчас напишем! Изменяя и дополняя его, вы сможете создать свой собственный драйвер, довольно быстро и легко.
Сразу оговорюсь: я не собираюсь подробно объяснять принципы функционирования драйверной системы Windows и растолковывать специальные понятия. Для этого существует специальная литература.
Что нам потребуется? Вот что:
1) Текстовый редактор.
Notepad. Но лучше что-нибудь поудобнее, например, Патриот XP.
2) MS Windows DDK.
DDK содержит почти всё, что требуется для создания драйверов. Но нам важны: справка DDK, Ассемблер MASM 6.1, компоновщик Link, также оттуда мы возьмём библиотеки и заголовочные файлы для C (что с ними делать -- см. далее).
3) Утилита для визуализации отладочного вывода. Я использую DbgView, который можно взять с сайта www.sysinternals.com
4) Delphi для компиляции тестовой программы.
Но вам необязательно набирать текст с нуля. К счастью, я сделал это до вас :) Скачайте файл с исходниками проекта AsmDrv и распакуйте его в подкаталог \NTDDK\src\AsmDrv. Вот, кажется, всё. Можно начинать!
Часть 2. API для WDM драйвера.
Большинство функций драйверного API, которые нас интересуют, предоставляются модулем ntoskrnl.exe. Для их использования надо сделать следующее:
1) Объявить типы данных и определить константы.
Большинство определений для C находятся в файлах ntdef.h и wdm.h.
2) Объявить прототипы функций, которые мы намерены использовать.
Эти определения для C также находятся в wdm.h
3) Выполнить сборку драйвера с подключением библиотеки wdm.lib
Все три файла (wdm.h, ntdef.h и wdm.lib) входят в состав Windows DDK.
Я перевёл часть заголовков на Ассемблер и поместил их в файл usewdm.inc, который находится в базовом каталоге проекта.
Часть 3. Пишем рыбу.
3.1. Итак, приступим.
Вы можете проследить за последовательностью и содержанием действий, открыв файл main.asm для просмотра.
Начнём, пожалуй, так:
.586p ; Процессор Intel Pentium, разрешены инструкции защищённого режима .model flat, stdcall ; Здесь всё ясно. Плоская модель адресации и тип вызовов stdcall. option casemap:none ; "case-sensitive"
Дальше нужно задействовать файл включений usewdm.inc и библиотеку wdm.lib, чтобы мы смогли использовать драйверный API:
.include usewdm.inc .includelib wdm.lib
Затем размещаем два сегмента -- данных и кода:
.data ; [...] .code ; [...]
3.2. Процедура инициализации
Каждый драйвер имеет процедуру инициализации. Эта процедура вызывается системой сразу после загрузки драйвера в память.
У нас такая процедура называется DriverEntry. Объявим её как
Driver Entry proc near public, DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING
DriverObject -- это указатель на служебную структуру, сопоставленную драйверу. Она используется системой для вызова процедур драйвера. Её-то и следует инициализировать -- записать в эту структуру адреса соответствующих процедур нашего драйвера.
Наш драйвер довольно прост. Он будет отрабатывать только 4 стандартных запроса:
- IRP_MJ_CREATE -- Вызов CreateFile() в приложении пользователя для установления связи с драйвером;
- IRP_MJ_CLOSE -- Вызов CloseHandle() в приложении пользователя для разрыва связи с драйвером;
- IRP_MJ_DEVICE_CONTROL -- Вызов DeviceIoControl() в приложении пользователя для запроса выполнения какой-либо функции в драйвере.
Все эти три запроса мы адресуем некоей диспетчерской функции OnDispatch. Мы узнаем о ней позже.
Четвёртый запрос -- на выгрузку. Об этом пойдёт речь ниже.
А пока необходимо сделать ещё 2 важные вещи - создать логический объект устройства при помощи функции IoCreateDevice() и символическую связь, имя которой пользовательские приложения будут использовать для связи с драйвером при помощи функции CreateFile(). Символическая связь создаётся при помощи вызова IoCreateSymbolicLink():
; Инициализируем юникодовые строки с именами устройства и линка invoke RtlInitUnicodeString, offset NtDeviceName, offset wsNtDeviceName invoke RtlInitUnicodeString, offset Win32DeviceName, offset wsWin32DeviceName ; [...] ; Создаём логический объект устройства invoke IoCreateDevice, DriverObject, 0, offset NtDeviceName, ; Проверим, не было ли ошибки. FILE_DEVICE_UNKNOWN,0,FALSE,offset DeviceObject; cmp eax,STATUS_SUCCESS jnz @F ; Создаём symbolic link ; в eax останется код результата invoke IoCreateSymbolicLink, offset Win32DeviceName, offset NtDeviceName @@: ret
Итак, только что мы завершили разбор процедуры инициализации.
3.3. Процедура выгрузки.
У нас она реализуется функцией OnUnload. Эта функция производит действия, обратные процедуре инициализации по отношению к связанным объектам: она удаляет символическую связь (вызов IoDeleteSymbolicLink()), и затем логическое устройство, сопоставленное драйверу (IoDeleteDevice()):
; Удаляем символическую связь invoke IoDeleteSymbolicLink, offset Win32DeviceName ; Удаляем логическое устройство invoke IoDeleteDevice, DeviceObject
3.4. Главная диспетчерская процедура.
Она называется OnDispatch и объявлена как
OnDispatch proc near, pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
Здесь нам важен указатель на структуру с данными запроса pIrp. Данная структура довольно сложна. Вы можете найти её объявление в файле usewdm.inc.
Но нам понадобятся лишь некоторые данные.
Сначала мы должны определить код запроса -- он будет один из трёх: IRP_MJ_CREATE, IRP_MJ_CLOSE или IRP_MJ_DEVICE_CONTROL.
Мы получаем этот код из структуры IO_STACK_LOCATION, указатель на которую мы получаем из структуры IRP (в свою очередь, указатель на IRP был передан нам в пераметре pIrp):
mov ebx,pIrp ; Восстанавливаем указатель на структуру IO_STACK_LOCATION mov eax,(_IRP ptr [ebx]).Tail.Overlay.CurrentStackLocation mov pIrpStack,eax mov ebx,pIrpStack ; al -- Код сообщения mov al,(IO_STACK_LOCATION ptr [ebx]).MajorFunction
Дальше отрабатываем запросы по-разному.
Для IRP_MJ_CREATE и IRP_MJ_CLOSE обработка фиктивная. Мы просто возвращаем код успеха STATUS_SUCCESS в регистре eax.
Для IRP_MJ_DEVICE_CONTROL мы должны получить данные о длине входного и выходного буферов приложения пользователя, восстановить указатель на промежуточный системный буфер и адрес переменной, в которую будет записана длина информационного пакета, передаваемого из драйвера приложению пользователя.
Мы размещаем эти данные в локальных переменных, чтобы потом вызвать вторичную функцию DeviceIoControlHandler, где и будет выполнена обработка.
Часть 4. Детализация.
Детализация заключается в размещении пользовательского кода во вторичной процедуре обработчика IRP_MJ_DEVICE_CONTROL.
Мы будем отрабатывать 2 запроса:
- IOCTL_USER_REQUEST_1 -- отправка переданной строки в отладочный вывод, и
- IOCTL_USER_REQUEST_2 -- перевод литер переданной строки в нижний регистр.
Коды запросов объявлены в файле-включении ioctlcodes.inc
В принципе, реализация этих вещей довольно проста и не требует комментариев.
Часть 5. Компиляция и сборка.
Для компиляции программы следует выполнить командный файл assemble.cmd.
Его содержимое:
..\..\bin\ml.exe -coff -Fl -c -Foasmdrv.obj main.asm
В результате мы получим листинг main.lst и объектный модуль asmdrv.obj.
Дальше мы должны собрать бинарник драйвера из объектного модуля. Для этой цели существует команда link.cmd:
..\..\bin\link.exe @linkcmd.rsp
в файле linkcmd.rsp размещены настройки линкера. Полный список выглядит так:
-MACHINE:IX86 -STACK:32768,4096 -OPT:REF -OPT:ICF -INCREMENTAL:NO -FORCE:MULTIPLE -RELEASE -DEFAULTLIB:wdm.lib -DRIVER -ALIGN:0x20 -SUBSYSTEM:NATIVE -BASE:0x10000 -ENTRY:DriverEntry@8 -OUT:disk1\asmdrv.sys asmdrv.obj
В результате сборки мы получаем файл AsmDrv.sys в подкаталоге Disk1.
Часть 6. Как правильно установить драйвер.
Чтобы установить драйвер в системе, нам потребуется специальный конфигурационный файл, хранящий некоторые дополнительные сведения о драйвере, важные для системы.
Это так называемый inf-файл.
Опять-таки, к счастью для вас, я уже написал этот файл -- asmdrv.inf. Вы можете открыть его для просмотра и изучить.
Файл находится в подкаталоге Disk1 проекта.
Ну что же? -- Пробуем установиться.
Открываем Панель управления, запускаем Мастер установки оборудования и указываем ему путь к файлу asmdrv.inf в режиме ручного выбора.
Завершаем установку.
Windows 98 на этом месте может попросить перезагрузки. Не отказывайте ей :)
Windows 2000/XP запускает драйвер сразу.
Вы можете проконтролировать установку, убедившись в наличии устройства "Простейший WDM драйвер на Ассемблере" в списке менеджера устройств.
Поздравляю, если вы всё сделали правильно, наш драйвер -- о, чудо! -- работает.
Часть 7. Тестовая программа.
Проект тестовой программы расположен в подкаталоге TestApp.
Откройте его в Delphi и перекомпилируйте.
В результате вы получите файл AsmDrvTest.exe, который нужно будет запустить.
В принципе, это одна из самых простых программ в мире. Она занимается отправкой драйверу AsmDrv.sys запросов IOCTL_USER_REQUEST_1 и _2 по требованию пользователя, передавая драйверу строку символов.
Работу этой программы рекомендуется изучить самостоятельно.
Одновременно с тестовой программой используйте утилиту DbgView для просмотра отладочного вывода.
Часть 8. Напоследок.
Мы убедились ещё раз, что не боги горшки обжигают. Следует ли писать драйверы WDM на ассемблере? -- Зависит от желания и возможностей. Однако вопрос оставлю открытым.
Оставить комментарий
Комментарии
Поэтому увидеть макет WDM-драйвера на ассемблере приятно ... но не более. Реально использовать его проблематично, т.к. для применения драйверного API необходимо собственноручно перелопатить кучу структур из wdm.h и прочих *.h в файлы *.inc, которые начиная с ddk98 Microsoft не утруждается поддерживать. Кроме всего прочего, придется еще и поработать с DDK'ашным Help'ом от Microsoft. Муки при изучении его полуметровых названий функций и структур настолько невыносимы, что приходиться ежесекундно вспоминать ласковыми словами славную компанию пацанов из Microsoft'а.
К моему удивлению, ранее мною легко решенный вопрос выделения блока памяти для ПДП в VXD-драйвере с помощью VMMCall _PageAllocate вызвал у меня затруднения в DDK 98. Аналога в DDK 98 этой функции я не нашел, видимо у пацанов из Microsoft'а в очередной раз произошла ломка, которая сподвигла их на радикальные изменения подходов к системному программированию. Основным моментом в функции _PageAllocate для ПДП является получение физического адреса буфера. Help 98 DDK отправил меня к использованию функции AllocateCommonBuffer, которая почему-то оказалась неразрывно связанной с ПДП, а следовательно и с кучей структур: DMA_ADAPTER, DEVICE_OBJECT, DRIVER_OBJECT и т.д. Убить время на перевод *.h в *.inc ? Очевидно, реку не повернуть вспять и следует стать СИонистом, взять в руки бубен и начать хороводить...
http://www.wasm.ru/tools/11/kmdkit17.zip
Вообще-то заголовочные файлы уже давно есть.