• 2.1 Использование Driver Wizard
  • 2.2 Компиляция и установка драйвера.
  • 2.3 Наращивание функциональных возможностей драйвера.
  • 2.4 Разработка dll-библиотеки для взаимодействия с драйвером
  • 2.5 Подключение dll-библиотеки к приложению.
  • 2.6 Отладка драйверов
  • 2. Разработка драйвера в среде DriverStudio.

    2.1 Использование Driver Wizard

    Процесс разработки драйвера при помощи DriverStudio во многом напоsминает разработку приложения в среде Visual C++. Создание проекта происходит при помощи мастера DriverWizard, похожего на мастер Visual C++. Мастер вызывается или из главного меню (Пуск→Программы→DriverStudio→DriverWorks→DriverWizard) или из среды Visual C++ при помощи пункта меню DriverStudio — DriverWizard. Программе DriverWizard соответствует иконка

    Далее при работе мастера появляется серия диалоговых окон, куда пользователь должен ввести данные, необходимые для формирования скелета драйвера.

    Рис 6. Первый шаг DriverWizard


    На первом шаге создания драйвера необходимо ввести имя проекта (в нашем случае — XDSP и директорию для проекта. После этого — нажать на кнопку Next, чтобы перейти к следующему шагу.

    Рис 7. Второй шаг DriverWizard


    На втором шаге следует выбрать архитектуру, по которой будет разрабатываться драйвер: Windows NT 4.0 (которая сейчас практически не используется) или WDM, которую нам и следует выбрать.

    Рис 8. Третий шаг DriverWizard


    На третьем шаге выберем шину, на которой располагается устройство, которое будет контролировать драйвер. Если это устройство будет подключаться к порту компьютера, например к параллельному — надо выбрать None — driver does not control any hardware. Если же устройство будет располагаться на одной из шин компьютера, например на PCI — надо задать дополнительные параметры. В случае PCI устройства надо указать следующие параметры:

    • Код производителя (PCI Vendor ID) — четырехзначное шестнадцатеричное число, которое однозначно идентифицирует производителя устройства. Пусть в нашем случае оно будет равно 1999.

    • Код устройства (PCI Device ID) — также четырехзначное шестнадцатеричное число, которое однозначно идентифицирует устройство нашего производителя. Пусть в нашем случае это будет 680C.

    • Номер подсистемы PCI. Обычно имеет вид код устройства + код производителя. В нашем случае — 680C1999.

    • Номер версии устройства (PCI Revision ID) — номер версии устройства. В нашем случае 01.

    Эти коды весьма важны: по ним система будет находить драйвер для устройства. Эти же коды аппаратно прошиты в PCI-карточке. И если коды, заданные в драйвере (если быть точным, то они задаются не в самом файле драйвера, а в инсталляционном скрипте — inf-файле), не совпадут с кодами в PCI-устройстве, то драйвер не установится.

    Рис 9. Четвертый шаг DriverWizard


    На четвертом шаге мастера необходимо задать имена, которые DriverWizard присвоит файлу С++, который содержит класс драйвера, и самому классу драйвера (Driver Class).

    Рис 13. Пятый шаг DriverWizard


    На пятом шаге следует указать, какие функции должен выполнять драйвер. Это может быть:

    • чтение (read) — обработка запросов на чтение.

    • запись (write) — обработка запросов на запись.

    • сброс (flush) — обычно это сброс буфера обмена с устройством.

    • управление устройством (device control) — обработка других запросов.

    • внутреннее управление устройством (internal device control) — обработка запросов от других драйверов устройств.

    Рис 14. Шестой шаг DriverWizard


    На шестом шаге DriverWizard задает вопросы о способе обработки запросов. Опция Select queuing method выбирает, каким образом будут буферизироваться запросы на ввод-вывод:

    • None — запросы не буферизируются в очереди. Эту опцию лучше не выбирать.

    • DriverManaged — драйвер содержит одну или более одной очередей, в которой сохраняются запросы на ввод-вывод, пришедшие от других драйверов или системы.

    • SystemManaged — драйвер использует только одну очередь сообщений.

    Также надо выбрать, будут ли буферизироваться запросы на чтение и запись. Как было сказано ранее, устройство может одновременно выполнять какую-то одну операцию, например, только чтение или только запись, или может выполнять несколько операций сразу. Чтобы гарантировать нормальную работу устройства в этом случае, следует буферизировать (Serialize) поступающие запросы на чтение и запись, помещая их в очередь. Установка флажков Seralize all Read requests и Serialize all Write requests позволяет буферизировать все запросы на чтение и запись, поступающие в объект устройства.

    Рис. 15 — Седьмой шаг DriverWizard.


    На седьмом шаге предлагается задать параметры, которые драйвер будет загружать из реестра Windows при старте, когда система загружается. При этом задается параметр реестра, имя переменной, куда сохраняется его значение, тип данного параметра и его значение по умолчанию. Если не менять настройки, то во время загрузки драйвер читает из реестра параметр BreakOnEntry типа boolean, сохраняет его значение в переменной m_BreakOnEntry. Значение по умолчанию для параметра — false. Обычно m_BreakOnEntry используется в отладочных целях.

    Запись и считывание параметров из реестра позволяет драйверу задавать какие-либо конфигурационные параметры, сохранять данные, необходимые для его запуска или работы.

    При помощи кнопок Add, Edit и Delete можно соответственно добавлять, редактировать и удалять параметры.

    Рис. 16 — Восьмой шаг DriverWizard.


    Восьмой шаг DriverWizard — один из самых важных моментов в разработке драйвера PCI–устройства при помощи DriverWorks. Поэтому окно мастера несет огромное количество информации и элементов управления.

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

    Окно DriverWizard также содержит несколько вкладок:

    Рис.17 — вкладка Resource


    Вкладка 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, с полным доступом.

    Рис. 18 — задание диапазона адресов памяти.


    По аналогичному принципу можно задать параметры портов ввода-вывода и линий DMA. Параметры линий запроса на прерывание посложнее: тут можно дать указание DriverWizard'у создать шаблоны для классов ISR, DPC и их функций (Make ISR/DPC class functions).

    Если в процессе задания ресурсов задана ошибка или необходимол внести какие-либо изменения, то для этого надо щелкнуть по названию ресурса в окне правой клавишей мыши. Появится контекстное меню, в котором надо выбрать пункт Delete, чтобы удалить ресурс или Edit — редактировать его.

    Рис. 19 — вкладка Interface.


    На вкладке Interface задается способ, каким образом будет осуществлятся связь программы или библиотеки DLL с драйвером.

    Надежным способом является связь при помощи GUID класса. GUID — уникальный номер, который однозначно идентифицирует какой-либо объект системы. При помощи GUID идентифицируются не только драйвера, а и СОМ–интерфейсы и пр.

    Другим способом реализации интерфейса является является символическая ссылка. Это более естественный путь, т.к. просто указать имя класса устройства — гораздо проще, чем указывать непонятного вида GUID.

    Рис. 20 — вкладка Buffers.


    На вкладке Buffers определяется метод, каким образом буферизируются запросы к устройству.

    Буферизированный (buffered) метод — пригоден для устройств типа мыши, клавиатуры, которые передают небольшие объемы данных за короткий промежуток времени. Прямой (direct) метод — используется при пересылке больших объемов информации за короткий промежуток времени, например, при обращении к дисководу.

    Рис. 21 — вкладка Power.


    При создании WDM–драйвера необходимо задать способ управления энергопотреблением. При помощи флажка Управлять энергопотреблением этого устройства (Manage power for this device) можно создать в драйвере методы управления энергопотреблением нашего устройства. В нашем простом случае мы не будем этого делать.

    Рис. 22 — девятый шаг DriverWizard.


    Естественно, для более-менее сложного драйвера устройства будет недостаточно двух запросов на чтение и запись. На девятом шаге можно задать коды управления драйвером устройства. Код управления (Device IO control code, IOCTL) просто представляет собой число, которое передается драйверу. Коды управления в драйвере обрабатываются специальной функцией. В ответ на каждый код драйвер выполняет какое-либо действие. Например, в нашем случае объект устройства будет возвращать количество памяти, которое имеет PCI-карточка. Для этого зададим код управления XDSP_GetMemSize. Для этого нажмем на кнопку Add, появится диалоговое окно Edit IO Control Code (редактирование кода управления).

    Рис. 23 — задание кода управления драйвером.


    При задании кода управления устройством нужно указать имя кода в понятном программисту виде, метод общения с устройством (прямой или буферизированный). Также задается порядковый номер кода (Ordinal) — число, являющееся его уникальным номером. Числа, меньшие 0x800 используются для стандартных кодов, таких, как чтение, запись и т.п.

    Запросы IOCTL также можно буферизировать, подобно запросам на чтение и запись. Для этого надо установить флажок Queue (serialize) this request code.

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

    Рис. 24 — десятый шаг DriverWizard.


    Одним из достоинств DriverWorks является то, что DriverWizard сразу создает консольное приложение для тестирования работоспособности драйвера. Конечно, такое тестирование бывает неполным и примитивным, но позволяет оценить, правильно ли работает драйвер и работает ли он вообще. Для того, чтобы DriverWizard создал такое приложение, нужно установить флажок Create test console application (создать консольное приложение для тестирования) и указать его имя. Также можно задать опции отладки. Они необходимы при отладке драйвера средствами DriverStudio. При написании простых драйверов эти опции, скорее всего, не понадобятся.

    Пройдя все эти шаги, нажмите на кнопку Finish. В ответ появится окошко, которое содержит сведения о каталоге с файлами проекта нашего драйвера, для чего предназначен каждый файл. Нажимаем на кнопку OK — DriverWizard сгенерирует все файлы нашего драйвера, приложения для тестирования и предложит открыть проект в Visual C++.

    2.2 Компиляция и установка драйвера.

    Проект, сгенерированный DriverWizard, находится в каталоге XDSP. В этом каталоге расположены файлы рабочего пространства (Workspace) VC++: XDSP.dsw, XDSP.ncd и XDSP.opt и два каталога – sys и exe. Здесь же находится файл XDSPioctl.h. В нем описаны управляющие коды, используемые при обращении к драйверу с помощью функции DeviceIOControl.

    В каталоге sys находится сгенерированный DriverWizard скелет драйвера и все необходимые для компиляции файлы. В нашем случае, имеем файлы:

    Function.h

     заголовочный файл, предназначенный для определения функций, входящих в драйвер;

    Makefile, Sources

     файлы с информацией, необходиой для компиляции проекта в VC++.

    XDSP.h , XDSP.cpp

     файлы, содержащие класс драйвера.

    XDSP.plg, XDSP.dsp

     проект VC++;

    XDSP.inf

     скрипт для инсталляции драйвера;

    XDSP.rc

     файл ресурсов проекта. В основном, содержит информацию о разработчике драйвера.

    XDSPDevice.cpp, XDSPDevice.h

     файлы, содержащие класс устройства.

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

    Makefile,Sources

     файлы с информацией, необходиой для компиляции проекта в VC++.

    Test_XDSP.plg, Test_XDSP.dsp

     проект VC++;

    Test_XDSP.cpp

     исходный текст приложения.

    Теперь самое время открыть проект драйвера в среде VC++ и посмотреть, что же мы имеем. Для этого надо запустить VC++ и открыть проект, используя команду File→Open Workspace. В появившемся диалоговом окне открытия файла выберите файл XDSP.dsw. Если все вышеописанные действия выполнены правлиьно, то проект откроется в среде VC++. Для того, чтобы проект скомпилировался правильно, следует установить переменные среды DriverStudio. Для этого нужно выбрать пункт меню DriverStudio→Driver Build Settings: На экране появится диалоговое окно установки переменных среды:

    Рис.25 — установка значений переменных среды.


    Для компиляци драйвера важны две переменные:

    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).

    –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –, –,

    –, –, –, –, –, –,

    В данном случае приложение установило связь с драйвером и прочитало из него 32 байта. Функция чтения в драйвере не определена, поэтому, естественно, драйвер вернет абракадабру. Если же будет получено сообщение вида

    C:\…Projects\XDSPdrv\exe\objchk\i386>Test_XDSP.exe r 32

    Test application Test_XDSP starting…

    ERROR opening device: (2) returned from CreateFile

    Exiting…

     — то приложение не смогло установить связь с драйвером. Следует попробовать переустановить драйвер.

    2.3 Наращивание функциональных возможностей драйвера.

    Рассмотрим подробно текст драйвера, сгенерированного DriverWizard и внесем в него необходимые изменения.

    В проекте пристствуют всего два класса:

    XDSP

     класс драйвера;

    XDSPDevice

     класс устройства.

    Также есть несколько глобальных функций и переменных:

    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 вызывается, когда драйвер получает запрос на чтение. Это важная функция. Т.к. мы хотим, чтобы приложение пользователя могло читать и писать в память микросхемы, то именно сюда необходимо добавлять наш код. Все фрагменты кода, добавленные программистом, будут выделены жирным шрифтом:

    //This code was added by the programmer

    Фактически в данном методе мы должны прочитать содержимое памяти и передать его приложению пользователя. Но тут самое время вспомнить, что плата обменивается с памятью 4– байтными словами. Поэтому для операций с памятью следует применять метод ind/outd.

    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 генерирует скелет драйвера автоматически. Это сильно облегчает процесс разработки драйвера, позволяя программисту не заботиться о написании скелета драйвера и предохраняя его от возможных ошибок.

    2.4 Разработка dll-библиотеки для взаимодействия с драйвером

    dll-библиотека (Dynamic Link Library) — программный модуль, который может быть динамически подключен к выполняющемуся процессу. Dll–библиотека может содержать функции и данные. При подключении dll к процессу она отображается на адресное пространство этого процесса.

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

    Dll-библиотека содержит два вида функций: внешние (External) и внутренние (Internal). Внутренние функции могут вызываться только самой dll, а внешние может также вызывать приложение, подключившее библиотеку. В этом случае говорят, что dll-библиотека экспортирует функции и данные.

    Как было упомянуть выше, в настоящее время для связи с драйвером используется схема Приложение→Библиотека dll→Драйвер. При использовании такой архитектуры запрос приложения на операцию ввода-вывода поступает в dll-библиотеку, проходит там предварительную обработку и передается драйверу. Результат, возвращенный драйвером библиотеке dll, также обрабатывается и передается приложению. Преимущества такого подхода очевидны:

    • Выпускается огромное количество различных периферийных устройств, и, соответственно, для каждого устройства разрабатывается свой драйвер. Программисту будет тяжело разбираться во всех тонкостях работы драйвера устройства: формат данных для чтения/записи, запоминать непонятные IOCTL-коды. Гораздо лучше — предоставить для него понятный интерфейс API-функций для работы с устройством. Еще лучше, если такой интерфейс будет унифицированным для всех устройств данного типа. Задача 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.

    Параметры:

    • HANDLE hModule — дескриптор (хэндл) нашей dll;

    • DWORD ul_reason_for_call — флаг, показывающий, почему была вызвана функция. Может принимать значения:

     • DLL_PROCESS_ATTACH или DLL_THREAD_ATTACH — библиотека подключается к процессу;

     • DLL_PROCESS_DETACH или DLL_THREAD_DETACH — библиотека отключается от процесса.

    • 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; //Количество прочитанных слов

     ReadFile(hDevice, data, len, &rd, NULL); //Системный вызов чтения данных из

      //файла. В данном случае – из нашего устройства

      //len – количество запрашиваемых слов

      //rd – количество прочитанных слов.

     data[len*4+1]=0; //Установить последний байт в 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 в нашем случае позволяет программисту не думать о сложных системных вызовах, необходимых для общения с драйвером, о формате передаваемых ему данных, а сосредоточится на решении прикладных задач.

    2.5 Подключение dll-библиотеки к приложению.

    После того, как написан драйвер и dll-библиотека для работы с ним, пришло время написать приложение пользоваеля, работающее с устройством. Оно будет взаимодействовать с драйвером через dll-библиотеку. Естественно, написано оно также будет в среде Visual C++. В принципе, его можно было бы реализовать в среде Visual Basic, Delphi или CВuilder, но это приведет к некоторым трудностям, прежде всего в использовании системных вызовов и структур данных. В данном разделе, в отличие от предыдущих, не рассматривается какое-либо конкретное приложение, а даются общие рекомендации по написанию такой программы.

    Подключение библиотеки к приложению не требует особых усилий. Библиотека подключается при помощи системного вызова HMODULE LoadLibrary(char* LibraryName), где LibraryName — строка с именем файла dll-библиотеки. Возвращаемое значение — хендл (дескриптор) бибилиотеки. Если функция возвратила NULL, то произошла ошибка при подключении библиотеки.

    После подключения библиотеки можно из нее импортировать функции. Импорт функции производится при помощи системного вызова

    FARPROC GetProcAdress(HMODULE hModule, char * ProcName)

    • hModule — хэндл библиотеки, возвращенный LoadLibrary;

    • ProcName — строка с именем импортируемой функции. Вызов GetProcAdress возвращает адрес функции с заданным именем и NULL, если такой функции нет в библиотеке.

    Так как из библиотеки импортируется не само тело функции, а ее адрес, то вызов такой функции превращается в непростое дело. Прежде всего, в программе объявляется не сама функция, а переменная, содержащая указатель на нее. Естественно, работа с таким указателем сильно отличается от работы с указателем на число или строку. Ведь функция в отличие от просто переменной возвращает значение и принимает некоторые параметры, поэтому указатель на нее должен быть объявлен специальным образом.

    Указатель на функцию, ипортируемую из 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.

    2.6 Отладка драйверов

    Разговор о драйверах был бы неполным, если не упомянуть об отладке драйверов. Т.к. драйвера работают в нулевом кольце защиты процессора со всеми вытекающими последствиями, то обыкновенные отладчики пользовательских приложений не пригодны для отладки драйверов.

    Если, например, разрабатывать драйвер под ОС 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, мы можем контролировать выполнение программы при помощи команд:

    HERE        (F7)

     шаг на следующую строку в окне кода;

    T       F8

     выполнить одну инструкцию процессора (трассировка);

    HBOOT

     перезагрузка системы;

    G

     перейти на указанный адрес;

    GENINT

    сгенерировать прерывание;

    X       F5

     продолжить выполнение программы (выход из SoftIce).

    Если драйвер был скомпилирован в отладочной конфигурации, то на экране будет виден текст драйвера, написанный на С++.

    SoftIce также может просматривать значения переменных пользователя. Для того, чтобы открыть/закрыть окно просмотра переменных (Watch), надо дать команду WW или нажать Alt+F4. Добавить/убрать переменную для просмотра можно при по– мощи команды WATCH.

    Это основные команды, применяемые для отладки драйверов устройств в SoftIce. А в общем, этот отладчик имеет огромное количество функциональных возможностей и его полное описание пославляется с программой и занимает порядка двухсот страниц.


    Надеюсь, это руководство было для Вас интересно. Если даже не интересно — то, надеюсь, Вы узнали что-то новое для себя.

    Написать мне

    Зайти на мою домашнюю страничку

    С уважением — Александр Тарво.







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх