Исходники
Статьи
Языки программирования
.NET Delphi Visual C++ Borland C++ Builder C/С++ и C# Базы Данных MySQL MSSQL Oracle PostgreSQL Interbase VisualFoxPro Веб-Мастеру PHP HTML Perl Java JavaScript Протоколы AJAX Технология Ajax Освоение Ajax Сети Беспроводные сети Локальные сети Сети хранения данных TCP/IP xDSL ATM Операционные системы Windows Linux Wap Книги и учебники
Скрипты
Магазин программиста
|
Александр Тарво,
2003.
Любое воспроизведение данного документа или его части без письменного разрешения автора запрещено и является нарушением авторского права. Исходный текст примеров к руководству: xdsp.zip Использование NuMega DriverStudio для написания WDM-драйверовДрайвер - системная программа, предназначенная для
управления
каким-либо физическим или виртуальным устройством компьютера. Драйверы устройств, как правило, - наиболее критичная часть программного обеспечения компьютеров. По иронии судьбы, это также и наиболее скрытая часть системы и программного обеспечения. Драйверы устройств системы Windows фирмы Microsoft не являются исключением. Наоборот, если в UNIX можно взять исходники ядра и помотреть, как там пишутся драйвера, то в Windows это вряд ли будет возможным. Вспомним первые персоналки и MS DOS, бывшую в то время практически единственным выбором для настольного ПК. Несмотря на всю ее просто ту, драйвера, конечно, присутствовали и в ней. Практически все дело ограничивалось накoпителями - дисководами, CD-ROM приводами, винчестерами, да элементарнейшими драйверами клавиатуры и дисплея. Для каждой программы, требующей большего, чем перечисленный набор оборудования, требовалось создавать собственный драйвер. Представьте себе, что вам требуется воспроизвести звук на имеющейся в компьютере звуковой карте. Если вы знаете ее модель и у вас есть хорошая документация, вы, потратив немало времени, напишете прoграмму, которая сделает все желаемое. По крайней мере, так утверждает идеология открытых систем в общем и Linux в частности. А если необходимо поддерживать две модели? Три? Двадцать? И это при учете того, что новая звуковая карта появляется не реже раза в полтора-два месяца? И все они могут быть подключены различными способами и общаться с компьютером через разнообразные шины? Естественный выход - возложить написание кода, специфичного для аппаратуры, на ее создателя. Да и фирма-производитель, наняв высококвалифицированных специалистов, справится с задачей намного эффективнее и быстрее. Во всех современных операционных системах так и поступают. Существуют требования, например, к драйверу звуковой карты, и пользователь устанавливает тот вариант, который соответствует его "железу". А программа-проигрыватель через вызовы системных функций указывает, что именно она хотела бы воспроизвести, не заботясь об особенностях аппаратуры. Данный труд не является руководством по написанию драйверов устройств под Windows - для этого пришлось бы писать пару толстых книжек. В нем рассматриваются вопросы написания простого драйвера PCI-устройства под Win 98/ME/2000 с использованием архитектуры драйверов WDM и пакета NuMega DriverStudio. В дальнейшем, будем считать, что нашей целью будет драйвер, работающий на компьютере с архитектурой ЦП i386 под управлением вышеупомянутых ОС. Наше гипотетическое устройство - это PCI - карточка, имеющая некоторое количество памяти. Прилагаемые к руководству исходные тексты драйвера и программ работы с ним были проверены на PCI-карточке XDSP680c. Руководство изначально писалось как методические указания для курса "Специальные компьютерные системы" специальности "Компьютерные системы и сети" Черниговского государственного технологического университета, но затем было переработано и дополнено. Предполагается, что читатель знаком с основами программирования под Win32 и средой Visual C++. 1. Общие сведения о драйверах устройств в системе Windows.Естественно, каждая операционная система имеет собственную архитектуру и свои особенности функционирования драйверов. Но практически во всех современных ОС можно выделить следующие особенности, характерные для работы подсистемы ввода-вывода:
В ОС Windows, как и в большинстве современных ОС, драйвера управляют буквально всем: работой с аппаратурой, поддержкой файловых систем различных типов, сетевых протоколов и т.п. Это дает определенные преимущества и делает систему более гибкой: например, для того, чтобы ОС стала "понимать" другой сетевой протокол, нужно всего лишь установить соответствующий драйвер. 1.1 Система ввода-вывода в Windows.На данный момент наиболее распространены два семейства ОС Windows: Windows NT, куда относятся Windows NT, 2000, XP, и Windows 9x (Win 95, 98, ME). При этом отмечается тенденция к отмиранию ветки 9х, хотя такие системы будут встречаться еще достаточно долго. Каждая ветка использует свою архитектуру ядра и подсистемы ввода-вывода. Поэтому естественно, написание драйверов для этих систем должно отличаться. В Windows 9x долгое время использовались .vxd - драйвера. Эта модель драйверов начинает свою историю еще с Windows 3.1. Для .vxd - драйверов сохранилась совместимость "снизу вверх": т.е. драйвер, написанный под Windows 3.1, будет нормально работать и под Windows 95, а может быть, и 98. Функции драйверов .vxd используются как Win32, так и Win16 приложениями. В Windows NT 4.0 появилась своя архитектура драйверов. Она ставила перед собой цели повышения устойчивости работы драйвера, переносимости с одной платформы на другую, поддержки многопроцессорности т.п. Вместе с тем архитектура драйверов Windows NT 4.0 была, что называется, "сырой" и недоработанной, хотя и очень перспективной. С выходом систем Win98 и Win2000 появилась новая архитектура драйверов - WDM (Windows Driver Model). Она развилась из архитектуры драйверов Windows NT 4.0 с небольшими изменениями. WDM - драйвера с равным успехом могут быть использованы как в Win 98, так и в Win 2000. Система Win 98 состоит как бы из двух слоев: User Mode (режим пользователя) и Kernel Mode (режим ядра). В режиме пользователя функционируют пользовательские приложения. Они работают в 3-м кольце защиты; каждая программа работает в своем виртуальном адресном пространстве. Для каждого DOS или Windows - приложения создается своя виртуальная машина (Virtual Machine, VM), задачей которой является виртуализация аппаратуры компьютера для данного приложения. Т.е. каждое приложение считает, что вся оперативная память и все остальные аппаратные ресурсы принадлежат только ему и приложение может обратиться к ним в любой момент. Ядро ОС содержи диспетчер виртуальных машин (Virtual Machine Manager, VMM). Задача VMM - корректно разрешать конфликты, возникающие при доступе к ресурсам системы из разных VM. Ядро, VMМ, виртуальные машины и драйвера виртуальных устройств (Virtual Device Drivers), естественно, работают в режиме ядра (Kernel Mode). Рис. 1. Подсистема ввода-вывода Win 98. В Windows 98 обработка запросов на ввод-вывод от приложений DOS и от старых Win16 - приложений отличается от обработки запросов новых Win32 - приложений. Для DOS - приложений создается своя виртуальная машина (DOS virtual machine), Win 16 и Win32 - приложения используют виртуальную машину Windows (System Virtual Machine). Обычно, когда приложение запрашивает операцию ввода-вывода (например, вызывает функцию API ReadFile - чтение из файла), этот запрос поступает в одну из системных DLL (в нашем случае - kernel32.dll). Оттуда запрос на операцию с внешним устройством передается сразу системным драйверам. Такая организация запроса Приложение -> dll -> Драйвер получила наибольшее распространение. Система Windows 2000 имеет другую архитектуру, отличную от Win98. Это обусловлено повышенными требованиями к надежности, защите и переносимости этой системы (теоретически, Win2000 - переносимая система, и существуют реализации Win2000 под системы Alpha, MIPS и др.). В настоящее время именно благодаря этим особенностям Win2000 завоевывает все большую популярность, поэтому стоит рассмотреть особенности ее архитектуры подробнее. Рис. 2 - главные компоненты Windows2000. Окружение Win2000 включает компоненты, которые работают в режиме пользователя (User mode) и в режиме ядра (Kernel mode). В режиме пользователя работают подсистема защиты, подсистема Win32-архитектуры (обеспечивает стандартные API - вызовы Windows), подсистема POSIX (обеспечение кроссплатформенности). В режиме ядра работают все основные компоненты системы: диспетчер ввода-вывода (I/O manager), диспетчер конфигурации (Configuration Manager), подсистема PnP, диспетчер управления энергопотреблением (Power Manager), диспетчер памяти (Memory Manager) и прочие жизненно необходимые службы. Драйвера в Win2000 включены в подсистему ввода-вывода. При этом драйвера тесно взаимодействуют практически со всеми компонентами ядра. Драйвера взаимодействуют с аппаратурой при помощи Hardware Abstraction Level, HAL (уровень абстракции аппаратуры). HAL - программный компонент ядра Win2000, который обеспечивает интерфейс ядра (в том числе и некоторых драйверов) с аппаратурой. Т.к. Win2000 - платформенно независимая система (уже сейчас есть версии Win2000 для процессоров Alpha и RISC), то HAL избавляет ядро от непосредственного общения с кэшем, прерываниями, шинами ввода-вывода и большинством прочих устройств, оставляя эту работу драйверам, специально написанным для данной системы. Таким образом, ядро системы представляется набором отдельных изолированных модулей с четко определенными внешними интерфейсами.
Все драйвера NT имеют множество стандартных методов драйвера, определенных системой, и, возможно, несколько специфических методов, определенных разработчиком. Драйвера Windows 2000 используют архитектуру WDM (Windows Driver Model). В Windows 2000 драйвера бывают следующих типов:
В свою очередь, существует три типа драйверов ядра, каждый тип имеет четко определенные структуру и функциональность.
Драйвера Windows 2000 должны удовлетворять следующим требованиям:
Система ввода-вывода Windows 2000 имеет следующие особенности:
2. Использование пакета NuMega Driver Studio для написания
|
31 | Сбой работы шины |
29 | Сбой в цепи питания |
28 | Запрос от другого процессора (в многопроцессорной системе) |
Прерывания, доступные устройствам В/В | |
2 | Выполнение DPC |
1 | Исключение защиты (page fault) |
0 | Passive level |
Для катастрофических событий ОС резервирует самые приоритетные прерывания (31 - 29). Для программных прерываний - прерывания с самым низким приоритетом (2 - 1). PassiveLevel - обычный режим работы драйвера. IRQL, предоставляемые для работы системных устройств, находятся где-то посредине нумерации уровней. О том, как эти прерывания сопрягаются с архитектурой компьютера, заботится HAL.
Естественно, в любой момент процессор может обрабатывать только один запрос на прерывание. Обработка поступившего прерывания прервется только в том случае, если поступит прерывание с более высоким приоритетом.
При проектировании процедуры обработки прерывания следует минимизировать время, которое будет затрачено на обработку прерывания. Иначе процессор будет чересчур долго обрабатывать прерывание и ни один процесс не сможет возобновить свою работу. Когда вызывается ISR первое, что она должна сделать сообщить оборудованию, что запрос на прерывание получен и обработан. После этого можно завершать обработку прерывания. Но как тогда обработать данные, поступившие от устройства, если мы сразу же завершим обработку прерывания? Для этого введен механизм вызова отложенных процедур (Deferred Procedure Call, DPC). Перед завершением работы ISR следует вызвать отложенную процедуру (DPC). DPC начнет выполнятся, как только процессор освободится от обработки прерываний. DriverWorks предоставляет класс KDeferredCall, в котором инкапсулируются данные и методы, необходимые для использования механизма DPC.
DriverWorks инкапсулирует все функции, необходимые для обработки прерываний, в классе KInterrupt. Экземпляр класса KInterrupt должен быть создан, как свойство в классе устройства. Пусть в нашем случае класс устройства называется MyDevice, объект класса KInterrupt - m_TheInterrupt. Далее в классе устройства описывается функция ISR:
BOOLEAN MyDevice::TheIsr(void);Далее, в методе OnStartDevice следует добавить код для привязки ISR к устройству:
status = m_TheInterrupt.InitializeAndConnect(pAssignedResource,Isr,Context,0,FALSE);где Context - значение без типа (void), передаваемое ISR.
Теперь осталось только добавить в конструктор следующий код:
VOID MyDevice::MyDevice(void) { . . . status = m_TheInterrupt.InitializeAndConnect(pAssignedResource, LinkTo(Isr), this, 0, FALSE ); . . . }Для отключения ISR следует вызвать метод Disconnect().
Естественно, данное описание не претендует быть полным описанием такой важной темы, как обработка прерываний и связанные с ней проблемы. Но в примере драйвера, описываемом ниже, отсутствует реакция на прерывания, а не упомянуть о них нельзя. Для более подробного обзора темы прерываний и DPC следует обратиться к документации DriverWorks или DDK.
Объекты для управления оборудованием
Как было упомянуто выше, объект устройства управляет работой устройства при помощи специальных объектов, управляющих работой оборудования - портами В/В, прерываниями, памятью, контроллерами ПДП. Драйвер создает эти объекты для представления физических параметров устройства.
Большинство периферийных устройств находятся на шинах компьютера. В современном компьютере есть несколько шин. Обычно процессор, внешняя кэш-память, и оперативная память находятся на высокоскоростной шине, архитектура которой специфична для данного типа процессора. Шина процессора соединена мостом со стандартной скоростной шиной, на которой находятся контроллеры дисплея, некоторые скоростные устройства. Архитектура этой шины может быть процессоро-независимой. Пример такой шины - PCI. Эта шина также может быть соединена мостом со вторичной локальной шиной, часто более медленной. На ней могут находиться контроллеры дисковых накопителей, сетевых адаптеров и т.п.
Периферийные устройства обычно имеют "на борту" регистры и диапазоны адресов памяти, при помощи которых реализуется интерфейс устройства с системой. Но добраться до них не так просто: процессор ведь физически использует другие механизмы для обращения к своим "родным" портам ввода-вывода и оперативной памяти. Для того, чтобы обратится к памяти и портам устройства, находящегося на локальной шине, процессор должен выполнить отображение (mapping) адресного пространства процессора и той шины, где находится наше устройство. В результате этой операции к участку памяти, физически находящийся в устройтсве, можно обращаться, как к участку оперативной памяти процессора. При таком обращении процессор переадресует запрос локальной шине. Но тут следует вспомнить об особенностях архитектуры Windows (да и практически любой современной ОС): ведь система поддерживает механизм виртуальной памяти! Пользовательские приложения теперь работают в своем адресном пространстве, а система, в том числе и драйвера, - в своем. Куда же будет отображена память устройства?
Ответ прост. Можно отобразить диапазон адресов устройства как на адресное пространство системы, так и на адресное пространство пользовательского процесса. Соответственно различаться будет и способ доступа к памяти устройства из приложения пользователя: в первом случае буфер с данными для записи или чтения будет передаваться драйверу из приложения, а в драйвере эти данные будут пересылаться устройству. Во втором случае приложение будет писать и читать данные в выделенный ему участок памяти, который находится в адресном пространстве процесса. Какой механизм выбрать - дело разработчика драйвера.
Объекты, представляющие адресное пространство периферийных устройств, представлены классами KPeripherialAdress, KIoRange, KMemoryRange, KIoregister, KMemoryRegister. KPeripherialAdress является базовым классом для большинства остальных классов управления диапазонами памяти и портов ввода-вывода. Сам класс KperipherialAdress в основном, не используется. Используются, в основном, следующие его подклассы:
KIoRange m_range; ... KIoRegister m_reg = m_range[6]; ...Применение KIoRegister упрощет процесс программирования и улучшает читабельность программы.
Стоит отметить, что немалая часть устройств могут общаються со своей памятью только словами. Длина слова зависит от устройства, и может колебаться в широких пределах. Обычно для PCI-устройств - 32 бит.
В документации настоятельно рекомендуется использовать только эти классы для управления оборудованием. Это связано с возможной переносимостью драйвера на другие платформы. При использовании этих классов, которые, в свою очередь, используют функции DDK для доступа к оборудованию, процесс портирования пройдет безболезненно, т.к. для доступа к устройству будет использован HAL. Если же программист будет пытаться управлять устройствами самостоятельно, то драйвер придется переписывать при переносе на другую платформу.
Есть еще одна причина, по которой стоит использовать эти классы: ведь с ними разрабатывать драйвер намного проще!
Объекты синхронизации
Как и все Windows - программы, драйвера являются частью многозадачной операционной системы, в которой выполняется множество процессов и потоков. Драйвер, как и программа, также может содержать несколько потоков. При этом, естественно, возникает проблема синхронизации работы этих потоков, совместного доступа к данным и т.п. Особенно актуальной эта проблема становится в многопроцессорной системе. Windows 2000 предназначается для работы в многопроцессорных системах, и если пренебречь синхронизацией при разработке драйвера, то это может повлечь за собой неприятные последствия.
Для решения задач синхронизации WDM (и, соответственно, DriverWorks) предлагает различные средства. Простейшим из объектов синхронизации является защелка (Spin Lock), представленная классом KSpinLock. Принцип действия защелки очень прост: чтобы запретить любому другому потоку в системе доступ к данным, нужно вызывать метод Lock защелки. Любой поток, пытающийся получить доступ к заблокированным данным, уснет. Чтобы снять блокировку, нужно вызвать метод Unlock.
Класс диспетчера KDispatcherObject является суперклассом для нескольких важных классов синхронизации. Эти классы управляют планировщиком Windows и позволяют синхронизировать как работу драйверов, так и работу приложения пользователя и драйвера. Все классы, порожденные от KDispatcherObject, имеют два важных отличия:
Подклассы класса KDispatcherObject:
KEvent - используется для синхронизации работы потоков. Kevent почти не отличается от объекта диспетчера.
KSemaphore инкапсулирует системный объект семафора. Семафор отличается от объекта события тем, что имеет счетчик. Семафор сигнализирует в том случае, если счетчик больше нуля. Семафоры могут быть полезны, например, при управлении несколькими пото- ками.
KTimer - таймер. При создании таймера его флажок находится в состоянии "молчит". Временной интервал таймера задается функцией Set с точностью до 100 нс. На практике таймер устойчиво работает с временем ожидания >= 10 мс. Когда пройдет указанный промежуток времени, таймер перейдет в состояние "сигнализирует". Подклассом Ktimer является класс KTimedCallBack. В нем по истечении промежутка времени выполняется вызов отложенной процедуры (DPC).
KSystemThread позволяет создать новый поток в драйвере. Потоки в драйвере используются в разных целях. В основном это - поллинг медленных устройств и работа на многопроцессорных системах. Для запуска потока следует создать функцию, которая станет функцией потока и вызвать метод Start. Для уничтожения потока - метод Terminate. При работе с потоками можно использовать все упомянутые выше классы синхронизации.
Дополнительные классы.
DriverWorks предоставляет дополнительные классы для нужд программиста. Это классы очередей, списков, стеков; классы файлов и Unicode- строк; классы синхронизации.
Списки представлены класами KList, KInterlockedList, KInterruptSafeList. Они представляют шаблоны двунаправленных списков и стандартные методы для вставки, удаления и добавления элементов. Различаются эти классы методами синхронизации. KList не содержит никаких методов синхронизации и защиты данных. KInterLockedList использует защелки (spin locks) для защиты внутренних связей в списке. KInterruptSafeList использует присоединенный объект прерывания для защиты связей. По аналогичному принципу работают шаблоны классов FIFO (стек): KFifo, KLockableFifo, KInterruptSafeFifo. Класс KFile инкапсулирует методы для работы с файлами. Этот класс позволяет читать и записывать данные в файл а также изменять атрибуты файлов. Для представления Unicode - строк используется класс KUstring. Методы данного класса позволяют выполнять сравнение, конкатенацию, доступ к символам строки и разнообразные преобразования типа.
Связь драйвера с приложением пользователя
Также остается неясным еще один вопрос, связанный с драйверами: как именно с нашим объектом устройства может связаться приложение или другой драйвер? Большинство из устройств в системе именованы, хотя теоретически допускается существование неименованных (anonymous) устройств. Связь с устройством можно установить двумя методами:
1. GUID (Globally Unique Identifier, глобально уникальный идентификатор) - 16-байтное уникальное число. GUID используются для идентификации в системе драйверов, СОМ-объектов и т.п. В идеале, во всем мире не может быть двух одинаковых GUID, поэтому GUID может быть абсолютно уникальным идентификатором драйвера. GUID генерируется на основе текущей даты, времени и номера сетевого адаптера, если такой присутствует, и обычно указывается в заголовочном файле класса устройства и программы, которая хочет связаться с ним приблизительно таким образом:
#define MyDevice_CLASS_GUID \ { 0xff779f4c, 0x8b57, 0x4a65, { 0x85, 0xc4, 0xc8, 0xad, 0x7a, 0x56, 0x64, 0xa6 } }
2. Символическая ссылка (symbloic link) похожа на путь к файлу и в тексте программы имеет вид:
char *sLinkName = "\\\\.\\MyDevice";
Если отбросить лишние символы бэкслэша, необходимые для соблюдения синтаксиса С++, то символическая ссылка оказывается строкой \\.\MyDevice. Чтобы понять принцип работы символической ссылки, следует знать, что в ОС есть системный каталог различных объектов, которые присутствуют в системе: драйверов, устройств, объектов событий, семафоров и т.п. Символическая ссылка - специфический тип объекта, который обеспечивает доступ к другим системным объектам. Специальный подкаталог системного каталога зарезервирован для символических ссылок на другие объекты ОС. Программа пользователя может обратиться к этим символическим ссылкам при помощи функций API.
Как же следует проектировать интерфейс с драйвером? Следует использовать GUID или символическую ссылку?
Идентификация драйвера при помощи GUID считается более правильной. Как было упомянуто выше, специальные алгоритмы гарантируют то, что GUID будет действительно уникальным. А кто мешает разработчику, находящемуся на другом конце света, также создать устройство с той же ссылкой на него \\.\MyDevice? Вообще-то, никто. Но с другой стороны, с написанной на понятном английском языке ссылкой гораздо проще обращаться, особенно на этапе разработки драйвера, чем с длинным и непонятным GUID. Так что, вероятно, на этапе разработки и отладки драйвера для интерфейса драйвера с приложением лучше использовать символическую ссылку, а для коммерческой версии драйвера - GUID.
Процесс разработки драйвера при помощи DriverStudio во многом напоsминает разработку приложения в среде Visual C++. Создание проекта происходит при помощи мастера DriverWizard, похожего на мастер Visual C++. Мастер вызывается или из главного меню ( Пуск - Программы - DriverStudio - DriverWorks - DriverWizard) или из среды Visual C++ при помощи пункта меню DriverStudio - DriverWizard. Программе DriverWizard соответствует иконка
Далее при работе мастера появляется серия диалоговых окон, куда пользователь должен ввести данные, необходимые для формирования скелета драйвера.
На первом шаге создания драйвера необходимо ввести имя проекта ( в нашем случае - XDSP) и директорию для проекта. После этого - нажать на кнопку Next, чтобы перейти к следующему шагу.
На втором шаге следует выбрать архитектуру, по которой будет разрабатываться драйвер: Windows NT 4.0 (которая сейчас практически не используется) или WDM, которую нам и следует выбрать.
На третьем шаге выберем шину, на которой располагается устройство, которое будет контролировать драйвер. Если это устройство будет подключаться к порту компьютера, например к параллельному - набо выбрать None - driver does not control any hardware. Если же устройство будет располагаться на одной из шин компьютера, например на PCI - надо задать дополнительные параметры. В случае PCI устройства надо указать следующие параметры:
На четвертом шаге мастера необходимо задать имена, которые DriverWizard присвоит файлу С++, который содержит класс драйвера, и самому классу драйвера (Driver Class).
На пятом шаге следует указать, какие функции должен выполнять драйвер. Это может быть:
На шестом шаге DriverWizard задает вопросы о способе обработки запросов. Опция Select queuing method выбирает, каким образом будут буферизироваться запросы на ввод-вывод:
Также надо выбрать, будут ли буферизироваться запросы на чтение и запись. Как было сказано ранее, устройство может одновременно выполнять какую-то одну операцию, например, только чтение или только запись, или может выполнять несколько операций сразу. Чтобы гарантировать нормальную работу устройства в этом случае, следует буферизировать (Serialize) поступающие запросы на чтение и запись, помещая их в очередь. Установка флажков Seralize all Read requests и Serialize all Write requests позволяет буферизировать все запросы на чтение и запись, поступающие в объект устройства.
На седьмом шаге предлагается задать параметры, которые драйвер будет загружать из реестра Windows при старте, когда система загружается. При этом задается параметр реестра, имя переменной, куда сохраняется его значение, тип данного параметра и его значение по умолчанию. Если не менять настройки, то во время загрузки драйвер читает из реестра параметр BreakOnEntry типа boolean, сохраняет его значение в переменной m_BreakOnEntry. Значение по умолчанию для параметра - false. Обычно m_BreakOnEntry используется в отладочных челях.
Запись и считывание параметров из реестра позволяет драйверу задавать какие-либо конфигурационные параметры, сохранять данные, необходимые для его запуска или работы.
При помощи кнопок Add, Edit и Delete можно соответственно добавлять, редактировать и удалять параметры.
Восьмой шаг DriverWizard - один из самых важных моментов в разработке драйвера PCI - устройства при помощи DriverWorks. Поэтому окно мастера несет огромное количество информации и элементов управления.
На данном шаге предлагается изменить имена классов устройства для данного драйвера. В списке в верхней части окна следует выбирать класс устройства, который следует переименовать, и, нажав на кнопку Rename, можно задать новое имя класса устройства.
Окно DriverWizard также содержит несколько вкладок:
Вкладка Resource. На ней определяются основные аппаратные ресурсы, которые есть в устройстве и которые будет контролировать этот драйвер. В их числе адреса памяти, диапазоны портов ввода-вывода, линии запроса на прерывание и линии прямого доступа к памяти (DMA), которые необходимы для работы драйвера. Задать ресурсы можно при помощи кнопок в нижней части вкладки.
Например, задать диапазон памяти, которую несет "на борту" устройство, можно, нажав на кнопку Add Memory Range. При этом выводится диалоговое окно, куда следует ввести сведения о новом диапазоне адресов памяти: имя объекта класса KMemoryRange, который будет контролировать этот диапазон адресов, адрес базового регистра в PCI - заголовке (PCI header) данного устройства, который определяет этот диапазон адресов, а также параметры доступа для данной памяти: только чтение (Read Only), только запись (Write Only) и полный доступ (Read/Write). Также можно еще задать опции разделения доступа (Share options). Эти опции позволяют разделять доступ к ресурсу: к нему можно обращаться только из класса данного устройства (Exclusive to this device), из любой части драйвера (Shareable within this driver) или из любого драйвера в системе (Shareable system wide). Впрочем, для разработки простых драйверов эти опции являются бесполезными и изменять их не стоит. В нашем случае мы создаем диапазон адресов памяти с именем m_MainMemoryRange, определяемый нулевым базовым регистром в PCI - header'e, с полным доступом.
По аналогичному принципу можно задать параметры портов ввода-вывода и линий DMA. Параметры линий запроса на прерывание посложнее: тут можно дать указание DriverWizard'у создать шаблоны для классов ISR, DPC и их функций (Make ISR/DPC class functions).
Если в процессе задания ресурсов задана ошибка или необходимол внести какие-либо изменения, то для этого надо щелкнуть по названию ресурса в окне правой клавишей мыши. Появится контекстное меню, в котором надо выбрать пункт Delete, чтобы удалить ресурс или Edit - редактировать его.
На вкладке Interface задается способ, каким образом будет осуществлятся связь программы или библиотеки DLL с драйвером.
Надежным способом является связь при помощи GUID класса. GUID - уникальный номер, который однозначно идентифицирует какой-либо объект системы. При помощи GUID идентифицируются не только драйвера, а и СОМ - интерфейсы и пр.
Другим способом реализации интерфейса является является символическая ссылка. Это более естественный путь, т.к. просто указать имя класса устройства - гораздо проще, чем указывать непонятного вида GUID.
На вкладке Buffers определяется метод, каким образом буферизируются запросы к устройству.
Буферизированный (buffered) метод - пригоден для устройств типа мыши, клавиатуры, которые передают небольшие объемы данных за короткий промежуток времени. Прямой (direct) метод - используется при пересылке больших объемов информации за короткий промежуток времени, например, при обращении к дисководу.
При создании WDM - драйвера необходимо задать способ управления энергопотреблением. При помощи флажка Управлять энергопотреблением этого устройства (Manage power for this device) можно создать в драйвере методы управления энергопотреблением нашего устройства. В нашем простом случае мы не будем этого делать.
Естественно, для более-менее сложного драйвера устройства будет недостаточно двух запросов на чтение и запись. На девятом шаге можно задать коды управления драйвером устройства. Код управления (Device IO control code, IOCTL) просто представляет собой число, которое передается драйверу. Коды управления в драйвере обрабатываются специальной функцией. В ответ на каждый код драйвер выполняет какое-либо действие. Например, в нашем случае объект устройства будет возвращать количество памяти, которое имеет PCI-карточка. Для этого зададим код управления XDSP_GetMemSize. Для этого нажмем на кнопку Add, появится диалоговое окно Edit IO Control Code (редактирование кода управления).
При задании кода управления устройством нужно указать имя кода в понятном программисту виде, метод общения с устройством (прямой или буферизированный). Также задается порядковый номер кода (Ordinal) - число, являющееся его уникальным номером. Числа, меньшие 0x800 используются для стандартных кодов, таких, как чтение, запись и т.п.
Запросы IOCTL также можно буферизировать, подобно запросам на чтение и запись. Для этого надо установить флажок Queue (serialize) this request code.
Внизу окна мастера указано имя заголовочного файла, в котором будут храниться коды управления устройством. В нашем случае этоXDSPioctl.h. Ненужные коды управления устройством можно удалить, нажав на кнопку Remove или редактировать, нажав кнопку Edit.
Одним из достоинств DriverWorks является то, что DriverWizard сразу создает консольное приложение для тестирования работоспособности драйвера. Конечно, такое тестирование бывает неполным и примитивным, но позволяет оценить, правильно ли работает драйвер и работает ли он вообще. Для того, чтобы DriverWizard создал такое приложение, нужно установить флажок Create test console application (создать консольное приложение для тестирования) и указать его имя. Также можно задать опции отладки. Они необходимы при отладке драйвера средствами DriverStudio. При написании простых драйверов эти опции, скорее всего, не понадобятся.
Пройдя все эти шаги, нажмите на кнопку Finish. В ответ появится окошко, которое содержит сведения о каталоге с файлами проекта нашего драйвера, для чего предназначен каждый файл. Нажимаем на кнопку ОК - DriverWizard сгенерирует все файлы нашего драйвера, приложения для тестирования и предложит открыть проект в Visual C++.
Проект, сгенерированный DriverWizard, находится в каталоге XDSP. В этом каталоге расположены файлы рабочего пространства (Workspace) VC++: XDSP.dsw, XDSP.ncd и XDSP.opt и два каталога - sys и exe. Здесь же находится файл XDSPioctl.h. В нем описаны управляющие коды, используемые при обращении к драйверу с помощью функции DeviceIOControl.
В каталоге sys находится сгенерированный DriverWizard скелет драйвера и все необходимые для компиляции файлы. В нашем случае, имеем файлы:
В каталоге ехе находится исходный код консольного приложения TextXDSP, предназначенного для тестирования работы драйвера. При помощи него можно убедится, правильно ли установлен драйвер в системе, а иногда даже проверить, как он работает. Хотя для более-менее сложного драйвера придется писать программу тестирования отдельно. В каталоге присутствуют файлы:
Теперь самое время открыть проект драйвера в среде VC++ и посмотреть, что же мы имеем. Для этого надо запустить VC++ и открыть проект, используя команду File -> Open Workspace. В появившемся диалоговом окне открытия файла выберите файл XDSP.dsw. Если все вышеописанные действия выполнены правлиьно, то проект откроется в среде VC++. Для того, чтобы проект скомпилировался правильно, следует установить переменные среды DriverStudio. Для этого нужно выбрать пункт меню DriverStudio -> Driver Build Settings: На экране появится диалоговое окно установки переменных среды:
Для компиляци драйвера важны две переменные:
1. CPU - определяет архитектуру процессора, под которую компилируется драйвер. Не стоит забывать, что Win2000 может работать на платформах i386 (классические процессоры Intel), IA64 (64-разрядные процессоры Intel) и Alpha. В нашем случае надо установить значение i386.
2. BASEDIR - путь к пакету DDK, установленному в системе. Для того, чтобы изменить значение одной из этих переменных, надо нажать кнопку Edit: диалогового окна. Появится окно установки значений переменной.
Установив требуемое значение, нажмите кнопку Set. Чтобы закрыть окно - Exit. Задав переменные среды, нажмите кнопку Accept. Теперь можно компилировать проекты.
Драйвер может быть скомпилирован в двух конфигурациях: Checked и Free.
Checked - отладочный вариант драйвера. Такой драйвер несет в себе информацию для отладки. Естественно, что для отладки драйверов непригодны обыкновенные отладчики, входящие в комплект сред VC++, Delphi и т.п. Все они работают в 3-м кольце привилегий процессора и даже не догадываются, какие драйвера есть в системе. Для отладки драйверов применяются специальные отладчики, работающие в режиме ядра ОС. В качестве отладчика лучше всего использовать SoftIce, поставляемый с DriverStudio.
Free - драйвер не несет отладочную информацию.
Активную
конфигурацию можно выставить при помощи пункта меню Build -> Set Active
Configuration:
Особенность сгенерированного DriverWizard рабочего пространства состоит в том, что оно содержит два проекта: XDSP и Test_XDSP. Как нетрудно догадаться, XDSP - это проект драйвера, а Test_XDSP - приложения тестирования. Информация о проектах выводится в окне Workspace среды VC++.
В каждый отдельный момент времени можно компилировать только активный проект. Имя активного проекта выводится жирным шрифтом. Сделать активным другой проект просто: надо щелкнуть на его названии правой клавишей мыши и в выпавшем контекстном меню выбрать пункт Set as Active Project (Сделать активным проектом).
Теперь можно выполнять компиляцию проекта. Если в процессе компиляции появляются сообщения об ошибках - значит вы не совсем точно следовали инструкциям, изложенным выше: или не скомпилировали библиотеки DriverWorks, или не установили переменные среды.
После компиляции драйвера следует скомпилировать тестовое приложение Test_XDSP. Оно должно скомпилироваться без каких-либо проблем.
Если все операции прошли гладко - то можете себя поздравить: драйвер готов к работе. Хотя, естественно, он не выполняет никаких разумных действий. Теперь можно протестировать наш драйвер.
После компиляции мы получили файл драйвера XDSP.sys. Он находится в каталоге .../XDSP/sys/obj/i386. В этом каталоге будут находится скомпилированные DriverStudio драйвера. Но для инсталляции кроме самого драйвера еще нужен скрипт XDSP.inf. Он обычно находится в самом каталоге XDSP.
Итак, для установки драйвера в системе предполагается наличие в системе PCI - карточки XDSP-680. После установки карточки (или перепрограммирования ее из среды Foundation) следует перезагрузить компьютер. При загрузке компьютер обнаружит новое устройство и потребует предоставить драйвер для него. Если же не потребует - значит в системе есть более ранняя версия драйвера. Для этого надо открыть список устройств, установленных на компьютере и обновить драйвер для устройства. Для этого надо указать путь к скрипту xdsp.inf и к файлу драйвера xdsp.sys.
Если же Вы разрабатываете драйвер, который не управляет каким-либо устройством или это устройство не является PnP - необходимо просто установить драйвер стандартными средствами Windows: Пуск -> Настройка -> Панель управления -> Установка оборудования. Когда Windows выведет полный список типов устройств и спросит, какое устройство Вы хотите установить, выберите свой тип устройства.
Если разработанный Вами драйвер не подходит под какой-либо из известных классов устройств, то "Другие устройства" также являются неплохим вариантом. Такая ситуация тоже случается нередко - мне, например, приходилось разрабатывать драйвер для программатора микроконтроллеров, подключавшегося через параллельный порт. Конечно же, он не подходил под какой-либо из известных в Windows типов устройств.
После того, как драйвер будет установлен, нужно будет проверить его функционирование. Запустите скомпилированный файл test_xdsp.exe с параметрами test_xdsp r 32 (команда прочитать 32 байта из устройства). Должно появиться сообщение, похожее на это:
C:\XDSP\exe\objchk\i386>Test_XDSP.exe r 32 Test application Test_XDSP starting... Device found, handle open. Reading from device - 0 bytes read from device (32 requested). -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -,
C:\...Projects\XDSPdrv\exe\objchk\i386>Test_XDSP.exe r 32 Test application Test_XDSP starting... ERROR opening device: (2) returned from CreateFile Exiting...- то приложение не смогло установить связь с драйвером. Следует попробовать переустановить драйвер.
Рассмотрим подробно текст драйвера, сгенерированного DriverWizard и внесем в него необходимые изменения.
В проекте пристствуют всего два класса:
PNPMinorFunctionName - возвращает строку с текстовым названием кода функции IOCTL. Эта функция используется при отладке, когда надо перевести числовое значение кода IOCTL в строку с его названием.
POOLTAG DefaultPoolTag('PSDX') - используется совместно с BoundsChecker для отслеживания возможных переполнений буфера и утечек памяти.
KTrace t("XDSPdrv") - глобальный объект трассировки драйвера. Этот объект используется для вывода сообщений трассировки при работе драйвера. Использование объекта трассировки аналогично использованию класса iostream в С++. Вывод отладочных сообщений производится при помощи оператора <<. Примеры использования объекта трассировки неоднократно встречаются в тексте драйвера, например:
t << "m_bBreakOnEntry loaded from registry, resulting value: [" << m_bBreakOnEntry << "]\n";
В данном примере объект трассировки используется для вывода строки "m_bBreakOnEntry loaded from registry, resulting value: [" и значения логической переменной m_bBreakOnEntry. Все сообщения трассировки можно прочитать в отладчике SoftIce.
Начнем анализ текста драйвера с класса XDSP (класс драйвера). В строке 31 при помощи макроса DECLARE_DRIVER_CLASS декларируется класс драйвера XDSP. Далее следует метод DriverEntry, который вызывается при инициализации драйвера:
NTSTATUS XDSPdrv::DriverEntry(PUNICODE_STRING RegistryPath) //В строке RegistryPath содержится ключ реестра, в котором // система хранит информацию о драйвере. { //Далее выводится трассировочное сообщение, информирующее // о вызове метода DriverEntry: t << "In DriverEntry\n"; //После этого драйвер создает объект Params класса // KRegistryKey и считывает данные из //реестра для этого драйвера: KRegistryKey Params(RegistryPath, L"Parameters"); //Далее производится проверка на успех: if ( NT_SUCCESS(Params.LastError()) ) { //Текст, заключенный в марос препроцессора DBG будет откомпилирован // только в отладочной версии //драйвера. #if DBG ULONG bBreakOnEntry = FALSE; // Читается значение переменной BreakOnEntry реестра: Params.QueryValue(L"BreakOnEntry", &bBreakOnEntry); // Если она принимает значение true,то инициировать // точку останова в отладчике. if (bBreakOnEntry) DbgBreakPoint(); #endif //Загрузить остальные параметры реестра. LoadRegistryParameters(Params); } m_Unit = 0; //Вернуть успех return STATUS_SUCCESS; }
Метод LoadRegistryParameters зaгружает из реестра все остальные параметры, необходимые для драйвера. Впрочем, в нашем драйвере таковых нет, и поэтому функция не выполняет никаких полезных действий (просто загружает значение переменной m_bBreakOnEntry).
void XDSPdrv::LoadRegistryParameters(KRegistryKey &Params) { m_bBreakOnEntry = FALSE; Params.QueryValue(L"BreakOnEntry", &m_bBreakOnEntry); t << "m_bBreakOnEntry loaded from registry, resulting value: [" << m_bBreakOnEntry << "]\n"; }
На этом заканчивается секция инициализации драйвера. Далее следует метод AddDevice. Он вызывается, когда система обнаруживает устройство, за которое отвечает драйвер (обычно это происходит при загрузке драйвера). В метод ситема передает указатель на физический объект устройства (Physical Device Object, PDO). Этот объект представляет собой некий блок информации о физическом устройстве, который используется ОС. Данный метод создает объект устройства XDSPDevice. С точки зрения системы, создается функциональный объект устройства (Functional Device Object, FDO).
NTSTATUS XDSPdrv::AddDevice(PDEVICE_OBJECT Pdo) { t << "AddDevice called\n"; //Здесь вызывается конструктор класса XDSPDevice. XDSPdrvDevice * pDevice = new ( static_cast(KUnitizedName(L"XDSPdrvDevice", m_Unit)), FILE_DEVICE_UNKNOWN, static_cast (KUnitizedName(L"XDSPdrvDevice", m_Unit)), 0, DO_DIRECT_IO ) XDSPDevice(Pdo, m_Unit); //m_Unit - количество таких устройств в системе. if (pDevice == NULL) //Не удалось создать объект устройства. Похоже, произошла какая-то ошибка. { t << "Error creating device XDSPdrvDevice" << (ULONG) m_Unit << EOL; return STATUS_INSUFFICIENT_RESOURCES; } //Получить статус создания устройства. NTSTATUS status = pDevice->ConstructorStatus(); if ( !NT_SUCCESS(status) ) //Похоже, устройство создано, но неудачно; произошла ошибка. { t << "Error constructing device XDSPdrvDevice" << (ULONG) m_Unit << " status " << (ULONG) status << EOL; delete pDevice; } else { m_Unit++; //Устройство создано удачно } //Вернуть статус устройства. return status; }
Все. Работа объекта драйвера на этом окончена. Как мы можем видеть, объект драйвера практически не выполняет каких-либо функций управления аппаратурой, но он жизненно необходим для правильной инициализации драйвера. В нашем случае НЕ ТРЕБУЕТСЯ вносить какие-либо изменения в текст, сформированный DriverWizard.
Основным классом драйвера является класс устройства. Класс устройства XDSPdrvDevice является подклассом класса KpnpDevice. Конструктор получает два параметра: указатель на PDO и номер драйвера в системе.
XDSPdrvDevice::XDSPdrvDevice(PDEVICE_OBJECT Pdo, ULONG Unit) : KPnpDevice(Pdo, NULL) { t << "Entering XDSPdrvDevice::XDSPdrvDevice (constructor)\n"; //Здесь проверяется код ошибки, которую вернул конструктор суперкласса. В случае //успешного создания объекта базового класса значение переменной m_ConstructorStatus //будет NT_SUCCESS. if ( ! NT_SUCCESS(m_ConstructorStatus) ) { //Ошибка в создании объекта устройства return; } //Запомнить номер драйвера m_Unit = Unit; //Инициализация устройства нижнего уровня. В роли устройства нижнего уровня в нашем //драйвере выступает PDO. Но в случае стека драйверов в качестве устройства нижнего //уровня может выступать объект устройства другого драйвера. m_Lower.Initialize(this, Pdo); // Установить объект нижнего уровня для нашего драйвера. SetLowerDevice(&m_Lower); // Установить стандартную политику PnP для данного устройства. SetPnpPolicy(); }Порядок вызова методов m_Lower.Initialize(this, Pdo), SetLowerDevice(&m_Lower) и SetPnpPolicy() является жизненно важным. Его нарушение может вызвать серьезные сбои в работе драйвера. Не стоит редактировать текст конструктора, сгенерированный DriverWizard.
Деструктор объекта устройства не выполняет никаких действий. Но для сложных драйверов, когда создаются системные потоки, разнообразные объекты синхронизации и выделяется память, то все созданные объекты должны быть уничтожены в деструкторе. В нашем простейшем случае не стоит вносить изменения в текст деструктора.
XDSPdrvDevice::~XDSPdrvDevice() { t << "Entering XDSPdrvDevice::~XDSPdrvDevice() (destructor)\n"; }Метод DefaultPnp - виртуальная функция, которая должна быть переопределена любым объектом устройства. Эта обработчик по умолчанию для IRP-пакета, у которого старший код функции (major function code) равен IRP_MJ_PNP. Драйвер обрабатывает некоторые из таких пакетов, у которых младший код функции равен IRP_MN_STOP_DEVICE, IRP_MN_START_DEVICE и т.п. (см. ниже) также при помощи виртуальных функций. Но те пакеты, которые не обрабатываются объектом устройства, передаются этой функции. Она ничего с ними не делает, а просто передает их устройству нижнего уровня (если такое есть, конечно). Не стоит изменять текст этой функции.
NTSTATUS XDSPdrvDevice::DefaultPnp(KIrp I) { t << "Entering XDSPdrvDevice::DefaultPnp with IRP minor function=" << PNPMinorFunctionName(I.MinorFunction()) << EOL; I.ForceReuseOfCurrentStackLocationInCalldown(); return m_Lower.PnpCall(this, I); }Метод SystemControl выполняет похожую функцию для IRP-пакетов, у которых старший код функции IRP_MJ_SYSTEM_CONTROL. Он также является виртуальной функцией и не выполняет никаких полезных действий, а просто передает IRP-пакет устройству нижнего уровня. Что-то менять в тексте этого метода надо только в том случае, если наше устройство является WMI-провайдером.
NTSTATUS XDSPdrvDevice::SystemControl(KIrp I) { t << "Entering XDSPdrvDevice::SystemControl\n"; I.ForceReuseOfCurrentStackLocationInCalldown(); return m_Lower.PnpCall(this, I); }Метод Invalidate вызывается, когда устройство тем или иным образом завершает свою работу: из функций OnStopDevice, OnRemoveDevice а также при всевозможных ошибках. Метод Invalidate объекта устройства также вызывается из деструктора. Его можно вызывать несколько раз - не произойдет ничего страшного; но в методах Invalidate нет никакой защиты от реентерабельности. Т.е. если при работе метода Invalidate возникает какая- либо ошибка и из-за этого Invalidate должен будет вызваться снова, то ни DriverWorks, ни ОС Windows не станут этому мешать. Разработчик должен сам предусмотреть такую возможность и принять меры, чтобы подобная ситуация не привела к нехорошим последствиям.
В методе Invalidate объекта устройства вызываются методы Invalidate всех ресурсов, которые использует драйвер: областей памяти, регистров, контроллеров DMA и т.п. При этом выполняется процедура, обратная процедуре инициализации: освобождаются все ресурсы, используемые объектом, закрываются все его хэндлы, но сам объект не уничтожается и может быть проинициализирован снова. В нашем простом случае нет смысла что-либо корректировать в тексте этого метода - DriverWizard все сделал за нас. Еще бы, ведь наше устройство имеет только один ресурс - диапазон адресов памяти. Но при проектировании более сложных драйверов следует обращать внимание на данный метод. Если разработчик добавляет какие-либо системные ресурсы вручную, то он должен включить соответствующий код в метод Invalidate.
VOID XDSPdrvDevice::Invalidate() { //Вызвать метод Invalidate для диапазона адресов памяти. m_MainMem.Invalidate(); }Далее следует виртуальная функция OnStartDevice. Она вызывается при приходе IRP- пакета со старшим кодом IRP_MJ_PNP и кодом подфункции IRP_MN_START_DEVICE. Обычно это происходит при старте драйвера после выполнения всех необходимых проверок и инициализаций. В этой функции драйвер инициализирует физическое устройство и приводит его в рабочее состояние. Также здесь драйвер получает список ресурсов, которые имеются в устройстве. На основе этого списка ресурсов выполняется их инициалиция. Хотя мы не вносим изменений в данную функцию, но нельзя не отметить ее огромную важность. Именно в данной функции выполняется инициализация устройства и получение списка его ресурсов. По-другому мы их получить никак не можем, т.к. имеем дело с PnP устройством, для которого система распределяет ресурсы самостоятельно.
NTSTATUS XDSPdrvDevice::OnStartDevice(KIrp I) { t << "Entering XDSPdrvDevice::OnStartDevice\n"; NTSTATUS status = STATUS_SUCCESS; I.Information() = 0; //Здесь драйвер получает список ресурсов устройства PCM_RESOURCE_LIST pResListRaw = I.AllocatedResources(); PCM_RESOURCE_LIST pResListTranslated = I.TranslatedResources(); // Наше устройство является PCI - карточкой и в своем конфигурационном поле содержит //базовые адреса диапазонов памяти и портов ввода-вывода. Получаем эти данные KPciConfiguration PciConfig(m_Lower.TopOfStack()); // Инициализируем каждый диапазон отображения адресов. Теперь, после инициализации, //базовый адрес отображенного диапазона в виртуальном адресном пространстве //процессора может быть получен при помощи вызова метода Base(). Физический адрес на //шине адреса ЦП - при помощи CpuPhysicalAddress(). status = m_MainMem.Initialize( pResListTranslated, pResListRaw, PciConfig.BaseAddressIndexToOrdinal(0) ); if (!NT_SUCCESS(status)) { //Неудача при инициализации области памяти Invalidate(); return status; } //Сюда можно добавить код, выполняющий необходимую инициализацию, специфичную для //этого устройства return status; }Виртуальная функция OnStopDevice вызывается при остановке устройства. В этом случае система посылает драйверу IRP с старшим кодом IRP_MJ_PNP и кодом подфункции IRP_MN_STOP_DEVICE. Драйвер должен осободить все используемые ресурсы.
NTSTATUS XDSPdrvDevice::OnStopDevice(KIrp I) { NTSTATUS status = STATUS_SUCCESS; t << "Entering XDSPdrvDevice::OnStopDevice\n"; //Освободить ресурсы устройства Invalidate(); // Здесь добавляется код, специфичный для данного устройства. return status; }Виртуальная функция OnRemoveDevice вызывается при извлечении устройства из системы. При этом системная политика PnP сама позаботится об удалении PDO.
NTSTATUS XDSPdrvDevice::OnRemoveDevice(KIrp I) { t << "Entering XDSPdrvDevice::OnRemoveDevice\n"; // Освободить ресурсы устройства Invalidate(); // Здесь добавляется код, специфичный для данного устройства. return STATUS_SUCCESS; }Иногда бывает необходимо отменить обработку IRP, уже поставленного в очередь (такие запросы иногда посылает диспетчер В/В). Когда такая ситуация может возникнуть?
Представим такую ситуацию: в приложении пользователя поток послал нашему драйверу запрос на ввод-вывод и завершил свою работу. А IRP-пакет попал в очередь запросов и терпеливо ждет своей очереди на обработку. Конечно же, обработка такого "бесхозного" IRP-пакета должна быть отменена. Для этого драйвером поддерживается целый механизм отмены обработки запросов. В Win2000 DDK подробно описано, почему ЛЮБОЙ драйвер должен поддерживать этот механизм. Это связано, в основном, с проблемами надежности и устойчивости работы системы. Ведь сбой в драйвере - это, практически, сбой в ядре ОС.
В классе KPnPDevice механизм отмены запроса реализован при помощи метода CancelQueuedIrp.
VOID XDSPdrvDevice::CancelQueuedIrp(KIrp I) { //Получаем очередь IRP-пакетов этого устройства. KDeviceQueue dq(DeviceQueue()); //Проверить, является ли IRP, который должен быть отменен, тем пакетом, который должен //быть обработан. if ( (PIRP)I == CurrentIrp() ) { //Уничтожить пакет. CurrentIrp() = NULL; //При вызове метода CancelQueuedIrp устанавливается глобальная системная //защелка (SpinLock). Теперь следует ее сбросить. CancelSpinLock::Release(I.CancelIrql()); t << "IRP canceled " << I << EOL; I.Information() = 0; I.Status() = STATUS_CANCELLED; //Обработать следующий пакет. PnpNextIrp(I); } //Удалить из очереди пакет. Если это удалось, то функция вернет true. else if (dq.RemoveSpecificEntry(I)) { // Это удалось. Теперь сбрасываем защелку. CancelSpinLock::Release(I.CancelIrql()); t << "IRP canceled " << I << EOL; I.Information() = 0; I.PnpComplete(this, STATUS_CANCELLED); } else { //Неудача. Сбрасываем защелку. CancelSpinLock::Release(I.CancelIrql()); } }Меотод StartIo является виртуальной функцией. Она вызывается системой, когда драйвер готов обрабатывать следующий запрос в очереди. Это чрезвычайно важная функция: она является диспетчером всех запросов на ввод-вывод, поступаемых к нашему драйверу. В функции вызываются обработчики запросов на чтение, запись а также обработчики вызовов IOCTL. К счастью, умный DriverWizard генерирует скелет функции автоматически и вносить изменения в нее в нашем простом случае не требуется. В принципе, в эту функцию можно ввести какие-то дополнительные проверки IRP-пакетов.
VOID XDSPdrvDevice::StartIo(KIrp I) { t << "Entering StartIo, " << I << EOL; //Здесь надо проверить, отменен этот запрос или нет. Это производится // при помощи вызова метода TestAndSetCancelRoutine. Также этот метод // устанавливает новую функцию отмены пакетов, если это необходимо. // Адрес новой функции передается вторым параметром. Если он равен // NULL,то вызывается старая функция. Если пакет должен быть отменен, // функция вернет FALSE. if ( !I.TestAndSetCancelRoutine( LinkTo(CancelQueuedIrp), NULL, CurrentIrp()) ) { //Пакет отменен. return; } // Начать обработку запроса. // Выбрать необходимую функцию switch (I.MajorFunction()) { case IRP_MJ_READ: //Чтение SerialRead(I); break; case IRP_MJ_WRITE: //Запись SerialWrite(I); break; case IRP_MJ_DEVICE_CONTROL: //IOCTL switch (I.IoctlCode()) { default: //Мы обрабатываем пакет, который не должен быть обработан. //Поэтому просто выходим. ASSERT(FALSE); break; } break; default: // Драйвер занес в очередь какой-то непонятный пакет, //он не должен быть обработан. ASSERT(FALSE); PnpNextIrp(I); break; } }
Метод Create вызывается, когда пользовательское приложение пытается установить связь с драйвером при помощи вызова API CreateFile(). Обычно этот запрос обрабатывается в нашем объекте устройства и нет смысла пересылать запрос устройству нижнего уровня.
NTSTATUS XDSPdrvDevice::Create(KIrp I) { NTSTATUS status; t << "Entering XDSPdrvDevice::Create, " << I << EOL; //Здесь можно вставить код пользователя, который должен быть вызван при установлении //приложением связи с устройством. status = I.PnpComplete(this, STATUS_SUCCESS, IO_NO_INCREMENT); t << "XDSPdrvDevice::Create Status " << (ULONG)status << EOL; return status; } Аналогично метод Close вызывается при разрыве связи приложения с драйвером. NTSTATUS XDSPdrvDevice::Close(KIrp I) { NTSTATUS status; t << "Entering XDSPdrvDevice::Close, " << I << EOL; //Здесь можно вставить код пользователя, который должен быть вызван при разрыве //приложением связи с устройством. status = I.PnpComplete(this, STATUS_SUCCESS, IO_NO_INCREMENT); t << "XDSPdrvDevice::Close Status " << (ULONG)status << EOL; return status; }В этих методах можно ввести проверки каких-либо условий. Отвлечемся на секунду от нашей PCI-карточки и обратим внимание на другой хороший пример - тот же программатор микроконтроллеров. Предположим, пользователь подключил программатор к компьютеру и начинает записывать в память микроконтроллера разработанную им программу. В принципе, ничто не помешает ему открыть еще одну копию программы и писать в ту же микросхему что-то совсем другое. В результате, в эту несчастную микросхему запишется невообразимая каша. Для того, чтобы избежать такой ситуации, в объекте драйвера надо установить флаг, который будет показывать, свободно ли это устройство, или оно уже кем-то используется. Это может выглядеть так:
NTSTATUS MyPrettyDevice::OnStartDevice(KIrp I) { NTSTATUS status = STATUS_SUCCESS; I.Information() = 0; . . . //Какая-то инициализация - может, PCI, //может - какое-то другое оборудование... //Устройство только что заработало - конечно, оно свободно... m_AlreadyUsed = false; return status; } NTSTATUS MyPrettyDevice::Create(KIrp I) { NTSTATUS status; if (m_AlreadyUsed) //Это устройство уже используется кем-то. Нельзя допустить его использование //несколькими приложениями одновременно. //Возвращаем ошибку. status = I.PnpComplete(this, STATUS_INVALID_PARAMETER, IO_NO_INCREMENT); else { //Это устройство свободно. Устанавливаем флаг и возвращаем успех. m_AlreadyUsed = false; status = I.PnpComplete(this, STATUS_SUCCESS, IO_NO_INCREMENT); } return status; } NTSTATUS MyPrettyDevice::Close(KIrp I) { NTSTATUS status; //Пользователь закончил работу с устройством, теперь оно свободно. //Сбрасываем флаг. m_AlreadyUsed = false; status = I.PnpComplete(this, STATUS_SUCCESS, IO_NO_INCREMENT); return status; }Функция SerialRead вызывается, когда драйвер получает запрос на чтение. Это важная функция. Т.к. мы хотим, чтобы приложение пользователя могло читать и писать в память микросхемы, то именно сюда необходимо добавлять наш код. Все фрагменты кода, добавленные программистом, будут выделены жирным шрифтом:
void XDSPdrvDevice::SerialRead(KIrp I) { t << "Entering XDSPdrvDevice::SerialRead, " << I << EOL; NTSTATUS status = STATUS_SUCCESS; //Здесь мы получаем буфер пользователя. Он передается через Irp. KMemory Mem(I.Mdl()); PUCHAR pBuffer = (PUCHAR) Mem.MapToSystemSpace(); //Теперь pBuffer - указательна буфер пользователя. //Здесь мы получаем число 4-байтных слов, которое должно быть прочитано. Оно также //передается через Irp, как запрашиваемое количество байт для чтения. ULONG dwTotalSize = I.ReadSize(CURRENT); ULONG dwBytesRead = dwTotalSize; //Здесь мы читаем заданное число байт из памяти устройства. Плата XDSP680 обменивается //с памятью 4-байтными словами.Начальный адрес - 0, dwTotalSize 4-байтных слов будут //прочитаны в буфер pBuffer. m_MainMem.ind(0,(ULONG*)pBuffer,dwTotalSize); //Возвращаем количество прочитанных слов I.Information() = dwBytesRead; I.Status() = status; //Обработать следующий IRP-пакет. PnpNextIrp(I); }Метод SerialWrite работает практически так же, только он записывает данные в память устройства, а не считывает их.
void XDSPdrvDevice::SerialWrite(KIrp I) { t << "Entering XDSPdrvDevice::SerialWrite, " << I << EOL; NTSTATUS status = STATUS_SUCCESS; KMemory Mem(I.Mdl()); PUCHAR pBuffer = (PUCHAR) Mem.MapToSystemSpace(); ULONG dwTotalSize = I.WriteSize(CURRENT); ULONG dwBytesSent = dwTotalSize; m_MainMem.outd(0,(ULONG*)pBuffer,dwTotalSize); I.Information() = dwBytesSent; I.Status() = status; PnpNextIrp(I); }Как мы упоминали ранее, для большинства драйверов устройств недостаточно функций чтения и записи. Мало-мальски сложное устройство требует еще и множества других операций: получить состояние, получить информацию об устройстве, как-то отконфигурировать его. Для выполнения этих задач служат функции управления вводом-выводом, IO Control; сокращенно - IOCTL. IOCTL предоставляет программисту возможность разработать практически неограниченное количество различных функций управления устройством.
И драйвер, и приложение пользователя различают, какую функцию управления устройством вызвать, при помощи IOCTL-кодов. Такой код представляет собой обыкновенное 32-разрядное число. Для удобства ему директивой #define задают какое-то понятное имя. Например, в нашем случае зададим IOCTL-код, при получении которого драйвер будет возвращать количество памяти "на борту" PCI-устройства.
#define XDSPDRV_IOCTL_GETMEMSIZE 0x800Если при чтении драйверу посылается IRP-пакет со старшим кодом функции IRP_MJ_READ, при записи - IRP_MJ_WRITE, то при вызове функции DeviceIOControl для нашего устройства драйвер получает пакет со старшим кодом IRP_MJ_IOCONTROL и младшим - код самой IOCTL-функции. Метод DeviceControl вызывается при получении драйвером IRP со старшим кодом IRP_MJ_DEVICE_CONTROL. Она действует подобно методу StartIo.В зависимости от кода IOCTL производится вызов соответствующей функции.
NTSTATUS XDSPdrvDevice::DeviceControl(KIrp I) { NTSTATUS status; t << "Entering XDSPdrvDevice::Device Control, " << I << EOL; switch (I.IoctlCode()) { case XDSPDRV_IOCTL_GETMEMSIZE: //Получен поределенный нами IOCTL-код XDSPDRV_IOCTL_GETMEMSIZE. //Вызвать соответствующий обработчик. status = XDSPDRV_IOCTL_GETMEMSIZE_Handler(I); break; default: //Этот код не определен. status = STATUS_INVALID_PARAMETER; break; } if (status == STATUS_PENDING) // Если драйвер по каким-то причинам отложил обработку запроса, переменной status //присваивается значение STATUS_PENDING. Этот код будет возвращен методом //DeviceControl. { return status; } else //В противном случае завершаем обработку пакета. { return I.PnpComplete(this, status); } }Метод XDSPDRV_IOCTL_GETMEMSIZE_Handler является обработчиком IOCTL - кода XDSPDRV_IOCTL_GETMEMSIZE. Получив этот код, драйвер возвращает общий объем памяти устройтсва. Шаблон метода сгенерирован DriverWizard, но программист должен написать практически весь его код.
NTSTATUS XDSPdrvDevice::XDSPDRV_IOCTL_GETMEMSIZE_Handler(KIrp I) { NTSTATUS status = STATUS_SUCCESS; t << "Entering XDSPdrvDevice::XDSPDRV_IOCTL_GETMEMSIZE_Handler, " << I << EOL; //Количество памяти будет возвращено как число unsigned long. Поэтому определяем //указатель на unsigned long. unsigned long *buf; //Получаем указатель на буфер пользователя buf=(unsigned long*) I.UserBuffer(); //Записываем туда количество памяти нашего устройства. Получаем его при помощи //метода Count объекта m_MainMem. *buf=m_MainMem.Count(); //Длина возвращаемых нами данных равна длине числа unsigned long. I.Information() = sizeof(unsigned long); //Возвращаем STATUS_SUCCESS return status; }Написание драйвера завершено. Возможно, у Вас сложилось впечатление, что DriverWizard может практически все и написание драйвера - очень простая задача. Но не следует забывать, что наш драйвер - всего-то простейшая демонстрационная программа, которая практически не выполняет никаких полезных действий. Написание реальных драйверов является гораздо более сложной задачей.
Если бы драйвер был написан с использованием пакета DDK, то он бы имел практически ту же структуру и почти тот же код (правда, не объектно-ориентированный). Но в таком случае весь драйвер пришлось бы писать вручную, а DriverWizard генерирует скелет драйвера автоматически. Это сильно облегчает процесс разработки драйвера, позволяя программисту не заботиться о написании скелета драйвера и предохраняя его от возможных ошибок.
dll-библиотека (Dynamic Link Library) - программный модуль, который может быть динамически подключен к выполняющемуся процессу. Dll - библиотека может содержать функции и данные. При подключении dll к процессу она отображается на адресное пространство этого процесса.
Если говорить по-русски, то это означает: в любой момент времени программа может загрузить dll-библиотеку, получить указатели на функции и данные этой библиотеки. Потом приложение как-то использует функции и данные библиотеки, и когда они больше не нужны - выгружает библиотеку.
Dll-библиотека содержит два вида функций: внешние (External) и внутренние (Internal). Внутренние функции могут вызываться только самой dll, а внешние может также вызывать приложение, подключившее библиотеку. В этом случае говорят, что dll-библиотека экспортирует функции и данные.
Как было упомянуть выше, в настоящее время для связи с драйвером используется схема Приложение -> Библиотека dll -> Драйвер. При использовании такой архитектуры запрос приложения на операцию ввода-вывода поступает в dll-библиотеку, проходит там предварительную обработку и передается драйверу. Результат, возвращенный драйвером библиотеке dll, также обрабатывается и передается приложению. Преимущества такого подхода очевидны:
В нашем случае нам необходимо разработать dll-библиотеку, которая будет предоставлять приложению три функции: чтение памяти, запись в память и получение общего количества памяти устройства. Естественно, dll - библиотеку мы также будем проектировать в среде Visual C++.
Запустите среду VC++ и создайте новый проект с названием XDSPInter. В качестве типа проекта выберите Win32 Dynamic-Link Library. Далее в качестве типа проекта выберите A Simple DLL (простая dll-библиотека). Среда VC++ создаст для Вас пустой проект с одной- единственной функцией DllMain().
Функция DllMain() вызывается при подключении и отключении dll процессом. DllMain() имеет возвращаемое значение BOOL APIENTRY (фактически, она возвращает значение типа BOOL) и три параметра - HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved.
Параметры:
Функция DllMain() - единственная функция, которая обязательно должна присутствовать в библиотеке. Остальные функции и переменные добавляет программист в соответствии с решаемой задачей.
В нашем случае dll - библиотека будет экспортировать следующие функции: bool IsDriverPresent(void). Функция будет определять, присутствует ли в системе необходимый драйвер и попытаться подключиться к нему. Если это удастся - функция вернет true, в противном случае - false.
int ReadMem(char data, int len) - чтение данных из памяти устройства. Char* data - буфер для данных, int len - число 32-битных слов для чтения. Функция вернет число прочитанных слов.
int WriteMem(char *data, int len) - аналогична предыдущей; запись данных в
память.
int GetMemSize(void) - получить объем доступной памяти
устройства. Для того, чтобы функция стала экспортируемой, она должна быть
скомпилирована со специальным объявлением типа:
extern "C" __declspec (dllexport)Для того, чтобы при каждом объявлении функции не писать эту длинную малопонятную строку, определим ее, как директиву препроцессора:
#define EXPORT extern "C" __declspec (dllexport)Теперь перед каждым объявлением функции просто следует писать слово EXPORT. Создадим заголовочный файл нашей dll-библиотеки, в котором перечислим все экспортируемые функции и директивы препроцессора:
#define EXPORT extern "C" __declspec (dllexport) EXPORT int ReadMem(char *data, int len); EXPORT int WriteMem(char *data, int len); EXPORT int GetMemSize(void); EXPORT bool IsDriverPresent(void);Теперь рассмотрим текст исходного срр - файла библиотеки.
//В начале идут включения заголовочных файлов: #include "stdafx.h" // Основной заголовочный файл MFC #include "XDSPInter.h" //Наш заголовочный файл //Определим IOCTL-код для нашего драйвера: #define XDSPDRV_IOCTL_GETMEMSIZE 0x800 //Введем переменную, которая будет содержать HANDLE драйвера, возвращаемый //вызовом API CreateFile. HANDLE hDevice = INVALID_HANDLE_VALUE; //Также введем строку со значением символической ссылки на наше устройство: char *sLinkName = "\\\\.\\XDSPdrvDevice0"; //И зарезервируем переменную для хранения объема памяти карточки UINT dwSize; //Вспомогательная внутренняя функция OpenByName будет пытаться связаться с //драйвером. HANDLE OpenByName(void) { // вызов API. return CreateFile(sLinkName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); //Функция возвращает NULL, если не удалось подключится к драйверу и хэндл //на него в противном случае. } //Далее - функция DllMain: BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { //Определяем, почему была вызвана функция: switch (ul_reason_for_call) { //Приложение подключает библиотеку. Ничего не делаем. case DLL_PROCESS_ATTACH: { break; } case DLL_THREAD_ATTACH: { break; } //Приложение отключает библиотеку. case DLL_THREAD_DETACH: { //Закрыть хэндл драйвера if (hDevice != INVALID_HANDLE_VALUE) CloseHandle(hDevice); hDevice = INVALID_HANDLE_VALUE; break; } case DLL_PROCESS_DETACH: { //Закрыть хэндл драйвера if (hDevice != INVALID_HANDLE_VALUE) CloseHandle(hDevice); hDevice = INVALID_HANDLE_VALUE; break; } } //Все операции завершились успешно. Вернем true. return TRUE; } //Эта внешняя функция будет вызываться приложением, которое захочет установить //связь с драйвером. Функция вернет true в случае успеха и false при неудаче. EXPORT bool IsDriverPresent(void) { //Попытаемся открыть хэндл драйвера hDevice=OpenByName(); if (hDevice == INVALID_HANDLE_VALUE) //неудача return(false); else //Успех. return(true); }; //Внешняя функция, производящая чтение памяти устройства. Char* data - буфер для //данных, int len - число 32-битных слов для чтения. Функция вернет число //прочитанных слов. EXPORT int ReadMem(char *data, int len) { unsigned long rd=0; //Количество прочитанных слов //Системный вызов чтения данных из //файла. В данном случае - из нашего устройства //len - количество запрашиваемых слов //rd - количество прочитанных слов. ReadFile(hDevice, data, len, &rd, NULL); //Установить последний байт в 0 - признак конца строки. data[len*4+1]=0; //Вернуть количество прочитанных слов. return(rd); } //Внешняя функция, производящая запись в память. Практически аналогична //предыдущей. EXPORT int WriteMem(char *data, int len) { unsigned long nWritten=0; WriteFile(hDevice, data, len, &nWritten, NULL); //len - количество запрашиваемых слов //nWritten - количество прочитанных слов. return nWritten; } //Эта функция возвращает количество памяти устройства, байт. EXPORT int GetMemSize(void) { CHAR bufInput[4]; // Это пустой входной буфер, который будет //передан устройсву unsigned long bufOutput; //Буфер, куда драйвер запишет результат. ULONG nOutput; //Длина возвращенных драйвером данных, байт // Вызвать функцию device IO Control с кодом XDSPDRV_IOCTL_GETMEMSIZE if (!DeviceIoControl(hDevice, XDSPDRV_IOCTL_GETMEMSIZE, bufInput, 4, &bufOutput, 4, &nOutput, NULL) ) return(0); //Неудача else return(bufOutput); //Кол-во памяти }Таким образом, наша библиотека экпортирует всего четыре функции для работы с устройством. Все они имеют простой синтаксис и просты в использовании. Использование dll в нашем случае позволяет программисту не думать о сложных системных вызовах, необходимых для общения с драйвером, о формате передаваемых ему данных, а сосредоточится на решении прикладных задач.
После того, как написан драйвер и dll-библиотека для работы с ним, пришло время написать приложение пользоваеля, работающее с устройством. Оно будет взаимодействовать с драйвером через dll-библиотеку. Естественно, написано оно также будет в среде Visual C++. В принципе, его можно было бы реализовать в среде Visual Basic, Delphi или CВuilder, но это приведет к некоторым трудностям, прежде всего в использовании системных вызовов и структур данных. В данном разделе, в отличие от предыдущих, не рассматривается какое-либо конкретное приложение, а даются общие рекомендации по написанию такой программы.
Подключение библиотеки к приложению не требует особых усилий. Библиотека под- ключается при помощи системного вызова HMODULE LoadLibrary(char *LibraryName), где LibraryName - строка с именем файла dll-библиотеки. Возвращаемое значение - хендл (дескриптор) бибилиотеки. Если функция возвратила NULL, то произошла ошибка при подключении библиотеки.
После подключения библиотеки можно из нее импортировать функции. Импорт функции производится при помощи системного вызова
FARPROC GetProcAdress(HMODULE hModule, char * ProcName)
Так как из библиотеки импортируется не само тело функции, а ее адрес, то вызов такой функции превращается в непростое дело. Прежде всего, в программе объявляется не сама функция, а переменная, содержащая указатель на нее. Естественно, работа с таким указателем сильно отличается от работы с указателем на число или строку. Ведь функция в отличие от просто переменной возвращает значение и принимает некоторые параметры, поэтому указатель на нее должен быть объявлен специальным образом.
Указатель на функцию, ипортируемую из dll-библиотеки должен также быть скомпилирован со специальным объявлением типа - __declspec(dllimport). Эту строку также удобно представить в виде директивы #define.
#define XDSPINTER_API __declspec(dllimport).Мы импортируем из библиотеки четыре функции, поэтому необходимо определить их типы: параметры, передаваемые в функцию, возвращаемое значение. Это можно сделать при помощи директивы typedef:
//Объявить тип-указательна функцию, возвращающую значение типа int // и принимающую два параметра - массив типа char и число int. // В библиотеке ей будет соответствовать функция // EXPORT int ReadMem(char *data, int len) typedef XDSPINTER_API int ( *MemReadFun)(char *data, int len); // EXPORT int WriteMem(char *data, int len) typedef XDSPINTER_API int ( *MemWrtFun)(char *data, int len); // EXPORT int GetMemSize(void) typedef XDSPINTER_API int ( *MemSizeFun)(); //EXPORT bool IsDriverPresent(void) typedef XDSPINTER_API bool ( *IsDrivFun)();Теперь пришло время создать сами указатели на функции:
MemReadFun ReadMem; MemWrtFun WriteMem; MemSizeFun GetMemSize; IsDrivFun IsDriverPresent;Теперь рассмотрим функцию, подключающую dll-библиотеку к приложению. Она будет подключать dll-библиотеку к приложению и пытаться установить связь с драйвером. Функция вернет true в случае успеха и false при неудаче. Т.к. VC++ - объектноориентированная среда, то эта функция будет методом одного из классов приложения (в нашем случае - класса представления).
bool CXDSPView::ConnectToDriver() { //Переменная, в которой будет храниться возвращаемое значение. success=true; //HMODULE InterDll -переменная экземпляра, где хранится хэндл библиотеки. InterDll=::LoadLibrary("XDSPInter"); if (InterDll==NULL) { //Не удалось подключиться к библиотеке AfxMessageBox("Couldn't load a library XDSPInter.dll",MB_ICONERROR | MB_OK); //Вернем неудачу. success=false; } else { //Библиотека подключена успешно. Импортируем функции. ReadMem=(MemReadFun)::GetProcAddress(InterDll,"ReadMem"); if (ReadMem==NULL) { //Не удалось импортировать функцию AfxMessageBox("Couldn't get adress for ReadMem function from library XDSPInter.dll",MB_ICONERROR | MB_OK); success=false; } WriteMem=(MemReadFun)::GetProcAddress(InterDll,"WriteMem"); if (WriteMem==NULL) { //Не удалось импортировать функцию AfxMessageBox("Couldn't get an adress for WriteMem function from library XDSPInter.dll",MB_ICONERROR | MB_OK); success=false; } GetMemSize=(MemSizeFun)::GetProcAddress(InterDll,"GetMemSize"); if (GetMemSize==NULL) { //Не удалось импортировать функцию AfxMessageBox("Couldn't get an adress for GetMemSize function from library XDSPInter.dll",MB_ICONERROR | MB_OK); success=false; } IsDriverPresent=(IsDrivFun)::GetProcAddress(InterDll,"IsDriverPresent"); if (IsDriverPresent ==NULL) { //Не удалось импортировать функцию AfxMessageBox("Couldn't get an adress for IsDriverPresent function from library XDSPInter.dll",MB_ICONERROR | MB_OK); success=false; } } return(success); }Вызов метода ConnectToDriver() целесообразно сделать в конструкторе класса. Там же надо реализовать и проверку, присутствует ли в системе драйвер. Тогда вся необходимая инициализация будет проведена еще при запуске приложения.
CXDSPView::CXDSPView() : CFormView(CXDSPView::IDD) { //{{AFX_DATA_INIT(CXDSPView) //}}AFX_DATA_INIT //Здесь мы добавляем свой код. Success - переменная экземпляра. Если она //равна true - то ошибок нет, иначе произошла какая-то ошибка. success=true; //Пробуем подключить dll: if (ConnectToDriver()) { //Удалось подключить библиотеку. Теперь пытаемся установить связь с //драйвером - вызываем функцию в dll: if (!IsDrvPresent()) { //Неудача success=false; AfxMessageBox("Necessary driver isn't present in the system", MB_ICONERROR | MB_OK); } } else //Не удалось подключиться к dll. success=false; }Метод, производящий чтение памяти устройства может выглядеть следующим образом:
void CXDSPView::OnRead() { int res; //Количество слов, прочитанных из памяти res=(*ReadMem)(dt,256); //Пытаемся читать 256 слов. m_buff.SetWindowText(dt); //Выводим данные на экран //Код, характерный для VC++. CXDSPDoc *m_doc; //Подключаем документ, связанный с представлением m_doc=GetDocument(); //копируем туда данные. strcpy((char*)m_doc->m_buffer,dt); //Примечание: оба буфера должны иметь достаточный объем - минимум //256*4+1 байт. }Аналогично может выглядеть метод записи в память устройство:
void CXDSPView::OnWrite() { //Получили данные, введенный пользователем m_buff.GetWindowText(dt,32767); int res; //Записываем его в память устройства. Заметим, что в качестве длины данных //мы передаем не длину в байтах, а в 4-байтых словах. res=(*WriteMem)(dt,strlen(dt)%4+1); }Метод, возвращающий длину памяти устройтсва, совсем прост и, думаю, в комментариях не нуждается.
int CXDSPView::GetTotalLen() { int res=(*GetMemSize)(); return(res); }Также введем еще один метод, который может быть полезным. Он будет очищать память устройства.
void CXDSPView::OnClear() { //Получили документ CXDSPDoc *m_doc; m_doc=GetDocument(); //Забиваем буфер нулями for (int i=0;i<1025;i++) dt[i]=0; //Обнуляем буфер в классе документа m_doc->m_buffer[0]=0; int res; //Записывем в память устройства нули res=(*WriteMem)(dt,256); //Обновляем данные в окне приложения. m_buff.SetWindowText(dt); }Конечно, написанные нами приложение и dll-библиотека весьма несовершенны. Например, сбои будут происходить, если будут заущены несколько приложений. Тогда они будут одновременно обращаться к одной и той же dll и обновременно работать с устройством. Это может породить множество сбоев. В лучшем случае данные, получаемые каждым из них будут неадекватными. В худшем - система зависнет. Впрочем, этот недостаток можно устранить, модифицировав драйвер способом, описанным выше. Также в нашем приложении производится работа только с первыми 1024 байтами памяти устройства.
Конечно, коммерческая ценность такой системы равна нулю. Но она может быть хорошим учебным примером для ознакомления с программированием WDM - драйверов в Windows и DriverStudio.
Разговор о драйверах был бы неполным, если не упомянуть об отладке драйверов. Т.к. драйвера работают в нулевом кольце защиты процессора со всеми вытекающими последствиями, то обыкновенные отладчики пользовательских приложений не пригодны для отладки драйверов.
Если, например, разрабатывать драйвер под ОС Linux, то ситуация там может быть немного хуже: в этой ОС вообще нет какой-либо возможности отлаживать драйвера, кроме как воспользоваться отладчиком gdb. Но в таком случае надо перекомпилировать ядро системы специальным образом и станцевать еще несколько подобных танцев с бубном. Поэтому зачастую отладка сводится к вызову функций printk, которые в великом множестве раскиданы по всему ядру системы.
К счастью, хоть в этом Windows имеет преимущества. Для того, чтобы можно было отлаживать драйвера, отладчик должен сам работать в нулевом кольце защиты. Естественно, разработка такой программы является чрезвычайно сложной задачей, поэтому таких отладчиков на сегодняшний день известно всего два: WinDbg (поставляется с пакетом DDK) и SoftIce (входит в состав NuMega DriverStudio). SoftIce считается одним из лучших отладчиков для Windows всех типов. Это надежный, мощный и довольно удобный в использовании инструмент. SoftIce может применяться для различных целей: для отладки драйверов и приложений пользователя, для просмотра информации о системе и т.п. Мы рассмотрим, как применять SoftIce для отладки драйверов устройств.
Будучи установленным в Win98, SoftIce прописывает в Autoexec.bat строку вида: c:\Progra~1\numega\driver~1\softice\winice
Т.е. SoftIce загружается после загрузки DOS и сам грузит Windows. При работе Windows SoftIce активизируется лишь при каком-нибудь системном исключении или в точке останова, заданной программистом в драйвере. Также вызвать SoftIce можно, нажав Ctrl+D. На экране появляется окно отладчика.
Пока окно SoftIce активно, вся деятельность ОС замирает; именно сейчас можно безболезненно отлаживать драйвера.
Окно SoftIce разбито на несколько окон. Обычно в центре видно окно кода, над ним - окно регистров процессора и в самом низу - окно сообщений. Перемещаться в пределах окна можно, используя клавиши управления курсором или мышь.
В самом низу окна SoftIce расположена командная строка. SoftIce не имеет графического интерфейса, и все команды управления отладчиком вводятся в командной строке. SoftIce имеет довольно неплохую систему помощи. Перечень команд выдается по команде help. Наверное, самая важная команда - это команда выхода из SoftIce. Для этого нужно нажать клавишу F5 или дать команду Х (регистр не имеет значения).
Внимательно изучив окно сообщений, мы там увидим разнообразные системные сообщения и те сообщения, которые наш драйвер выводит через объект трассировки. Таким образом, можно просматривать все важные сведения, которые драйвер хочет сообщить нам. Если мы хочем, чтобы драйвер не выводил какие-то сообщения или выводил другие сообщения, нам надо отредактировать текст драйвера, добавив новые или удалив существующие трассировочные сообщения. После этого надо перекомпилировать драйвер и перезагрузить его.
Но, естественно, программисту мало простого чтения сообщений, посланных драйвером. Для эффективной отладки любой программы надо установить точку останова (breakpoint), просмотреть значения регистров. К счастью, SoftIce предоставляет такую возможность.
Универсальной точкой останова является использование прерывания INT 3. Как и в ОС MS-DOS, в Windows INT 3 также является прерыванием отладки. Для этого в тексте драйвера, где необходимо установить breakpoint, необходимо вставить следующий код:
_asm { int 3 }При этом присходит вызов прерывания INT 3.
Но по умолчанию SoftIce не реагирует на INT 3. Для того, чтобы по этому прерыванию активизировался отладчик, необходимо вызвать SoftIce и дать команду:
SET I3HERE ON
Теперь при вызове INT 3 произойдет <всплывание> этого кода в отладчике. Для отключения режима отладки по INT 3 следует дать команду SET I3HERE OFF.
После того, как наш драйвер <всплыл> в SoftIce, мы можем контролировать выполнение программы при помощи команд:
SoftIce также может просматривать значения переменных пользователя. Для того, чтобы открыть/закрыть окно просмотра переменных (Watch), надо дать команду WW или нажать Alt+F4. Добавить/убрать переменную для просмотра можно при по- мощи команды WATCH.
Это основные команды, применяемые для отладки драйверов устройств в SoftIce. А в общем, этот отладчик имеет огромное количество функциональных возможностей и его пол- ное описание пославляется с программой и занимает порядка двухсот страниц. Надеюсь, это руководство было для Вас интересно. Если даже не интересно - то, надеюсь, Вы узнали что-то новое для себя.