Как написать драйвер на ассемблере

В предыдущей статье, описывающей алгоритм установки драйвера в систему Windows, мы немного разбирались в том, из каких шагов состоят алгоритмы обнаружения оборудования и установки соответствующих драйверов. Вскоре после этого родилась идея перейти к практической части и осуществить попытку написания собственного, самого простого драйвера на ассемблере. Нет, не каких-то монументальных циклов статей, а исключительно для себя, так сказать, что бы как-то уже начать изучение области разработки драйверов на Ассемблере под операционную систему Windows. После продолжительного обследования предметной области выяснилось, что более-менее серьезная разработка драйверов для Windows ведется на языках программирования C/C++, это объясняется тем, что данные языки являются фактически «языками системы», то есть на них и написаны многие части операционной системы. Однако это вовсе не исключает утверждения, что и на ассемблере вполне себе можно разработать драйвер. Да, соглашусь что делать это не так уж и сподручно, по сравнению с тем же C, поскольку для последнего в качестве серьезного подспорья выпущен специализированный пакет разработки (DDK), но тем не менее все же возможно.

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

Поскольку драйвер у нас будет первым и самым простейшим, то в качестве платформы разработки возьмем 32-битную версию системы Windows. Понимаю что сейчас на дворе уже 21 век, и доминирование 64-битных операционных систем на рынке с каждым годом все заметнее, тем не менее, на этапе «нулевого знания» пример с 32-битным драйверов будет не менее показателен. Соответственно:

Рассматриваемый в статье драйвер является 32-битным, поэтому работа его в 64-битной системе не гарантирована, и даже более того — невозможна.

После изучения некоторого количества материала, найденного в Сети, стало очевидным что не имея опыта программирования в режиме ядра, довольно сложно изобрести что-нибудь оригинальное и, что самое главное, это оригинальное потом еще и реализовать :) Первое, что приходит в незамутненную опытом голову после прочтения теории драйвера — это реализация элементарного драйвера, содержащего одну единственную процедуру инициализации DriverEntry. Как вы уже должны были понять по указанному выше материалу — без неё при написании драйвера нам никак не обойтись, она является «базовой» и должна присутствовать в каждом без исключения драйвере. Но чем эта процедура будет у нас заниматься, какой код мы могли бы в ней разместить? Понятное дело, что для простого драйвера на ассемблере и логика должна быть достаточно простой, поскольку при любом ином раскладе мы рискуем запутать процесс обучения настолько, что начинающие разработчики и вовсе могут потерять к теме интерес. Можно было бы просто-напросто скомпилировать пустой каркас драйвера, указав процедуру DriverEntry с инструкцией возврата ret, но при таком подходе как мы получим какое-либо подтверждение о том, что наш драйвер выполняется? Поэтому, было бы интересно получить какой-нибудь осязаемый результат, например визуальное или аудиальное подтверждение работоспособности. С пониманием, как именно выводить на экран монитора из кода ядра (драйвера) у нас пока тяжеловато, а вот со звуком дела обстоят попроще, поскольку в Сети можно встретить примеры с выводом на встроенный динамик персонального компьютера, всеми уже заезженная вдоль и поперек задача. И в нашем примере я решил реализовать проигрывание известной мелодии под названием «Имперский марш» из фильма Звездные войны (так же известной под названием «тема Дарта Вейдера»). Традиционно, для начала приведем исходный код драйвера:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

format PE native 4.0 at 10000h

entry DriverEntry

include ‘%include%win32w.inc’

include ‘%include%DDKntstatus.inc’

M = 3000000 ; Коэффициент задержки

;— первая октава ————————————————————————————————————————-

C4 =       0106h ; 261.63  Hz  ::  C   ::  (До)

Db4 =       0115h ; 277.18  Hz  ::  Cs  ::  (До диез)

D4 =       0126h ; 293.66  Hz  ::  D   ::  (Ре)

Eb4 =       0137h ; 311.13  Hz  ::  Ds  ::  (Ре диез)

E4 =       014Ah ; 329.63  Hz  ::  E   ::  (Ми)

F4 =       015Dh ; 349.23  Hz  ::  F   ::  (Фа)

Gb4 =       0172h ; 369.99  Hz  ::  Fs  ::  (Фа диез)

G4 =       0188h ; 392.00  Hz  ::  G   ::  (Соль)

Ab4 =       019Fh ; 415.30  Hz  ::  Gs  ::  (Соль диез)

LA4 =       01B8h ; 440.00  Hz  ::  A   ::  (Ля)

Bb4 =       01D2h ; 466.16  Hz  ::  As  ::  (Ля диез)

B4 =       01EEh ; 493.88  Hz  ::  H   ::  (Си)

;— вторая октава ——————————————————————————————————————————————

C5 =       020Bh ; 523.25  Hz  ::  C   ::  (До)

Db5 =       022Ah ; 554.37  Hz  ::  Cs  ::  (До диез)

D5 =       024Bh ; 587.33  Hz  ::  D   ::  (Ре)

Eb5 =       026Eh ; 622.25  Hz  ::  Ds  ::  (Ре диез)

E5 =       0293h ; 659.26  Hz  ::  E   ::  (Ми)

F5 =       02BAh ; 698.46  Hz  ::  F   ::  (Фа)

Gb5 =       02E4h ; 739.99  Hz  ::  Fs  ::  (Фа диез)

G5 =       0310h ; 783.99  Hz  ::  G   ::  (Соль)

Ab5 =       033Fh ; 830.61  Hz  ::  Gs  ::  (Соль диез)

LA5 =       0370h ; 880.00  Hz  ::  A   ::  (Ля)

Bb5 =       03A4h ; 932.33  Hz  ::  As  ::  (Ля диез)

B5 =       03DCh ; 987.77  Hz  ::  H   ::  (Си)

;=== сегмент кода ============================================================

section ‘.text’ code readable executable notpageable

proc DriverEntry DriverObject:DWORD, RegistryPath:DWORD

stdcall PlaySound, sound_buffer

mov eax, STATUS_DEVICE_CONFIGURATION_ERROR

ret

endp

;——————————————————————————

proc PlaySound buffer:DWORD

push eax ebx ecx esi

cld

mov esi, [buffer] ; данные

mov ecx, SIZEOF.sound_buffer / 8 ; размер буфера, счетчик = размер/8 (потому как для каждой записи нота-задержка используется 8 байт)

.loop:

lodsd

pushad

invoke HalMakeBeep, eax ; вывод сигнала необходимой частоты на динамик

popad

.delay:

lodsd

@@: dec eax

jnz @b

pushad

invoke HalMakeBeep, 0 ; отключаем динамик

popad

dec ecx

jnz .loop

pop esi ecx ebx eax

ret

endp

;——————————————————————————

sound_buffer dd G4 ,350*M,G4 ,350*M,G4 ,350*M,Eb4,250*M,Bb4,100*M,G4 ,350*M,Eb4,250*M,Bb4,100*M,G4 ,700*M

dd D5 ,350*M,D5 ,350*M,D5 ,350*M,Eb5,250*M,Bb4,100*M,Gb4,350*M,Eb4,250*M,Bb4,100*M,G4 ,700*M

dd G5 ,350*M,G4 ,250*M,G4 ,100*M,G5 ,350*M,Gb5,250*M,F5 ,100*M,E5 ,100*M,Eb5,100*M,E5 ,450*M

dd Ab4,150*M,Db5,350*M,C5 ,250*M,B4 ,100*M,Bb4,100*M,LA4,100*M,Bb4,450*M

dd Eb4,150*M,Gb4,350*M,Eb4,250*M,Bb4,100*M,G4 ,750*M

SIZEOF.sound_buffer = $sound_buffer

;=== таблица импорта =========================================================

section ‘.idata’ import readable writeable

library hal,‘hal.dll’

import hal,

HalMakeBeep, ‘HalMakeBeep’

;=== таблица перемещений =====================================================

section ‘.relocs’ fixups readable writeable discardable

Самой первой строкой исходного кода является указание на формат выходного (получаемого по завершении процесса компиляции) исполняемого файла. В нашем примере впервые для многих встречается неизвестный нам ранее формат native, за которым следует указание некоего «смещения» со значением 10000h. Как мы уже писали в этой статье, native — это «родной» (нативный) формат приложения, часто используемый для драйверов (фактически библиотек/приложений режима ядра, которым не требуется инициализация подсистемы Win32 на стадии подготовки образа к исполнению). Значение 10000h — это предпочитаемый базовый адрес образа, то есть адрес загрузки драйвера в адресное пространство ядра, по которому он хотел бы расположиться. Тем не менее, загрузчик образов, проверяя значение в поле ImageBase, самостоятельно принимает решение по выбору загрузочного адреса, в подавляющем большинстве случаев загрузчик размещает драйвер по иному адресу. Тем не менее, для того, что бы выполнить подобное перемещение, требуется изменить указатели переходов, переменных и других данных в коде драйвера, ведь компилятор на этапе сборки бинарного модуля жестко привязывает адреса в коде к выбираемому при компиляции базовому смещению. Именно для подобных ситуаций в исполняемый файл включается таблица перемещений (relocation table, секция .relocs в конце), где указаны все адреса, которые требуют изменения. После обработки перемещений производится связывание (fix-up) импорта (адресов функций внешних библиотек).

Почему для драйвера так важно описывать секцию таблицы перемещений в исходном коде, ведь для типовых Win32-приложений эта процедура не является обязательной? Я думаю, что ответ тут заключается в том, что для типового приложения создается «собственное» виртуальное адресное пространство процесса, поэтому загрузчик всегда размещает код по предпочитаемому адресу и перемещений не требуется. А вот для драйвера (библиотеки) он подобного гарантировать не может, поскольку он(а) загружается в уже созданное адресное пространство ядра (процесса) по произвольному динамическому адресу.

В строке 2 кода драйвера мы наблюдаем определение точки входа (директива entry) драйвера на функцию под названием DriverEntry. На основании диалекта FASM, точкой входа является первая инструкция метки, указанной после директивы entry. После прохождения процедуры загрузки драйвера в память, менеджер ввода-вывода вызывает данную функцию, иными словами система передает управление по на первый байт функции DriverEntry после загрузки драйвера в память.

Код функции DriverEntry всегда выполняется в качестве одного из потоков процесса System, в контексте этого же процесса.

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

NTSTATUS DriverEntry( _In_ struct _DRIVER_OBJECT *DriverObject, _In_ PUNICODE_STRING RegistryPath );

или более привычный нам по диалекту FASM:

DriverEntry DriverObject:DRIVER_OBJECT, RegistryPath:UNICODE_STRING

Как мы видим, процедура принимает два входных параметра: указатели на некие структуры с именами DriverObject и RegistryPath предопределенных типов.

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

struc DRIVER_OBJECT

{

    .Type              dw ?

    .Size              dw ?

    .DeviceObject      dd ?

    .Flags             dd ?

    .DriverStart       dd ?

    .DriverSize        dd ?

    .DriverSection     dd ?

    .DriverExtension   dd ?    ; 28

    .DriverName        UNICODE_STRING

    .HardwareDatabase  dd ?

    .FastIoDispatch    dd ?

    .DriverInit        dd ?

    .DriverStartIo     dd ?

    .DriverUnload      dd ?

    .MajorFunction     dd (IRP_MJ_MAXIMUM_FUNCTION + 1) dup (?)

} ; DRIVER_OBJECT

и

struc UNICODE_STRING   ;size 8

{

   .Length             dw ?

   .MaximumLength      dw ?

   .Buffer             dd ?

}

где:

  • DriverObject — указатель на объект только что созданного драйвера (структура DRIVER_OBJECT). Первый параметр, передающийся в функцию DriverEntry. Перед вызовом процедуры DriverEntry диспетчер ввода-вывода, наряду с другими действиями по подготовке кода драйвера к запуску, создает объект «драйвер» (driver object), который представляет ключевые параметры драйвера для системы. Получается, что именно посредством этого объекта система «управляет» нашим драйвером. Объект представляет собой структуру данных типа DRIVER_OBJECT, некоторые поля которой заполняет система, а иные придется инициализировать разработчику, и часто это осуществляется именно в процедуре DriverEntry. Используя этот указатель в коде, мы можем заполнить требуемые нам поля структуры DRIVER_OBJECT, однако в рассматриваемом нами простейшем драйвере в этом нет необходимости.
  • RegistryPath — указатель на раздел реестра (дерево RegistryMachineSystemCurrentControlSetServicesИмя_драйвера), содержащий параметры инициализации драйвера. Фактически это указатель на структуру типа UNICODE_STRING, которая содержит указатель непосредственно на саму Unicode-строку, содержащую имя раздела. Указатель используется в коде драйвера для чтения или записи в реестр ключевой информации, которую драйвер может в дальнейшем использовать. В нашем случае мы, опять же, не работаем с данным параметром.

Сама структура UNICODE_STRING выглядит следующим образом:

Поле Описание
Length Длина строки в байтах (не в символах), без учета нуль-терминатора (символа завершающего нуля);
MaximumLength Длина в байтах (не в символах) буфера, указываемого через член структуры Buffer;
Buffer Указатель непосредственно на Unicode-строку (не всегда, кстати, завершающейся нулем).

Итак, вернемся к рассмотрению исходного кода драйвера. В строка 1034 у нас размещаются константы нот и соответствующие им частоты, из них у нас будет формироваться мелодия, данные которой размещаются в секции данных. Чуть ниже по листингу мы видим функцию DriverEntry, в которой происходит вызов внутренней процедуры с именем PlaySound, являющейся фактически основной полезной нагрузкой драйвера, именно она проигрывает мелодию на встроенном PC-спикере (динамике). Давайте подробнее рассмотрим код самой процедуры PlaySound (размещенный в строках 4871). Начинается он с сохранения регистров, которые меняются в самой процедуре, потом выставляется флаг направления, как вы уже догадались для осуществления цикла чтения нот и их проигрывания. Загружаем в регистр esi указатель на буфер, который указывается входным параметром buffer процедуры, этот буфер должен содержать проигрываемую мелодию в следующем формате:

Частота, Задержка, Частота, Задержка, Частота, Задержка, Частота, Задержка …

В регистр ecx загружается счетчик цикла, он же размерность массива (деленная на 8, поскольку каждая итерация цикла будет проигрывать целую тональность, а она у нас имеет по 4 байта на частоту и 4 на задержку). Следом происходит загрузка в регистр eax двойного слова из памяти, указанной смещением из регистра esi, это у нас частота. И считанная частота передается на вход следующей по коду функции HalMakeBeep, которая работает с динамиком напрямую через порты.

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

Функция HalMakeBeep экспортируется системной библиотекой hal.dll и является, честно говоря, не самым лучшим выбором, поскольку не документирована и, соответственно, официально разработчиками не гарантируется неизменное ее состояние при переходе системы от версии к версии. Тем не менее, что-то более простое для вывода звука придумать сложно, в противном случае для вывода мелодии нам пришлось бы программировать порты напрямую, что создало бы нам для первого раза довольно избыточно-усложненную логику, которая (в свою очередь) потянула бы за собой задачу по изучению (и описанию) принципов работы перепрограммируемого интервального таймер и программируемого контроллера периферийного интерфейса Intel 8255. А библиотека hal.dll предоставляет нам готовую функцию одной из библиотек ядра, являющейся самым «низким» уровнем перед оборудованием компьютера и используемых для сопряжения с аппаратной частью, функции её просты и интуитивно-понятны, и именно поэтому выбор был сделать в её пользу.

Если в коде вызова функции вы активно работаете с регистрами общего назначения (eax, ebx, ecx, edx …), то будьте внимательны, поскольку многие функции режима ядра (как впрочем и некоторые функции Win32 API) в своем теле не сохраняют/не восстанавливают состояния некоторых регистров общего назначения, что в ситуации с кодом режима ядра приводит порой к падению в синий экран. Простым решением проблемы видится самостоятельное сохранение регистров перед вызовом и последующее восстановление.

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

  • задержки указаны в массиве в качестве числа (например: 350*M), поэтому на разных конфигурациях ПК они будут звучать с разным темпом, все зависит от значения константы M (указанной в строке 7).
  • задержки реализованы в коде в виде пустого цикла из пары инструкций dec + jnz (строки 6162), то есть «грузят» ЦП.

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

Итак, вернемся к основному коду, далее в приложении (строка 64) динамик у нас отключается подачей значения 0 на вход функции HalMakeBeep. Ну и после этого код у нас входит в цикл повторения заданное количество раз всех описанных выше действий, перебирая каждую записи в буфере мелодии.
После того, как код функции DriverEntry отработал, управление возвращается диспетчеру ввода-вывода (строки 4445). Но тут вот есть один интересный момент, функция DriverEntry, как и подавляющее большинство других функций Windows, должна вернуть код возврата, сигнализирующий о статусе завершения функции. В штатном режиме, когда инициализация драйвера проходит успешно, функция возвращает статус STATUS_SUCCESS, но в нашем случае мы пишем драйвер-пустышку, который кроме кода своей процедуры инициализации (проигрывание мелодии) ничего не делает, так зачем же он нам в памяти ядра постоянно загруженным? Поэтому есть предложение не оставлять его в памяти, а посему в нашем коде мы возвращаем системе в регистре eax значение STATUS_DEVICE_CONFIGURATION_ERROR (NTSTATUS код 0C0000182h), то есть код фиктивной ошибки, после чего система должна удалить драйвер из памяти (в действительности же этого не происходит и драйвер висит в памяти со статусом Stopped).
Сама мелодия размещается в блоке данных с меткой sound_buffer (строки 7377). Блок содержит, как мы уже отмечали выше, чередующиеся значения констант частот и задержек. Частота у нас задается константами, определенными в начале исходного кода драйвера (строки 10-34) и носящими имена соответствующих нот. Каждой константе (ноте) соответствует своя частота.

Заключение

После компиляции исходного кода нашего с вами драйвера, на выходе мы получаем исполняемый файл с расширением .sys, который, теоретически, готов к выполнению. Осталось дело за малым — заставить драйвер загружаться в адресное пространство ядра. Сделать это можно несколькими способами (будут описаны позже в отдельной статье), и самый простой из них заключается в загрузке драйвера на одном из этапов загрузки операционной системы, путем создания и редактирования ключа реестра HKLMSYSTEMCurrentControlSetServicesимя_драйвера и вложенных параметров.

Краткое содержание.

  1. Цель работы. Постановка задачи.
  2. API для WDM драйвера.
  3. Пишем рыбу.
  4. Детализация.
  5. Компиляция и сборка.
  6. Как правильно установить драйвер.
  7. Тестовая программа.
  8. Напоследок.

Часть 1. Цель работы. Постановка задачи.

Признаюсь честно: мне нравится Ассемблер. Вернее, даже не сам Ассемблер, а стиль общения с компьютером через него.
В сети есть несколько примеров создания драйверов виртуальных устройств VxD на Ассемблере.
Но нет ни одного аналогичного примера для драйвера WDM.
Так исправим же эту досадную оплошность!

Создание несложного драйвера с использованием только лишь Ассемблера — довольно трудоёмкое занятие.
По двум причинам:

  • Отсутствие ассемблерных заголовочных файлов для использования драйверного API.
  • Методически трудная отладка драйверов в системе Windows.

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

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

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

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

Что нам потребуется? Вот что:

1) Текстовый редактор.

Notepad. Но лучше что-нибудь поудобнее, например, Патриот XP.

2) MS Windows DDK.

DDK содержит почти всё, что требуется для создания драйверов. Но нам важны: справка DDK, Ассемблер MASM 6.1, компоновщик Link, также оттуда мы возьмём библиотеки и заголовочные файлы для C (что с ними делать — см. далее).

3) Утилита для визуализации отладочного вывода. Я использую DbgView, который можно взять с сайта www.sysinternals.com

4) Delphi для компиляции тестовой программы.

Но вам необязательно набирать текст с нуля. К счастью, я сделал это до вас :)

Скачайте файл с исходниками проекта AsmDrv и распакуйте его в подкаталог NTDDKsrcAsmDrv.

Вот, кажется, всё. Можно начинать!

Часть 2. API для WDM драйвера.

Большинство функций драйверного API, которые нас интересуют, предоставляются модулем ntoskrnl.exe.

Для их использования надо сделать следующее:

1) Объявить типы данных и определить константы.

Большинство определений для C находятся в файлах ntdef.h и wdm.h.

2) Объявить прототипы функций, которые мы намерены использовать.

Эти определения для C также находятся в wdm.h

3) Выполнить сборку драйвера с подключением библиотеки wdm.lib

Все три файла (wdm.h, ntdef.h и wdm.lib) входят в состав Windows DDK.

Я перевёл часть заголовков на Ассемблер и поместил их в файл usewdm.inc, который находится в базовом каталоге проекта.

Часть 3. Пишем рыбу.

3.1. Итак, приступим.

Вы можете проследить за последовательностью и содержанием действий, открыв файл main.asm для просмотра.

Начнём, пожалуй, так:

.586p ; Процессор Intel Pentium, разрешены инструкции защищённого режима
.model flat, stdcall ; Здесь всё ясно. Плоская модель адресации и тип вызовов stdcall.
option casemap:none ; "case-sensitive"

Дальше нужно задействовать файл включений usewdm.inc и библиотеку wdm.lib, чтобы мы смогли использовать драйверный API:

.include usewdm.inc
.includelib wdm.lib

Затем размещаем два сегмента — данных и кода:

.data

; [...]

.code

; [...]

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

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

У нас такая процедура называется DriverEntry. Объявим её как

Driver Entry proc near public, DriverObject:PDRIVER_OBJECT,
RegistryPath:PUNICODE_STRING

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

Наш драйвер довольно прост. Он будет отрабатывать только 4 стандартных запроса:

  • IRP_MJ_CREATE — Вызов CreateFile() в приложении пользователя для установления связи с драйвером;
  • IRP_MJ_CLOSE — Вызов CloseHandle() в приложении пользователя для разрыва связи с драйвером;
  • IRP_MJ_DEVICE_CONTROL — Вызов DeviceIoControl() в приложении пользователя для запроса выполнения какой-либо функции в драйвере.

Все эти три запроса мы адресуем некоей диспетчерской функции OnDispatch. Мы узнаем о ней позже.

Четвёртый запрос — на выгрузку. Об этом пойдёт речь ниже.

А пока необходимо сделать ещё 2 важные вещи — создать логический объект устройства при помощи функции IoCreateDevice() и символическую связь, имя которой пользовательские приложения будут использовать для связи с драйвером при помощи функции CreateFile(). Символическая связь создаётся при помощи вызова IoCreateSymbolicLink():

; Инициализируем юникодовые строки с именами устройства и линка

invoke RtlInitUnicodeString, offset NtDeviceName, offset wsNtDeviceName
invoke RtlInitUnicodeString, offset Win32DeviceName, offset wsWin32DeviceName

; [...]

; Создаём логический объект устройства

invoke IoCreateDevice, DriverObject, 0, offset NtDeviceName,
; Проверим, не было ли ошибки.
FILE_DEVICE_UNKNOWN,0,FALSE,offset DeviceObject; cmp eax,STATUS_SUCCESS 
jnz @F
; Создаём symbolic link
; в eax останется код результата
invoke IoCreateSymbolicLink, offset Win32DeviceName, offset NtDeviceName 
@@:
ret

Итак, только что мы завершили разбор процедуры инициализации.

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

У нас она реализуется функцией OnUnload. Эта функция производит действия, обратные процедуре инициализации по отношению к связанным объектам: она удаляет символическую связь (вызов IoDeleteSymbolicLink()), и затем логическое устройство, сопоставленное драйверу (IoDeleteDevice()):

; Удаляем символическую связь
invoke IoDeleteSymbolicLink, offset Win32DeviceName
; Удаляем логическое устройство
invoke IoDeleteDevice, DeviceObject

3.4. Главная диспетчерская процедура.

Она называется OnDispatch и объявлена как

OnDispatch proc near, pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

Здесь нам важен указатель на структуру с данными запроса pIrp. Данная структура довольно сложна. Вы можете найти её объявление в файле usewdm.inc.

Но нам понадобятся лишь некоторые данные.

Сначала мы должны определить код запроса — он будет один из трёх: IRP_MJ_CREATE, IRP_MJ_CLOSE или IRP_MJ_DEVICE_CONTROL.

Мы получаем этот код из структуры IO_STACK_LOCATION, указатель на которую мы получаем из структуры IRP (в свою очередь, указатель на IRP был передан нам в пераметре pIrp):

mov ebx,pIrp
; Восстанавливаем указатель на структуру IO_STACK_LOCATION
mov eax,(_IRP ptr [ebx]).Tail.Overlay.CurrentStackLocation 
mov pIrpStack,eax
mov ebx,pIrpStack
; al -- Код сообщения
mov al,(IO_STACK_LOCATION ptr [ebx]).MajorFunction 

Дальше отрабатываем запросы по-разному.

Для IRP_MJ_CREATE и IRP_MJ_CLOSE обработка фиктивная. Мы просто возвращаем код успеха STATUS_SUCCESS в регистре eax.

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

Мы размещаем эти данные в локальных переменных, чтобы потом вызвать вторичную функцию DeviceIoControlHandler, где и будет выполнена обработка.

Часть 4. Детализация.

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

Мы будем отрабатывать 2 запроса:

  • IOCTL_USER_REQUEST_1 — отправка переданной строки в отладочный вывод, и
  • IOCTL_USER_REQUEST_2 — перевод литер переданной строки в нижний регистр.

Коды запросов объявлены в файле-включении ioctlcodes.inc

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

Часть 5. Компиляция и сборка.

Для компиляции программы следует выполнить командный файл assemble.cmd.

Его содержимое:

....binml.exe -coff -Fl -c -Foasmdrv.obj main.asm

В результате мы получим листинг main.lst и объектный модуль asmdrv.obj.

Дальше мы должны собрать бинарник драйвера из объектного модуля. Для этой цели существует команда link.cmd:

....binlink.exe @linkcmd.rsp

в файле linkcmd.rsp размещены настройки линкера. Полный список выглядит так:

-MACHINE:IX86 
-STACK:32768,4096
-OPT:REF
-OPT:ICF
-INCREMENTAL:NO
-FORCE:MULTIPLE
-RELEASE
-DEFAULTLIB:wdm.lib
-DRIVER
-ALIGN:0x20
-SUBSYSTEM:NATIVE
-BASE:0x10000
-ENTRY:DriverEntry@8
-OUT:disk1asmdrv.sys

asmdrv.obj

В результате сборки мы получаем файл AsmDrv.sys в подкаталоге Disk1.

Часть 6. Как правильно установить драйвер.

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

Это так называемый inf-файл.

Опять-таки, к счастью для вас, я уже написал этот файл — asmdrv.inf. Вы можете открыть его для просмотра и изучить.

Файл находится в подкаталоге Disk1 проекта.

Ну что же? — Пробуем установиться.

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

Завершаем установку.

Windows 98 на этом месте может попросить перезагрузки. Не отказывайте ей :)

Windows 2000/XP запускает драйвер сразу.

Вы можете проконтролировать установку, убедившись в наличии устройства «Простейший WDM драйвер на Ассемблере» в списке менеджера устройств.

Поздравляю, если вы всё сделали правильно, наш драйвер — о, чудо! — работает.

Часть 7. Тестовая программа.

Проект тестовой программы расположен в подкаталоге TestApp.

Откройте его в Delphi и перекомпилируйте.

В результате вы получите файл AsmDrvTest.exe, который нужно будет запустить.

В принципе, это одна из самых простых программ в мире. Она занимается отправкой драйверу AsmDrv.sys запросов IOCTL_USER_REQUEST_1 и _2 по требованию пользователя, передавая драйверу строку символов.

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

Одновременно с тестовой программой используйте утилиту DbgView для просмотра отладочного вывода.

Часть 8. Напоследок.

Мы убедились ещё раз, что не боги горшки обжигают.

Следует ли писать драйверы WDM на ассемблере? —

Зависит от желания и возможностей.

Однако вопрос оставлю открытым.

Драйвер — это просто

Время на прочтение
7 мин

Количество просмотров 94K

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

Сперва нам нужно определится в чем мы же будем создавать наш первый драйвер. Поскольку материал ориентирован на новичков, то язык программирования был выбран один из простых, и это не Си или ассемблер, а бейсик. Будем использовать один из диалектов бейсика — PureBasic. Из коробки он не обучен создавать драйверы, но у него удачный набор файлов, используемых для компиляции и небольшое шаманство позволяет добавить эту возможность. Процесс компиляции состоит из нескольких этапов. Если кратко, то он происходит следующим образом: Сначала транслятор «перегоняет» basic-код в ассемблер, который отдается FASM’у (компилятор ассемблера), который создает объектный файл. Далее в дело вступает линкер polink, создающий исполняемый файл. Как компилятор ассемблера, так и линкер могут создавать драйверы и если немного изменить опции компиляции, то получим не исполняемый файл, типа EXE или DLL, а драйвер режима ядра (SYS).

Скачать немного модифицированную бесплатную демо версию PureBasic 4.61 x86 можно на файлопомойке, зеркало.
Если нужно создать драйвер для x64 системы, качайте эту версию, зеркало.

Дистрибутивы имеют небольшие размеры, около 3 МБ каждый. С помощью этой версии можно создавать только драйвера.
Скачиваем, распаковываем и запускаем, кликнув по файлу «PureBasic Portable». При этом запустится IDE и вылезет окошко с сообщением что это демо-версия и списком ограничений. Из него наиболее существенным является ограничение числа строк кода, равное 800, а для создания простых драйверов этого может хватить. Остальные ограничения в нашем случае, не существенны.

Окно IDE с загруженным кодом драйвера показано на скрине.

image

Компиляция драйвера выполняется через меню «Компилятор» (это если кто не понял).
image

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

Код драйвера:

Declare DriverEntry(*DriverObject, *RegistryPath)

!public PureBasicStart
!section '.code' code readable executable align 8
!PureBasicStart:
*A=@DriverEntry()
!jmp [p_A] ; Переход в процедуру DriverEntry().

#IOCTL_MyPlus = $200

!extrn PB_PokeL
    
CompilerSelect #PB_Compiler_Processor
  CompilerCase #PB_Processor_x86
    !extrn _IoCompleteRequest@8 ; Объявление импортируемых функций ядра.
    !extrn _RtlInitUnicodeString@8
    !extrn _IoCreateDevice@28
    !extrn _IoDeleteDevice@4
    !extrn _IoCreateSymbolicLink@8
    !extrn _IoDeleteSymbolicLink@4

    !extrn _PB_PeekI@4
    
    Import "ntoskrnl.lib" 
  CompilerCase #PB_Processor_x64
    
    !extrn IoCompleteRequest; Объявление импортируемых функций ядра.
    !extrn RtlInitUnicodeString
    !extrn IoCreateDevice
    !extrn IoDeleteDevice
    !extrn IoCreateSymbolicLink
    !extrn IoDeleteSymbolicLink
    !extrn PB_PeekI
    
    ImportC "ntoskrnl.lib"
  CompilerEndSelect
  
    
; Импорт функций ядра системы.
  IoCompleteRequest(*IRP, PriorityBoost)
  RtlInitUnicodeString(*UString, *String)
  IoCreateDevice(*DriverObject, DeviceExtensionSize, *UDeviceName, DeviceType, DeviceCharacteristics, Exclusive, *DeviceObject)
  IoDeleteDevice(*DeviceObject)
  IoCreateSymbolicLink(*SymbolicLinkName, *DeviceName)
  IoDeleteSymbolicLink(*SymbolicLinkName)
EndImport

Structure MyData ; Данные, передаваемые в драйвер.
  Plus_1.l
  Plus_2.l
EndStructure


; Прцедура обмена данными с программой.
Procedure DeviceIoControl(*DeviceObject.DEVICE_OBJECT, *pIrp.IRP)
  Protected *Stack.IO_STACK_LOCATION
  Protected *InpBuff, *OutBuff
  Protected InBuffSize, OutBuffSize
  Protected ntStatus, *MyData.MyData
  
  ntStatus = #STATUS_SUCCESS ; Все ОК.
  
  *Stack = *pIrpTailOverlayCurrentStackLocation
  
  ; Размеры буферов (см. WinAPI функцию DeviceIoControl())
  InBuffSize  = *StackParametersDeviceIoControlInputBufferLength
  OutBuffSize  = *StackParametersDeviceIoControlOutputBufferLength
  
  If InBuffSize >= SizeOf(Integer) And OutBuffSize >= 4
    
    Select *StackParametersDeviceIoControlIoControlCode
      Case #IOCTL_MyPlus
        
        *Point = *pIrpSystemBuffer
        If *Point
          *MyData = PeekI(*Point)
          
          If *MyData
            Result.l = *MyDataPlus_1 + *MyDataPlus_2
            PokeL(*pIrpSystemBuffer, Result)
            *pIrpIoStatusInformation = 4
          Else
            ntStatus = #STATUS_BUFFER_TOO_SMALL
            *pIrpIoStatusInformation = 0
          EndIf
          
        EndIf
                
      Default
        ntStatus = #STATUS_UNSUCCESSFUL
        *pIrpIoStatusInformation = 0
    EndSelect
  
  Else
    ntStatus = #STATUS_BUFFER_TOO_SMALL ; Размер буфера слишком мал.
    *pIrpIoStatusInformation = 0
  EndIf
  
  *pIrpIoStatusStatus = ntStatus
  IoCompleteRequest(*pIrp, #IO_NO_INCREMENT)
  
  ProcedureReturn ntStatus
EndProcedure


; Выгрузка драйвера. Вызывается при завершении работы драйвера.
Procedure UnloadDriver(*DriverObject.DRIVER_OBJECT)
  Protected uniDOSString.UNICODE_STRING
  
  ; Инициализация объектов-строк.
  RtlInitUnicodeString(@uniDOSString, ?DosDevices)
  ; Удаление символьной связи.
  IoDeleteSymbolicLink (@uniDOSString)
  
  ; Удаление устройства.
  IoDeleteDevice(*DriverObjectDeviceObject)
EndProcedure


; Вызывается при доступе к драйверу с помощью функци CreateFile().
Procedure CreateDispatch(*DeviceObject.DEVICE_OBJECT, *pIrp.IRP)
  *pIrpIoStatusInformation = 0
  *pIrpIoStatusStatus = #STATUS_SUCCESS
  IoCompleteRequest(*pIrp, #IO_NO_INCREMENT)
  ProcedureReturn #STATUS_SUCCESS
EndProcedure


; Вызывается при осовбождении драйвера функцией CloseHandle().
Procedure CloseDispatch(*DeviceObject.DEVICE_OBJECT, *pIrp.IRP)
  *pIrpIoStatusInformation = 0
  *pIrpIoStatusStatus = #STATUS_SUCCESS
  IoCompleteRequest(*pIrp, #IO_NO_INCREMENT)
  ProcedureReturn #STATUS_SUCCESS
EndProcedure


; Процедура загрузки драйвера. Вызывается однократно при его запуске.
Procedure DriverEntry(*DriverObject.DRIVER_OBJECT, *RegistryPath.UNICODE_STRING)
  Protected deviceObject.DEVICE_OBJECT
  Protected uniNameString.UNICODE_STRING
  Protected uniDOSString.UNICODE_STRING
  
  
  ; Инициализация объектов-строк.
  RtlInitUnicodeString(@uniNameString, ?Device)
  RtlInitUnicodeString(@uniDOSString, ?DosDevices)
  
  ; Создание устройства.
  status = IoCreateDevice(*DriverObject, 0, @uniNameString, #FILE_DEVICE_UNKNOWN, 0, #False, @deviceObject)
  If status <> #STATUS_SUCCESS
    ProcedureReturn status
  EndIf
    
  ; Создане символьной связи между именем этого устройства и именем,
  ; находящимся в видимой области для user-mode, для того, чтобы
  ; приложение могло получить доступ к этому устройству.
  status = IoCreateSymbolicLink(@uniDOSString, @uniNameString)
  If status <> #STATUS_SUCCESS
    IoDeleteDevice(@deviceObject)
    ProcedureReturn status
  EndIf
  
  ; Указатель на функцию выгрузки драйвера.
  *DriverObjectDriverUnload = @UnloadDriver()
  
  *DriverObjectMajorFunction[#IRP_MJ_CREATE] = @CreateDispatch()
  *DriverObjectMajorFunction[#IRP_MJ_CLOSE]  = @CloseDispatch()
  
  ; Указываем какая функция будет обрабатывать запросы WinAPI DeviceIoControl().
  *DriverObjectMajorFunction[#IRP_MJ_DEVICE_CONTROL] = @DeviceIoControl()
	
	ProcedureReturn #STATUS_SUCCESS
EndProcedure



; Имя драйвра (юникод).
DataSection
  Device:
  !du 'DevicepbDrPlus', 0, 0
  
  DosDevices:
  !du 'DosDevicespbDrPlus', 0, 0
EndDataSection

Может показаться что это куча бессмысленного кода, но это не так.

У каждого драйвера должна быть точка входа, обычно у нее имя DriverEntry() и выполнена она в виде процедуры или функции. Как видите, в этом драйвере есть такая процедура. Если посмотрите на начало кода, то в первых строках увидите как ей передается управление. В этой процедуре происходит инициализация драйвера. Там же назначается процедура завершения работы драйвера, которая в нашем случае имеет имя UnloadDriver(). Процедуры CreateDispatch() и CloseDispatch() назначаются обработчиками соединения и отсоединения проги из юзермода.
Процедура DeviceIoControl() будет обрабатывать запросы WinAPI функции DeviceIoControl(), являющейся в данном драйвере связью с юзермодом. В конце кода расположена так называемая ДатаСекция (DataSection), в которой находятся имена драйвера, сохраненные в формате юникода (для этого использована одна из фишек ассемблера FASM).

Теперь рассмотрим как драйвер будет взаимодействовать с внешним миром. Это происходит в процедуре DeviceIoControl(). В ней отслеживается одно сообщение, а именно — #IOCTL_MyPlus, которое отправляет юзермодная прога, когда ей нужно сложить два числа в режиме ядра (круто звучит, правда?). Когда такое сообщение получено, то считываем из системного буфера, адрес указателя на структуру со слагаемыми, производим сложение и результат помещаем в системный буфер. Собственно это основная задача нашего первого драйвера.

Видите сколько понадобилось кода для выполнения простейшей математической операции — сложения двух чисел?

А теперь рассмотрим программу, работающую с этим драйвером. Она написана на том же PureBasic.


#DriverName = "pbDrPlus"
#IOCTL_MyPlus = $200

XIncludeFile "..DrUserModeFramework.pbi"

Structure MyData ; Данные, передаваемые в драйвер.
  Plus_1.l
  Plus_2.l
EndStructure

; Абсолютный путь к файлу-драйверу.
DrFile.s = GetPathPart(ProgramFilename())+#DriverName+".sys"

; Загружает драйвер и если успешно, то порлучаем его хэндл.
hDrv=OpenDriver(DrFile, #DriverName, #DriverName, #DriverName)

If hDrv=0
  ; Деинсталляция драйвера из системы.
  Driver_UnInstall(#DriverName)
  MessageRequester("", "Ошибка загрузки драйвера")
  End
EndIf

; Обмен данными с драйвером.
Procedure.q Plus(hDrv, x1, x2)
  Protected MyData.MyData, Result, *Point
  
  MyDataPlus_1=x1
  MyDataPlus_2=x2
  *Point = @MyData
  DeviceIoControl_(hDrv, #IOCTL_MyPlus, @*Point, SizeOf(MyData), @Result, 4, @BytesReturned, 0)
  
  ProcedureReturn Result
EndProcedure


OpenWindow(1,300,300,140,90,"Title",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)
StringGadget(1,10,10,50,20,"")
StringGadget(2,10,40,50,20,"")
TextGadget(3,70,30,70,20,"")
Repeat
  ev=WaitWindowEvent()
  If ev=#PB_Event_Gadget
    op1=Val(GetGadgetText(1))
    op2=Val(GetGadgetText(2))
    Result = Plus(hDrv, op1, op2)
    SetGadgetText(3,Str(Result))
  EndIf
Until ev=#PB_Event_CloseWindow


; Если драйвер загружен, то закрываем связь с ним.
If hDrv
  CloseHandle_(hDrv)
  hDrv=0
EndIf

; Деинсталляция драйвера из системы.
Driver_UnInstall(#DriverName)

При старте программы вызывается функция OpenDriver(), которая загружает драйвер. Для упрощения, имя драйвера, имя службы и описание службы заданы одинаковыми — «pbDrPlus». Если загрузка неудачная, то выводится соответствующее сообщение и программа завершает свою работу.

Процедура Plus() осуществляет связь с драйвером. Ей передаются хэндл, доступа к драйверу и слагаемые числа, которые помещаются в структуру и указатель на указатель которой, передается драйверу. Результат сложения чисел будет в переменной «Result».

Далее следует код простейшего GUI калькулятора, скопированного из википедии.

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

Результат сложения чисел 8 и 2 на скриншоте.

image

Исходные коды драйвера и программы, можно найти в папке «Examples», PureBasic на файлопомойке, ссылку на который давал в начале статьи. Там так же найдете примеры драйвера прямого доступа к порам компа и пример работы с памятью ядра.

PS.
Помните, работа в ядре чревата мелкими неожиданностями аля, BSOD (синий экран смерти), поэтому экспериментируйте осторожно и обязательно всё сохраняйте перед запуском драйвера.

За возможную потерю данных, я ответственности не несу!

Пишем драйвер WDM на Ассемблере.
(Комментарии к исходникам AsmDrv)

Часть 1. Цель работы. Постановка задачи.

Признаюсь честно: мне нравится Ассемблер. Вернее, даже не сам Ассемблер, а стиль общения с компьютером через него.

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

Но нет ни одного аналогичного примера для драйвера WDM.

Так исправим же эту досадную оплошность!

Создание несложного драйвера с использованием только лишь Ассемблера – довольно трудоёмкое занятие.

По двум причинам:

1) Отсутствие ассемблерных заголовочных файлов для использования драйверного API.

2) Методически трудная отладка драйверов в системе Windows.

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

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

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

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

Что нам потребуется? Вот что:

1) Текстовый редактор.

Notepad. Но лучше что-нибудь поудобнее, например, Патриот XP.

2) MS Windows DDK.

DDK содержит почти всё, что требуется для создания драйверов. Но нам важны: справка DDK, Ассемблер masm 6.1, компоновщик Link, также оттуда мы возьмём библиотеки и заголовочные файлы для C (что с ними делать – см. далее).

3) Утилита для визуализации отладочного вывода. Я использую DbgView , который можно взять с сайта www.sysinternals.com

4) Delphi для компиляции тестовой программы.

Но вам необязательно набирать текст с нуля. К счастью, я сделал это до вас :)

Скачайте файл с исходниками проекта AsmDrv и распакуйте его в подкаталог NTDDKsrcAsmDrv.

(http://progrex.narod.ru/sources/AsmDrv.zip)

Вот, кажется, всё. Можно начинать!

Часть 2. API для WDM драйвера.

Большинство функций драйверного API, которые нас интересуют, предоставляются модулем ntoskrnl.exe.

Для их использования надо сделать следующее:

1) Объявить типы данных и определить константы.

Большинство определений для C находятся в файлах ntdef.h и wdm.h.

2) Объявить прототипы функций, которые мы намерены использовать.

Эти определения для C также находятся в wdm.h

3) Выполнить сборку драйвера с подключением библиотеки wdm.lib

Все три файла (wdm.h, ntdef.h и wdm.lib) входят в состав Windows DDK.

Я перевёл часть заголовков на Ассемблер и поместил их в файл usewdm.inc, который находится в базовом каталоге проекта.

Часть 3. Пишем рыбу.

3.1. Итак, приступим.

Вы можете проследить за последовательностью и содержанием действий, открыв файл main.asm для просмотра.

Начнём, пожалуй, так:

.586p ; Процессор Intel Pentium, разрешены инструкции защищённого режима

.model flat, stdcall ; Здесь всё ясно. Плоская модель адресации и тип вызовов stdcall.

option casemap:none ; "case-sensitive"

Дальше нужно задействовать файл включений usewdm.inc и библиотеку wdm.lib, чтобы мы смогли использовать драйверный API:

.include usewdm.inc

.includelib wdm.lib

Затем размещаем два сегмента – данных и кода:

.data

; […]

.code

; […]

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

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

У нас такая процедура называется DriverEntry. Объявим её как

Driver Entry proc near public, DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

DriverObject – это указатель на служебную структуру, сопоставленную драйверу. Она используется системой для вызова процедур драйвера. Её-то и следует инициализировать – записать в эту структуру адреса соответствующих процедур нашего драйвера.

Наш драйвер довольно прост. Он будет отрабатывать только 4 стандартных запроса:

IRP_MJ_CREATE – Вызов CreateFile() в приложении пользователя для установления связи с драйвером;

IRP_MJ_CLOSE – Вызов CloseHandle() в приложении пользователя для разрыва связи с драйвером;

IRP_MJ_DEVICE_CONTROL – Вызов DeviceIoControl() в приложении пользователя для запроса выполнения какой-либо функции в драйвере.

Все эти три запроса мы адресуем некоей диспетчерской функции OnDispatch. Мы узнаем о ней позже.

Четвёртый запрос – на выгрузку. Об этом пойдёт речь ниже.

А пока необходимо сделать ещё 2 важные вещи – создать логический объект устройства при помощи функции IoCreateDevice() и символическую связь, имя которой пользовательские приложения будут использовать для связи с драйвером при помощи функции CreateFile(). Символическая связь создаётся при помощи вызова IoCreateSymbolicLink():

; Инициализируем юникодовые строки с именами устройства и линка

invoke RtlInitUnicodeString, offset NtDeviceName, offset wsNtDeviceName

invoke RtlInitUnicodeString, offset Win32DeviceName, offset wsWin32DeviceName

; […]

; Создаём логический объект устройства

invoke IoCreateDevice, DriverObject, 0, offset NtDeviceName, FILE_DEVICE_UNKNOWN,0,FALSE,offset DeviceObject;

cmp eax,STATUS_SUCCESS ; Проверим, не было ли ошибки.

jnz @F

; Создаём symbolic link

invoke IoCreateSymbolicLink, offset Win32DeviceName, offset NtDeviceName ; в eax останется код результата

@@:

ret

Итак, только что мы завершили разбор процедуры инициализации.

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

У нас она реализуется функцией OnUnload. Эта функция производит действия, обратные процедуре инициализации по отношению к связанным объектам: она удаляет символическую связь (вызов IoDeleteSymbolicLink()), и затем логическое устройство, сопоставленное драйверу (IoDeleteDevice()):

; Удаляем символическую связь

invoke IoDeleteSymbolicLink, offset Win32DeviceName

; Удаляем логическое устройство

invoke IoDeleteDevice, DeviceObject

3.4. Главная диспетчерская процедура.

Она называется OnDispatch и объявлена как

OnDispatch proc near, pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

Здесь нам важен указатель на структуру с данными запроса pIrp. Данная структура довольно сложна. Вы можете найти её объявление в файле usewdm.inc.

Но нам понадобятся лишь некоторые данные.

Сначала мы должны определить код запроса – он будет один из трёх: IRP_MJ_CREATE, IRP_MJ_CLOSE или IRP_MJ_DEVICE_CONTROL.

Мы получаем этот код из структуры IO_STACK_LOCATION, указатель на которую мы получаем из структуры IRP(в свою очередь, указатель на irp был передан нам в пераметре pIrp):

mov ebx,pIrp

mov eax,(_IRP ptr [ebx]).Tail.Overlay.CurrentStackLocation ; Восстанавливаем указатель на структуру IO_STACK_LOCATION

mov pIrpStack,eax

mov ebx,pIrpStack

mov al,(IO_STACK_LOCATION ptr [ebx]).MajorFunction ; al – Код сообщения

Дальше отрабатываем запросы по-разному.

Для IRP_MJ_CREATE и IRP_MJ_CLOSEобработка фиктивная. Мы просто возвращаем код успеха STATUS_SUCCESS в регистреeax.

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

Мы размещаем эти данные в локальных переменных, чтобы потом вызвать вторичную функцию DeviceIoControlHandler, где и будет выполнена обработка.

Часть 4. Детализация.

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

Мы будем отрабатывать 2 запроса:

IOCTL_USER_REQUEST_1 – отправка переданной строки в отладочный вывод, и

IOCTL_USER_REQUEST_2 – перевод литер переданной строки в нижний регистр.

Коды запросов объявлены в файле-включении ioctlcodes.inc

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

Часть 5. Компиляция и сборка.

Для компиляции программы следует выполнить командный файл assemble.cmd.

Его содержимое:

....binml.exe –coff –Fl –c –Foasmdrv.obj main.asm

В результате мы получим листинг main.lst и объектный модульasmdrv.obj.

Дальше мы должны собрать бинарник драйвера из объектного модуля. Для этой цели существует команда link.cmd:

....binlink.exe @linkcmd.rsp

в файле linkcmd.rsp размещены настройки линкера. Полный список выглядит так:

-MACHINE:IX86

-STACK:32768,4096

-OPT:REF

-OPT:ICF

-INCREMENTAL:NO

-FORCE:MULTIPLE

-RELEASE

-DEFAULTLIB:wdm.lib

-DRIVER

-ALIGN:0x20

-SUBSYSTEM:NATIVE

-BASE:0x10000

-ENTRY:DriverEntry@8

-OUT:disk1asmdrv.sys

asmdrv.obj

В результате сборки мы получаем файлAsmDrv.sys в подкаталоге Disk1.

Часть 6. Как правильно установить драйвер.

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

Это так называемый inf-файл.

Опять-таки, к счастью для вас, я уже написал этот файл – asmdrv.inf. Вы можете открыть его для просмотра и изучить.

Файл находится в подкаталоге Disk1 проекта.

Ну что же? – Пробуем установиться.

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

Завершаем установку.

Windows 98 на этом месте может попросить перезагрузки. Не отказывайте ей :)

Windows 2000/XP запускает драйвер сразу.

Вы можете проконтролировать установку, убедившись в наличии устройства «Простейший WDM драйвер на Ассемблере» в списке менеджера устройств.

Поздравляю, если вы всё сделали правильно, наш драйвер – о, чудо! – работает.

Часть 7. Тестовая программа.

Проект тестовой программы расположен в подкаталоге TestApp.

Откройте его в Delphi и перекомпилируйте.

В результате вы получите файл AsmDrvTest.exe, который нужно будет запустить.

В принципе, это одна из самых простых программ в мире. Она занимается отправкой драйверу AsmDrv.sys запросов IOCTL_USER_REQUEST_1 и _2по требованию пользователя, передавая драйверу строку символов.

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

Одновременно с тестовой программой используйте утилиту DbgView для просмотра отладочного вывода.

Часть 8. Напоследок.

Мы убедились ещё раз, что не боги горшки обжигают.

Следует ли писать драйверы WDM на ассемблере? –

Зависит от желания и возможностей.

Однако вопрос оставлю открытым.

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

Оглавление

  • Часть 1. Цель работы. Постановка задачи.
  • Часть 2. API для WDM драйвера.
  • Часть 3. Пишем рыбу.
  •  
    3.1. Итак, приступим.
  •  
    3.2. Процедура инициализации
  •  
    3.3. Процедура выгрузки.
  •  
    3.4. Главная диспетчерская процедура.
  • Часть 4. Детализация.
  • Часть 5. Компиляция и сборка.
  • Часть 6. Как правильно установить драйвер.
  • Часть 7. Тестовая программа.
  • Часть 8. Напоследок.
  • Драйверы режима ядра: Часть 3: Простейшие драйверы

    Дата публикации 18 дек 2002

    Драйверы режима ядра: Часть 3: Простейшие драйверы — Архив WASM.RU

    Вот мы и добрались до исходного текста простейших драйверов.
    Полнофункциональные нас ждут впереди. Все исходные тексты драйверов я буду
    оформлять в виде *.bat файла, который, на самом деле, является комбинацией *.bat
    и *.asm файлов, но имеет расширение .bat.

    1.  .386                      ; начало исходного текста драйвера
    2.  end DriverEntry           ; конец исходного текста драйвера
    3.  masm32binml /nologo /c /coff driver.bat
    4.  masm32binlink /nologo /driver /base:0x10000 /align:32 /out:driver.sys /subsystem:native driver.obj

    Если такой «самокомпилирующийся» файл запустить, то произойдет следущее.
    Первые две команды закомментарены, поэтому, они игнорируются компилятором masm,
    но принимаются командным процессором, который, в свою очередь, игнорирует символ
    «точка с запятой». Управление передается на метку :make, за которой
    находятся инструкции для компилятора и компоновщика. Все, что находится за
    директивой ассемблера end, игнорируется компилятором masm. Таким образом, весь
    текст между командой goto make и меткой :make, игнорируется
    командным процессором, но принимается компилятором masm. А все, что вне (включая
    команду goto make и метку :make), игнорируется компилятором masm,
    но принимается командным процессором. Этот метод чрезвычайно удобен, т.к.
    исходный текст «помнит» с какими параметрами его нужно компилировать. Я буду
    применять такую технику в исходных текстах драйверов, а в исходных текстах
    программ управления, буду пользоваться обычным методом.

    Параметры компоновки имеют следующий смысл:

    /driver

    — Указывает компоновщику, что нужно сформировать файл драйвера режима
    ядра Windows NT;

    /base:0x10000

    — Устанавливает предопределенный адрес загрузки образа драйвера равным
    10000h. Я уже говорил про это в предыдущей статье;

    /align:32

    — Память режима ядра — драгоценный ресурс. Поэтому, файлы драйверов
    имеют более «мелкое» выравнивание секций;

    /out:driver.sys

    — По умолчанию компоновщик производит файлы с расширением .exe. При
    наличии ключа /dll файл будет иметь расширение .dll. Нам нужно получить
    файл с расшрением .sys;

    /subsystem:native

    — В PE-заголовке имеется поле, указывающее загрузчику образа
    исполняемого файла, для какой подсистемы этот файл предназначен: Win32,
    POSIX или OS/2. Это нужно для того, чтобы поместить образ в необходимое
    ему окружение. Подсистема Win32 автоматически запускается при загрузке
    системы. Если же запускается файл, предназначенный для функционирования,
    например, в подсистеме POSIX, то сначала операционная система запускает
    саму подсистему POSIX. Таким образом, с помощью этого ключа можно указать
    компоновщику, какая подсистема необходима. Когда мы компилируем *.exe или
    *.dll, то указываем под этим ключем значение windows, которое означает,
    что файлу требуется подсистема Win32. Драйверу вообще не нужна ни одна из
    подсистем, т.к. он работает в естественной (native) для самой
    операционной системы среде.

    Самый простой драйвер режима ядра

    Вот исходный текст простейшего драйвера режима ядра.

    1.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    2.  ; simplest — Самый простой драйвер режима ядра
    3.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    4.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    5.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
    6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    7.  include masm32includew2kntstatus.inc
    8.  include masm32includew2kntddk.inc
    9.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    10.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    11.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    12.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    13.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
    14.      mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    15.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    16.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    17.  masm32binml /nologo /c /coff simplest.bat
    18.  masm32binlink /nologo /driver /base:0x10000 /align:32 /out:simplest.sys /subsystem:native simplest.obj

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

    1.  DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

    К сожалению, Microsoft отошла от принципа «венгерской нотации» при
    составлении заголовочных файлов и документации DDK. Возможно, это связано с
    большим количеством специфических типов данных, используемых в DDK. Хотя, в
    обозначении типов кое-что осталось. В исходных текстах я буду придерживаться
    этого принципа везде, где только возможно, т.к. настолько привык им
    пользоваться, что исходники не использующие «венгерскую нотацию» мне кажутся
    совершенно нечитабельными. Поэтом, легким движением руки, DriverObject
    превращается в pDriverObject, а RegistryPath в pusRegistryPath.

    Типы данных PDRIVER_OBJECT и PUNICODE_STRING определены в файлах
    includew2kntddk.inc и includew2kntdef.inc соответственно.

    1.  PDRIVER_OBJECT   typedef PTR DRIVER_OBJECT
    2.  PUNICODE_STRING  typedef PTR UNICODE_STRING

    pDriverObject

    — указатель на объект только что созданного драйвера.

    Windows является объектно-ориентированной системой. Поэтому, понятие
    объект распространяется на все, что только можно, и что нельзя тоже. И
    объект «драйвер» не является исключением. Загружая драйвер, система
    создает объект «драйвер» (driver object), представляющий для нее
    образ драйвера в памяти. Через этот объект система управляет драйвером.
    Звучит красиво, но не дает никакого представления о том, что же в
    действительности происходит. Если отбросить всю эту
    объектно-ориентированную мишуру, то станет очевидно, что объект «драйвер»
    представляет собой обыкновенную структуру данных типа DRIVER_OBJECT
    (определена в includew2kntddk.inc). Некоторые поля этой структуры
    заполняет система, некоторые придется заполнять нам самим. Обращаясь к
    этой структуре, система и управляет драйвером. Итак, как вы наверное уже
    поняли, первым параметром, передающимся в функцию DriverEntry, как раз и
    является указатель на эту самую структуру (или пользуясь
    объектно-ориентированной терминологией — объект «драйвер»). Используя этот
    указатель, мы можем (и будем, но позже) заполнить соответствующие поля
    структуры DRIVER_OBJECT. Но, в рассматриваемых в этой части статьи
    драйверах этого не требуется, поэтому мы, пока, оставим
    pDriverObject без внимания.

    pusRegistryPath

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

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

    О формате данных UNICODE_STRING следует сказать особо. В отличие от режима
    пользователя, режим ядра оперирует строками в формате UNICODE_STRING. Эта
    структура определена в файле includew2kntdef.inc следующим образом:

    1.      woLength        WORD    ?  ; длина строки в байтах (не символах)
    2.      MaximumLength   WORD    ?  ; длина буфера содержащего строку в байтах (не символах)
    3.      Buffer          PWSTR   ?  ; указатель на буфер содержащий строку

    woLength

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

    MaximumLength

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

    Buffer

    — указатель на саму Unicode-строку.

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

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

    Это приведет к остановке системы и появлению BSOD (Blue Screen Of Death). А
    выполнение такого кода приведет к перезагрузке компьютера:

    Такой радикальный способ, прервать попытку исследования программы, иногда
    встречается в защитах. Честно говоря, я и сам на это не раз попадался ;-)

    В этих двух случаях, процедура DriverEntry никогда не вернет управление.
    Поэтому, возвращаемое ей значение не важно. Если же действия выполняемые
    DriverEntry будут более конструктивными, как, например, в драйвере beeper.sys,
    то надо вернуть системе некое значение, указывающее на то, как прошла
    инициализация драйвера. Если вернуть STATUS_SUCCESS, то инициализация считается
    успешной, и драйвер остается в памяти. Любое другое значение STATUS_* указывает
    на ошибку, и в этом случае драйвер выгружается системой. Вышеприведенный драйвер
    (srcArticle2-3simplestsimplest.sys) является самым простым, какой только
    можно себе представить. Единственное что он делает, это позволяет себя
    загрузить. Т.к. ничего кроме этого он сделать больше не может, то возвращает код
    ошибки STATUS_DEVICE_CONFIGURATION_ERROR. Я просто подобрал подходящее по смыслу
    значение (полный список можно посмотреть в файле includew2kntstatus.inc).
    Если возвратить STATUS_SUCCESS, то драйвер так и останется болтаться в памяти
    без дела, и выгрузить его средствами SCM будет невозможно, т.к. мы не определили
    процедуру отвечающую за выгрузку драйвера. Эта процедура должна находиться в
    самом драйвере. Она выполняет действия, зеркальные по отношению к DriverEntry.
    Если драйвер выделил себе какие-то ресурсы, например, память, то в процедуре
    выгрузки эта память должна быть возвращена системе. И только сам драйвер знает
    об этом. Но, тут я немного забежал вперед. Пока нам это не понадобится.

    Драйвер режима ядра beeper.sys

    Теперь перейдем к рассмотрению драйвера, программу управления которым, мы
    писали в прошлый раз. Мне пришлось переименовать его из beep.sys в beeper.sys,
    потому что, как оказалось, в NT4 и в некоторых версиях XP уже существует драйвер
    beep.sys. Вобще говоря, beep.sys есть во всех версиях NT
    (%SystemRoot%System32Driversbeep.sys), но он еще должен быть зарегистрирован
    в реестре. Как бы там ни было, надеюсь beeper.sys будет уникальным. Вот его
    исходный текст:

    1.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    2.  ;  beeper — Драйвер режима ядра
    3.  ;  Пищит системным динамиком
    4.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    5.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    6.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
    7.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    8.  include masm32includew2kntstatus.inc
    9.  include masm32includew2kntddk.inc
    10.  include masm32includew2khal.inc
    11.  includelib masm32libw2khal.lib
    12.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    13.  ;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы                                
    14.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    15.  TIMER_FREQUENCY        equ 1193167                   ; 1,193,167 Гц
    16.  OCTAVE                 equ 2                         ; множитель октавы
    17.  PITCH_C                equ 523                       ; До        —  523,25 Гц
    18.  PITCH_Cs               equ 554                       ; До диез   —  554,37 Гц
    19.  PITCH_D                equ 587                       ; Ре        —  587,33 Гц
    20.  PITCH_Ds               equ 622                       ; Ре диез   —  622,25 Гц
    21.  PITCH_E                equ 659                       ; Ми        —  659,25 Гц
    22.  PITCH_F                equ 698                       ; Фа        —  698,46 Гц
    23.  PITCH_Fs               equ 740                       ; Фа диез   —  739,99 Гц
    24.  PITCH_G                equ 784                       ; Соль      —  783,99 Гц
    25.  PITCH_Gs               equ 831                       ; Соль диез —  830,61 Гц
    26.  PITCH_A                equ 880                       ; Ля        —  880,00 Гц
    27.  PITCH_As               equ 988                       ; Ля диез   —  987,77 Гц
    28.  PITCH_H                equ 1047                      ; Си        — 1046,50 Гц
    29.  ; Нам нужны три звука для до-мажорного арпеджио (до, ми, соль)
    30.  TONE_1                 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
    31.  TONE_2                 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
    32.  TONE_3                 equ (PITCH_G*OCTAVE)           ; для HalMakeBeep
    33.  DELAY                  equ 1800000h                   ; для моей ~800mHz машины
    34.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    35.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    36.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    37.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    38.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    39.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    40.  MakeBeep1 proc dwPitch:DWORD
    41.      ; Прямой доступ к оборудованию через порты ввода-вывода
    42.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    43.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    44.  MakeBeep2 proc dwPitch:DWORD
    45.      ; Прямой доступ к оборудованию используя функции
    46.      ; WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll
    47.      invoke WRITE_PORT_UCHAR, 43h, 10110110y
    48.      invoke WRITE_PORT_UCHAR, 42h, eax
    49.      invoke WRITE_PORT_UCHAR, 42h, eax
    50.      invoke READ_PORT_UCHAR, 61h
    51.      invoke WRITE_PORT_UCHAR, 61h, eax
    52.      invoke READ_PORT_UCHAR, 61h
    53.      invoke WRITE_PORT_UCHAR, 61h, eax
    54.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    55.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    56.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
    57.      ; Прямой доступ к оборудованию используя функцию HalMakeBeep из модуля hal.dll
    58.      invoke HalMakeBeep, TONE_3
    59.      mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    60.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    61.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    62.  masm32binml /nologo /c /coff beeper.bat
    63.  masm32binlink /nologo /driver /base:0x10000 /align:32 /out:beeper.sys /subsystem:native beeper.obj

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

    1.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    2.  ;  Программа управления драйвером giveio.sys
    3.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    4.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    5.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
    6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    7.  include masm32includewindows.inc
    8.  include masm32includekernel32.inc
    9.  include masm32includeuser32.inc
    10.  include masm32includeadvapi32.inc
    11.  includelib masm32libkernel32.lib
    12.  includelib masm32libuser32.lib
    13.  includelib masm32libadvapi32.lib
    14.  include masm32MacrosStrings.mac
    15.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    16.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    17.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    18.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    19.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    20.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    21.      ; Подробнее смотри Ralf Brown’s Interrupt List
    22.      ;:::::::::::::::::: Установим формат таймера ::::::::::::::::::
    23.      mov al, 0Bh               ; Управляющий регистр B
    24.      push eax                  ; Сохраним старый фармат таймера
    25.      and al, 11111011y         ; Бит 2: Формат — 0: упакованный двоично-десятичный, 1: двоичный
    26.      or al, 010y               ; Бит 1: 24/12 формат часа — 1 включает 24-часовой режим
    27.      ;:::::::::::::::::::: Получим текущую дату ::::::::::::::::::::
    28.      CMOS 32h                  ; Две старшие цифры года
    29.      CMOS 09h                  ; Две младшие цифры года
    30.      xor eax, eax              ; Завершим строку нулем
    31.      ;:::::::::::::::::::: Получим текущее время :::::::::::::::::::
    32.      xor eax, eax              ; Завершим строку нулем
    33.      ;:::::::::::::: Восстановим старый формат таймера :::::::::::::
    34.      ;::::::::::::::::: Покажем текущие дату и время :::::::::::::::
    35.      invoke wsprintf, addr acOut, $CTA0(«Date:t%snTime:t%s»), addr acDate, addr acTime
    36.      invoke MessageBox, NULL, addr acOut, $CTA0(«Current Date and Time»), MB_OK
    37.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    38.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    39.  LOCAL acDriverPath[MAX_PATH]:CHAR
    40.      and fOK, 0        ; Предположим, что произойдет ошибка
    41.      ; Открываем базу данных SCM
    42.      invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    43.          invoke GetFullPathName, $CTA0(«giveio.sys»), sizeof acDriverPath, addr acDriverPath, esp
    44.          invoke CreateService, hSCManager, $CTA0(«giveio»), $CTA0(«Current Date and Time fetcher.»),
    45.                  SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START,
    46.                  SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
    47.              invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE,
    48.                                      $CTA0(«SYSTEM\CurrentControlSet\Services\giveio»),
    49.                                      0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
    50.                  ; Добавляем в реестр идентификатор текущего процесса
    51.                  invoke GetCurrentProcessId
    52.                  invoke RegSetValueEx, hKey, $CTA0(«ProcessId», szProcessId), NULL, REG_DWORD,
    53.                                          addr dwProcessId, sizeof DWORD
    54.                      invoke StartService, hService, 0, NULL
    55.                      inc fOK                ; Устанавливаем флаг
    56.                      invoke RegDeleteValue, hKey, addr szProcessId
    57.                      invoke MessageBox, NULL, $CTA0(«Can’t add Process ID into registry.»),
    58.                  invoke MessageBox, NULL, $CTA0(«Can’t open registry.»), NULL, MB_ICONSTOP
    59.              ; Удаляем драйвер из базы данных SCM
    60.              invoke DeleteService, hService
    61.              invoke CloseServiceHandle, hService
    62.              invoke MessageBox, NULL, $CTA0(«Can’t register driver.»), NULL, MB_ICONSTOP
    63.          invoke CloseServiceHandle, hSCManager
    64.          invoke MessageBox, NULL, $CTA0(«Can’t connect to Service Control Manager.»),
    65.      ; Если все ОК, получаем и показываем текущие дату и время
    66.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    67.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

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

    1.  invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE,
    2.                          $CTA0(«SYSTEM\CurrentControlSet\Services\giveio»),
    3.                          0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
    4.      invoke GetCurrentProcessId
    5.      invoke RegSetValueEx, hKey, $CTA0(«ProcessId», szProcessId), NULL, REG_DWORD,
    6.                            addr dwProcessId, sizeof DWORD
    7.          invoke StartService, hService, 0, NULL

    Перед запуском драйвера, мы создаем в подразделе реестра, соответствующем
    драйверу, дополнительный параметр ProcessId, и устанавливаем его значение
    равным идентификатору текущего процесса, т.е. процесса программы управления.
    Обратите внимание на то, что вызывая макрос $CTA0, я указываю метку szProcessId,
    которой будет помечен текст «ProcessId», для того, чтобы позже к нему
    обратиться. Если добавление параметра прошло без ошибок, то запускаем драйвер.
    Зачем нужен этот дополнительный параметр вы узнаете позже, когда мы будем
    разбирать текст драйвера.

    1.          invoke RegDeleteValue, hKey, addr szProcessId
    2.          invoke MessageBox, NULL, $CTA0(«Can’t add Process ID into registry.»),

    Получив управление от функции StartService, мы считаем, что драйвер успешно
    отработал и устанавливаем флаг fOK. Вызов функции RegDeleteValue делать не
    обязательно. Все равно, весь раздел реестра будет удален последующим вызовом
    DeleteService. Просто, я стараюсь придерживаться в программировании правила
    «хорошего тона»: нагадил — подотри ;-)

    Удалив драйвер из базы данных SCM и закрыв все открытые описатели, мы
    вызывает процедуру DateTime, предварительно проверив флаг fOK.

    На материнской плате компьютера имеется специальная микросхема, выполненная
    по технологии CMOS (Complementary Metal-Oxide Semiconductor,
    Металл-Окисел-Полупроводник с Комплементарной структурой, КМОП), и питающаяся от
    батарейки. В этой микросхеме реализован еще один таймер, называемый часами
    реального времени
    (Real Time Clock, RTC), который работает постоянно, даже
    при выключенном питании компьютера. Помимо таймера, в этой микросхеме имеется
    небольшой блок памяти, в котором хранится собственно текущее время, а также
    кое-какая информация о физических параметрах компьютера. Достаточно подробно об
    этом можно узнать в справочнике «Ralf Brown’s Interrupt List». Получить
    содержимое памяти CMOS можно обратившись к портам ввода-вывода 70h и 71h.

    1.  mov al, 0Bh               ; Управляющий регистр B
    2.  push eax                  ; Сохраним старый фармат таймера
    3.  and al, 11111011y         ; Бит 2: Формат — 0: упакованный двоично-десятичный, 1: двоичный
    4.  or al, 010y               ; Бит 1: 24/12 формат часа — 1 включает 24-часовой режим

    Сначала устанавливаем удобный нам формат данных, которые мы будем получать,
    используя управляющий регистр B. Хотя, по умолчанию, он и так установлен, но тем
    не менее. Нам удобно получать данные в упакованном двоично-десятичном формате (в
    одном байте две цифры — по 4 бита на каждую). Поскольку, у нас принята
    24-часовая система деления суток, то этот формат мы и устанавливаем.

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

    1.  invoke wsprintf, addr acOut, $CTA0(«Date:t%snTime:t%s»), addr acDate, addr acTime
    2.  invoke MessageBox, NULL, addr acOut, $CTA0(«Current Date and Time»), MB_OK

    Получив текущие дату и время, составляем из них единую строку и выводим ее на
    экран. Управляющая последовательность t вставляет символ горизонтальной
    табуляции, а n перевода строки (подробнее см. MacrosStrings.mac). И на экране
    мы должны увидеть:

    Рис. 3-2. Результат работы программы DateTime.exe

    Самым странным, в вышеприведенном тексте, является обращение к портам
    ввода-вывода прямо из режима пользователя. Как я уже упомянул выше, доступ к
    портам ввода-вывода свято охраняется Windows NT. И тем не менее, мы к ним
    обратились. Это стало возможно благодаря драйверу giveio.sys, к рассмотрению
    исходного текста которого мы и переходим.

    1.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    2.  ;  giveio — Драйвер режима ядра
    3.  ;  Дает прямой доступ к портам ввода-вывода из режима пользователя
    4.  ;   Основан на исходном тексте Дейла Робертса
    5.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    7.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
    8.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    9.  include masm32includew2kntstatus.inc
    10.  include masm32includew2kntddk.inc
    11.  include masm32includew2kntoskrnl.inc
    12.  includelib masm32libw2kntoskrnl.lib
    13.  include masm32MacrosStrings.mac
    14.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    15.  ;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы                                
    16.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    17.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    18.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    19.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    20.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    21.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
    22.  LOCAL oa:OBJECT_ATTRIBUTES
    23.  LOCAL kvpi:KEY_VALUE_PARTIAL_INFORMATION
    24.      invoke DbgPrint, $CTA0(«giveio: Entering DriverEntry»)
    25.      mov status, STATUS_DEVICE_CONFIGURATION_ERROR
    26.      InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
    27.      invoke ZwOpenKey, addr hKey, KEY_READ, ecx
    28.      .if eax == STATUS_SUCCESS
    29.          invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING(«ProcessId», 4),
    30.                                 KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
    31.          .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) &amp;&amp; ( ecx != 0 )
    32.              invoke DbgPrint, $CTA0(«giveio: Process ID: %X»),
    33.                                  dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data
    34.              ; выделяем буфер для карты разрешения ввода-вывода
    35.              invoke MmAllocateNonCachedMemory, IOPM_SIZE
    36.                  invoke PsLookupProcessByProcessId,
    37.                          dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
    38.                  .if eax == STATUS_SUCCESS
    39.                      invoke DbgPrint, $CTA0(«giveio: PTR KPROCESS: %08X»), pProcess
    40.                      invoke Ke386QueryIoAccessMap, 0, pIopm
    41.                          ; Открываем доступ к порту 70h
    42.                         ; Открываем доступ к порту 71h
    43.                          invoke Ke386SetIoAccessMap, 1, pIopm
    44.                              invoke Ke386IoSetAccessProcess, pProcess, 1
    45.                                  invoke DbgPrint, $CTA0(«giveio: I/O permission is successfully given»)
    46.                                  invoke DbgPrint, $CTA0(«giveio: I/O permission is failed»)
    47.                                  mov status, STATUS_IO_PRIVILEGE_FAILED
    48.                              mov status, STATUS_IO_PRIVILEGE_FAILED
    49.                          mov status, STATUS_IO_PRIVILEGE_FAILED
    50.                      invoke ObDereferenceObject, pProcess
    51.                      mov status, STATUS_OBJECT_TYPE_MISMATCH
    52.                  invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
    53.                  invoke DbgPrint, $CTA0(«giveio: Call to MmAllocateNonCachedMemory failed»)
    54.                  mov status, STATUS_INSUFFICIENT_RESOURCES
    55.      invoke DbgPrint, $CTA0(«giveio: Leaving DriverEntry»)
    56.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    57.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    58.  masm32binml /nologo /c /coff giveio.bat
    59.  masm32binlink /nologo /driver /base:0x10000 /align:32 /out:giveio.sys /subsystem:native giveio.obj

    Код драйвера основан на хорошо известных изысканиях Дейла Робертса,
    восходящих аж к 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 байт.

    Всё, что сказано выше о I/O Permission Map верно, но не для операционных
    систем Windows NT+. Разработчики этих систем отказались от использования
    отдельного TSS для каждого процесса, по причине худшей производительности,
    а фирма Intel задумывала именно так и процессоры этой фирмы такую возможность
    поддерживают. Операционные систем Windows NT+ используют один TSS на все процессы.
    Поскольку TSS глобален, то и IOPM тоже. Это значит, что любые манипуляции с
    ней отражаются на все выполняющиеся, а также те, которые будут выполняться процессы.

    Для манипулирования IOPM в модуле
    ntoskrnl.exe имеются две полностью недокументированные функции:
    Ke386QueryIoAccessMap и Ke386SetIoAccessMap. Приведу их описание составленное
    стараниями Дейла Робертса и моими тоже.
    1.  Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

    Копирует текущую IOPM размером 2000h из TSS в буфер, указатель на который
    содержится в параметре pIopm.

    dwFlag

    0 — заполнить буфер единичными битами (т.е запретить доступ ко всем
    портам);
    1 — скопировать текущую IOPM из TSS в буфер.

    pIopm

    — указатель на блок памяти для приема IOPM, размером не менее 2000h
    байт.
    При успешном завершении, возвращает в регистре al ненулевое значение.

    Если произошла ошибка, то al равен нулю.
    1.  Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

    Копирует переданную IOPM длинной 2000h из буфера, указатель на который
    содержится в параметре pIopm, в TSS.

    dwFlag

    только 1 — разрешает копирование. При любом другом значении функция
    возвращает ошибку.

    pIopm

    — указатель на блок памяти содержащий IOPM, размером не менее 2000h
    байт.
    При успешном завершении, возвращает в регистре al ненулевое значение.

    Если произошла ошибка, то al равен нулю.
    И еще одна очень полезная, также полностью недокументированная, функция из
    модуля ntoskrnl.exe.
    1.  Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD

    Разрешает/запрещает использование IOPM для процесса.

    pProcess

    — указатель на структуру KPROCESS (чуть подробней ниже).

    dwFlag

    0 — запретить доступ к портам ввода-вывода, установкой смещения IOPM за
    границу сегмента TSS;
    1 — разрешить доступ к портам ввода-вывода,
    устанавливая смещение IOPM в пределах TSS равным 88h.
    При успешном завершении, возвращает в регистре al ненулевое значение.

    Если произошла ошибка, то al равен нулю.
    По префиксу в имени функции можно определить к какому компоненту она
    относится: Ke — ядро, Ob — диспетчер объектов, Ps — поддержка процессов, Mm —
    диспетчер памяти и т.д.

    Для доступа к объектам код режима пользователя использует описатели
    (handles), которые являются ни чем иным как индексами в системных таблицах, в
    которых содержится сам указатель на объект. Ну а что такое, на самом деле,
    объект мы уже немного поговорили выше. Таким образом, посредством описателей
    система отрезает код режима пользователя от прямого доступа к объекту. Код
    режима ядра, напротив, пользуется именно указателями, т.к. он и есть сама
    система и имеет право делать с объектами что хочет. Функция
    Ke386IoSetAccessProcess требует, в качестве первого параметра, указатель на
    объект «процесс» (process object), т.е. на структуру KPROCESS (см.
    includew2kw2kundoc.inc. Я специально поставил префикс «w2k», т.к. в Windows
    XP недокументированные структуры сильно отличаются. Так что, использовать этот
    файлик при компиляции драйвера предназначенного для XP, не самая лучшая идея).
    Код функции Ke386IoSetAccessProcess устанавливает член IopmOffset
    структуры KPROCESS в соответствующее значение.

    Раз мы будем вызывать функцию Ke386IoSetAccessProcess, нам потребуется
    указатель на объект «процесс». Его можно получить разными способами. Я выбрал
    наиболее простой — по идентификатору. Именно поэтому, в модуле DateTime, мы
    получаем идентификатор текущего процесса и помещаем его в реестр. В данном
    случае мы используем реестр просто для передачи данных в драйвер. Т.к. процедура
    DriverEntry выполняется в контексте процесса System, нет возможности узнать,
    какой процесс на самом деле запустил драйвер. Вторым параметром,
    pusRegistryPath, в процедуре DriverEntry мы имеем указатель на раздел
    реестра, содержащий параметры инициализации драйвера. Мы воспользуемся им, чтобы
    извлечь из реестра идентификатор процесса.

    Теперь можно перейти к разбору кода драйвера giveio.sys.

    1.  InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

    Для последующего вызова функции ZwOpenKey нам потребуется указатель на
    заполненную структуру OBJECT_ATTRIBUTES (includew2kntdef.inc). Для ее
    заполнения я использую макрос InitializeObjectAttributes. Можно заполнить и
    «вручную»:

    1.  assume ecx:ptr OBJECT_ATTRIBUTES
    2.  mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
    3.  mov [ecx].RootDirectory, eax                       ; NULL
    4.  mov [ecx].Attributes, eax                          ; 0
    5.  mov [ecx].SecurityDescriptor, eax                  ; NULL
    6.  mov [ecx].SecurityQualityOfService, eax            ; NULL

    Макрос InitializeObjectAttributes находится еще на стадии разработки, так что
    не советую использовать его способом отличным от приведенного выше. Если что не
    так — я не виноват ;-)

    1.  invoke ZwOpenKey, addr hKey, KEY_READ, ecx
    2.  .if eax == STATUS_SUCCESS
    3.      invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING(«ProcessId», 4),
    4.                              KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp

    Вызовом функции ZwOpenKey получаем описатель раздела реестра в переменной
    hKey. Вторым параметром в эту функцию передаются права доступа, третьим —
    указатель на структуру OBJECT_ATTRIBUTES, заполненную на предыдущем этапе. С
    помощью функции ZwQueryValueKey получаем значение идентификатора процесса,
    записанное в параметре реестра ProcessId. Вторым параметром в эту функцию
    передается указатель на инициализированную структуру UNICODE_STRING, содержащую
    имя параметра реестра, значение которого мы хотим получить. Я стараюсь
    использовать возможности препроцессора masm на «полную катушку», поэтому, и тут
    использую самописный макрос $CCOUNTED_UNICODE_STRING (все там же —
    MacrosStrings.mac). Обратите внимание на то, что я указываю выравнивание
    строки по границе двойного слова (выравнивание самой структуры UNICODE_STRING
    жестко прописано в макросе и равно двойному слову). Какой-то особой
    необходимости в этом тут нет, просто, я даю вам возможность оценить гибкость и
    удобство моих макросов. Рекламная пауза ;-) Если органически не перевариваете
    макросы, то можно использовать традиционный способ определения Unicode-строки, и
    структуры UNICODE_STRING ее содержащей:

    1.  usz dw ‘U’, ‘n’, ‘i’, ‘c’, ‘o’, ‘d’, ‘e’, ‘ ‘, ‘s’, ‘t’, ‘r’, ‘i’, ‘n’, ‘g’, 0
    2.  us UNICODE_STRING {sizeof usz — 2, sizeof usz, offset usz}

    Меня этот способ никогда не вдохновлял, поэтому, я и написал для этой цели
    макросы COUNTED_UNICODE_STRING, $COUNTED_UNICODE_STRING,
    CCOUNTED_UNICODE_STRING, $CCOUNTED_UNICODE_STRING (см. MacrosStrings.mac).

    Третий параметр функции ZwQueryValueKey определяет тип запрашиваемой
    информации. KeyValuePartialInformation — символьная константа равная 2
    (includew2kntddk.inc). Четвертый и пятый параметры — указатель на структуру
    KEY_VALUE_PARTIAL_INFORMATION и ее размер соответственно. В члене Data этой
    структуры мы и получим значение идентификатора процесса. Последний параметр —
    указатель на переменную, размером DWORD, в которую будет записано количество
    скопированных из реестра байт. Перед самым вызовом ZwQueryValueKey, мы
    резервируем на стеке для него место, а после вызова извлекаем значение. Я
    постоянно пользуюсь таким приемом — очень удобно.

    1.  .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) &amp;&amp; ( ecx != 0 )
    2.      invoke MmAllocateNonCachedMemory, IOPM_SIZE

    Если вызов ZwQueryValueKey прошел успешно, выделяем с помощью функции
    MmAllocateNonCachedMemory кусочек памяти в пуле неподкачиваемой памяти (такая
    память никогда не сбрасывается на диск), размером 2000h байт — максимальный
    размер карты разрешения ввода-вывода. Сохраняем указатель в переменной pIopm.

    1.  invoke PsLookupProcessByProcessId,
    2.            dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
    3.  .if eax == STATUS_SUCCESS
    4.      invoke Ke386QueryIoAccessMap, 0, pIopm

    Передавая в функцию PsLookupProcessByProcessId полученный ранее идентификатор
    процесса, получаем указатель на KPROCESS в переменной pProcess. Вызовом функции
    Ke386QueryIoAccessMap, копируем IOPM в буфер.

    1.      invoke Ke386SetIoAccessMap, 1, pIopm
    2.          invoke Ke386IoSetAccessProcess, pProcess, 1
    3.              mov status, STATUS_IO_PRIVILEGE_FAILED
    4.           mov status, STATUS_IO_PRIVILEGE_FAILED
    5.      mov status, STATUS_IO_PRIVILEGE_FAILED

    Сбрасываем биты соответствующие портам ввода-вывода 70h и 71h, и записываем
    модифицированную IOPM. Вызовом функции Ke386IoSetAccessProcess разрешаем доступ.
    Обратите внимание, что Microsoft предусмотрела специальный код ошибки
    STATUS_IO_PRIVILEGE_FAILED. В принципе, здесь совершенно не важно, какой код
    ошибки мы вернем системе при выходе из DriverEntry. Я, просто потихоньку, ввожу
    вас в курс дела.

    1.      invoke ObDereferenceObject, pProcess
    2.      mov status, STATUS_OBJECT_TYPE_MISMATCH

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

    1.              invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
    2.              mov status, STATUS_INSUFFICIENT_RESOURCES

    С помощью функции MmFreeNonCachedMemory освобождаем выделенный буфер, и,
    вызовом функции ZwClose, закрываем описатель раздела реестра.

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

    В этом примере я обратился к памяти CMOS, просто, для разнообразия. Можно
    было, как в предыдущем драйвере beeper.sys, попищать системным динамиком.
    Оставляю это вам, в качестве домашнего задания. Надо будет открыть доступ к
    соответствующим портам ввода-вывода. Вызвать процедуру MakeBeep1, предварительно
    убрав из ее тела каманды cli и sti, т.к. выполнять привилегированные команды
    процессора в режиме пользователя, вам никто не разрешит. Вызывать функции из
    модуля hal.dll, естественно, тоже нельзя, т.к. они находятся в адресном
    пространстве ядра. Максимум, что вы можете себе позволить — это предоставить
    доступ ко всем 65535 портам, одним махом:

    1.  invoke MmAllocateNonCachedMemory, IOPM_SIZE
    2.      invoke RtlZeroMemory, pIopm, IOPM_SIZE
    3.      invoke PsLookupProcessByProcessId,
    4.                  dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
    5.      .if eax == STATUS_SUCCESS
    6.          invoke Ke386SetIoAccessMap, 1, pIopm
    7.              invoke Ke386IoSetAccessProcess, pProcess, 1
    8.          invoke ObDereferenceObject, pProcess
    9.      invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
    10.      mov status, STATUS_INSUFFICIENT_RESOURCES
    Помните только, что баловство с системным динамиком и чтение памяти 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

    Что в архиве

    В архиве к этой
    статье, помимо исходных кодов примеров и макросов, вы обнаружите:

    toolsprotoize

    — утилита конвертации библиотечных .lib файлов во включаемые .inc файлы
    сделанная f0dder;

    Некоторые inc-файлы в каталоге includew2k изготовлены с ее помощью.
    Правда, все __cdecl-функции мне пришлось фиксить руками :-(

    toolsKmdManager

    — утилита динамической загрузки/выгрузки драйверов (с исходниками,
    конечно). Порывшись хорошенько в сети, вы обнаружите несколько подобных
    инструментов, как с консольным, так и с графическим интерфейсом, но все
    они чем-либо да не устраивали меня. Поэтому, я написал свою собственную.
    Пока она не поддерживает буферов ввода-вывода, но, думаю, в следующей
    версии я этот недостаток исправлю. Если захотите ее перекомпилировать, то
    потребуется мой пакет cocomac
    v1.2
    ;

    includew2k

    — необходимые включаемые файлы;

    libw2k

    — необходимые библиотечные файлы.

    В связи с тем, что Microsoft прекратила свободное распространение DDK,
    у вас могут возникнуть некоторые проблемы при компиляции драйверов. Прежде
    всего — это отсутствие .lib файлов. В этом каталоге находятся файлы от
    свободного выпуска Windows 2000, но подойдут без проблем и для Windows XP,
    и, думаю, для Windows NT4.0 тоже. Надеюсь, Microsoft на меня за это не
    очень обидится ;-)

    Что почитать

    Документацию 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


    archive

    archive
    New Member

    Регистрация:
    27 фев 2017
    Публикаций:
    532


    WASM

    Цитата
    Сообщение от Bolbine84455
    Посмотреть сообщение

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

    Начни с изучения документации по программированию драйверов в Windows.
    Она начинается здесь:

    Getting started with Windows drivers
    https://msdn.microsoft.com/en-… s.85).aspx

    Также где-то в сети есть руководство от Four-F по разработке драйверов на ассемблере,
    называется KmdTut.

    Еще я собирал полезные ссылки по данной теме здесь:

    Документация и загрузки для разработчика драйверов

    Сейчас для разработки драйверов под Windows есть два основных инструмента:

    1. Window Driver Kit 7.1

    Бесплатный набор инструментов, доступен для загрузки с сайта Microsoft:
    https://www.microsoft.com/en-u… x?id=11800

    Позволяет разрабатывать драйверы под все версии Windows, начиная с Windows XP и
    Windows Server 2003. Среды разработки (IDE) как таковой нету, но можно, например,
    использовать VisualDDK:

    http://visualddk.sysprogs.org/

    либо в Visual Studio самому прописывать нужные ключи сборки, например в Pre/Post-build events,
    управляя всем процессом самостоятельно.

    2. Visual Studio 2015 + Windows SDK 10 + Windows Driver Kit 10

    Скачать WDK, WinDbg и связанные средства
    https://developer.microsoft.co… driver-kit

    WDK 8 и выше интегрирован в Visual Studio и позволяет пользоваться «из коробки»
    всеми преимуществами интегрированной среды разработки. Соответствие между
    версиями Visual Studio и WDK такое:

    WDK8 — Visual Studio 2012
    WDK8.1 — Visual Studio 2013
    WDK10 — Visual Studio 2015

    К сожалению, разработка в последних версиях WDK поддерживает только
    целевые версии Windows 7 и выше, так что если нужна совместимость с
    Windows XP, придется использовать WDK 7.1.

    Кстати, средства разработки Microsoft также поддерживают ассемблер (MASM).
    Например, в WDK 7.1 достаточно положить свой .asm-файл в подпапку,
    соответствующую архитектуре (i386 или amd64) и добавить в файл sources
    ссылку на него в виде I386_SOURCES или AMD64_SOURCES соответственно.

    Будут конкретные вопросы — задавай.

    Ну вот мы и добрались до самого интересного — сейчас будем писать драйвер. Итак, приступим. Для удобства выполнения, всю последовательность действий по написанию драйвера я распишу по пунктам.

    1. Создание директории проекта

    Установив DDK, на Вашем компьютере в директории C:WINDDK2600.1106 должны появиться файлы DDK. В этой директории создадим папку, в которой будут храниться наши проекты. Назовем ее, например, MyDrivers.

    В папке MyDrivers создадим папку FirstDriver — тут будет находится наш первый проект драйвера.

    2. Подготовка файлов проекта

    В папке FirstDriver создайте пустой текстовый файл и переименуйте его под именем FirstDriver.c

    При попытке переименовки со сменой расширения файла, появляется следующее предупреждение:

    Не обращаем внимания на это предупреждение, и нажимаем Да. При этом наш файл примет вид:

    Если же никакого предупреждения не было и переименованный файл так и остался текстовым с именем FirstDriver.c и расширением .txt, то в настройках своийств папки, которые можно найти в Пуск-> Настройка-> Панель управления-> Свойства паки уберите галочку напротив пункта «Скрывать расширения для зарегестрированных типов файлов». Попробуйте еще раз и все должно быть в порядке.

    Теперь нам надо добавить еще два очень важных файла в наш проект, без которых драйвер нам не сделать. Они называются makefile и sources (обратите внимание, у них нет расширения). Их можно создать самим, но мы сделаем проще: скопируем готовые из какого либо примера проекта драйвера из DDK. Например, возьмем их из C:WINDDK2600.1106srcgeneralcancelsys. Итак, копируем из указанной директории эти два файла и вставляем их в нашу папку проекта FirstDriver. Эти файлы управляют процессрм компиляции драйвера. Файл makefile оставляем без изменений, а вот sources надо подредактировать.

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

    Первым параметром идет TARGETNAME, которому мы присвоили Port. Это значит, что когда DDK откомпилирует наш код и создаст драйвер, имя этого файла будет Port.sys Следующем параметром идет TARGETPATH, которому мы указали путь к папке нашего проекта. Если Вы устанавливали DDK в другое место, или создали пупку проекта в другой директории, здесь Вам надо это поправить на тот путь, который у Вас. Параметр TARGETTYPE пока оставлю без комментариев. В параметре SOURCES указываем, из каких файлов будет компилироваться драйвер. У нас это файл FirstDriver.c, вот мы его и указали.

    Всю подготовительную работу мы сделали. Можно приступать к самой содержательной части — коду драйвера. Писать мы будем его на Си.

    Еще раз напомню решаемую нами задачу: надо написать драйвер под Windows 2000, XP с помощью которого можно будет работать с портами компьютера (читать и писать данные) .

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

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

    Вряд ли пользователь домашнего ПК заинтересуется тем, чтобы блокировать устройства на своем ПК. Но если дело касается корпоративной среды, то все становится иначе. Есть пользователи, которым можно доверять абсолютно во всем, есть такие, которым можно что-то делегировать, и есть те, кому доверять совсем нельзя. Например, вы заблокировали доступ к Интернету одному из пользователей, но не заблокировали устройства этого ПК. В таком случае пользователю достаточно просто принести USB-модем, и Интернет у него будет. Т.е. простым блокированием доступа к Интернету дело не ограничивается.

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

    В этой статье я расскажу немного теоретическую часть, на основе которой все строится, и расскажу принцип самого решения.

    Также полные исходные коды могут быть найдены в папке USBLock хранилища git по адресу: https://github.com/anatolymik/samples.git.

    Структура DRIVER_OBJECT

    Для каждого загруженного драйвера система формирует структуру DRIVER_OBJECT. Этой структурой система активно пользуется, когда отслеживает состояние драйвера. Также драйвер отвечает за ее инициализацию, в частности за инициализацию массива MajorFunction. Этот массив содержит адреса обработчиков для всех запросов, за которые драйвер может отвечать. Следовательно, когда система будет посылать запрос драйверу, она воспользуется этим массивом, чтобы определить, какая функция драйвера отвечает за конкретный запрос. Ниже представлен пример инициализации этой структуры.

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

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

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

    Обратите внимание на то, что в поле DriverExtension->AddDevice устанавливается адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство, за работу которого драйвер отвечает. Данное поле может быть оставлено непроинициализированным, в таком случае драйвер не сможет обрабатывать это событие.

    Структура DEVICE_OBJECT

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

    Массив MajorFunction ранее упомянутой структуры DRIVER_OBJECT содержит адреса обработчиков именно с таким прототипом.

    Сама структура DEVICE_OBJECT всегда создается драйвером при помощи функции IoCreateDevice. Если система посылает запрос драйверу, то она всегда направляет его какому-либо DEVICE_OBJECT, как это следует из вышепредставленного прототипа. Также, прототип принимает второй параметр, который содержит адрес IRP-структуры. Эта структура описывает сам запрос, и она существует в памяти до тех пор, пока драйвер не завершит его. Запрос отправляется драйверу на обработку при помощи функции IoCallDriver как системой, так и другими драйверами.

    Также со структурой DEVICE_OBJECT может быть связано имя. Таким образом, этот DEVICE_OBJECT может быть найден в системе.

    Фильтрация

    Фильтрация являет собой механизм, который позволяет перехватывать все запросы, направленные к конкретному DEVICE_OBJECT. Чтобы установить такой фильтр, необходимо создать другой экземпляр DEVICE_OBJECT и прикрепить его к DEVICE_OBJECT, запросы которого необходимо перехватывать. Прикрепление фильтра выполняется посредством функции IoAttachDeviceToDeviceStack. Все DEVICE_OBJECT, прикрепленные к перехватываемому DEVICE_OBJECT, вместе с ним формируют так называемый стек устройства, как это изображено ниже.

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

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

    PnP менеджер

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

    Когда драйвер той или иной шины обнаруживает устройства на своих интерфейсах, то для каждого дочернего устройства он создает DEVICE_OBJECT. Этот DEVICE_OBJECT также называют Physical Device Object или PDO. Затем посредством функции IoInvalidateDeviceRelations он уведомляет PnP менеджер о том, что произошли изменения на шине. В ответ на это PnP менеджер посылает запрос с minor кодом IRP_MN_QUERY_DEVICE_RELATIONS с целью запросить список дочерних устройств. В ответ на этот запрос драйвер шины возвращает список PDO. Ниже изображен пример такой ситуации.

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

    Как только PnP менеджер получит список всех PDO, он по отдельности соберет всю необходимую информацию об этих устройствах. Например, будет послан запрос с minor кодом IRP_MN_QUERY_ID. Посредством этого запроса PnP менеджер получит идентификаторы устройства, как аппаратные, так и совместимые. Также PnP менеджер соберет всю необходимую информацию о требуемых аппаратных ресурсах самим устройством. И так далее.

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

    Следующая задача PnP — это запуск драйвера устройства. Если драйвер не был ранее установлен, тогда PnP будет ожидать установки. Иначе, при необходимости, PnP загрузит его и передаст ему управление. Ранее упоминалось, что поле DriverExtension->AddDevice структуры DRIVER_OBJECT содержит адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство. Прототип этого обработчика изображен ниже.

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

    В задачу обработчика входит создание DEVICE_OBJECT и его прикрепление к PDO. Прикрепленный DEVICE_OBJECT также называют Functional Device Object или FDO. Именно этот FDO и будет отвечать за работу устройства и представление его интерфейсов в системе. Ниже представлен пример, когда PnP завершил вызов драйвера, отвечающего за работу устройства.

    Как отражено на примере, кроме драйвера самого устройства также могут быть зарегистрированы нижние и верхние фильтры класса устройства. Следовательно, если таковые имеются, PnP также загрузит их драйвера и вызовет их AddDevice обработчики. Т.е. порядок вызова драйверов следующий: сначала загружаются и вызываются зарегистрированные нижние фильтры, затем загружается и вызывается драйвер самого устройства, и в завершении загружаются и вызываются верхние фильтры. Нижние и верхние фильтры являются обычным DEVICE_OBJECT, которые создают драйвера и прикрепляют их к PDO в своих обработчиках AddDevice. Количество нижних и верхних фильтров не ограничено.

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

    Также, когда драйвер шины определяет, что произошли изменения на шине, он посредством функции IoInvalidateDeviceRelations уведомляет PnP о том, что следует заново собрать информацию о подключенных устройствах. В этот момент драйвер не удаляет ранее созданный PDO. Просто при получении запроса с minor кодом IRP_MN_QUERY_DEVICE_RELATIONS он не включит этот PDO в список. Затем PnP на основании полученного списка опознает новые устройства и устройства, которые были отключены от шины. PDO отключенных устройств драйвер удалит тогда, когда PnP пошлет запрос с minor кодом IRP_MN_REMOVE_DEVICE. Для драйвера этот запрос означает, что устройство более никем не используется, и оно может быть безопасно удалено.

    Суть решения

    Суть самого решения заключается в создании верхнего фильтра класса USB-шины. Зарезервированные классы можно найти по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff553419(v=vs.85).aspx. Нас интересует класс USB с GUID равным 36fc9e60-c465-11cf-8056-444553540000. Как гласит MSDN, этот класс используется для USB хост контроллеров и хабов. Однако практически это не так, этот же класс используется, например, flash-накопителями. Это немного добавляет нам работы. Код обработчика AddDevice представлен ниже.

    Как следует из примера, мы создаем DEVICE_OBJECT и прикрепляем его к PDO. Таким образом, мы будем перехватывать все запросы, направленные к USB-шине.

    В нашу задачу входит перехватывать запросы с minor кодом IRP_MN_START_DEVICE. Код обработчика этого запроса изображен ниже.

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

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

    Все упомянутые определения выполняются на основе идентификаторов устройств.

    Заключение

    Вы можете помочь и перевести немного средств на развитие сайта

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

    Хочу сразу же сделать несколько предупреждений.
    Данная статья всё-таки подразумевает определённый уровень подготовки.
    Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок.
    Поэтому хорошее знание обоих языков весьма желательно (если не сказать — обязательно).
    Если же ты пока не можешь этим похвастаться,
    но желание писать драйвера есть — что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы,
    ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду.
    Может как-нибудь в другой раз. Согласен? Тогда поехали!


    Скоро здесь, возможно, будет стоять твоё имя.

    Практически в любом деле, как мне кажется, нужно начинать с теории.
    Вот и начнём с неё. Для начала уясним себе поточнее основные понятия.
    Первое: что есть драйвер? Драйвер — в сущности
    кусок кода ОС, отвечающий за взаимодействие с аппаратурой.
    Слово «аппаратура» в данном контексте следует понимать в самом широком смысле.
    С момента своего появления как такого до сегодняшнего дня драйвер беспрерывно эволюционировал.
    Вот, скажем, один из моментов его развития. Как отдельный и довольно независимый модуль драйвер сформировался не сразу.
    Да и сейчас этот процесс до конца не завершён:
    ты наверняка сталкивался с тем, что во многих
    дистрибутивах никсов для установки/перестановки etc драйверов нужно перекомпилировать ядро,
    т.е. фактически заново пересобирать систему.
    Вот, кстати ещё один близкий моментец: разные принципы работы с драйверами в Windows 9x и NT.
    В первом процесс установки/переустановки драйверов проходит практически без проблем,
    во втором же случае это тяжёлое и неблагодарное дело,
    для «благополучного» завершения которого нередко приходится прибегать к полной переустановке ОС.
    А зато в Windows 9x. так,стоп,открывается широкая и волнующая тема,
    которая уведёт меня далеко от темы нынешней статьи,
    так что вернёмся к нашим баранам. ой,то есть к драйверам.
    В порядке общего развития интересно сравнить особенности драйверов в Windows и *nix(xBSD) системах:

    1) Способ работы с драйверами как файлами (подробнее см. ниже)
    2) Драйвер, как легко заменяемая честь ОС (учитывая уже сказанные выше примечания)
    3) Существование режима ядра

    Теперь касательно первого пункта. Это значит,
    что функции, используемые при взаимодействии с файлами,
    как и с драйверами, практически идентичные (имеется в виду лексически):
    open, close, read и т.д. И напоследок стоит отметить идентичность механизма
    IOCTL (Input/Output Control Code-код управления вводом-выводом)
    -запросов.

    Драйвера под Windows делятся на два типа:
    Legacy (устаревший) и WDM (PnP). Legacy драйверы (иначе называемые «драйверы в стиле
    NT») чрезвычайно криво работают (если работают вообще)
    под Windows 98, не работают с PnP устройствами, но зато могут пользоваться старыми функциями
    HalGetBusData, HalGetInterruptVector etc, но при этом не имеют поддержки в лице шинных драйверов.
    Как видишь, весьма средненький драйвер. То ли дело
    WDM: главный плюс — поддержка PnP и приличненькая совместимость:
    Windows 98, Me, 2000, XP, 2003, Server 2003 и т.д. с вариациями; но он тоже вынужден за это расплачиваться:
    например, он не поддерживает некоторые устаревшие функции
    (которые всё таки могут быть полезны). В любом случае,
    не нужно ничего воспринимать как аксиому, везде бывают свои исключения.
    В некоторых случаях лучше написания Legacy драйвера ничего не придумать.

    Как ты наверняка знаешь, в Windows есть два мода работы:
    User Mode и Kernel Mode — пользовательский режим и режим ядра соответственно.
    Первый — непривилегированный, а второй — наоборот.
    Вот во втором чаще всего и сидят драйвера (тем
    более, что мы в данный момент говорим именно о драйверах режима ядра).
    Главные различия между ними: это доступность всяких привилегированных команд процессора.
    Программировать (а уж тем более качественно) в Kernel mode посложнее будет,
    чем писать прикладные незамысловатые проги.
    А драйвера писать без хорошего знания Kernel mode — никак.
    Нужно попариться над назначением выполнения разнообразных работ отдельному подходящему уровню IRQL, желательно выучить новое API (так как в Kernel mode API отличается от прикладного).
    в общем, предстоит много всяких радостей. Но тем не менее,
    это очень интересно, познавательно, и даёт тебе совершенно иной уровень власти над компьютером.

    А раз уж я упомянула про IRQL, разьясню и это понятие.
    IRQL (Interrupt Request Level — уровень приоритета выполнения) — это приоритеты,
    назначаемые специально для кода, работающего в режиме ядра.
    Самый низкий уровень выполнения — PASSIVE_LEVEl. Работающий поток может быть прерван потоком только с более высоким
    IRQL.

    Ну и напоследок разъясним ещё несколько терминов:

    1) ISR (Interrupt Service Routine) — процедура обслуживания прерываний.
    Эта функция вызывается драйвером в тот момент,
    когда обслуживаемая им аппаратура посылает сигнал прерывания.
    Делает самые необходимые на первый момент вещи:
    регистрирует callback — функцию и т.д.

    2) DpcForISR (Deferred Procedure Call for ISR) — процедура отложенного вызова для обслуживания прерываний.
    Эту функцию драйвер регистрирует в момент работы ISR для выполнения основной работы.

    3) IRP (Input/Output Request Packet) — пакет запроса на ввод — вывод.
    Пакет IRP состоит из фиксированной и изменяющейся частей.
    Вторая носит название стека IRP или стека ввода — вывода (IO stack).

    4) IO stack location — стек ввода — вывода в пакете IRP.

    5) Dispatch Routines (Рабочие процедуры) — эти функции регистрируются в самой первой (по вызову) процедуре драйвера.

    6) Major IRP Code — старший код IRP пакета.

    7) Minor IRP Code — соответственно, младший код IRP пакета.

    8) DriverEntry — эта функция драйвера будет вызвана первой при его загрузке.

    9) Layering (Многослойность) — данной возможностью обладают только WDM — драйвера.
    Она заключается в наличии реализации стекового соединения между драйверами.
    Что такое стековое соединение? Для этого необходимо знать про Device
    Stack (стек драйверов) — поэтому я обязательно вспомню про всё это чуточку ниже.

    10) Device Stack, Driver Stack (стек устройств, стек драйверов) — всего лишь
    объемное дерево устройств. Его, кстати, можно рассмотреть во всех подробностях с помощью программы
    DeviceTree (из MS DDK), например.

    11) Стековое соединение — как и обещала, объясняю. В стеке драйверов самый верхний драйвер — подключившийся позднее.
    Он имеет возможность посылать/переадресовывать IRP запросы другим драйверам,
    которые находятся ниже его. Воти всё. Правда,просто?

    12) AddDevice — функция, которую обязательно должны поддерживать WDM драйверы.
    Её название говорит само за себя.

    13) Device Object, PDO, FDO (Объект устройства, физический,
    функциональный) — при подключении устройства к шине она создаёт PDO.
    А уже к PDO будут подключаться FDO объекты WDM драйверов.
    Обьект FDO создаётся самим драйвером устройства при помощи функции IOCreateDevice.
    Обьект FDO также может иметь свою символическую ссылку, от которой он будет получать запросы от драйвера.
    Это что касается WDM драйверов. С драйверами «в стиле NT» ситуация несколько иная.
    Если он не обслуживает реальных/PnP устройств,
    то PDO не создаётся. Но для связи с внешним миром без FDO не обойтись.
    Поэтому он присутствует и тут.

    14) Device Extension (Расширение обьекта устройства) — «авторская» структура,
    т.е. она полностью определяется разработчиком драйвера.
    Правилом хорошего тона считается, например,
    размещать в ней глобальные переменные.

    15) Monolithic Driver (Монолитный драйвер) — это драйвер,
    который самостоятельно обрабатывает все поступающие
    IRP пакеты и сам работает с обслуживаемым им устройством
    (в стеке драйверов он не состоит). Данный тип драйверов используется только если обслуживается не
    PnР устройство или же всего лишь требуется окошко в ring0.

    16) DIRQL (уровни аппаратных прерываний) —
    прерывания, поступающие от реальных устройств, имеют наивысший приоритет IRQL,
    поэтому для них решено было придумать специальное название
    (Device IRQL).

    17) Mini Driver (Мини — драйвер) — чуть меньше «полного» драйвера.
    Обычно реализуется в виде DLL-ки и имеет оболочку в виде «полного» драйвера.

    18) Class Driver (Классовый драйвер) — высокоуровневый драйвер,
    который предоставляет поддержку класса устройств.

    19) РnP Manager (PnP менеджер) — один из главных компонентов операционной системы.
    Состоит из двух частей: PnP менеджера пользовательского и «ядерного» режимов.
    Первый в основном взаимодействует с пользователем;
    когда тому нужно, например, установить новые драйвера и т.д.
    А второй управляет работой, загрузкой и т.д. драйверов.

    20) Filter Driver (фильтр — драйвер) — драйверы, подключающиеся к основному драйверу либо сверху
    (Upper), либо снизу (Lower). Фильтр драйверы (их может быть несколько) выполняют фильтрацию IRP пакетов.
    Как правило, для основного драйвера Filter Drivers неощутимы.

    21) Filter Device Object — объект устройства, создаваемый фильтр — драйвером.

    22) HAL (Hardware Abstraction Layer) — слой аппаратных абстракций.
    Данный слой позволяет абстрагироваться компонентам операционной системы от особенностей конкретной платформы.

    23) Synchronization Objects (Обьекты синхронизации) — с помощью этих
    объектов потоки корректируют и синхронизируют свою работу.

    24) Device ID — идентификатор устройства.

    25) DMA (Direct Memory Access) — метод обмена данными между устройством и памятью
    (оперативной) в котором центральный процессор не принимает участия.

    25) Polling — это особый метод программирования, при котором не устройство посылает сигналы прерывания драйверу,
    а сам драйвер периодически опрашивает обслуживаемое им устройство.

    26) Port Driver (Порт-драйвер) — низкоуровневый драйвер,
    принимающий системные запросы. Изолирует классовые драйверы устройств от аппаратной специфики последних.

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

    Краткий экскурс в архитектуру Windows NT

    Наш обзор архитектуры Windows NT мы начнём с разговора об уровнях разграничения привилегий. Я уже упоминала об user и kernel mode.
    Эти два понятия тесно связаны с так называемыми кольцами (не толкиеновскими ).
    Их ( колец) в виде всего четыре: Ring3,2,1 и 0. Ring3 — наименее привилегированное кольцо,
    в котором есть множество ограничений по работе с устройствами,
    памятью и т.д. Например, в третьем кольце нельзя видеть адресное пространство других приложений без особого на то разрешения. Естественно,
    трояну вирусу etc эти разрешения получить будет трудновато, так что хакеру в третьем кольце жизни никакой. В третьем кольце находится user mode. Kernel mode сидит в нулевом кольце — наивысшем уровне привилегий. В этом кольце можно всё:
    смотреть адресные пространства чужих приложений без каких — либо ограничений и разрешений, по своему усмотрению поступать с любыми сетевыми пакетами, проходящими через машину, на всю жизнь скрыть какой-нибудь свой процесс или файл и т.д. и т.п. Естественно,
    просто так пролезть в нулевое кольцо не получиться:
    для этого тоже нужны дополнительные телодвижения. У легального драйвера с этим проблем нет:
    ему дадут все необходимые API — шки, доступ ко всем нужным системным таблицам и проч. Хакерской же нечисти опять приходиться туго:
    все необходимые привилегии ему приходиться «выбивать»
    незаконным путём. Но это уже тема отдельной статьи, и мы к ней как-нибудь ещё вернёмся. А пока продолжим.

    У тебя наверняка возник законный вопрос:
    а что же сидит в первом и втором кольцах ? В том то всё и дело,
    что программисты из Microsoft почему — то обошли эти уровни своим вниманием. Пользовательское ПО сидит в user mode,а всё остальное (ядро,
    драйвера. ) — в kernel mode. Почему они так сделали — загадка, но нам это только на руку. А теперь разберёмся с компонентами (или, иначе говоря, слоями ) операционной системы Windows
    NT.

    Посмотри на схему — по ней многое можно себе уяснить. Разберём её подробнее.
    С пользовательским режимом всё понятно. В kernel mode самый низкий уровень аппаратный. Дальше идёт HAL, выше — диспетчер ввода — вывода и драйвера устройств в одной связке, а также ядрышко вместе с исполнительными компонентами. О HAL я уже говорила, поэтому поподробнее поговорим об исполнительных компонентах. Что они дают? Прежде всего они приносят пользу ядру. Как ты уже наверняка уяснил себе по схеме, ядро отделено от исполнительных компонентов. Возникает вопрос:
    почему ? Просто на ядре оставили только одну задачу:
    просто управление потоками, а все остальные задачи (управление доступом,
    памятью для процессов и т.д.) берут на себя исполнительные компоненты (еxecutive). Они реализованы по модульной схеме, но несколько компонентов её (схему) не поддерживают . Такая концепция имеет свои преимущества:
    таким образом облегчается расширяемость системы. Перечислю наиболее важные исполнительные компоненты:

    1) System Service Interface (Интерфейс системных служб )
    2) Configuration Manager (Менеджер конфигурирования)
    3) I/O Manager (Диспетчер ввода-вывода,ДВВ)
    4) Virtual Memory Manager,VMM (Менеджер виртуальной памяти)
    5) Local Procedure Call,LPC (Локальный процедурный вызов )
    6) Process Manager (Диспетчер процессов)
    7) Object Manager (Менеджер объектов)

    Так как эта статья — первая в цикле, обзорная, подробнее на этом пока останавливаться не будем. В процессе практического обучения написанию драйверов, я буду разъяснять все неясные термины и понятия. А пока перейдём к API.

    API (Application Programming Interface) — это интерфейс прикладного программирования. Он позволяет обращаться прикладным программам к системным сервисам через их специальные абстракции. API-интерфейсов несколько, таким образом в Windows-системах присутствуют несколько подсистем. Перечислю:

    1) Подсистема Win32.
    2) Подсистема VDM (Virtual DOS Machine — виртуальная ДОС — машина)
    3) Подсистема POSIX (обеспечивает совместимость UNIX — программ)
    4) Подсистемиа WOW (Windows on Windows). WOW 16 обеспечивает совместимость 32-х разрядной системы с 16-битными приложениями. В 64-х разрядных системах есть подсистема WOW 32,
    которая обеспечивает аналогичную поддержку 32 — битных приложений.
    5) Подсистема OS/2. Обеспечивает совместимость с OS/2 приложениями.

    Казалось бы, всё вышеперечисленное однозначно говорит в пользу WINDOWS NT систем!
    Но не всё так хорошо. Основа WINDOWS NT (имеются ввиду 32-х разрядные версии) — подсистема Win32. Приложения, заточенные под одну подсистему не могут вызывать функции другой. Все остальные (не Win32) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность приложений, разработанных, скажем, для подсистемы POSIX и запущенных под винду — очевидны.
    Увы.

    Подсистема Win32 отвечает за графический интерфейс пользователя, за обеспечение работоспособности Win32 API и за консольный ввод — вывод. Каждой реализуемой задаче
    соответствуют и свои функции: функции, отвечающие за графический фейс,
    за консольный ввод — вывод (GDI — функции) и функции управления потоками,
    файлами и т.д. Типы драйверов, наличествующие в Windows, я уже упоминала в разделе терминов:
    монолитный драйвер, фильтр — драйвер и т.д. А раз так, то пора закругляться. Наш краткий обзор архитектуры Windows NT можно считать завершённым. Этого тебе пока хватит для общего понимания концепций Windows NT, и концепций написания драйверов под эту ось — как следствие.

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

    Без чего нельзя обойтись ни в коем случае — это Microsoft DDK (Driver Development Kit ). К этому грандиозному пакету прилагается и обширная документация. Её ценность — вопрос спорный. Но в любом случае, хотя бы ознакомиться с первоисточником информации по написанию драйверов для Windows — обязательно. В принципе, можно компилять драйвера и в Visual Studio, но это чревато долгим и нудным копанием в солюшенах и vcproj-ектах, дабы код твоего драйвера нормально откомпилировался. В любом случае, сорцы придётся набивать в визуальной студии, т.к. в DDK не входит
    нормальная IDE. Есть пакеты разработки драйверов и от третьих фирм:
    WinDriver или NuMega Driver Studio, например. Но у них есть отличия от майкрософтовского базиса функций (порой довольно большие ) и многие другие мелкие неудобства. Так что DDK — лучший вариант. Если же ты хочешь писать драйвера исключительно на ассемблере, тебе подойдёт KmdKit (KernelMode Driver DevelopmentKit) для MASM32. Правда, этот вариант только для Win2k/XP.

    Теперь можно поговорить о сторонних утилитах. Некоторые уже включены в стандартную поставку
    Windows: редактор реестра. Но их в любом случае не хватит. Надо будем инсталлить отдельно.
    Множество наиполезнейших утилит создали патриархи системного кодинга в
    Windows: Марк Руссинович, Гарри Нэббет, Свен Шрайбер. и т.д. Вот о них и поговорим.
    Марк Руссинович создал много полезных утилит:
    RegMon, FileMon (мониторы обращений к реестру и файлам соответственно), WinObj (средство просмотра директорий имен
    объектов), DebugView,DebugPrint (программы просмотра, сохранения и т.д. отладочных сообщения) и проч. и проч. Все эти утилиты и огромное количество других можно найти на знаменитом сайте Руссиновича
    http://www.sysinternals.com.

    На диске, прилагающемся к знаменитой книге «Недокументированные возможности Windows 2000» Свена Шрайбера,
    есть замечательные утилиты w2k_svc, -_sym, -_mem, позволяющие просматривать установленные драйвера, приложения и службы, работающие в режиме ядра, делать дамп памяти и т.д. Все эти утилиты, а также другие программы с диска можно скачать с
    http://www.orgon.com/w2k_internals/cd.html.

    Напоследок нельзя не упомянуть такие хорошие проги, как PE
    Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.

    Ну вот и подошла к концу первая статья из цикла про написание драйверов под Windows. Теперь ты достаточно «подкован» по
    теоретической части, так что в следующей статье мы перейдём к практике. Желаю тебе удачи в этом интереснейшем деле — написании драйверов! Да не облысеют твои пятки!

    Понравилась статья? Поделить с друзьями:
  • Как написать драйвер на python
  • Как написать драйвер на canon
  • Как написать драйвер для устройства под windows 10
  • Как написать драйвер для сканера
  • Как написать драйвер для принтера под windows 10 64 bit