Драйверы режима ядра: Часть 16 : Драйвер-фильтр (не PnP)

Дата публикации 12 янв 2005

Драйверы режима ядра: Часть 16 : Драйвер-фильтр (не PnP) — Архив WASM.RU



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



Стек клавиатуры

Для начала совсем чуть-чуть теории о том, как функционирует стек клавиатуры.

Физическую связь клавиатуры с шиной осуществляет микроконтроллер клавиатуры Intel 8042 (или совместимый с ним). На современных компьютерах он интегрирован в чипсет материнской платы. Этот контроллер может работать в двух режимах: AT-совместимом и PS/2-совместимом. AT-клавиатуру, сейчас, наверное, уже сложно найти. Все клавиатуры уже давно являются PS/2-совместимыми или клавиатурами, подключаемыми через интерфейс USB. В PS/2-совместимом режиме микроконтроллер клавиатуры также связывает с шиной и PS/2-совместимую мышь. Всем этим хозяйством управляет функциональный драйвер i8042prt (Intel 8042 Port Driver), полный исходный код которого, можно найти в DDK (DDK\src\input\pnpi8042). Драйвер i8042prt создает два безымянных объекта "устройство" и подключает один к стеку клавиатуры, а другой к стеку мыши. В прошлой статье на рисунке 15-4 вы видели, что на машине с системой Terminal Server у клавиатуры (и у мыши тоже) имеется более одного (определяется количеством терминальных сессий) стека. На "обычной" машине клавиатурный и "мышиный" стеки выглядят примерно так:

Код (Text):
  1.  
  2.  
  3.  kd> !drvobj <font color="blue">i8042prt</font>
  4.  Driver object (818377d0) is for:
  5.   \Driver\i8042prt
  6.  Driver Extension List: (id , addr)
  7.  
  8.  Device Object list:
  9.  8181a020  8181b020
  10.  
  11.  
  12.  kd> !devstack 8181a020
  13.    !DevObj   !DrvObj            !DevExt   ObjectName
  14.    8181ae30  \Driver\Mouclass   8181aee8  PointerClass0
  15.  > 8181a020  \Driver\i8042prt   8181a0d8
  16.    81890df0  \Driver\ACPI       8186e008  00000017
  17.  !DevNode 8188fe48 :
  18.    DeviceInst is "ACPI\PNP0F13\4&2658d0a0&0"
  19.    ServiceName is "i8042prt"
  20.  
  21.  
  22.  kd> !devstack 8181b020
  23.    !DevObj   !DrvObj            !DevExt   ObjectName
  24.    8181be30  \Driver\Kbdclass   8181bee8  KeyboardClass0
  25.  > 8181b020  \Driver\i8042prt   8181b0d8
  26.    81890f10  \Driver\ACPI       8189d228  00000016
  27.  !DevNode 8188ff28 :
  28.    DeviceInst is "ACPI\PNP0303\4&2658d0a0&0"
  29.    ServiceName is "i8042prt"
  30.  
  31.  

Поверх драйвера i8042prt, точнее поверх его устройств, располагаются именованные объекты "устройство" драйверов Kbdclass и Mouclass. Имя "KeyboardClass" является базовым, и к нему добавляются индексы (0, 1 и т.д.). Базовое имя хранится в параметре реестра HKLM\SYSTEM\CurrentControlSet\Services\Kbdclass\Parameters\KeyboardDeviceBaseName и может быть также "KeyboardPort" в случае если клавиатура использует унаследованные (legacy) драйверы, хотя подробно я в этом не разбирался (см. исходный код драйвера Kbdclass). Мы будем использовать в качестве устройства-цели для подключения фильтра объект "устройство" под именем "KeyboardClass0".

Драйверы Kbdclass и Mouclass являются так называемыми драйверами класса (class drivers) и реализуют общую функциональность для всех типов клавиатур и мышей, т.е. для всего класса этих устройств. Оба эти драйвера устанавливаются как высокоуровневые драйверы фильтры и их полный исходный код также можно найти в DDK (DDK\src\input\kbdclass и DDK\src\input\mouclass, соответственно). В архиве к этой статье в каталоге SetKeyboardLeds находится простейший драйвер, зажигающий все три индикатора клавиатуры. Примерно так (т.е. через порты ввода/вывода) функциональный драйвер i8042prt управляет устройством "клавиатура". В исходном коде драйверов Kbdclass и Mouclass обращения к портам вы конечно не найдете.

Стек клавиатуры обрабатывает несколько типов запросов (полный список см. в разделе DDK "Kbdclass Major I/O Requests"). Нас будут интересовать только IRP типа IRP_MJ_READ, которые несут с собой коды клавиш. Генератором этих IRP является поток необработанного ввода RawInputThread системного процесса csrcc.exe. Этот поток открывает объект "устройство" драйвера класса клавиатуры для эксклюзивного использования и с помощью функции ZwReadFile направляет ему IRP типа IRP_MJ_READ. Получив IRP, драйвер Kbdclass, используя макрос IoMarkIrpPending, отмечает его как ожидающий завершения (pending), ставит в очередь и возвращает STATUS_PENDING. Потоку необработанного ввода придется ждать завершения IRP (если точнее, то RawInputThread получает клавиатурные события как вызов асинхронной процедуры (Asynchronous Procedure Call, APC)). Подключаясь к стеку, драйвер Kbdclass регистрирует у драйвера i8042prt процедуру обратного вызова KeyboardClassServiceCallback, направляя ему IRP IOCTL_INTERNAL_KEYBOARD_CONNECT. Драйвер i8042prt тоже регистрирует у системы свою процедуру обработки прерывания (Interrupt Service Routine, ISR) I8042KeyboardInterruptService, вызовом функции IoConnectInterrupt. Когда будет нажата или отпущена клавиша, контроллер клавиатуры выработает аппаратное прерывание. Его обработчик вызовет I8042KeyboardInterruptService, которая прочитает из внутренней очереди контроллера клавиатуры необходимые данные. Т.к. обработка аппаратного прерывания происходит на повышенном IRQL, ISR делает только самую неотложную работу и ставит в очередь вызов отложенной процедуры (Deferred Procedure Call, DPC). DPC работает при IRQL = DISPATCH_LEVEL. Когда IRQL понизится до DISPATCH_LEVEL, система вызовет процедуру I8042KeyboardIsrDpc, которая вызовет зарегистрированную драйвером Kbdclass процедуру обратного вызова KeyboardClassServiceCallback (также выполняется на IRQL = DISPATCH_LEVEL). KeyboardClassServiceCallback извлечет из своей очереди ожидающий завершения IRP, заполнит структуру KEYBOARD_INPUT_DATA (на самом деле i8042prt старается опустошить всю очередь контроллера клавиатуры, а Kbdclass соответственно заполнить столько структур KEYBOARD_INPUT_DATA, сколько влезет в буфер IRP), несущую всю необходимую информацию о нажатиях/отпусканиях клавиш и завершит IRP. Поток необработанного ввода пробуждается, обрабатывает полученную информацию и вновь посылает IRP типа IRP_MJ_READ драйверу класса, который опять ставится в очередь до следующего нажатия/отпускания клавиши. Таким образом, у стека клавиатуры всегда есть, по крайней мере, один, ожидающий завершения IRP и находится он в очереди драйвера Kbdclass. "Мышиный" стек ведет себя подобным же образом.

Код (Text):
  1.  
  2.  
  3.  kd> !devobj 8181be30
  4.  Device object (8181be30) is for:
  5.   KeyboardClass0 \Driver\Kbdclass DriverObject 818372b0
  6.  Current Irp <font color="blue">815a2e68</font> RefCount 0 Type 0000000b Flags 00002044
  7.  DevExt 8181bee8 DevObjExt 8181bfd8
  8.  ExtensionFlags (0000000000)
  9.  AttachedTo (Lower) 8181b020 \Driver\i8042prt
  10.  Device queue is busy -- Queue empty.
  11.  
  12.  kd> !irp <font color="blue">815a2e68</font> 1
  13.  Irp is active with 6 stacks 6 is current (= 0x815a2f8c)
  14.   No Mdl System buffer = <font color="blue">81664b48</font> Thread <font color="blue">8171bca0</font>:  Irp stack trace.
  15.  Flags = 00000970
  16.  ThreadListEntry.Flink = 8171beac
  17.  ThreadListEntry.Blink = 8171beac
  18.  IoStatus.Status = 00000103
  19.  IoStatus.Information = 00000000
  20.  RequestorMode = 00000000
  21.  Cancel = 00
  22.  CancelIrql = 0
  23.  ApcEnvironment = 00
  24.  UserIosb = e28b1028
  25.  UserEvent = 00000000
  26.  Overlay.AsynchronousParameters.UserApcRoutine = a0063334
  27.  Overlay.AsynchronousParameters.UserApcContext = e28b1008
  28.  Overlay.AllocationSize = e28b1008 - a0063334
  29.  CancelRoutine = <font color="blue">f3ec82e0</font>
  30.  UserBuffer = e28b1068
  31.  &Tail.Overlay.DeviceQueueEntry = 0006da94
  32.  Tail.Overlay.Thread = 8171bca0
  33.  Tail.Overlay.AuxiliaryBuffer = 00000000
  34.  Tail.Overlay.ListEntry.Flink = 00000000
  35.  Tail.Overlay.ListEntry.Blink = 00000000
  36.  Tail.Overlay.CurrentStackLocation = 815a2f8c
  37.  Tail.Overlay.OriginalFileObject = 8171aa08
  38.  Tail.Apc = 00000000
  39.  Tail.CompletionKey = 00000000
  40.       cmd  flg cl Device   File     Completion-Context
  41.   [  0, 0]   0  0 00000000 00000000 00000000-00000000
  42.  
  43.                          Args: 00000000 00000000 00000000 00000000
  44.   [  0, 0]   0  0 00000000 00000000 00000000-00000000
  45.  
  46.                          Args: 00000000 00000000 00000000 00000000
  47.   [  0, 0]   0  0 00000000 00000000 00000000-00000000
  48.  
  49.                          Args: 00000000 00000000 00000000 00000000
  50.   [  0, 0]   0  0 00000000 00000000 00000000-00000000
  51.  
  52.                          Args: 00000000 00000000 00000000 00000000
  53.   [  0, 0]   0  0 00000000 00000000 00000000-00000000
  54.  
  55.                          Args: 00000000 00000000 00000000 00000000
  56.  >[  3, 0]   0  1 8181be30 8171aa08 00000000-00000000    pending
  57.                 \Driver\Kbdclass
  58.                          Args: <font color="blue">00000078</font> 00000000 00000000 00000000
  59.  
  60.  

Как видно, драйвер Kbdclass имеет незавершенный IRP типа IRP_MJ_READ (цифра 3 в квадратных скобках). Колонка cl показывает содержимое поля IO_STACK_LOCATION.Control. В данном случае это флаг SL_PENDING_RETURNED, установленный вызовом макроса IoMarkIrpPending. Строка Args представляет собой содержимое вложенной структуры IO_STACK_LOCATION.Read. Размер буфера (78h) достаточен ровно для 10 структур KEYBOARD_INPUT_DATA. Сам буфер находится по адресу System buffer = 81664b48. Обратите также внимание на строку CancelRoutine = f3ec82e0. Это адрес процедуры отмены IRP (cancel routine), принадлежащей драйверу Kbdclass. Отмена IRP - это ещё одна большая и относительно сложная тема (которую я не планирую освещать). Позже мы немного поговорим об этом.

Этот, ожидающий завершения, IRP, естественно, принадлежит потоку RawInputThread.

Код (Text):
  1.  
  2.  
  3.  kd> !thread <font color="blue">8171bca0</font>
  4.  THREAD 8171bca0  Cid a4.bc  Teb: 00000000  Win32Thread: e28ae5a8 WAIT: (WrUserRequest) KernelMode Alertable
  5.      8171bf20  SynchronizationEvent
  6.      8171bc08  SynchronizationEvent
  7.      8171bbc8  NotificationTimer
  8.      8171bc48  SynchronizationEvent
  9.  IRP List:
  10.      <font color="blue">815a2e68</font>: (0006,0148) Flags: 00000970  Mdl: 00000000
  11.  Not impersonating
  12.  Owning Process 81736160
  13.  Wait Start TickCount    57881
  14.  Context Switch Count    18896
  15.  UserTime                  0:00:00.0000
  16.  KernelTime                0:00:00.0070
  17.  Start Address win32k!<font color="blue">RawInputThread</font> (0xa00ad1aa)
  18.  Stack Init bfd1d000 Current bfd1caf0 Base bfd1d000 Limit bfd1a000 Call 0
  19.  Priority 19 BasePriority 13 PriorityDecrement 0 DecrementCount 0
  20.  
  21.  

Когда мы установим фильтр, то IRP сначала будут попадать к нам. Т.к. IRP типа IRP_MJ_READ является фактически запросом на чтение данных, то когда он идет вниз по стеку его буфер, естественно пуст. Прочитанный данные буфер будет содержать только после завершения IRP. Для того чтобы их (данные) увидеть фильтр должен установить в каждый IRP (точнее в свой блок стека) процедуру завершения. Т.к. находящийся в очереди драйвера Kbdclass IRP был послан до того, как мы установим фильтр, то в нем нет нашей процедуры завершения, а значит, мы никак не сможем увидеть код той клавиши, которая будет нажата сразу после установки фильтра. Когда клавиша будет нажата, ожидающий завершения IRP будет завершен и RawInputThread пошлет новый IRP типа IRP_MJ_READ. Этот и все последующие IRP мы уже перехватим и поставим процедуру завершения. Когда клавиша будет отпущена, мы прочитаем в процедуре завершения её код. Именно поэтому фильтр не видит момента нажатия на первую клавишу, и в мониторе вы всегда будете видеть для первой нажатой клавиши только её код break (отпускание клавиши). Дальше фильтр перехватывает все нажатия и все отпускания любых клавиш.

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



Спин-блокировка

Поскольку обработка IRP по своей природе асинхронна, то зачастую "правая рука не знает, что делает левая". Поэтому в каждом более-менее серьезном драйвере приходится использовать механизм синхронизации под названием "взаимоисключающий доступ". В тринадцатой части цикла - "Базовая техника. Синхронизация: Взаимоисключающий доступ" мы уже познакомились с тем, как организовать взаимоисключающий доступ с помощью мьютекса. Поэтому про макросы MUTEX_INIT, MUTEX_ACQUIRE и MUTEX_RELEASE, которые мы вновь будем использовать, я молчу.

Синхронизация с помощью мьютекса удобна, но, к сожалению, у нее есть один недостаток: как известно, ожидать объекты на IRQL равном DISPATCH_LEVEL и выше нельзя. Для организации взаимоисключающего доступа на IRQL до DISPATCH_LEVEL включительно существует механизм под названием "спин-блокировка" (spin lock).

Код (Text):
  1.  
  2.  
  3.  KfAcquireSpinLock proc                   ; Захват спин-блокировки в однопроцессорном HAL
  4.      xor     eax, eax
  5.      mov     al, ds:0FFDFF024h            ; al = KPCR.Irql
  6.      mov     byte ptr ds:0FFDFF024h, 2    ; KPCR.Irql = DISPATCH_LEVEL
  7.      ret
  8.  KfAcquireSpinLock endp
  9.  
  10.  KfAcquireSpinLock proc                   ; Захват спин-блокировки в многопроцессорном HAL
  11.      mov     edx, ds:0FFFE0080h           ; edx = APIC[TASK PRIORITY REGISTER]
  12.      mov     dword ptr ds:0FFFE0080h, 41h ; APIC[TASK PRIORITY REGISTER] = DPC VECTOR
  13.      shr     edx, 4
  14.      movzx   eax, ds:HalpVectorToIRQL[edx]; OldIrql
  15.  
  16.  trytoacquire:
  17.      lock bts dword ptr [ecx], 0          ; Попробуем атомарно захватить блокировку.
  18.      jb      spin                         ; Если блокировка занята будем крутить цикл.
  19.      ret                                  ; Мы захватили блокировку, возвращаем IRQL,
  20.                                           ; на котором процессор находился до блокировки
  21.      align 4
  22.  
  23.  spin:                                    ; Блокировка занята. Будем крутить цикл.
  24.      test    dword ptr [ecx], 1           ; Проверим блокировку.
  25.      jz      trytoacquire                 ; Если блокировка освободилась, попробуем захватить её ещё раз.
  26.      pause                                ; Если нет, крутим цикл дальше.  Инструкция pause специально
  27.                                           ; предназначена для использования в цикле спин-блокировки.
  28.                                           ; Подробности можно почитать в разделе "PAUSE-Spin Loop Hint"
  29.                                           ; IA-32 Intel Architecture Software Developer's Manual
  30.                                           ; Volume 2 : Instruction Set Reference.
  31.      jmp     spin
  32.  KfAcquireSpinLock endp
  33.  
  34.  

На однопроцессорной машине захват спин-блокировки заключается в простом повышении IRQL до DISPATCH_LEVEL. Как известно, на этом IRQL не происходит планирования потоков, и поток, владеющий процессором, будет выполняться до тех пор, пока IRQL не понизится. Поскольку IRQL является атрибутом процессора, на многопроцессорной машине простого повышения IRQL не достаточно, т.к. это не блокирует потоки, выполняющиеся другими процессорами. Поэтому в многопроцессорных HAL спин-блокировка реализована несколько сложнее и является спин-блокировкой в истинном смысле этого слова (spin - крутить, вертеть; пускать волчок). Т.е. если блокировка занята, поток крутит бесконечный цикл, пытаясь её захватить. При этом т.к. цикл выполняется на IRQL = DISPATCH_LEVEL, планирования потоков на этом процессоре не происходит и процессор не может заняться полезной работой. Именно поэтому спин-блокировки гораздо более критичны ко времени. Т.е. освободить спин-блокировку нужно как можно быстрее. DDK даже определяет максимальный временной интервал в 25 микросекунд, в течении которого можно держать спин-блокировку. В этом смысле мьютексы и другие объекты ожидания менее требовательны, т.к. поток, ожидающий занятый объект, просто исключается из планирования, и процессор получает другой, готовый к выполнению поток.

И раз уж нам пришлось коснуться спин-блокировки, то запомните несколько классических правил. Во-первых, захват спин-блокировки, как мы только что выяснили, повышает IRQL до DISPATCH_LEVEL, а значит, нам доступна только неподкачиваемая память и сам код также должен находиться в неподкачиваемой памяти. Во-вторых, повторный захват той же самой спин-блокировки, естественно, приведет к полной блокировке (deadlock). В третьих, захватывать спин-блокировку на IRQL выше DISPATCH_LEVEL нельзя, т.к. это фактически означает явное понижение IRQL, что неминуемо приведет к BSOD. В-четвертых, если требуется захватить две (или больше) спин-блокировки, то все потоки должны это делать в одном и том же порядке. Иначе возможна взаимная блокировка. Например, двум потокам требуется захватить блокировки A и B. Если они будут делать это в разном порядке, то, возможно, что в одно и то же время первый поток захватит блокировку A, а второй блокировку B. После этого оба потоку будут бесконечно ждать: первый - когда освободится блокировка B, а второй - когда освободиться блокировка A.

Код (Text):
  1.  
  2.  
  3.  LOCK_ACQUIRE MACRO lck:REQ
  4.      mov ecx, lck
  5.      fastcall KfAcquireSpinLock, ecx
  6.  ENDM
  7.  
  8.  LOCK_RELEASE MACRO lck:REQ, NewIrql:REQ
  9.  
  10.      mov ecx, lck
  11.      mov dl, NewIrql
  12.  
  13.      .if dl == DISPATCH_LEVEL
  14.          fastcall KefReleaseSpinLockFromDpcLevel, ecx
  15.      .else
  16.          and edx, 0FFh
  17.          fastcall KfReleaseSpinLock, ecx, edx
  18.      .endif
  19.  ENDM
  20.  
  21.  

Для захвата спин-блокировки будем использовать макросы. Это упрощенные версии. В макросе LOCK_RELEASE мы используем небольшую оптимизацию: если мы были до захвата спин-блокировки на IRQL = DISPATCH_LEVEL, то выгоднее вызвать KefReleaseSpinLockFromDpcLevel вместо KfReleaseSpinLock, т.к. изменять IRQL не требуется и на однопроцессорной машине KefReleaseSpinLockFromDpcLevel является "пустой" функцией.

Код (Text):
  1.  
  2.  
  3.  KeReleaseSpinLockFromDpcLevel proc
  4.      retn 4
  5.  KeReleaseSpinLockFromDpcLevel endp
  6.  
  7.  

Нечто подобное (я имею в виду оптимизацию) можно сделать и для макроса LOCK_ACQUIRE. Потребуется только узнать текущий IRQL и если он равен DISPATCH_LEVEL, то вызвать KeAcquireSpinLockAtDpcLevel, которая (на однопроцессорной машине) тоже выполняет инструкцию ret.

Код (Text):
  1.  
  2.  
  3.  KeAcquireSpinLockAtDpcLevel proc
  4.      retn 4
  5.  KeAcquireSpinLockAtDpcLevel endp
  6.  
  7.  

Я не стал оптимизировать макрос LOCK_ACQUIRE, т.к. написал эти макросы давно и несколько раз успешно использовал, к тому же не ясно, что быстрее: просто вызвать KfAcquireSpinLock или выяснять IRQL и в зависимости от его значения вызывать KeAcquireSpinLockAtDpcLevel. Поэтому я не стал ничего мудрить и оставил всё как есть. Если есть неуёмное желание оптимизировать, изучайте hal.dll/halmps.dll и ntoskrnl.exe/ntkrnlmp.exe и оптимизируйте на здоровье.

Для полноты картины, надо ещё добавить, что есть функция KeAcquireSpinLockRaiseToSynch, повышающая IRQL при захвате блокировки до CLOCK2_LEVEL (28).



Процедура DriverEntry

Теперь займемся собственно нашим фильтром. Как я уже говорил, это не-Pnp драйвер. Кода довольно много и я не буду приводить его полностью (см. архив к статье).

Код (Text):
  1.  
  2.  
  3.      invoke IoCreateDevice, pDriverObject, 0, addr g_usControlDeviceName, \
  4.                              FILE_DEVICE_UNKNOWN, 0, TRUE, addr g_pControlDeviceObject
  5.  
  6.  

На этот раз наш драйвер будет управлять уже двумя объектами: объектом "устройство-фильтр" (filter device object) и объектом "устройство управления" (control device object). Объект "устройство-фильтр" будет подключен к стеку клавиатуры, и через него будут проходить все IRP управляющие клавиатурой. Посредством объекта "устройство управления" программа управления будет отдавать драйверу необходимые команды: "подключить фильтр", "отключить фильтр", "передать перехваченные данные". На данный момент нам нужно только устройство управления. Этот объект будет именованным, для того чтобы программа управления могла получить к нему доступ. Мы не хотим работать одновременно с несколькими клиентами. Поэтому создадим эксклюзивный объект, определив TRUE в параметре Exclusive. В этом случае диспетчер объектов позволит создать только один описатель объекта. К сожалению, этот простой способ не очень надёжен, и открыть объект все же можно по относительному пути, т.е. открыв каталог "\Device" и передав его описатель в параметре RootDirectory макроса InitializeObjectAttributes. DDK вообще говорит, что параметр Exclusive зарезервирован. Поэтому мы добавим кое-какую дополнительную обработку запросов IRP_MJ_CREATE и IRP_MJ_CLOSE.

Код (Text):
  1.  
  2.  
  3.              invoke ExAllocatePool, NonPagedPool, sizeof NPAGED_LOOKASIDE_LIST
  4.              .if eax != NULL
  5.  
  6.                  mov g_pKeyDataLookaside, eax
  7.  
  8.                  invoke ExInitializeNPagedLookasideList, g_pKeyDataLookaside, \
  9.                                          NULL, NULL, 0, sizeof KEY_DATA_ENTRY, 'ypSK', 0
  10.  
  11.  

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

Код (Text):
  1.  
  2.  
  3.  KEY_DATA STRUCT
  4.      dwScanCode  DWORD   ?
  5.      Flags       DWORD   ?
  6.  KEY_DATA ENDS
  7.  PKEY_DATA typedef ptr KEY_DATA
  8.  
  9.  KEY_DATA_ENTRY STRUCT
  10.      ListEntry   LIST_ENTRY  
  11.      KeyData     KEY_DATA    
  12.  KEY_DATA_ENTRY ENDS
  13.  
  14.  

Экземпляры этой структуры будут хранить данные о перехваченных нажатиях/отпусканиях клавиш и их (экземпляры структуры) мы будем хранить в двусвязном списке. В седьмой части цикла - "Базовая техника: Работа с памятью. Использование ассоциативных списков" мы достаточно подробно разобрали как ассоциативный список (look-aside list), так и двусвязный список (doubly linked list). Уверен, что многие просто пропустили эту статью ;) Если это так, то придется её прочитать сейчас, т.к. я не буду повторяться, а без этого материала кое-что может быть не понятно. Единственная разница в том, что сейчас мы будем использовать неподкачиваемый ассоциативный список. Функции ExAllocateFromNPagedLookasideList и ExFreeToNPagedLookasideList для работы с неподкачиваемым ассоциативным списком реализованы в DDK как макросы, в отличие от именно функций для подкачиваемого ассоциативного списка. К сожалению, ввиду ограниченности макроязыка masm, мне пришлось реализовать их в виде функций _ExAllocateFromNPagedLookasideList и _ExFreeToNPagedLookasideList. Неподкачиваемый ассоциативный список нам потребовался, как вы догадываетесь, потому, что мы будем работать с ним на IRQL = DISPATCH_LEVEL.

Код (Text):
  1.  
  2.  
  3.                  InitializeListHead addr g_KeyDataListHead
  4.  
  5.  

Глобальная переменная g_KeyDataListHead является головой двусвязного списка структур KEY_DATA_ENTRY.

Код (Text):
  1.  
  2.  
  3.                  invoke KeInitializeSpinLock, addr g_KeyDataSpinLock
  4.  
  5.  

Спин-блокировка нам потребуется для организации монопольного доступа к списку структур KEY_DATA_ENTRY. Использовать объекты синхронизации, например, мьютекс, мы не можем, т.к. будем обращаться к списку на IRQL = DISPATCH_LEVEL.

Код (Text):
  1.  
  2.  
  3.                  invoke KeInitializeSpinLock, addr g_EventSpinLock
  4.  
  5.  

Эта спин-блокировка поможет нам организовать монопольный доступ к переменной g_pEventObject, в которой будет храниться указатель на объект событие. Этот объект будет использоваться для уведомления программы управления, о новых данных (Подробнее см. часть 14 "Базовая техника. Синхронизация: Использование объекта "событие" для взаимодействия драйвера с программой управления").

Код (Text):
  1.  
  2.  
  3.                  MUTEX_INIT g_mtxCDO_State
  4.  
  5.  

С помощью этого мьютекса мы сможем монопольно выполнять некоторые участки кода.

Код (Text):
  1.  
  2.  
  3.                  mov ecx, IRP_MJ_MAXIMUM_FUNCTION + 1
  4.                  .while ecx
  5.                      dec ecx
  6.                      mov [eax].MajorFunction[ecx*(sizeof PVOID)], offset DriverDispatch
  7.                  .endw
  8.  
  9.  

Заполняем все элементы массива указателей на процедуры диспетчеризации драйвера, адресом единственной процедуры DriverDispatch. Эта процедура будет распределять запросы между фильтром и устройством управления. Устройство управления будет получать от программы управления всего три запроса: IRP_MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_DEVICE_CONTROL. А вот устройство-фильтр может получить любой запрос, т.к. оно подключается в уже существующий стек, по которому могут циркулировать IRP любого типа. Зачастую, весь спектр IRP, проходящий по фильтруемому стеку вообще не известен. Фильтровать приходится только некоторые типы IRP, но если фильтр получит запрос, который его не интересует, он обязан направить его ниже по стеку. Именно поэтому мы должны заполнить весь массив MajorFunction. Иначе в незаполненных элементах останется указатель на системную функцию IopInvalidDeviceRequest, которая будет завершать IRP с кодом STATUS_INVALID_DEVICE_REQUEST, и мы блокируем продвижение таких запросов.



Процедура DriverDispatch

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

Код (Text):
  1.  
  2.  
  3.      IoGetCurrentIrpStackLocation pIrp
  4.  
  5.      movzx eax, (IO_STACK_LOCATION PTR [eax]).MajorFunction
  6.      mov dwMajorFunction, eax
  7.  
  8.      mov eax, pDeviceObject
  9.      .if eax == g_pFilterDeviceObject
  10.  
  11.          mov eax, dwMajorFunction
  12.          .if eax == IRP_MJ_READ
  13.              invoke FiDO_DispatchRead, pDeviceObject, pIrp
  14.              mov status, eax
  15.          .elseif eax == IRP_MJ_POWER
  16.              invoke FiDO_DispatchPower, pDeviceObject, pIrp
  17.              mov status, eax
  18.          .else
  19.              invoke FiDO_DispatchPassThrough, pDeviceObject, pIrp
  20.              mov status, eax
  21.          .endif
  22.  
  23.      .elseif eax == g_pControlDeviceObject
  24.  
  25.          mov eax, dwMajorFunction
  26.          .if eax == IRP_MJ_CREATE
  27.              invoke CDO_DispatchCreate, pDeviceObject, pIrp
  28.              mov status, eax
  29.          .elseif eax == IRP_MJ_CLOSE
  30.              invoke CDO_DispatchClose, pDeviceObject, pIrp
  31.              mov status, eax
  32.          .elseif eax == IRP_MJ_DEVICE_CONTROL
  33.              invoke CDO_DispatchDeviceControl, pDeviceObject, pIrp
  34.              mov status, eax
  35.          .else
  36.  
  37.              mov ecx, pIrp
  38.              mov (_IRP PTR [ecx]).IoStatus.Status, STATUS_INVALID_DEVICE_REQUEST
  39.              and (_IRP PTR [ecx]).IoStatus.Information, 0
  40.  
  41.              fastcall IofCompleteRequest, ecx, IO_NO_INCREMENT
  42.  
  43.              mov status, STATUS_INVALID_DEVICE_REQUEST
  44.    
  45.          .endif
  46.    
  47.      .else
  48.  
  49.          mov ecx, pIrp
  50.          mov (_IRP PTR [ecx]).IoStatus.Status, STATUS_INVALID_DEVICE_REQUEST
  51.          and (_IRP PTR [ecx]).IoStatus.Information, 0
  52.  
  53.          fastcall IofCompleteRequest, ecx, IO_NO_INCREMENT
  54.  
  55.          mov status, STATUS_INVALID_DEVICE_REQUEST
  56.  
  57.      .endif
  58.  
  59.      mov eax, status
  60.      ret
  61.  
  62.  

Используя глобальные указатели g_pFilterDeviceObject и g_pControlDeviceObject, определяем, к какому объекту пришел запрос и, в зависимости от типа запроса вызываем соответствующую процедуру. Наше устройство управления обрабатывает только три типа запросов: IRP_MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_DEVICE_CONTROL. Но мы обязаны обработать все запросы к фильтру. Обработка будет заключаться в простой передаче IRP нижестоящему драйверу в процедуре FiDO_DispatchPassThrough. Запросы типа IRP_MJ_READ несут в себе коды клавиш, поэтому для этого типа запросов обработка будет особой. IRP типа IRP_MJ_POWER просто требует специфической обработки, поэтому и выделен в отдельную процедуру. Если мы, вдруг (чего быть не может), получили запрос для неизвестного нам устройства, завершаем его с кодом ошибки, т.к. не понятно, что ещё с этим IRP можно сделать.

Сначала разберем обработку запросов к устройству управления.



Процедура CDO_DispatchCreate

Код (Text):
  1.  
  2.  
  3.      .while TRUE
  4.  
  5.          invoke RemoveEntry, addr KeyData
  6.          .break .if eax == 0
  7.  
  8.      .endw
  9.  
  10.  

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

Код (Text):
  1.  
  2.  
  3.      MUTEX_ACQUIRE g_mtxCDO_State
  4.  
  5.      .if g_fCDO_Opened
  6.  
  7.          mov status, STATUS_DEVICE_BUSY
  8.  
  9.      .else
  10.  
  11.          mov g_fCDO_Opened, TRUE
  12.        
  13.          mov status, STATUS_SUCCESS
  14.  
  15.      .endif
  16.  
  17.      MUTEX_RELEASE g_mtxCDO_State
  18.  
  19.  

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



Процедура CDO_DispatchClose

Код (Text):
  1.  
  2.  
  3.      and g_fSpy, FALSE
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.      MUTEX_ACQUIRE g_mtxCDO_State
  4.                
  5.      .if ( g_pFilterDeviceObject == NULL )
  6.  
  7.          .if g_dwPendingRequests == 0
  8.  
  9.              mov eax, g_pDriverObject
  10.              mov (DRIVER_OBJECT PTR [eax]).DriverUnload, offset DriverUnload
  11.  
  12.          .endif
  13.  
  14.      .endif
  15.  
  16.  

Если переменная g_pFilterDeviceObject пуста, то, очевидно, что нет и фильтра. Если к тому же у нас нет незавершенных IRP, завершение которых привело бы к вызову нашей процедуры завершения ReadComplete, находящейся в теле драйвера, то можно разрешить его выгрузить. Если фильтр всё ещё существует, драйвер остается невыгружаемым. Перед завершением работы программа управления просит драйвер отключить и удалить фильтр. Но возможны ситуации, когда драйвер не сможет этого сделать. Например, если кто-то подключен к стеку поверх нас, отключение фильтра "разорвет стек". Программа управления может просто забыть отключить фильтр или в ней может произойти исключение и описатель устройства автоматически закрывается системой. Речь, разумеется, не идет о нашей программе управления, в которой (я надеюсь) все сделано правильно. Имеется в виду программа управления вообще, т.е. общий принцип. Наконец, пользователь может завершать сеанс работы с системой, и все пользовательские процессы принудительно завершаются. В любом случае, как я уже сказал, драйвер и программа управления построены таким образом, что программу управления можно запустить повторно.

Код (Text):
  1.  
  2.  
  3.      and g_fCDO_Opened, FALSE    
  4.  
  5.      MUTEX_RELEASE g_mtxCDO_State
  6.  
  7.  

Т.к. единственный наш клиент только что "ушел", сбрасываем флаг g_fCDO_Opened.



Процедура CDO_DispatchDeviceControl

Код (Text):
  1.  
  2.  
  3.              MUTEX_ACQUIRE g_mtxCDO_State
  4.  
  5.              mov edx, [esi].AssociatedIrp.SystemBuffer
  6.              mov edx, [edx]
  7.  
  8.              mov ecx, ExEventObjectType
  9.              mov ecx, [ecx]
  10.              mov ecx, [ecx]
  11.    
  12.              invoke ObReferenceObjectByHandle, edx, EVENT_MODIFY_STATE, ecx, \
  13.                                          UserMode, addr pEventObject, NULL
  14.              .if eax == STATUS_SUCCESS
  15.  
  16.  

При получении от программы управления управляющего кода IOCTL_KEYBOARD_ATTACH, захватываем мьютекс и проверяем переданный нам описатель объекта "событие". Это мы уже делали в Process Monitor (см. часть 14). Если это действительно объект "событие", то у нас есть два варианта: либо мы должны создать фильтр и подключить его к стеку клавиатуры, либо фильтр уже существует и подключён.

Код (Text):
  1.  
  2.  
  3.                  .if !g_fFiDO_Attached
  4.  
  5.                      invoke KeyboardAttach
  6.                      mov [esi].IoStatus.Status, eax
  7.  
  8.  

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

Код (Text):
  1.  
  2.  
  3.                      .if eax == STATUS_SUCCESS
  4.  
  5.                          mov eax, pEventObject
  6.                          mov g_pEventObject, eax
  7.  
  8.                          mov g_fFiDO_Attached, TRUE
  9.                          mov g_fSpy, TRUE
  10.        
  11.                      .else
  12.                          invoke ObDereferenceObject, pEventObject
  13.                      .endif
  14.  
  15.  

Если подключение прошло успешно, запоминаем указатель на объект "событие" в глобальной переменной g_pEventObject и взводим флаги g_fFiDO_Attached и g_fSpy. Хотя фильтр уже подключен, блокировать доступ к переменной g_pEventObject, в данном случае, не требуется, т.к. флаг g_fSpy взводится после инициализации переменной g_pEventObject, а до тех пор процедура FiDO_DispatchRead не будет устанавливать процедуру завершения, а значит ReadComplete вообще не будет вызываться и g_pEventObject кроме нас никто трогать не будет.

Код (Text):
  1.  
  2.  
  3.                  .else
  4.  
  5.                      LOCK_ACQUIRE g_EventSpinLock
  6.                      mov bl, al
  7.  
  8.  

Если фильтр уже подключен, необходимо блокировать доступ к переменной g_pEventObject, т.к. к ней может обращаться наша процедура завершения ReadComplete. Спин-блокировка требуется из-за того, что ReadComplete работает на IRQL = DISPATCH_LEVEL.

Код (Text):
  1.  
  2.  
  3.                      mov eax, g_pEventObject
  4.                      .if eax != NULL
  5.                          and g_pEventObject, NULL
  6.                          invoke ObDereferenceObject, eax
  7.                      .endif
  8.  
  9.                      mov eax, pEventObject
  10.                      mov g_pEventObject, eax
  11.  
  12.                      LOCK_RELEASE g_EventSpinLock, bl
  13.  
  14.  

На всякий случай, если g_pEventObject содержит указатель на объект событие, уменьшаем счетчик ссылок и заносим туда указатель на новый объект событие. Тут требуется небольшое пояснение. На первый взгляд, этот код может показаться бессмысленным. Дело в том, что в предыдущих примерах, мы, для простоты, предполагали корректное поведение программы управления драйвером. Но, в идеале, драйвер должен быть непотопляем, даже если его собственная программа управления или кто-то другой выполнит какие-то непредсказуемые действия. В состав инструментов DDK для тестирования драйверов даже входит специальная утилита Device Path Exerciser (dc2.exe), которая, среди других тестов, шлет драйверу огромное количество управляющих кодов с заведомо неверными параметрами. Если программа управления дважды пошлет драйверу IOCTL_KEYBOARD_ATTACH, то, благодаря вышеприведенному коду, мы сможем корректно разобраться с двумя объектами "событие", а мьютекс g_mtxCDO_State избавит нас от множества потенциальных проблем.

Код (Text):
  1.  
  2.  
  3.          MUTEX_ACQUIRE g_mtxCDO_State
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.          .if g_fFiDO_Attached
  4.  
  5.              and g_fSpy, FALSE
  6.  
  7.              invoke KeyboardDetach
  8.              mov [esi].IoStatus.Status, eax
  9.  
  10.  

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

Код (Text):
  1.  
  2.  
  3.              .if eax == STATUS_SUCCESS
  4.                  mov g_fFiDO_Attached, FALSE
  5.              .endif
  6.  
  7.  

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

Код (Text):
  1.  
  2.  
  3.              LOCK_ACQUIRE g_EventSpinLock
  4.              mov bl, al
  5.  
  6.              mov eax, g_pEventObject
  7.              .if eax != NULL
  8.                  and g_pEventObject, NULL
  9.                  invoke ObDereferenceObject, eax
  10.              .endif
  11.  
  12.              LOCK_RELEASE g_EventSpinLock, bl
  13.  
  14.  

Под защитой спин-блокировки, удаляем ссылку на объект "событие".

Код (Text):
  1.  
  2.  
  3.              invoke FillKeyData, [esi].AssociatedIrp.SystemBuffer, \
  4.                          [edi].Parameters.DeviceIoControl.OutputBufferLength
  5.  
  6.  

При получении от программы управления управляющего кода IOCTL_GET_KEY_DATA, копируем в пользовательский буфер имеющиеся у нас к настоящему моменту структуры KEY_DATA. Процедуру FillKeyData, а также AddEntry и RemoveEntry я разбирать не буду, т.к. если вы читали седьмую часть цикла "Использование ассоциативных списков", их содержимое не должно представлять сложность, а подробности относительно структуры KEYBOARD_INPUT_DATA смотрите в DDK.



Процедура KeyboardAttach

Код (Text):
  1.  
  2.  
  3.      .if ( g_pFilterDeviceObject != NULL )
  4.  
  5.          mov status, STATUS_SUCCESS
  6.  
  7.  

Если переменная g_pFilterDeviceObject не равна нулю, очевидно, она содержит указатель на объект "устройство-фильтр" и, наверное, он уже подключен к стеку.

Код (Text):
  1.  
  2.  
  3.      .else
  4.  
  5.  

Если фильтра нет, создадим его.

Код (Text):
  1.  
  2.  
  3.          mov eax, g_pControlDeviceObject
  4.          mov ecx, (DEVICE_OBJECT PTR [eax]).DriverObject
  5.  
  6.          invoke IoCreateDevice, ecx, sizeof FiDO_DEVICE_EXTENSION, NULL, \
  7.                      FILE_DEVICE_UNKNOWN, 0, FALSE, addr g_pFilterDeviceObject
  8.          .if eax == STATUS_SUCCESS
  9.  
  10.  

Объект "устройство-фильтр" должен быть безымянным, для того чтобы его нельзя было открыть напрямую по имени. Т.к. фильтр принадлежит стеку, но является привнесенным объектом, то явно не ему решать, разрешать ли открытие описателя или нет. Пусть с этим разбираются нижестоящие драйверы. На самом деле это не всегда верно. В случае со стеком клавиатуры, высокоуровневый драйвер фильтра Kbdclass имеет именованный объект "устройство-фильтр" KeyboardClassX и именно он обрабатывает запрос IRP_MJ_CREATE. Второй параметр функции IoCreateDevice определяет размер дополнительной области памяти объекта "устройство" (device extension), которая описывается гипотетической структурой DEVICE_EXTENSION. Гипотетической в том смысле, что такой структуры нет. Вы сами определяете, что необходимо поместить в дополнительную область памяти объекта "устройство" и сами определяете структуру. Device extension следует сразу за структурой DEVICE_OBJECT и инициализируется нулями. В нашем случае это структура FiDO_DEVICE_EXTENSION. Использование device extension позволяет драйверу создать сколь угодно много объектов "устройство" и хранить все относящиеся к ним данные в самих этих объектах.

Код (Text):
  1.  
  2.  
  3.              invoke IoGetDeviceObjectPointer, addr g_usTargetDeviceName, FILE_READ_DATA, \
  4.                                         addr pTargetFileObject, addr pTargetDeviceObject
  5.              .if eax == STATUS_SUCCESS
  6.  
  7.  

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

Код (Text):
  1.  
  2.  
  3.                  mov eax, g_pDriverObject
  4.                  and (DRIVER_OBJECT PTR [eax]).DriverUnload, NULL
  5.  
  6.  

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

Код (Text):
  1.  
  2.  
  3.  PDEVICE_OBJECT
  4.    IoGetAttachedDevice(
  5.      IN PDEVICE_OBJECT pDeviceObject
  6.      )
  7.  {
  8.  
  9.      while  pDeviceObject->AttachedDevice  {
  10.  
  11.          pDeviceObject = pDeviceObject->AttachedDevice
  12.      }
  13.  
  14.      return pDeviceObject
  15.  }
  16.  
  17.  
  18.  PDEVICE_OBJECT
  19.    IoAttachDeviceToDeviceStack(
  20.      IN PDEVICE_OBJECT pSourceDevice,
  21.      IN PDEVICE_OBJECT pTargetDevice
  22.      )
  23.  {
  24.      PDEVICE_OBJECT     pTopMostDeviceObject
  25.      PDEVOBJ_EXTENSION  pSourceDeviceExtension
  26.  
  27.      pSourceDeviceExtension = pSourceDevice->DeviceObjectExtension
  28.  
  29.      ExAcquireSpinLock( &IopDatabaseLock, ... )
  30.  
  31.      pTopMostDeviceObject = IoGetAttachedDevice( pTargetDevice )
  32.  
  33.      if  pTopMostDeviceObject->Flags & DO_DEVICE_INITIALIZING
  34.             ||
  35.          pTopMostDeviceObject->DeviceObjectExtension->ExtensionFlags &
  36.          (DOE_UNLOAD_PENDING | DOE_DELETE_PENDING | DOE_REMOVE_PENDING | DOE_REMOVE_PROCESSED)  {
  37.  
  38.          pTopMostDeviceObject = NULL
  39.  
  40.      } else {
  41.  
  42.          pTopMostDeviceObject ->AttachedDevice = pSourceDevice
  43.  
  44.          pSourceDevice->AlignmentRequirement = pTopMostDeviceObject->AlignmentRequirement
  45.          pSourceDevice->SectorSize           = pTopMostDeviceObject->SectorSize
  46.          pSourceDevice->StackSize            = pTopMostDeviceObject->StackSize + 1
  47.  
  48.  
  49.          if  pTopMostDeviceObject ->DeviceObjectExtension->ExtensionFlags & DOE_START_PENDING  {
  50.  
  51.              pSourceDevice->DeviceObjectExtension->ExtensionFlags |= DOE_START_PENDING
  52.          }
  53.  
  54.          pSourceDeviceExtension->AttachedTo = pTopMostDeviceObject
  55.      }
  56.  
  57.      ExReleaseSpinLock( &IopDatabaseLock, ... )
  58.  
  59.      return pTopMostDeviceObject
  60.  }
  61.  
  62.  

Сначала функция IoAttachDeviceToDeviceStack получает указатель на структуру DEVOBJ_EXTENSION (не путать с необязательной структурой DEVICE_EXTENSION). В поле ExtensionFlags этой структуры имеются кое-какие интересующие функцию IoAttachDeviceToDeviceStack флаги. Затем блокируется база данных диспетчера ввода/вывода и в pTopMostDeviceObject заносится указатель на объект "устройство", находящийся на вершине стека. Т.к. база данных диспетчера ввода/вывода блокирована, то состояние стека не изменится до снятия блокировки. Если объект "устройство" ещё не инициализирован или устройство или его драйвер отмечены для удаления или уже находится в состоянии удаления, функция IoAttachDeviceToDeviceStack отказывается прикреплять к стеку новый объект и возвращает NULL. В противном случае в поля AttachedDevice и AttachedTo объектов "устройство" заносятся соответствующие указатели (в этом и заключается процесс подключения к стеку нового объекта) и в подключенном объекте обновляются поля AlignmentRequirement, SectorSize и StackSize. AlignmentRequirement и SectorSize важны для устройств хранения, а StackSize необходимо увеличить на единицу в любом случае, т.к. глубина стека увеличилась на один объект (подробности см. в предыдущей статье). Обратите внимание на то, что подключение происходит не к объекту, указатель на который передан в параметре pTargetDevice, а к вершине стека кто бы на ней не находился. Если в промежутке между получением указателя на pTargetDevice и вызовом IoAttachDeviceToDeviceStack кто-то успеет подключить к стеку свой объект, pTargetDevice и возвращаемый pTopMostDeviceObject будут отличаться. В любом случае, возвращаемое из IoAttachDeviceToDeviceStack значение, в случае успеха, является указателем на объект "устройство", к которому был подключен фильтр. А фильтр теперь является вершиной стека и первым получает все IRP для этого стека предназначенные. В начале статьи мы выяснили, что поток необработанного ввода открывает один из объектов стека клавиатуры и, используя его описатель (точнее описатель объекта "файл", соответствующий объекту "устройство"), направляет ему запросы на чтение. Если IRP предназначаются какому-то устройству ниже по стеку, то каким образом они попадают в фильтр? Подсистема ввода/вывода ведет себя аналогично функциям IoGetAttachedDevice и IoAttachDeviceToDeviceStack, в том смысле, что в качестве адресата для IRP использует указатель на вершину стека. Вот, например, что делает функция ZwReadFile.

Код (Text):
  1.  
  2.  
  3.  NTSTATUS
  4.     ZwReadFile(
  5.      IN HANDLE hFile,
  6.      . . .
  7.      )
  8.  {
  9.  
  10.      PFILE_OBJECT    pFileObject
  11.      PDEVICE_OBJECT  pDeviceObject
  12.  
  13.      ObReferenceObjectByHandle( hFile, ... &pFileObject ... )
  14.  
  15.      pDeviceObject = IoGetRelatedDeviceObject( pFileObject )
  16.  
  17.      . . .
  18.  }
  19.  
  20.  

Функция IoGetRelatedDeviceObject (исходный код см. в предыдущей статье) возвращает указатель на самый верхний объект "устройство" в стеке. Если же IRP формируется драйвером, так сказать вручную (см. исходный код процедуры QueryPnpDeviceState в предыдущей статье), то он будет послан напрямую целевому устройству и перехватить такой запрос с помощью фильтра невозможно, если конечно фильтр не находится ниже по стеку.

Код (Text):
  1.  
  2.  
  3.                  invoke IoAttachDeviceToDeviceStack, g_pFilterDeviceObject, pTargetDeviceObject
  4.                  .if eax != NULL
  5.  
  6.                      mov edx, eax
  7.  
  8.                      mov ecx, g_pFilterDeviceObject
  9.                      mov eax, (DEVICE_OBJECT ptr [ecx]).DeviceExtension
  10.                      assume eax:ptr FiDO_DEVICE_EXTENSION
  11.                      mov [eax].pNextLowerDeviceObject, edx
  12.                      push pTargetFileObject
  13.                      pop [eax].pTargetFileObject
  14.                      assume eax:nothing
  15.  
  16.  

Если IoAttachDeviceToDeviceStack подключила нас к стеку, заполняем структуру FiDO_DEVICE_EXTENSION. Туда мы помещаем указатель на объект, к которому мы подключились и указатель на объект "файл" ассоциированный с целевым объектом устройство (подробности см. в предыдущей статье). При отключении мы должны будем вызвать ObDereferenceObject по отношению к этому объекту "файл".

Код (Text):
  1.  
  2.  
  3.                      assume edx:ptr DEVICE_OBJECT
  4.                      assume ecx:ptr DEVICE_OBJECT
  5.  
  6.                      mov eax, [edx].DeviceType
  7.                      mov [ecx].DeviceType, eax
  8.  
  9.                      mov eax, [edx].Flags
  10.                      and eax, DO_DIRECT_IO + DO_BUFFERED_IO + DO_POWER_PAGABLE
  11.                      or [ecx].Flags, eax
  12.  
  13.  

Несколько флагов в нашем объекте фильтре придется обновить самостоятельно. Дело в том, что для диспетчера ввода/вывода наш объект должен выглядеть также как объект, к которому мы подключились. Например, флаг DO_BUFFERED_IO говорит диспетчеру ввода/вывода о том, что при операциях чтения/записи он должен копировать пользовательские буферы в системное адресное пространство, т.е. использовать метод ввода/вывода METHOD_BUFFERED. Флаги DO_DIRECT_IO и DO_BUFFERED_IO, естественно, взаимоисключающи. Хотя нам заранее известны флаги, которые использует устройство KeyboardClass0, а именно DO_BUFFERED_IO и DO_POWER_PAGABLE, мы используем более общий и универсальный механизм.

Код (Text):
  1.  
  2.  
  3.                      and [ecx].Flags, not DO_DEVICE_INITIALIZING
  4.  
  5.  

Функция IoCreateDevice создает объект "устройство" с установленным флагом DO_DEVICE_INITIALIZING. До сих пор мы не касались этого момента потому, что создавали устройства только в процедуре DriverEntry. Дело в том, что по выходу из DriverEntry диспетчер ввода/вывода (в функции IopReadyDeviceObjects) сам сбрасывает этот флаг во всех объектах "устройство", созданных драйвером. Если же мы создаем устройство не в DriverEntry, придется сбросить флаг DO_DEVICE_INITIALIZING самостоятельно, иначе никто не сможет подключиться к неинициализированному объекту, как вы только что видели в коде IoAttachDeviceToDeviceStack. Также этот флаг проверяется при некоторых других операциях.



Процедура KeyboardDetach

Код (Text):
  1.  
  2.  
  3.      .if g_pFilterDeviceObject != NULL
  4.  
  5.          mov eax, g_pFilterDeviceObject
  6.          or (DEVICE_OBJECT ptr [eax]).Flags, DO_DEVICE_INITIALIZING
  7.  
  8.  

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

Код (Text):
  1.  
  2.  
  3.  PDEVICE_OBJECT
  4.    IoGetAttachedDeviceReference(
  5.      IN PDEVICE_OBJECT pDeviceObject
  6.      )
  7.  {
  8.      ExAcquireSpinLock( &IopDatabaseLock, ... )
  9.  
  10.      pDeviceObject = IoGetAttachedDevice( pDeviceObject )
  11.      ObReferenceObject( pDeviceObject )
  12.  
  13.      ExReleaseSpinLock( &IopDatabaseLock, ... )
  14.  
  15.      return pDeviceObject
  16.  }
  17.  
  18.  

Здесь должно быть всё понятно.

Код (Text):
  1.  
  2.  
  3.          invoke IoGetAttachedDeviceReference, g_pFilterDeviceObject
  4.          mov pTopmostDeviceObject, eax
  5.  
  6.          .if eax != g_pFilterDeviceObject
  7.  
  8.              mov eax, g_pFilterDeviceObject
  9.              and (DEVICE_OBJECT ptr [eax]).Flags, not DO_DEVICE_INITIALIZING
  10.  
  11.  

Если, возвращенный функцией IoGetAttachedDeviceReference, указатель не является указателем на наш фильтр, значит, к нам кто-то подключен. В этом случае отключаться от стека мы не будем и сбросим флаг DO_DEVICE_INITIALIZING. Если мы вызовем IoDetachDevice, то просто "разорвем стек", т.к. IoDetachDevice не делает каких бы то ни было проверок. Отключение объекта "устройство" от стека состоит в простом обнулении соответствующих указателей в связанных объектах.

Код (Text):
  1.  
  2.  
  3.  VOID
  4.    IoDetachDevice(
  5.      IN OUT PDEVICE_OBJECT pTargetDeviceObject
  6.      )
  7.  {
  8.      PDEVICE_OBJECT    pDeviceToDetach
  9.      PDEVOBJ_EXTENSION pDeviceToDetachExtension
  10.  
  11.      ExAcquireSpinLock( &IopDatabaseLock, ... )
  12.  
  13.      pDeviceToDetach          = pTargetDeviceObject->AttachedDevice
  14.      pDeviceToDetachExtension = pDeviceToDetach->DeviceObjectExtension
  15.  
  16.      pDeviceToDetachExtension->AttachedTo = NULL
  17.      pTargetDeviceObject->AttachedDevice  = NULL
  18.  
  19.      if  pTargetDeviceObject->DeviceObjectExtension->ExtensionFlags &
  20.          (DOE_UNLOAD_PENDING | DOE_DELETE_PENDING | DOE_REMOVE_PENDING)
  21.              &&
  22.          pTargetDeviceObject->ReferenceCount == 0
  23.      {
  24.  
  25.          // Complete Unload Or Delete
  26.      }
  27.  
  28.      ExReleaseSpinLock( &IopDatabaseLock, ... )
  29.  
  30.  }
  31.  
  32.  

Отключив устройство от стека, IoDetachDevice проверяет, не ожидает ли оно удаления, а его драйвер выгрузки. И если это так и счетчик ссылок на объект равен нулю, инициирует отложенные операции.

Код (Text):
  1.  
  2.  
  3.          .else          
  4.  
  5.              mov eax, g_pFilterDeviceObject
  6.              mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
  7.              mov ecx, (FiDO_DEVICE_EXTENSION ptr [eax]).pTargetFileObject
  8.  
  9.              fastcall ObfDereferenceObject, ecx
  10.  
  11.              mov eax, g_pFilterDeviceObject
  12.              mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
  13.              mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject
  14.  
  15.              invoke IoDetachDevice, eax
  16.            
  17.              mov status, STATUS_SUCCESS
  18.  
  19.  

Если мы на вершине стека, уменьшаем счетчик ссылок на файловый объект, ассоциированный с объектом устройство, и отключаемся от стека. Восстанавливать флаг DO_DEVICE_INITIALIZING не имеет смысла, т.к. сейчас мы удалим фильтр.

Код (Text):
  1.  
  2.  
  3.              mov eax, g_pFilterDeviceObject
  4.              and g_pFilterDeviceObject, NULL
  5.              invoke IoDeleteDevice, eax
  6.  
  7.  

Удаляем объект "устройство-фильтр", но драйвер всё ещё остается невыгружаемым, т.к. мы можем иметь ожидающий завершения IRP, содержащий указатель на нашу процедуру завершения.

Код (Text):
  1.  
  2.  
  3.          .endif
  4.  
  5.          invoke ObDereferenceObject, pTopmostDeviceObject
  6.  
  7.  

Функция IoGetAttachedDeviceReference, в отличие от функции IoGetAttachedDevice, увеличивает счетчик ссылок в объекте, указатель на который возвращает. Это гарантирует, что объект не будет удален. Если мы были на вершине стека, то увеличили счетчик ссылок в нашем же объекте "устройство-фильтр" и IoDeleteDevice не сможет его удалить.

Код (Text):
  1.  
  2.  
  3.  VOID
  4.    IoDeleteDevice(
  5.      IN PDEVICE_OBJECT pDeviceObject
  6.      )
  7.  {
  8.      ...
  9.  
  10.      ExAcquireSpinLock( &IopDatabaseLock, ... )
  11.  
  12.      pDeviceObject->DeviceObjectExtension->ExtensionFlags |= DOE_DELETE_PENDING
  13.  
  14.      if  pDeviceObject->ReferenceCount == 0  {
  15.  
  16.          // Complete Unload Or Delete
  17.      }
  18.  
  19.      ExReleaseSpinLock( &IopDatabaseLock, ... )
  20.  }
  21.  
  22.  

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

Теперь разберем процедуры обработки запросов к фильтру.



Процедура FiDO_DispatchPower

Код (Text):
  1.  
  2.  
  3.      invoke PoStartNextPowerIrp, pIrp
  4.  
  5.      IoSkipCurrentIrpStackLocation pIrp
  6.    
  7.      mov eax, pDeviceObject
  8.      mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
  9.      mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject
  10.  
  11.      invoke PoCallDriver, eax, pIrp
  12.  
  13.  

IRP типа IRP_MJ_POWER обрабатываются отличным от всех остальных типов IRP способом.

Макрос IoCopyCurrentIrpStackLocationToNext мы подробно разобрали в прошлой статье (его мы будем использовать в процедуре FiDO_DispatchRead). Макрос IoSkipCurrentIrpStackLocation намного проще.

Код (Text):
  1.  
  2.  
  3.  IoSkipCurrentIrpStackLocation MACRO pIrp:REQ
  4.      mov eax, pIrp
  5.      inc (_IRP PTR [eax]).CurrentLocation
  6.      add (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation, sizeof IO_STACK_LOCATION
  7.  ENDM
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.      Irp->CurrentLocation--
  4.      pIrp->Tail.Overlay.CurrentStackLocation -= sizeof(IO_STACK_LOCATION)
  5.  
  6.  

Если перед этим использовать макрос IoSkipCurrentIrpStackLocation, то получиться, что указатель блока стека вообще не изменится и нижестоящий драйвер получить тот же самый блок стека, что и драйвер вызвавший IoCallDriver (PoCallDriver). Вызов макроса IoSkipCurrentIrpStackLocation - это просто оптимизация. Действительно, если нам не нужно устанавливать процедуру завершения, то вызов макроса IoCopyCurrentIrpStackLocationToNext скопирует наш блок стека в блок стека нижестоящего драйвера (поля Control, CompletionRoutine и Context, как вы помните, не копируются). Т.о. нижестоящий драйвер всё равно получит те же самые параметры. Используя макрос IoSkipCurrentIrpStackLocation вместо IoCopyCurrentIrpStackLocationToNext, мы избегаем ненужной операции копирования блоков стека. Но, повторяю, это можно делать, только если не требуется устанавливать процедуру завершения, что должно быть и так понятно.



Процедура FiDO_DispatchPassThrough

Код (Text):
  1.  
  2.  
  3.      IoSkipCurrentIrpStackLocation pIrp
  4.  
  5.      mov eax, pDeviceObject
  6.      mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
  7.      mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject
  8.  
  9.      invoke IoCallDriver, eax, pIrp
  10.      ret
  11.  
  12.  

Здесь просто передаем IRP нижестоящему драйверу.



Процедура FiDO_DispatchRead

Код (Text):
  1.  
  2.  
  3.      .if g_fSpy
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.          lock inc g_dwPendingRequests
  4.  
  5.  

Атомарно увеличиваем значение счетчика незавершенных запросов g_dwPendingRequests на единицу. Когда IRP будет завершаться, система вызовет нашу процедуру завершения ReadComplete, она прочитает код клавиши и уменьшит счетчик g_dwPendingRequests. "Атомарно" - означает, что только один поток даже на многопроцессорной машине сможет изменить значение переменной, а IRQL, на котором он выполняется, вообще не имеет значения. Даже если поток, выполняющийся на другом процессоре, попытается в то же самое время (на MP-машине в буквальном смысле) выполнить такой же код, он получит уже обновленное первым потоком значение. Это достигается за счет использования префикса lock. Увидев этот префикс, процессор блокирует шину данных на время выполнения инструкции. Другие процессоры не смогут в этот момент обратиться к этой области памяти и изменить её. Даже если эта область памяти кэшируется несколькими процессорами, в действие вступит механизм обеспечения когерентности кэша (processor's cache coherency mechanism) и кэши других процессоров будут объявлены недействительными, в результате чего процессоры должны будут повторно загрузить кеши уже обновленным содержимым памяти. Префикс lock может использоваться не со всеми инструкциями, а некоторые (например, xchg) всегда выполняются с этим префиксом. Подробнее см. "Intel Architecture Software Developer's Manual". Система (как ядро, так и режим пользователя) экспортирует целый набор Interlocked-функций, реализующих атомарный доступ, но мы можем использовать средства ассемблера.

Код (Text):
  1.  
  2.  
  3.          IoCopyCurrentIrpStackLocationToNext pIrp
  4.  
  5.          IoSetCompletionRoutine pIrp, ReadComplete, NULL, TRUE, TRUE, TRUE
  6.  
  7.  

Устанавливаем процедуру завершения (детали см. в предыдущей статье). Когда IRP будет завершаться, мы сможем узнать код клавиши.

Код (Text):
  1.  
  2.  
  3.      .else
  4.  
  5.          IoSkipCurrentIrpStackLocation pIrp
  6.  
  7.  

Если флаг g_fSpy сброшен, процедура FiDO_DispatchRead ведет себя аналогично процедуре FiDO_DispatchPassThrough.

Код (Text):
  1.  
  2.  
  3.      .endif
  4.  
  5.      mov eax, pDeviceObject
  6.      mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
  7.      mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject
  8.  
  9.      invoke IoCallDriver, eax, pIrp
  10.  
  11.      ret
  12.  
  13.  

Обратите внимание на то, что из всех процедур FiDO_XXX мы возвращаем код, который вернула функция IoCallDriver (PoCallDriver). Соответственно, DriverDispatch возвращает его в систему.



Процедура ReadComplete

Ну, и наконец, процедура ReadComplete, где собственно и происходят главные события, а именно получение кодов клавиш. Мы установили адрес этой процедуры в наш блок стека вызовом макроса IoSetCompletionRoutine. Когда IRP завершается, функция IoCompleteRequest последовательно вызывает все процедуры завершения. Этому и была посвящена практически вся предыдущая статья. IRP завершается в результате пост-обработки аппаратного прерывания (в нашем случае прерывания от контроллера клавиатуры), а значит в контексте случайного потока и на повышенном IRQL.

Код (Text):
  1.  
  2.  
  3.      .if [esi].IoStatus.Status == STATUS_SUCCESS
  4.  
  5.          mov edi, [esi].AssociatedIrp.SystemBuffer
  6.          assume edi:ptr KEYBOARD_INPUT_DATA
  7.  
  8.  

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

Код (Text):
  1.  
  2.        
  3.          mov ebx, [esi].IoStatus.Information
  4.  
  5.  

Поле Information содержит размер действительной части буфера и должно быть кратно размеру структуры KEYBOARD_INPUT_DATA.

Код (Text):
  1.  
  2.  
  3.          and cEntriesLogged, 0
  4.          .while sdword ptr ebx >= sizeof KEYBOARD_INPUT_DATA
  5.            
  6.              movzx eax, [edi].MakeCode
  7.              mov KeyData.dwScanCode, eax
  8.  
  9.              movzx eax, [edi].Flags
  10.              mov KeyData.Flags, eax
  11.  
  12.              invoke AddEntry, addr KeyData
  13.                
  14.              inc cEntriesLogged
  15.  
  16.              add edi, sizeof KEYBOARD_INPUT_DATA
  17.              sub ebx, sizeof KEYBOARD_INPUT_DATA
  18.          .endw
  19.  
  20.          assume edi:nothing
  21.  
  22.  

Перекладываем интересующие нас поля структуры KEYBOARD_INPUT_DATA в нашу структуру KEY_DATA_ENTRY и привязываем её к двусвязному списку g_KeyDataListHead. Это мы делаем в функции AddEntry и под защитой спин-блокировки g_KeyDataSpinLock. Блокировка списка нужна, как вы понимаете, для монопольного доступа к списку, а спин-блокировкой она должна быть потому, что процедура ReadComplete выполняется на IRQL = DISPATCH_LEVEL. DDK утверждает, что процедуры завершения могут быть вызваны на IRQL <= DISPATCH_LEVEL, но в данном случае мы всегда будем строго на IRQL = DISPATCH_LEVEL. Функция KeyboardClassServiceCallback драйвера Kbdclass, которая собственно и завершает IRP, использует KeAcquireSpinLockAtDpcLevel и KeReleaseSpinLockFromDpcLevel для блокировки.

Код (Text):
  1.  
  2.  
  3.  VOID
  4.    KeyboardClassServiceCallback(
  5.      . . .
  6.      )
  7.  {
  8.  
  9.      . . .
  10.  
  11.      //
  12.      // N.B. We can use KeAcquireSpinLockAtDpcLevel, instead of
  13.      //      KeAcquireSpinLock, because this routine is already running
  14.      //      at DISPATCH_IRQL.
  15.      //
  16.  
  17.      KeAcquireSpinLockAtDpcLevel( &deviceExtension->SpinLock );
  18.  
  19.      . . .
  20.  
  21.      //
  22.      // Release the class input data queue spinlock.
  23.      //
  24.  
  25.      KeReleaseSpinLockFromDpcLevel( &deviceExtension->SpinLock );
  26.  
  27.      . . .
  28.  
  29.      IoCompleteRequest( irp, IO_KEYBOARD_INCREMENT );
  30.  
  31.      . . .
  32.  }
  33.  
  34.  

Но я всё равно использую макросы LOCK_ACQUIRE и LOCK_RELEASE (мне так больше нравится ;) ).

Код (Text):
  1.  
  2.  
  3.          .if cEntriesLogged != 0
  4.  
  5.              LOCK_ACQUIRE g_EventSpinLock
  6.              mov bl, al
  7.  
  8.              .if g_pEventObject != NULL
  9.                  invoke KeSetEvent, g_pEventObject, 0, FALSE
  10.              .endif
  11.            
  12.              LOCK_RELEASE g_EventSpinLock, bl
  13.                        
  14.          .endif
  15.  
  16.  

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

Код (Text):
  1.  
  2.  
  3.      .if [esi].PendingReturned
  4.          IoMarkIrpPending esi
  5.      .endif
  6.  
  7.  

Поскольку мы возвращаем из процедуры завершения код отличный от STATUS_MORE_PROCESSING_REQUIRED, то должны следовать правилу №6 (см. часть 15).

Код (Text):
  1.  
  2.  
  3.      lock dec g_dwPendingRequests
  4.  
  5.  

Запрос обработан - атомарно уменьшаем счетчик g_dwPendingRequests.

Код (Text):
  1.  
  2.  
  3.      mov eax, STATUS_SUCCESS
  4.  
  5.  

Завершение IRP должно быть продолжено. Из материала предыдущей статьи вы должны помнить, что из процедур завершения можно возвращать либо STATUS_MORE_PROCESSING_REQUIRED, либо любой другой код. Код STATUS_SUCCESS использован для наглядности.



Программа управления

Код программы управления разберите сами. Ничего принципиально нового там нет. Поясню только несколько моментов. Во-первых, как известно, скорость набора текста примерно в 10 знаков в секунду является хорошим показателем. Так что максимально мы можем получать около 20 структур KEY_DATA в секунду, и в то же самое время до клавиатуры могут очень долго не дотрагиваться. Поэтому для исключения лишних запросов к драйверу мы забираем накопившуюся информацию не чаше чем раз в секунду. А если забирать нечего, то и вовсе ничего не запрашиваем. Такая логика работы достигается за счет усыпления потока на некоторое время и ожидания события, которое сигнализирует драйвер. Во-вторых, т.к. 20 структур KEY_DATA - это намного меньше, чем одна страница, мы используем буферизованный ввод/вывод и забираем информацию через DeviceIoControl. Если требуется обмен бОльшими объемами (скажем, ориентировочно, несколько страниц), то лучше использовать метод METHOD_NEITHER, а вместо DeviceIoControl - ReadFile. В-третьих, пользователь может закрыть программу управления либо с помощью мыши, либо с помощью клавиатуры. Если он использует мышь, то драйвер не будет выгружен, т.к. последний прошедший через драйвер IRP содержит указатель на нашу процедуру завершения и находится сейчас в очереди драйвера Kbdclass. Чтобы программа управления имела возможность выгрузить драйвер, пользователь должен нажать на клавишу. Поэтому, выводя соответствующее сообщение, мы даем пользователю необходимые инструкции. И наконец, про обещанную отмену IRP. Если бы можно было отменить этот злосчастный ожидающий завершения IRP, то нам не пришлось бы пугать пользователя странными сообщениями. Мы бы просто отменили этот запрос и выгрузили бы драйвер. И такой механизм существует. Драйвер Kbdclass поддерживает отмену только одного типа IRP и это как раз IRP_MJ_READ. Проблема в том, что отменить IRP, находящийся в очереди другого драйвера не так то просто. В своей книге "Programming The Windows Driver Model" 2nd Edition Вальтер Они приводит пару способов отмены чужих IRP. Первый их них точно не подойдет, а вот второй… Если не подойдет и второй, то остается только организовать свою собственную очередь и помещать туда все пришедшие IRP типа IRP_MJ_READ, а нижестоящему драйверу слать их дубликаты. Завершения дублированных IRP перехватывать, извлекать из очереди их оригиналы и перекладывать в них необходимые данные. Если у фильтра будет своя очередь, то отмена IRP становится делом техники. Насколько этот сценарий возможен практически, я не знаю, т.к. при его реализации могут возникнуть непредвиденные сложности.

Ну, и самое последнее. В архиве вы обнаружите сразу два фильтра. Второй - MouSpy, получен путем замены слов "keyboard", "kbd" и т.п. в прародителе на их "мышиные" аналоги. Ну и конечно я не смог удержаться и добавил ещё кое-что. Поэтому этот фильтр не просто пассивно следит за "мышиными" событиями, но может вносить в них некоторые коррективы. Но, если у вас клавиатура/мышь USB, то подключить фильтры, скорее всего, не удастся. Во всяком случае, подключить фильтр к стеку для USB-мыши мне не удалось, а USB-клавиатуры у меня нет. Причина в том, что внутренне функция IoGetDeviceObjectPointer вызывает функцию ZwOpenFile, а она, в свою очередь, формирует запрос IRP_MJ_CREATE и шлет его в стек (см. предыдущую статью). Вот три стека мыши на одной из моих машин: первый для классической PS/2 мыши, второй для терминальной сессии и третий для мыши USB.

Код (Text):
  1.  
  2.  
  3.  kd> !drvobj mouclass
  4.  Driver object (816a68e8) is for:
  5.   \Driver\Mouclass
  6.  Driver Extension List: (id , addr)
  7.  
  8.  Device Object list:
  9.  812b5a20  8169e820  816a3030
  10.  
  11.  
  12.  kd> !devstack 816a3030
  13.    !DevObj   !DrvObj            !DevExt   ObjectName
  14.  > 816a3030  \Driver\Mouclass   816a30e8  PointerClass0
  15.    816a63a8  \Driver\nmfilter   816a6460  0000006c
  16.    816a6530  \Driver\i8042prt   816a65e8
  17.    8192f3e8  \Driver\ACPI       81969008  00000051
  18.  !DevNode 818685e8 :
  19.    DeviceInst is "ACPI\PNP0F13\3&13c0b0c5&0"
  20.    ServiceName is "i8042prt"
  21.  
  22.  
  23.  kd> !devstack 8169e820
  24.    !DevObj   !DrvObj            !DevExt   ObjectName
  25.  > 8169e820  \Driver\Mouclass   8169e8d8  PointerClass1
  26.    8169ea08  \Driver\TermDD     8169eac0  RDP_CONSOLE1
  27.    8197f970  \Driver\PnpManager 8197fa28  00000038
  28.  !DevNode 8197f828 :
  29.    DeviceInst is "Root\RDP_MOU\0000"
  30.    ServiceName is "TermDD"
  31.  
  32.  
  33.  kd> !devstack 812b5a20
  34.    !DevObj   !DrvObj            !DevExt   ObjectName
  35.  > 812b5a20  \Driver\Mouclass   812b5ad8  PointerClass2
  36.    813c1e20  \Driver\mouhid     813c1ed8
  37.    815f2a90  \Driver\HidUsb     815f2b48  00000074
  38.  !DevNode 81361008 :
  39.    DeviceInst is "HID\Vid_09da&Pid_000a\6&3a964113&0&0000"
  40.    ServiceName is "mouhid"
  41.  
  42.  

Видимо, один из драйверов в стеке (скорее всего mouhid) отказывается обработать запрос IRP_MJ_CREATE, возвращая код STATUS_SHARING_VIOLATION, т.е. совместный доступ к файлу запрещен (имеется в виду объект "файл" ассоциированный с объектом "устройство"). Как бы там ни было, в дальнейшие детали я не вдавался. Не удаётся подключиться к USB стеку… и слава богу, ибо "со свиным рылом, да в калашный ряд?"… Как мы будем обрабатывать IRP_MN_QUERY_REMOVE_DEVICE, IRP_MN_REMOVE_DEVICE и IRP_MN_SURPRISE_REMOVAL, в случае если пользователь отключит/выдернет клавиатуру/мышку из USB-порта? Так что, доставайте из чулана своих старых боевых подруг или ждите следующую статью (если я её когда-нибудь напишу ;) ).

Рис. 16-1. KbdSpy и MouSpy в действии.

Исходный код драйвера в архиве.

© Four-F

0 2.706
archive

archive
New Member

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