Целью этой статьи является пошаговая демонстрация процесса разработки всего набора программного обеспечения необходимого для организации связи самодельного устройства с компьютером посредством USB.
На данный момент, большинство радиолюбителей реализуют такой тип подключения используя чипы переходники USB в RS232 таким образом организуя связь со своим устройством посредством драйвера виртуального COM порта поставляемого с чипом переходником. Минусы такого подхода думаю понятны. Это как минимум лишний чип на плате и ограничения накладываемые этим чипом и его драйвером.
Мне же хочется осветить весь процесс организации такого взаимодействия так как оно и должно быть сделано, и как делается во всех серьезных устройствах.
В конце концов, сейчас 21-й век, модуль USB есть почти во всех микроконтроллерах. Именно о том, как наиболее быстро воспользоваться этим модулем и будет эта статья.
Так как для демонстрации процесса написания драйвера USB устройства нам необходимо собственно само устройство, то выберем одну из распространенных отладочных плат доступных в России. У меня это плата производства компании OLIMEX модель LPC-P2148. Основой платы является микроконтроллер LPC2148 архитектуры ARM7TDMI производства компании NXP. Всю информацию по плате можно получить на сайте производителя по следующей ссылке. Вот как она выглядит.
Выбор контроллера и отладочной платы абсолютно не принципиален т.к. процесс разработки взаимодействия между ОС на персональном компьютере и самой платой от этого не зависит. Среду разработки прошивки микроконтроллера будем использовать KEIL версии 4.23, что так же не принципиально. В итоге, планируется реализовать только BULK тип передачи. Будем считывать массив данных из устройства в компьютер, а передавать на устройство будем состояние светодиодов, чтобы было видно, что плата реагирует на наши команды.
Для удобства понимания разделим дальнейшие действия на стадии и будем проходить их по-порядку.
1. Адаптация готового примера USB устройства под нашу плату с целью убедиться, что плата работает и USB канал так же работоспособен. Это будет как бы наша стартовая точка.
2. Изменение прошивки платы, чтобы она стала для Windows неизвестным устройством, требующее драйвер производителя.
3. Адаптация базового шаблона, пустого драйвера, чтобы Windows могла его корректно установить, для обслуживания нашего устройства.
4. Реализация взаимодействия драйвера с пользовательским приложением.
5. Написание консольного приложения Windows для работы с нашим драйвером, а следовательно и подключенным USB устройством.
6. Наполнение всей системы необходимыми функциями.
Чего в этой статье не будет. Я не буду расписывать механизмы работы ОС, позволяющие находить и устанавливать нужный драйвер. Не будет описания, как собирать прошивку в среде KEIL. Не будет описания параметров дескрипторов USB и вообще практически не будет ничего сказано про то, как работает прошивка. В конце я предоставлю ссылки на все источники информации, мои исходные коды и собранные бинарные файлы. Таким образом, описание любого момента не охваченного данной статьей, можно будет легко найти по указанным источникам. Поймите правильно, нереально вместить в одну статью подробную информацию по всем этим темам. Тем более, что есть более компетентные источники.
1. Адаптация примера RTX_Memory под плату OLIMEX LPC-P2148
За основу прошивки к нашему проекту мы возьмем пример RTX_Memory поставляемый вместе с KEIL. Данный пример, когда успешно заработает, позволит нашу плату подключать к компьютеру и она будет там видна как обычная USB флешка. Таким образом мы получим прошивку, которая заведомо корректно настраивает USB модуль и всю необходимую процессору периферию.
Проект находится в папке ARMBoardsKeilMCB2140RLUSB. Пути здесь и далее я буду указывать относительно основной папки, куда установлена среда KEIL.
Скопируем проект в отдельное место, загрузим его в KEIL и соберем. Собраться должен без ошибок. В итоге мы получили HEX файл, который можем прошить с помощью утилиты FlashMagic.
Правда можно пока его не прошивать так как очевидно, что он работать на нашей плате не будет.
Если сравнить схему нашей платы и платы для которой написан пример, а это модель MCB2140 производства KEIL, то видно различия в подключении подтяжки линии D+.
На плате MCB2140 она всегда подтянута к 3.3В, а на LPC-P2148 этой подтяжкой управляет микроконтроллер через транзистор.
Схемы обеих плат доступны на сайтах www.olimex.com и www.keil.com соответственно.
Для простоты, мы немного изменим код инициализации, чтобы наша плата всегда при включении включала подтяжку линии D+, о чем будет сообщать светодиод USB_LINK.
В процедуре USB_Init() отключим линию CONNECT от модуля USB и будем ею управлять сами. А так как на этом же транзисторе есть еще и светодиод USB_LINK то получится, когда мы его включим, автоматически вулючится и подтяжка линии D+.
Кроме того, на нашей плате меньше светодиодов чем у MCB2140. По-этому их назначение так же нужно переопределить. На данном этапе я их переназначил просто для индикации процессов чтения/записи.
Так как у нас нет индикаторов LED_CFG и LED_SUSP то закоментируем их использование везде по коду проекта.
Теперь можно собрать проект и прошить его в контроллер. Подключив плату к компьютеру, видно, что он ее распознает как внешний накопитель и в системе появляется еще один диск размером всего около 25КБайт и с файлом readme.txt.
На этом первый этап можно считать законченным.
2. Переход от USB накопителя к уникальному устройству.
На данный момент мы имеем устройство, которое на любом компьютере с любой ОС будет распознаваться, как внешний USB накопитель. Но нам требуется, чтобы Windows не знала, ким образом работать с нашим устройством и требовала драйвер. О том, что подключенное устройство относится ко классу накопителей, говорит параметр Interface class находящийся в дескрипторе интерфейса.
Если открыть файл usbdesc.c и найти там этот параметр то будет видно что он имеет значение USB_DEVICE_CLASS_STORAGE.
Заменим его на USB_DEVICE_CLASS_VENDOR_SPECIFIC, и следующие за ним два поля заменим на нули.
Теперь пересобрав проект и прошив плату мы увидим, что Windows больше не знает, что наше устройство является накопителем и требует предоставить подходящий драйвер.
Тут может возникнуть проблема. Дело в том, что Windows запомнив VID и PID нашего устройства в предыдущий раз, как относящиеся к устройству внешнего хранения, может продолжать ставить на него свой драйвер не обращая внимание на то, что класс устройства поменялся. Решение простое. Если плата по-прежнему определяется как накопитель, найдите ее в ветке USB диспетчера устройств и удалите драйвер вручную. После этого ОС должна начать просить драйвер.
3. Создаем базовый драйвер.
Итак, у нас есть рабочее USB устройство для которого требуется предоставить драйвер.
Для начала мы напишим самый простой драйвер, который не будет делать ничего полезного, кроме как загружаться в систему при появлении нашего устройства на шине USB. Драйвер будет иметь минимальный код, чтобы только корректно загрузиться и выгрузиться системой.
Писать драйвер мы будем самым минималистическим методом. Сам код будет редактироваться в блокноте, а собираться будет в командной строке.
Для начала, нужно скачать с сайта Microsoft набор для разработки драйвером. Называется он Windows Driver Kit. Я использую версию WDK 7600.16385.1.
После установки, мы получим много примеров, окружение для сборки и документацию. В меню пуск, нужно найти раздел WDK и там Build Environments. Это так называемые окружения для сборки. Фактически они предоставляют нам консоль, которая уже настроина так, чтобы собирать драйверы для нужной системы.
Вы видите, что там для каждой ОС отдельная папке, где находится пара окружений Checked и Free. Первое для так называемых Checked систем, собирает драйвер с дополнительной информацией полезной при отладке.
Второе собирает релиз драйвера, который потом и используется.
Я буду использовать далее окружение «x86 Checked Build Environment» от windows XP. Это даст мне универсальный драйвер корректно работающий на системах от Windows XP и новее.
Теперь займемся поиском шаблона, с которого было бы удобней всего начать.
Самым подходящим кандидатом оказался пример к некой плате OSR USB-FX2 learning kit. Что это за плата я абсолютно не имею понятия, но нужный нам пример находится в WDK по пути srcusbosrusbfx2. Самое интересное, что это не просто пример, а пошаговое обучение, как сделать драйвер к этой плате. Как раз то, что нам и нужно. Зайдем глубже в директорию kmdfsys и видим, что там все шаги и лежат по папочкам. Подробнее о них можно почитать в описании примера, находящемся в файле osrusbfx2.htm.
Тут я сделаю небольшое отступление, чтобы немножко сделать более понятней следующие действия.
Дело в том, что с момента появления Windows NT кое что изменилось в процессе написания драйвера. В те времена нам приходилось напрямую использовать функции ядра ОС и часто, просто чтобы сделать пустышку способную правильно загружаться, выгружаться, отвечать на события PNP и т.п. базовые функции, приходилось много чего изучить и не один раз вылететь в BSOD. Потом Microsoft сделала модель, которую назвала Windows Driver Model и которая внесла некоторого рода стандарт что ли, как должен выглядеть драйвер. Особого облегчения, лично я от этого не почувствовал. А следующим шагом был сделан фреймворк, который называется Windows Driver Framework. И вот благодаря этому жить стало намного проще. Теперь фреймворк берет на себя реализацию всех базовых действий необходимых для обслуживания основных событий, а нам останется только правильным образом добавить нужных нам функций. Вот именно эту технологию мы и будем использовать.
Начинаем с первого шага. Запускаем «x86 Checked Build Environment» и при помощи команды “cd” перемещаемся в папку WinDDK7600.16385.1srcusbosrusbfx2kmdfsysstep1.
Выплняем команду build -ceZ.
Происходит процесс сборки, и в результате создается папка objchk_wxp_x86( ее название зависит от выбранного окружения ), где мы и находим файл с расширением sys. Это и есть наш драйвер. Чтобы установить его, нам нужен INF файл. Найдем его в папке final этого же проекта. Она называется osrusbfx2.inf. Проблема только в том, что он рассчитан на плату из примера. Чтобы этот файл был способен установить драйвер для нашей платы, просто поменяем в нем везде значения VID и PID на те, которые прописаны в дескрипторе USB устройства в файле usbdesc.c. Просмотрев глазами INF файл, можно заметить, что для установки драйвера еще требуется файл WdfCoInstaller01009.dll. Он тоже находится в поставке WDK.
Итак, копируем в отдельную папку три файла: собранный SYS, INF, WdfCoInstaller01009.dll.
Подключаем нашу плату к компьютеру, и на вопрос Windows о пути к драйверу указываем эту папку.
Наблюдаем обычный процесс копирования файлов драйвера и в диспетчере устройств появляется наше устройство под классом Sample Device. Все, операционная система удовлетворена!
А вот тут может возникнуть вопрос, а как мы вообще знаем, что наш код исполняется. А другими словами, хотелось бы получить от драйвера какого-нибудь рода обратную связь. Все верно, настал момент добавить в драйвер вывод отладочной информации, чтобы понимать что вообще происходит.
В режиме ядра, отладочную информацию выводит функция KdPrint(). Ее использование такое же, как всем известной printf(). Чтобы увидеть ее вывод, нужно установить программу DbgView. Она доступна на сайте Microsoft по ссылке http://technet.microsoft.com/en-us/sysinternals/bb896647. Просто держите ее запущенной и будете видеть вывод всей отладочной информации из режима ядра ОС. Я обычно настраиваю фильтр, чтобы отображались только сообщения нужного мне модуля. В моем варианте Step_1 я добавил вывод в процедуры DeviceEntry() и DeviceAdd() так, что он просто пишет какая функция вызвалась. Подключая и отключая плату, в окне DbgView хорошо видно в каком порядке это происходит.
4. Взаимодействие между режимами ядра и пользователя.
Как известно, драйверы устройств работают в режиме ядра( за некоторым исключением ), а наши приложения в режиме пользователя. Для взаимодействия используется тот же механизм, что и для работы с файлами. Иными словами, для каждого подключенного устройства в системе есть символическое имя, по которому его можно открыть, как обычный файл. Ну а потом использовать обычные процедуры для работы с файлами типа ReadFile() и WriteFile(). В этой части, мы добавим в наш драйвер функционал, позволяющий его открывать, закрывать, писать и читать из него данные.
Записанные данные будем сохранять, чтобы потом отдавать их при операции считывания.
Первое, что нужно сделать, это зарегистрировать свой callback функцию для события EvtDevicePrepareHardware, которую вызовет менеджер PnP после того, как устройство перейдет в неинициализированное состояние D0 и перед тем, как сделать его доступное драйверу. По сути это означает очень простую вещь, устройство мы воткнули, драйвер загрузился, но возможно ваше устройство требует некоторой настройки перед тем, как с ним станет возможно работать. Вот такого рода настройку мы и сделаем в этом событии. В применении к USB, как минимум нужно выбрать нужную конфигурацию. Итак, регистрируем нашу функцию. Для этого добавляем в DriverEntry следующий код:
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = EvtDevicePrepareHardware;
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);
Второе. Если обратите внимание на вызов процедуры WdfDeviceCreate из кода драйвера предыдущего параграфа, то можно заметить, что второй параметр этой процедуры передается константа WDF_NO_OBJECT_ATTRIBUTES. Означает это, что объект устройства не имеет никаких атрибутов. Но в реальной жизни нам понадобится как минимум один атрибут. Это так называемый контекст устройства. Упрощенно говоря, это некоторого рода структура, которая относится к конкретному экземпляру устройства поддерживаемого драйвером, и будет далее доступна нам практически в любом месте драйвера. Например она может содержать какой-нибудь буфер. А привязывается она к объекту устройства, а не драйвера т.к. К компьютеру может быть подключено несколько одинаковых устройств, которые будет обслуживать один и тот же драйвер, но все они будут иметь свой собственный объект устройства.
Итак, создадим структуру контекста, и инициализируем ею, параметр атрибутов, передаваемый далее в WdfDeviceCreate:
typedef struct _DEVICE_CONTEXT {
WDFUSBDEVICE UsbDevice;
WDFUSBINTERFACE UsbInterface;
WDFUSBPIPE BulkReadPipe;
WDFUSBPIPE BulkWritePipe;
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DEVICE_CONTEXT, GetDeviceContext)
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DEVICE_CONTEXT);
Третье. Теперь необходимо создать интерфейс, через который драйвер станет доступный программам пользовательского режима. Раньше, программист должен был сам жестко прописывать имя, по которому мог быть открыт доступ к устройству через процедуру CreateFile. Теперь все стало проще. Нам нужно только создать интерфейс вызвав одну процедуру, а для его идентификации используется сгенерированный GUID. Далее в пользовательском режиме мы будем использовать этот же GUID, чтобы получить имя файла устройства. Итак, вот наш GUID и код связывающий его с интерфейсом:
DEFINE_GUID(GUID_DEVINTERFACE_OSRUSBFX2, // Generated using guidgen.exe
0x573e8c73, 0xcb4, 0x4471, 0xa1, 0xbf, 0xfa, 0xb2, 0x6c, 0x31, 0xd3, 0x84);
// {573E8C73-0CB4-4471-A1BF-FAB26C31D384}
status = WdfDeviceCreateDeviceInterface(device,
(LPGUID) &GUID_DEVINTERFACE_OSRUSBFX2,
NULL);// Reference String
Последнее. В первом пункте мы зарегистрировали процедуру, обрабатывающую событие EvtDevicePrepareHardware. Теперь нужно ее написать. Не буду переписывать ее текст в статью, думаю проще будет глянуть в исходном коде. Скажу только, что в этой процедуре, мы подготавливаем все что нужно для последующей работы драйвера с подключенным устройством. А конкретно создаем объект USB устройства, выбираем нужную конфигурацию, и сохраняем в контексте устройства идентификаторы каналов, относящихся к BULK конечным точкам реализованного в устройстве интерфейса. Нам эти идентификаторы понадобятся позже, для реализации передачи данных. Для наглядности, я добавил вывод параметров каналов в DbgView. Можно заметить, что их параметры — это ни что иное, как те же самые значения, которые мы прописали в дескрипторах конечных точек в файле usbdesc.h прошивки.
Итак, теперь можно опять пересобрать драйвер, и обновить его в системе. На данный момент наш драйвер может уже не просто загрузиться. Он уже умеет настраивать подключенное устройство, и, что самое важное, стал доступен для программ из режима пользователя.
5. Работаем с драйвером из режима пользователя.
Теперь мы напишем простую консольную программу, которая будет только пытаться получить доступ к нашему драйверу. Как вы помните, на данный момент наш драйвер больше ничего делать не умеет, кроме как дать возможность получить к себе доступ.
Работа с устройствами, сводится к открытию их, как обычного файла, и записи и чтения данных при помощи обычных процедур WriteFile и ReadFile. Есть еще очень полезная процедура DeviceIoControl, для организации взаимодействия с драйвером, которое выходит за формат работы с файлами, но мы ее использовать не будем. Открывается файл обычным вызовом CreateFile, вот только нам нужно имя файла. И тут нам пригодится GUID, который мы привязали к интерфейсу драйвера. Я не буду описывать всю процедуру получения имени через GUID, и честно признаюсь, что полностью взял ее из примеров WDK. Процедура GetDevicePath получает GUID и возвращает полный путь ему соответствующий.
Файл открыт. Добавим пару вызовов, которые запишут и считают из файла десяток байт.
Но вернемся к нашему драйверу. В пользовательской программе мы уже пишем в драйвер и читаем из него, но сам код драйвера про это ничего не знает. Исправим ситуацию.
Логика тут такая же, как и с EvtDevicePrepareHardware. Нам нужно зарегистрировать callback функции, которые вызовутся, когда произойдут процедуры чтения из драйвера или записи в него. Делается это в EvtDeviceAdd. Необходимо инициализировать очередь ввода/вывода, заполнить ее поля указателями на наши callback функции и создать ее, прицепив к объекту устройства. Поехали:
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQueueConfig,
WdfIoQueueDispatchParallel);
ioQueueConfig.EvtIoRead = EvtIoRead;
ioQueueConfig.EvtIoWrite = EvtIoWrite;
status = WdfIoQueueCreate(device,
&ioQueueConfig,
WDF_NO_OBJECT_ATTRIBUTES,
WDF_NO_HANDLE);
Кроме объявления процедур чтения и записи, нужно не забыть их реализовать. На данном этапе я просто поставил заглушки, которые выводят переданные данные в DbgView и отдают массив из 10 байт при чтении. Код их вы можете посмотреть в исходниках. Там ничего интересного, только советую обратить внимание на работу с памятью. Необходимо по определенным правилам получать буферы т.к. Данные у нас перемещаются между режимами ядра и пользователя. На скриншоте хорошо видно, как мы посылаем данные в драйвер и они появляются в окне DbgView. Потом мы читаем пакет из драйвера и получаем его в выводе консольного приложения.
6. Делаем драйвер полезным.
Вот и настало время, сделать наш драйвер полезным. На данный момент он производит коммуникацию с режимом пользователя но никак не работает с реальным устройством. А все что нам осталось сделать, это в процедуре записиси добавить код, передающий данные на устройство, а в процедуре чтения — код принимающий данные с устройства. В исходниках вы видите, как совсем незначительно изменились процедуры обслуживающие ввод/вывод в драйвере. Мы всего лишь передаем наши буферы далее подсистеме USB ядра, а она уже все сделает, как нужно.
Перед началой реальной передачи данных между PC и устройством, нам еще нужно изменить прошивку устройства, чтобы она как-то реагировала на наши данные.
Изменим немного код в обработке события приема данных таким образом, чтобы если первый принятый байт 0x01 то включим LED_1, а если он 0x02 то включим LED_2. А т.к. После записи в устройство мы из него сразу читаем 10 байт, то добавим этот код тоже. Обратите внимание, что мы отправляем пакет на передачу в событии обработки входящего пакета. Это такая особенность работы модуля USB. Нам нужно заранее отдать ему данные для передачи, чтобы он мог исполнить IN транзакцию. А для наглядности, будем передавать два разных массива. Меняем содержимое MSC_BulkOut() следующим образом:
void MSC_BulkOut (void) {
BulkLen = USB_ReadEP(MSC_EP_OUT, BulkBuf);
LED_Off( LED_RD | LED_WR );
if( BulkBuf[ 0 ] == 0x01 )
{
USB_WriteEP( MSC_EP_IN, (unsigned char*)aBuff_1, sizeof( aBuff_1 ) );
LED_On( LED_RD );
}
else
if( BulkBuf[ 0 ] == 0x02 )
{
USB_WriteEP( MSC_EP_IN, (unsigned char*)aBuff_2, sizeof( aBuff_1 ) );
LED_On( LED_WR );
}
}
А в процедуре MSC_BulkIn() закоментируем весь код, оставив ее полностью пустой.
Результат работы всей связки вы видете на скриншоте.
При этом сама плата моргает двумя светодиодами.
Вот собственно и все. Мы написали прошивку и полноценный драйвер для собственного устройства USB. Если запустить передачу блоками по 4кб, можно добиться скорости 800 Кбайт/сек.
Как видите текст драйвера довольно прост и содержит всего около 250-ти строк.
В статье я описал только основные шаги, которые нужно предпринять, чтобы получился работоспособный драйвер. Более подробную информацию по используемым процедурам необходимо читать в WDK. Тем более, что сейчас эту документацию стало довольно приятно читать и они изобилуют примерами.
Полный архив с исходниками можно скачать по ссылке.
В архиве находятся папки проименованные по пунктам, каждая содержит конечный результат, который мы достигли в соответствующем пункте.
Надеюсь статья получилась непохожей на руководство «как нарисовать сову», и кому-нибудь окажется полезной.
Информационная безопасность, Open source, Системное программирование, Блог компании Аладдин Р.Д.
Рекомендация: подборка платных и бесплатных курсов таргетированной рекламе — https://katalog-kursov.ru/
Вряд ли пользователь домашнего ПК заинтересуется тем, чтобы блокировать устройства на своем ПК. Но если дело касается корпоративной среды, то все становится иначе. Есть пользователи, которым можно доверять абсолютно во всем, есть такие, которым можно что-то делегировать, и есть те, кому доверять совсем нельзя. Например, вы заблокировали доступ к Интернету одному из пользователей, но не заблокировали устройства этого ПК. В таком случае пользователю достаточно просто принести USB-модем, и Интернет у него будет. Т.е. простым блокированием доступа к Интернету дело не ограничивается.
Однажды примерно такая задача и стояла передо мной. Времени на поиск каких-либо решений в Интернете не было, да и они, как правило, небесплатные. Поэтому мне было проще написать такой драйвер, и его реализация отняла у меня один день.
В этой статье я расскажу немного теоретическую часть, на основе которой все строится, и расскажу принцип самого решения.
Также полные исходные коды могут быть найдены в папке USBLock хранилища git по адресу: https://github.com/anatolymik/samples.git.
Структура DRIVER_OBJECT
Для каждого загруженного драйвера система формирует структуру DRIVER_OBJECT. Этой структурой система активно пользуется, когда отслеживает состояние драйвера. Также драйвер отвечает за ее инициализацию, в частности за инициализацию массива MajorFunction. Этот массив содержит адреса обработчиков для всех запросов, за которые драйвер может отвечать. Следовательно, когда система будет посылать запрос драйверу, она воспользуется этим массивом, чтобы определить, какая функция драйвера отвечает за конкретный запрос. Ниже представлен пример инициализации этой структуры.
for ( ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
DriverObject->MajorFunction[i] = DispatchCommon;
}
DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
DriverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchCleanup;
DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
DriverObject->DriverUnload = DriverUnload;
DriverObject->DriverExtension->AddDevice = DispatchAddDevice;
Такая инициализация обычно выполняется при вызове системой точки входа драйвера, прототип которой изображен ниже.
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath );
Как видно из примера, сначала весь массив MajorFunction инициализируется одним и тем же обработчиком. В реальности типов запросов больше, чем в примере. Поэтому предварительно весь массив инициализируется так, чтобы запросы, которые не поддерживаются драйвером, обрабатывались корректно. Например, завершались с ошибкой. После инициализации массива обычно инициализируются обработчики для тех запросов, за которые драйвер отвечает.
Также инициализируется поле DriverUnload структуры, которое содержит адрес обработчика, отвечающего за завершение работы драйвера. Это поле может быть оставлено непроинициализированным, в таком случае драйвер становится невыгружаемым.
Обратите внимание на то, что в поле DriverExtension->AddDevice устанавливается адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство, за работу которого драйвер отвечает. Данное поле может быть оставлено непроинициализированным, в таком случае драйвер не сможет обрабатывать это событие.
Более подробно данная структура описана по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff544174(v=vs.85).aspx.
Структура DEVICE_OBJECT
Структура DEVICE_OBJECT представляет ту или иную функциональность драйвера. Т.е. эта структура может представлять физическое устройство, логическое устройство, виртуальное устройство или просто некий функционал, предоставляемый драйвером. Поэтому когда система будет посылать запросы, то в самом запросе она будет указывать адрес этой структуры. Таким образом, драйвер сможет определить, какой функционал от него запрашивается. Если не использовать такую модель, тогда драйвер может обрабатывать только какую-нибудь одну функциональность, а в современном мире это недопустимо. Прототип функции, которая обрабатывает конкретный запрос, приведена ниже.
NTSTATUS Dispatch( PDEVICE_OBJECT DeviceObject, PIRP Irp );
Массив MajorFunction ранее упомянутой структуры DRIVER_OBJECT содержит адреса обработчиков именно с таким прототипом.
Сама структура DEVICE_OBJECT всегда создается драйвером при помощи функции IoCreateDevice. Если система посылает запрос драйверу, то она всегда направляет его какому-либо DEVICE_OBJECT, как это следует из вышепредставленного прототипа. Также, прототип принимает второй параметр, который содержит адрес IRP-структуры. Эта структура описывает сам запрос, и она существует в памяти до тех пор, пока драйвер не завершит его. Запрос отправляется драйверу на обработку при помощи функции IoCallDriver как системой, так и другими драйверами.
Также со структурой DEVICE_OBJECT может быть связано имя. Таким образом, этот DEVICE_OBJECT может быть найден в системе.
Более подробно структура DEVICE_OBJECT описана по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff543147(v=vs.85).aspx. А структура IRP описана по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff550694(v=vs.85).aspx.
Фильтрация
Фильтрация являет собой механизм, который позволяет перехватывать все запросы, направленные к конкретному 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 содержит адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство. Прототип этого обработчика изображен ниже.
NTSTATUS DispatchAddDevice(
PDRIVER_OBJECT DriverObject,
PDEVICE_OBJECT PhysicalDeviceObject
);
Т.е. всякий раз, когда 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. Для драйвера этот запрос означает, что устройство более никем не используется, и оно может быть безопасно удалено.
Более подробную информацию о модели драйверов WDM можно найти по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff548158(v=vs.85).aspx.
Суть решения
Суть самого решения заключается в создании верхнего фильтра класса 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 представлен ниже.
NTSTATUS UsbCreateAndAttachFilter(
PDEVICE_OBJECT PhysicalDeviceObject,
bool UpperFilter
) {
SUSBDevice* USBDevice;
PDEVICE_OBJECT USBDeviceObject = nullptr;
ULONG Flags;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
for ( ;; ) {
// если нижний фильтр уже прикреплен, тогда здесь больше делать нечего
if ( !UpperFilter ) {
USBDeviceObject = PhysicalDeviceObject;
while ( USBDeviceObject->AttachedDevice ) {
if ( USBDeviceObject->DriverObject == g_DriverObject ) {
return STATUS_SUCCESS;
}
USBDeviceObject = USBDeviceObject->AttachedDevice;
}
}
// создаем фильтр
Status = IoCreateDevice(
g_DriverObject,
sizeof( SUSBDevice ),
nullptr,
PhysicalDeviceObject->DeviceType,
PhysicalDeviceObject->Characteristics,
false,
&USBDeviceObject
);
if ( !NT_SUCCESS( Status ) ) {
break;
}
// инициализируем флаги созданного устройства, копируем их из объекта к
// которому прикрепились
Flags = PhysicalDeviceObject->Flags &
(DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE);
USBDeviceObject->Flags |= Flags;
// получаем указатель на нашу структуру
USBDevice = (SUSBDevice*)USBDeviceObject->DeviceExtension;
// инициализируем деструктор
USBDevice->DeleteDevice = DetachAndDeleteDevice;
// инициализируем обработчики
for ( ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
USBDevice->MajorFunction[i] = UsbDispatchCommon;
}
USBDevice->MajorFunction[IRP_MJ_PNP] = UsbDispatchPnp;
USBDevice->MajorFunction[IRP_MJ_POWER] = UsbDispatchPower;
// инициализируем семафор удаления устройства
IoInitializeRemoveLock(
&USBDevice->Lock,
USBDEVICE_REMOVE_LOCK_TAG,
0,
0
);
// заполняем структуру
USBDevice->SelfDevice = USBDeviceObject;
USBDevice->BaseDevice = PhysicalDeviceObject;
USBDevice->UpperFilter = UpperFilter;
// инициализируем paging семафор
USBDevice->PagingCount = 0;
KeInitializeEvent( &USBDevice->PagingLock, SynchronizationEvent, true );
// прикрепляем устройство к PDO
USBDevice->LowerDevice = IoAttachDeviceToDeviceStack(
USBDeviceObject,
PhysicalDeviceObject
);
if ( !USBDevice->LowerDevice ) {
Status = STATUS_NO_SUCH_DEVICE;
break;
}
break;
}
// в зависимости от результата делаем
if ( !NT_SUCCESS( Status ) ) {
// отчистку
if ( USBDeviceObject ) {
IoDeleteDevice( USBDeviceObject );
}
} else {
// или сбрасываем флаг инициализации
USBDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
}
return Status;
}
static NTSTATUS DispatchAddDevice(
PDRIVER_OBJECT DriverObject,
PDEVICE_OBJECT PhysicalDeviceObject
) {
UNREFERENCED_PARAMETER( DriverObject );
return UsbCreateAndAttachFilter( PhysicalDeviceObject, true );
}
Как следует из примера, мы создаем DEVICE_OBJECT и прикрепляем его к PDO. Таким образом, мы будем перехватывать все запросы, направленные к USB-шине.
В нашу задачу входит перехватывать запросы с minor кодом IRP_MN_START_DEVICE. Код обработчика этого запроса изображен ниже.
static NTSTATUS UsbDispatchPnpStartDevice( SUSBDevice* USBDevice, PIRP Irp ) {
bool HubOrComposite;
NTSTATUS Status;
PAGED_CODE();
for ( ;; ) {
// проверить, позволено ли устройству работать, также обновить
// информацию об устройстве, является ли оно хабом или композитным
Status = UsbIsDeviceAllowedToWork( &HubOrComposite, USBDevice );
if ( !NT_SUCCESS( Status ) ) {
break;
}
USBDevice->HubOrComposite = HubOrComposite;
// продвинуть запрос
Status = ForwardIrpSynchronously( USBDevice->LowerDevice, Irp );
if ( !NT_SUCCESS( Status ) ) {
break;
}
break;
}
// завершаем запрос
Irp->IoStatus.Status = Status;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
// и освобождаем устройство
IoReleaseRemoveLock( &USBDevice->Lock, Irp );
return Status;
}
Как изображено на рисунке, обработчик вызывает функцию UsbIsDeviceAllowedToWork. Эта функция выполняет все необходимые проверки, чтобы определить, разрешено ли устройству работать. В первую очередь функция позволяет всегда работать хабам и композитным устройствам, клавиатурам и мышам. А также тем устройствам, которые находятся в списке разрешенных. Если функция возвращает неуспешный код возврата, тогда запрос завершается с ошибкой. Таким образом, работа устройства будет заблокирована.
Обратите внимание: функция определяет, является ли устройство хабом или композитным устройством. Это необходимо потому, что, как уже было упомянуто, класс устройств, который используется для хабов и хост контроллеров, используется не только этими устройствами. А нам в первую очередь необходимо контролировать дочерние устройства только хабов, хост контроллеров и композитных устройств. Т.е. для хабов и композитных устройств дополнительно перехватывается запрос перечисления дочерних устройств, на этом этапе, важно также прикрепить ко всем дочерним устройствам фильтр, и этот фильтр будет нижним. В противном случае контроль над дочерними устройствами будет потерян.
Все упомянутые определения выполняются на основе идентификаторов устройств.
Заключение
Несмотря на свою простоту в моем случае данный драйвер достаточно эффективно решает поставленную задачу. Хотя из недостатков следует выделить обязательную перезагрузку после того, как список разрешенных устройств будет обновлен. Чтобы устранить этот недостаток, драйвер потребуется несколько усложнить. Еще большим недостатком является полное блокирование устройства, а не частичное. Описание, представленное выше, не раскрывает всех деталей реализации. Сделано это было намеренно, и упор был сделан больше на саму концепцию. Желающие разобраться во всем до конца могут ознакомиться с исходным кодом.
description | title | ms.date |
---|---|---|
In this topic you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio Professional 2019 to write a simple kernel-mode driver framework (KMDF)-based client driver. |
How to write your first USB client driver (KMDF) |
06/07/2019 |
How to write your first USB client driver (KMDF)
In this topic you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio Professional 2019 to write a simple kernel-mode driver framework (KMDF)-based client driver. After building and installing the client driver, you’ll view the client driver in Device Manager and view the driver output in a debugger.
For an explanation about the source code generated by the template, see Understanding the KMDF template code for a USB client driver.
Prerequisites
For developing, debugging, and installing a kernel-mode driver, you need two computers:
- A host computer running Windows 7 or a later version of the Windows operating system. The host computer is your development environment, where you write and debug your driver.
- A target computer running Windows Vista or a later version of Windows. The target computer has the kernel-mode driver that you want to debug.
Before you begin, make sure that you meet the following requirements:
Software requirements
- Your host computer hosts your development environment and has Visual Studio Professional 2019.
- Your host computer has the latest Windows Driver Kit (WDK) for Windows 8. The kit include headers, libraries, tools, documentation, and the debugging tools required to develop, build, and debug a KMDF driver. To get the latest version of the WDK, see Download the Windows Driver Kit (WDK).
- Your host computer has the latest version of debugging tools for Windows. You can get the latest version from the WDK or you can Download and Install Debugging Tools for Windows.
- Your target computer is running Windows Vista or a later version of Windows.
- Your host and target computers are configured for kernel debugging. For more information, see Setting Up a Network Connection in Visual Studio.
Hardware requirements
Get a USB device for which you will be writing the client driver. In most cases, you are provided with a USB device and its hardware specification. The specification describes device capabilities and the supported vendor commands. Use the specification to determine the functionality of the USB driver and the related design decisions.
If you are new to USB driver development, use the OSR USB FX2 learning kit to study USB samples included with the WDK. You can get the learning kit from OSR Online. It contains the USB FX2 device and all the required hardware specifications to implement a client driver.
You can also get a Microsoft USB Test Tool (MUTT) devices. MUTT hardware can be purchased from JJG Technologies. The device does not have installed firmware installed. To install firmware, download the MUTT software package from this Web site and run MUTTUtil.exe. For more information, see the documentation included with the package.
Recommended reading
- Concepts for All Driver Developers
- Device nodes and device stacks
- Getting started with Windows drivers
- Kernel-Mode Driver Framework
- Developing Drivers with Windows Driver Foundation, written by Penny Orwick and Guy Smith. For more information, see Developing Drivers with WDF.
Instructions
Step 1: Generate the KMDF driver code by using the Visual Studio Professional 2019 USB driver template
For instructions about generating KMDF driver code, see the steps in Writing a KMDF driver based on a template.
For USB-specific code, select the following options in Visual Studio Professional 2019
- In the New Project dialog box, in the search box at the top, type USB.
- In the middle pane, select Kernel Mode Driver, USB (KMDF).
- Select Next.
- Enter a project name, choose a save location, and select Create.
The following screen shots show the New Project dialog box for the USB Kernel-Mode Driver template.
This topic assumes that the name of the Visual Studio project is «MyUSBDriver_». It contains the following files:
Files | Description |
---|---|
Public.h | Provides common declarations shared by the client driver and user applications that communicate with the USB device. |
<Project name>.inf | Contains information required to install the client driver on the target computer. |
Trace.h | Declares tracing functions and macros. |
Driver.h; Driver.c | Declares and defines driver entry points and event callback routines. |
Device.h; Device.c | Declares and defines event callback routine for the prepare-hardware event. |
Queue.h; Queue.c | Declares and defines an event callback routine for the event raised by the framework’s queue object. |
Step 2: Modify the INF file to add information about your device
Before you build the driver, you must modify the template INF file with information about your device, specifically the hardware ID string.
In Solution Explorer, under Driver Files, double-click the INF file.
In the INF file you can provide information such as the manufacturer and provider name, the device setup class, and so on. One piece of information that you must provide is the hardware identifier of your device.
To provide the hardware ID string:
-
Attach your USB device to your host computer and let Windows enumerate the device.
-
Open Device Manager and open properties for your device.
-
On the Details tab, select Hardward Ids under Property.
The hardware ID for the device is displayed in the list box. Select and hold (or right-click) and copy the hardware ID string.
-
Replace USBVID_vvvv&PID_pppp in the following line with your hardware ID string.
[Standard.NT$ARCH$] %MyUSBDriver_.DeviceDesc%=MyUSBDriver__Device, USBVID_vvvv&PID_pppp
Step 3: Build the USB client driver code
To build your driver
- Open the driver project or solution in Visual Studio Professional 2019
- Select and hold (or right-click) the solution in the Solution Explorer and select Configuration Manager.
- From the Configuration Manager, select the Active Solution Configuration (for example, Windows 8 Debug or Windows 8 Release) and the Active Solution Platform (for example, Win32) that correspond to the type of build you’re interested in.
- From the Build menu, select Build Solution.
For more information, see Building a Driver.
Step 4: Configure a computer for testing and debugging
To test and debug a driver, you run the debugger on the host computer and the driver on the target computer. So far, you have used Visual Studio on the host computer to build a driver. Next you need to configure a target computer. To configure a target computer, follow the instructions in Provision a computer for driver deployment and testing.
Step 5: Enable tracing for kernel debugging
The template code contains several trace messages (TraceEvents) that can help you track function calls. All functions in the source code contain trace messages that mark the entry and exit of a routine. For errors, the trace message contains the error code and a meaningful string. Because WPP tracing is enabled for your driver project, the PDB symbol file created during the build process contains trace message formatting instructions. If you configure the host and target computers for WPP tracing, your driver can send trace messages to a file or the debugger.
To configure your host computer for WPP tracing
-
Create trace message format (TMF) files by extracting trace message formatting instructions from the PDB symbol file.
You can use Tracepdb.exe to create TMF files. The tool is located in the <install folder>Windows Kits8.0bin<architecture> folder of the WDK. The following command creates TMF files for the driver project.
tracepdb -f [PDBFiles] -p [TMFDirectory]
The -f option specifies the location and the name of the PDB symbol file. The -p option specifies the location for the TMF files that are created by Tracepdb. For more information, see Tracepdb Commands.
At the specified location you’ll see three files (one per .c file in the project). They are given GUID file names.
-
In the debugger, type the following commands:
-
.load Wmitrace
Loads the Wmitrace.dll extension.
-
.chain
Verify that the debugger extension is loaded.
-
!wmitrace.searchpath +<TMF file location>
Add the location of the TMF files to the debugger extension’s search path.
The output resembles this:
Trace Format search path is: 'C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDE;c:driverstmf'
-
To configure your target computer for WPP tracing
-
Make sure you have the Tracelog tool on your target computer. The tool is located in the <install_folder>Windows Kits8.0Tools<arch> folder of the WDK. For more information, see Tracelog Command Syntax.
-
Open a Command Window and run as administrator.
-
Type the following command:
tracelog -start MyTrace -guid #c918ee71-68c7-4140-8f7d-c907abbcb05d -flag 0xFFFF -level 7-rt -kd
The command starts a trace session named MyTrace.
The guid argument specifies the GUID of the trace provider, which is the client driver. You can get the GUID from Trace.h in the Visual Studio Professional 2019 project. As another option, you can type the following command and specify the GUID in a .guid file. The file contains the GUID in hyphen format:
tracelog -start MyTrace -guid c:driversProvider.guid -flag 0xFFFF -level 7-rt -kd
You can stop the trace session by typing the following command:
tracelog -stop MyTrace
Step 6: Deploy the driver on the target computer
- In the Solution Explorer window, select and hold (or right-click) the <project name>Package , and choose Properties.
- In the left pane, navigate to Configuration Properties > Driver Install > Deployment.
- Check Enable deployment, and check Import into driver store.
- For Remote Computer Name, specify the name of the target computer.
- Select Install and Verify.
- Select Ok.
- On the Debug menu, choose Start Debugging, or press F5 on the keyboard.
Note Do not specify the hardware ID of your device under Hardware ID Driver Update. The hardware ID must be specified only in your driver’s information (INF) file.
For more information about deploying the driver to the target system in Visual Studio Professional 2019, see Deploying a Driver to a Test Computer.
You can also manually install the driver on the target computer by using Device Manager. If you want to install the driver from a command prompt, these utilities are available:
-
PnPUtil
This tool comes with the Windows. It is in WindowsSystem32. You can use this utility to add the driver to the driver store.
C:>pnputil /a m:MyDriver_.inf Microsoft PnP Utility Processing inf : MyDriver_.inf Driver package added successfully. Published name : oem22.inf
For more information, see PnPUtil Examples.
-
DevCon Update
This tool comes with the WDK. You can use it to install and update drivers.
devcon update c:windowsinfMyDriver_.inf USBVID_0547&PID_10025&34B08D76&0&6
Step 7: View the driver in Device Manager
-
Enter the following command to open Device Manager:
devmgmt
-
Verify that Device Manager shows a node for the following node:
Samples
MyUSBDriver_Device
Step 8: View the output in the debugger
Visual Studio first displays progress in the Output window. Then it opens the Debugger Immediate Window. Verify that trace messages appear in the debugger on the host computer. The output should look like this, where «MyUSBDriver_» is the name of the driver module:
[3]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverContextCleanup Entry [1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Entry [1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Exit [0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Entry [0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Exit
Related topics
Understanding the KMDF template code for a USB client driver
Getting started with USB client driver development
description | title | ms.date |
---|---|---|
In this topic you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio Professional 2019 to write a simple kernel-mode driver framework (KMDF)-based client driver. |
How to write your first USB client driver (KMDF) |
06/07/2019 |
How to write your first USB client driver (KMDF)
In this topic you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio Professional 2019 to write a simple kernel-mode driver framework (KMDF)-based client driver. After building and installing the client driver, you’ll view the client driver in Device Manager and view the driver output in a debugger.
For an explanation about the source code generated by the template, see Understanding the KMDF template code for a USB client driver.
Prerequisites
For developing, debugging, and installing a kernel-mode driver, you need two computers:
- A host computer running Windows 7 or a later version of the Windows operating system. The host computer is your development environment, where you write and debug your driver.
- A target computer running Windows Vista or a later version of Windows. The target computer has the kernel-mode driver that you want to debug.
Before you begin, make sure that you meet the following requirements:
Software requirements
- Your host computer hosts your development environment and has Visual Studio Professional 2019.
- Your host computer has the latest Windows Driver Kit (WDK) for Windows 8. The kit include headers, libraries, tools, documentation, and the debugging tools required to develop, build, and debug a KMDF driver. To get the latest version of the WDK, see Download the Windows Driver Kit (WDK).
- Your host computer has the latest version of debugging tools for Windows. You can get the latest version from the WDK or you can Download and Install Debugging Tools for Windows.
- Your target computer is running Windows Vista or a later version of Windows.
- Your host and target computers are configured for kernel debugging. For more information, see Setting Up a Network Connection in Visual Studio.
Hardware requirements
Get a USB device for which you will be writing the client driver. In most cases, you are provided with a USB device and its hardware specification. The specification describes device capabilities and the supported vendor commands. Use the specification to determine the functionality of the USB driver and the related design decisions.
If you are new to USB driver development, use the OSR USB FX2 learning kit to study USB samples included with the WDK. You can get the learning kit from OSR Online. It contains the USB FX2 device and all the required hardware specifications to implement a client driver.
You can also get a Microsoft USB Test Tool (MUTT) devices. MUTT hardware can be purchased from JJG Technologies. The device does not have installed firmware installed. To install firmware, download the MUTT software package from this Web site and run MUTTUtil.exe. For more information, see the documentation included with the package.
Recommended reading
- Concepts for All Driver Developers
- Device nodes and device stacks
- Getting started with Windows drivers
- Kernel-Mode Driver Framework
- Developing Drivers with Windows Driver Foundation, written by Penny Orwick and Guy Smith. For more information, see Developing Drivers with WDF.
Instructions
Step 1: Generate the KMDF driver code by using the Visual Studio Professional 2019 USB driver template
For instructions about generating KMDF driver code, see the steps in Writing a KMDF driver based on a template.
For USB-specific code, select the following options in Visual Studio Professional 2019
- In the New Project dialog box, in the search box at the top, type USB.
- In the middle pane, select Kernel Mode Driver, USB (KMDF).
- Select Next.
- Enter a project name, choose a save location, and select Create.
The following screen shots show the New Project dialog box for the USB Kernel-Mode Driver template.
This topic assumes that the name of the Visual Studio project is «MyUSBDriver_». It contains the following files:
Files | Description |
---|---|
Public.h | Provides common declarations shared by the client driver and user applications that communicate with the USB device. |
<Project name>.inf | Contains information required to install the client driver on the target computer. |
Trace.h | Declares tracing functions and macros. |
Driver.h; Driver.c | Declares and defines driver entry points and event callback routines. |
Device.h; Device.c | Declares and defines event callback routine for the prepare-hardware event. |
Queue.h; Queue.c | Declares and defines an event callback routine for the event raised by the framework’s queue object. |
Step 2: Modify the INF file to add information about your device
Before you build the driver, you must modify the template INF file with information about your device, specifically the hardware ID string.
In Solution Explorer, under Driver Files, double-click the INF file.
In the INF file you can provide information such as the manufacturer and provider name, the device setup class, and so on. One piece of information that you must provide is the hardware identifier of your device.
To provide the hardware ID string:
-
Attach your USB device to your host computer and let Windows enumerate the device.
-
Open Device Manager and open properties for your device.
-
On the Details tab, select Hardward Ids under Property.
The hardware ID for the device is displayed in the list box. Select and hold (or right-click) and copy the hardware ID string.
-
Replace USBVID_vvvv&PID_pppp in the following line with your hardware ID string.
[Standard.NT$ARCH$] %MyUSBDriver_.DeviceDesc%=MyUSBDriver__Device, USBVID_vvvv&PID_pppp
Step 3: Build the USB client driver code
To build your driver
- Open the driver project or solution in Visual Studio Professional 2019
- Select and hold (or right-click) the solution in the Solution Explorer and select Configuration Manager.
- From the Configuration Manager, select the Active Solution Configuration (for example, Windows 8 Debug or Windows 8 Release) and the Active Solution Platform (for example, Win32) that correspond to the type of build you’re interested in.
- From the Build menu, select Build Solution.
For more information, see Building a Driver.
Step 4: Configure a computer for testing and debugging
To test and debug a driver, you run the debugger on the host computer and the driver on the target computer. So far, you have used Visual Studio on the host computer to build a driver. Next you need to configure a target computer. To configure a target computer, follow the instructions in Provision a computer for driver deployment and testing.
Step 5: Enable tracing for kernel debugging
The template code contains several trace messages (TraceEvents) that can help you track function calls. All functions in the source code contain trace messages that mark the entry and exit of a routine. For errors, the trace message contains the error code and a meaningful string. Because WPP tracing is enabled for your driver project, the PDB symbol file created during the build process contains trace message formatting instructions. If you configure the host and target computers for WPP tracing, your driver can send trace messages to a file or the debugger.
To configure your host computer for WPP tracing
-
Create trace message format (TMF) files by extracting trace message formatting instructions from the PDB symbol file.
You can use Tracepdb.exe to create TMF files. The tool is located in the <install folder>Windows Kits8.0bin<architecture> folder of the WDK. The following command creates TMF files for the driver project.
tracepdb -f [PDBFiles] -p [TMFDirectory]
The -f option specifies the location and the name of the PDB symbol file. The -p option specifies the location for the TMF files that are created by Tracepdb. For more information, see Tracepdb Commands.
At the specified location you’ll see three files (one per .c file in the project). They are given GUID file names.
-
In the debugger, type the following commands:
-
.load Wmitrace
Loads the Wmitrace.dll extension.
-
.chain
Verify that the debugger extension is loaded.
-
!wmitrace.searchpath +<TMF file location>
Add the location of the TMF files to the debugger extension’s search path.
The output resembles this:
Trace Format search path is: 'C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDE;c:driverstmf'
-
To configure your target computer for WPP tracing
-
Make sure you have the Tracelog tool on your target computer. The tool is located in the <install_folder>Windows Kits8.0Tools<arch> folder of the WDK. For more information, see Tracelog Command Syntax.
-
Open a Command Window and run as administrator.
-
Type the following command:
tracelog -start MyTrace -guid #c918ee71-68c7-4140-8f7d-c907abbcb05d -flag 0xFFFF -level 7-rt -kd
The command starts a trace session named MyTrace.
The guid argument specifies the GUID of the trace provider, which is the client driver. You can get the GUID from Trace.h in the Visual Studio Professional 2019 project. As another option, you can type the following command and specify the GUID in a .guid file. The file contains the GUID in hyphen format:
tracelog -start MyTrace -guid c:driversProvider.guid -flag 0xFFFF -level 7-rt -kd
You can stop the trace session by typing the following command:
tracelog -stop MyTrace
Step 6: Deploy the driver on the target computer
- In the Solution Explorer window, select and hold (or right-click) the <project name>Package , and choose Properties.
- In the left pane, navigate to Configuration Properties > Driver Install > Deployment.
- Check Enable deployment, and check Import into driver store.
- For Remote Computer Name, specify the name of the target computer.
- Select Install and Verify.
- Select Ok.
- On the Debug menu, choose Start Debugging, or press F5 on the keyboard.
Note Do not specify the hardware ID of your device under Hardware ID Driver Update. The hardware ID must be specified only in your driver’s information (INF) file.
For more information about deploying the driver to the target system in Visual Studio Professional 2019, see Deploying a Driver to a Test Computer.
You can also manually install the driver on the target computer by using Device Manager. If you want to install the driver from a command prompt, these utilities are available:
-
PnPUtil
This tool comes with the Windows. It is in WindowsSystem32. You can use this utility to add the driver to the driver store.
C:>pnputil /a m:MyDriver_.inf Microsoft PnP Utility Processing inf : MyDriver_.inf Driver package added successfully. Published name : oem22.inf
For more information, see PnPUtil Examples.
-
DevCon Update
This tool comes with the WDK. You can use it to install and update drivers.
devcon update c:windowsinfMyDriver_.inf USBVID_0547&PID_10025&34B08D76&0&6
Step 7: View the driver in Device Manager
-
Enter the following command to open Device Manager:
devmgmt
-
Verify that Device Manager shows a node for the following node:
Samples
MyUSBDriver_Device
Step 8: View the output in the debugger
Visual Studio first displays progress in the Output window. Then it opens the Debugger Immediate Window. Verify that trace messages appear in the debugger on the host computer. The output should look like this, where «MyUSBDriver_» is the name of the driver module:
[3]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverContextCleanup Entry [1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Entry [1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Exit [0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Entry [0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Exit
Related topics
Understanding the KMDF template code for a USB client driver
Getting started with USB client driver development
Урок 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 – Та же ошибка в программе пользовательского режима
Подход к написанию драйвера USB устройства аналогичен драйверу PCI: драйвер регистрирует свой объект драйвера с USB подсистемой и затем использует идентификаторы поставщика и устройства для сообщения, когда его оборудование было установлено.
Какие устройства поддерживает драйвер?
Структура struct usb_device_id содержит список различных типов USB устройств, которые поддерживает этот драйвер. Этот список используется ядром USB, чтобы решить, какой драйвер предоставить устройству, или скриптами горячего подключения, чтобы решить, какой драйвер автоматически загрузить, когда устройство подключается к системе.
Структура struct usb_device_id определена со следующими полями:
__u16 match_flags
Определяет, какие из следующих полей в структуре устройства должны сопоставляться. Это битовое поле определяется разными значениями USB_DEVICE_ID_MATCH_*, указанными в файле include/linux/mod_devicetable.h. Это поле, как правило, никогда не устанавливается напрямую, а инициализируется с помощью макросов типа USB_DEVICE, описываемых ниже.
__u16 idVendor
Идентификатор поставщика USB для устройства. Этот номер присваивается форумом USB для своих членов и не может быть присвоен кем-то еще.
__u16 idProduct
Идентификатор продукта USB для устройства. Все поставщики, которые имеют выданный им идентификатор поставщика, могут управлять своими идентификаторами продукта, как они предпочитают.
__u16 bcdDevice_lo
__u16 bcdDevice_hi
Определяют нижнюю и верхнюю границу диапазона назначаемого поставщиком номера версии продукта. Значения bcdDevice_hi является включительным; его значение является значением наибольшего номера устройства. Обе эти величины представлены в двоично-десятичной (BCD) форме. Эти переменные в сочетании с idVendor и idProduct используются для определения данного варианта устройства.
__u8 bDeviceClass
__u8 bDeviceSubClass
__u8 bDeviceProtocol
Определяют класс, подкласс и протокол устройства, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB. Эти значения определяют поведение для всего устройства, в том числе все интерфейсы на этом устройстве.
__u8 bInterfaceClass
__u8 bInterfaceSubClass
__u8 bInterfaceProtocol
Подобно зависимым от устройства вышеприведённым величинам, эти определяют класса, подкласс и протокол отдельного интерфейса, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB.
kernel_ulong_t driver_info
Это значение не используется для сравнения, но оно содержит информацию о том, что драйвер может использовать, чтобы отличать разные устройства друг от друга в функции обратного вызова probe драйвера USB.
Как и с PCI устройствами, существует ряд макросов, которые используются для инициализации этой структуры:
USB_DEVICE(vendor, product)
Создаёт struct usb_device_id, которая может быть использована только для соответствия указанными значениям идентификаторов поставщика и продукта. Она очень часто используется для устройств USB, которым необходим специальный драйвер.
USB_DEVICE_VER(vendor, product, lo, hi)
Создаёт struct usb_device_id, которая может быть использована только для соответствия указанным значениям идентификаторов поставщика и продукта внутри диапазона версий.
USB_DEVICE_INFO(class, subclass, protocol)
Создаёт struct usb_device_id, которая может быть использованы для соответствия определённому классу USB устройств.
USB_INTERFACE_INFO(class, subclass, protocol)
Создаёт struct usb_device_id, которая может быть использована для соответствия определённому классу USB интерфейсов.
Итак, для простого драйвера USB устройства, который управляет только одним USB устройством от одного поставщика, таблица struct usb_device_id будет определяться как:
/* таблица устройств, которые работают с этим драйвером */
static struct usb_device_id skel_table [ ] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
{ } /* Завершающая запись */
};
MODULE_DEVICE_TABLE (usb, skel_table);
Как и с драйвером PCI, необходим макрос MODULE_DEVICE_TABLE, чтобы разрешить инструментам пространства пользователя выяснить, какими устройствами может управлять этот драйвер. Но для USB драйверов первым значением в этом макросе должна быть строка usb.
Регистрация USB драйвера
Основной структурой, которую должны создать все USB драйверы, является struct usb_driver. Эта структура должна быть заполнена драйвером USB и состоит из ряда функций обратного вызова и переменных, описывающих USB драйвер для кода USB ядра:
struct module *owner
Указатель на модуль владельца этого драйвера. Ядро USB использует его для правильного подсчёта ссылок на этот драйвер USB, чтобы он не выгружался в несвоевременные моменты. Переменной должен быть присвоен макрос THIS_MODULE.
const char *name
Указатель на имя драйвера. Он должен быть уникальным среди всех USB драйверов в ядре и, как правило, установлен на такое же имя, что и имя модуля драйвера. Оно проявляется в sysfs в /sys/bus/usb/drivers/, когда драйвер находится в ядре.
const struct usb_device_id *id_table
Указатель на таблицу struct usb_device_id, которая содержит список всех различных видов устройств USB, которые драйвер может распознать. Если эта переменная не установлена, функция обратного вызова probe в драйвере USB никогда не вызывается. Если вы хотите, чтобы ваш драйвер всегда вызывался для каждого USB устройства в системе, создайте запись, которая устанавливает только поле driver_info:
static struct usb_device_id usb_ids[ ] = {
{.driver_info = 42},
{ }
};
int (*probe) (struct usb_interface *intf, const struct usb_device_id *id)
Указатель на зондирующую функцию в USB драйвере. Эта функция (описанная в разделе «probe и disconnect в деталях») вызывается USB ядром, когда оно думает, что оно имеет структуру usb_interface, которую этот драйвер может обработать. Указатель на struct usb_device_id, который использовало USB ядро, чтобы принять это решение также передается в эту функцию. Если USB драйвер признаёт переданную ему структуру usb_interface, он должен правильно проинициализировать устройство и вернуть 0. Если драйвер не хочет признавать устройство или произошла ошибка, он должен вернуть отрицательное значение ошибки.
void (*disconnect) (struct usb_interface *intf)
Указатель на функцию отключения в USB драйвере. Эта функция (описанная в разделе «probe и disconnect в деталях») вызывается USB ядром, когда структура usb_interface была удалена из системы, или когда драйвер выгружается из ядра USB.
Таким образом, чтобы создать значимую структуру struct usb_driver, должны быть проинициализированы только пять полей:
static struct usb_driver skel_driver = {
.owner = THIS_MODULE,
.name = «skeleton»,
.id_table = skel_table,
.probe = skel_probe,
.disconnect = skel_disconnect,
};
struct usb_driver содержит несколько больше обратных вызовов, которые, как правило, очень часто не используются, и не требуются для правильной работы USB драйвера:
int (*ioctl) (struct usb_interface *intf, unsigned int code, void *buf)
Указатель на функцию ioctl в USB драйвере. Если он присутствует, то вызывается, когда программа пользовательского пространства делает вызов ioctl для записи файловой системы устройств usbfs, связанной с устройством USB, относящемуся к этому USB драйверу. На практике только драйвер USB концентратора использует этот ioctl, так как любому другому USB драйверу нет иной реальной необходимости его использовать.
int (*suspend) (struct usb_interface *intf, u32 state)
Указатель на функцию приостановки в USB драйвере. Она вызывается, когда работа устройства должна быть приостановлена USB ядром.
int (*resume) (struct usb_interface *intf)
Указатель на функцию возобновления в USB драйвере. Она вызывается, когда работа устройства возобновляется USB ядром.
Чтобы зарегистрировать struct usb_driver в USB ядре, выполняется вызов usb_register_driver с указателем на struct usb_driver. Для USB драйвера это традиционно делается в коде инициализации модуле:
static int __init usb_skel_init(void)
{
int result;
/* регистрируем этот драйвер в подсистеме USB */
result = usb_register(&skel_driver);
if (result)
err(«usb_register failed. Error number %d», result);
return result;
}
Когда драйвер USB будет выгружаться, необходимо разрегистрировать struct usb_driver в ядре. Это делается с помощью вызова usb_deregister. Когда происходит этот вызов, любые USB интерфейсы, которые в настоящее время связаны с этим драйвером, отключаются и для них вызывается функция disconnect.
static void __exit usb_skel_exit(void)
{
/* отменяем регистрацию этого драйвера в подсистеме USB */
usb_deregister(&skel_driver);
}
probe и disconnect в деталях
В структуре struct usb_driver structure, описанной в предыдущем разделе, драйвер указывает две функции, которые в соответствующее время вызывает ядро USB. Функция probe вызывается, когда установлено устройство, которым, как думает ядро USB, должен управлять этот драйвер; функция probe должна выполнять проверки информации, переданной ей об устройстве, и решать, действительно ли этот драйвер подходит для этого устройства. Функция disconnect вызывается, когда по каким-то причинам драйвер не должен больше управлять устройством и может делать очистку.
Оба функции обратного вызова probe и disconnect вызываются в контексте потока USB узла ядра, так что засыпать в них допускается. Тем не менее, рекомендуется, чтобы большая часть работы выполнялась, когда устройство открыто пользователем, если это возможно, чтобы сократить время зондирования USB к минимуму. Такое требование появляется потому, что USB ядро обрабатывает добавление и удаление устройств USB в одном потоке, так что любой медленный драйвер устройства может привести к замедлению обнаружения USB устройства и это станет заметно для пользователя.
В функции обратного вызова probe, USB драйвер должен проинициализировать любые локальные структуры, которые он может использовать для управления USB устройством. Следует также сохранить в локальные структуры любую необходимую информацию об устройстве, так как это обычно легче сделать в данное время. Например, USB драйверы обычно хотят обнаружить адрес оконечной точки и размеры буферов для данного устройства, так как они необходимы для общения с устройством. Вот пример некоторого кода, который определяет две оконечные точки ВХОДА и ВЫХОДА поточного типа и сохраняет некоторую информацию о них в локальной структуре устройства:
/* установить информацию оконечной точки */
/* используем только первые поточные точки входа и выхода */
iface_desc = interface->cur_altsetting;
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
if (!dev->bulk_in_endpointAddr &&
(endpoint->bEndpointAddress & USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)
== USB_ENDPOINT_XFER_BULK)) {
/* мы нашли оконечную точку входного потока */
buffer_size = endpoint->wMaxPacketSize;
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!dev->bulk_in_buffer) {
err(«Could not allocate bulk_in_buffer»);
goto error;
}
}
if (!dev->bulk_out_endpointAddr &&
!(endpoint->bEndpointAddress & USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)
== USB_ENDPOINT_XFER_BULK)) {
/* мы нашли оконечную точку выходного потока */
dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
}
}
if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
err(«Could not find both bulk-in and bulk-out endpoints»);
goto error;
}
Этот блок кода сначала в цикле обходит каждую оконечную точку, которая присутствует в этом интерфейсе, и назначает локальный указатель для структуры оконечной точки для облегчения доступа впоследствии:
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
Затем, после того, как мы получили оконечную точку, и если мы уже не нашли ВХОДНУЮ оконечную точку поточного типа, мы проверяем, является ли направление этой оконечной точки ВХОДНЫМ. Это может быть проверено просмотром, содержится ли битовая маска USB_DIR_IN в переменной bEndpointAddress оконечной точки. Если это так, мы определяем, имеет ли оконечная точки тип поточной или нет, сначала накладывая битовую маску USB_ENDPOINT_XFERTYPE_MASK на переменную bmAttributes, а затем проверяя, совпадает ли она со значением USB_ENDPOINT_XFER_BULK:
if (!dev->bulk_in_endpointAddr &&
(endpoint->bEndpointAddress & USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)
== USB_ENDPOINT_XFER_BULK)) {
Если все эти тесты успешны, драйвер знает, что нашёл оконечную точку правильного типа и может сохранить информацию об оконечной точке, в которой позднее будет нуждаться для общения через неё, в локальной структуре:
/* мы нашли оконечную точку входного потока */
buffer_size = endpoint->wMaxPacketSize;
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!dev->bulk_in_buffer) {
err(«Could not allocate bulk_in_buffer»);
goto error;
}
Поскольку драйверу USB позднее в жизненном цикле устройства необходимо получать локальные структуры данных, связанные с этой struct usb_interface, может быть вызвана функция usb_set_intfdata:
/* сохраняем наш указатель на данные в этом интерфейсе устройства */
usb_set_intfdata(interface, dev);
Эта функция принимает указатель на любой тип данных и сохраняет его в структуре struct usb_interface для последующего доступа. Для получения данных должна быть вызвана функция usb_get_intfdata:
struct usb_skel *dev;
struct usb_interface *interface;
int subminor;
int retval = 0;
subminor = iminor(inode);
interface = usb_find_interface(&skel_driver, subminor);
if (!interface) {
err («%s — error, can’t find device for minor %d»,
__FUNCTION__, subminor);
retval = -ENODEV;
goto exit;
}
dev = usb_get_intfdata(interface);
if (!dev) {
retval = -ENODEV;
goto exit;
}
usb_get_intfdata обычно вызывается в функции open USB драйвера и снова в функции disconnect. Благодаря этим двум функциям USB драйверам не требуется держать статический массив указателей, которые хранят отдельные структуры устройства для всех текущих устройств в системе. Косвенная ссылка на информацию об устройстве позволяет любому USB драйверу поддерживать неограниченное количество устройств.
Если USB драйвер не связан с другим типом подсистемы, которая обрабатывает взаимодействие пользователя с устройством (такой, как ввод, терминал, видео и так далее), драйвер может использовать старший номер USB, чтобы использовать традиционный интерфейс символьного драйвера с пользовательским пространством. Чтобы сделать это, драйвер USB должен вызвать функцию usb_register_dev в функции probe, когда он хочет зарегистрировать устройство в USB ядре. Убедитесь, что устройство и драйвер находятся в надлежащем состоянии, чтобы выполнить желание пользователя получить доступ к устройству, как только вызвана эта функция.
/* мы можем зарегистрировать это устройство сейчас, так как оно готово */
retval = usb_register_dev(interface, &skel_class);
if (retval) {
/* что-то помешало зарегистрировать этот драйвер */
err(«Not able to get a minor for this device.»);
usb_set_intfdata(interface, NULL);
goto error;
}
Функция usb_register_dev требует указатель на struct usb_interface и указатель на struct usb_class_driver. struct usb_class_driver используется для определения ряда различных параметров, о которых драйвер USB желает, чтобы их знало USB ядро при регистрации на младший номер. Эта структура состоит из следующих переменных:
char *name
Имя, которое использует sysfs для описания устройства. Головное имя пути, если присутствует, используется только в devfs и в этой книге не рассматривается. Если ряду устройств необходимо быть в этом имени, в строке имени должны быть символы %d. Например, чтобы создать в devfs имя usb/foo1 и в sysfs имя класса foo1, строка имени должна быть установлена как usb/foo%d.
struct file_operations *fops;
Указатель на struct file_operations, которую этот драйвер определил, чтобы использовать для регистрации в качестве символьного устройства. Смотрите Главу 3 для получения дополнительной информации об этой структуре.
mode_t mode;
Режим для файла devfs, который будет создан для этого драйвера; иначе неиспользуемый. Типичный установкой для этой переменной будет значение S_IRUSR в сочетании со значением S_IWUSR, которыми владелец файла устройства предоставит доступ только для чтения и записи.
int minor_base;
Это начало установленного младшего диапазона для этого драйвера. Все устройства, связанные с этим драйвером, создаются с уникальными, увеличивающимися младшими номерам, начиная с этого значения. Если в ядре была включена опция конфигурации CONFIG_USB_DYNAMIC_MINORS, в любой момент допускается только 16 устройств, связанных с этим драйвером. Если это так, эта переменная игнорируется и все младшие номера для этого устройства распределяются по принципу «первый пришёл, первым обслужен». Рекомендуется, чтобы системы, которые имеют эту опцию разрешённой, использовали такие программы, как udev для управления узлами устройств в системе, так как статическое дерево /dev не будет работать должным образом.
После отключения USB устройства, все ресурсы, связанные с устройством должны быть очищены, если это возможно. В это время, если в функции probe для выделения младшего номера для этого USB устройства была вызвана usb_register_dev, должна быть вызвана функция usb_deregister_dev, чтобы вернуть USB ядру младший номер обратно.
В функции disconnect также важно извлечь из этого интерфейса все данные, которые была ранее установлены вызовом usb_set_intfdata. Затем установить указатель на данные в структуре struct usb_interface в NULL, чтобы предотвратить дальнейшие ошибки при доступе к данным ненадлежащим образом:
static void skel_disconnect(struct usb_interface *interface)
{
struct usb_skel *dev;
int minor = interface->minor;
/* предохраняем skel_open( ) от гонки со skel_disconnect( ) */
lock_kernel( );
dev = usb_get_intfdata(interface);
usb_set_intfdata(interface, NULL);
/* возвращаем наш младший номер */
usb_deregister_dev(interface, &skel_class);
unlock_kernel( );
/* уменьшаем наш счётчик использования */
kref_put(&dev->kref, skel_delete);
info(«USB Skeleton #%d now disconnected», minor);
}
Обратите внимание на вызов lock_kernel в предыдущем фрагменте кода. Он получает большую блокировку ядра, чтобы обратный вызов disconnect не находился в состоянии гонки с вызовом open при попытке получить указатель на правильную структуру данных интерфейса. Поскольку open вызывается с полученной большой блокировкой ядра, если disconnect также получает эту блокировку, только одна часть драйвера может получить доступ, и затем установить указатель данных интерфейса.
Перед вызовом для устройства USB функции disconnect все urb-ы, которые в настоящее время находятся в процессе передачи для устройства, будут отменены ядром USB, поэтому драйвер не должен явно вызывать usb_kill_urb для этих urb-ов. Если драйвер пытается отправить urb в USB устройство после того, как оно было отключено вызовом usb_submit_urb, отправка завершится неудачно с ошибочным значением -EPIPE.
Отправка и управление Urb
Когда драйвер имеет данные для передачи в USB устройство (как обычно бывает в функции записи драйвера), для передачи данных на устройство должен быть создан urb:
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
retval = -ENOMEM;
goto error;
}
После успешного создания urb-а, для отправки данных в устройство наиболее эффективным образом также должен быть создан буфер DMA и данные, которые переданы в драйвер, должны быть скопированы в этот буфер:
buf = usb_buffer_alloc(dev->udev, count, GFP_KERNEL, &urb->transfer_dma);
if (!buf) {
retval = -ENOMEM;
goto error;
}
if (copy_from_user(buf, user_buffer, count)) {
retval = -EFAULT;
goto error;
}
После того как данные должным образом скопированы из пространства пользователя в локальный буфер, urb должен быть правильно проинициализирован, прежде чем он может быть отправлен в ядро USB:
/* проинициализируем urb надлежащим образом */
usb_fill_bulk_urb(urb, dev->udev,
usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
buf, count, skel_write_bulk_callback, dev);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
Теперь, когда должным образом выделен urb, должным образом скопированы данные и urb проинициализирован соответствующим образом, он может быть отправлен в ядро USB для передачи в устройство:
/* отправляем данные из поточного порта */
retval = usb_submit_urb(urb, GFP_KERNEL);
if (retval) {
err(«%s — failed submitting write urb, error %d», __FUNCTION__, retval);
goto error;
}
После того, как urb успешно передан в USB устройство (или что-то произошло при передаче), USB ядром выполняется обратный вызов urb. В нашем примере мы проинициализировали urb для указания на функцию skel_write_bulk_callback и это та самая функция, которая вызывается:
static void skel_write_bulk_callback(struct urb *urb, struct pt_regs *regs)
{
/* сообщения об синхронные/асинхронные разъединениях не являются ошибками */
if (urb->status &&
!(urb->status == -ENOENT ||
urb->status == -ECONNRESET ||
urb->status == -ESHUTDOWN)) {
dbg(«%s — nonzero write bulk status received: %d»,
__FUNCTION__, urb->status);
}
/* освобождаем наш выделенный буфер */
usb_buffer_free(urb->dev, urb->transfer_buffer_length, urb->transfer_buffer, urb->transfer_dma);
}
Первое вещью, которую делает функция обратного вызова, является проверка состояния urb-а для определения, завершён ли этот urb успешно или нет. Ошибочные значения, -ENOENT, -ECONNRESET и -ESHUTDOWN являются не реальными ошибками передачи, а просто сообщают об условиях, сопровождающих успешную передачу. (Смотрите список возможных ошибок для urb-ов, подробно изложенный в разделе «struct urb».) Затем обратный вызов освобождает выделенный буфер, который был выделен для передачи этого urb-а.
Для другого urb-а характерно быть отправленным в устройство во время выполнения функции обратного вызова urb-а. Это полезно, когда в устройство передаются потоковые данные. Помните, что обратный вызов urb-а работает в контексте прерывания, поэтому он не должен выполнять любые выделения памяти, удерживать какие-либо семафоры, или не делать чего-то другого, что может привести процесс к засыпанию. При отправке urb-а изнутри обратного вызова используйте флаг GFP_ATOMIC, чтобы приказать USB ядру не засыпать, если необходимо выделять новые куски памяти во время процесса отправки.
Предыстория
Для очередного проекта возникла необходимость написать простенький софтверный драйвер под 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.