Общие приемы отладки
Зачастую проблемы исправления ошибок можно легко устранить при наличии достаточного объема диагностических сообщений. Ниже приводятся приемы, основанные как раз на этом.
Общий взгляд на структуру драйвера режима ядра
Уточняя уже представленную ранее метафору, что драйвер есть DLL режима ядра, можно сказать, что драйвер представляет собой всего лишь коллекцию процедур, которые вызываются системным программным обеспечением, как правило, Диспетчером ввода/вывода. Драйверные процедуры пассивно ожидают того момента, когда к ним обратится программный код Диспетчера ввода/вывода.
В зависимости от назначения драйвера, Диспетчер ввода/вывода может вызывать процедуры драйвера в следующих ситуациях:
При загрузке драйвера.
При выгрузке драйвера и выполнении отката системы.
В моменты, когда устройство, обслуживаемое драйвером, подключается или удаляется из системной конфигурации.
Программы пользовательского режима выполняют вызовы системных служб для ввода/вывода.
Совместно используемые аппаратные ресурсы становятся доступными для использования драйвером.
В различные моменты во время реального функционирования обслуживаемого устройства (скажем, для обработки прерывания, поступившего от обслуживаемого устройства).
В моменты, связанные с изменениями в энергоснабжении.
При опросе конфигурации устройства PnP Менеджером.
Ниже приводится краткое описание основных категорий процедур, входящих в состав драйвера режима ядра.
Обслуживание прерываний
В операционной системе Windows NT 5 все прерывания изначально обрабатываются ядром. Это сделано для облегчения переносимости операционной системы на разные аппаратные платформы. Ядро обеспечивает диспетчеризацию прерываний между драйверами путем создания и последующего подключения объектов прерывания по вызову IoConnectInterrupt
(см. таблицу 8.10). Этот вызов получает адрес процедуры для обслуживания прерываний (ISR, Interrupt Service Routine) драйвера, и таким образом операционная система связывает указанное аппаратное прерывание с определенным драйвером и принадлежащей ему ISR функцией.
Вызов IoConnectInterrupt возвращает указатель на объект прерывания (через первый аргумент). Возвращенный указатель должен быть сохранен в структуре расширения объекта устройства, так как он понадобится в дальнейшей работе, в частности, при отключении от источника прерываний.
Когда операционная система получает сигнал прерывания от устройства, она использует свой список объектов прерывания для локализации ISR процедуры, в ведении которой находится обслуживание данного события. Она "пробегает" по всем объектам прерывания, подключенным к DIRQL этого прерывания, и вызывает ISR процедуры до тех пор, пока одна из них не заявит о своих правах на него.
Диспетчер прерываний режима ядра вызывает ISR процедуру на уровне синхронизации SynchronizeIrql, указанном в вызове IoConnectInterrupt. Обычно, это один из DIRQL уровней. Кроме того, диспетчер прерываний получает владение над объектом спин-блокировки pSpinLock и удерживает ее во время выполнения ISR процедуры, что предохраняет от выполнения ISR процедуры на других процессорах.
При выполнении на столь высоком уровне IRQL существует некоторые вещи, которые процедура ISR не может себе позволить. В дополнении к обычным предостережениям избегать манипуляций со страничной памятью, ISR процедура не должна пытаться получать или освобождать какие-либо системные ресурсы, даже нестраничную память. Если разработчик предполагает сделать системный вызов из ISR процедуры, следует обязательно обратить внимание на уровень, на котором тот может выполняться.
Вполне вероятно, что такие системные вызовы придется перепоручить DPC процедуре, запуск которой вполне может запланировать данная функция обслуживания прерываний.
Таблица 8.10. Прототип функции IoConnectInterrupt
NTSTATUS IoConnectInterrupt | IRQL == PASSIVE_LEVEL |
Параметры | Регистрирует процедуру обслуживания прерывания, предоставляемую драйвером, и "подключает" ее к источнику прерываний |
OUT PKINTERRUPT *pInterruptObject |
Адрес указателя, в котором будет возращен указатель на объект прерывания |
IN PKSERVICE_ROUTINE ServiceRoutine |
Процедура (функция) драйвера, которая теперь будет обслуживать прерывание |
IN PVOID pServiceContext | Аргумент, передаваемый в процедуру ISR, обычно рекомендуется приводить здесь указатель на структуру расширения объекта устройства |
IN PKSPIN_LOCK pSpinLock | Инициализированный объект спин-блокировки |
IN ULONG Vector | Транслированное значение вектора прерывания |
IN KIRQL Irql | Значение DIRQL для данного устройства |
IN KIRQL SynchronizeIrql | Обычно равно значению Irql |
IN KINTERRUPT_MODE InterruptMode | Аппаратный тип прерывания. Одно из значений: • LevelSensitive • Latched |
IN BOOLEAN isSharableVector | Если TRUE — данный вектор прерывания является совместно используемым (разделяемым) |
IN KAFFINITY ProcessorEnableMask | Установить набор процессоров, которые могут получать сигналы прерывания |
IN BOOLEAN doFloatingSave | Если TRUE — сохранять состояние регистров сопроцессора (FPU). Обычно используется FALSE |
Возвращаемое значение |
• STATUS_SUCCESS • STATUS_INVALID_PARAMETER • STATUS_UNSUFFUCIENT_RESOURCES |
BOOLEAN ISR | IRQL == DIRQL |
Параметры | Процедура драйвера, предоставляемая им для обслуживания прерывания |
IN PKINTERRUPT *pInterruptObject | Объект прерывания, "генерирующий" прерывания |
IN VOID pServiceContext | Контекстный аргумент, указанный при регистрации в IoConnectInterrupt |
Возвращаемое значение |
• TRUE — прерывание было обслужено ISR • FALSE — прерывание не обслуживается |
Параметр InterruptMode вызова IoConnectInterrupt интерпретируется операционной системой следующим образом. Когда драйверы подключили свои ISR процедуры к прерыванию, считая его LevelSensitive, операционная система вызывает все подключенные таким образом ISR функции до тех пор, пока одна из них не возвратит TRUE. В противном случае, то есть если указано значение Latched для параметра InterruptMode, то операционная система вызывает все из подключенных таким образом ISR процедур, и эти вызовы повторяются до тех пор, пока все ISR процедуры не возвратят значение FALSE.
В рамках общего подхода Windows любой программный код должен минимизировать свое пребывание на высоких приоритетах. Всегда следует оптимизировать код ISR процедуры для достижения максимальной скорости его выполнения. Действия, которые нельзя отнести к абсолютно необходимым именно в ISR процедуре, следует вынести в процедуру отложенного вызова (DPC). Особенно важно, чтобы ISR процедура сразу же определилась, будет ли она обрабатывать поступившее прерывание. Возможно, многие ISR процедуры ожидают этого прерывания, а малозначительный код данной ISR процедуры блокирует их работу.
Использование объектов прерываний дело достаточно хлопотное. Во-первых, если ISR процедура обслуживает более чем одно прерывание или драйвер имеет более чем одну ISR процедуру, должна использоваться спин-блокировка для того, чтобы не возникло недоразумений в использовании контекстного аргумента pServiceContext процедур(ы) ISR.
Во-вторых, в случае, если процедура ISR управляет более чем одним прерыванием (вектором прерывания), следует позаботиться о том, чтобы значение, указанное в качестве SynchronizeIrql было наибольшим значение DIRQL из всех обслуживаемых прерываний.
Наконец, процедура ISR драйвера должна быть готова к работе с момента выполнения вызова IoConnectInterrupt. Даже в том случае, если выполнены еще не все действия по инициализации драйвера.
В общих чертах, процедура обслуживания прерываний должна придерживаться следующего плана:
Определить, относится ли поступившее прерывание к данному драйверу. Если не относится — немедленно возвратить FALSE.
Выполнить все операции над устройством, необходимые для того, чтобы подтвердить устройству получение прерывания.
Определить, существует ли необходимость в передаче данных и дополнительных действиях, которые могут быть выполнены на низких уровнях IRQL. Если такая работа имеется, то следует запланировать вызов DPC процедуры (предоставляемой драйвером), то есть поставить в очередь DPC-запрос вызовом IoRequestDpc.
Возвратить значение TRUE.
Таблица 8.12. Прототип вызова IoRequestDpc
VOID IoRequestDpc | IRQL == DIRQL |
Параметры | Помещает DPC вызов в очередь |
IN PDEVICE_OBJECT pDevObject | Объект устройства, для которого зарегистрирована DPC процедура |
IN PIRP pIrp | Указатель на интересующий IRP пакет |
IN VOID pServiceContext | Контекстный аргумент |
Возвращаемое значение | void |
Обычно рабочие процедуры драйвера получают указатель на объект устройства и указатель на адресованный этому объекту IRP пакет через заголовок при вызове. Но прототип ISR процедур этого не предусматривает. Как же тогда сделать из нее вызов IoRequestDpc, чтобы запланировать DPC процедуру?
Данное затруднение решается, если при регистрации ISR процедуры вызовом IoConnectInterrupt
в качестве контекстного аргумента pServiceContext ввести указатель на структуру расширения структуры (извините за неблагозвучные повторы) данного объекта устройства, то есть указатель на DEVICE_EXTENSION. При условии заблаговременного сохранения там указателя на объект устройства (например, как это было сделано в DriverEntry примера главы 3) процедура ISR драйвера не будет испытывать затруднения с тем, откуда ей взять данный указатель.
Остается вопрос, что такое pIrp и где его найти?
Возможны два варианта. Во-первых, для обработки прерывания действительно требуется текущий обрабатываемый драйвером IRP пакет, и тогда его можно найти, как pDeviceObject->CurrentIrp, но только для пакета из системной очереди, SystemQueuing. Во-вторых, IRP пакет может и не потребоваться (по логике прерывания и работы драйвера), тогда можно просто указать NULL. Более того, во многих случаях IRP пакет указать невозможно или нежелательно. Поскольку Диспетчер ввода/вывода не анализирует этот параметр, то в нем можно передавать нужные (по логике работы) данные так же, как и в указателе pServiceContext. В результате, ISR процедура может выглядеть следующим образом:
BOOLEAN OnInterrupt ( PKINTERRUPT pInterruptObject, PVOID pServiceContext ) { PMYDEVICE_EXTENSION pDevExt=(PMYDEVICE_EXTENSION) pServiceContext;
// Считываем нужный регистр нашего устройства чтобы // определить, действительно ли оно посылало сигнал прерывания ULONG intrStatus = READ_PORT_ULONG((PULONG) (pDevExt->InterruptStateReg)); if( intrStatus!=. . . ) return FALSE; // Это чужое прерывание
// Некоторые действия, например, изменение состояния // регистров устройства, чтобы оно знало: о нем помнят. . . . // Планируем вызов DPC процедуры, например, без IRP пакета: IoRequestDpc( pDevExt->DeviceObject, NULL, pDevExt);
return TRUE; // Прерывание обработано }
Детальное рассмотрение вопросов обработки прерываний и применения DPC процедур выполнено в главе 11, "Обработка аппаратных прерываний".
Ограничения, накладываемые на WDM драйверы спецификацией PnP
Для того чтобы соответствовать драйверной модели WDM, драйвер обязан поддерживать обработку специфичных PnP IRP пакетов, каких конкретно — это определяется конкретным типом объекта устройства — не-шинный FDO, шинный FDO и PDO. Во всяком случае, IRP пакеты с приведенными в таблице кодами IRP_MN_Xxx должны поддерживаться драйверами всех типов.
Таблица 9.11. Суб-коды IRP_MN_Xxx
IRP_MN_Xxx | Значение | |
IRP_MN_START_DEVICE | (Ре)Инициализация устройства с заданными ресурсами | |
IRP_MN_QUERY_STOP_DEVICE | Осуществима ли остановка устройства для возможного переопределения ресурсов? | |
IRP_MN_STOP_DEVICE | Остановка устройства с потенциальной возможностью перезапуска или удаления из системы | |
IRP_MN_CANCEL_STOP_DEVICE | Уведомляет, что предыдущий запрос QUERY_STOP не получит дальнейшего развития | |
IRP_MN_QUERY_REMOVE_DEVICE | Может ли быть выполнено безопасное удаление устройства в текущий момент? | |
IRP_MN_REMOVE_DEVICE | Выполнить работу, обратную работе AddDevice | |
IRP_MN_CANCEL_REMOVE_DEVICE | Уведомляет, что предыдущий запрос QUERY_REMOVE не получит дальнейшего развития | |
IRP_MN_SURPRISE_REMOVAL | Уведомляет, что устройство было удалено без предварительного предупреждения |
Online документация Microsoft
На интернет-сайте Microsoft, уже упомянутом выше, хранятся также статьи, посвященные частным случаям проблем, связанных с программированием в Windows при помощи продуктов Microsoft, в частности, связанные с разработкой драйверов при помощи DDK. Статьи, зачастую, возникают как ответы на поступившие вопросы пользователей и имеют последовательно возрастающую нумерацию. Таким образом, если на одном из форумов некто написал, что некая волнующая собеседника проблема решается способом, описанным в Q115486 от Microsoft, то это означает следующее: следует перейти по интернет адресу microsoft.com и ввести упомянутый выше код в окне поиска, что даст возможность ознакомиться со статьей под заголовком "HOWTO: Control Device Driver Load Order (Q115486)" — "Как управлять порядком загрузки драйверов?".
Более полную информацию о всех онлайн-ресурсах этого интернет-сайта, которая имеет прямое или косвенное отношение к драйверам, можно получить на странице с интернет адресом microsoft.com/hwdev/driver
(хотя совершенно не исключено, что в будущем эта страница будет мигрировать).
Операции bus master DMA
Более сложные устройства, которые не желают быть зависимыми от системных DMA контроллеров, содержат собственные аппаратные средства обеспечения DMA. Так как эти средства принадлежат собственно устройству, то передача происходит по "воле" устройства — если, разумеется, протокол шины поддерживает такие действия. Для выполнения своей операции устройству необходимо получить "контроль над шиной" в результате процедуры, которая описывается в протоколах соответствующих шин.
Операции над строками ANSI символов
Работа со строками символов ANSI (8-битными) более привычна для программистов. Такие средства тоже присутствуют в наборе встроенных функций пакета и в наборе системных вызовов RtlXxx.
Строками простых 8-битных символов, в которых окончание обозначено нулем (так называемые строки SZ, String, Zero end), можно манипулировать при помощи уцелевших встроенных функций strlen, memcpy, strcpy, strncpy, strcat и даже strstr. Набор функций, которыми можно попытаться воспользоваться, приведен в файле DDK XP в заголовочном файле inc\crt\string.h, хотя в официальной документации DDK они не упоминаются вовсе, так что к ним следует применять правила использования по-умолчанию. Например, не обращаться к строкам, размещенным в страничной памяти, на высоких уровнях IRQL и т.п. Другие функции, например, мощную sprintf или хотя бы atoi, компилятор DDK не предоставляет.
Вариант счетной строки обычных 8-битных символов вводится в пакете DDK как тип данных ANSI_STRING. Это аналогичная UNICODE_STRING структура данных, в которой собственно строка хранится по адресу, указанному в поле Buffer, как уже было показано выше. Длину строки можно взять из поля Length. Хотя в большинстве случаев вполне успешно проходит обращение со строкой по адресу Buffer, как если бы она была строкой SZ (заканчивающейся нулем), однако, строго говоря, надеяться на это не следует, поскольку документация DDK не указывает, что на это можно вполне рассчитывать.
Можно ли перейти от строки UNICODE_STRING к обычной строке 8-битных символов? Можно, но при этом произойдет потеря информации о языковой локализации. Ниже показан несложный способ, как перейти к счетной строке ANSI, а затем и к обычной строке SZ.
// Полагаем, что уже существует счетная строка UNICODE_STRING // по адресу ptrToUNICODE_STRING ANSI_STRING ansiStr; // стековая переменная (не инициализирована) char str[100];// стековая переменная
NTSTATUS stat = // инициализируем строку ANSI RtlUnicodeStringToAnsiString( &ansiStr, ptrToUNICODE_STRING, TRUE );
if(NT_SUCCESS(stat)) { // Теперь поле ansiString. Buffer указывает на выделенный буфер, // куда помещена строка ANSI символов, полученная из строки // UNICODE_STRING, изначально находившейся по адресу, куда // указывает переменная ptrToUNICODE_STRING. // Поле ansiStr.Length уже содержит информацию о длине строки. int len= (ansiStr.Length > 99? 99: ansiStr.Length); strncpy(str, ansiStr.Buffer, len); str[len+1]=0;
DbgPrint("SZ string = %s(len=%d).", str, len);
// Освобождаем буфер, выделенный под хранение ANSI строки: RtlFreeAnsiString(&ansiStr); }
Заметим, что переменная str будет хранить строку 8-битных символов до выхода из данного программного модуля, поскольку str — локальная переменная.
Прототип вызова функции RtlUnicodeStringToAnsiString описан в таблице ниже. В том случае, если третий параметр указан как TRUE, то будет выполнено выделение памяти для хранения строки, полученной из строки UNICODE_STRING. Соответственно, по окончании использования строки ANSI_STRING следует освободить числящуюся за драйвером область памяти, что выполняется вызовом RtlFreeAnsiString.
Таблица 7.38. Прототип вызова RtlUnicodeStringToAnsiString
NTSTATUS RtlUnicodeStringToAnsiString | IRQL == PASSIVE_LEVEL |
Параметры | Преобразует информацию строки UNICODE_STRING в строку ANSI_STRING |
IN OUT PANSI_STRING ansiStringPtr | Указатель на строку ANSI_STRING, которая должна быть получена в результате вызова |
IN PUNICODE_STRING Source | Указатель на исходную строку UNICODE_STRING |
BOOLEAN AllocFlag | TRUE — выделить память под буфер строки ANSI_STRING |
Возвращаемое значение | STATUS_SUCCESS или код ошибки |
Операции над строками UNICODE_STRING
Операционная система Windows NT издавна ориентирована на использование так называемых "широких" символов (занимающих два байта), что, в отличие от символов ASCII, o которых будет сказано ниже, без особых затруднений обеспечивает поддержку всех типов алфавитов, включая поддержку языков Юго-Восточной Азии.
Собственно тип данных UNICODE_STRING описывается в пакете DDK следующим образом (см. заголовочный файл ntdef.h):
typedef struct _UNICODE_STRING { USHORT Length; // Длина строки (в двухбайтных символах) USHORT MaximumLength; // Максимально возможная длина строки PWSTR Buffer; // указатель на буфер с двухбайтными символами } UNICODE_STRING, *PUNICODE_STRING;
Иногда UNICODE_STRING определяется выражением "counted string", что довольно точно передает сущность этого типа данных, то есть — "счетная строка", строка, где поддерживается учет действующих символов.
Нетрудно догадаться, что у программистов, долгое время привыкавших к простым ASCII кодировкам и столь же несложным функциям, оперирующим ASCIIZ строками, переход к использованию кодировки Юникод не вызовет энтузиазма.
Тем не менее, при работе в режиме ядра Windows NT это совершенно необходимый инструмент. (Правда, функции отладочной диагностики типа DbgPrint
позволяют пользоваться строками в прежней манере.)
Работа с типом UNICODE_STRING покажется менее сложной, если смириться с тем, что не следует "трогать его руками" (примерно, как CString в MFC), и научиться использовать набор системных функций, предназначенный для работы с ним.
Прежде всего, простая буква 'L', примененная перед строкой символов, дает указание препроцессору, трактовать эту строку как строку "широких" символов (строку WCHAR, но еще — не UNICODE_STRING). Соответственно, перейти от обычного текста, набираемого на клавиатуре компьютера, к строке UNICODE_STRING можно так:
UNICODE_STRING myNewUString; RtlInitUnicodeString( &myNewUString, L"My Unicode text example." );
В следующем примере посредником при инициализации UNICODE_STRING выступает тип данных ANSI_STRING (счетная строка однобайтных символов):
ANSI_STRING myNewANSIString; UNICODE_STRING myNewUString; RtlInitAnsiString( &myNewUString, "My second text example." ); RtlAnsiStringToUnicodeString(&myNewUString, &myNewANSIString, TRUE);
Третий параметр, указанный как TRUE, заставляет данную функцию выделять память под буфер двухбайтных символов. Соответственно, по окончании работы с UNICODE_STRING (а в данном случае — при выходе из текущей функции, поскольку myNewUString определена как локальная переменная) следует выполнить освобождение буфера двухбайтных символов вызовом RtlFreeUnicodeString. To же необходимо проделать и в первом примере. Более того, аналогичное требование и к типу данных ANSI_STRING, для которого следует использовать RtlFreeAnsiString.
Сам тип ANSI_STRING определяется следующим образом (см. ntdef.h):
typedef struct _STRING { USHORT Length; USHORT MaximumLength; PCHAR Buffer; // Здесь 'PCHAR' - просто 'char' указатель } STRING, *PSTRING; typedef STRING ANSI_STRING;
Очевидно, второй способ более трудоемкий, и им редко кто пользуется.
После того как экземпляр UNICODE_STRING получен, над ним можно выполнять разнообразные операции. Предназначенные для этого системные функции описываются ниже.
Таблица 7.33. Прототип вызова RtlAppendUnicodeStringToString
NTSTATUS RtlAppendUnicodeStringToString | IRQL — любой (если это допускает тип памяти буферов двухбайтных символов) |
Параметры | Объединяет строки UNICODE_STRING |
IN OUT PUNICODE_STRING Destination | Указатель на строку-получатель |
IN OUT PUNICODE_STRING AppendString | Указатель на присоединяемую строку |
Возвращаемое значение | STATUS_SUCCESS — строка присоединена и длина строки получателя обновлена STATUS_BUFFER_TOO_SMALL — слишком мал размер буфера двухбайтных символов строки-получателя |
Если требуемый размер буфера не составит труда вычислить как
(Destination->Length + AppendString->Length) * sizeof(WCHAR)
то расширение буфера строки-получателя — задача несколько более сложная. Хотя логично было бы иметь соответствующую системную функцию, однако в документации DDK o подобном вызове нет никакого упоминания.
Таблица 7.34. Прототип вызова RtlCompareUnicodeString
LONG RtlCompareUnicodeString | IRQL == PASSIVE_LEVEL |
Параметры | Выполняет сравнение строк UNICODE_STRING |
IN PUNICODE_STRING pString1 | Указатель на первую строку |
IN PUNICODE_STRING pString2 | Указатель на вторую строку |
BOOLEAN CaseInSensitive | TRUE — игнорировать регистр (верхний/нижний) |
Возвращаемое значение | 0 — строки идентичны < 0 — строка 1 меньше строки 2 |
BOOLEAN RtlEqualUnicodeString | IRQL == PASSIVE_LEVEL |
Параметры | Выполняет сравнение строк UNICODE_STRING |
IN PUNICODE_STRING pString1 | Указатель на первую строку |
IN PUNICODE_STRING pString2 | Указатель на вторую строку |
BOOLEAN CaseInSensitive | TRUE — игнорировать регистр (верхний/нижний) |
Возвращаемое значение | TRUE — строки идентичны FALSE — строки различаются |
NTSTATUS RtlInt64ToUnicodeString | IRQL == PASSIVE_LEVEL |
Параметры | Преобразует число int64 в UNICODE_STRING |
IN ULONGLONG Value | Исходное число |
IN ULONG Base | Формат: 16 — шестнадцатеричный, 8 — октавный, 2 — двоичный, 0 или 10 — десятичный. |
IN OUT PUNICODE_STRING s | Строка UNICODE_STRING |
Возвращаемое значение |
STATUS_SUCCESS STATUS_BUFFER_OVERFLOW — слишком мал размер буфера UNICODE_STRING STATUS_INVALID_PARAMETER — ошибочен параметр Base |
и RtlIntPtrToUnicodeString, соответственно.
Таблица 7.37. Прототип вызова RtlUpcaseUnicodeString
NTSTATUS RtlUpcaseUnicodeString | IRQL == PASSIVE_LEVEL |
Параметры | Преобразует все символы строки src в символы верхнего регистра |
IN OUT OPTIONAL PUNICODE_STRING dest | Указатель на строку с буфером, подготовленным для приема преобразованной строки или NULL (в последнем случае преобразование происходит по месту) |
IN OUT PUNICODE_STRING src | Исходная строка |
IN BOOLEAN AllocateDstStringBuff | Если TRUE — выделить буфер под результат преобразования (в этом случае его следует освободить вызовом RtlFreeUnicodeString по окончании работы с этой строкой) |
Возвращаемое значение |
STATUS_SUCCESS STATUS_BUFFER_OVERFLOW — слишком мал размер буфера UNICODE_STRING STATUS_INVALID_PARAMETER — ошибочен параметр Base |
Операции с памятью
Операционная система Windows оперирует тремя типами адресов:
Виртуальные адреса, которые транслируются в физические адреса перед доступом к области памяти.
Физические адреса, которые реально указывают в область физической памяти. Следует отметить, что по этим адресам содержимое памяти всегда появляется на шине доступа к памяти, независимо от того, реально ли она присутствует в ОЗУ, или содержится внутри обслуживаемых драйвером устройств.
Логические адреса. Этот тип описывает специальные адреса, используемые уровнем HAL при общении с устройствами. Соответственно, уровень HAL и отвечает за операции с этими адресами.
Работа по программированию в режиме ядра всегда связана с тонкостями работы с памятью. В каком контексте работает программный поток, какого типа памятью он манипулирует (пользовательской или режима ядра), какого типа память (страничная или нестраничная) используется, если идет работа с памятью режима ядра, наконец, приемлем ли текущий уровень приоритета IRQL для доступа к данному типу памяти? Разумеется, все эти вопросы возникают, только лишь, если разрабатывается код режима ядра.
Адреса 4 гигабайтного виртуального пространства памяти 32-разрядных версий операционной системы Windows NT 5 (об отличиях для 64-разрядных версий было сказано в главе 4) делятся на 2 нижних гигабайта памяти пользовательского виртуального пространства, имеющего смысл только в контексте пользовательского приложения (процесса), которому оно выделено, и 2 верхних гигабайт системного виртуального пространства режима ядра. Системное адресное пространство доступно всем программным потокам режима ядра. (Иначе, как смогло бы работать программное обеспечение режима ядра собственно операционной системы?!) Все 4-х гигабайтное адресное пространство можно представить в виде книги с одной обложкой. Толстая обложка — это системное адресное пространство, тонкие бумажные листы — это виртуальные и автономные пользовательские адресные пространства.
Системное виртуальное пространство памяти режима ядра делится на диапазоны (обычная архитектура x86), представленные в таблице 7.1.
Адреса с 0xC0000000 по 0xC0800000 используются для хранения данных Менеджера памяти, который поддерживает механизм виртуальной памяти.
Диапазон с 0xFFBE0000 по 0xFFC00000 используются для хранения информации о страничном файле (файле подкачки), которая используется для сброса содержимого физической памяти в этот файл. (Методология crash дампа предусматривает создание crash dump файла из этого файла подкачки при следующей загрузке системы.)
Адреса с 0xE1000000 по 0xFFBE0000 занимают области странично и нестранично организованной памяти, что вместе составляет менее 500 мегабайт.
Какие трюки можно проделывать с виртуальной памятью? На этот не слишком конкретный вопрос существует ответ в виде вопроса: а для достижения чего именно?
Рассмотрим довольно незамысловатую ситуацию, предпосылки которой рассматривались ранее. Предположим, драйвер создал программный поток (вызовом PsCreateSystemThread), который в некоторой ситуации должен выполнять некоторую работу, например, по сигналу функции обработки IOCTL запросов — выполнить перенос данных в буфер, предоставленный пользовательским приложением. Предположим, что разработчик драйвера так задал IOCTL код (используя метод буферизации NEITHER), что в драйвер поступает пользовательский адрес буфера (значение меньше 0x80000000). Разработчик драйвера через внутренние переменные передает этот адрес программному потоку, который должен выполнить перенос, и... Наступает сбой системы.
Что случилось? Чтобы объяснить сложившуюся ситуацию и решить проблему, необходимо, прежде всего, согласиться с тем, что простые приемы пользовательского режима следует оставить пользовательскому режиму.
Когда драйверная функция обработки IOCTL запросов пользовательского режима (по вызову DeviceIoControl) получает адрес пользовательского буфера по методу буферизации NEITHER, то этот пользовательский виртуальный адрес имеет смысл в этой функции, поскольку она работает в контексте пользовательского программного потока и интерпретация пользовательского виртуального адреса не вызовет проблем.
Другое дело, если этот адрес окажется в распоряжении программного потока, созданного драйвером по вызову PsCreateSystemThread, где интерпретация данного адреса вызовет ошибку, поскольку, как указано в документации DDK, этот системный программный поток не имеет никакого пользовательского контекста. Это одна из проблем.
|
Таблица 7.1. Диапазоны памяти системного адресного пространства Windows 2000 |
Чтобы решить задачу в данной постановке необходимо, во-первых, создать список MDL (структуру, хранящую отображение блока виртуальной памяти на физическую память), зафиксировать пользовательский буфер в физической оперативной памяти и передать MDL список (или соответствующий виртуальный адрес системного адресного пространства) рабочему потоку. По выполнении работы, следует разблокировать страницы пользовательского буфера и освободить структуру MDL списка. Соответственно, при этом делаются вызовы системных функций: IoAllocateMdl, MmProbeAndLockPages, MmGetSystemAddressForMdl, MmUnlockPages и IoFreeMdl.
Вызов IoAllocateMdl создает структуру MDL списка для указанного виртуального адреса (пользовательского или системного адресного пространства) с указанной длиной.
Вызов MmProbeAndLockPages проверяет присутствие страниц в физической памяти, подгружает (для страничной памяти), если они отсутствуют, и производит их фиксацию (после чего данные страницы не могут быть сброшены в страничный файл на жестком диске). Для корректного вызова MmProbeAndLockPages
по поводу MDL, составленного для страничной памяти, программный поток должен работать на уровне IRQL ниже DISPATCH_LEVEL — чтобы позволить отработать системному коду, если страница окажется в страничном файле. (В случае, если исходный буфер находился бы в нестраничной памяти, то данный вызов можно было бы сделать с уровня IRQL равном DISPATCH_LEVEL или ниже.)
Функция MmGetSystemAddressForMdl возвращает виртуальный адрес, вычисленный из MDL списка, так, будто рассматриваемая область памяти находится в системном адресном пространстве, а именно — в нестраничном пуле. Этот адрес можно использовать в любом месте кода драйвера на любых уровнях IRQL, даже в процедуре обработки прерываний. Контекст выполнения для этого адреса не имеет никакого значения. (Справедливости ради, следует отметить, что перевод в системный адрес не является обязательной операцией, можно и далее использовать MDL список, что позволяют делать, в частности, вызовы нижних драйверов в стеке.) Вызов (точнее, макроопределение) MmGetSystemAddгessForMdl являeтcя устаревшим, и его следует использовать только в WDM драйверах, предназначенных для работы еще и в Windows 98. Использование макроопределения MmGetSystemAddressForMdlSafe
является предпочтительным. Оба эти макроопределения могут быть вызваны из кода, работающего на уровне IRQL не выше DISPATH_LEVEL.
Вызов MmUnlockPages отменяет фиксацию страниц в оперативной памяти, а вызов IoFreeMdl уничтожает структуру MDL списка.
Чтобы отследить ошибки, связанные с фиксацией блока виртуальной памяти в памяти оперативной, рекомендуется выполнять вызов MmProbeAndLockPages
внутри try-except блока, например:
__try { MmProbeAndLockPages( pMdl, UserMode, IoModifyAccess); } __except( EXCEPTION_EXECUTE_HANDLER) { pIrp->IoStatus.Status = STATUS_ACCESS_VIOLATION; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_CONFLICTING_ADDRESSES; }
На рассмотренном выше примере видно, что операционная система предоставляет достаточно инструментов для преобразования адресов пользовательского пространства в адреса системного пространства и физические адреса. Важно только корректно отслеживать, когда и что следует использовать.
Ниже приводятся более полные сведения о системных функциях, которые полезны для работы с виртуальной и физической памятью.
Операции с плавающей точкой
Редко, но все-таки случаются ситуации, когда необходимо использовать операции с плавающей точкой и, соответственно, задействовать сопроцессор (FPU). Особенность этой ситуации состоит в том, что в режиме ядра программный поток, намеревающийся использовать сопроцессор, обязан сохранить текущее состояние всех регистров сопроцессора (ST0-ST7, MMX0-MMX7, XMM0-XMM7). Для сохранения этой информации в наборе системных вызовов режима ядра предусмотрены функции KeSaveFloatingPointState
и KeRestoreFloatingPointState, первая из которых сохраняет состояние регистров сопроцессора в предоставляемом буфере, описываемом типом KFLOATING_SAVE, а вторая выполняет восстановление состояния сопроцессора по окончании работы с ним. Пример кода для работы с сопроцессором в режиме ядра приводится ниже:
double Xfloat; NTSTATUS status; KFLOATING_SAVE savedFPUData; status = KeSaveFloatingPointState(&savedFPUData);
if (NT_SUCCESS(status)) {// Работа с сопроцессором Xfloat = 0.; . . . . . . . . . . // Восстановление состояния сопроцессора из savedFPUData : KeRestoreFloatingPointState(&savedFPUData); }
Следует обратить внимание на то, что работа с сопроцессором выполняется только в случае, если удачно завершен вызов функции KeSaveFloatingPointState, которая должна вызываться на уровне IRQL не выше DISPATCH_LEVEL. Вызов функции KeRestoreFloatingPointState для восстановления состояния сопроцессора должен выполняться на том же уровне, что было выполнено сохранение.
Определение размещения при компиляции
Для того чтобы драйверные процедуры оказались после загрузки в памяти определенного типа, можно использовать директивы указания компилятору #pragma.
#pragma code_seg("INIT") <программный текст> #pragma code_seg()
#pragma code_seg("PAGE") <программный текст> #pragma code_seg()
Здесь первая строка вводит программный код категории INIT. Этот код, подобно сгоревшей ступени ракеты, растворится в небытии сразу по окончании инициализации драйвера (работы драйверной процедуры DriverEntry). Традиции такого кода восходят еще ко временам операционной системы DOS, когда малый размер драйвера был его важнейшим достоинством.
Директива '#pragma code_seg()' восстанавливает правила по умолчанию.
Директива '#pragma code_seg("PAGE")' обеспечивает размещение кода в областях странично организованной памяти.
Все остальные процедуры драйвера размещаются в областях нестранично организованной памяти (действие по умолчанию).
Аналогичным образом директивы компилятора применимы и к сегментам (секциям) данных, см. два примера ниже.
#pragma data_seg("INIT") <описание переменных> #pragma data_seg()
#pragma data_seg("PAGE") <описание переменных> #pragma data_seg()
Другой способ добиться того же самого для отдельных функций, применяя другую форму синтаксиса, представлен ниже.
#ifdef ALLOC_PRAGMA #pragma alloc_text( "INIT", DriverEntry ) #pragma alloc_text( "PAGE", MyUnloadProcedure ) #endif
В приведенном выше фрагменте процедура DriverEntry будет отнесена к категории INIT, а процедура MyUnloadProcedure будет размещена в станичной памяти.
Основные процедуры драйвера
В примере драйвера Example.sys, представленном в главе 3, были кратко представлены основные процедуры, наличие которых обязательно для драйвера. Общий взгляд на структуру драйвера (6 глава) дал более полное представление о наборе и назначении этих функций. Теперь рассмотрим эти процедуры с технической точки зрения — как они вызываются, какие параметры получают, какие значения должны возвращать вызвавшему их Диспетчеру ввода/вывода.
Основные сведения об аппаратном обеспечении
Несмотря на великое многообразие типов и областей применения устройств, потребность в подключении которых к компьютеру возникает на практике, можно выделить несколько общих черт, в курсе которых необходимо быть разработчику драйвера.
Поддерживает ли устройство спецификацию PnP и как устройство может известить систему о своем существовании при подключении?
Как происходит доступ к регистрам состояния и контроля устройства?
Как устройство может генерировать сигнал прерывания?
Как устройство участвует в передаче данных?
Использует ли устройство какую-либо встроенную память?
Как устройство конфигурируется и какэто можно сделать программно?
Особенности механизма DPC
Как правило, работа с отложенными процедурными вызовами не является сложной, поскольку операционные системы Windows 2000/XP/Server 2003 предлагают большой набор системных вызовов, скрывающих большую часть деталей этого процесса. Тем не менее, особо следует выделить два наиболее обманчивых момента в работе с DPC.
Во-первых, Windows NT 5 устанавливает ограничение, состоящее в том, что один экземпляр DPC объекта может быть помещен в системную DPC очередь в определенный временной промежуток. Попытки поместить в очередь DPC объект, в точности совпадающий с уже там присутствующим, отвергаются. В результате, происходит только один вызов DPC процедуры, даже если драйвер ожидает выполнение двух вызовов. Это может произойти, если два прерывания были вызваны обслуживаемым устройством, а обработка первого отложенного процедурного вызова еще не начиналась. Первый экземпляр DPC объекта еще пребывает в очереди, в то время как драйвер уже приступил к обработке второго прерывания.
Конструкция драйвера должна предусматривать такой ход работы DPC механизма. Возможно, следует предусмотреть дополнительно счетчик DPC запросов, либо драйвер может реализовать собственную реализацию очереди запросов. В момент выполнения реальной отложенной процедуры можно проверять счетчик и собственную очередь запросов для того, чтобы определить, какую конкретно работу следует выполнять.
Во-вторых, существуют значительные синхронизационные проблемы при работе на многопроцессорных платформах. Предположим, программный код, выполняемый на одном процессоре, выполняет обслуживание прерывания и планирует вызов DPC процедуры (размещая DPC объект в системной очереди). Тем не менее, еще до момента полного завершения обработки прерывания, другой процессор может начать обработку DPC объекта, помещенного в системную очередь. Таким образом, возникает ситуация, в которой программный код обслуживания прерываний выполняется параллельно и одновременно с кодом DPC процедуры. По этой причине необходимо предусмотреть меры по надежной синхронизации доступа программного кода DPC процедуры к ресурсам, используемым совместно с драйверной процедурой обслуживания прерывания.
Если присмотреться к списку параметров вызовов IoInitializeDpcRequest
и IoRequestDpc (предназначенных для работы с DpcForIsr процедурами), то нетрудно заметить, что DPC объект "привязан" к объекту устройства. При размещении этого объекта в DPC очереди в момент работы ISR процедуры также указывается объект устройства. Этим и достигается определенность, вызов какой конкретно DPC процедуры "заказан" ISR процедурой (соотнесение по объекту устройства). Это же говорит о том, что драйвер, который создал несколько объектов устройств (достаточно редкий случай), может эксплуатировать и несколько процедур DpcForIsr — по одной для каждого объекта устройства.
Системный DPC механизм предотвращает возможность одновременной обработки DPC объектов из системной очереди, даже в условиях многопроцессорной конфигурации. Таким образом, если ресурсы используются совместно несколькими отложенными процедурами, то нет необходимости заботиться о синхронизации доступа к ним.
Выше было рассмотрено использование DPC процедур для завершения обработки прерываний, то есть DpcForIsr. Однако DPC процедуры можно использовать и в другом ключе, например, совместно с таймерами для организации ожидания. Для этого создастся объект DPC при помощи вызова KeInitializeDPC, который связывает этот объект с DPC процедурой, входящей в состав драйвера. После этого можно выполнять установку времени ожидания в предварительно инициализированном (используя KeInitializeTimer или KeInitializeEx) объекте таймера. Для установки интервала ожидания используется вызов KeSetTimer, которому в качестве одного из параметров необходимо передать указатель на инициализированный DPC объект. Пo истечении интервала ожидания DPC объект будет внесен в системную DPC очередь, и DPC процедура, ассоциированная с ним, будет вызвана так скоро, насколько этo будет возможно. Процедуры DPC данного, второго, типа обозначены в документации DDK термином 'Custom DPC'. (Этот вариант использования DPC процедур делает их весьма напоминающими APC вызовы пользовательского режима.) |
Для очистки системной DPC очереди от Custom DPC процедур, например, если драйвер должен срочно завершить работу, предназначен вызов KeRemoveQueueDpc, который может быть вызван из кода любого уровня IRQL.
Отключение от источника прерываний
В случае, если драйвер должен обладать возможностью быть выгружаемым, то имеется насущная необходимость его отключения от источника прерываний. При этом он удаляется из внутреннего списка кода ядра, где он обозначен как обработчик прерываний. Разумеется, это необходимо выполнить до того, как драйвер будет удален из оперативной памяти. В противном случае код ядра операционной системы в ответ на прерывание, сгенерированной устройством, осуществит вызов процедуры по адресу нестраничном пуле, где раньше "обитала" процедура ISR. Это неминуемо приведет к краху системы.
Отключение от источника прерываний является двухступенчатой процедурой. Во-первых, следует использовать KeSynchronizeExecution и процедуру SynchCritSection для того, чтобы обеспечить такое состояние устройства, когда он не будет производить генерацию сигналов на прерывание. Во-вторых, следует произвести удаление ISR процедуры из системного списка обработчиков прерываний путем осуществления вызова IoDisconnectInterrupt (с передачей этому вызову в качестве аргумента указателя на полученный ранее объект прерывания для данного устройства).
Таблица 8.15. Прототип вызова IoDisconnectInterrupt
VOID IoDisconnectInterrupt | IRQL == PASSIVE_LEVEL |
Параметры | Регистрирует DpcForIsr процедуру для данного объекта устройства |
IN PKINTERRUPT pInterruptObject | Указатель на объект прерывания, полученный ранее в результате вызова IoConnectInterrupt |
Возвращаемое значение | void |
Нерассмотренным остается один весьма деликатный момент. При регистрации ISR процедуры для обслуживания конкретного прерывания используется вызов IoConnectInterrupt, подробно описанный в таблице 8.10. Наиболее важным и трудным в обращении является параметр Vector, представляющий транслированное прерывание, к которому и производится подключение регистрируемой ISR процедуры. Данная процедура имеет свою специфику для каждого типа не-WDM драйверов (в зависимости от того, к шине какого типа подключено устройство, обслуживаемое драйвером, например, ISA или PCI). Однако для WDM драйверов эта процедура универсальна. В данном случае, когда PnP Менеджер делает запрос с кодом IRP_MJ_PNP и субкодом IRP_MN_START_DEVICE, то в каждом таком запросе он передает и список присвоенных драйверу ресурсов. |
Драйвер WDM, например, из пакета разработки устройств PCI шины фирмы PLX Technology, выходит из положения следующим образом.
Прежде всего, данный драйвер, получив IRP пакет с указанными признаками (IRP_MJ_PNP, субкодом IRP_MN_START_DEVICE), дает возможность сначала обработать его нижележащим драйверам, перехватывая его на обратном пути. После этого он производит анализ полученных ресурсов, в частности, подключает собственную функцию обработки прерываний PlxOnInterrupt к прерыванию, как оно было предопределено PnP Менеджером, передавшим выделенные ресурсы в ячейке стека пакета IRP.
int i, count; BOOLEAN interruptPresented = FALSE; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation( pIrp ); PCM_PARTIAL_RESOURCE_LIST pRawResource= &stack->Parameters.StartDevice.AllocatedResources-> List[0].PartialResourceList; count = pRawResource->Count; for (i = 0; i < count; i++, pRawResource++) { //Просмотр всех выделенных драйверу ресурсов switch (pRawResource ->Type) { case CmResourceTypeInterrupt: // Прерывание interruptPresented = TRUE; IrqL = (KIRQL) Resource->u.Interrupt.Level; vector = Resource->u.Interrupt.Vector; affinity = Resource->u.Interrupt.Affinity; if (ResourceRaw->Flags == CM_RESOURCE_INTERRUPT_LATCHED) mode = Latched; else mode = LevelSensitive; break; . . . } } if (interruptPresented) { // Здесь следует запретить прерывания от ведомого устройства . . . // Создание объекта прерывания и подключение к нему status = IoConnectInterrupt( &pDevExtension->pInterruptObject, PlxOnInterrupt, pDevExtension, NULL, vector, IrqL, IrqL, mode, TRUE, affinity, FALSE ); if ( !NT_SUCCESS(status) ) { // обработка ошибки } else { // разрешить прерывания от устройства } }
Здесь pDevExtension указывает на структуру расширения объекта устройства (соответственно, эта структура должна предусмотреть в своем составе место для хранения указателя на создаваемый объект прерывания, здесь pInterruptObject), a PlxOnInterrupt передает адрес предоставляемой данным драйвером ISR процедуры, созданной в соответствии с прототипом, описанным в таблице 8.11.
Отладчик SoftIce
Отладку драйвера режима ядра на одном компьютере обеспечивает мощный отладчик SoftIce, разработанный фирмой CompuWare Corporation и входящий в состав пакета DriverStudio. Внешний вид интерфейса отладчика сохранился еще со времен MS DOS, однако, при некоторой тренировке с использованием мнемонических команд, вряд ли это доставит много неудобств, тем более что SoftIce управляется мышкой, включая мышку с колесиком.
Параметры запуска устанавливаются конфигуратором DSConfig, который определяет способ запуска SoftIce (при загрузке, по СОМ, по IP адресу, запуск по требованию на данном компьютере), настройки размера окна, настройки мышки.
При настройке загрузки по требованию следует явно запустить из меню команд Пуск -... - Start SoftIce. Данная команда загружает отладчик, а собственно его активация производится комбинацией Ctrl-D.
В том случае, если нужно выполнить отладку драйвера, после загрузки SoftIce следует загрузить файл драйвера (для отладки в исходных текстах — обязательно отладочную версию) программой loader32 командой Пуск — ... — Symbol Loader. Лишь после этого следует активировать отладчик нажатием Ctrl-D.
При помощи команды h в окне активированного отладчика можно ознакомиться с полным набором и форматом команд. Это же можно узнать и из прилагаемой документации.
При помощи определенных команд несложно отыскать драйвер в памяти, если известно имя драйвера или объекта устройства, после чего можно устанавливать точки прерывания в коде драйвера. Затем следует деактивировать отладчик повторным нажатием Ctrl-D. В момент, когда приложение пользовательского режима вызовет драйвер и, соответственно, достигнет указанной точки прерывания, отладчик активизируется самостоятельно. В окне отладчика будет виден нужный фрагмент драйвера.
В то время, когда активно окно отладчика SoftIce, активность операционной системы замирает: не обновляются показания часов, не работает обмен через clipboard и т.п.
Следует отметить, что при работе с SoftIce не следует использовать сложные мышки (например, четырехкнопочные, с двумя колесиками) и мышки USB. Объясняется это тем, что, являясь настоящим отладчиком режима ядра, SoftIce пытается работать с устройствами ввода напрямую, не всегда понимая новые экзотические устройства.
Более детально ознакомиться с отладчиком SoftIce можно, загрузив trial-версию с Интернет сайта CompuWare Corporation.
Отладчик WinDbg
Одним из отладчиков, который может использовать разработчик драйверов, является отладчик WinDbg, поставляющийся в составе пакета DDK. Отладчик WinDbg представляет собой гибрид отладчика уровня ядра и пользовательского режима. При помощи WinDbg можно проводить анализ файлов "crash dump files" (отображение физической памяти в момент сбоя), отлаживать драйверный код путем трассирования (исполнения), анализировать "dump file" приложений (пользовательского режима).
Помимо того, что WinDbg выполняет свои задачи и в режиме ядра, и в пользовательском режиме, он также предоставляет рабочий интерфейс и в графической форме, и в форме командной строки, характерной для старых отладчиков.
Одной из самых значительных возможностей, которой обладает отладчик WinDbg, является его способность поддерживать отладку компонентов уровня ядра в исходных текстах отлаживаемых модулей. При этом отладчику должны быть доступны и исходные тексты, и файлы с отладочными символами (далее они будут называться "файлы идентификаторов").
Тем не менее, отладка в исходных текстах представляет достаточно существенную организационную проблему.
Во-первых, для отладки драйвера требуется два компьютера, на одном из которых работает собственно отладчик, а на втором — отладочный агент, относительно небольшой объем кода, который получает управляющие команды с первого компьютера.
Во-вторых, для проведения отладки потребуется отладочная версия операционной системы, для которой имеются соответствующие файлы отладочных символов.
Сумма этих затруднений может перевесить чашу весов в пользу других отладчиков, в особенности, отладчика SoftIce фирмы CompuWare Corporation (разумеется, если не принимать в рассмотрение цену полной версии пакета Driver Studio).
Отличия между версиями
Отличия версий Windows 2000 (NT 5.0), XP (NT 5.1) и Server 2003 (NT 5.2), разумеется, существуют. И касаются они не только пользовательского интерфейса и наполнения сервисными программами, но и наборов системных вызовов, предоставляемых в режиме ядра. Тем, кого интересует большее количество деталей, нежели будет представлено далее, можно порекомендовать три статьи, размещенные сегодня в Интернете:
статья Марка Руссиновича и Дэвида Соломона "Windows XP: Kernel Improvements Create a More Robust, Powerful, and Scalable OS", размещенная на сайте Microsoft по адресу msdn.microsoft.com/msdnmag/issues/01/12/XPKernel/default.aspx;
статья Марка Валла и Роберта Вильямса "Windows .NET Structure and Architecture", размещенной в Интернет-журнале "Network Windows & .NET Magazine" по адресу windowsitlibrary.com/Content/717/02/toc.html;
статья "Compare the Editions of Windows Server 2003" (Standard, Enterprise, Datacenter & Web Edition), размещенная на Интернет-сайте Microsoft по адресу microsoft.com/windowsserver2003/evaluation/features/compareeditions.mspx.
Не вдаваясь в подробности, отметим наиболее важные для разработчика драйверов отличия. Прежде всего, следует различать 32 и 64-разрядные клоны. Если 64 разрядная Windows 2000 была собрана лишь один раз в тестовом режиме, то Windows XP (и уж тем более, Server 2003) в 64-разрядной сборке представляет собой реальный коммерческий продукт. Организация адресного пространства памяти в 64 разрядной версии сильно отличается от организации виртуального пространства в 32-разрядном исполнении (таблица 7.1). Приложения, созданные как 32-разрядные программы, запускаются под управлением подсистемы Win32 в рамках модели WOW (Windows On Windows 64), аналогично тому, как 16-разрядные приложения работают в 32-разрядной среде.
Драйвер в 64 разрядной версии компилируется как 64-разрядный код — в частности, с соответствующей длиной указателей. В данной ситуации драйверу актуально знать, от какого приложения он получил запрос — от нового 64-разрядного или старого 32-разрядного.
Эта проблема не представляет большого затруднения, поскольку 64-разрядный драйвер может выяснить тип вызывающего процесса при помощи системной функции IoIs32bitProcess, доступной для применения в 64-разрядных сборках драйверов. Пример приводится ниже.
if( IoIs32bitProcess(Irp) == TRUE ) { #if DBG DbgPrint("IRP request is made by 32 bit process."); #endif } else { #if DBG DbgPrint("IRP request is made NOT by 32 bit process."); #endif }
В соответствии с ответом драйвер может скорректировать некоторые свои операции.
С какой конкретно версией операционной системы драйвер имеет дело, можно легко определить по тому, какую версию модели WDM поддерживает система. Это можно узнать с помощью системного вызова IoIsWdmVersionAvailable, например:
if (IoIsWdmVersionAvailable(l, 0x30)) { // Windows Server 2003 } else if (IoIsWdmVersionAvailable(l, 0x20)) { // Windows XP } else if (IoIsWdmVersionAvailable(l, 0x10)) { // Windows 2000 } else if (IoIsWdmVersionAvailable(l, 0x05)) { // Windows Me } else { // Windows 98 }
Тем не менее, пока что широкое использование в России дорогих 64-разрядных конфигураций не предвидится, что можно с некоторой степенью уверенности констатировать на основании неширокого применения в прошлом и настоящем относительно "продвинутых" процессоров XEON.
Сосредоточим внимание на 32-разрядных версиях. Среди них произошли следующие изменения.
При переходе от Windows 2000 к Windows XP переписан загрузчик NTLDR (см. Приложение Б), в результате чего процесс загрузки ускорился в 4-5 раз.
Модуль NTOSKRNL.EXE Windows 2000 экспортировал 1129 функций (большинство из которых — системные вызовы, доступные из драйверов режима ядра), Windows XP экспортирует 1464, a Windows Server 2003 — уже 1525.
Модуль HAL.DLL экспортирует, соответственно, 95, 92 и 92 вызова. Причем, по косвенным признакам заметно, что от версии 2000 к XP его постигла значительная переработка, а от версии XP к Server 2003 — лишь процесс "шлифовки".
Пополнение системных вызовов в Server 2003 произошло, в основном, за счет вызовов с суффиксом Ex, то есть функций в чем-то расширяющих существующие системные вызовы Windows XP. Например, IoCsqInitializeEx получился из вызова IoCsqInitialize.
При переходе от Windows 2000 к Windows XP были смягчены многие ограничения на предельные размеры.
Операционная система Windows 2000 ограничивала общий размер адресного пространства под драйверами 220 Мбайтами, в XP этот предел отодвинут до 960. Таким же он остался и в 32 разрядной версии Server 2003.
Предельный общий размер файлов Системного Реестра Windows XP жестко не фиксирован, в то время как в Windows 2000 он не должен был превышать 376 Мбайт. Увеличен размер системных страничных таблиц, в результате чего системное виртуальное адресное пространство увеличилось до 1.3 Гбайт — против 660 Мбайт в Windows 2000. При этом 960 Мбайт в системном виртуальном адресном пространстве XP непрерывны, в других же его "местах" имеются разрывы. (Заметим, что 64 разрядная версия поддерживает размер виртуального системного адресного пространства равный 128 Гбайт.) Весьма вероятно, что такие же параметры остались и у Windows Server 2003 (точные сведения отсутствуют).
Поскольку, страничные таблицы и большая часть Системного Реестра размещаются в физической памяти резидентно, очевидно, что цена такого "послабления" — новое повышение требований к минимальному размеру установленной на компьютере оперативной памяти — до 128 Мбайт.
Увеличено число процессоров, которые может поддерживать операционная система в симметричной многопроцессорной конфигурации (SMP). В редакции Datacenter Windows Server 2003 может поддерживать до 64 процессоров (в 64 разрядной версии). 32-разрядная версия (как и Datacenter Windows 2000) поддерживает по-прежнему до 32 процессоров. Разумеется, такая архитектура обязана быть должным протестирована, и Microsoft анонсировала особый подход к продажам таких "тяжелых" серверов: операционная система будет поставляться только вместе с аппаратурой OEM поставщиками.
И, наконец, самые интересный вопрос: что же означают громадные цифры поддерживаемой оперативной памяти в 32-разрядных версиях: от 2 Гбайт (редакция Web Windows Server 2003) до 64 Гбайт (редакция Datacenter Windows Server 2003)?
Ответ кроется в двух аббревиатурах: AWE и РАЕ, Address Windowing Extension и Physical Address Extension, соответственно. Операционная система, подчиняясь параметру /РАЕ, заданному в файле boot.ini (см. Приложение Б), загружается в модифицированной конфигурации, поддерживающей режим работы РАЕ. В таком режиме возможна манипуляция физическими адресами оперативной памяти (тип PHYSICAL_ADDRESS) за пределами 4 Гбайтного пространства функциями категории MmAllocatePagesForMdl. Наиболее простое и находящееся "на поверхности" применение данного расширения — создание драйверов, реализующих RAM диск. Правда, осложняет дело высокая цена оборудования. На сегодня доля материнских плат, поддерживающих размер оперативной памяти более 8 Гбайт, не превышает 1,5% рынка.
В заключение отметим еще два факта.
Во-первых, по-прежнему все рассмотренные версии продолжают поддерживать все три файловых системы: FAT, FAT32, NTFS. Иными словами, Windows Server 2003 Datacenter Edition вполне устанавливается на логическом диске FAT32, занимая при этом чуть более 1,3 Гбайт.
Во-вторых, Microsoft окончательно (в Windows Server 2003) отошла от поддержки подсистем POSIX и OS/2.
Впрочем, осталось неизменным самое важное — драйверная модель, заложенная в Windows 2000, обеспечивает практически полную совместимость программного кода (а иногда — и бинарного, как можно было убедиться на примере главы 3) во всех рассматриваемых версиях.
Отложенные процедурные вызовы (DPC)
В то время, когда выполняется программный код режима ядра, приоритет которого имеет наибольшее значение, никакой другой код на данном процессоре стартовать не может. Разумеется, если слишком большие объемы программного кода будут выполняться при слишком высоких значениях IRQL, то это неминуемо приведет к общей деградации системы.
Для разрешения такого типа проблем, программный код, предназначенный для работы в режиме ядра, должен быть сконструирован таким образом, чтобы избегать продолжительной работы при повышенных уровнях IRQL. Одним из самых важных компонентов этой стратегии являются Deferred Procedure Calls (DPC) — отложенные процедурные вызовы.
Отслеживание ошибок
Исследования показывают, что ошибки не распределяются равномерно по всему программному коду, а имеют тенденцию концентрироваться в нескольких специфических процедурах, обычно, почти пропорционально их функциональной сложности. Тщательное протоколирование позволяет идентифицировать эти процедуры для того, чтобы держать их впоследствии "на заметке".
Важнейшим моментом отладки является воспроизведение ошибочных ситуаций. Протоколирование может помочь и здесь. Хорошо выполненное протоколирование позволяет понять проблемы, проистекающие из конфигурации проекта, упорядочивая упомянутую выше первую фазу тестирования — этап накопления наблюдений. Кроме того, выявляются бреши в собственно стратегии тестирования.
Paged Memory, Paged Pool
Странично организованная память, страничный пул, страничная память.
Виртуальная память, которая может быть перемещена системой на жесткий диск, в любой момент, когда она сочтет эту операцию целесообразной (например, при низкой активности приложения). Эта перемещенная область имеет название "paged", то есть "постранично сброшенная на диск". В том случае, если приложение, например, снова становится активным и обращается к отсутствующей в физической памяти области своей виртуальной памяти, то возникает исключение, хорошо известное под названием "page fault". B результате его перехвата к работе приступает системный обработчик этого исключения и "подтягивает" в физическую память отсутствующую информацию.
Однако при работе в режиме ядра кода, имеющего IRQL уровень приоритета равный или выше DISPATCH_LEVEL, возникает катастрофическая ситуация. Если обстоятельства сложились так, что этот программный код должен получить доступ к виртуальной странице, сброшенной на диск, и эта страница могла бы быть размещена в физической памяти усилиями системного обработчика исключения типа "page fault", но... Но уровень IRQL обработчика ниже приоритета DISPATCH_LEVEL, и система не дает ему возможности приступить к работе, вместо этого прекращая всю работу системы! Обращаться к страничной памяти (если не предпринимать специальных мер) и производить получение новых областей категорически рекомендуется только из кода, работающего на IRQL уровнях PASSIVE_LEVEL или APC_LEVEL.
Можно, конечно, организовать собственный перехват исключения в сомнительном месте, как это сделано во фрагменте кода ниже.
__try { char x=*(char*)0x0L; } __except(EXCEPTION_EXECUTE_HANDLER) { DbgPrint("Exception detected in my driver"); };
Однако такое решение позволяет лишь уйти от конкретной ошибки (да и работает только на IRQL уровне PASSIVE_LEVEL). Получение доступа к нужным данным так и остается нерешенной задачей. Другое, более правильное решение предлагается ниже.
Пакеты IRР
Практически весь процесс ввода/вывода, имеющий место в Windows, является пакетно-управляемым. Отдельная транзакция ввода/вывода описывается рабочим рецептом, предписывающим драйверу, что делать. При помощи IRP прослеживается также обработка запроса в подсистеме ввода/вывода. Этот рабочий рецепт имеет форму структуры данных, называемой I/o Request Packet (IRP) — пакет запроса на ввод/вывод.
При каждом запросе из программного кода клиента драйвера на выполнение операции ввода/вывода, включая IOCTL запросы (управляющие воздействия на аппаратуру), Диспетчер ввода/вывода выделяет под IRP область нестраничной памяти. Определив по дескриптору открытого файла, к какому драйверу и объекту устройства адресовано обращение, и по запрошенному коду операции ввода/вывода (IRP_MJ_Xxx), Диспетчер передает сформированный пакет IRP в соответствующую рабочую (см.ниже) процедуру драйвера. (Следует отметить, что для доступа из программного кода клиента применяется "файловая абстракция" процесса взаимодействия с драйвером — открытие, чтение, запись, дескрипторы и т.п.)
Пакеты IRP являются структурами данных переменной длины, и состоят из стандартного заголовка, содержащего общую учетную информацию, и одного или нескольких блоков параметров, называемых I/O stack location — ячейкой стека ввода/вывода.
Рис. 8.1 Структура пакета IRP |
Память, отведенная устройствам
Отведенная память, используемая РСI функциями (функциональными единицами PCI устройств), может быть размещена в любом месте 32 разрядного адресного пространства. Эта возможность может быть использована при работе в базисе "функция устройства" — "функция устройства".
Интересная возможность спецификации PCI состоит в том, что в конфигурационном пространстве предусмотрено место для указания ссылки на область расширения ПЗУ (Extension ROM Base Address, см. таблицу 5.4), встроенного в данное PCI устройство. В этом ПЗУ могут быть записаны фрагменты кодов инициализации, предназначенные для инициализации устройства на разных аппаратных платформах, что дает возможность поставлять один и тот же продукт для разных компьютерных платформ. Спецификация PCI определяет стандартный заголовочный формат для этих блоков ПЗУ, поэтому программное обеспечение, выполняющее инициализацию, может определить необходимый фрагмент, хранящийся в ПЗУ и загрузить именно его для выполнения инициализации на имеющейся платформе.
Память, отведенная устройству
Третий из механизмов передачи данных состоит в том, что для доступа к устройству используются диапазоны адресов памяти, открытые для совместного использования, см. рисунок 5.1.
Рис. 5.1 Доступ к памяти, имеющейся в устройстве |
В некоторых случаях эти диапазоны определены жестко, как в случае с диапазоном адресов 0x0A0000-0x0AFFFF буфера адаптера VGA.
Память, отведенная устройству, является ресурсом, предоставляемым драйверу устройства операционной системой, и обычно с ним можно ознакомиться в апплете свойств драйвера в настройках системы (Пуск - Настройка - Панель Управления - Система - Устройства - Диспетчер Устройств - устройство - ресурсы).
Параметр DisplayName
Значение этого параметра описывает текст, используемый в служебных программах операционной системы, в частности, в программах Панели Управления. В случае, если данный параметр не указан, используется имя драйвера.
Параметр ErrorControl
Параметр ErrorControl (тип REG_DWORD) предписывает операционной системе способ поведения в той ситуации, когда при загрузке и инициализации драйвера (отработке процедуры DriverEntry) произошла ошибка.
Таблица B-1. Значения параметра ErrorControl
Значение | Символьное имя (см. wdm.h, ntddk.h, winnt.h) |
Поведение операционной системы | |
0x0 | SERVICE_ERROR_IGNORE | В процессе загрузки ошибки игнорируются, загрузка продолжается без уведомления об ошибках в данном драйвере | |
0x1 | SERVICE_ERROR_NORMAL | В процессе загрузки ошибки игнорируются, но выводятся сообщения о них, при этом загрузка продолжается | |
0x2 | SERVICE_ERROR_SEVERE | Порядок загрузки нарушается и начинается заново с использованием набора параметров LastKnownGood, а если он уже используется, то ошибка игнорируется | |
0x3 | SERVICE_ERROR_CRITICAL | Порядок загрузки нарушается и начинается заново с использованием набора параметров LastKnownGood, а если он уже используется, то загрузка прерывается и выводится сообщение об ошибке |
Параметр ImagePath
Значение этого параметра описывает полный путь к файлу, содержащему исполняемый код драйвера. В данном примере, загрузка драйвера была выполнена программой monitor из файла K:\Ex\Example.sys, что и было отражено в значении параметра ImagePath в данном подразделе Реестра. В случае, если данный параметр не указан, то используется значение %system%\Drivers\drivername.sys.
Параметр Start
Значение этого параметра описывает стадию загрузки операционной системы, когда следует загружать данный драйвер.
Таблица B-2. Значения параметра Start
Значение | Символьное имя (см. wdm.h, ntddk.h, winnt.h) |
Время загрузки драйвера | |
0x0 | SERVICE_BOOT_START | Драйвер запускается загрузчиком NTLDR | |
0x1 | SERVICE_SYSTEM_START | Драйвер запускается на стадии загрузки компонентов ядра ОС | |
0x2 | SERVICE_AUTO_START | Драйвер будет запущен средствами SCM после загрузки компонентов ядра ОС | |
0x3 | SERVICE_DEMAND_START | Драйвер запущен "вручную", то есть по запросу пользовательского приложения при помощи средств SCM (после загрузки ОС) | |
0x4 | SERVICE_DISABLED | Драйвер не может быть запущен |
Параметр Туре
Значение этого параметра описывает тип драйвера, некоторые из значений приведены в таблице B-3.
Таблица B-3. Значения параметра Туре
Значение | Символьное имя (см. wdm.h, ntddk.h, winnt.h) |
Тип драйвера | |
0x1 | SERVICE_KERNEL_DRIVER | Драйвер режима ядра | |
0x2 | SERVICE_FILE_SYSTEM_DRIVER | Драйвер файловой системы | |
0x4 | SERVICE_ADAPTER | Драйвер адаптера |
Помимо описанных выше часто используемых параметров, используемых для описания драйверов в Системном Реестре, могут встречаться параметры DependOnGroup, DependOnService, Tag, Group, определяющие порядок загрузки, и параметр Parameters, официально предназначенный для хранения частных настроечных параметров.
Параметры подраздела \Enum
Подраздел HKLM\System\CurrentControlSet\Services\drivername\Enum
(здесь drivername — имя драйвера) в Системном Реестре присутствует постоянно
для драйверов, установленных при помощи Мастера Установки Оборудования. Для драйверов, загружаемых при помощи сервиса SCM, он появляется только после их удачного старта. В этом подразделе присутствуют параметры Count (число обслуживаемых устройств), NextInstance, параметры 0, 1 и т.п.
Рис. В-3 Подраздел \Enum после удачного старта драйвера Example.sys |
Параметры 0, 1 и т.п. появляются только для удачно стартовавших драйверов, а их значения указывают на подраздел в HKLM\System\CurrentControlSet\Enum
(где отражаются все когда-либо удачно стартовавшие драйвера). Если пойти по указанной в параметре 0 (или 1 и т.п.) ссылке, то можно увидеть в соответствующем подразделе HKLM\System\CurrentControlSet\Enum\...\Control параметр ActiveService, дающий обратную ссылку HKLM\System\CurrentControlSet\Services\drivername. Если одна из этих ссылок в Реестре не существует (прямая или обратная), то это признак того, что драйвер не запустился.
PCI: Peripheral Component Interconnect
Быстрые сетевые приложения, высококачественное видео, 16 и 20 разрядный звук и быстро развивающиеся музыкальные приложения, дисплеи с глубиной цвета 24 бит — все эти новшества потребовали введения нового стандарта шины с высокими скоростями передачи данных. Шина PCI явилась попыткой удовлетворить все эти возросшие потребности. И хотя первоначальная конструкция зародилась в Intel (первая спецификация версии 1.0 появилась в июне 1992), шине PCI оказана поддержка со стороны других компьютерных производителей. Она является относительно независимой от процессорных конфигураций и успешно используется теперь в компьютерах Alpha(DEC) и PowerPC (Motorola). Ha рисунке 5.2 показана типовая конфигурация системы, содержащей PCI шину.
Рис. 5.2 Типовая конфигурация системы с шиной PCI |
Повысив тактовую частоту до 33 МГц (позже и до 66 МГц) и применив множество технических уловок, разработчики PCI добились того, что пропускная способность PCI шины возросла до 132 Мбайт/сек при передаче данных по 32-разрядной шине. При передаче данных по 64-разрядной шине пропускная способность увеличивается вдвое. Некоторые из принципов, которые позволили обеспечить столь значительный прогресс, таковы:
Протокол PCI подразумевает, что каждая передача данных происходит как пакетно-монопольная операция (burst operation), в результате чего для быстрых устройств, предающих большие объемы данных, действительно достигаются физически предельные показатели PCI.
Протокол PCI поддерживает режим многих "хозяев шины" (bus master) и разрешает прямую передачу данных устройство-устройство (без промежуточного использования памяти), что может быть использовано для повышения степени параллельности между операциями ввода/вывода и работой процессора.
Центральный шинный арбитр (central bus arbiter) совмещает операции арбитрирования и собственно перенос данных, чем уменьшается время задержек. Это позволяет следующей операции стартовать сразу же, как только текущая операция заканчивается.
Мост PCI, наделенный существенными способностями, между главным процессором и собственно шиной представляет разнообразные возможности кэширования и выполнения операций упреждающего чтения данных (readahead functions). Bce это позволяет уменьшить время, которое процессор тратит на ожидание данных.
Мост PCI является устройством шины, позволяющим подключать вторичные шины PCI. В таком случае мост называется 'PCI-to-PCI bridge'. (В частности, ранее это было решением проблемы увеличения числа слотов PCI, если их требовалось более 4.)
Архитектура PCI позволяет подключить к шине до 32 физических единиц, называемых устройствами (devices). Каждая из этих единиц может содержать до 8 отдельных функциональных единиц, называемых функциями (functions). После отбрасывания одного адреса функции для генерации широковещательных сообщений (broadcast messages — сообщений, предназначенных всем устройствам), остается 255 адресуемых единиц-функций на одной шине PCI. (Учитывая, что PCI-PCI мост является обычным устройством шины, возникает возможность подключения к основной шине до 255 дополнительных шин PCI).
Печатные издания на русском языке
А.В. Фролов, Г.В. Фролов, Операционная система MS-DOS, том 1, - M., "Диалог-МИФИ", 1991 - 240 с.
Даниель Нортон, Драйвера Windows, ввиду малого тиража на русском языке и давности момента издания невозможно установить выходные данные. Издана около 1995 года.
Свен Шрайбер, Недокументированные возможности Windows 2000 (+CD), - СПб.: Питер, 2002 - 544 с.
Джеффри Рихтер, Windows для профессионалов: Создание эффективных Win32-пpилoжeний с учетом специфики 64-разрядной версии Windows. Пер. с англ. - 4 изд. - СПб.:Питер: М.:Издательство торговый дом "Русская редакция", 2001 - 752 с.
С.И. Сорокина, А.Ю. Тихонов, А.Ю. Щербаков, Программирование драйверов и систем безопасности: Учебное пособие - СПб.:БХВ-Петербург, M.: Издатель Молгачева СВ., 2002 - 256 с.
Ольга Кокорева, Реестр Windows XP, - СПб.:БХВ-Петербург: 2003 - 560 с.
Передача PnP IRP пакетов нижним драйверным слоям
Все запросы PnP инициируются PnP Менеджером, и он всегда направляет эти запросы драйверу, находящемуся в стеке устройств на вершине стека.
Независимо от того, какие коды IRP_MN_Xxx в составе IRP запросов могут быть обработаны драйвером, те из них, которые не обрабатываются, должны быть переданы ниже по стеку устройств нижележащим драйверам, которые могут реализовывать свои собственные обработчики.
Трансляция PnP запросов вниз по стеку устройств необходима по многим причинам. Некоторые драйверы в стеке могут вносить свою лепту в обработку запроса, но не один драйвер не должен подразумевать, что запрос может быть полностью завершен на данном уровне. Например, уведомление об остановке устройства является критичным для всех слоев драйверных объектов.
Чтобы передать PnP запрос вниз, драйвер помечает IRP пакет как "завершенный" установкой соответствующих значений в полях IoStatus.Status и IoStatus.Information, а затем производит вызовы IoCopyCurrentStackLocationToNext
и IoCallDriver. Нижележащий драйвер известен еще при выполнении AddDevice (из вызова IoAttachDeviceToDeviceStack), а указатель на него рекомендуется сохранять в структуре расширении объекта устройства. Пример кода, выполняющего эти действия, приводится ниже.
... IoCopyCurrentIrpStackLocationToNext( pIrp ); PDEVICE_EXTENSION pThisDeviceExtension = (PDEVICE_EXTENSION) pThisDeviceObject->DeviceExtension; IoCallDriver( pThisDeviceExtension ->pUnderlyingDevice, pIrp ); ...
В случае, если у драйвера нет необходимости ожидать окончания обработки переданного вниз запроса драйверами нижних уровней, то может быть использован более эффективный механизм для пропуска участка текущего IRP стека. Функция IoSkipCurrentIrpStackLocation
просто удаляет участок текущего стека IRP пакета из участия в обработке. Этот механизм предлагается для обращения с PnP запросами, которые не обрабатываются данным драйвером и которые должны быть просто переданы следующему нижележащему драйверу:
NTSTATUS OnlyTranslateIrpDown(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp) { IoSkipCurrentIrpStackLocation( pIrp ); PDEVICE_EXTENSION pDeviceExtension = (PDEVICE_EXTENSION) pDeviceObject ->DeviceExtension; return IoCallDriver(pThisDeviceExtension->pUnderlyingDevice, pIrp); }
Бывают случаи, когда драйвер вынужден пропускать вниз PnP запросы раньше, чем он завершает собственную работу над ними. Например, при обработке запроса с кодом IRP_MN_START_DEVICE драйверу, как правило, необходимо дождаться, пока стартуют низкоуровневые драйверы перед началом работы их собственного аппаратного обеспечения. Шина и любое низкоуровневое аппаратное обеспечение инициализируется до старта отдельных устройств. Таким образом, высокоуровневые драйвера должны сначала транслировать вниз запрос и затем дождаться завершения низкоуровневой обработки перед продолжением своей работы.
Действия, которые драйвер должен выполнить, когда его рабочая процедура возвращает управление Диспетчеру ввода/вывода, рассматриваются ниже.
Передача сигналов прерываний
При генерации прерываний используются две основные стратегии. Более старый и менее предпочтительный механизм носит название edge-triggered
(управляемые перепадом, управляемые фронтом) или latched
прерывания. Устройства, которые генерируют такие прерывания, сигнализируют о потребности в обслуживании осуществлением перехода на физически существующем проводе (например, на одном из проводников шины или соединительного кабеля) из одного логического состояния в другое, скажем, из 1 в 0. Как только этот переходной процесс прошел, устройство может отпустить линию (отпустить прерывание), восстановив параметры электрического сигнала, в данном случае, обозначающие логическую 1. Другими словами, линия прерывания такого устройства "пульсирует" под его руководством, а процессор должен обратить внимание на проходящие импульсы.
Этот тип прерываний является потенциальным источником ошибочных срабатываний, поскольку шум на линии может выглядеть как сигнал прерывания. Кроме того, более сложная проблема возникает, когда два устройства с таким типом прерываний используют одну линию. В случае если два устройства "просигнализируют" одновременно, процессор различит лишь одно прерывание, и тот факт, что прерывания сгенерировали два устройства, будет потерян безвозвратно.
Классическим примером оборудования, где случаются потери такого типа прерываний, являются последовательные СОМ порты. По традиции, COM1 и COM3 используют одно определяемое по фронту прерывание, а именно IRQ4. В результате, невозможно одновременно подключить к этим двум портам оборудование, которое использует управляемое прерываниями программное обеспечение. Попытки включить мышь в COM1 и модем в COM3 рано или поздно, но — неминуемо приводят к остановке одного из драйверов, который ожидает "пропавший" сигнал о прерывании.
Эти ограничения преодолеваются реализацией второго механизма, известного как level-sensitive, чувствительный к уровню, или level-triggered, управляемый уровнем. Устройства удерживают общую аппаратную линию (совместно используемый проводник) в соответствующем состоянии до тех пор, пока потребность каждого не будет удовлетворена. Теперь для процессора нет проблем в том, чтобы зарегистрировать оба прерывания, так как линия удерживается в этом состоянии до момента завершения обслуживания и первого, и второго устройства — это позволяют делать специальные типы цифровых схем. В случае если два прерывания происходят одновременно, обслуживается устройство с более высоким приоритетом, а после и другое устройство, поскольку оно продолжает сигнализировать, удерживая линию в соответствующем состоянии и после окончания обработки запроса первого, высокоприоритетного, устройства.
Перехват некорректных условий
Разработчик драйвера, как и всякий разработчик, создает свой продукт, отталкиваясь от некоторых изначальных допущений. Важно лишь, чтобы они не были беспочвенными или чрезмерно оптимистическими. В том случае, если возникают некоторые сомнения в условиях работы некоторых важных участков кода драйвера, лучше подстраховаться. Например, предполагая, что некий фрагмент кода, будет вызван только на определенном уровне IRQL, автор закладывает потенциальную возможность будущих сбоев (если данные ожидания не будут оправданы).
Для того чтобы перехватить эти непредусмотренные отклонения, следует применять несложный прием: такие допущения должны проверяться во время выполнения, хотя бы в отладочной сборке драйвера. Макроопределения ASSERT и ASSERTMSG помогут решить эту задачу:
ASSERT( Expression ); ASSERTMSG( Message, Expression );
В случае если выражение Expression, рассмотренное как логическое, будет равно FALSE, макроопределение ASSERT произведет запись сообщения в командное окно WinDbg, подключенного на хост-компьютере для проведения отладки. Это сообщение содержит исходный код выражения, значение которого оказалось равным FALSE, имя файла (в котором был исходный текст данного фрагмента) и номер строки, где было вызвано макроопределение ASSERT. Затем предлагается сделать выбор, сделать ли прерывание в месте данного макроопределения ASSERT, игнорируя причину остановки, либо прервать процесс или поток, в котором "сработал" ASSERT (что, впрочем, совершенно аналогично применению ASSERT
в Visual Studio).
Макроопределение ASSERTMSG демонстрирует точно такое же поведение, с той лишь разницей, что в сообщение включается содержимое Message (являющееся строкой). В отличие от debug print функций, описанных ранее, ASSERTMSG
не допускает форматного вывода данных в стиле printf.
Следует также обратить внимание на то, что оба макроопределения используют условную компиляцию, вследствие чего в версии "free" оно совершенно исчезает. Следовательно, совершенно недопустимо помещать какой-либо исполняемый код внутрь этих макроопределений — в версии "free" они исчезнут вовсе.
Кроме того, в основе упомянутых выше макроопределений лежит использование функции RtlAssert, которая во "free" версиях Windows 2000/XP/2003 превращается в пустышку. Следовательно, чтобы наблюдать последствия ошибок в макроопределениях ASSERT и ASSERTMSG следует тестировать драйвер под отладочной (checked) версией Windows.
Переносимость
В качестве способа решения задачи переносимости конструкторы NT выбрали многослойную архитектуру, как показано на рисунке 4.1.
Рис. 4.1 Слои операционной системы Windows NT 5 |
Слой аппаратных абстракций (Hardware Abstraction Layer, HAL) изолирует процессорные и платформенные особенности от кода операционной системы. Его услугами Microsoft предлагает пользоваться и разработчику драйверного кода. Вполне возможно так написать драйвер, что для перенесения его на другую платформу потребуется разве что перекомпилировать его. Как можно это сделать, если изначально драйвер есть такая программная единица, которая жестко привязана и к своему устройству, и к конкретному процессору, и к конкретной платформе?! Просто драйвер должен обратиться к использованию средств уровня HAL для взаимодействия с аппаратными регистрами и аппаратной поддержкой шин. В отдельных случаях разработчик драйвера может опереться на код, предоставляемый Диспетчером ввода/вывода, для работы с совместно используемыми аппаратными ресурсами. Например, при DMA операциях (прямого доступа к памяти) используется такая абстракция программирования, как объект адаптера
PnP идентификаторы IDE устройств
Идентификаторы IDE устройств схожи с идентификаторами для SCSI устройств. Для IDE допустимо в inf-файлах представлять следующие варианты идентификационной информации, например:
IDE\ttttv_vrrrrrrrr IDE\v_vrrrrrrrr IDE\ttttv_v v_vrrrrrrrr gggg
Здесь tttt является типо-кодом устройства (см. таблицу 12.11), v_v является 40-символьным идентификатором поставщика (производителя), rrrrrrrr — 8-сим-вольный номер версии разработки. В случае, если идентификатор поставщика короче 40 символов, то он дополняется символами подчеркивания. Пример для третьего варианта представления PnP идентификационной информации в inf-файле:
IDE\CdRomALPS_DC544______________________________
PnP идентификаторы PCI устройств
Полный идентификатор для PnP PCI устройств имеет форму
PCI\Ven_vvvv&Dev_dddd&SubSys_ssssssss&Rev_rr
Здесь vvvv является идентификатором поставщика (производителя), зарегистрированным в группе PCI Special Interest Group, dddd — идентификатор, присвоенный производителем данной PCI карте, ssssssss — идентификатор конструкции (subsystem ID), rr — номер версии разработки. Все упомянутые поля вводятся как шестнадцатеричные числа. Поле ssssssss обычно вводится как нулевое.
Кроме того, допустимо в inf-файлах представлять усеченные варианты идентификационной информации, например:
PCI\Ven_vvvv&Dev_dddd&SubSys_ssssssss PCI\Ven_vvvv&Dev_dddd&Rev_rr PCI\Ven_vvvv&Dev_dddd PCI\Ven_vvvv&Dev_dddd&Rev_rr&CC_ccss PCI\Ven_vvvv&Dev_dddd&CC_ccsspp PCI\Ven_vvvv&Dev_dddd&CC_ccss PCI\Ven_vvvv&CC_ccsspp PCI\Ven_vvvv&CC_ccss PCI\Ven_vvvv PCI\CC_ccsspp PCI\CC_ccss
Здесь cc является кодом базового класса из конфигурационного пространства PCI устройства, ss — код подкласса, pp — идентификатор программного интерфейса.
PnP идентификаторы SCSI устройств
Полный идентификатор для PnP SCSI устройств имеет форму
SCSI\ttttvvvvvvvvpppppppppppppppprrrr
Здесь tttt является типо-кодом устройства, vvvvvvvvявляется 8-символьным идентификатором поставщика (производителя), pppppppppppppppp— 16 символьный идентификатор устройства, rrrr — номер версии разработки.
Кроме того, допустимо в inf-файлах представлять усеченные варианты идентификационной информации, например:
SCSI\ttttvvvvvvvvpppppppppppppppp SСSI\ttttvvvvvvvv SCSI\vvvvvvvvppppppppppppppppr vvvvvvvvppppppppppppppppr gggg
Здесь gggg является одним из групповых типов (generic type) классов, приведенных в таблице 12.11.
Таблица 12.11. Типы SCSI
u IDE устройств
SCSI код | Устройство | Тип | Групповой тип | |
DIRECT_ACCESS_DEVICE (0) | Дисковое | Disk | GenDisk | |
SEQUENTIAL_ACCESS_DEVICE (1) | Последовательное | Sequential | ||
PRINTER_DEVICE (2) | Принтер | Printer | GenPrinter | |
PROCESSOR_DEVICE (3) | Сканнеры, принтеры и т.п. | Processor | ||
WRITE_ONCE_READ_MULTIPLE_DEVICE (4) | Worm | Worm | GenWorm | |
READ_ONLY_DIRECT_ACCESS_DEVICE (5) | CD ROM | CdRom | GencdRom | |
SCANNER_DEVICE (6) | Сканирующее | Scanner | GenScanner | |
OPTICAL_DEVICE (7) | Оптические диски | Optical | GenOptical | |
MEDIUM_CHANGER (8) | Устройство со сменными носителями | Changer | ScsiChanger либо GenChanger (для IDE) |
|
COMMUNICATION_DEVICE (9) | Сетевое | Net | ScsiNet |
Для SCSI диска, имеющего полный установочный PnP идентификатор SCSI\DiskSEAGATE_ST39102LW____0004, шинный драйвер сконструирует также и следующий список идентификаторов:
SCSI\DiskSEAGATE_ST39102LW____0004 SCSI\DiskSEAGATE_ SCSI\DiskSEAGATE_ST39102LW____0 DiskSEAGATE_ST39102LW____0004 GenDisk
PnP идентификаторы USB устройств
Полный идентификатор для PnP USB устройств имеет форму
USB\Vid_vvvv&Pid_dddd&Rev_rr
Здесь vvvv является идентификатором поставщика (производителя), зарегистрированным в Комитете USB производителей, dddd — идентификатор, присвоенный производителем данной модели устройства, rr — номер версии разработки. Все упомянутые поля вводятся как шестнадцатеричные числа.
Кроме того, допустимо в inf-файлах представлять усеченные варианты идентификационной информации, например:
USB\Vid_vvvv_&Pid_dddd USB\Class_cc&SubClass_ss&Prot_pp USB\Class_cc&SubClass_ss USB\Class_cc
Здесь cc является кодом базового класса из полученного дескриптора устройства или дескриптора интерфейса данного USB устройства, ss — код подкласса, pp — идентификатор протокола.
PnP идентификаторы устройств IEEE-1394 (FireWire)
Полный идентификатор для PnP USB устройств имеет форму
1394\VendorName&ModelName 1394\UnitSpecId&UnitSwVersion
Здесь VendorName является наименованием поставщика (производителя), ModelName — идентификатор, присвоенный производителем данной модели устройства, UnitSpecId и UnitSwVersion — идентификаторы программных спецификаций, получаемые из конфигурационных ПЗУ подключаемых устройств, например:
1394\MICROSOFT&1394_DIAGNOSTIC_DEVICE 1394\031887&040892
PnP Manager
PnP Менеджер, один из ключевых компонентов операционной системы, конструктивно состоящий из двух частей, PnP Менеджера, работающего в режиме ядра, и PnP Менеджера пользовательского режима. Первый взаимодействует с остальными компонентами системы и драйверами в процессе загрузки программного обеспечения, необходимого для обслуживания имеющихся в системе устройств. Его часть, работающая в пользовательском режиме, отвечает за взаимодействие с пользователем в ситуациях, требующих установки новых драйверов или настройки рабочих параметров в существующих.
Все драйверы должны обеспечивать PnP поддержку. В противном случае будут существенно ограничены PnP поддержка и управление энергопотреблением системы в целом.
Если обратиться к процессу нумерации (enumeration) устройств и воспользоваться программой DeviceTree из состава пакета DDK, то станет очевидно, что во главе этого процесса стоит именно PnP Менеджер режима ядра (см. рисунки 2.6 и 2.7).
Подготовка к загрузке
Компьютер тестирует себя (стадия POST, Power On Self Test), оперативную память, физические устройства. Если BIOS поддерживает спецификацию PnP, то происходит определение и настройка такого типа устройств.
BIOS обнаруживает загрузочное устройство (жесткий диск, привод CD ROM), загружает и запускает на выполнение основную загрузочную запись (MBR).
MBR просматривает таблицу разделов (partition table), чтобы найти активный, загружает загрузочный вектор активного раздела в память и запускает его на выполнение.
Загружает и инициализирует файл NTLDR, который представляет собой загрузчик ОС.
В качестве основного для Windows
В качестве основного для Windows 2000/XP/Server 2003 интерфейса API (32-разрядных версий), подсистема Win32 ответственна за:
графический пользовательский интерфейс (Graphical User Interface, GUI), который наблюдает пользователь системы. Win32 отвечает за реализацию видимых окон, диалоговых элементов и элементов управления (кнопок, полос прокрутки и т.п.), то есть общий стиль оформления системы.
Консольный ввод/вывод, включая клавиатуру, мышь и дисплей для всей операционной системы и других подсистем.
Функционирование Win32 API, при помощи которого приложения и другие подсистемы взаимодействуют с исполнительными компонентами режима ядра.
Так как подсистема Win32 имеет особый статус среди остальных подсистем, а вследствие этого к ней предъявляются повышенные требования, то и реализация этой подсистемы существенно отличается. В частности, подсистема Win32 разделена на несколько компонентов, часть из которых работает пользовательском режиме, а другая в режиме ядра. Функции Win32 можно разделить на три категории:
Функции, которые предоставляются пользователю для управления окнами, меню, диалогами и элементами контроля (кнопками, полосами прокрутки, переключателями, закладками и т.п.).
Функции GDI, реализующие прорисовку изображений на физических устройствах, экране, принтере, графопостроителе.
Функции, которые управляют неграфическими ресурсами, такими как процессы, программные потоки, файлы и объекты синхронизации. KERNEL-функции тесно связаны с системными службами исполнительных компонентов.
Со времен NT 4.0 большая часть функций первых двух категорий из приведенной выше классификации была реализована в режиме ядра. Пользовательские процессы, которые запрашивают услуги GUI, обращаются непосредственно к коду режима ядра при использовании System Service Interface (Интерфейса Системных Служб). Код, представляющий эти функции и работающие в режиме ядра, локализован в модуле WIN32K.SYS.
Функции третьей категории при обработке запросов от пользовательских процессов опираются на стандартный серверный процесс CSRSS.exe (Client-Server Runtime Subsystem), который и обращается собственно к коду исполнительных компонентов для завершения обработки этих обращений.
Polling
Метод программирования работы с устройством, когда драйвер периодически опрашивает устройство на предмет изменения его состояния — в отличие от работы по сигналу прерывания, который может поступать от устройств, конструктивно имеющих такую способность. В частности, устройства USB конструктивно не могут генерировать сигналы прерывания, но могут сообщать о подобных желаниях в момент их опроса хост-контроллером.
Получение информации о состоянии устройства и об ошибках
Следует определить протокол, который устройство использует для сообщения о своем текущем состоянии и для сообщения о возникших ошибках. Необходимо обладать механизмом уверенного обнаружения сбоев в функционировании устройства.
Pool Memory
Память в пулах (страничном или нестраничном). Области в пространстве памяти ядра (адреса выше 0x80000000 для стандартной конфигурации системы — поскольку для сервера можно определить иначе), в которых можно динамически выделять (получать, аллокировать) и освобождать (деаллокировать) области памяти. Менеджер Памяти (Memory Manager) различает два типа пулов, к которым драйвер может получать доступ при помощи вызовов функций исполнительного блока Ex(ecutive):
Paged pool — страничный пул, в котором каждый процесс имеет собственный набор РТЕ (Page Table Entries) — записей в таблице страниц.
Nonpaged pool — нестраничный пул, в котором все процессы совместно используют один набор РТЕ — записей в таблице страниц.
Выделение областей для физически непрерывных областей или областей некэшируемой памяти производится из ресурсов нестраничного пула.
Port Driver
Порт-драйвер. Драйвер самого низкого уровня, который отвечает на стандартные системные запросы и, возможно, дополнительно — на специальные IOCTL запросы соответствующего классового драйвера. Порт-драйвер изолирует классовые драйверы от специфики аппаратуры (возможно, при помощи мини-порт-драйвеpa) и синхронизирует их операции.
Вообще говоря, любой драйвер устройства, являющегося "интеллектуальным контроллером" или шинным адаптером, может считаться порт-драйвером, если он общается с хотя бы одним классовым драйвером по определенному протоколу и синхронизирует доступ к контроллеру или шине. Среди примеров — SCSI порт-драйвер, видео порт-драйвер, драйвер параллельного порта.
Последовательность действий рабочих процедур
Конкретное поведение каждой из рабочих процедур драйвера будет зависеть от функций, которые ей будет поручено поддерживать. Тем не менее, общие обязанности этих процедур включают следующие моменты:
Вызов IoGetCurrentIrpStackLocation для того, чтобы получить указатель на ячейку стека IRP пакета, относящуюся к ведению данного драйвера.
Дополнительную проверку параметров, специфичную для данного типа запроса и устройства.
Продолжение обработки IRP до момента успешного завершения или возникновения ошибочной ситуации, препятствующей дальнейшей обработке.
Когда рабочая процедура драйвера обрабатывает пакет IRP, существует только три возможных варианта окончания ее работы.
Параметры запроса не проходят проверку на полноту и правильность, и запрос отклоняется.
Запрос может быть обработан в пределах данной рабочей процедуры драйвера, без вовлечения физического устройства, например, чтение/запись данных нулевой длины.
Происходит обращение к физическому устройству с целью получить данные или выполнить действия над устройством, необходимые для завершения запроса.
Последовательность обслуживания запросов ввода/вывода
Весьма важным для разработчика драйвера является понимание жизненного цикла IRP запроса. Ниже рассматривается продвижение запроса — от программного кода пользовательского режима, через код Диспетчера ввода/вывода к драйверу устройства. Все запросы на ввод/вывод проходят следующие основные стадии:
Предварительная обработка Диспетчером ввода/вывода.
Предварительная обработка драйвером устройства.
Старт устройства и обслуживание прерывания.
Пост-обработка драйвером.
Пост-обработка Диспетчером ввода/вывода.
Пост-обработка, выполняемая Диспетчером ввода/вывода
После того как DPC процедура или одна из рабочих процедур помечают IRP пакет как завершенный, Диспетчер ввода/вывода (разумеется, когда получит управление) осуществляет завершающие действия, которые в литературе и документации DDK собирательно называются "очисткой" (cleanup). Это означает:
В том случае, если выполнялась операция записи при буферизованном вводе/выводе (buffered I/O), Диспетчер ввода/вывода освобождает буферную область, использованную во время только что завершенного переноса данных.
В случае, если выполнялась процедура прямого ввода/вывода (direct I/O), Диспетчер ввода/вывода отменяет фиксацию (lock) в оперативной памяти тех страниц, в которых размещался пользовательский буфер.
Диспетчер ввода/вывода помещает в очередь запрос к программному потоку, инициатору первоначального запроса, на выполнение асинхронного процедурного вызова (asynchronous procedure call, APC), работающего в режиме ядра. Этот APC вызов будет выполнять программный код Диспетчера ввода/вывода в контексте потока-инициатора запроса на операцию ввода/вывода.
Выполняющийся в режиме ядра программный АРС-поток производит перенос информации о состоянии и размере переданных данных в пространство пользовательского приложения.
В случае, если выполнялась процедура чтения категории буферизованного ввода/вывода (buffered I/O), процедура APC производит копирование содержимого буферной области, размещенной в нестраничном пуле памяти, в область памяти, предоставленную пользовательским приложением в качестве рабочего буфера. Затем освобождается системный буфер в нестраничном пуле.
В случае, если исходный запрос был сделан для выполнения асинхронного ввода/вывода, процедура APC устанавливает ассоциированное событие (event) и/или файловый объект в состояние, сигнализирующее пользователю об окончании обработки запроса. (Если читатель не знаком с асинхронным вводом/выводом, то может получить доступ к его описанию в документации MSDN по ключевым словам OVERLAPPED, CancelIo, ReadFileEx и т.п.)
В случае, если исходный запрос содержал процедуру завершения (как, например, при использовании функций пользовательского режима ReadFileEx/WriteFileEx
в пользовательском приложении), APC процедура производит планирование вызова в дальнейшем APC процедуры пользовательского режима, которая выполнит вызов процедуры завершения, указанных в параметрах вызовов ReadFileEx/WriteFileEx.
Пост-обработка, выполняемая драйвером
Диспетчер DPC выполняет вызовы DPC процедур драйвера для того, чтобы решать задачи пост-обработки, а именно:
В случае, если выполнялся набор операции по переносу данных и некоторая часть данных еще не была передана, DPC процедура производит установку аппаратуры, производит "старт" устройства и, в ожидании нового прерывания, возвращает управление Диспетчеру ввода/вывода. При этом IRP пакет остается пока в состоянии 'pending' (о его завершении будет объявлено позже — по окончании переноса).
В случае, если произошла ошибка или превышено время ожидание отклика (таймаут), DPC процедура может записать это событие во внутреннюю очередь, поддерживаемую для данного объекта устройства, и затем либо сделать повторную попытку, либо прервать обработку запроса на ввод/вывод. Очереди IRP пакетов для устройств, Device Queue, могут поддерживаться драйверами как альтернатива системным очередям (System Queuing).
DPC процедура освобождает необходимые для переноса ресурсы, удерживаемые драйвером (среди которых могут быть DMA ресурсы).
DPC процедура помещает размер переданных данных и информацию о финальном состоянии в IRP пакет.
Наконец, если обработка IRP пакета действительно завершена, DPC процедура сообщает Диспетчеру ввода/вывода об окончании обработки текущего IRP запроса тем, что помечает его как завершенный (вместо 'pending' — ожидающий обработки) и делает вызов IoStartNextPacket. Это указывает Диспетчеру ввода/вывода на то, что следует переходить к вызову процедуры StartIo для следующего IRP пакета, если таковой ожидает обработки.
Постановка эксперимента
Как известно, аппаратные прерывания — это сигналы, поступающие по специальным линиям в процессор, ради которых процессор приостанавливает работу, сохраняет состояние отложенной работы (контекст) и приступает к обработке возникшей ситуации. Разумеется, при условии, что поступивший сигнал должным образом зарегистрирован, как требующий обработки специально на то предназначенным фрагментом кода, имеющим заранее оговоренный приоритет.