Программирование драйверов Windows

         

Средства локальных процедурных вызовов


Local Procedure Call (LPC), локальный процедурный вызов, является механизмом вызовов между процессами на одном компьютере. Так как меж-"процессный" вызов (interprocess call) может проходить между разными адресными пространствами, то существуют и соответствующий Исполнительный компонент, который делает это действие возможным и эффективным. Как правило, драйверному коду не требуется осуществление вызова другого процесса и, соответственно, LPC средства.



Сродство к процессору


В тех случаях, когда аппаратное обеспечение системы содержит более чем один процессор, возрастает значение вопроса: как прерывания будут обрабатываться? Как подключена линия прерывания устройства — к одному процессору или ко всем?

Как правило, существуют некоторые компоненты аппаратного обеспечения, которые позволяют драйверу конфигурировать и распределять сигнал прерывания. Если отдельный процессор обслуживает прерывания, поступающие только от определенного устройства, то про такие прерывания говорят, что они имеют "сродство к процессору" (processor affinity). Назначение конкретного процессора для обработки конкретного прерывания может быть использовано для контроля сбалансированности загрузки отдельного устройства, если в системе присутствуют несколько процессоров и несколько управляемых устройств.



Стадия загрузки


Загрузчик NTLDR считывает файл BOOT.INI, использует его для отображения экрана загрузчика, предоставляет пользователю возможность выбора ОС, выбирает профиль оборудования и загружает ядро, но не инициализирует его. Файл BOOT.INI содержит 2 раздела: [Boot Loader] и [Operating System]



Старт операции ввода/вывода


В том случае, если рабочая процедура решит задействовать механизм System Queuing и послать Диспетчеру ввода/вывода запрос на вызов процедуры StartIo, то тот выполнит проверку, не занято ли устройство. Диспетчер ввода/вывода может это установить путем проверки, завершены или нет IRP для данного устройства. Если предыдущий запрос еще не завершен, новый запрос на старт операции ввода/вывода помещается в очередь. В противном случае, процедура StartIo вызывается непосредственно.

Ожидается, что процедура старта ввода/вывода, реализация которой возлагается исключительно на разработчика драйвера, выполняет следующие задачи (или часть из них):

Проверяет код IRP функции (чтение, запись и т.п.) и выполняет установочные действия для данного типа операций.

В случае, если устройство (то есть объект устройства, которому адресован IRP пакет) по логике работы олицетворяет только одну из функций сложного реального устройства, запрашивает исключительный доступ к заранее созданному объекту контроллера, планируя при этом вызов соответствующей процедуры ControllerControl.

В случае, если запрос требует DMA операций, выполняет надлежащие операции над объектом адаптера и планирует вызов процедуры AdapterControl (которая будет вызвана, сразу же, как только условия исключительного доступа к соответствующему DMA каналу будут удовлетворены).

Использует процедуру SynchCritSection для выполнения безопасного доступа к тем ресурсам, которые могут потребоваться процедуре обработки прерываний.

Возвращает управление Диспетчеру ввода/вывода в ожидании сигналов прерывания от устройства.

Пример драйвера с использованием механизма System Queuing подробно рассматривается в главе 11.



Structure


Структура, тип данных языка С. Состоит из простых типов данных (char, int и т.п.) и вложенных структур или объединений (union), Например, тип данных LARGE_INTEGER иногда (файл ntdef.h) определяется как структура, состоящая из одного поля:

typedef struct _LARGE_INTEGER { LONGLONG QuadPart; } LARGE_INTEGER;



Структура INF файла


Инсталляционный inf-файл является текстовым файлом, поставляемым вместе с драйверным программным обеспечением и аппаратным обеспечением, соответственно.

В операционных системах Windows 9x/Me размер inf-файл не может превосходить 64 килобайта. Для NT систем ограничений нет. Если не указано иначе для конкретного типа ОС, то максимальная длина любого поля в inf-файле составляет 512 символов.



Symbolic Link


Символьная ссылка, символическая ссылка, строго говоря — символьная связь. В области разработки драйверов — файловый объект с особыми свойствами. Выполняя операцию по открытию такого файла (CreateFile, ZwCreateFile), клиент драйвера получает к нему доступ (в виде ненулевого дескриптора, HANDLE), причем все запросы клиента будут адресованы как раз тому FDO объекту, с которым соотнесена данная символьная ссылка (созданная по запросу драйвером в результате вызова IoCreateSymbolicLink).



Synchronization Objects


Объекты синхронизации. Эти объекты позволяют программным потокам корректировать последовательность своей работы или последовательность доступа к критическим данным, которые не могут быть доступны для чтения и записи разными потоками (то есть результат их модификации должен быть предсказуем в любой момент времени).

В данную категорию входят События (Event), Мьютексы (Mutex, Mutual Exception — взаимное исключение), Семафоры (Semaphore), Спин-блокировки (Spin Lock) и даже объекты потоков. Не вполне объектами, но элементами синхронизации можно также считать аналог критических секций, что в режиме ядра реализуется при помощи вызовов KeEnterCriticalRegion и KeLeaveCriticalRegion.

Следует отдавать себе отчет, что владение объектами синхронизации или возможность установки состояний объектов синхронизации не ведет непосредственно к остановке или запуску какого-либо из потоков. Имеется в виду тот факт, что объект синхронизации подобен светофору на перекрестке — только тот водитель, который соблюдает правила, останавливается. Лихач же, игнорирующий правила, может проехать, игнорируя любой сигнал.

Для простой остановки выполнения кода потока (из него же самого) на некоторое время используются системные вызовы, например KeStallExecutionProcessor. Для организации запуска определенных процедур (функций) драйвера используются объекты таймеров, управление которыми выполняется при помощи соответствующих вызовов (KeSetTimer и т.п.).

Все объекты синхронизации являются объектами ядра, общими для использования и в режиме ядра, и в пользовательском режиме. Если передать в пользовательское приложение дескриптор, например, объекта события (Event Object), который был создан в драйвере, то можно организовать пробуждение пользовательских потоков в нужное время вместо того, чтобы заставлять пользовательское приложение постоянно опрашивать драйвер.



System Paging File


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



Таймерные процедуры


Драйвер, которому нужно выполнять точный отсчет временных интервалов, должен использовать либо DPC процедуру IoTimer (которая, если она зарегистрирована должным образом, всегда вызывается один раз в секунду), либо Custom DPC процедуру для таймера (кратко была рассмотрена выше). В литературе последняя упоминается как CustomTimerDpc.



Таймеры и их использование


В рассмотренном выше методе организации временных задержек с использованием предоставляемой драйвером callback-функции IoTimerRoutine объект таймера, хотя и участвовал, но в скрытой форме. К тому же, во всем обширном наборе примеров, прилагаемых к DDK, этот прием используется всего 2 раза. (Правда, возможно оттого, что столь длительные интервалы ожидания редко требуются в современной компьютерной технике.)

Простейшим из синхронизационных примитивов является объект события, event, который имеет два состояния: сигнальное и несигнальное. Переход в сигнальное состояние стимулирует работу функции KeWaitXxx, а выполняется он под влиянием вызова KeSetEvent. Подробнее объекты события будут рассмотрены ниже, пока что отметим, что пребывание в сигнальном либо несигнальном состоянии &#8212 есть самое основное свойство всех объектов синхронизации.

Не составляют исключения и таймеры. Можно сказать, что таймер &#8212 это объект события, который самостоятельно переходит в сигнальное состояние по истечении некоторого времени, заданного при запуске таймера. При этом таймер может выполнять дополнительные "услуги", например, планировать запуск DPC процедуры (специально созданной и зарегистрированной драйвером процедуры отложенного вызова), что будет рассмотрено позже.

Системные вызовы для работы с таймерными объектами перечислены в таблице 10.15.

Таблица 10.15. Системные вызовы для работы с таймерными объектами

Системные вызовы Описание
KeInitializeTimer

KeInitializeTimerEx

Инициализация таймерного объекта
KeSetTimer

KeSetTimerEx

Установка таймерного объекта в несигнальное состояние (подготовка к срабатыванию)
KeCancelTimer Прекращает работу таймера
KeReadStateTimer Возвращает TRUE, если таймер в сигнальном состоянии
KeInitializeDpc Инициализирует DPC объект, подготавливая его для работы с таймерными вызовами

Можно сказать, что вызов KeInitializeDpc находится в чужой компании, однако, этот вызов совершенно необходим, если таймер будет работать с DPC функциями.


Прежде чем перейти к подробному рассмотрению системных вызовов, обслуживающих операции над таймерными объектами, разберем сначала вызовы KeWaitXxx, то есть KeWaitForSingleObject и KeWaitForMultipleObjects.

Если есть сигнализирующие объекты (события, мьютексы, семафоры, таймеры и т.п.), то у программных потоков должны быть и специальные средства, которые позволили бы им остановиться и ожидать изменений в состоянии этих объектов. Именно такими средствами, применяемыми программными потоками для остановки и ожидания, являются вызовы KeWaitForSingleObject и KeWaitForMultipleObjects.

Первый из них заставляет дожидаться перехода одного объекта в сигнальное состояние, второй &#8212 сразу нескольких.

Таблица 10.16. Прототип вызова KeWaitForSingleObject

NTSTATUS KeWaitForSingleObject IRQL &#60= DISPATCH_LEVEL
Параметры Приостанавливает данный программный поток до момента перехода указанного объекта в сигнальное состояние либо до момента превышения значения Timeout
IN PVOID pObject Указатель на инициализированный ранее объект синхронизационного примитива (объект события, объект таймера, объект потока и т.п.)
IN KWAIT_REASON Reason Для драйверов: Executive

IN KPROCESSOR_MODE WaitMode Для драйверов: KernelMode
IN BOOLEAN bAlertable Для драйверов: FALSE
IN PLARGE_INTEGER Timeout • Предельное время ожидания, положительное значение &#8212 абсолютное время, отрицательное &#8212 относительный временной интервал (в 100нс отсчетах)

• NULL при безусловном (неограниченном) ожидании
Возвращаемое значение Для драйверов:

• STATUS_SUCCESS &#8212 ожидание успешно завершено

• STATUS_TIMEOUT &#8212 превышено предельное время ожидания
Вызов KeWaitForSingleObject на уровне IRQL равном DISPATCH_LEVEL следует выполнять только при нулевом значении параметра Timeout. Ha практике этот вызов выполняется из программного потока, работающего на уровне IRQL, равном PASSIVE_LEVEL.

"Самоостанов" программного потока вызовом KeWaitForSingleObject



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

С функцией KeWaitForMultipleObjects (таблица 10.17) следует обращаться с осторожностью, поскольку существуют системные ограничения на количество объектов, которые могут быть заданы в качестве влияющих на ожидание. Каждый поток имеет встроенный массив Wait-блоков, который он использует для действующих совместно "операций ожидания". Поток может использовать этот массив для ожидания переходов состояния более чем THREAD_WAIT_OBJECTS объектов. В случае, если число THREAD_WAIT_OBJECTS недостаточно, драйвер должен предложить собственный массив Wait-блоков при выполнении вызова KeWaitForMultipleObjects. В любом случае, число объектов, от которых зависит завершение ожидания, не может превышать MAXIMUM_WAIT_OBJECTS.

Таблица 10.17. Прототип вызова KeWaitForMultipleObjects

NTSTATUS KeWaitForMultipleObjects IRQL &#60= DISPATCH_LEVEL
Параметры Приостанавливает данный программный поток до момента перехода всех или хотя бы одного из указанных объектов в сигнальное состояние либо до момента превышения значения Timeout
IN ULONG Count Число объектов, по которым определяется момент окончания ожидание
IN PVOID pObjects[] Массив указателей на инициализированные объекты
IN WAIT_TYPE WaitType • WaitAll &#8212 ожидание все объектов

• WaitAny &#8212 ожидание хотя бы одного
IN KWAIT_REASON Reason Для драйверов: Executive
IN KPROCESSOR_MODE WaitMode Для драйверов: KernelMode
IN BOOLEAN bAlertable Для драйверов: FALSE
IN PLARGE_INTEGER Timeout • Предельное время ожидания, положительное значение &#8212 абсолютное время, отрицательное &#8212 относительный временной интервал (в 100нс отсчетах)

• NULL при безусловном (неограниченном) ожидании
IN PKWAIT_BLOCK WaitBlocks[]

OPTIONAL
Массив Wait-блоков для этой операции (можно указать NULL)
Возвращаемое значение Для драйверов:

• STATUS_SUCCESS &#8212 ожидание успешно завершено

• STATUS_TIMEOUT &#8212 превышено предельное время ожидания
<


Вызов KeWaitForMultipleObjects (также как и описанный выше вызов KeWaitForSingleObject) на уровне IRQL равном DISPATCH_LEVEL следует выполнять только при нулевом значении параметра Timeout.

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

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

или KeInitializeTimerEx. Выделенная память должна быть резидентна. Иными словами, ее следует выделять в нестраничном пуле, например, вызовом ExAllocatePool.

Таблица 10.18. Прототип вызова KeInitializeTimer

VOID KeInitializeTimer IRQL &#60= DISPATCH_LEVEL
Параметры Инициализирует таймер типа NotificationTimer
IN PKTIMER pTimerObj Указатель на место для объекта таймера, заранее подготовленное инициатором вызова
Возвращаемое значение void
Таблица 10.19. Прототип вызова KeInitializeTimerEx

VOID KeInitializeTimerEx IRQL &#60= DISPATCH_LEVEL
Параметры Инициализирует таймер с указанием типа
IN PKTIMER pTimerObj Указатель на место для объекта таймера, заранее подготовленное инициатором вызова
IN TIMER_TYPE Type • NotificationTimer

• SynchronizationTimer
Возвращаемое значение void
Таймер типа NotificationTimer запускает выполнение всех потоков, ожидавших его перехода в сигнальное состояние, и остается в сигнальном состоянии до тех пор, пока кто-то не переведет его явным образом (вызовом KeSetTimerEx

или KeSetTimer) в несигнальное.

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



После того как объект таймера создан (это можно сделать в самом начале работы, например, поместив указатель на объект таймера в структуру расширения объекта устройства), следует его запустить, разумеется, в нужном месте программного кода драйвера &#8212 в соответствии с логикой работы. Делается это вызовами KeSetTimer либо KeSetTimerEx.

Когда время ExpirationTime, заданное в KeSetTimer, истекает, объект таймера извлекается из системной очереди таймерных объектов и переходит в сигнальное состояние. Если задана процедура отложенного вызова CustomTimerDpc, то в этот момент соответствующий ей объект pTimerDpcObject помещается в системную очередь DPC объектов (разумеется, если там его еще нет) с тем, чтобы выполнить CustomTimerDpc в ближайшее время. Ранее окончания ожидания процедура CustomTimerDpc вызвана быть не может. Для многократного запуска автоматического таймера следует использовать вызов KeSetTimerEx.

Таблица 10.20. Прототип вызова KeSetTimer

BOOLEAN KeSetTimer IRQL &#60= DISPATCH_LEVEL
Параметры Инициализирует таймер с указанием типа
IN PKTIMER pTimerObject Указатель на объект таймера, инициализированный вызовами KeInitializeTimer или KeInitializeTimerEx
IN LARGE_INTEGER ExpirationTime Время ожидания, положительное значение &#8212 абсолютное время, отрицательное &#8212 относительный временной интервал (в 100нс отсчетах)
IN PKDPC pTimerDpcObject OPTIONAL Указатель на объект DPC процедур (прототип см. в таблице) либо NULL
Возвращаемое значение TRUE &#8212 если объект таймера находился в системной очереди таймерных объектов в момент вызова
Когда время ExpirationTime, заданное в KeSetTimerEx, истекает, объект таймера извлекается из системной очереди таймерных объектов и переходит в сигнальное состояние. Если существует процедура отложенного вызова CustomTimerDpc, то в этот момент соответствующий ей объект pTimerDpcObject помещается в системную очередь DPC объектов (разумеется, если там его еще нет) с тем, чтобы выполнить CustomTimerDpc в ближайшее время.


Ранее времени окончания ожидания ExpirationTime процедура CustomTimerDpc вызвана быть не может, зато по окончании ExpirationTime она вызывается через каждый интервал Period, если таковой задан.

Таблица 10.21. Прототип вызова KeSetTimerEx

BOOLEAN KeSetTimerEx IRQL &#60= DISPATCH_LEVEL
Параметры Инициализирует таймер с указанием типа
IN PKTIMER pTimerObject Указатель на объект таймера, инициализированный вызовами KeInitializeTimer или KeInitializeTimerEx
IN LARGE_INTEGER ExpirationTime Время ожидания, положительное значение &#8212 абсолютное время, отрицательное &#8212 относительный временной интервал (в 100нс отсчетах)
IN LONG Period OPTIONAL Значение периода (в миллисекундах) вызова функции CustomTimerDpc, если она указана.
IN PKDPC pTimerDpcObject OPTIONAL Указатель на объект DPC процедур (прототип см. в таблице) либо NULL
Возвращаемое значение TRUE &#8212 если объект таймера находился в системной очереди таймерных объектов в момент вызова
Процедура CustomTimerDpc может прекратить существование объекта таймера, если KeSetTimerEx указал нулевое значение параметра Period.

В случае, если вызовы KeSetTimer или KeSetTimerEx

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

Функция CustomTimerDpc (разумеется, ее имя определяется разработчиком драйвера, данное же используется лишь для определенности при изложении материала) является процедурой отложенного вызова, которая связывается с таймерами. Она запускается (однократно или многократно) не ранее окончания интервала ожидания, выставленного в таймере. Операционная система автоматически организует очередь из объектов, соответствующих DPC процедурам, ожидающим выполнения. Диспетчер DPC процедур извлекает данный объект из очереди, и лишь тогда CustomTimerDpc начинает свою работу. Практически всегда имеется некоторая задержка между моментом срабатывания таймера, когда интервал ожидания истек, и стартом CustomTimerDpc.



Как и все другие DPC процедуры, CustomTimerDpc работает на уровне IRQL равном DISPATCH_LEVEL. В таблице 10.22 описан прототип ее вызова. Следует обратить внимание на то, что эта процедура получает два зарезервированных параметра, значение которых на настоящий момент еще не определено.

Таблица 10.22. Прототип CustomTimerDpc

VOID CustomTimerDpc IRQL == любой
Параметры Описание
IN PKDPC pDpc DPC объект, инициализировавший вызов
IN PVOID pContext Контекст, указанный в вызове KeInitializeDpc

при инициализации данной функции как DPC процедуры
IN PVOID SystemArg1 Зарезервирован
IN PVOID SystemArg2 Зарезервирован
Возвращаемое значение void
Работа с процедурой CustomTimerDpc достаточно проста. Следует выполнить следующие действия:

Получить область в нестраничной памяти (возможно, запомнить полученный указатель в структуре расширения объекта устройства) для объекта KDPC, например, при помощи вызова ExAllocatePool.

Выполнить, например, во время работы AddDevice, вызов KeInitializeDpc

(см. описание прототипа в таблице 10.23) для того, чтобы связать с функцией CustomTimerDpc передаваемые ей при вызове контекстные указатели (pDpc и pContext). Адрес расширения структуры объекта устройства также является хорошим кандидатом для передачи в вызываемую функцию CustomTimerDpc.

Для того чтобы отменить срабатывание активного таймера используется вызов KeCancelTimer

(прототип описан в таблице 10.24). Этот же вызов может остановить работу "многократного" таймера и, соответственно, вызовы процедуры CustomTimerDpc. Следует обратить внимание, что освобождать память, занятую под объектом таймера или объектом DPC следует только после вызова KeCancelTimer &#8212 нарушение этого правила легко приводит к краху системы. Для определения, истекло ли время ожидания таймера, можно использовать вызов KeReadStateTimer

(см. таблицу 10.25).

Таблица 10.23. Прототип вызова KeInitializeDpc

VOID KeInitializeDpc IRQL == PASSIVE_LEVEL
Параметры Описание
IN PKDPC pDpc DPC объект, для которого инициатор данного вызова предоставил область памяти.
IN PKDIFERRED_ROUTINE DeferredProcedure Указатель на процедуру, которая будет вызываться в момент извлечения объекта DPC из очереди, в данном случае&#8212 CustomTimerDpc
IN PVOID pContext Контекстный указатель, передаваемый вызываемой DPC процедуре, в данном случае &#8212 CustomTimerDpc
Возвращаемое значение void
<


Таблица 10.24. Прототип вызова KeCancelTimer

BOOLEAN KeCancelTimer IRQL &#60= DISPATCH_LEVEL
Параметры Описание
IN PKTIMER Timer Указатель на объект таймера, который следует "отменить".
Возвращаемое значение • TRUE

&#8212 если таймер был "взведен" (несигнальное состояние) перед вызовом

• FALSE &#8212 в противном случае
Таблица 10.25. Прототип вызова KeReadStateTimer

BOOLEAN KeReadStateTimer IRQL &#60= DISPATCH_LEVEL
Параметры Описание
IN PKTIMER Timer Указатель на объект опрашиваемого таймера.
Возвращаемое значение • TRUE &#8212 если таймер "истек" и перешел в сигнальное состояние

• FALSE &#8212 если таймер еще "взведен" и находится в несигнальном состоянии
Программный код, осуществляющий инициализацию DPC и таймерных объектов, должен выполняться на уровне IRQL равном PASSIVE_LEVEL. При выполнении установки, отмены и чтении состояния таймера код должен выполняться на уровне IRQL меньшем или равном DISPATCH_LEVEL. В общем случае, следует избегать применения вызова KeInsertQueueDpc

к тем DPC объектам, которые используются в связке с процедурами типа рассмотренной выше CustomTimerDpc, работающей с таймером, поскольку это может привести к возникновению гонки состояний внутри драйвера.

Рассмотрим пример, реализующий добровольную задержку программного потока драйвера при использовании таймера. В данном примере задержка вставлена в драйверный код обработчика IOCTL запросов пользовательского приложения, который работает в контексте пользовательского потока (что характерно для обработчика IOCTL запросов), соответственно, уровень IRQL данного кода не превышает PASSIVE_LEVEL. По этой причине использование задержек в 1-10 секунд не вызывает никаких возражений со стороны операционной системы.

// Хотя и нехорошо делать глобальные переменные в драйвере: PKTIMER pTimer=NULL; // указатель на таймер PKDPC pDpcObject=NULL; // указатель на объект DPC #define IDLE_INTERVAL (10000)



VOID MyDeferredRoutine( IN PKDPC pthisDpcObject, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2 ) { PKTIMER ptrTimer = (PKTIMER)DeferredContext; DbgPrint("-Example- In MyDeferredRoutine."); if( KeReadStateTimer(ptrTimer) ) { DbgPrint("-Example- DPC: KeReadStateTimer returns TRUE."); } else { DbgPrint("-Example- DPC: KeReadStateTimer returns FALSE."); } } // Обработчик IOCTL вызовов: NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp ) { NTSTATUS status; . . . switch( ControlCode) { case IOCTL_TEST_TIMER: { NTSTATUS status; // Выводим сообщения только в отладочной версии DbgPrint("-Example- IOCTL_PRINT_DEBUG_MESS."); int shortCycles;

if( pTimer==NULL ) // если объект таймера не существует: { // выделяем память под объект таймера pTimer=(PKTIMER)ExAllocatePool(NonPagedPool,sizeof(KTIMER)); KeInitializeTimer(pTimer); // инициализируем объект таймер // выделяем память под DPC объект и инициализируем его pDpcObject=(PKDPC)ExAllocatePool(NonPagedPool,sizeof(KDPC)); KeInitializeDpc(pDpcObject, MyDeferredRoutine, pTimer); }

LARGE_INTEGER dueTime; dueTime.QuadPart = -10000 * IDLE_INTERVAL; // 10000*10000*1нс // "взводим" таймер: KeSetTimerEx( pTimer, dueTime, // время ожидания, относительный интервал (IDLE_INTERVAL/2), // период 5 с, то есть 5000*1 мс pDpcObject );

// во время ожидания сигнального состояния таймера // выполним 100 циклов по 50 мкс: for ( shortCycles=0; shortCycles &#60 100; shortCycles++) { DbgPrint("-Example-KeStallExecutionProcessor.shortCycles=%d.", shortCycles); KeStallExecutionProcessor(50); if( KeReadStateTimer(pTimer) ) { DbgPrint("-Example- KeReadStateTimer returns TRUE."); } else { DbgPrint("-Example- KeReadStateTimer returns FALSE."); } } // Останавливаем поток status = KeWaitForSingleObject( pTimer, Executive, // IN KWAIT_REASON WaitReason, KernelMode, // IN KPROCESSOR_MODE WaitMode, FALSE, // IN BOOLEAN Alertable, NULL); // IN PLARGE_INTEGER Timeout OPTIONAL



if( !NT_SUCCESS(status) ) { DbgPrint("-Example- Error in KeWaitForSingleObject."); DbgPrint("-Example- STATUS eq %x.",status); } else { DbgPrint("-Example- KeWaitForSingleObject OK."); }

// Считываем состояние таймера после окончания ожидания: if(KeReadStateTimer(pTimer)) { DbgPrint("-Example- KeReadStateTimer returns TRUE (after)."); } else { DbgPrint("-Example- KeReadStateTimer returns FALSE (after)."); } break; } case IOCTL_CANCEL_TIMER: // Удаляем объект таймера и объект DPC { BOOLEAN result = KeCancelTimer(pTimer); if(result) { DbgPrint("-Example- KeCancelTimer returns TRUE."); } else { DbgPrint("-Example- KeCancelTimer returns FALSE."); } ExFreePool(pTimer); ExFreePool(pDpcObject); pTimer=NULL; break; }

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

DWORD BytesReturned; unsigned long ioctlCode=IOCTL_PRINT_DEBUG_MESS; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input NULL, 0, // Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_PRINT_DEBUG_MESS!" ); } // Запуская данное приложение в отладчике в пошаговом режиме, // здесь сделаем паузу, давая отработать несколько раз DPC // процедуре, шаги с 216 по 241 в распечатке ниже. // Интервал вызовов DPC процедуры составляет 5 секунд. ioctlCode=IOCTL_CHANGE_IRQL; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input NULL, 0, // Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_CHANGE_IRQL!" ); }

Функция MyDeferredRoutine начинает запускаться только после перехода таймера в сигнальное состояние. Период ее запусков определен значением третьего параметра в вызове KeSetTimerEx.

Ниже приводится распечатка log-файла сообщений, выводимых макросами DbgPrint

в окно программы DebugView. Вторая колонка &#8212 отсчеты в секундах.

00000010 0.00241344 -Example- IOCTL_TEST_TIMER.
00000011 0.00243578 -Example- KeStallExecutionProcessor.shortCycles=0.


00000012 0.00250171 -Example- KeReadStateTimer returns FALSE.
00000013 0.00251568 -Example- KeStallExecutionProcessor.shortCycles=1.
00000014 0.00257910 -Example- KeReadStateTimer returns FALSE.
00000015 0.00259810 -Example- KeStallExecutionProcessor.shortCycles=2.
00000016 0.00266179 -Example- KeReadStateTimer returns FALSE.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 00000208 0.01016721 -Example- KeReadStateTimer returns FALSE.
00000209 0.01018118 -Example- KeStallExecutionProcessor.shortCycles=99.
00000210 0.01024572 -Example- KeReadStateTimer returns FALSE.

00000211 9.99725244 -Example- In MyDeferredRoutine.
00000212 9.99728234 -Example- DPC: KeReadStateTimer returns TRUE.
00000213 9.99730357 -Example- KeWaitForSingleObject OK.
00000214 9.99732033 -Example- KeReadStateTimer returns TRUE (after).
00000215 9.99734128 -Example- DeviceIoControl: 0 bytes written.

00000216 15.00447208 -Example- In MyDeferredRoutine.
00000217 15.00450169 -Example- DPC: KeReadStateTimer returns TRUE.
00000218 20.01165400 -Example- In MyDeferredRoutine.
00000219 20.01168780 -Example- DPC: KeReadStateTimer returns TRUE. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 00000240 80.09805464 -Example- In MyDeferredRoutine.
00000241 80.09808928 -Example- DPC: KeReadStateTimer returns TRUE.

00000242 80.18758250 -Example- IOCTL_CANCEL_TIMER. 00000243 80.53109152 -Example- KeCancelTimer returns TRUE.

Заметим, что выполнение освобождения памяти вызовами ExFreePool

без выполнения вызова KeCancelTimer здесь неминуемо привело бы к краху системы.

В DPC процедурах, подключенных к таймеру при помощи вызова KeSetTimerEx, допустимо выполнять переустановку таймера (вызов KeSetTimerEx), что приводит к повторному ожиданию. При этом до окончания ожидания DPC процедура более не вызывается.

VOID MyDeferredRoutine( IN PKDPC pthisDpcObject, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2 ) { . . . PKTIMER ptrTimer = (PKTIMER)DeferredContext; LARGE_INTEGER dueTime; dueTime.QuadPart = -10000 * IDLE_INTERVAL; // 10secs KeSetTimerEx( ptrTimer, dueTime, (IDLE_INTERVAL/2), // 5000 ms pthisDpcObject ); }

Как было сказано ранее, указатели на объекты таймера и DPC рекомендуется хранить в структуре расширения объекта устройства. Следовательно, если объект устройства удаляется (например, при выключении питания PnP устройства) и драйвер удаляется, то необходимо корректно очистить занимаемую такими объектами память. Проблемы отладки ситуаций, когда драйвер завершает работу с еще активным таймером, настолько сложны, что Driver Verifier (программное средство "тренировки" драйверов) делает специальную проверку относительно освобождения памяти, которая содержит работающие таймеры.


Тестирование аппаратуры


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

Базисные тесты. Прежде всего, следует удостовериться, что все устройства и все соединительные кабели являются совместимыми и их подключение выполнено правильно и надежно. Например, неэкранированный кабель USB будет удовлетворительно работать с USB 1.1 (Low и Full Speed), но, возможно, будет причиной сбоев при работе на скорости Hi Speed в USB 2.0. Что касается подключения внешних устройств к параллельному порту, то совершенно одинаковые внешне кабели LPT могут создавать совершенно разные условия подключения, а при попытке повысить скорость передачи в режимах EPP или ECP некачественные кабели могут быть причиной экзотических сбоев.

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

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

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



Тестирование и отладка


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

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

Тестирование можно разделить на два этапа. Первый &#8212 достаточно сумбурный этап первоначального эмпирического накопления данных о поведении сложившейся конфигурации аппаратуры и программного обеспечение. Разумеется, у опытных разработчиков этот этап короче и проходит рациональнее.

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

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

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

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

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

Разумеется, должна быть установлена хорошая дисциплина для обеспечения достаточного времени и для разработки, и для тестирования. Сокращение стадии тестирования для ускорения общего графика &#8212 плохая услуга любому проекту.


Тестовое приспособление CheckIt Loopback Device


Читатель вправе задать вопрос: для повторения эксперимента, с каким бы то ни было драйвером и аппаратными прерываниями на его персональном компьютере, обязательно понадобится устройство, которое эти прерывания генерирует. Где его взять?

Идея простых тестовых устройств, позволяющих тестировать параллельный порт (до сих пор все еще существующий в персональных компьютерах на пока еще существующей внутренней шине ISA) и получать в нем прерывания, возникла практически в момент появления параллельного порта. Одно из таких устройств называется "заглушка CheckIt" (CheckIt Loopback Device), которую по настоящее время производит фирма Smith Micro Software (см. Интернет сайт smithmicro.com). Данная конструкция неоднократно использовалась авторами книг по драйверам, например, Артом Бейкером и Джерри Лозано, и идеально подходит для практического ознакомления с процедурами обслуживания прерываний. Внутреннее устройство этого приспособления давно является всеобщим достоянием, и его несложно найти в Интернете. Самостоятельное "приготовление" доступно каждому и состоит в том, что следует взять стандартный 25-выводной male-разъем и замкнуть в нем 5 пар контактов. Схему для выполнения этой манипуляции можно взять, например, в книге Михаила Гука "Аппаратные средства IMM PC, Энциклопедия", 2-е издание, СПб, Питер, 2002, в разделе "Неисправности и тестирование параллельных портов". Однако чтобы не отвлекать читателя поисками столь простой схемы, приведем ее еще раз, см. рис. 11.1.

Если сопоставить схему с информацией, приведенной ранее в таблице 5.1, Регистры интерфейса стандартного параллельного порта (SPP), то становится вполне понятной идея этого приспособления. Стандартный параллельный порт имеет регистр данных DR (для ввода/вывода), регистр управления CR (для вывода) и регистр состояния SR (для ввода). Согласно схеме 11.1, выполняя вывод в регистр данных и управления, можно получать выведенные данные в регистре состояния. Поскольку один из разрядов (один из пяти используемых для вывода) поступает на вывод ACK# (бит 6 регистра состояния, SR.6), предназначенный для получения сигнала о прерывании, то получается, что передать можно 4 бита информации и сигнал о прерывании.
При выводе данных в регистры DR и CR, они поступят снова в параллельный порт в регистр SR. Включив определенное воображение, можно считать, что к параллельному порту компьютера подключено "сложное" внешнее устройство, которое способно генерировать сигналы прерываний.

Рис. 11.1

Разводка тестовой заглушки CheckIt, вид со стороны пайки
В соответствии со схемой рис. 11.1 и свойствами параллельного порта (режим SPP), получаем следующие переносы данных.

Бит CR.0 (Strobe#, контакт 1, единичное значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.4 (Select, контакт 13, высокий уровень напряжения на линии соответствует единичному значение бита, низкий &#8212 нулю в регистре), то есть CR.0=1 -&#62 SR.4=0. Бит при передаче инвертируется.

Бит CR.1 (Auto LF#, контакт 14, единичное значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.5 (Paper End, контакт 12, высокий уровень напряжения на линии соответствует единичному значение бита, низкий &#8212 нулю в регистре), то есть CR.1=1 -&#62 SR.5=0. Бит при передаче инвертируется.

Бит CR.2 (Init#, контакт 16, нулевое значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.6 (Ack#, контакт 10, высокий уровень напряжения на линии соответствует единичному значению бита, низкий &#8212 нулю в регистре), то есть CR.2=0 -&#62 SR.6=0. Бит передается нормально.

Бит CR.3 (Select In#, контакт 17, единичное значение бита соответствует низкому уровню на линии) поступает в регистр состояния как бит SR.7 (Busy, контакт 11, низкий уровень в линии соответствует единичному значению бита), то есть CR.3=1 -&#62 SR.7=1. Бит при передаче инвертируется дважды, то есть передается нормально.

Бит DR.0 (нулевой бит регистра данных, вывод 2) поступает в регистр состояния как бит SR.3 (Error#, контакт 15, высокий уровень на линии соответствует единичному значению бита), то есть DR.0=1 -&#62 SR.3 =1.


Бит передается нормально.

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

В упомянутом тесте, описанном Артом Бейкером и Джерри Лозано, распознавание поступившего прерывания в ISR процедуре производится по значению бита SR.2 (PIRQ, внутренний бит 2 регистра состояния SR). В некоторых реализация параллельного порта этот бит действительно хранит флаг прерывания. Он имеет нулевое значение, если прерывание имело место (напряжение на выводе Ack#, SR.6, контакт 10, выполнило отрицательный перепад &#8212 переход из высокого в низкое значения при условии CR.4=1). Единичное значение бита SR.2 устанавливается по аппаратному с6росу или при чтении регистра состояния. Однако на всех компьютерах, доступных автору, параллельный порт в конфигурации SPP вел себя, точно следуя первоисточникам (например, ]an Axelson, Parallel Port Complete), где указывается, что бит SR.2 не используется. Соответственно, тест Арта Бейкера и Джерри Лозано не работает без отключения строки кода, выполняющего эту проверку. Строго говоря, такому обстоятельству может быть и несколько иное объяснение. В Windows XP, где разрабатывались приведенные ниже примеры, тяжело отказаться от услуг всех системных компонентов, потенциально касающихся параллельного порта. Поэтому при тестах представляемых драйверов сам стандартный системный драйвер параллельного порта было решено не отключать, а именно он (его процедура обслуживания прерывания) и считывает регистр состояния SR перед тем как получит управление Isr процедура собственно испытываемых драйверов. Даже если сам порт и позволил бы использовать бит SR, то все равно Isr процедура собственно испытываемых драйверов могла бы считать только единичное значение бита SR.2 (что говорило бы об отсутствии прерывания).

Thread, Thread Object


Программный поток ("нитка"), объект потока.

Поток является минимальной единицей исполнения и планирования в многозадачной операционной системе. Для учета потоков Windows использует объекты потоков. В режиме ядра следует планировать работу с такими потоками, чтобы обеспечивать их естественное завершение (например, сигнализируя им при помощи объектов синхронизации). Особенностью многопроцессорных систем является возможность определения, на каком из конкретных процессоров будет исполняться код потока (параметра, известного как affinity). Состояние потока описывается его контекстом.



В былые времена разработчик драйвера


В былые времена разработчик драйвера мог изучить новую аппаратуру, интерфейс общения операционной системы и драйверов, окинуть все это критическим взором, сосредоточиться и создать драйвер. Хорошо это или плохо, но пора монолитных драйверов практически прошла. Сегодня разработчик драйверов должен изучить архитектуру аппаратных шин, архитектуру многослойной подсистемы ввода/вывода для того лишь, чтобы определить мотивы выбора конструкции драйвера. Определение, какого типа драйвер следует написать, уже само по себе важное решение. Выбор, следует ли заново реализовать или можно повторно использовать какой-нибудь уже имеющийся в подсистеме ввода/вывода слой, является еще одним важным решением.

Если взять самый крупный план, то можно разделить драйверы, используемые Windows NT 5, на две группы: драйверы пользовательского режима и драйверы режима ядра. Первые, как подразумевает их название, являются системным программным кодом, функционирующим в пользовательском режиме. В качестве примера можно назвать драйверы-симуляторы (виртуализаторы) для воображаемой аппаратуры или новых исполнительных подсистем (WOW, POSIX, OS/2). Так как Windows 2000 не допускает непосредственной работы с аппаратурой для кода пользовательского режима, то такие драйверы должны полагаться в этой части на драйверы, работающие в режиме ядра. Предположим, в примере главы 3 (драйвер Example.sys) была бы создана DLL, которая работала бы в пользовательском режиме и общалась бы с драйвером на манер приложения ExampleTest. В свою очередь, если бы пользовательские приложения обращались бы к ней (а не к помощи функций CreateFile, DeviceIoControl

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

Драйверы режима ядра (kernel-mode drivers) целиком состоят из кода системного уровня, выполняющегося в режиме ядра. Поскольку коду режима ядра разрешено работать непосредственно с аппаратурой (мы это видели в коде примера главы 3 при обработке IOCTL запроса IOCTL_TOUCH_PORT_378H), такие драйверы имеют прямой доступ к управлению устройствами, содержащимися в компьютере или подключенными к компьютеру.
Разумеется, ничто не может помешать такому драйверу представлять вымышленную аппаратуру &#8212 воля разработчика, где этим заниматься &#8212 в пользовательском режиме или режиме ядра.

Ограничившись категорией драйверов режима ядра, чему и посвящена данная книга, перемещаемся как раз в код режима ядра. На этом уровне можно выполнить деление драйверов еще на две категории: наследованные (legacy, доставшиеся как наследство от Windows NT 3.5, 4) и WDM драйверы. Пример драйвера Example.sys, рассмотренный в предыдущей главе, как раз и является примером legacy драйвера. Он использует функции заголовочного файла ntddk.h, скомпилирован без директивы DRIVERTYPE=WDM, не зарегистрировал ни одной процедуры типового WDM драйвера, не выполнил подключение объекта своего устройства к родительскому объекту и не реализует свои запросы путем обращения к стеку устройств. Драйверы типа legacy (если не говорить о задачах проникновения в режим ядра с задачами чистого программирования или исследования) предназначены для работы с теми устройствами, которые не поддерживают PnP спецификацию, поскольку только разработчик драйвера знает, как различить его присутствие в системе, не говоря уже о приемах работы с ним.

К счастью, практически все знания, касающиеся наследуемых драйверов NT, полностью применимы к модели WDM, по которой можно построить драйверы, работающие в Windows 2000/XP/Server 2003 (и Windows 98, Ме). Правда, как мы видели на примере Example.sys, и некоторые экземпляры драйверов типа legacy ("в-стиле-NT") могут работать во всех перечисленных ОС.

Способность драйверов WDM работать по PnP спецификации включает в себя: участие в управлении энергоснабжением системы, автоматическое конфигурирование устройства и возможность его "горячего" подключения. Корректно написанный WDM драйвер может быть использован и под Windows NT 5.x и под Windows 98/Ме, хотя Microsoft не гарантирует бинарной совместимости (простого переноса .sys файла в другую ОС, как это получилось с Example.sys).


B большинстве случаев все еще необходима перекомпиляция под Windows 98 DDK.

Наследуемые и WDM драйверы можно разделить также и на другие три категории: высокоуровневые, средне и низкоуровневые драйверы. Как подразумевает эта классификация, высокоуровневые драйверы зависят от драйверов среднего и низкого уровня в выполнении своих задач. Драйверы среднего уровня (intermediate, промежуточные), соответственно, в своей работе зависят от функционирования драйверов низкого уровня (low-level drivers).

К высокоуровневым драйверам, например, относятся драйверы файловых систем (file system drivers, FSDs). Такие драйверы предоставляют инициаторам запросов нефизическую абстракцию получателя, и уже эти запросы транслируется в специфические запросы к лежащим ниже драйверам. Необходимость в создании высокоуровневых драйверов возникает тогда, когда основные услуги аппаратуры уже реализованы драйверами нижнего уровня, и требуется только создать новую фигуру абстрагирования, которая необходима для предъявления инициатору запросов (клиенту драйвера).

Фирма Microsoft поставляет комплект программного обеспечения Installable File System (IFS) Kit, который распространяется отдельно от MSDN или других продуктов. Пакет IFS Kit требует наличия пакета DDK (и некоторых других средств) для того, чтобы заняться разработкой файловой системы всерьез. При этом существуют многочисленные ограничения на то, какие типы файловых систем могут быть получены при помощи этого пакета (IFS Kit). Дополнительную информацию по пакету IFS Kit можно получить на интернет-сайте Microsoft.

Драйверы среднего уровня могут быть проиллюстрированы такими примерами, как драйверы зеркальных дисков, классовые драйверы (class drivers), мини-драйверы (mini drivers) и фильтр-драйверы (filter drivers). Эти драйверы позиционируют себя между высокоуровневыми абстракциям высокоуровневых драйверов и средствами физической поддержки на нижних уровнях. Например, драйвер зеркальных дисков получает запрос от высокоуровневого драйвера файловой системы (FSD), транслирует этот запрос в два запроса к двум разным дисковым драйверам более низкого уровня.


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

Драйверы класса являются возможностью повторного использования кода в пределах драйверной модели. Так как много драйверов определенного типа могут иметь много общего, то программный код, описывающий общие места, может быть помещен в общий для данного типа классовый драйвер, отдельно от специфичного для обслуживания конкретных устройств кода. Например, драйверы HID (human interface device, устройства ввода по шине USB) используют такие сходства. Драйверы специфических HID устройств могли бы быть, в такой ситуации, реализованы как мини-драйверы, взаимодействующие с классовыми драйверами. Мини-драйверы отличаются тем, что они, как правило, общаются только с другими драйверами верхнего уровня (представляющими для них оболочку), не выходя на "прямой контакт" с Диспетчером ввода/вывода. Мини-драйвер и его клиент наверху имеют заранее обусловленный протокол общения, и обычно мини-драйвер экспортирует набор функций своего интерфейса по запросу драйвера-оболочки, после чего возможно общение между драйверами минуя Диспетчер ввода/вывода, что существенно ускоряет работу.

Фильтр-драйверы (filter drivers) являются драйверами среднего уровня, которые позиционирует себя во время загрузки над или под интересующим их драйвером и перехватывают запросы, идущие к нему или от него. Фильтр-драйверы, как правило, предназначены для модификации запроса к существующему драйверу или для реализации некоей дополнительной функции, изначально не заложенной в существующем драйвере (в простейшем случае это может быть подсчет пропущенных IRP пакетов). В операционной системе фильтр-драйверы с большой долей вероятности можно опознать по отсутствию имен у объектов устройств, созданных такими драйверами (при использовании программы DeviceTree).

Наконец, документация DDK упорно внедряет такую категорию (по отношению к WDM драйверам) как функциональные драйверы (functional drivers), причем эти драйверы могут быть либо классовыми, либо мини-драйверами.


Что имеет в виду документация DDK, когда вводит термин "функциональный"? Дело в том, что такие драйверы всегда работают как интерфейс между абстрактным запросом ввода/вывода и кодом низкоуровневого физического драйвера, "физического" &#8212 в том смысле, что он связан непосредственно с устройством и в его функционировании хорошо просматриваются особенности этого устройства. Характерна в данном случае следующая деталь. Когда типовой WDM драйвер нормального PnP устройства собирается подключить себя к стеку устройств (это должно происходить в процедуре AddDevice), он выполняет подключение функционального объекта устройства (FDO), созданного им самим, к физическому объекту устройства (PDO), предоставленному ему родительским драйвером. Как правило, родительским является драйвер шины, который первоначально обнаружил подключение данного устройства, инициировав затем обращение к рассматриваемому WDM драйверу устройства. Вот здесь и проявляется функциональность последнего: он получает общие функциональные запросы, а превращает их в низкоуровневые (например, в URB запросы для устройств USB), понятные шинному драйверу и устройству, олицетворяя при этом для своего клиента функции устройства, а не его конструкцию и внутреннюю логику.


Типы возвращаемых значений функций


Все функции, описываемые в языке С, либо возвращают значение определенного типа, либо не возвращают ничего (в последнем случае они описываются типом void). В переводе на новые термины, предлагаемые DKK, функции возвращают значения типа CHAR, UCHAR, SHORT, USHORT и т.п., либо описываются как VOID функции.

В дополнение к этим, знакомым и понятным типам функций в DDK добавлен тип NTSTATUS. Что означает этот новый тип?

Тип NTSTATUS несет код завершения функции и, на самом деле, является простым переопределением типа integer long, что можно увидеть, например, в файле ntdef.h, а именно:

typedef LONG NTSTATUS; typedef NTSTATUS *PNTSTATUS;

Неотрицательные значения переменных этого типа (попросту говоря, неотрицательные целые значения) относятся к кодам удачного завершения и предупреждениям, отрицательные &#8212 сигнализируют об ошибке.

Когда Диспетчер ввода/вывода вызывает одну из процедур драйвера, а она возвращает ему управление, передавая при этом код NTSTATUS, то сигнализирует ему об условиях окончания работы. Файл NTSTATUS.h содержит символьные имена для всех возможных значений кодов NTSTATUS.

Код, сообщающий об удачном завершении, носит имя STATUS_SUCCESS (кстати, равный 0). В случае, если работа завершена с ошибкой, код неудачного завершения, например, STATUS_UNEXPECTED_IO_ERROR (равный 0xC00000E9), транслируется системой в системный код ошибки и передается пользовательскому приложению, для которого работала данная процедура драйвера.

Для работы с кодами завершения NTSTATUS существуют несколько полезных макроопределений (описанных в файле ntdef.h), среди которых самым употребительным является макроопределение NT_SUCCESS(). При употреблении в логических выражениях, оно позволяет проверять, означает ли анализируемый код удачное завершение работы, например:

NTSTATUS Status = IoConnectInterrupt(); if( !NT_SUCCESS(Status) ) { &#60обработка ошибки&#62 }



Toc


УДК

ББК

004.43

32.973.26-018.1

  С60
 
Солдатов В.П.

Программирование драйверов Windows. Изд. 2-е, перераб. и доп. &#8212 М.: ООО "Бином-Пресс", 2004 г. &#8212 480 с: ил.

Книга представляет собой систематизированное введение в программирование драйверов Windows 2000/XP/Server 2003 и Windows 98/Ме с использованием Microsoft Windows DDK. Начиная с рассмотрения базовых понятий и терминов программирования драйверов, автор затем подробно рассматривает набор программных средств, необходимых для разработки драйверов режима ядра в операционной системе Windows, а после реализации законченного драйвера, что дает накопление стартового опыта, переходит к детальному рассмотрению структуры драйверов модели WDM и "драйверов-в-стиле-NT". Подробно рассмотрены особенности работы с памятью в режиме ядра, вопросы взаимодействия с подсистемой ввода/вывода Windows, создания и синхронизации программных потоков, особенности работы с совместно используемыми данными, процедуры для работы с файлами, текстом, временем и Системным Реестром в режиме ядра. Рассмотрены разные способы инсталляции драйверов &#8212 от взаимодействия с системным мастером Установки новой аппаратуры до динамической загрузки с использованием сервисов SCM Менеджера. Отдельная глава посвящена составлению и отладке inf-файлов &#8212 вопросу, крайне редко затрагиваемому в русскоязычной литературе по программированию. Затрагиваются также и общие вопросы работы с аппаратурой, включая обзор шин современного компьютера &#8212 PCI, USB, FireWire, PC Cards. Завершается книга рассмотрением методов тестирования и отладки драйверов. В приложениях приводится справочная информация, полезная разработчику драйверных систем как, впрочем, и всем профессиональным программистам в среде Windows.

Материал книги дает ясное представление об основных типах данных и структурах драйверов и механизмах их взаимодействия с операционной системой. Минимальным требованием к читателю является хорошее знание языка программирования С (без расширений C++).


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

ISBN 5-9518-0099-4 © Солдатов В.П., 2004

© Издательство Бином, 2004
Научно-техническое издание

Вячеслав Петрович Солдатов

Программирование драйверов Windows

Оформление обложки И.Ю. Буровой

Подписано в печать 19.07.2004. Формат 70х100/16. Усл. печ. л. 39

Гарнитура Петербург. Бумага газетная. Печать офсетная

Тираж 4 000 экз. Заказ #3489

Издательство "Бином-Пресс", 2004 г.

170026, Тверь, Комсомольский просп., 12

Отпечатано с готовых диапозитивов во ФГУП ИПК

"Ульяновский Дом печати". 432980, г. Ульяновск, ул. Гончарова, 14


Торможение программных потоков


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

Самая простая ошибка состоит в том, что драйвер по какой-то причине не выполняет вызов IoCompleteRequest. В результате, присланный IRP пакет никогда не возвращается Диспетчеру ввода/вывода. Иногда не столь очевидна необходимость сделать вызов IoStartNextPacket (при использовании очередей IRP пакетов). Однако даже если не существует запросов, ожидающих обработки, драйвер должен выполнить этот вызов, поскольку только таким образом объект устройства будет "помечен" как простаивающий. Без этого вызова новые IRP пакеты будут помещаться в очередь ожидания обработки, так и не поступая в процедуру StartIo драйвера.

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

Аналогично, DMA драйверы могут остановиться в точке, где они сделали запрос на владение объектом адаптера. Драйверы, которые управляют сложными (составными) контроллерами, могут создать сходные проблемы в случае, если они не освобождают объект контроллера.

К сожалению, не существует безусловно хорошего способа решения таких проблем. Иногда полезно определить некую структуру данных, в которой все владельцы объектов синхронизации будут "отмечаться", когда они что-то используют, и удалять эти отметки, когда использование соответствующего объекта синхронизации прекращается. Разумеется, такой прием потребует дополнительных усилий, но он весьма эффективен в выявлении источника проблем.

Порой ошибки драйвера могут вызвать блокировку всей системы. Например, фатальную взаимоблокировку могут вызвать некорректное перекрестное использование нескольких объектов спин-блокировок или попытки повторно получить спин-блокировку на однопроцессорной платформе. Бесконечные циклы из кода процедур обслуживания прерывания или процедур DpcForIsr могут привести к аналогичному результату. Лучшим решением в данной ситуации было бы осуществление интерактивной отладки драйвера.



Удаление IRP пакетов


Как бывает и в реальной жизни, кто-то, инициировавший IRP запрос, может передумать и инициализировать снятие запроса "с повестки". Пользовательское приложение может запросить уничтожение пакета после длительного ожидания. Приложение может вовсе прекратить работу, бросив все на попечение операционной системы. Наконец, приложение может попытаться завершить свою асинхронную операцию Win32 API вызовом CancelIo.

Таблица 9.19. Прототип функции IoCancelIrp

BOOLEAN IoCancelIrp IRQL &#60= DISPATCH_LEVEL
Параметры Помечает пакет IRP как требующий удаления и вызывает процедуры удаления, если таковые определены
IN PIRP pIrp Указатель на удаляемый IRP пакет
Возвращаемое значение TRUE &#8212 если пакет удален

FALSE &#8212 в случае неудачи

В режиме ядра для удаления запроса выполняется вызов IoCancelIrp

(таблица 9.19). Операционная система также вызывает IoCancelIrp

для всех IRP пакетов, относящихся к потоку, выполнение которого прекращается.

Предположим, некий код режима ядра направил пакет (в данном случае &#8212 синхронный) другому драйверу. Как он может выполнить удаление отправленного пакета, например, в результате превышения времени ожидания? Пример ниже иллюстрирует этот случай.

// формирует синхронный пакет: PIRP pIrp= IoBuildSynchronousFsdRequest(. . ., &event, &iosb); // Подключаем процедуру завершения: IoSetCompletionRoutine( pIrp, MyCompletionRoutine, (VOID*)&event, TRUE, TRUE, TRUE ); NTSTATUS status = IoCallDriver(. . .); if( status == STATUS_PENDING ) { // Некоторое время ожидаем естественного завершения LARGE_INTEGER waitDelay; waitDelay.QuadPart = - 10000; // относительное время if( KeWaitForSingleObject( &event, KernelMode, FALSE, &waitDelay) == STATUS_TIMEOUT ) { IoCancelIrp(pIrp); KeWaitForSingleObject( &event, KernelMode, FALSE, NULL); } } // Синхронные IRP пакеты - их удаляет Диспетчер ввода/вывода: IoCompleteRequest(pIrp, IO_NO_INCREMENT); . . .

// Процедура завершения NTSTATUS MyCompletionRoutine( PDEVICE_OBJECT pThisDevice, PIRP pIrp, PVOID pContext ) { if (pIrp-&#62PendingReturned) KeSetEvent((PKEVENT) pContext, IO_NO_INCREMENT, FALSE); return STATUS_MORE_PROCESSING_REQUIRED; }


Процедура IoCancelIrp устанавливает флаг (cancel bit) в IRP пакете и выполняет вызов процедуры CancelRoutine, если таковая имеется в IRP пакете.

Таблица 9.20. Прототип предоставляемой драйвером функции CancelRoutine

VOID CancelRoutine IRQL == DISPATCH_LEVEL
Параметры Выполняет действия, сопутствующие удалению пакета IRP
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, которое (точнее &#8212 драйвер) и зарегистрировало ранее эту функцию в IRP пакете вызовом IoSetCancelRoutine

(см. ниже)
IN PIRP pIrp Указатель на удаляемый IRP пакет
Возвращаемое значение void
Таблица 9.21. Прототип функции IoSetCancelRoutine

PDRIVER_CANCEL IoSetCancelRoutine IRQL &#60= DISPATCH_LEVEL
Параметры Устанавливает (переустанавливает) определяемую драйвером функцию CancelRoutine для данного IRP пакета
IN PIRP pIrp Указатель на IRP пакет, которому будет соответствовать устанавливаемая функция CancelRoutine
IN PDRIVER_CANCEL CancelRoutine Указатель на функцию, которая соответствует прототипу, описанному в таблице 9.20, или NULL (если следует отменить функцию CancelRoutine для данного пакета IRP)
Возвращаемое значение Указатель на ранее установленную для данного IRP пакета функцию CancelRoutine. Соответственно, если таковой не было, то возвращается NULL. Значение NULL возвращается также, если пакет находится в обработке и не может быть удален
В отличие от процедур завершения, которые могут быть указаны для каждого драйвера (в соответствующих ячейках стека IRP), место для процедуры CancelRoutine в IRP пакете единственное, что указывает на ее необычный статус &#8212 она вызывается один раз для удаляемого пакета. Соответственно, эта процедура и должна принадлежать тому драйверу, который наиболее компетентно может распорядиться судьбой IRP пакета. Правда, этот драйвер может регистрировать разные свои функции для удаления разнотипных IRP пакетов (чтения, записи и т.п.).

Драйвер должен участвовать в схеме удаления IRP пакетов, если он реализует собственную очередь пакетов или участвует в использовании системной очереди (то есть зарегистрировал процедуру StartIo).


При этом подразумевается, что процедуре удаления подвергаются прежде всего пакеты в состоянии ожидания, то есть не выполненные сразу, а помещенные по этой причине в очередь. Пакеты IRP, которые переданы на обработку нижним драйверам и "застряли" там &#8212 это худшее, что можно придумать в момент удаления.

Код вызова IoCancelIrp устроен примерно следующим образом &#8212 по крайней мере, так уверяет Уолтер Оней:

BOOLEAN IoCancelIrp(PIRP pIrp) { IoAcquireCancelSpinLock(&pIrp-&#62CancelIrql); pIrp-&#62Cancel=TRUE; PDRIVER_CANCEL CancelRoutine = IoSetCancelRoutine(pIrp, NULL); if( CancelRoutine != NULL) { PIO_STACK_LOCATION currentCell = IoGetCurrentIrpStackLocation(pIrp); (*CancelRoutine)(currentCell-&#62DeviceObject, pIrp); return TRUE; } else { IoReleaseCancelSpinLock(pIrp-&#62CancelIrql); return FALSE; } }

Для ограничения доступа к удаляемому пакету, код IoCancelIrp, прежде всего, запрашивает объект спин-блокировки вызовом IoAcquireCancelSpinLock

(в переменной pIrp-&#62CancelIrql сохраняется значение текущего уровня IRQL для использования при последующем вызове IoReleaseCancelSpinLock). В случае, если за IRP пакетом закреплена процедура CancelRoutine, то она вызывается (теперь на нее возложена задача освобождения спин-блокировки). Если же такой процедуры нет, то вызов IoCancelIrp завершает работу, освобождая спин-блокировку.

Процедура CancelRoutine, получив управление, может оказаться в одной из описанных ниже ситуаций.

Во-первых, рассматриваемый IRP пакет в настоящий момент обрабатывается. Если такой пакет все еще находится в одной из рабочих процедур драйвера, то он имеет право на несколько секунд жизни, после чего драйвер должен принять решение о его дальнейшей судьбе, скорее всего, завершить с ошибкой, например:

Irp-&#62IoStatus.Status = STATUS_IO_TIMEOUT; Irp-&#62IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT);

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


Лучшим приемом будет &# 8212 дождаться естественного развития событий, когда пакет вернется, вероятнее всего, с каким-нибудь кодом ошибки.

Выяснить, обрабатывается ли рассматриваемый пакет именно сейчас, можно при помощи следующего кода, поскольку, если задействован механизм System Queuing и какой-либо пропущенный через него IRP пакет в настоящий момент обрабатывается, то именно адрес этого IRP пакета "лежит" в поле pDeviceObject-&#62CurrentIrp (иначе там будет NULL):

VOID MyCancelRoutine ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp) { if( pIrp == pDeviceObject-&#62CurrentIrp ) { . . .

Конкретная реализация действий по удалению текущего пакета (если она возможна) остается задачей разработчика драйвера.

Существенно проще становится ситуация, когда пакет, предназначенный для уничтожения, только что поступил в процедуру StartIo либо еще находится в очереди отложенных (pending) пакетов.

VOID StartIo ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIRp) { KIRQL CancelIrql; IoAcquireCancelSpinLock(&CancelIrql); If(pIrp-&#62Cancel) { IoReleaseCancelSpinLock(CancelIrql); return; } // Удаляем процедуру обработки удаления, делая пакет // "not cancelable" - неуничтожаемым IoSetCancelRoutine(pIrp, NULL); IoReleaseCancelSpinLock(CancelIrql); . . . }

В случае, если удаляемый IRP пакет пока находится в системной очереди (которая называется еще "управляемая StartIo"), a перед его размещением

там (то есть вместе с вызовом IoMarkIrpPending) была зарегистрирована процедура MyCancelRoutine для этого IRP пакета, то действия по удалению такого пакета (не текущего &#8212 не находящегося в обработке) могут выглядеть следующим образом:

VOID MyCancelRoutine( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp) { if( pIrp == pDeviceObject-&#62CurrentIrp ) { // Текущий IRP IoReleaseCancelSpinLock(pIrp-&#62CancelIrql); // Вряд ли можно сделать что-то еще... } else { // Удаляем из системной очереди: KeRemoveEntryDeviceQueue( &pDeviceObject-&#62DeviceQueue, &pIrp-&#62Tail.Overlay.DeviceQueueEntry); // Только теперь можно освободить спин-блокировку: IoReleaseCancelSpinLock(pIrp-&#62CancelIrql);



pIrp-&#62IoStatus.Status=STATUS_CANCELLED; pIrp-&#62IoStatus.Information = 0; IoCompleteRequest( pIrp, IO_NO_INCREMENT ); } return; }

Приведенный ниже пример выполняет удаление пакета из очереди, поддерживаемой собственно драйвером (так называемой "Device-Managed Queue").

VOID MyOtherCancelRoutine ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp ) { KIRQL oldIRQL; PMYDEVICE_EXTENSION pDevExt = (PMYDEVICE_EXTENSION)pDeviceObject-&#62DeviceExtension; IoSetCancelRoutine(pIrp, NULL); // Освобождаем спин-блокировку, установленную еще IoCancelIrp IoReleaseCancelSpinLock(pIrp-&#62CancelIrp);

// Удаляем IRP из очереди под защитой спин-блокировки KeAcquireSpinLock(&pDevExt-&#62QueueLock, &oldIRQL); RemoveEntryList (&pIrp-&#62Tail.Overlay.ListEntry); KeReleaseSpinLock(&pDevExt-&#62QueueLock, oldIRQL); // pIrp-&#62IoStatus.Status = STATUS_CANCELLED; pIrp-&#62IoStatus.Information = 0; IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return; }

Предполагается, что объект спин-блокировки, используемый для синхронизации доступа к очереди пакетов, pDevExt-&#62QueueLock был заранее создан и сохранен в структуре расширения данного устройства.

Вопросы создания и поддержки очередей IRP пакетов, ведомых собственно драйвером, в данной книге не рассматриваются. Хотя этот прием достаточно широко распространен и присутствует в примерах пакета DDK.
В заключение, следует отметить, что после вызова IoCompleteRequest

в конце процедуры CancelRoutine (обработки удаления IRP пакета в данном драйвере) запускаются вызовы процедур завершения вышестоящих драйверов &#8212 если такие драйвера имеются и если соответствующие процедуры были зарегистрированы для данного IRP пакета на случай его удаления (см. описание вызова IoSetCompletionRoutine

в таблице 9.8).


Unicode


Двухбайтная кодировка символов алфавитов. Делает возможной поддержку всех языков, имеющих буквенный или слоговый алфавит. Давно и широко применяется в Windows NT. B режиме ядра существует достаточный набор функций для работы со строками Unicode.



Union


Объединение, тип данных языка С. Состоит из простых типов данных (char, jnt и т.п.) и вложенных структур или объединений. Реализует доступ к одной и той же области памяти, как к данным разных типов. Например, тип данных LARGE_INTEGER может быть определен (в файле ntdef.h это выполняется при помощи условной компиляции) следующим образом:

typedef union _LARGE_INTEGER { struct { ULONG LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER;

Тип данных LARGE_INTEGER используется, например, в вызове KeSetTimer

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

LARGE_INTEGER interval = RtlConvertLongToLargeInteger(100*10);

Здесь и далее обращения к системным функциям (типа RtlXxx, KeXxx, loXxx, read, CreateFile

и т.п.) будут называться вызовами, а в тексте они будут обозначаться жирным шрифтом.



Управление размещением кода драйвера в памяти


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



в работе системы, разработчики NT


Для достижения устойчивости в работе системы, разработчики NT выбрали для построения ядра так называемую 'архитектуру клиент-сервер'. В данном случае, пользовательское приложение и является клиентом служб операционной системы.

Пользовательское приложение функционирует в специальном режиме (относительно аппаратного обеспечения), называемом 'user mode' — пользовательский режим. В пределах этого режима, код приложения ограничен выполнением "безвредных" инструкций. Например, через реализацию "таинственного" маппинга (mapping, отображение) виртуальной памяти (страничное представление виртуальной памяти) пользовательский код лишается возможности доступа к виртуальной памяти, предоставленной другим приложениям (за исключением случаев обоюдного согласия, что реализуется специально предназначенными на тот случай методами). Инструкции аппаратного ввода/вывода также не могут быть выполнены кодом пользовательского режима. Целый класс инструкций центрального процессора (называемых привилегированными) запрещен в Windows NT для выполнения кодом пользовательского режима, как, например, команды процессора IN, OUT (результат таких попыток запечатлен на рисунке 1.1). Если вдруг приложению потребуется выполнить что-нибудь из числа таких запрещенных для нее действий, оно должно запросить соответствующую службу операционной системы.

Код самой операционной системы выполняется в так называемом 'kernel mode' — режиме ядра (режиме уровня ядра). Код режима ядра вправе выполнить любую процессорную инструкцию, не исключая инструкций ввода/вывода. Память, принадлежащая любому приложению, может быть доступна коду режима ядра, конечно, если страничная память приложения в данный момент не сброшена на жесткий диск.

Современные процессоры реализуют несколько форм привилегированного режима в отличие от непривилегированного. Код режима ядра выполняется в привилегированном контексте, в то время как пользовательский код выполняется в непривилегированной среде. Так как разные процессоры (и платформы на их основе) реализуют привилегированные режимы по-разному, то, для обеспечения переносимости, разработчики операционной системы ввели особые абстрактные элементы программирования, которые позволяют разграничивать пользовательский режим и режим ядра. Код операционной системы использует их для переключения привилегированного/непривилегированного контекста, следовательно, при перенесении операционной системы только лишь код этих дополнительных элементов необходимо "портировать" (переписывать конкретно под специфику новой аппаратной платформы). На платформе Intel пользовательский режим реализуется из набора инструкций Ring 3 (Кольца 3), в то время как режим ядра реализован с использованием Ring 0 (Кольца 0).

Драйверы уровня ядра (режима ядра) работают в привилегированном контексте. Соответственно, плохо написанный драйверный код может оказаться вредоносным для операционной системы. Разработчик должен с особым вниманием относиться к создаваемому коду, чтобы не обрушить все здание операционной системы. Фирма Microsoft пытается решить проблему надежности драйверов, поставляемых в составе дистрибутива Windows, через механизм тестирования и подписания драйверов.


USB: Universal Serial Bus


Спецификация USB была разработана консорциумом компаний, включая Intel и Microsoft. Целью нового стандарта было обеспечение организации недорогой среднескоростной шины в таких областях применения, как передача цифровых изображений, компьютерная телефония и мультимедийные игры. Текущими версиями спецификации USB является версия 1.1 и версия 2.0 (разумеется, во вторую заложены более высокие скоростные характеристики), и список компаний-членов, участвующих в консорциуме, постоянно растет.

Предельная скорость передачи данных по шине USB спецификации 1.1 составляет 12 Мбит/сек (Full Speed). Медленные используют низкую скорость передачи 1,5 Мбит/сек (Low Speed). Стандарт USB версии 2.0 поддерживает физическую скорость передачи 480 Мбит/сек (High Speed). Реально же максимальная скорость соединения компьютер - USB устройство (в разрабатываемом автором приборе) достигала 32 Мбайт/с. Данные передаются последовательно по паре проводников. Питание для некоторых устройств доступно по отдельным проводникам питания и заземления (для устройств с небольшим энергопотреблением).

Устройства USB могут быть подключены 5-метровым кабелем (а практически &#8212 и более длинным). Использовании USB хаба (hub &#8212 сетевой концентратор), позволяет увеличить дальность размещения устройств от хост-компьютера и увеличить количество устройств, подключаемых к одной шине USB. Последовательно можно включить до пяти устройств-хабов, обеспечив длину соединения 30 метров. Пример конфигурации "сети", построенной с использованием USB-хабов приводится на рисунке 5.4.

Рис. 5.4

Пример конфигурации устройств шины USB

Следует заметить, что устройства High Speed (для USB 2.0) должны поддерживать функционирование и на скорости Full Speed. Более того, процесс "перечисления" проводится как раз на этой скорости.

При конфигурировании устройств при использовании внешних USB хабов следует помнить, что хаб, предназначенный для работы на некоторой скорости (например, Full Speed), ограничит скорость работы устройств, стоящих в цепочке устройств далее от хост-компьютера (например, High Speed устройства), и последние не смогут работать на скорости, превышающую скорость этого медленного хаб-концентратора. При отсутствии возможности работать на скорости Hi Speed устройства Hi Speed работают на скорости Full Speed.

Работа программиста, создающего драйвер внешнего (не находящегося на материнской плате) USB устройства сводится к тому, чтобы воспользоваться программным интерфейсом системных драйверов шины USB, общение с которым происходит при помощи пакетов, называемых URB (USB Request Block) пакетами. Работа с регистрами USB контроллеров на материнской плате теперь стала уделом узкого круга специалистов &#8212 разработчиков материнских плат и операционных систем. Всем остальным разработчикам USB устройств с управлением программ под Windows предлагается достаточно развитый программный интерфейс к системным WDM драйверам, которые берут на себя все аппаратно-ориентированные операции.

К хост-компьютеру можно подключить до 127 устройств, шинный адрес которых устанавливается динамически при подключении устройств.



User mode


Пользовательский режим. Непривилегированный режим, в котором выполняются обычные приложения, не имеющие возможности получать доступ к системным данным иначе, как обращаясь к вызовам функций подсистем (например, Win32, GDI, Posix), которые, в свою очередь, прибегают к помощи системных вызовов.

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



User Space


Пользовательское пространство памяти. Область виртуальной памяти, выделенная для работы пользовательских приложений. Обычно составляет 2 гигабайта и ограничена сверху адресом 0x79999999 (в серверной конфигурации возможны варианты, когда, в результате регулировки настроек операционной системы, приложения пользовательского режима получают возможность использовать 3 гигабайта виртуальной памяти, оставляя под системные нужды лишь 1 гигабайт). Все сказанное относится к 32-разрядным версиям Windows. B 64-разрядных версиях Windows XP и Windows Server 2003 ситуация более сложная, хотя для 32-разрядных приложений и там ничего не меняется. Подробнее вопросы адресации в 64-разрядных версиях Windows будут рассмотрены в главе 4.



Установка фиксированных точек прерывания


При использовании интерактивных отладчиков не найдется много причин устанавливать фиксированные точки прерывания внутри кода драйвера (hard breakpoints). Тем не менее, это можно сделать, если воспользоваться двумя функциями:

VOID DbgBreakPoint(); VOID KdBreakPoint();

Вызов KdBreakPoint представляет из себя макроопределение, которое определяет условную компиляцию с целью выполнить вызов DbgBreakPoint. Это макроопределение не выполняет данного вызова, если выполнена релизная (без отладочных инструкций) сборка драйвера (free build).

Будьте внимательны: Windows дает фатальный сбой с сообщением KMODE_EXCEPTION_NOT_HANDLED в том случае, если драйвер применил фиксированную точку прерывания (через упомянутые вызовы), но в этот момент клиент отладки (см. рисунок 13.1) был недоступен. Если драйвер добрался до такой точки прерывания, и не оказалось отладчика, подключенного к последовательному порту, то драйвер "виснет". В некоторых случаях, ситуация может быть выправлена запуском отладчика на хост-компьютере.



Установка параметров загрузки в файле boot.ini


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

Его можно редактировать при помощи редактора notepad (если убрать атрибут "только для чтения", например, в программе WinCommander-TotalCommander), но более правильно это делать в окне системного апплета, показанного на рисунке 13.3.

В документации DDK несложно отыскать описание задаваемых параметров загрузки &#8212 по ключу "boot.ini" - "Parameters for boot.ini". Помимо параметра /sos, подробно рассмотренного в приложении Б, разработчику драйверов может быть также полезен параметр /maxmem=Xxx, который ограничивает размер используемой физической памяти, что позволит протестировать систему и драйвер в условиях недостатка физической памяти.



Установка PnP устройств


В момент, когда подключается PnP устройство, в результате взаимодействия нескольких подсистем инициируется загрузка нового драйвера.

При подключении нового устройства аппаратное обеспечение, представляющее шину, используя механизмы автоматического обнаружения и уведомления, оповещает шинный драйвер о наличии устройства. В зависимости от аппаратного обеспечения шины, это может привести к тому, что шинный драйвер, которому поступило уведомление об изменениях, инициирует новую процедуру перечисления (enumeration) всех подключенных к шине устройств. В любом случае, в конце этой операции шинный драйвер будет точно знать, что новое устройство подключено, и какой специфический идентификатор оно имеет. Дальнейшую работу можно разделить на этапы:

PnP Менеджер режима ядра (см. документацию DDK по указателю на ключевую фразу "Device Installation Components") уведомляет PnP Менеджер пользовательского режима о том, что в системе обнаружено новое устройство со специфическими кодами PnP идентификации (кодами производителя, модели, версии и т.п.). PnP Менеджер пользовательского режима конструирует список возможных драйверов для нового устройства, в частности, проверяется системный файловый каталог inf-файлов на наличие подходящего inf-файла (по полученной от нового устройства информации). Инсталляционные inf-файлы для дополнительно доставляемых драйверов чаще всего попадают туда под новым именем oemXxx.inf, где Xxx &#8212 это целое число, начиная с 0.

Если подходящий inf-файл не обнаружен, система откладывает все последующие действия до момента, пока в систему войдет пользователь с достаточным уровнем привилегий. Этому пользователю и предлагается диалог с Мастером Установки Оборудования (Add Hardware Wizard). Пользователь должен указать место (чаще всего, CD), где размещены файлы нового драйвера и его inf-файл.

Как только выявлен приемлемый inf-файл, начинается его обработка при помощи библиотеки вызовов Configuration Manager API (CfgMgr API, см. документацию DDK по указателю на ключевую фразу "Device Installation Components"). Выполняется копирование файлов драйвера и модификация информации Системного Реестра. Эта работу делает, главным образом, PnP Менеджер режима ядра.

На основе директив inf-файла PnP Менеджер режима ядра загружает все фильтр-драйверы нижнего уровня, затем функциональный драйвер и, наконец, верхние фильтр-драйверы, предназначенные для обслуживания нового устройства. Драйверу, который находится на вершине стека, затем направляются соответствующие PnP запросы (IRP пакеты с кодом IRP_MJ_PNP), включая IRP_MN_START_DEVICE.



Утечка ресурсов


Операционная система никогда не контролирует, как драйвер использует ресурсы и как он их возвращает по окончании работы. Когда драйвер завершает работу и выгружается, то именно на нем лежит вся ответственность за освобождение всех когда-либо занятых им ресурсов. Утечка памяти может происходить и в то время, когда драйвер еще продолжает работать. Например, если он периодически производит временное выделение памяти под свои нужды и "забывает" их освободить. Драйверы, которые занимаются генерацией дополнительных пакетов IRP, могут "забывать" выполнить очистку этих областей памяти. Утечки ресурсов приводят к падению производительности системы и, в конечном счете, к ее фатальному сбою.

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



Вариант 2. Модификация драйвера для работы с прерываниями


И хотя второй вариант драйвера не улучшает соотношения "перенос/прерывание", в нем рассматриваются несколько приемов, которые часто встречаются в драйверах Windows, исходные тексты которых доступны для анализа.

Прежде всего, это &#8212 использование системных очередей IRP пакетов для обеспечения сериализации при доступе к обслуживаемому устройству. Во-вторых, в приведенном ниже примере демонстрируется способ синхронизации работы драйвера и вызывающего его приложения по совместно используемому событию. В-третьих, используется более традиционная схема использования DPC процедур для завершения работы по поводу прерывания, которая связывает только одну DPC процедуру с объектом устройства и использует вызовы IoInitializeDpcRequest и IoRequestDpc. Наконец, практически показано, как можно использовать для хранения временной информации незадействованные поля IRP пакетов, здесь &#8212 Parameters.DeviceIoControl.Type3InputBuffer в составе текущей (на момент обработки) ячейки стека IRP пакета.

Помимо упомянутых отличий, в драйвере используется механизм IOCTL запросов от приложения для операций чтения и записи в устройство, тогда как обработчиков запросов от Win32 вызовов WriteFile и ReadFile

в данной реализации драйвера нет вовсе.

Условия компиляции и сборки драйвера и тестирующего приложения не изменились.



Векторы прерывания


Некоторые устройства и некоторые процессорные архитектуры допускают механизм автоматической диспетчеризации &#8212 переходов "по вектору", то есть адресу программных процедур, при поступлении сигналов о прерываниях. При другом методе векторизация не применяется, и для всех типов прерываний обслуживание предоставляется обобщенной процедурой. Такая процедура просматривает в порядке приоритетности весь список устройств, которые могли бы вызвать прерывание, и определяет, какое устройство требует обслуживания на самом деле. Очевидно, что в современных компьютерных системах, где генерируется от нескольких десятков до нескольких тысяч прерываний в секунду, существенно более эффективным оказывается векторный подход.



Virtual Memory


Виртуальная память.

Рассмотрим пример. Предположим, некая фирма имеет оплачиваемое пространство на складе своего оптового поставщика. Учет ее имущества производится хозяином склада в следующей форме. Каждая коробка имеет двузначный номер ПК (Полка/Коробка), где К — это действительно номер коробки 0..9 на соответствующей полке, а вот с номером полки дело обстоит сложнее. Для того чтобы узнать сквозной (физический) номер полки на складе, необходимо заглянуть в журнал выделения полок фирмам и получить из него настоящий номер полки. Правда, склад в нашем примере небольшой — всего десять полок!

Неудивительной будет ситуация, если через несколько дней работы по предложенной системе коробка, относящаяся к рассматриваемой фирме, с виртуальным номером 19 будет находиться на полке 2, притом, что коробка номер 20 — уже на полке 8. Но нумерация коробок осталась непрерывной, что очень нравится владельцам коробок!

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

Аналогичная методика принята в современных операционных системах. Преимущества ее использования огромны. Приложениям не нужно ожидать получения непрерывных пространств памяти — достаточно наличия фрагментов стандартной длины. Более того, если какое-либо приложения имеет низкую активность, можно его виртуальную память "сбросить" в файл на жестком диске (этот процесс называется swapping), a физическую память предоставить активным приложениям. (То есть — хранить коробки в подвале, но к моменту приезда клиентов — элегантно размещать их на полках склада. При определенной сноровке можно внушить каждому клиенту с большими запасами, что склад используется только для хранения его товара.)

Помимо этого, можно относительно легко контролировать несанкционированный доступ приложений к памяти по адресам, которые им не были предоставлены.

Столь объемное лирическое отступление было выполнено ради того, чтобы описать ситуацию, в которой проявляются весьма существенные проблемы.



Возможности DMA


Спецификация PCI не включает понятия slave DMA. Вместо этого, собственные функции PCI (функциональные единицы) являются либо "хозяевами шины" (bus masters), выполняющими свой собственный DMA перенос данных, либо они используют программируемый ввод/вывод. Единственным типом устройств, которые могут использовать DMA перенос данных с использованием системных контроллеров (slave DMA), являются не-PCI платы, подключенные через (E)ISA мост.

В DMA операциях собственно PCI шины, участники называются агентами (agents), и в каждой транзакции всегда участвуют два из них, а именно:

Инициатор (Initiator). Это "хозяин шины" (bus master), который выиграл право доступа к шине и хочет определить операцию переноса.

Цель (Target). Это PCI функция (функциональная единица PCI устройства), адресуемая в настоящий момент инициатором с целью выполнения передачи данных.

Поскольку любой "хозяин шины" PCI может быть инициатором, то возможно передача данных напрямую между двумя PCI устройствами без промежуточной остановки данных в оперативной памяти. Эта мощная возможность просто предназначена для использования в высокоскоростных сетевых и видео приложениях.

Необходимо также упомянуть, что спецификация PCI не определяет стратегию, которая должна быть использована при арбитрировании доступа к шине. Определены только временные диаграммы арбитрирующих сигналов в шине. Метод по нахождению того, "кто будет следующим" в использовании шины, является определяемым конкретной системой, где реализованы шины PCI.


Хост-контроллер IEEE 1394 использует DMA для передачи данных и команд в/из системной памяти. Устройства, подключенные к шине 1394, не могут иметь прямого доступа к системной памяти, поэтому адаптеры OHCI предоставляют диапазон адресов (в системной памяти),с которыми, собственно, и работают прикладные программы и контроллер DMA. Таким образом, возникает иллюзия возможности DMA передачи данных для каждого устройства, подключенного к шине.




Устройства USB шины не имеют прямого доступа к системной памяти. Они изолированы от системных ресурсов USB интерфейсом хост-компьютера и не поддерживают способа DMA передачи данных в привычном смысле. Тем не менее, USB интерфейс хост-компьютера обеспечивает "иллюзию" поддержки DMA для логических pipe-каналов, обеспечивающих доступ к конечным точкам (т.е. буферам) внутри подключенных к шине устройств. По мере получения данных от подключенных USB устройств, интерфейс хост-контроллера использует DMA доступ для того, чтобы поместить полученные из устройства данные в системную память. Таким образом, USB интерфейс, состоящий из внешнего устройства, контроллера в хост-компьютере и системные драйверы (но не каждое USB устройство по отдельности!), поддерживает возможность DMA передачи данных &#8212 или, строго говоря, ее иллюзию.




Первоначально предложенный стандарт PC CARD не предполагал возможности DMA передачи данных. Более поздний стандарт, принятый в 1995 году, ввел DMA расширение в стандарт, названный 'DMA'. Это расширение стандарта позволяет выполнять передачу 16 разрядных слов в манере, сходной со стандартом ISA. Этот стандарт подразумевает, что все устройства, подключенные к шине, выступают в роли 'bus slave' устройств, совместно использующих DMA контроллеры. Так же, как и в архитектуре ISA, трудно реализовать режим 'bus master DMA'.

Стандарт CardBus позволяет выполнять операции DMA передачи данный почти что таким же образом, как это выполняется в архитектуре PCI. Данное дополнение, позволяющее работать с устройствами в роли 'bus master', является весьма важным. 16 и 32 разрядные операции DMA передачи данных в стандарте CardBus могут выполняться при частоте 33 МГц. Правда, фактор ограниченности геометрических размеров требует использования компонентов (ИС) более высокой степени интеграции.



Выбор конфигурации


После того как NTLDR начинает загрузку и получит информацию об оборудовании, будет отображен экран с диалогом "Выбор профиля оборудования/Восстановление системы" (Hardware profile/Configuration recovery). B случае если профиль оборудования является единственным, то диалога представлено не будет.



Выполнения кода процедуры DpcForIsr


В ответ на выполненный ISR процедурой вызов IoRequestDpc, принадлежащая данному драйверу процедура DpcForIsr добавляется в очередь DPC объектов (DPC dispatch queue). Когда значение процессорного IRQL падает ниже DISPATCH_LEVEL, диспетчер процедур обработки отложенных вызовов выполняет вызов DpcForIsr данного драйвера. Процедура DpcForIsr выполняется на уровне DISPATCH_LEVEL, что означает: она не должна работать с адресами страничной памяти.

Диспетчер ввода/вывода игнорирует множественные вызовы IoRequestDpc

для данного объекта устройства до начала выполнения DpcForIsr. Попытки повторно поместить объект DPC в очередь отклоняются. Это является нормальным отношением ко всем DPC объектам. В случае если логика работы драйвера такова, что он может выдавать перекрывающиеся DPC запросы для одного и того же устройства, и их следует учитывать, то такой драйвер должен реализовать собственную очередь DPC запросов (объектов).

Перечень обязанностей типовой процедуры DpcForIsr может включать:

Установку данных в блоке IoStatus данного пакета IRP, то есть размещение нужного кода STATUS_XXX в поле Status и числа действительно переданных байт данных в поле Information.

Выполнение вызова IoCompleteRequest для завершения обработки пакета IRP с соответствующим повышением приоритета. Будучи однажды вызван, пакет IRP не должен обрабатываться снова.

Выполнение вызова IoStartNextPacket &#8212 чтобы инициировать поступление следующего IRP пакета в процедуру StartIO.

VOID DpcForIsr( PKDPC Dpc, PDEVICE_OBJECT pDevObject, PIRP junk, PDEVICE EXTENSION pDevExtension) { // Неким образом получаем текущий IRP (например, из очереди // queueReadWrite, которую поддерживает сам драйвер): PIRP pIrp = GetCurrentIrp(&pDevExtension-&#62queueReadWrite);

// Инициируем поступление IRP из внутренней очереди в // в процедуру StartIO(): TodoStartNextPacket(&pDevExtension-&#62dqReadWrite, pDevObject)

// Даем возможность отработать процедурам завершения всех // вышестоящий драйверов, если они есть: IoCompleteRequest(pIrp, IO_NO_INCREMENT); }



Вывод на экран информации о процессе загрузки


Указав параметр /sos в соответствующей строке файла boot.ini, например,

multi(0)disk(0)rdisk(0)partition(2)\Windows="Комментарий для пользователя" /sos

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

multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\ntoskrnl.exe multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\hal.dll multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\KDCOM.DLL multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\BOOTVID.DLL multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\config\system multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\c_1251.nls multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\c_866.nls multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\l_intl.nls multi(0)disk(0)rdisk(0)partition(1)\Windows\FONTS\vga866.fon multi(0)disk(0)rdisk(0)partition(1)\Windows\AppPatch\drvmain.sdb multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\ACPI.SYS multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\WMILIB.SYS multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\pci.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\isapnp.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\viaide.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\PCIIDEX.SYS multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\MountMgr.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\ftdisk.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\dmload.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\dmio.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\PartMgr.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\VolSnap.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\atapi.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\disk.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\CLASSPNP.SYS multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\sr.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\Fastfat.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\KSecDD.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\NDIS.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\viaagp.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\nmfilter.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\Simvid.sys multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\DRIVERS\Mup.sys


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

В том случае, если в строке описания параметров загрузки после опции /sos ввести /PAE (поддержка расширенной физической адресации, PAE, см. главу 4), то первая строка примет вид: multi(0)disk(0)rdisk(0)partition(1)\Windows\System32\ntkrnlpa.exe


Убедиться программным способом в том, поддерживает ли операционная система в настоящий момент работу с РАЕ (то есть с каким ключом она загружена), несложно. Достаточно в текст драйвера включить следующий фрагмент:

if(*Mm64bitPhysicalAddress==TRUE)

{

       #if DBG

              DbgPrint("System supports IO operations over 4GB");

       #endif

}

else

{

       #if DBG

              DbgPrint("System doesn't support IO ioerations over 4GB");

       #endif

}

Переменная Mm64BitPhysicalAddress объявляется в файлах wdm.h и ntddk.h.
Другое подтверждение работы операционной системы с поддержкой РАЕ (Physical Address Extension) можно обнаружить в Системном Реестре: в таком случае параметр PhysicalAddressExtension в разделе HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management примет значение 1 (в отличие от значения 0 в отсутствие поддержки РАЕ).

Вызовы для выделения и освобождения областей виртуальной памяти


Для манипуляций с областями памяти (выделение и освобождение) в режиме ядра используются специальные системные вызовы, отличающиеся от API вызовов режима ядра: ExAllocatePool, ExAllocatePoolWithTag, ExFreePool. В режиме ядра возможно выделение и освобождение области физически непрерывной памяти, что выполняется при помощи вызовов MmAllocateContiguousMemory

и, соответственно, MmFreeContiguousMemory. Эти вызовы рассмотрены ниже.

Таблица 7.2. Прототип вызова ExAllocatePool

PVOID ExAllocatePool IRQL &#60 DISPATCH_LEVEL
Параметры Выполняет выделение области памяти
IN POOL_TYPE PoolType Тип виртуальной памяти, в которой следует выделять область. Наиболее употребительны значения:

• PagedPool &#8212 страничная

• NonPagedPool

&#8212 нестраничная, тогда данную функцию можно вызывать при любом уровне IRQL

IN ULONG NumberOfBytes Размер запрашиваемой области
Возвращаемое значение Указатель на выделенную область либо NULL (в случае, если память выделить невозможно). Выделенная область всегда выравнивается на 8 байт. В случае, если запрашиваемый размер превышает PAGE_SIZE, то выделяемая область выравнивается на размер страницы.

Таблица 7.3. Прототип вызова ExAllocatePoolWithTag

PVOID ExAllocatePoolWithTag IRQL &#60 DISPATCH_LEVEL
Параметры Выполняет выделение области памяти
IN POOL_TYPE PoolType См. описание ExAllocatePool

выше.

IN ULONG NumberOfBytes Размер запрашиваемой области
IN ULONG Tag Метка (тег) для данной области, можно задавать как 4 символа, например, 'ABCD'. Удобно для отладки.
Возвращаемое значение См. описание ExAllocatePool

выше.

Таблица 7.4. Прототип вызова ExFreePool

VOID ExFreePool IRQL &#60 DISPATCH_LEVEL
Параметры Выполняет освобождение области памяти
IN PVOID pBuffer Указатель на освобождаемую область памяти, выделенную вызовами ExAllocatePool или ExAllocatePoolWithTag. Если освобождается нестраничная память, вызов может быть сделан из кода на уровне DISPATCH_LEVEL IRQL
Возвращаемое значение void
<
Таблица 7.5. Прототип вызова MmAllocateContiguousMemory

PVOID MmAllocateContiguousMemory IRQL == PASSIVE_LEVEL
Параметры Выполняет выделение физически непрерывной области памяти
IN ULONG NumberOfBytes Размер запрашиваемой области
IN PHYSICAL_ADDRESS maxAcceptableAddress Верхний предел адресов для запрашиваемой области. Поле HighPart=0, поле LowPart принимает значения, например:

• 0x000FFFFF (до 1 МБ)

• 0x00FFFFFF (до 16 МБ)

• 0xFFFFFFFF(до 4ГБ)
Возвращаемое значение Виртуальный адрес или NULL (при неудаче).

(Для повышения вероятности успешного завершения рекомендуется выполнять вызов в DriverEntry, поскольку память при работе системы быстро становится сильно дефрагментированной)
Таблица 7.6. Прототип вызова MmFreeContiguousMemory

VOID MmFreeContiguousMemory IRQL == PASSIVE_LEVEL
Параметры Выполняет освобождение области памяти
IN PVOID pBuffer Указатель на область памяти, выделенную ранее с использованием системного вызова MmAllocateContiguousMemory
Возвращаемое значение void
Таблица 7.7. Прототип вызова MmIsAddressValid

BOOLEAN MmlsAddressValid IRQL &#60= DISPATCH_LEVEL
Параметры Выполняет проверку виртуального адреса
IN PVOID VirtualAddress Виртуальный адрес, который следует проверить
Возвращаемое значение TRUE &#8212 если присутствует в оперативной памяти

FALSE &#8212 если вызовет прерывание PAGE FAULT

Замечание. Если адрес не находится в нестраничной памяти (или не зафиксирован в оперативной памяти), возврат TRUE не гарантирует отсутствие проблем при работе на повышенных уровнях IRQL (поскольку к моменту использования данного виртуального адреса ситуация может измениться).
Таблица 7.8. Прототип вызова MmGetPhysicalAddress

PHYSICAL_ADDRESS MmGetPhysicalAddress IRQL &#8212 любой
Параметры Определяет физический адрес, соответствующий данному виртуальному
IN PVOID VirtualAddress Анализируемый виртуальный адрес
Возвращаемое значение Физический адрес. Перед данным вызовом следует воспользоваться MmlsAddressValid

Взаимоблокировки


Взаимоблокировки (deadlocks) могут возникать при недостаточно корректном использовании спин-блокировок, объектов событий, мьютексов и семафоров, то есть практически любых синхронизационных примитивов. Даже объекты потоков могут дать взаимоблокировку, если потоки ожидают окончания работы друг друга. Взаимоблокировки легко возникают там, где происходит состязание нескольких потоков за владение несколькими ресурсами, причем каждому из них нужно несколько единиц ресурсов одновременно &#8212 и это при том, что каждый поток может получить отказ при попытке доступа к каждому из ресурсов.

Разумеется, каждый случай такой "неразберихи" уникален, тем более, что здесь может оказывать влияние еще один квази-ресурс &#8212 приоритет. Тем не менее, можно выделить три направления решения этой проблемы.

Во-первых, можно указывать максимальное время ожидания (параметр Timeout) при вызовах KeWaitForXxx. Этот способ не так хорош, поскольку не устраняет логическую ошибку в алгоритме работы драйвера, однако, дает локальное решение проблемы.

Второй способ состоит в пере-разбиении или укрупнении ресурсов, что сокращает "количество поводов" для разногласия.

В-третьих, можно заставить все потоки производить получение ресурсов в одном и том же порядке. Скажем, два потока имеют в своем распоряжении по одному ресурсу и начинают спор о владении вторым (он, разумеется, находится у конкурента). Проблемы не возникло бы, если оба потока решили, кому владеть первым ресурсом, победителю достался бы и второй, после чего он освободил бы оба ресурса, предоставив их конкуренту.



WDM, Windows Driver Model


Драйверная модель для Windows. Ориентирована на устройства, поддерживающие спецификацию PnP (в особенности, самоидентификацию — сообщение своих идентификационных номеров — при подключении к соответствующей шине). В драйвер WDM модели введены новые рабочие процедуры, которые отражают стадии подключения-обнаружения PnP устройства, события в изменении энергоснабжения и факт отключения устройства. "Правильное" PnP устройство обнаруживается шинным драйвером (той шины, к которой оно подключается), затем о нем узнает PnP Менеджер, после чего и загружается драйвер — в соответствии с полученными идентификаторами. Если WDM драйвер "правильный", то он должным образом подключает себя к стеку драйверов на этой шине. После этого почти все в дальнейшей жизни драйвера зависит от обращения с IRP запросами к драйверам, подключившимся к стеку ранее (начиная от запросов о конфигурации шины и заканчивая запросами к "своему" устройству). Драйвер модели WDM использует только определения, доступные из файла wdm.h, в результате чего не может использовать "полупартизанские" функции прежних NT-драйверов (функции типа HalGetBusData и т.п.). Драйверы модели WDM зачастую полностью (вплоть до бинарной формы) совместимы для использования в Windows 98, Windows Me, Windows 2000, Windows XP и Windows Server 2003, хотя бывают и исключения.

Разумеется, можно не поддерживать обработку всех требуемых для PnP драйверов запросов, что вполне приемлемо для драйверов, которые нужны лишь в качестве окна в режим ядра. Однако для таких исследований проще использовать драйверы "в-стиле-NT" (NT style или legacy драйверы, о чем рассказывается ниже).



Заголовочный файл Driver.h


Ниже приводится полный текст файла Driver.h, содержащий объявления, необходимые для компиляции драйвера Example.sys.

#ifndef _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_ #define _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_ // Выше приведены две строки (в конце файла имеется еще #endif), // которые в больших проектах запрещают повторные проходы по тексту, // который находится внутри h-файла (что весьма удобно для повышения // скорости компиляции). // (Файл Driver.h)

#ifdef __cplusplus extern "C" { #endif

#include "ntddk.h"

//#include "wdm.h" // ^^^^^^^^^^^^^^ если выбрать эту строку и закомментировать // предыдущую, то компиляция в среде DDK (при помощи утилиты Build) // также пройдет успешно, однако драйвер Example не станет от этого // настоящим WDM драйвером.

#ifdef __cplusplus } #endif // Определяем структуру расширения устройства. Включим в нее // указатель на FDO (для удобства последующей работы UnloadRoutine) и // имя символьной ссылки в формате UNOCODE_STRING.

typedef struct _EXAMPLE_DEVICE_EXTENSION { PDEVICE_OBJECT fdo; UNICODE_STRING ustrSymLinkName; // L"\\DosDevices\\Example" } EXAMPLE_DEVICE_EXTENSION, *PEXAMPLE_DEVICE_EXTENSION;

// Определяем собственные коды IOCTL, с которыми можно будет // обращаться к драйверу при помощи вызова DeviceIoControl. // Определение макроса CTL_CODE можно найти в файле DDK Winioctl.h. // Там же можно найти и численные значения, скрывающиеся под именами // METHOD_BUFFERED и METHOD_NEITHER.

// Внимание! Текст приведенный ниже должен войти в файл Ioctl.h, // который будет необходим для компиляции тестового приложения. // (Разумеется, за исключением последней строки с "#endif".)

#define IOCTL_PRINT_DEBUG_MESS CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_CHANGE_IRQL CTL_CODE(\ FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_MAKE_SYSTEM_CRASH CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_TOUCH_PORT_378H CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_SEND_BYTE_TO_USER CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)

// Вариант : //#define IOCTL_SEND_BYTE_TO_USER CTL_CODE( \ // FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS) #endif

Третий параметр CTL_CODE называется Function и при составлении собственных (пользовательских) IOCTL кодов его значение не должно быть менее 0x800. Пересечение пользовательских IOCTL кодов со значениями IOCTL кодов других драйверов не имеет никакого значения, поскольку они действуют только в пределах конкретного драйвера.


ULONG xferCount, // текущий передаваемый байт xferRest; // остаток непереданных байт (индикатор завершенности) //============================================= PUCHAR portBase; // адрес порта ввода/вывода ULONG Irq; // Irq в терминах шины ISA для параллельного порта //============================================= PKINTERRUPT pIntObj; // interrupt object KDPC DpcForIsr_Object; // DPC object

} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

// Маски для выделения бит в регистре управления: #define CR_NOT_RST 0x04 // CR.2 - 0 Reset printer #define CR_INT_ENB 0x10 // CR.4 - 1 Interrupt enable #define CR_DEFAULT 0xC0 // неиспользуемые биты

// Макроопределения для записи и чтения в параллельный порт #define DATA_REG 0 #define STATUS_REG 1 #define CONTROL_REG 2

#define WriteControlRegister( pDeviceExtension, byte ) \ ( WRITE_PORT_UCHAR( pDeviceExtension->portBase + CONTROL_REG, byte ) )

#define WriteDataRegister( pDeviceExtension, byte ) \ ( WRITE_PORT_UCHAR( pDeviceExtension->portBase + DATA_REG, byte ) )

#define ReadStatusRegister( pDeviceExtension ) \ ( READ_PORT_UCHAR( pDeviceExtension->portBase + STATUS_REG ) )


Заголовок IRP


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

Таблица 8.5. Заголовок пакета IRP

Поля Описание
IO_STATUS_BLOCK IoStatus Код состояния (статус) запроса
PVOID AssociatedIrp.SystemBuffer Указатель на системный буфер для случая, если устройство поддерживает буферизированный ввод/вывод
PMDL MdlAddress Указатель на MDL список в случае, если устройство поддерживает прямой ввод/вывод
PVOID UserBuffer Адрес пользовательского буфера для ввода/вывода
BOOLEAN Cancel Индикатор того, что пакет IRP должен быть аннулирован

Фрагмент структуры IRP под названием IoStatus фиксирует окончательное состояние данной операции ввода/вывода. Когда драйвер готов завершить обработку пакета IRP, он устанавливает в поле IoStatus.Status значение STATUS_XXX. В поле IoStatus.Information этого блока записывается 0 (если произошла ошибка) или другое определенное операцией ввода/вывода значение, чаще всего &#8212 количество переданных/полученных байт данных (которое может быть и равно нулю).

Вопросы адресации и доступа к буферам данных, описываемых пакетом IRP, будут рассмотрены позже.



Загрузка ядра


На этапе загрузки ядра NTLDR выполняет следующие действия:

Загружает код ядра из файла NTOSKRNL.EXE (NTKRNLPA.EXE при наличии опции /РАЕ в файле boot.ini), но не инициализирует его.

Загружает код слоя аппаратных абстракций из файла HAL.DLL.

Загружает раздел HKLM\SYSTEM из %systemroot%\System32\Config\System.

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

Загружает драйверы (обычно это низкоуровневые драйвера, как, например, драйвера дисков) со значением параметра Start равным 0x0.

Значение параметра List в HKLM\SYSTEM\CurrentContorlSet\Control\ServiceGroupOrder

определяет порядок загрузки их загрузчиком NTLDR. Драйверы, регулирующие свою загрузку таким способом, должны иметь соответствующие значения параметра Group в своих подразделах раздела Системного Реестра HKLM\System\CurrentControlSet\Services.



Загрузка операционной системы


При загрузке 32-разрядных версий операционной системы Windows NT 5 (2000, XP или Server 2003) используются следующие файлы:

C:\NTLDR &#8212 стадии подготовки к загрузке и загрузка

C:\BOOT.INI &#8212 стадия загрузки ядра

C:\BOOTSECT.DOS &#8212 стадия загрузки ядра (опционально)

C:\NTDETECT.COM &#8212 стадия загрузки ядра

%systemroot%\System32\NTOSKRNL.EXE &#8212 стадия загрузки ядра (или NTKRNLPA.EXE)

%systemroot%\System32\HAL.DLL &#8212 стадия загрузки ядра

Раздел реестра HKLM\SYSTEM &#8212 стадия инициализации ядра

%systemroot%\System32\*.sys &#8212 стадия инициализации ядра

Здесь под %systemroot% подразумевается директория, где размещены основные файлы операционной системы, например, D:\Windows &#8212 для Windows XP, установленной на логический диск D. Для системы Windows 2000, например, установленной на логическом диске С, это будет директория C:\WINNT.



В данной главе представлены начальные


В данной главе представлены начальные сведения по предметной области, называемой "Драйверы Windows".


В данной главе не были рассмотрены программные средства, которые можно назвать средствами "второго эшелона". Среди них — программы верификации драйверов (которые тестируют драйверы, обращаясь к ним со всевозможными "глупыми" запросами — то есть такими обращениями, которые, как полагает разработчик, никогда не поступят в драйвер), а также модификация настроечных параметров запуска Windows (файл boot.ini), позволяющих, в частности, имитировать небольшой размер физической памяти. Кроме того, в составе утилит Windows DDK и на интернет сайтах упомянутых выше разработчиков можно обнаружить множество программ, которые могут оказаться полезными разработчику драйвера при решении отдельных специфических проблем.


В данной главе был рассмотрен простейший драйвер Example.sys, реализованный как драйвер "в-стиле-NT" (тип драйвера, называемый еще Legacy Driver). Ha его примере были представлены начальные сведения об основных этапах разработки простого драйвера: компиляция и сборка checked версии в среде Microsoft Visual Studio, в среде Windows DDK, реализация тестирующего приложения пользовательского режима, инсталляция и запуск. Изложение этого материала можно признать поверхностным и беглым, однако, предназначено оно для того, чтобы дать первый фактический материал начинающему разработчику для более продуктивного чтения последующих глав этой книги.


Операционные системы ряда Windows NT 5.x (Windows 2000/XP/Server 2003) представляют богатые возможности для разработки приложений. Схема обработки операций ввода/вывода в Windows 2000/XP/Server 2003 сложна и требует достаточных усилий, чтобы охватить картину в целом, что необходимо для продуктивной работы. Главы 5 и 6 рассматривают эти вопросы более подробно.


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


Краткое введение в подсистему ввода/ вывода операционной системы Windows NT 5 завершено. Полностью, хотя и весьма кратко, описана структура драйвера Windows 2000/XP/Server 2003, который предназначен для работы в режиме ядра.
Прежде, чем перейти к деталям реализации драйверных процедур, остановимся, в следующей главе, на некоторых практических приемах программирования в режиме ядра, а именно &#8212 на работе с памятью, строками Unicode, объектами и т.п.


Краткий обзор структур данных и функций общего назначения и первой необходимости при программировании в режиме ядра завершен. Более детальную информацию и сведения о нерассмотренных здесь системных вызовах можно почерпнуть в документации DDK. Автор надеется, что приведенные в этой главе сведения существенно в этом помогут.
Позже, в главе 10, будет рассмотрена еще одна достаточно важная группа функций, посвященная работе с программными потоками и ведающая вопросами синхронизации.


Рабочие процедуры составляют основу интерфейса между драйвером и инициатором запроса. В данной главе были рассмотрены основные особенности конструкции этих функций и обсуждались детали получения доступа к буферным областям, задаваемым инициатором запроса, и к другим параметрам, передаваемым в драйвер клиентским приложением. Были затронуты также вопросы, связанные с обработкой прерываний.
Практические примеры, посвященные отработке приемов обслуживания прерываний будут рассмотрены в главе 11, "Обработка аппаратных прерываний".
В следующей главе будут рассмотрены дополнения, привнесенные драйверной моделью WDM.


В данной главе были рассмотрены мотивы, обусловившие внедрение PnP спецификации как стандарта конструирования аппаратуры. Были рассмотрены также пути приспособления драйверной модели DOS-Windows к новым условиям, в результате чего конечной версией драйверной модели на сегодня стала WDM модель. Были обсуждены специфические аспекты разработки драйверных процедур в канве новой модели и рассмотрены примеры использования стандартных системных вызовов режима ядра в процедурах WDM драйверов, реализующих многослойную методологию современной драйверной модели Windows.


В данной главе были рассмотрены основы построения исполнительного рисунка программного кода режима ядра &#8212 способы запуска программных потоков, основные синхронизационные примитивы и системные вызовы для работы с ними. Использование программных потоков позволяет отойти от жесткой схемы "Диспетчер ввода/вывода &#8212 рабочая процедура &#8212 Диспетчер ввода/вывода" и реализовывать более гибкие алгоритмы, в том числе &#8212 с помощью объектов синхронизации.
Следующая глава будет посвящена рассмотрению двух примеров драйверов, работающих с аппаратными прерываниями LPT порта.


Данная глава была посвящена рассмотрению двух реализаций драйвера LPTPort.sys, работающего с параллельным портом (устройством отмирающей, но еще вполне "живой" шины ISA), который при дополнении его тестовой заглушкой CheckIt становится отличным полигоном для детального рассмотрения зарождения и обработки прерываний в драйверах режима ядра.
Драйвер обладает определенными недостатками. Обслуживая не-PnP устройство, он опять-таки построен по методике Legacy (как драйвер "в стиле NT"), a не как современный WDM драйвер. Кроме того, для простоты реализации, он использует ресурсы, обозначенные другим, системным драйвером, не уделив должного внимания собственному "захвату" аппаратных ресурсов с последующей регистрацией их в операционной системе.
Тем не менее, заглушка CheckIt позволит любому читателю данной книги заполучить несложное устройство, которое позволяет произвольно, по собственному желанию, получать и обрабатывать прерывания на любом компьютере, в офисе или дома.
Первая, упрощенная версия драйвера LPTPort.sys, знакомит с общей методикой обработки прерываний в Windows NT 5.x. Вторая, усложненная версия вводит в использование механизмов системных очередей IRP пакетов как метода обеспечения последовательного доступа к устройству, а также знакомит с приемами совместного использования объектов события приложениями пользовательского режима и драйверами.
На базе приведенных примеров читатель может самостоятельно достроить достаточно интересные тесты, например, по одновременному доступу к драйверу из разных приложений (с использованием событий уведомляющего, а не синхронизационного типа), а также по реализации собственных очередей отложенных IRP пакетов.
Следующая глава будет посвящена рассмотрению вопросов установки драйверов с использованием inf-файлов.


Установка драйвера при помощи inf-файлов является приемлемым и общепринятым решением для проведения установки драйвера в системе, особенно если драйвер следует передать для использования другим людям, которые не могут и вовсе не обязаны быть в курсе тонкостей установки разработанного драйвера.
В данной главе были приведены сведения, как создавать корректные inf-файлы, как раз и предоставляющие преимущества стандартизованного механизма установки.
В следующей главе будут рассмотрены общие вопросы отладки драйверного кода.


Данная глава была посвящена рассмотрению общих вопросов тестирования драйверов.
Ошибки программирования драйверов обладают большой разрушительной и деморализующей силой. Простой пользователь в этой ситуации напоминает мирного жителя, который не может точно сосчитать неожиданно появившихся диверсантов. И не следует его в том винить.
Драйвер является кодом, которому операционная система априори доверяет больше, нежели коду приложений пользовательского режима. Эта банальность есть основной аргумент в пользу тщательной проверки всех неясных мест в коде драйвера, включая дополнительную проверку, зачастую, весьма скупо описанных в документации DDK возможностей и особенностей системных вызовов. Возможно, приведенные в данной главе немногочисленные методы обнаружения, изоляции и предотвращения ошибок драйверного кода помогут разработчику в его нелегком труде.
Интерактивная отладка всегда более привлекательна, однако, оснащение для этого требуется существенное, включая возможные затраты по подписке на дополнительную информацию от Microsoft.
Операционная система Windows NT 5 (версии 2000, XP и Server 2003) отличается богатым набором инструментальных средств, которыми разработчик быстро и эффективно может локализовать ошибку, особенно, если она является в большей степени программной (в отличие от аппаратных ошибок обслуживаемого устройства). Остается лишь научиться это применять на практике.

Замечания по декорированию имен


Начинающие разработчики inf-файлов в большинстве своем испытывают значительные затруднения при работе с ними. Между тем, при определенном взгляде на данное "странное явление", каковым является inf-файл, трудности можно существенно уменьшить.

Прежде всего, маркер, который является идентификатором, взятым в два знака процента, есть не что иное, как формальная переменная ('икс' в школьной задаче). Его внешнее сходство с чем-либо еще в записях inf-файла обманчиво. Во всех тех местах, где встречается маркер, следует подставлять его значение, соотнесенное с ним в секции [Strings]. Причем, это значение, может быть не обязательно текстовым, например:

[Example.Service] DisplayName = %Example.ServiceName% ServiceType = %SERVICE_KERNEL_DRIVER% StartType = %SERVICE_DEMAND_START%

[Strings] Example.ServiceName="Example NTDDK driver (V.001)" SERVICE_KERNEL_DRIVER=1 SERVICE_DEMAND_START=3

Здесь с маркером %Example.ServiceName% соотнесено строковое значение "Example NTDDK driver (V.001)", а с маркером %SERVICE_KERNEL_DRIVER% соотнесено значение 1. Текстовые значения (здесь Example.ServiceName) называются локализуемыми значениями (их можно задавать разными для разных языковых версий операционной системы, локализаций). Нетекстовые значения считаются нелокализуемыми.

В том случае, если значение маркера не раскрывается, то будет полагаться, что поле, в котором введен маркер, просто имеет текстовое значение, совпадающее с маркером, например, "%Example.ServiceName%" в примере выше.

Во-вторых, двусмысленна роль точки в формировании имен секций и маркеров.

Если точка встречается в написании маркера, то следует считать ее обычным символом, который разработчик употребил ради большей выразительности.

В случае если точка встретилась в имени секции (или в ссылке на имя секции), то возможно два варианта:

Присоединяемый после точки суффикс &#8212 один из предопределенных суффиксов NT, NTx86, x86, alpha, ia64, NTia64, NT.5.1, NT.5.2 либо Services, NT.Services, NTx86.Services и некоторые другие. В данном случае, это модификация

свойств секции. В частности, секция [ModelList.NT.5.1] будет приниматься к рассмотрению только в операционной системе Windows XP.

Присоединяемый суффикс не является модификатором свойств секции. В данном случае точку следует рассматривать как рядовой видимый символ, как и в упомянутом выше случае, когда точка встречается в идентификаторе маркера.



Запуск драйвера


После того как выполнена описанная модификация Системного Реестра и файл Example.sys был размещен в директории C:\Windows\System32\Drivers\, необходимо выполнить перезагрузку операционной системы (как в случае Windows 98, так и в случае Windows 2000, XP, Server 2003) для того, чтобы драйвер был загружен и начал работу.



Запуск и окончание отладочной сессии


Основное предназначение отладчика WinDbg &#8212 интерактивная отладка. Для этого необходимо наличие двух компьютеров: целевого (на нем будет запущен тестовый код) и хост-компьютер (на котором производится разработка и работает WinDbg). Существенно здесь то, что для управления и мониторинга активности на целевом компьютере используется интерфейс последовательных портов. На рисунке 13.1 показано соединение хост и целевого компьютеров и размещение файлов на них.

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

Рис. 13.1

Интерактивная отладка с использованием WinDbg

Инсталлировать бинарный файл драйвера на целевой машине, причем там не нужно устанавливать файлы идентификаторов или исходного текста.

На хост-компьютере необходимо иметь файлы идентификаторов драйвера и файлы идентификаторов операционной системы, установленной на целевом компьютере. Неплохо было бы, чтобы на двух компьютерах была бы установлена одна и та же версия системы (и одинаковые обновления, Service pack), однако это не является жестким условием.

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

Необходимо выбрать отладку в режиме ядра в меню View закладки Options Kernel Debugger в отладчике WinDbg.

Необходимо установить приемлемые значения номера СОМ порта и скорости передачи из упомянутого диалогового окна.

Следует выбрать кнопку GO или ввести команду g в окне командной строки отладчика для того, чтобы перевести WinDbg в режим ожидания соединения с целевым компьютером.

Необходимо выполнить перезагрузку целевого компьютера, разрешив использование клиента отладки. Когда завершится перезагрузка системы на целевом компьютере, соединение будет установлено. В командном окне WinDbg будет выведено соответствующее сообщение.

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

Чтобы разорвать отладочную сессию, необходимо выполнить следующее.

Сделать паузу, нажав Ctrl-C в командном окне WinDbg.

Следует выбрать Edit &#8212 Breakpoints в отладчике WinDbg, затем выполнить Clear All. Важно выполнить очистку точек останова до прекращения отладочной сессии.

Следует выбрать Run &#8212 Go для того, чтобы разрешить дальнейшую работу на целевом компьютере.

Выйти из WinDbg (Alt-F4).

Целевой компьютер может отреагировать некоторой задержкой на разрыв соединения, возможно из-за того, что процедуры KdPrint и DbgPrint

более не имеют получателя своих сообщений и им нужно некоторое время для отработки этой ситуации.



Значения HKR


Аббревиатурой HKR в inf-файлах обозначаются подходящие по контексту подразделы Системного Реестра, применимые для данной операции.

Число типов приемлемых подразделов, на которые мог бы указывать контекстный параметр HKR, невелико, среди которых разработчику драйверов могут понадобиться следующие:

Подраздел экземпляра аппаратуры, Hardware Instance Key. Такие подразделы описывают экземпляр устройства в процессе перечисления и видны в разделе HKLM\System\CurrentControlSet\Enum, например, HKLM\System\CurrentControlSet\Enum\USB\Vid_0458&Pid_000e\5&1e1f5333&0&1, мышь "Genius".

Подраздел класса, Class Key, описывает зарегистрированный класс драйвера. Его можно найти в разделе Системного Реестра HKLM\System\CurrentControlSet\Control\Class (обязательно с добавлением GUID класса), например, для описанной выше мышки это будет ...\Class\{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}, то есть HIDClass.

Драйверный подраздел, Driver Key, описывает установленный драйвер в подразделе для всех устройств данного класса (подразделе класса), например, .. .\Class\{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}\0000.

Сервисный подраздел, Service Кеу (или Software Кеу), описывает, где находится загружаемый файл драйвера, когда его следует загружать, как обрабатывать ошибки и т.п. Такие подразделы видны в разделе HKLM\System\CurrentControlSet\Services, для примера с USB мышкой это будет HKLM\System\CurrentControlSet\Services\HidUsb.

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

Таблица 12.9. Значение параметра HKR

в секциях [AddReg]

Откуда исходит ссылка на [AddReg] Куда указывает HKR
Секция [DDInstall], директива AddReg Драйверный подраздел
Секция [DDInstall.Xxx.Hw], директива AddReg Подраздел экземпляра аппаратуры
Директива AddReg в секции [ServiceInstall], на которую указывает директива AddService

секции [DDInstall.Xxx.Services]

Сервисный подраздел
Секция [ClassInstall32] или [ClassInstall], директива AddReg Подраздел класса
Секция [DDInstall.Xxx.Coinstallers], директива AddReg Драйверный подраздел