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

         

Потоки как объекты синхронизации


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

PKTHREAD pThreadObject; // Предположим, поток уже работает, его дескриптор hThread. // Получаем указатель на его объект: NTSTATUS status = ObReferenceObjectByHandle( hThread, THREAD_ALL_ACCESS, NULL, KernelMode, (PVOID *)& pThreadObject, NULL); if( !NT_SUCCESS(status) ) { // Действия по обработке ошибки. Может быть поток уже завершен? }

// Ожидаем окончания потока hThread status = KeWaitForSingleObject( (PVOID) pThreadObject, Suspended, KernelMode, FALSE, (PLARGE_INTEGER)NULL); // Поток завершился. // Даем системе возможность удалить объект потока ObDereferenceObject(pThreadObject); . . .



Поведение, связанное с использованием прерываний


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



а для начала решает спросить


Когда кто-то приступает к большому делу, а для начала решает спросить у людей сведущих что-то вроде "Как съесть слона?", то самый правильный ответ, который он только может получить: "По частям!".
Несерьезно? Но зато как верно!
Данная книга — это попытка ввести Вас, Читатель, в не самое дружелюбное подпространство мира программ — разработку драйверов, а если быть совершенно точным — драйверов для операционных систем Microsoft Windows NT 5.x, представленных на сегодня версиями Windows 2000, Windows XP и Windows Server 2003. Идея книги была подсказана обескураживающей тишиной в этой области (разумеется, речь идет о России), когда лишь только 2002 год мог бы похвастаться заметным нарушением этого молчания.
Предназначенная для студентов ВУЗ'ов и специалистов, чья профессиональная деятельность заставляет их обратиться к разработке собственных драйверов для Windows или просто к программированию в режиме ядра Windows, книга предполагает наличие у читателей достаточной подготовки. Прежде всего, разработчик драйвера должен владеть программированием на языке С (без расширений С++), поскольку описание синтаксиса и применения конструкций этого языка не рассматриваются в данной книге вовсе. Во-вторых, разработчик драйверов, пусть начинающий, должен иметь твердо сформировавшееся представление о программировании в многозадачной среде при интенсивном использовании многопоточности. Конечно же, указанные требования не столь объемны и могут быть выполнены в результате короткого "самообразовательного штурма", но здесь придется корректировать свои планы на величину различия между этапами "я Это знаю" и "я умею Этим пользоваться".
Необходимость знания Читателем языка программирования С, как было сказано выше, продиктовано тем обстоятельством, что излагаемый материал ориентирует Читателя на использование пакета Microsoft DDK (Device Driver Kit — пакет программного обеспечения для разработки драйверов), хотя существуют коммерческие программные пакеты и от других фирм, которые базируются на использовании других языков программирования (подробнее эти вопросы будут рассмотрены далее, в главе 2).


Наверное, следовало бы упомянуть и о таком требовании к потенциальному потребителю приведенной в книге информации, как "предрасположенность" или "дружественность" к аппаратуре, поскольку основное назначение драйвера все-таки — взаимодействие с аппаратным обеспечением. Однако во-первых, это подразумевается. Во-вторых, сведения из данной книги можно применять и для разработки таких модулей режима ядра, которые лишь формально являются драйверами, но ни с какими устройствами не связаны и используются лишь как агент доступа к богатому и полезному набору функций режима ядра.
Как Вы, уважаемый Читатель, сможете неоднократно убедиться далее, в книге использован прием повтора некоторых важных положений и выводов, что призвано помочь в расстановке должных, с точки зрения автора, смысловых акцентов. (А вовсе не по причине его забывчивости и не по ошибке редактора!) Начинающему разработчику драйверов настоятельно рекомендуется не пропускать первые главы книги, поскольку такое легкомыслие чревато серьезными проблемами в дальнейшем понимании материала.
Обилие англоязычных синонимов к используемым терминам в тексте так же решает свою задачу. Рано или поздно (скоре всего, уже случилось!) Читателю придется обратиться к чтению DDK документации, поставляемой вместе с программами и библиотеками фирмой Microsoft. По ряду причин это нельзя назвать простым делом. Поскольку чтение англоязычной документации — процесс, которого разработчику драйверов не избежать, чтобы облегчить вступление на этот нелегкий путь, в книге приводятся многочисленные наборы синонимичных терминов с развернутыми вариантами переводов.
За пределами рассмотрения данной книги остались вопросы, которые можно назвать "сложным программированием" драйверов. Не рассматриваются принтерные, SCSI, видео и сетевые драйверы, поскольку этот емкий материал может легко заслонить приоритетные задачи — объяснение, какова внутренняя логика подсистемы ввода/вывода Windows и ознакомление с приемами программирования в режиме ядра.
Книга ориентирована на разработчиков программного обеспечения, но некоторые ее части будут небесполезны и для разработчиков аппаратуры.



Во время создания этой эл.


Во время создания этой эл. книги я старался избегать внесения своих ошибок (по крайней мере, делал это как можно тщательнее), при этом исправив значительное количество опечаток и ошибок оригинала (коих на самом деле немало).. Но я не буду гарантировать полное отсутствие ошибок - это было бы, по меньшей мере, глупо; а потому, если Вы все-таки обнаружите таковые, буду весьма признателен, если Вы сообщите мне об этом.
Кроме того, если Вы интересуетесь драйверами, я бы мог посоветовать Вам, кроме источников, указанных в книге, цикл статей от Four-F "Драйверы режима ядра", который можно найти на превосходном сайте Windows Assembler (wasm.ru)
Если вы поделитесь имеющимися у вас русскоязычными источниками информации о драйверах, моя благодарность не будет знать границ.. =))

Предварительная обработка Диспетчером ввода/вывода


На данном этапе производится не связанная с устройством подготовка запроса и его предварительная верификация.

Подсистема Win32 (ограничимся этой подсистемой) преобразует запрос в системный сервисный вызов (native system service call). Диспетчер системного сервиса переходит в режим ядра и управление передается Диспетчеру ввода/вывода.

Диспетчер ввода/вывода выделяет память под структуру данных, известную под именем I/O RequestPacket (IRP), пакет запроса на ввод/вывод (пакет IRP). В следующей главе эта структура описывается детальнее, но в первом приближении, вполне можно считать пакет IRP рабочим рецептом (предписанием), выдаваемым драйверу и, соответственно, устройству, которое тот обслуживает. Структура данных IRP заполняется необходимой информацией, включая код, которым обозначается тип запроса на ввод/вывод.

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

В случае, если запрос является операцией буферизованного ввода/вывода (buffered I/O), Диспетчер ввода/вывода получает область памяти в области нестранично организованной памяти (нестраничном пуле) под создание буфера, после чего копирует данные из области пользовательского буфера в этот системный буфер. В случае, если запрос к устройству требует прямого ввода/вывода (direct I/O), производится блокирование пользовательского буфера в физической памяти и создается список дескрипторов страниц (MDL список), через которые драйвер имеет возможность доступа к этому пространству физической памяти.

Диспетчер ввода/вывода производит вызов необходимых рабочих процедур драйвера (dispatch routines).



Предварительная обработка в драйвере


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

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

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

В случае, если запрос требует для завершения обработки реального обращения к обслуживаемому физическому устройству, рабочая процедура может пометить IRP пакет как незавершенный (pending). Таким образом, Диспетчеру ввода/вывода сообщается, что необходимо поместить в очередь запрос на вызов процедуры StartIo как только устройство освободится &#8212 возможно, что устройство еще обрабатывает предыдущий запрос. Вообще говоря, этап общения с устройством может быть реализован весьма экзотическими приемами, тут как раз место, где фантазия разработчиков разыгрывается не на шутку.



Прерывания, вызванные программно


Нижние строки таблицы 6.1 описывают уровни IRQL, связанные с прерываниями, вызванными программно. Некоторые виды обработки прерываний инициируются программным кодом, работающим в режиме ядра, путем выполнения привилегированных инструкций (процессорных команд). Операционная система Windows NT 5 использует эти программно вызываемые прерывания для расширения своей схемы приоритетов, и это позволяет ей лучше выполнять планирование потоков. Введение этих IRQL позволяет разрешить противоречия между соревнующимися потоками путем принудительного повышения приоритета одного из них по сравнению с другими, например, при выполнении критических операций, связанных с конкретным устройством. В примере кода драйвера в главе 3 такая операция выполнена при помощи вызова KeRaiseIrlq.



При методе METHOD_BUFFERED


Диспетчер ввода/вывода выделяет единственный буфер в нестраничной памяти, достаточно большой, чтобы вместить входной или выходной буфер инициатора вызова. Адрес этой области размещается в пакете IRP в поле AssociatedIrp.SystemBuffer. Затем производится копирование входного (input) буфера с данными инициатора запроса в эту область. В поле UserBuffer пакета IRP заносится оригинальный адрес буфера для получения данных инициатора запроса.

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



При методе METHOD_IN_DIRECT и METHOD_OUT_DIRECT


Диспетчер ввода/вывода проверяет приемлемость выходного (output) буфера инициатора вызова и производит его фиксацию (lock) в физической памяти. Затем производит построение списка MDL (Memory Descriptor List) для выходного буфера и сохраняет указатель на MDL в поле MdlAddress пакета IRP.

Кроме того, Диспетчер ввода/вывода выделяет временную область в нестраничном пуле и сохраняет этот адрес в поле AssociatedIrp.SystemBuffer пакета IRP. Производится копирование содержимого входного (input) буфера инициатора вызова в выделенный системный буфер, а в поле UserBuffer производится запись значения NULL. После этого IRP пакет поступает в вызываемую рабочую процедуру драйвера.



При методе METHOD_NEITHER


Диспетчер ввода/вывода помещает адрес входного (input) буфера инициатора вызова в поле Parameters.DeviceIoControl.Type3InputBuffer в текущей ячейке стека пакета IRP текущей операции ввода/вывода. В поле UserBuffer производится запись адреса выходного (output) буфера инициатора вызова, где инициатор вызова ожидает получить результаты выполнения операции. Оба этих адреса указывают в область памяти инициатора вызова.



Приемы программирования в режиме ядра


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



Прикасаясь к аппаратуре


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

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



Приложение для тестирования драйвера


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

Сборку приложения можно осуществлять при помощи такого файла Sources:

TARGETNAME=test TARGETTYPE=PROGRAM UMTYPE=console UMENTRY=main UMBASE=0x400000 TARGETPATH=. INCLUDES= $(BASEDIR)\inc

SOURCES=test.cpp

А это, собственно, исходный код тестового приложения: //======================================================================= // Файл тестовой программы test.cpp //=======================================================================

#include &#60windows.h&#62 #include &#60stdio.h&#62

// Предварительное объявление int ReadWrite(HANDLE devHandle);

#define BUFFSIZE (17) static unsigned char outBuffer[BUFFSIZE], inBuffer[BUFFSIZE*2];

int __cdecl main() { printf("\n\n\n\n\nParallel Port CheckIt Loopback Device Test Program.\n" );

HANDLE devHandle; devHandle = CreateFile( "\\\\.\\LPTPORT0", GENERIC_READ | GENERIC_WRITE, 0, // share mode none NULL, // no security OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); // no template

if ( devHandle == INVALID_HANDLE_VALUE ) { printf("Error: can not open device PLPTPORT0. Win32 errno %d\n", GetLastError() ); return -1; } printf("Congratulation. LPTPORT0 device is open.\n\n"); //========================================== DWORD i=3,j=0; for ( ; j&#60sizeof(outBuffer); ) outBuffer[j++] = (unsigned char)i++; //========================================== //for(i=0; i&#60 100000; i++) //{ int result = ReadWrite(devHandle); // if(result) break; //} //========================================== // Завершение работы if ( ! CloseHandle(devHandle) ) { printf("\n Error during CloseHandle: errno %d.\n", GetLastError() ); return 5; } printf("\n\n\n Device LPTPORT0 successfully closed.
Normal exit.\n"); return 0; }

//========================================================================== // Выделим запись и чтение данных в отдельную функцию: // int ReadWrite(HANDLE devHandle) { //========================================== // Передача данных драйверу printf("Writing to LPTPORT0 device...\n");

DWORD bytesWritten, outCount = sizeof(outBuffer); if ( !WriteFile(devHandle, outBuffer, outCount, &bytesWritten, NULL) ) { printf("Error during WriteFile: errno %d.\n", GetLastError() ); return 1; } if ( outCount != bytesWritten ) // если не все передалось: { printf("Error: while wrote %d bytes, WriteFile reported %d bytes.\n", outCount, bytesWritten); return 2; } printf("Successfully written %d bytes.\n Buffer content was: \n", outCount); for (DWORD i=0; i&#60bytesWritten; i++ ) printf("%02X ",outBuffer[i]); //========================================== //Sleep(10); // Ожидание 10 миллисекунд //========================================== // Получение данных из драйвера printf("\n\nReading from device LPTPORT0...\n");

DWORD bytesRead, inCount = sizeof(inBuffer); if ( !ReadFile(devHandle, inBuffer, inCount, &bytesRead, NULL) ) { printf("Error during ReadFile: errno %d.\n", GetLastError() ); return 3; } if ( bytesRead != bytesWritten ) { // размер записанных и прочитанных данных не совпадает printf("Error: is to read %d bytes, but ReadFile reported %d bytes.\n", bytesWritten, inCount); return 4; } printf("Succesfully read %d bytes.\n Buffer content is: \n", bytesRead); for ( i=0; i&#60bytesRead; i++ ) printf( "%02X ", (UCHAR)inBuffer[i] ); return 0; // Нормальное завершение }

При запуске тестовой программы в консольном окне наблюдаем вывод на экран следующих сообщений:

L:\#ex_lpt\test\>test
Parallel Port CheckIt Loopback Device Test Program.
Congratulation. LPTPORT0 device is open.

Writing to LPTPORT0 device...
Successfully written 17 bytes.


Buffer content was:
03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13

Reading from device LPTPORT0... Successfully read 17 bytes. Buffer content is:
03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00 01 02 03

Device LPTPORT0 successfully closed. Normal exit.

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

Ниже приводится информация из отладочного вывода, перехваченного программой DebugView (log-файл этой программы). Средняя часть этого файла, сообщения с 28 по 90, опущена, поскольку в них содержится однообразная и малоинтересная информация.

00000000 0.00000000 LPTPORT: in DriverEntry, RegistryPath is:
00000001 0.00000223 \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\LPTPort.
00000002 0.00003911 LPTPORT: Interrupt 7 converted to kIrql = 8,
kAffinity = 1, kVector = 191(hex)
00000003 0.00004833 LPTPORT: Interrupt successfully connected.
00000004 0.00007878 LPTPORT: Symbolic Link is created: \DosDevices\LPTPORT0.
00000005 5.38805379 LPTPORT: in DispatchCreate now
00000006 5.38822867 LPTPORT: in DispatchWrite now
00000007 5.38823342 LPTPORT: DoNextTransfer:
00000008 5.38823985 LPTPORT: SendingOxO3 to port 378
00000009 5.38824487 LPTPORT: generating next interrupt...
00000010 5.38835690 LPTPORT: In Isr procedure, ISR_Irql=8
00000011 5.38836388 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 0
00000012 5.38837115 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus=1F
ReadByte=03
00000013 5.38837422 LPTPORT:
00000014 5.38837785 LPTPORT: DoNextTransfer:
00000015 5.38838260 LPTPORT: Sending 0x04 to port 378
00000016 5.38838763 LPTPORT: generating next interrupt...
00000017 5.38849854 LPTPORT: In Isr procedure, ISR_Irql=8
00000018 5.38850524 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 1
00000019 5.38851167 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 27


ReadByte = 04
00000020 5.38851446 LPTPORT:
00000021 5.38851781 LPTPORT: DoNextTransfer:
00000022 5. 38852228 LPTPORT: Sending 0x05 to port 378
00000023 5.38852731 LPTPORT: generating next interrupt...
00000024 5.38863822 LPTPORT: In Isr procedure, ISR_Irql=8
00000025 5.38864465 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 2
00000026 5.38865135 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 2F
ReadByte = 05
00000027 5.38865442 LPTPORT: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
00000091 5.38991464 LPTPORT: DoNextTransfer:
00000092 5.38991939 LPTPORT: Sending 0x0F to port 378
00000093 5.38992442 LPTPORT: generating next interrupt...
00000094 5.39003533 LPTPORT: In Isr procedure, ISR_Irql=8
00000095 5.39004175 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 12
00000096 5.39004874 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = BF
ReadByte = 0F
00000097 5.39005181 LPTPORT:
00000098 5.39005516 LPTPORT: DoNextTransfer:
00000099 5.39005963 LPTPORT: Sending 0x00 to port 378
00000100 5.39006494 LPTPORT: generating next interrupt...
00000101 5.39017557 LPTPORT: In Isr procedure, ISR_Irql=8
00000102 5.39018199 LPTPORT: We are now in DpcForIsr, currentIrql=2
xferCount = 13
00000103 5.39018870 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 07
ReadByte = 00
00000104 5.39019177 LPTPORT:
00000105 5.39019512 LPTPORT: DoNextTransfer: 00000106 5.39019959 LPTPORT: Sending 0x01 to port 378
00000107 5.39020434 LPTPORT: generating next interrupt...
00000108 5.39031469 LPTPORT: In Isr procedure, ISR_Irql=8
00000109 5.39032112 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 14
00000110 5.39032754 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 0F ReadByte = 01
00000111 5.39033061 LPTPORT:
00000112 5.39033425 LPTPORT: DoNextTransfer:
00000113 5.39033872 LPTPORT: Sending 0x02 to port 378
00000114 5.39034374 LPTPORT: generating next interrupt...


00000115 5. 39045493 LPTPORT: In Isr procedure, ISR_Irql=8
00000116 5.39046136 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 15
00000117 5.39046778 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 17 ReadByte = 02
00000118 5.39047058 LPTPORT:
00000119 5.39047393 LPTPORT: DoNextTransfer:
00000120 5.39047840 LPTPORT: Sending 0x03 to port 378
00000121 5.39048315 LPTPORT: generating next interrupt...
00000122 5.39059350 LPTPORT: In Isr procedure, ISR_Irql=8
00000123 5.39059992 LPTPORT: We are now in DpcForIsr, currentIrql=2 xferCount = 16
00000124 5.39060663 LPTPORT: ReadDataSafely, currentIrql=8 ReadStatus = 1F ReadByte = 03
00000125 5.39060942 LPTPORT:
00000126 5.39061333 LPTPORT: We are now in DpcForIsr, all data transmitted.
00000127 5.39104691 LPTPORT: in DispatchRead now
00000128 5.39105166 LPTPORT: DispatchRead: 17 byted transferred.
00000129 5.39139639 LPTPORT: in DispatchClose now
00000130 10.93612205 LPTPORT: in DriverUnload now
00000131 10.93615557 LPTPORT: SymLink \DosDevices\LPTPORT0 deleted


Приложение для тестирования драйвера Example.sys


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

//////////////////////////////////////////////////////////////////// // (Файл ExampleTest.cpp) // Консольное приложение для тестирования драйвера Example.sys // 22-Feb-2003 1.0.0 SVP ////////////////////////////////////////////////////////////////////

// Заголовочные файлы, которые необходимы в данном приложении: #include

#include

#include

#include

// Внимание! Файл Ioctl.h должен быть получен из файла Driver.h // (см. комментрарии к Driver.h) и размещен в одной директории с // данным файлом (TestExam.cpp). #include "Ioctl.h"

// Имя объекта драйвера и местоположение загружаемого файла #define DRIVERNAME _T("Example") //#define DRIVERBINARY _T("C:\\Example\\Example.sys") //#define DRIVERBINARY _T("C:\\Ex\\objchk_w2k\\i386\\Example.sys") #define DRIVERBINARY _T("C:\\Ex\\tester\\Example.sys")

// Функция установки драйвера на основе SCM вызовов BOOL InstallDriver( SC_HANDLE scm, LPCTSTR DriverName, LPCTSTR driverExec ) { SC_HANDLE Service = CreateService ( scm, // открытый дескриптор к SCManager DriverName, // имя сервиса - Example DriverName, // для вывода на экран SERVICE_ALL_ACCESS, // желаемый доступ SERVICE_KERNEL_DRIVER, // тип сервиса SERVICE_DEMAND_START, // тип запуска SERVICE_ERROR_NORMAL, // как обрабатывается ошибка driverExec, // путь к бинарному файлу // Остальные параметры не используются - укажем NULL NULL, // Не определяем группу загрузки NULL, NULL, NULL, NULL); if (Service == NULL) // неудача { DWORD err = GetLastError(); if (err == ERROR_SERVICE_EXISTS) {/* уже установлен */} // более серьезная ощибка: else printf ("ERR: CanТt create service. Err=%d\n",err); // (^^ Ётот код ошибки можно подставить в ErrLook): return FALSE; } CloseServiceHandle (Service); return TRUE; }


// Функция удаления драйвера на основе SCM вызовов BOOL RemoveDriver(SC_HANDLE scm, LPCTSTR DriverName) { SC_HANDLE Service = OpenService (scm, DriverName, SERVICE_ALL_ACCESS); if (Service == NULL) return FALSE; BOOL ret = DeleteService (Service); if (!ret) { /* неудача при удалении драйвера */ }

CloseServiceHandle (Service); return ret; }

// Функция запуска драйвера на основе SCM вызовов BOOL StartDriver(SC_HANDLE scm, LPCTSTR DriverName) { SC_HANDLE Service = OpenService(scm, DriverName, SERVICE_ALL_ACCESS); if (Service == NULL) return FALSE; /* open failed */ BOOL ret = StartService( Service, // дескриптор 0, // число аргументов NULL ); // указатель на аргументы if (!ret) // неудача { DWORD err = GetLastError(); if (err == ERROR_SERVICE_ALREADY_RUNNING) ret = TRUE; // OK, драйвер уже работает! else { /* другие проблемы */} }

CloseServiceHandle (Service); return ret; } // Функция останова драйвера на основе SCM вызовов BOOL StopDriver(SC_HANDLE scm, LPCTSTR DriverName) { SC_HANDLE Service = OpenService (scm, DriverName, SERVICE_ALL_ACCESS ); if (Service == NULL) // Невозможно выполнить останов драйвера { DWORD err = GetLastError(); return FALSE; } SERVICE_STATUS serviceStatus; BOOL ret = ControlService(Service, SERVICE_CONTROL_STOP, &serviceStatus); if (!ret) { DWORD err = GetLastError(); // дополнительная диагностика }

CloseServiceHandle (Service); return ret; }

// Соберем вместе действия по установке, запуску, останову // и удалению драйвера (для обобщения сведений). // (Однако пользоваться этой функцией в данном примере не придется.) /* Закомментируем ее. void Test_SCM_Installation(void) { SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(scm == NULL) // неудача { // Получаем код ошибки и ее текстовый эквивалент unsigned long err = GetLastError(); PrintErrorMessage(err); // см. п. 2.1.5 return; } BOOL res; res = InstallDriver(scm, DRIVERNAME, DRIVERBINARY ); // Ошибка может оказаться не фатальной. Продолжаем: res = StartDriver (scm, DRIVERNAME ); if(res) { //Е Здесь следует разместить функции работы с драйвером .. .. ..


res = StopDriver (scm, DRIVERNAME ); if(res) res = RemoveDriver (scm, DRIVERNAME ); } CloseServiceHandle(scm); return; } */

#define SCM_SERVICE // ^^^^^^^^^^^^^^^^ вводим элемент условной компиляции, при помощи // которого можно отключать использование SCM установки драйвера // в тексте данного приложения. (Здесь Ц использование SCM включено.)

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

int __cdecl main(int argc, char* argv[]) { #ifdef SCM_SERVICE // Используем сервис SCM для запуска драйвера. BOOL res; // Получаем доступ к SCM : SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(scm == NULL) return -1; // неудача

// Делаем попытку установки драйвера res = InstallDriver(scm, DRIVERNAME, DRIVERBINARY ); if(!res) // Неудача, но возможно, он уже инсталлирован printf("Cannot install service");

res = StartDriver (scm, DRIVERNAME ); if(!res) { printf("Cannot start driver!"); res = RemoveDriver (scm, DRIVERNAME ); if(!res) { printf("Cannot remove driver!"); } CloseServiceHandle(scm); // Отключаемся от SCM return -1; } #endif

HANDLE hHandle = // Получаем доступ к драйверу CreateFile( "\\\\.\\Example", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if(hHandle==INVALID_HANDLE_VALUE) { printf("ERR: can not access driver Example.sys !\n"); return (-1); } DWORD BytesReturned; // Переменная для хранения числа // переданных байт // Последовательно выполняем обращения к драйверу // с различными кодами IOCTL:

unsigned long ioctlCode=IOCTL_PRINT_DEBUG_MESS; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input NULL, 0, // Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_PRINT_DEBUG_MESS!" ); return(-1); }

ioctlCode=IOCTL_CHANGE_IRQL; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input NULL, 0, // Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_CHANGE_IRQL!" ); return(-1); }



ioctlCode=IOCTL_TOUCH_PORT_378H; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input NULL, 0, // Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_TOUCH_PORT_378H!" ); return(-1); }

// Следующий тест. Получаем 1 байт данных из драйвера. // По окончании данного вызова переменная xdata должна // содержать значение 33: unsigned char xdata = 0x88; ioctlCode=IOCTL_SEND_BYTE_TO_USER; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input &xdata, sizeof(xdata),// Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_SEND_BYTE_TO_USER!" ); return(-1); }

// Вывод диагностического сообщения в консольном окне: printf("IOCTL_SEND_BYTE_TO_USER: BytesReturned=%d xdata=%d", BytesReturned, xdata);

// Выполнение следующего теста в Windows NT приведет к // фатальному сбою операционной системы (намеренно выполненное // падение ОС может быть полезно при изучении, например, // организации crash dump файла и работы с отладчиком). /* ioctlCode=IOCTL_MAKE_SYSTEM_CRASH; if( !DeviceIoControl( hHandle, ioctlCode, NULL, 0, // Input NULL, 0, // Output &BytesReturned, NULL ) ) { printf( "Error in IOCTL_MAKE_SYSTEM_CRASH!" ); return(-1); } */ // Закрываем дескриптор доступа к драйверу: CloseHandle(hHandle);

#ifdef SCM_SERVICE // Останавливаем и удаляем драйвер. Отключаемся от SCM. res = StopDriver (scm, DRIVERNAME ); if(!res) { printf("Cannot stop driver!"); CloseServiceHandle(scm); return -1;

}

res = RemoveDriver (scm, DRIVERNAME ); if(!res) { printf("Cannot remove driver!"); CloseServiceHandle(scm); return -1; }

CloseServiceHandle(scm); #endif

return 0; }

Сообщения намеренно введены на английском языке. Использование кириллицы в консольных приложениях Windows для правильного отображения на экране требует дополнительного преобразования с использованием функции CharToOem.

ы описания процедуры копирования файлов


Рассмотрим простейший пример взаимодействия информации, вводимой в секциях, управляющих копированием файлов [SourceDisksNames], [SourceDisksFiles], [DestinatonDirs] и [CopyFiles].

[Manufacturer] %ThisMfg%= ModelList ; ссылка на секцию моделей

[ModelList] "ISA Hammer"=InstallHammer, ISA\Hammer

[InstallHammer] ; секция инсталляции конкретной модели CopyFiles=CopyHammerFiles ; секция CopyFiles CopyFiles=CopyHammerHelp ; еще одна секция CopyFiles AddReg=HammerRegSection ; ссылка на секцию AddReg

[DestinationDirs] ; Куда следует выполнять копирование: DefaultDestDir=12 ; по умолчанию -&#62 %windir%\system32\drivers CopyHammerHelp=18 ; стандартная директория для help файлов

[CopyHammerFiles] Hammer.sys ; &#60- будет скопирован в директорию dirid=12 [Copy Hammer Help] Hammer.hlp ; &#60- будет скопирован в директорию dirid=18

[SourceDisksNames] ; Подразумевается, что устанавливаемые файлы ; находятся в том же файловом каталоге, что и данный inf-файл. l="Hammer Driver Files"

[SourceDisksFiles] Hammer.sys=1; Ссылается на единственную запись в [SourceDisksNames] Hammer.hlp=1; Ссылается на единственную запись в [SourceDisksNames]

[Strings] ThisMfg="Big Hammer Manufacturer"

Файл hammer.sys будет скопирован в директорию Windows\Help (Windows XP) или WinNT\Help (Windows 2000) . Немного модифицируем пример: используем в имени секции точку и изменяем направление

[InstallHammer] CopyFiles=CopyLaunchHelp.Section . . . [DestinationDirs] CopyHammerHelp.Section = -1, C:\Hammer

[CopyHammerHelp.Section] Hammer.hlp

От введения суффикса .Section взаимодействие секций не изменяется. Поскольку в секции [DestinationDirs] теперь указано '-1' (абсолютный путь), то файл hammer.hlp будет скопирован в каталог C:\Hammer. В случае, если такой каталог не существует, он будет создан.



Приоритеты прерываний


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

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

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



Приоритеты выполнения программного кода


Поскольку разные процессорные архитектуры реализуют разные подходы к управлению аппаратурой при помощи прерываний, разделяемых по приоритетам, операционная система Windows NT использует идеализированную схему, которая удовлетворяет особенностям всех аппаратных платформ. Практическая сторона данной схемы состоит в использовании процедур слоя аппаратный абстракций HAL, которые и берут на себя специфические особенности аппаратуры, позволяя создавать платформенно-независимый драйверный код.

Основой этой схемы абстрактных приоритетов прерываний является IRQL (interrupt request level) &#8212 уровень запроса на прерывание. Уровень IRQL представляет собой число, определяющее приоритет. Программный код, выполняющийся на данном уровне IRQL, не может быть прерван программным кодом, имеющим равный или более низкий IRQL. В таблице 6.1 приводятся уровни IRQL, используемые в Windows 2000/XP/Server 2003. Именно так уровни IRQL видятся драйверу, независимо от того, на каком процессоре или в какой шинной архитектуре приходится драйверу работать. Важно также и то, что в любой конкретный момент времени каждая инструкция (оператор программного кода) выполняется на одном определенном уровне приоритета со специфическим значением IRQL. Уровень IRQL входит в состав контекста выполнения каждого потока, следовательно, в любой момент времени операционной системе достоверно известен его текущий уровень IRQL.

Таблица 6.1. Уровни IRQL

Генерируется Наименование Назначение
Аппаратным

обеспечением

HIGH_LEVEL Проверка компьютера и шинные ошибки
POWER_LEVEL Прерывание по сбою в энергоснабжении
  IPI_LEVEL Прерывания межпроцессорного взаимодействия для многопроцессорных систем
  CLOCK_LEVEL Интервальный таймер
  PROFILE_LEVEL Таймер профилирования
  DIRQL Платформенно-зависимое число уровней для прерываний устройств ввода/вывода
Программным

обеспечением

DISPATCH_LEVEL Планирование потоков и выполнение отложенных процедурных вызовов (DPC)
  APC_LEVEL Выполнение асинхронных процедурных вызовов (1)
  PASSIVE_LEVEL Уровень нормального исполнения потоков (0)
<
В приведенной схеме значения IQRL аппаратных прерываний лежат в интервале выше DISPATCH_LEVEL и ниже PROFILE_LEVEL. Эти уровни еще встречаются в литературе под названием 'device IRQL', DIRQL &#8212 уровни IRQL устройств. Уровни выше PASSIVE_LEVEL называются повышенными (elevated IRQLs). Программные потоки, работающие на повышенных уровнях, могут быть вытеснены только потоками с более высоким уровнем IRQL. Такой способ работы с потоками называется в литературе dispatching, диспетчеризация.

Потоки, работающие на уровне PASSIVE_LEVEL, попадают под управление планировщика заданий (sheduler). Приоритеты, которые различает планировщик заданий для потоков с уровнем PASSIVE_LEVEL, принимают значения от 0 до 32 (MAXIMUM_PRIORITY) и называются в ряде источников 'приоритетом планирования'

(sheduler priority, 'приоритет планировщика').

Между потоками PASSIVE_LEVEL, имеющими приоритеты планирования Real-Time и Normal имеется существенное различие. Первые продолжают свою работу до тех пор, пока не появится поток с большим приоритетом, так что потоки низких приоритетов должны дожидаться, пока текущий поток RealTime не завершит работу естественным путем. Потоки с приоритетами Normal планируются по другим правилам. Для работы им выделяется определенный квант времени, после чего управление передается другим потокам такого же приоритета. Время от времени планировщик может повышать приоритет отложенного потока в пределах диапазона Normal, в результате чего все программные потоки среди потоков этой группы, даже имеющие самые низкие приоритеты, рано или поздно получают управление.

Таблица 6.2. Приоритеты планирования для потоков уровня PASSIVE_LEVEL IRQL

Приоритеты Наименование Назначение
RealTime

(Приоритеты реального

времени)
HIGH_PRIORITY (31)

:
Приоритеты системных программных потоков (программного кода режима ядра)
:

LOW_REALTIME_PRIORITY (16)

 
Normal

(Динамические приоритеты)
Normal maximum (15)

:
Приоритеты потоков пользовательских приложений
:

Normal Idle (1)

 
LOW_PRIORITY (0) Системный поток обнуления страничной памяти
<


Драйвер имеет возможность создавать системные программные потоки с приоритетами планирования, укладывающимися в диапазон RealTime и регулировать их приоритеты в этом диапазоне при помощи вызова KeSetPriorityThread (рекомендуемым DDK документацией значением для этой операции является LOW_REALTIME_PRIORITY, равное 16, см. заголовочные файлы wdm.h или ntddk.h). Подробнее вопросы работы с программными потоками будут рассмотрены в главе 10. Получить текущее значение приоритета планирования известного потока можно при помощи вызова KeQueryPriorityThread, в то время как получить текущее значение IRQL (при работе внутри самого потока режима ядра) можно при помощи вызова KeGetCurrentIrql, как это было сделано в коде драйвера Example, см. главу 3.

Значение приоритета системного потока сразу после его создания (без искусственного изменения) в Windows XP равно 8.

Что касается программных потоков пользовательского режима, то они могут иметь как приоритеты планирования из диапазона Normal, так и более низкие. Например, приоритет THREAD_BASE_PRIORITY_IDLE имеет численное значение &#8212 15.


Прямой доступ к памяти


Прямой доступ к памяти (Direct Memory Access, DMA) использует выгоды от вовлечения в работу вторичного процессора, называемого контроллером DMA (DMA controller, DMAC, контроллер ПДП). Этот контроллер является вспомогательным процессором с ограниченным набором возможностей и обязанностей, но при этом с достаточным интеллектом и статусом, чтобы выполнять передачу данных между устройством и оперативной памятью. Контроллер DMA работает параллельно с основным процессором (процессорами), и обычно его деятельность почти незаметна в общем функционировании системы.

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

Следует обратить особое внимание на то, что в Windows NT при DMA операциях всегда используются логические адреса (трюк с памятью, схожий с виртуальной адресацией, но предназначенный специально для DMA операций), позволяющие "обмануть" DMA контроллер, для которого логические адреса кажутся непрерывными, хотя и могут представлять физически разрывные области памяти.

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

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



Проблема приоритетов времени выполнения


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

Разработчик драйвера должен всегда следить за приоритетом текущего программного кода и документированным уровнем IRQL вызываемых системных функций. В противном случае возможен (а иногда &#8212 просто неминуем) крах операционной системы, а ответ придется искать уже в crach dump файле.

Показательным случаем неправильного использования приоритетов является вызов другого драйвера при помощи IoCallDriver, который формально может быть выполнен на уровнях IRQL APC_LEVEL и DISPATCH_LEVEL. Повысив текущий приоритет потока перед вызовом IoCallDriver до значения, например, DISPATCH_LEVEL, драйвер, скорее всего, организует блокировку: если вызываемый драйвер должен работать на уровне PASSIVE_LEVEL, то он не может сделать нужные ему вызовы, пока работает вызвавший его поток, который ждет возвращения управления из IoCallDriver. Система приходит в неработоспособное состояние ("замерзает").



Проблемы, возникающие при перемещении кода в страничную память


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

Соответственно, если пользователь прекращает работу с драйвером вызовом CloseHandle

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

Контроль за количеством обращений к драйверу можно вести при помощи системных вызовов InterlockedIncrement и InterlockedDecrement, обеспечивающих безопасное обращение к счетчику, который можно разместить, например, в расширении структуры устройства (device extension), состав которой полностью зависит от воли разработчика драйвера.

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



Процедура AdapterControl


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

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



Процедура AddDevice


Процедура AddDevice, выполняющая в WDM драйверах работу по подготовке устройства к работе будет подробно рассмотрена в следующей главе в контексте вопросов, связанных с PnP методологией и использованием стека WDM драйверов.



Процедура CancelRoutine


Драйвер должен учитывать возможность того, что какой-либо из IRP запросов (текущий или находящийся в состоянии ожидания обработки) может быть отменен. Простейшая ситуация, которая к этому ведет, возникает, когда пользовательское приложение, не дождавшись отклика от устройства и драйвера, попросту снимается при помощи диспетчера задач. Тем временем драйвер, будто ничего не случилось, пытается завершить порученные ему задачи. Диспетчер ввода/вывода выполняет удаление IRP запросов, находящихся в ожидании обработки, и мог бы сообщить об этом драйверу вызовом callback процедуры CancelRoutine (под таким именем она фигурирует в документации DDK), если бы тот зарегистрировал ее при помощи вызова IoSetCancelRoutine. Особенность применения этих процедур состоит в том, что при помощи вызова IoSetCancelRoutine

можно назначать разные CancelRoutine процедуры для отслеживания разных экземпляров IRP пакетов. Более того, даже на разных уровнях работы драйвера можно заново определять процедуру CancelRoutine (необходимо только хранить указатели на интересующие IRP пакеты), а то и вовсе отменить отслеживание уничтожения IRP пакетов &#8212 указав NULL вместо адреса CancelRoutine при вызове IoSetCancelRoutine.

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

(см. заголовочный файл winbase.h или документацию MSDN). Правда, применение этого вызова требует, чтобы доступ к драйверу был получен с использованием флага FILE_FLAG_OVERLAPPED, а этот прием программисты применяют крайне редко. Подробнее о CancelRoutine см. в 9 главе.



Процедура ControllerControl


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

В Windows NT эта абстракция реализована в виде объектов контроллера, которые создается вызовом IoAllocateController. Как правило, процедура, запускающая операцию ввода/вывода, выполняет запрос "владения" объектом контроллера при помощи вызова IoAllocateController, одновременно устанавливая процедуру, которая получит управление, как только запрос на владение будет удовлетворен. Эта callback процедура известна в литературе под именем ControllerControl. По завершении обработки запроса ввода/вывода драйвер освобождает контроллер вызовом IoFreeController.

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

Объекты контроллера в модели WDM не поддерживаются, то есть разработчики DDK ограничили применение этой абстракции только драйверами "в-стиле-NT" (legacy драйверами).



Процедура DriverEntry


Диспетчер ввода/вывода вызывает данную процедуру при загрузке драйвера. Возможно, это произойдет в процессе загрузки операционной системы, но драйвер может быть загружен и динамически в любое время, как это было сделано с драйвером Example.sys в главе 3. Процедура DriverEntry решает первоочередные задачи, в частности, регистрацию в специальном массиве адресов других драйверных процедур (для того, чтобы Диспетчер ввода/вывода имел возможность позже производить их вызов по адресу). В случае если драйвер не работает по модели WDM (legacy драйвер, драйвер "в-стиле-NT"), то в этой же начальной процедуре можно провести локализацию оборудования, которое будет обслуживать драйвер, выделить или подтвердить использование аппаратных ресурсов (портов ввода/вывода, прерываний, каналов DMA) и предоставить имя, видимое всей остальной системе, для каждого обнаруженного устройства. Для WDM драйверов, поддерживающих спецификацию PnP, процесс определения аппаратных ресурсов откладывается на более позднее время и выполняется драйверной процедурой AddDevice.

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


Здесь все запросы будут приводить к вызову функции myPassIrpDown, которая занимается только лишь переадресацией запросов нижним драйверам в стеке WDM драйверов. Исключение составит функция-обработчик IOCTL запросов, которые будут поступать в функцию DeviceControlRoutine фильтр-драйвера.

Помимо регистрации функций, процедура DriverEntry драйвера "в-стиле-NT" может выполнять следующую работу:

DriverEntry определяет аппаратное обеспечение, которое драйвер будет контролировать. Это аппаратное обеспечение выделяется драйверу, то есть помечается как находящееся под управлением данного драйвера.

Если драйвер управляет многокомпонентным (multiunit) или многофункциональным контроллером, используется IoCreateController для создания объекта контроллера, после чего инициализируется структура расширения контроллера.

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

Созданные устройства затем делаются видимыми для приложений пользовательского режима путем выполнения вызова IoCreateSymbolicLink.

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

Шаги с 3 по 5 повторяются для каждого физического или логического устройства, работающего под управлением данного драйвера.

В случае успешного завершения, функция DriverEntry должна возвратить Диспетчеру ввода/вывода значение STATUS_SUCCESS.

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


Регистрация такой процедуры выполняется системным вызовом IoRegisterDriverReinitialization (см. таблицу 8.2).

Таблица 8.2. Параметры системного вызова IoRegisterDriverReinitialization

VOID IoRegisterDriverReinitialization IRQL == PASSIVE_LEVEL
Параметры Регистрирует функцию драйвера для отложенной инициализации
IN PDRIVER_OBJECT pDriverObject Указатель на объект драйвера
IN PDRIVER_REINITIALIZE DriverReinitializationRoutine Указатель на процедуру реинициализации, предоставляемую драйвером (см. таблицу 8.3 ниже).
IN PVOID Context Контекстный указатель, который получит регистрируемая функция при вызове
Возвращаемое значение void
Таблица 8.3. Описание параметров вызова myReinitializeFunction

VOID myReinitializeFunction IRQL == PASSIVE_LEVEL
Параметры Функция драйвера, регистрируемая для выполнения отложенной инициализации
IN PDRIVER_OBJECT pDriverObject Указатель на объект драйвера
IN PVOID Context Контекстный блок, указанный при регистрации
IN ULONG Количество вызовов процедуры ре-инициализации (отсчет от 0)
Возвращаемое значение void
Как было указано в главе 7, процедуру DriverEntry можно разместить в "отстреливающемся" сегменте кода INIT.


Процедура DriverEntry и предварительные объявления


Все приведенные ниже отрывки кода следует последовательно поместить в один файл (обычно, файл, содержащий описание DriverEntry, разработчики называют Init.c). Редактирование, разумеется, удобнее всего выполнять в каком-нибудь редакторе интегрированной среды. Рекомендуется использовать редактор из среды Visual Studio, поскольку в нем производится динамический контроль синтаксиса и типов данных языка С. В главе 2 приводится содержимое файлов настройки проекта для драйвера Example, соблюдение которых позволит воспользоваться динамическими подсказками среды во время редактирования и позволит также выполнять контрольную компиляцию кода. Последнее весьма удобно, поскольку в интегрированной среде легко перейти к месту возникновения ошибки по диагностическому сообщению.

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

///////////////////////////////////////////////////////////////////// // init.cpp: Инициализация драйвера // Замечание. Рабочая версия данного драйвера должна быть // скомпилирована как не-WDM версия. В противном случае - драйвер // не сможет корректно загружаться и выгружаться с использованием // программы monitor (пакет Numega Driver Studio) и сервисов SCM // Менеджера.

///////////////////////////////////////////////////////////////////// // DriverEntry Главная точка входа в драйвер // UnloadRoutine Процедура выгрузки драйвера // DeviceControlRoutine Обработчик DeviceIoControl IRP пакетов ///////////////////////////////////////////////////////////////////// #include "Driver.h"

// Предварительные объявления функций: NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp ); VOID UnloadRoutine(IN PDRIVER_OBJECT DriverObject); NTSTATUS ReadWrite_IRPhandler( IN PDEVICE_OBJECT fdo, IN PIRP Irp ); NTSTATUS Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp); NTSTATUS Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);


// Хотя и нехорошо делать глобальные переменные в драйвере... KSPIN_LOCK MySpinLock; #pragma code_seg("INIT") // начало секции INIT ///////////////////////////////////////////////////////////////////// // (Файл init.cpp) // DriverEntry - инициализация драйвера и необходимых объектов // Аргументы: указатель на объект драйвера // раздел реестра (driver service key) в UNICODE // Возвращает: STATUS_Xxx

extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_OBJECT fdo; UNICODE_STRING devName;

#if DBG DbgPrint("=Example= In DriverEntry."); DbgPrint("=Example= RegistryPath = %ws.", RegistryPath->Buffer); #endif

// Экспорт точек входа в драйвер (AddDevice объявлять не будем) // DriverObject->DriverExtension->AddDevice= OurAddDeviceRoutine; DriverObject->DriverUnload = UnloadRoutine; DriverObject->MajorFunction[IRP_MJ_CREATE]= Create_File_IRPprocessing; DriverObject->MajorFunction[IRP_MJ_CLOSE] = Close_HandleIRPprocessing; DriverObject->MajorFunction[IRP_MJ_READ] = ReadWrite_IRPhandler; DriverObject->MajorFunction[IRP_MJ_WRITE] = ReadWrite_IRPhandler; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]= DeviceControlRoutine; //======================================================== // Действия по созданию символьной ссылки // (их нужно было бы делать в OurAddDeviceRoutine, но у нас // очень простой драйвер и эта процедура отсутствует): RtlInitUnicodeString( &devName, L"\\Device\\EXAMPLE" );

// Создаем наш Functional Device Object (FDO) и получаем // указатель на созданный FDO в нашей переменной fdo. // (В WDM драйвере эту работу также следовало бы выполнять // в процедуре OurAddDeviceRoutine.) При создании FDO // будет выделено место и под структуру расширения устройства // EXAMPLE_DEVICE_EXTENSION (для этого мы передаем в вызов // ее размер, вычисляемый оператором sizeof): status = IoCreateDevice(DriverObject, sizeof(EXAMPLE_DEVICE_EXTENSION), &devName, // может быть и NULL FILE_DEVICE_UNKNOWN, 0, FALSE, // без эксклюзивного доступа &fdo); if(!NT_SUCCESS(status)) return status;



// Получаем указатель на область, предназначенную под // структуру расширение устройства PEXAMPLE_DEVICE_EXTENSION dx = (PEXAMPLE_DEVICE_EXTENSION)fdo->DeviceExtension; dx->fdo = fdo; // Сохраняем обратный указатель

// Применяя прием условной компиляции, вводим функцию DbgPrint, // сообщения которой мы сможем увидеть в окне DebugView, если // выполним сборку нашего драйвера как checked (отладочную) // версию: #if DBG DbgPrint("=Example= FDO %X, DevExt=%X.",fdo,dx); #endif

//======================================= // Действия по созданию символьной ссылки // (их нужно было бы делать в OurAddDeviceRoutine, но у нас // очень простой драйвер): UNICODE_STRING symLinkName; // Сформировать символьное имя: // #define SYM_LINK_NAME L"\\??\\Example" // Такого типа символьные ссылки ^^ проходят только в NT. // (То есть, если перенести бинарный файл драйвера в // Windows 98, то пользовательские приложения заведомо // не смогут открыть файл по такой символьной ссылке.) // Для того, чтобы ссылка работала в и Windows 98 и в NT, // необходимо поступать следующим образом: #define SYM_LINK_NAME L"\\DosDevices\\Example" RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME ); dx->ustrSymLinkName = symLinkName;

// Создаем символьную ссылку status = IoCreateSymbolicLink( &symLinkName, &devName ); if (!NT_SUCCESS(status)) { // при неудаче v удалить Device Object и вернуть управление IoDeleteDevice( fdo ); return status; } // Теперь можно вызывать CreateFile("\\\\.\\Example",...); // в пользовательских приложениях

// Объект спин-блокировки, который будем использовать для // разнесения во времени выполнения кода обработчика // IOCTL запросов. Инициализируем его: KeInitializeSpinLock(&MySpinLock);

// Снова используем условную компиляцию, чтобы выделить код, // компилируемый в отладочной версии и не компилируемый в // версии free (релизной): #if DBG DbgPrint("=Example= DriverEntry successfully completed."); #endif return status; } #pragma code_seg() // end INIT section


Процедура IoCompletion


Драйвер модели WDM, работающий внутри многослойной драйверной структуры, может испытывать потребность в том, чтобы его уведомляли, когда будет завершена обработка IRP запроса, посланного драйверу нижнего уровня. Для этой цели драйвер может зарегистрировать callback процедуру IoCompletion (так она называется в документации DDK), при помощи которой он может выполнять весьма впечатляющие трюки. В частности, драйвер может разбивать крупные операции ввода/вывода на более мелкие. Механизм достаточно прост: по окончании частичного переноса данных, управление оказывается в процедуре IoCompletion, которая тут же начинает следующий перенос данных. Регистрация этой callback процедуры выполняется при помощи вызова IoSetCompletionRoutine.



Процедура обратного вызова Bugcheck


В том случае, если драйверу необходимо получить управление при крахе системы, он должен реализовать callback-процедуру Bugcheck. Эта процедура, при должной ее регистрации, будет вызвана ядром операционной системы в соответствующем месте выполнения 'crash'-процесса &#8212 сколь ни противоречиво звучит эта фраза.



Процедура обслуживания прерываний


Процедура обслуживания прерываний (Interrupt Service Routine, ISR), входящая в набор процедур драйвера, вызывается диспетчером прерываний ядра (Kernel's interrupt dispatcher) всякий раз, когда устройство генерирует сигнал прерывания. На этой процедуре лежит обязанность полного обслуживания аппаратного прерывания.

Собственно в ISR процедуре драйвера должна быть реализована самая минимальная обработка создавшейся ситуации. Если дополнительная, требующая больших временных затрат обработка прерывания требуется по логике работы устройства, то следует прибегнуть к использованию механизма DPC (отложенных процедурных вызовов), то есть запланировать отложенный процедурный вызов в текущей процедуре обработки прерываний (ISR). После этого, остаток работы ISR процедуры можно завершить на уровне IRQL ниже уровня аппаратных прерываний (DIRQL), понизив приоритет выполняемого кода данных комплексом мер.



Процедура обслуживания прерываний ISR


При возникновении прерываний, диспетчер прерываний (из состава кода ядра) производит вызов драйверной ISR процедуры. Процедура ISR обычно выполняет следующие действия:

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

Освобождает (завершает) прерывание.

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

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

В случае, если произошла ошибка или передача данных не была завершена, то ISR могла бы запланировать вызов DPC процедуры для того, чтобы выполнить пост-обработку при более низком уровне IRQL.



Процедура ре-инициализации


Некоторые драйверы, по природе обслуживаемого ими устройства, иногда не могут завершить процесс инициализации во время работы DriverEntry. Это может иметь место, если драйвер зависит от других драйверов или системных служб, которые еще не загружены к моменту вызова DriverEntry. Такого типа драйвера должны сделать запрос определенного вида, чтобы их инициализация была отложена. Отложенная инициализация будет выполнена вызовом процедуры ре-инициализации ReInitialize, адрес которой необходимо сообщить во время работы DriverEntry вызовом IoRegisterDriverReinitialization, см. таблицу 8.2.



Процедура Shutdown


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



Процедура StartIo


Выполняя регистрацию рабочей процедуры StartIo, драйвер соглашается участвовать в процессе, называющемся System Queuing, то есть создание очередей необработанных запросов при помощи системных средств &#8212 в отличие от Driver Queuing&#8212 ведение учета необработанных запросов средствами собственно драйвеpa (Для создания и ведения внутренних очередей драйверы используют вызовы KeInitalizeDeviceQueue

и прочие Ke...Queue).

В том случае, если какая-либо из рабочих процедур (например, обработки запросов Read-Write) не может завершить обработку запроса сразу, то она помечает текущий IRP пакет как 'pending' при помощи вызова, а на самом деле &#8212 макроопределения (см. файлы wdm.h или ntddk.h) IoMarkIrpPending, таблица 9.12. После этого она делает вызов IoStartPacket и возвращает управление Диспетчеру ввода/вывода с кодом STATUS_PENDING.

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

Как правило, процедура StartIo организует внутреннюю сортировку поступающих в нее IRP пакетов по кодам IRP_MJ_Xxx (например, обычным оператором 'switch'). Завершается работа StartIo тем, что текущий IRP пакет помечается как обработанный вызовом IoCompleteRequest (таблица 9.10), после чего выполняется вызов IoStartNextPacket, что побуждает Диспетчера ввода/вывода вызывать StartIo снова &#8212 для следующего из оставшихся в очереди IRP пакетов.

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


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

Размещение необработанных пакетов в очереди производится на принципах FIFO, то есть в конец очереди. Однако можно изменить это правило, задавая определенные значения параметра Key в вызове IoStartPacket. Изменить порядок извлечения очередного необработанного пакета из очереди можно, задавая определенные значения параметра Key в вызове IoStartNextPacketByKey.

Процедура StartIo регистрируется во время работы DriverEntry записью ее адреса в поле DriverStartIo в структуре объекта драйвера (что похоже на регистрацию процедуры AddDevice).


Процедура Unload


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

Таблица 8.4. Описание прототипа функции Unload

VOID Unload IRQL == PASSIVE_LEVEL
Параметры Выполняет завершающие действия
IN PDRIVER_OBJECT pDriverObject Указатель на объект драйвера
Возвращаемое значение void

Хотя действия процедуры Unload могут меняться от драйвера к драйверу, общими являются следующие шаги, характерные более для драйверов "в-стиле-NT".

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

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

Символьная ссылка должна быть удалена из пространства имен, видимого пользовательскими приложениями. Это выполняется при помощи вызова IoDeleteSymbolicLink.

Объект устройства должен быть удален вызовом IoDeleteDevice.

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

Следует выполнить освобождение памяти, выделенной драйверу, во всех типах оперативной памяти.

Драйверы WDM модели выполняют практически все из описанных выше действий в обработчике IRP_MJ_PNP запросов с субкодом IRP_MN_REMOVE (то есть посвященном удалению устройства из системы).


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

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

Процедура выгрузки Unload


Диспетчер ввода/вывода выполняет вызов драйверной процедуры Unload в случае, если драйвер выгружается динамически, то есть не в связи с откатом (завершением работы) операционной системы. Эта процедура должна выполнить действия, обратные работе DriverEntry, так чтобы не осталось "числящихся" за драйвером системных ресурсов. Эти действия включают удаление системного имени устройства и символьной ссылки, которые были предоставлены во время инициализации, объекта прерывания, если он был задействован, занятых областей памяти &#8212 поскольку за их удаление никто, кроме самого драйвера, в режиме ядра не отвечает. В драйверах WDM удаление ресурсов устройств (включая имена) выполняется при удалении каждого устройства (если драйвер обслуживает несколько устройств) в процессе выполнения RemoveDevice.

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



Процедуры DPC


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



Процедуры инициализации драйвера и очистки


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



Процедуры обратного вызова для синхронизации доступа к ресурсам


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

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

Один из методов, применяемых для синхронизации в пределах программного кода режима ядра, состоит в том, что предоставляется адрес callback процедуры (процедуры обратного вызова), которую следует использовать для синхронизации доступа к определенным ресурсам. Когда у драйвера появляется необходимость в доступе к совместно используемым ресурсам, то при содействии Диспетчера ввода/вывода он помещает свой запрос в очередь на доступ к такому ресурсу. Когда ресурс становится доступным, Диспетчер ввода/вывода вызывает предоставленную драйвером процедуру обратного вызова, ассоциированную с запросом. Разумеется, это означает, что все поступившие запросы от всех потоков выстраиваются последовательно, и в каждый конкретный момент ресурсом "владеет" только один из таких потоков.

Существует три типа callback процедур, поддерживаемых Диспетчером ввода/вывода, о чем ниже.



Процедуры отложенного вызова обслуживания прерываний DpcForIsr


Вызов процедуры DpcForIsr может быть запланирован из собственно ISR процедуры вызовом IoRequestDpc. Прототип этой функции драйвера описан в таблице 8.13.

Таблица 8.13. Прототип функции для процедуры DpcForIsr

VOID DpcForIsr IRQL == DISPATCH_LEVEL
Параметры Завершает обработку прерывания, начатую в ISR процедуре драйвера
IN PKDPC pDpc DPC-объект
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, для которого зарегистрирована данная DPC процедура
IN PIRP pIrp Интересующий пакет IRP
IN VOID pContext Контекстный указатель, переданный вызовом IoRequestDpc
Возвращаемое значение void

При анализе прототипа вызова IoRequestDpc и примера кода перед таблицей 8.13, возникает правомерный вопрос: вызов какой функции планирует ISR процедура, выполняя вызов IoRequestDpc? Ведь DpcForIsr функция не фигурирует ни при регистрации ISR процедуры вызовом IoConnectInterrupt, ни в тексте OnInterrupt.

Ответ состоит в том, что DpcForIsr регистрируется для конкретного объекта устройства вызовом IoInitializeDpcRequest (см. таблицу 8.14). Если обратить внимание на структуру DEVICE_OBJECT, описанную в заголовочных файлах wdm.h и ntddk.h (см. пакет DDK), то несложно заметить поле 'KDPC Dpc', предназначенное как раз для хранения DPC объекта. Соответственно, выполняя вызов IoRequestDpc

c указателем на объект устройства в качестве первого аргумента, ISR процедура драйвера однозначно определяет, какая именно DpcForIsr функция будет вызвана позже &#8212 хранящаяся в объекте устройства.

Таблица 8.14. Прототип вызова IoInitializeDpcRequest

VOID IoInitializeDpcRequest IRQL == PASSIVE_LEVEL
Параметры Регистрирует DpcForIsr процедуру для данного объекта устройства, создает и инициализирует DPC объект
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, для которого регистрируется данная DPC процедура
IN VOID (*DpcForIsr) Адрес DpcForIsr процедуры, которая должна иметь прототип, описанный в таблице 8.13
Возвращаемое значение void

Регистрацию DpcForIsr процедуры (и ее связывание с объектом устройства) для PnP драйверов рекомендуется делать в процедуре AddDevice драйвера.

Первый параметр pDpc в описании прототипа процедуры DpcForIsr (таблица 8.13) не должен пугать разработчика. Если используется вызов IoInitializeDpcRequest, то именно он создает данный объект, а в вызов DpcForIsr поступит указатель на уже готовый DPC объект.



Процедуры передачи данных


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

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

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

Драйвер Example.sys для обслуживания указанных запросов зарегистрировал функции ReadWrite_IRPhandler и DeviceControlRoutine.



Процедуры SynchCritSection


Обслуживание прерывания происходит на одном из уровней аппаратных DIRQL (что зависит от типа устройства), в то время как весь остальной код драйвера работает на уровне приоритетов не выше DISPATCH_LEVEL. В случае, если когда-либо этому относительно низкоприоритетному коду понадобится поработать с ресурсами, которые использует и ISR (процедура обслуживания прерываний) драйвера, то эти действия должны выполняться только внутри callback процедуры SynchCritSection. Тот программный код, которому хочется корректно обратиться к ресурсам, что, возможно, затребует неожиданно вступившая в права высокоприоритетная процедура обслуживания прерываний, должен воспользоваться посредническими услугами callback процедуры SynchCritSection. Запустить эту callback процедуру необходимо вызовом KeSynchronizeExecution

из программного кода, который работает на любом из уровней не выше DIRQL прерывания. Сама процедура SynchCritSection будет работать с приоритетом прерывания, поэтому пребывание внутри ее кода следует минимизировать (постоянное требование для кода высоких уровней IRQL).

Таким образом, фрагмент кода с изначально низким уровнем IRQL через процедуру обратного вызова получает возможность сделать работу при уровне DIRQL устройства, не опасаясь, что в это время управление будет передано в ISR процедуру. По окончании SynchCritSection прежнее значение IRQL восстанавливается.



Process, Process Object


Процесс, объект процесса.

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



Программа DebugPrint


Весьма интересная программа DebugPrint (полное название — Debug Print Monitor), была разработана фирмой PHD Computer Consultant Ltd., с программными продуктами которой подробнее можно ознакомиться на интернет сайте phdcc.com.

Совместно с программой пользовательского режима DebugPrint работает драйвер режима ядра DebugPrt.sys (разработанный в PHD CC Ltd.), который должен быть установлен заранее. Доступ к DebugPrt.sys клиентский драйвер должен получить средствами специального программного кода (использующего, в частности, редкий для драйверного кода вызов ZwCreateFile). Разработчик присоединяет к отладочной версии своего драйвера (в данной ситуации &#8212 клиентского по отношению к DebugPrt.sys) этот специальный программный код, предоставляемый в форме исходного текста на языке С, обеспечивая таким образом связь с DebugPrt.sys и вывод сообщений в его внутренние буферные области. Работа собственно программы Debug Print Monitor состоит в том, чтобы выводить в своем рабочем окне сообщения отладочного характера, переданные драйверу DebugPrt.sys клиентским драйвером.

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

Для разработчика драйвера использование функций передачи выглядят почти что как использование функций printf или sprintf. Однако следует изначально использовать приемы условной компиляции, чтобы использование этого отладочного трюка не навредило в окончательной версии драйвера (где такие операции являются непозволительной роскошью и при слишком обильной диагностике &#8212 могут приводить к сбою системы), а переход от отладочной (checked) версии к релизной (free) и наоборот отнимал бы минимум усилий у разработчика. При правильном применении условной компиляции разработчику достаточно выбрать среду компиляции DDK (checked/free), что уже определяет полностью версию собранного проекта (см. п. 2.1.2).

Драйвер DebugPrt.sys, необходимый для работы программы Debug Print Monitor, устанавливается при помощи мастера установки оборудования и inf файла и запускается в процессе загрузки операционной системы.



Программа DebugView


Программа DebugView (рисунок 2.15) позволяет наблюдать в своем рабочем окне текст сообщений, которые во время своей работы выводит драйвер, если он использует специальные отладочные printf-подобные функции, такие как вызовы режима ядра DbgPrint в Windows NT или Win32 вызов OutputDebugString. Пример использования этих функций (а именно &#8212 DbgPrint) в программном коде драйвера режима ядра можно увидеть в следующей главе (на примере драйвера Example.sys).

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

Рис. 2.15

Программа DebugView


Для просмотра сообщений, которые выдают драйверы при работе инициализационных процедур (например, DriverEntry или AddDevice), но которые загружаются при запуске системы, можно использовать следующий прием. В случае, если драйвер был инсталлирован при помощи Мастера Установки и inf файла, в результате чего драйвер виден в Панели Настроек в окне Диспетчера Устройств следует выполнить отключение устройства, после чего из системы выгрузится драйвер (в частности, отработает процедура Unload). (Заметим, кстати, что между включением и отключением можно произвести замену файла драйвера на новую версию &#8212 если это необходимо). После этого необходимо выполнить там же, в Диспетчере Устройств, включение драйвера &#8212 в русскоязычной версии Windows это действие обозначено словом "задействовать". В результате будет динамически загружен ранее отключенный драйвер, а его работа начнется с вызова процедуры DriverEntry и т.д., что позволит увидеть все диагностические сообщения для программы DebugView (разумеется, если разработчик их предусмотрел).



Программа Depends


Программа Depends предназначена для просмотра вызовов дополнительных библиотек. Программа Depends была создана в 1996 году и ранее поставлялась в составе Visual Studio 6. Теперь она является частью Platform SDK.

Скриншот (снимок экрана), представленный на рисунке 2.2, выполнен для просмотра дерева вызовов известного драйвера GiveIo.Sys. (Этот драйвер разблокирует доступ к портам ввода/вывода из приложений пользовательского режима при работе под Windows NT, используя при этом недокументированные возможности Windows.)

На приведенном рисунке видно, что драйвер обращается к функциям, экспортируемым NTOSKRNL.EXE, причем видно, что первыми (в порядке алфавита) являются вызовы IoCreateDevice, IoCreateSymbolicLink, IoDeleteDevice, IoDeleteSymbolicLink и др.

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

Рис. 2.2

Программа Depends для драйвера GiveIo



Программа DevCon


Консольное приложение DevCon из состава вспомогательных утилит пакета DDK позволяет запускать, останавливать драйверы и собирать информацию об отдельных устройствах или их группах. Например, по команде devcon stack =usb >stack_usb.txt

(собрать информацию обо всех устройствах в стеке USB) данная утилита выводит в файл stack_usb.txt следующую информацию (немного изменен формат): PCI\VEN_1106&DEV_3038&SUBSYS_12340925&REV_1A\3&61AAA01&0&3A Name: VIA Rev 5 USB Setup Class: {36FC9E60-C465-11CF-8056-444553540000} USB Controlling service: usbuhci

PCI\VEN_1106&DEV_3038&SUBSYS_12340925&REV_1A\3&61AAA01&0&3B Name: VIA Rev 5 USB Setup Class: {36FC9E60-C465-11CF-8056-444553540000} USB Controlling service: usbuhci

USB\ROOT_HUB\4&1E8F7657&0 Name: Setup Class: {36FC9E60-C465-11CF-8056-444553540000} USB Controlling service: usbhub

USB\ROOT_HUB\4&BD5C5B2&0 Name: Setup Class: {36FC9E60-C465-11CF-8056-444553540000} USB Controlling service: usbhub

4 matching device(s) found.



Программа DevCtl


Консольное приложение DevCtl из состава вспомогательных утилит пакета DDK проводит тестирование драйвера путем применения к нему наиболее употребительных вызовов ввода/вывода пользовательского режима, например NtCreateFile

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

В качестве указателя на тестируемый драйвер программе DevCtl следует передавать имя устройства (из числа тех, что видны в директории Device в рабочем окне программы WinObj, описание которой см. ниже). Более подробное описание программы DevCtl можно найти в DDK.



Программа DeviceTree


В составе Windows DDK поставляется программа DeviceTree (рисунок 2.5), абсолютно незаменимая при самостоятельном изучении WDM модели, поскольку визуализирует представление о стеке драйверов (устройств) в операционной системе Windows.

Данная программа выполняет построение дерева устройств с двух точек зрения: с точки зрения принадлежности объектов устройств драйверам (режим D, рисунок 2.6) и с точки зрения взаимной подчиненности объектов устройств при выполнении операции перечисления устройств, enumeration process (режим P, рисунок 2.7). Программа позволяет отслеживать подчиненность объектов устройств в локальных стеках драйверов, их принадлежность драйверам, выявлять существующие фильтр-драйверы, устанавливать (выяснять) коды IPP пакетов, обслуживание которых объявил драйвер, и некоторую другую специфическую информацию.

Рис. 2.5

Заставка программы DeviceTree

На рисунке 2.6 показан фрагмент дерева устройств на участке шинного драйвера PCI. 11 объектов устройств (за исключением безымянного нижнего) представляют созданные этим шинным драйвером физические объекты устройств (так называемые PDO) для всех имеющихся в системе реальных PCI устройств, включая мосты (PCI-PCI, PCI-ISA), контроллеры USB, AGP адаптер, аудио на материнской плате, PCI адаптер Ethernet и т.п.

Рис. 2.6

Шинный драйвер PCI и его объекты устройств

Другой взгляд на этот же участок драйверного стека приведен на рисунке 2.7. Здесь показана взаимная подчиненность объектов устройств, возникающая в операционной системе при последовательно проводимой переписи устройств. Данное дерево отражает в своей структуре иерархию реальных устройств и очередность их обнаружения драйверами родительских устройств. Например, шинный драйвер PCI обнаруживает подключенные к шине устройства, что приводит к загрузке их драйверов, что, в свою очередь, приводит к обнаружению новых устройств, подключенных к ним — как в случае с шиной USB (ee контроллер является дочерним устройством шины PCI).

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

Рис.2.7

Драйверный стек от ACPI драйвера до шинного драйвера PCI



Программа DevView от Уолтера Оней


Программа DevView (полное название Device Object Viewer), созданная Уолтером Оней, автором замечательной книги "Programming The Microsoft Windows Driver Model", вышедшей уже в двух изданиях, является полным аналогом программы WinObj, правда, работающим только под Windows NT.



Программа ErrLook


Программа ErrLook, поставляемая и в составе DDK и в составе Visual Studio, представляет собой декодер сообщений об ошибках. Полученный в приложениях пользовательского режима код ошибки (целое число) по вызову функции GetLastError несет мало информации для недостаточно квалифицированного пользователя. Получить по этому коду текстовое представления сообщения об ошибке можно с помощью специальных функций (при программировании приложения), либо подставив это число в программу ErrLook, как это показано на рисунке 2.3.

Рис. 2.3

Программа ErrLook

Предположим, рабочая процедура драйвера возвратила управление Диспетчеру ввода/вывода, установив код завершения обработки пакета IRP равным STATUS_DEVICE_POWERED_OFF. В результате, если пользовательское приложение обратилось к драйверу с вызовом DeviceIoControl, но драйвером ответил отказом такого рода, то Диспетчер ввода/вывода передает приложению код ошибочного завершения вызова функции (для DeviceIoControl это 0) и устанавливает код ошибки (известный в документации MSDN под названием "system error code"). B свою очередь, пользовательское приложение может вызвать функцию GetLastError

и в результате получит число 21 (которое и определил Диспетчер вода/вывода), что соответствует ошибке ERROR_NOT_READY — "The device is not ready". В примере, представленном на рисунке 2.3, программа ErrLook транслирует код 21 в текстовое сообщение, которое в русскоязычной версии Windows выдается в нижнем окошке на русском языке.

Следует обратить внимание, что, например, коду ошибки 21 (который выдается функцией GetLastError пользовательского режима в пользовательских приложениях) соответствует сразу несколько кодов завершения обработки IRP пакетов в драйвере:

STATUS_DEVICE_NOT_CONNECTED

STATUS_DEVICE_NOT_READY

STATUS_DEVICE_OFF_LINE

STATUS_DEVICE_POWER_FAILURE

STATUS_DEVICE_POWERED_OFF

Эту особенность неоднозначного соответствия кодов ошибок в пользовательском режиме (system error code) и кодов завершения обработки запросов на ввод/вывод от Диспетчера BB (IRP запросов к драйверу) следует учитывать при выборе соответствующих значений STATUS_Xxx для достоверного информирования клиентов драйвера об ошибочных ситуациях.

Транслировать код ошибки в текстовую форму программно можно при помощи несложной функции, текст которой приведен ниже. Функция получает код ошибки (system error code, например, 21) и выводит текст, расшифровывающий это значение. #include &#60winbase.h&#62 #include &#60stdio.h&#62 void PrintErrorMessage(DWORD err) { LPTSTR msg; DWORD res= ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, NULL, err, // код ошибки 0, // идентификатор языка по-умолчанию (LPTSTR) &msg, 0, NULL); if(res == 0) { /* неудача */ } else { /* успешное завершение */ printf("%s",msg); // вывод сообщения на экран LocalFree(msg); // освобождение буфера с текстом сообщения } return; }



Программа GuidGen (UUIDGEN)


Программа GuidGen (UUIDGEN — ее консольная версия) выполняет генерацию 128 разрядного уникального ключа (GUID — глобально уникальный идентификатор), который может использоваться для регистрации интерфейса драйвера, в процессе инсталляции и т.п. Вероятность повторения данного значения весьма и весьма низка (хотя и не равна нулю), так что программа GuidGen является типовым инструментом для этих целей. Программа встречается во всех пакетах Microsoft.

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

Значения GUID можно встретить и в программах, работающих по технологии СОМ, и в Системном Реестре, где они часто используются как идентификаторы классов устройств или входят в состав имен параметров, описывающих устройства.

Рис. 2.4

Программа GuidGen



Программа Monitor от CompuWare Corporation


Программа Monitor от Numega (теперь CompuWare Corporation) позволяет динамически загружать, запускать, останавливать и выгружать драйверы, выполненные в-стиле-NT (не-WDM), в большинстве случаев без перезапуска системы и без создания собственной программы загрузки драйвера при помощи SCM сервисов, а также без использования inf файлов и системного Менеджера Устройств. Таким образом, достаточно подготовить лишь .sys файл и затем воспользоваться программой Monitor.

Вообще говоря, имеются и иные программы с данным сервисом, однако Monitor от CompuWare Corporation имеет наиболее завершенный вид (младшие версии работали еще с VxD драйверами) и удобный графический интерфейс.

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

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

Рис. 2.10

Программа Monitor