Библиотека Интернет Индустрии I2R.ru |
|||
|
Драйверы режима ядра Windows 2000: Часть 3: Простейшие драйверыВот мы и добрались до исходного текста простейших драйверов. Полнофункциональные нас ждут впереди. Все исходные тексты драйверов я буду оформлять в виде *.bat файла, который, на самом деле, является комбинацией *.bat и *.asm файлов, но имеет расширение .bat.
Если такой "самокомпилирующийся" файл запустить, то произойдет следущее. Первые две команды закомментарены, поэтому, они игнорируются компилятором masm, но принимаются командным процессором, который, в свою очередь, игнорирует символ "точка с запятой". Управление передается на метку :make, за которой находятся инструкции для компилятора и компоновщика. Все, что находится за директивой ассемблера end, игнорируется компилятором masm. Таким образом, весь текст между командой goto make и меткой :make, игнорируется командным процессором, но принимается компилятором masm. А все, что вне (включая команду goto make и метку :make), игнорируется компилятором masm, но принимается командным процессором. Этот метод чрезвычайно удобен, т.к. исходный текст "помнит" с какими параметрами его нужно компилировать. Я буду применять такую технику в исходных текстах драйверов, а в исходных текстах программ управления, буду пользоваться обычным методом. Параметры компоновки имеют следующий смысл:
Самый простой драйвер режима ядра Вот исходный текст простейшего драйвера режима ядра.
Как и у любого другого выполнимого модуля, у драйвера должна быть точка входа, на которую система передаст управление после загрузки драйвера в память. Как и полагается в программе на ассемблере, точкой входа является первая инструкция, обозначенная меткой указанной в директиве end. У нас, как и в текстах на с, это DriverEntry, которая оформлена в виде процедуры, принимающей два параметра. Имя процедуры, естественно, может быть любым. Прототип DriverEntry выглядит так:
К сожалению, Microsoft отошла от принципа "венгерской нотации" при составлении заголовочных файлов и документации DDK. Возможно, это связано с большим количеством специфических типов данных, используемых в DDK. Хотя, в обозначении типов кое-что осталось. В исходных текстах я буду придерживаться этого принципа везде, где только возможно, т.к. настолько привык им пользоваться, что исходники не использующие "венгерскую нотацию" мне кажутся совершенно нечитабельными. Поэтом, легким движением руки, DriverObject превращается в pDriverObject, а RegistryPath в pusRegistryPath. Типы данных PDRIVER_OBJECT и PUNICODE_STRING определены в файлах \include\w2k\ntddk.inc и \include\w2k\ntdef.inc соответственно.
О формате данных UNICODE_STRING следует сказать особо. В отличие от режима пользователя, режим ядра оперирует строками в формате UNICODE_STRING. Эта структура определена в файле \include\w2k\ntdef.inc следующим образом:
Главное достоинство этого формата в том, что он явно определяет, как текущую длину строки, так и ее максимально возможную длину. Это позволяет, при операциях с такой строкой, обойтись без некоторых дополнительных вычислений. Почему в процедуру DriverEntry передаются именно эти два указателя? Потому, что доступ к ним (особенно к первому) является ключевым моментом в инициализации и последующей жизни драйвера. Подробнее об этом мы поговорим в следующих статьях. Пока же, мы рассматриваем простейшие драйверы, время жизни которых, ограничено временем выполнения процедуры DriverEntry. Что же мы можем тут полезного (или вредного) сделать? Ну, вредного хоть отбавляй. Мы ведь уже в нулевом кольце защиты. Можно, например, выполнить такой код:
Это приведет к остановке системы и появлению BSOD (Blue Screen Of Death). А выполнение такого кода приведет к перезагрузке компьютера:
Такой радикальный способ, прервать попытку исследования программы, иногда встречается в защитах. Честно говоря, я и сам на это не раз попадался ;-) В этих двух случаях, процедура DriverEntry никогда не вернет
управление. Поэтому, возвращаемое ей значение не важно. Если же действия
выполняемые DriverEntry будут более конструктивными, как, например, в
драйвере beeper.sys, то надо вернуть системе некое значение, указывающее
на то, как прошла инициализация драйвера. Если вернуть STATUS_SUCCESS, то
инициализация считается успешной, и драйвер остается в памяти. Любое
другое значение STATUS_* указывает на ошибку, и в этом случае драйвер
выгружается системой. Вышеприведенный драйвер
(\src\Article2-3\simplest\simplest.sys) является самым простым, какой
только можно себе представить. Единственное что он делает, это позволяет
себя загрузить. Т.к. ничего кроме этого он сделать больше не может, то
возвращает код ошибки STATUS_DEVICE_CONFIGURATION_ERROR. Я просто подобрал
подходящее по смыслу значение (полный список можно посмотреть в файле
\include\w2k\ntstatus.inc). Если возвратить STATUS_SUCCESS, то драйвер так
и останется болтаться в памяти без дела, и выгрузить его средствами SCM
будет невозможно, т.к. мы не определили процедуру отвечающую за выгрузку
драйвера. Эта процедура должна находиться в самом драйвере. Она выполняет
действия, зеркальные по отношению к DriverEntry. Если драйвер выделил себе
какие-то ресурсы, например, память, то в процедуре выгрузки эта память
должна быть возвращена системе. И только сам драйвер знает об этом. Но,
тут я немного забежал вперед. Пока нам это не понадобится. Драйвер режима ядра beeper.sys Теперь перейдем к рассмотрению драйвера, программу управления которым, мы писали в прошлый раз. Мне пришлось переименовать его из beep.sys в beeper.sys, потому что, как оказалось, в NT4 и в некоторых версиях XP уже существует драйвер beep.sys. Вобще говоря, beep.sys есть во всех версиях NT (\%SystemRoot%\System32\Drivers\beep.sys), но он еще должен быть зарегистрирован в реестре. Как бы там ни было, надеюсь beeper.sys будет уникальным. Вот его исходный текст:
Задача этого драйвера, исполнять на системном динамике восходящее до-мажорное арпеджио. Что это такое, вы, наверное уже послушали. Для этого драйвер использует инструкции процессора in и out, обращаясь к соответствующим портам ввода-вывода. Общеизвестно, что доступ к портам ввода-вывода - это свято охраняемый Windows NT системный ресурс. Попытка обращения к любому из них, как на ввод, так и на вывод, из режима пользователя, неизбежно приводит к завершению приложения. Но, на самом деле, есть способ обойти и это ограничение, т.е. обращаться к портам ввода-вывода прямо из третьего кольца. В этом вы убедитесь ниже. Правда, для этого, опять таки, нужен драйвер. На материнской плате находится устройство системный таймер, который является перепрограммируемым. Таймер содержит несколько каналов, 2-ой управляет системным динамиком компьютера, генерируя прямоугольные импульсы с частотой 1193180/<начальное значение счетчика> герц. Начальное значение счетчика является 16-битным, и устанавливается через порт 42h. 1193180 Гц - частота тактового генератора таймера. Тут есть одна тонкость, которую я не совсем понимаю. Функция QueryPerformanceFrequency из kernel32.dll действительно возвращает значение 1193180. Оно просто жестко зашито в тело функции. Но дизассемблировав hal.dll, в функции HalMakeBeep я обнаружил несколько другое значение, равное 1193167 Гц. Его я и использую. Возможно, здесь учтена какая-то временная задержка, или что-то подобное. В любом случае, пищать системным динамиком нам это никак не помешает. Я не буду подробно останавливаться на описании системного таймера. Эту тему очень любят мусолить почти в каждой книжке по программированию на ассемблере. Достаточно подробную информацию можно найти в сети. Итак, первый звук до-мажорного арпеджио мы воспроизводим пользуясь процедурой MakeBeep1.
Выводом в порт 43h двоичного числа 10110110, мы помещаем в управляющий регистр таймера значение, определяющее номер канала, которым мы будем управлять, тип операции, режим работы канала и формат счетчика.
Затем, в порт 42h выводим 16-битное начальное значение счетчика. Сначала младший байт, затем старший.
И, наконец, посредством вывода в порт 61h значения, с установленными 0-ым и 1-ым битами, включаем динамик.
Даем данамику позвучать некоторое время, пользуясь макросом DO_DELAY. Да - примитивно, но - эффективно ;-)
И выключаем динамик, сбрасывая два младших бита. При этом надо не забывать, что таймер - это глобальный системный ресурс. Поэтому, на время работы с регистрами таймера, мы запрещаем аппаратные прерывания. Второй звук (ми) мы воспроизводим посредством процедуры MakeBeep2, тем же самым образом, но используя для обращения к портам ввода-вывода функции WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll. Помимо этих двух, в модуле hal.dll имеется целый набор подобных функций. Они призваны скрыть межплатформенные различия. Вспомните, что я говорил про HAL в первой части статьи. Для процессора alpha, например, внутренняя реализация этих функций будет совершенно другой, но для драйвера ничего не изменится. Я использовал эти функции для разнообразия. Просто, чтобы показать, что такие функции есть. Третий звук (соль) мы воспроизводим пользуясь функцией HalMakeBeep, находящейся в модуле hal.dll. Внутри этой функции происходят события, полностью аналогичные двум предыдущим случаям. Опять же, имеется в виду модуль hal.dll для платформы x86. При этом, в качестве параметра, нужно использовать не частное частоты тактового генератора таймера и начального значения счетчика, а само значение частоты, которую мы хотим воспроизвести. В начале файла beeper.bat определены все 12 нот. Я использую только до, ми и соль. Остальные оставлены для вашего будущего супер-пуппер синтезатора ;-). Для выключения динамика, надо вызвать HalMakeBeep еще раз, передав в качестве аргумента 0. На этом работу драйвера beeper.sys можно считать законченной. Он возвращает системе код ошибки и благополучно удаляется из памяти. На всякий случай повторяю: код ошибки нужно вернуть, только для того, чтобы система удалила драйвер из памяти. Все что мог, он уже сделал. Когда мы доберемся до полнофункциональных драйверов, то, естественно, будем возвращать STATUS_SUCCESS. Программа scp.exe производит загрузку драйвера beeper.sys по требованию. Для того, чтобы закончить с этим вопросом, думаю, будет уместно попробовать загрузить его автоматически, раз уж мы так подробно разобрали этот вопрос в прошлый раз. Проще всего это сделать так: закомментарьте вызов функции DeleteService, в вызове функции CreateService замените SERVICE_DEMAND_START на SERVICE_AUTO_START, а SERVICE_ERROR_IGNORE на SERVICE_ERROR_NORMAL, перекомпилируйте csp.asm и запустите. В реестре останется соответствующая запись. Теперь можете забыть об этом до следующей перезагрузки системы. Драйвер beeper.sys сам напомнит о себе, а в журнале событий системы останется запись о произошедшей ошибке. Посмотреть на нее можно с помощью оснастки Администрирование > Просмотр событий (Administrative Tools > Event Viewer).
Рис. 3-1. Сообщение об ошибке Не забудьте удалить после этого подраздел реестра, соответствующий
драйверу beeper.sys, иначе до-ми-соль будут звучать при каждой загрузке.
Драйвер режима ядра giveio.sys Теперь рассмотрим программу управления другим драйвером - giveio.sys.
Ничего нового в самой процедуре загрузки нет, за исключением нескольких моментов.
Перед запуском драйвера, мы создаем в подразделе реестра, соответствующем драйверу, дополнительный параметр ProcessId, и устанавливаем его значение равным идентификатору текущего процесса, т.е. процесса программы управления. Обратите внимание на то, что вызывая макрос $CTA0, я указываю метку szProcessId, которой будет помечен текст "ProcessId", для того, чтобы позже к нему обратиться. Если добавление параметра прошло без ошибок, то запускаем драйвер. Зачем нужен этот дополнительный параметр вы узнаете позже, когда мы будем разбирать текст драйвера.
Получив управление от функции StartService, мы считаем, что драйвер успешно отработал и устанавливаем флаг fOK. Вызов функции RegDeleteValue делать не обязательно. Все равно, весь раздел реестра будет удален последующим вызовом DeleteService. Просто, я стараюсь придерживаться в программировании правила "хорошего тона": нагадил - подотри ;-)
Удалив драйвер из базы данных SCM и закрыв все открытые описатели, мы вызывает процедуру DateTime, предварительно проверив флаг fOK. На материнской плате компьютера имеется специальная микросхема, выполненная по технологии CMOS (Complementary Metal-Oxide Semiconductor, Металл-Окисел-Полупроводник с Комплементарной структурой, КМОП), и питающаяся от батарейки. В этой микросхеме реализован еще один таймер, называемый часами реального времени (Real Time Clock, RTC), который работает постоянно, даже при выключенном питании компьютера. Помимо таймера, в этой микросхеме имеется небольшой блок памяти, в котором хранится собственно текущее время, а также кое-какая информация о физических параметрах компьютера. Достаточно подробно об этом можно узнать в справочнике "Ralf Brown's Interrupt List". Получить содержимое памяти CMOS можно обратившись к портам ввода-вывода 70h и 71h.
Сначала устанавливаем удобный нам формат данных, которые мы будем получать, используя управляющий регистр B. Хотя, по умолчанию, он и так установлен, но тем не менее. Нам удобно получать данные в упакованном двоично-десятичном формате (в одном байте две цифры - по 4 бита на каждую). Поскольку, у нас принята 24-часовая система деления суток, то этот формат мы и устанавливаем. Затем, используя макрос CMOS, выдергиваем по одному байту нужной нам информации, попутно форматируя получающуюся строку.
Получив текущие дату и время, составляем из них единую строку и выводим ее на экран. Управляющая последовательность \t вставляет символ горизонтальной табуляции, а \n перевода строки (подробнее см. \Macros\Strings.mac). И на экране мы должны увидеть:
Рис. 3-2. Результат работы программы DateTime.exe Самым странным, в вышеприведенном тексте, является обращение к портам ввода-вывода прямо из режима пользователя. Как я уже упомянул выше, доступ к портам ввода-вывода свято охраняется Windows NT. И тем не менее, мы к ним обратились. Это стало возможно благодаря драйверу giveio.sys, к рассмотрению исходного текста которого мы и переходим.
Код драйвера основан на хорошо известных изысканиях Дейла Робертса, восходящих аж к 96 году прошлого века, в области предоставления процессу режима пользователя доступа к портам ввода-вывода на платформе Windows NT. Я решил, что здесь это будет очень кстати. Перевод статьи Дейла Робертса "Прямой ввод-вывод в среде Windows NT" можно почитать http://void.ru/?do=printable&id=701. Я не буду подробно останавливаться на теории, т.к. достаточно подробно это описано в вышеупомянутой статье. Если очень коротко, то процессор поддерживает гибкий механизм защиты, позволяющий операционной системе предоставлять доступ к любому подмножеству портов ввода-вывода для каждого отдельно взятого процесса. Это возможно благодаря карте разрешения ввода-вывода (I/O Permission Map, IOPM). Немного подробнее про эту карту здесь: http://www.sasm.narod.ru/docs/pm/pm_tss/chap_5.htm. Про сегмент состояния задачи (Task State Segment, TSS), также активно принимающий в этом участие, можно почитать там же: http://www.sasm.narod.ru/docs/pm/pm_tss/chap_3.htm. Каждый процесс может иметь свою собственную IOPM. Каждый бит в этой
карте соответствует байтовому порту ввода-вывода. Если он (бит)
установлен, то доступ к соответствующему порту запрещен, если сброшен -
разрешен. Поскольку, пространство портов ввода-вывода в архитектуре x86
составляет 65535, то максимальный размер IOPM равен 2000h байт. Для
манипулирования IOPM в модуле ntoskrnl.exe имеются две полностью
недокументированные функции: Ke386QueryIoAccessMap и Ke386SetIoAccessMap.
Приведу их описание составленное стараниями Дейла Робертса и моими тоже.
Копирует текущую IOPM размером 2000h из TSS в буфер, указатель на который содержится в параметре pIopm.
При успешном завершении, возвращает в регистре al ненулевое значение.
Копирует переданную IOPM длинной 2000h из буфера, указатель на который содержится в параметре pIopm, в TSS.
При успешном завершении, возвращает в регистре al ненулевое значение.
И еще одна очень полезная, также полностью недокументированная, функция
из модуля ntoskrnl.exe.
Разрешает/запрещает использование IOPM для процесса.
При успешном завершении, возвращает в регистре al ненулевое значение.
По префиксу в имени функции можно определить к какому компоненту она
относится: Ke - ядро, Ob - диспетчер объектов, Ps - поддержка процессов,
Mm - диспетчер памяти и т.д. Для доступа к объектам код режима пользователя использует описатели (handles), которые являются ни чем иным как индексами в системных таблицах, в которых содержится сам указатель на объект. Ну а что такое, на самом деле, объект мы уже немного поговорили выше. Таким образом, посредством описателей система отрезает код режима пользователя от прямого доступа к объекту. Код режима ядра, напротив, пользуется именно указателями, т.к. он и есть сама система и имеет право делать с объектами что хочет. Функция Ke386IoSetAccessProcess требует, в качестве первого параметра, указатель на объект "процесс" (process object), т.е. на структуру KPROCESS (см. \include\w2k\w2kundoc.inc. Я специально поставил префикс "w2k", т.к. в Windows XP недокументированные структуры сильно отличаются. Так что, использовать этот файлик при компиляции драйвера предназначенного для XP, не самая лучшая идея). Код функции Ke386IoSetAccessProcess устанавливает член IopmOffset структуры KPROCESS в соответствующее значение. Раз мы будем вызывать функцию Ke386IoSetAccessProcess, нам потребуется
указатель на объект "процесс". Его можно получить разными способами. Я
выбрал наиболее простой - по идентификатору. Именно поэтому, в модуле
DateTime, мы получаем идентификатор текущего процесса и помещаем его в
реестр. В данном случае мы используем реестр просто для передачи данных в
драйвер. Т.к. процедура DriverEntry выполняется в контексте процесса
System, нет возможности узнать, какой процесс на самом деле запустил
драйвер. Вторым параметром, pusRegistryPath, в процедуре
DriverEntry мы имеем указатель на раздел реестра, содержащий параметры
инициализации драйвера. Мы воспользуемся им, чтобы извлечь из реестра
идентификатор процесса. Теперь можно перейти к разбору кода драйвера giveio.sys.
Для последующего вызова функции ZwOpenKey нам потребуется указатель на заполненную структуру OBJECT_ATTRIBUTES (\include\w2k\ntdef.inc). Для ее заполнения я использую макрос InitializeObjectAttributes. Можно заполнить и "вручную":
Макрос InitializeObjectAttributes находится еще на стадии разработки, так что не советую использовать его способом отличным от приведенного выше. Если что не так - я не виноват ;-)
Вызовом функции ZwOpenKey получаем описатель раздела реестра в переменной hKey. Вторым параметром в эту функцию передаются права доступа, третьим - указатель на структуру OBJECT_ATTRIBUTES, заполненную на предыдущем этапе. С помощью функции ZwQueryValueKey получаем значение идентификатора процесса, записанное в параметре реестра ProcessId. Вторым параметром в эту функцию передается указатель на инициализированную структуру UNICODE_STRING, содержащую имя параметра реестра, значение которого мы хотим получить. Я стараюсь использовать возможности препроцессора masm на "полную катушку", поэтому, и тут использую самописный макрос $CCOUNTED_UNICODE_STRING (все там же - \Macros\Strings.mac). Обратите внимание на то, что я указываю выравнивание строки по границе двойного слова (выравнивание самой структуры UNICODE_STRING жестко прописано в макросе и равно двойному слову). Какой-то особой необходимости в этом тут нет, просто, я даю вам возможность оценить гибкость и удобство моих макросов. Рекламная пауза ;-) Если органически не перевариваете макросы, то можно использовать традиционный способ определения Unicode-строки, и структуры UNICODE_STRING ее содержащей:
Меня этот способ никогда не вдохновлял, поэтому, я и написал для этой цели макросы COUNTED_UNICODE_STRING, $COUNTED_UNICODE_STRING, CCOUNTED_UNICODE_STRING, $CCOUNTED_UNICODE_STRING (см. \Macros\Strings.mac). Третий параметр функции ZwQueryValueKey определяет тип запрашиваемой информации. KeyValuePartialInformation - символьная константа равная 2 (\include\w2k\ntddk.inc). Четвертый и пятый параметры - указатель на структуру KEY_VALUE_PARTIAL_INFORMATION и ее размер соответственно. В члене Data этой структуры мы и получим значение идентификатора процесса. Последний параметр - указатель на переменную, размером DWORD, в которую будет записано количество скопированных из реестра байт. Перед самым вызовом ZwQueryValueKey, мы резервируем на стеке для него место, а после вызова извлекаем значение. Я постоянно пользуюсь таким приемом - очень удобно.
Если вызов ZwQueryValueKey прошел успешно, выделяем с помощью функции MmAllocateNonCachedMemory кусочек памяти в пуле неподкачиваемой памяти (такая память никогда не сбрасывается на диск), размером 2000h байт - максимальный размер карты разрешения ввода-вывода. Сохраняем указатель в переменной pIopm.
Передавая в функцию PsLookupProcessByProcessId полученный ранее идентификатор процесса, получаем указатель на KPROCESS в переменной pProcess. Вызовом функции Ke386QueryIoAccessMap, копируем IOPM в буфер.
Сбрасываем биты соответствующие портам ввода-вывода 70h и 71h, и записываем модифицированную IOPM. Вызовом функции Ke386IoSetAccessProcess разрешаем доступ. Обратите внимание, что Microsoft предусмотрела специальный код ошибки STATUS_IO_PRIVILEGE_FAILED. В принципе, здесь совершенно не важно, какой код ошибки мы вернем системе при выходе из DriverEntry. Я, просто потихоньку, ввожу вас в курс дела.
Предыдущий вызов функции PsLookupProcessByProcessId, увеличил количество ссылок на обьект процесса. Система раздельно хранит количество открытых описателей обьекта и количество предоставленных ссылок на объект. Описателями, в основном, пользуется код режима пользователя, ссылками - только код режима ядра. Пока, хотя бы одно из этих значений, не равно нулю, система не удаляет объект из памяти, считая что он еще используется каким-то кодом. Вызовом функции ObDereferenceObject мы уменьшаем количество ссылок на обьект процесса.
С помощью функции MmFreeNonCachedMemory освобождаем выделенный буфер, и, вызовом функции ZwClose, закрываем описатель раздела реестра. Работа сделана - драйвер больше не нужен. Т.к. он возвращает один из кодов ошибки, система удаляет его из памяти. Но теперь, код режима пользователя имеет доступ к двум портам ввода-вывода, чем он и пользуется, обращаясь к памяти CMOS. В этом примере я обратился к памяти CMOS, просто, для разнообразия. Можно было, как в предыдущем драйвере beeper.sys, попищать системным динамиком. Оставляю это вам, в качестве домашнего задания. Надо будет открыть доступ к соответствующим портам ввода-вывода. Вызвать процедуру MakeBeep1, предварительно убрав из ее тела каманды cli и sti, т.к. выполнять привилегированные команды процессора в режиме пользователя, вам никто не разрешит. Вызывать функции из модуля hal.dll, естественно, тоже нельзя, т.к. они находятся в адресном пространстве ядра. Максимум, что вы можете себе позволить - это предоставить доступ ко всем 65535 портам, одним махом:
Помните только, что баловство с системным динамиком и чтение памяти
CMOS, достаточно безобидное занятие. Но обращение к каким-то другим портам
может быть небезопасно, т.к. в режиме пользователя его невозможно
синхронизировать. Пара слов об отладке Сейчас мы уже более предметно можем поговорить об этом увлекательном процессе. Как я уже говорил в первой части, удобнее всего использовать в качестве отладчика SoftICE. Базовой техникой является расстановка в нужных местах исходного текста отладочного прерывания int 3. При этом нужно убедиться, что в SoftICE включено отслеживание этого прерывания. В более поздних версиях SoftICE, для адресов режима ядра (>80000000h), это сделано автоматически. Проверить это можно с помощью команды i3here. Если отлов int 3 не включен, сделать это можно с помощью той же команды i3here on (выключается - i3here off). Очень советую прописать эту команду прямо в параметры инициализации SoftICE. Если вы забудите это сделать при следующей загрузке системы, и запустите драйвер с таким прерыванием, то BSOD не заставит себя ждать. Есть еще одна команда приводящая к тому же результату - bpint 3. Разница в том, что в первом случае, вы окажетесь в SoftICE на инструкции следующей за int 3, а во втором, прямо на int 3. Можно сделать и так: bpint 3 do "r eip eip+1", но это менее удобно. В коде драйвера giveio я неоднократно вызывал функцию DbgPrint. Эта
функция выводит на консоль отладчика форматированные сообщения. SoftICE
прекрасно их понимает. Можно использовать утилиту DebugView Марка
Руссиновича http://sysinternals.com/ntw2k/utilities.shtml
Что в архиве В архиве к этой статье, помимо исходных кодов примеров и макросов, вы обнаружите:
Что почитать Документацию DDK, помимо сайта http://www.microsoft.com/, можно посмотреть тут: "Windows XP SP1 DDK Documentation On-line". Все Zw* функции и некоторые структуры описаны подробно в книге Гэри Неббета "Справочник по базовым функциям API Windows NT/2000", Издательский дом "Вильямс", 2002. В сети можно найти электронную версию этой книги: Gary Nebbett, "Windows NT-2000 Native API Reference". Вобщем, на первых порах, можно обойтись и без DDK. Если чувствуете, что
чего-то не хватает - ищите в сети. При желании найти можно многое.
Все драйверы я тестировал под Windows 2000 Pro и Windows XP Pro. Но все должно работать и на более ранних выпусках Windows NT. До встречи в следующей статье, где мы поговорим о подсистеме ввода-вывода вообще, и о диспетчере ввода-вывода в частности. Four-F |
|
2000-2008 г. Все авторские права соблюдены. |
|