Драйверы режима ядра: Часть 12: Базовая техника. Синхронизация: Таймер и системный поток — Архив WASM.RU

Все статьи

Драйверы режима ядра: Часть 12: Базовая техника. Синхронизация: Таймер и системный поток — Архив WASM.RU



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

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

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

Для решения подобных задач операционная система предоставляет весьма обширный набор объектов синхронизации (dispatcher objects): событие (event), мьютекс (mutex) - в ядре этот объект называется мутантом (mutant), семафор (semaphore) и др., а также средства управления этими объектами. Большинство объектов синхронизации используется как в ядре, так и в режиме пользователя. Точнее говоря, почти все объекты синхронизации режима пользователя являются оболочками соответствующих объектов режима ядра. В ядре, правда, набор механизмов синхронизации несколько богаче.

Все синхронизирующие объекты первым членом своих структур имеют структуру DISPATCHER_HEADER, через которую система управляет ожиданием. Вот как выглядят структуры объектов "таймер" (timer object) и "поток" (thread object) - эти объекты мы будем сегодня использовать.


 KTIMER STRUCT
     Header            DISPATCHER_HEADER <>
 . . .
 KTIMER ENDS

 KTHREAD STRUCT
     Header            DISPATCHER_HEADER <>
 . . .
 KTHREAD ENDS

Логика работы каждого объекта отличается от логики работы его собратьев, что вполне естественно. Какой объект использовать в том или ином случае зависит от его природы. Я не буду подробно на этом останавливаться, так как предполагаю, что вы уже достаточно поработали с этими объектами в режиме пользователя. Напомню лишь, что каждый объект синхронизации может находиться в одном из двух состояний: свободен (signaled) или занят (nonsignaled). Слова свободен и занят ужасно плохо отражают суть некоторых объектов, но это устоявшиеся термины.

Принципиальной разницы в управлении объектами синхронизации, в режиме пользователя и режиме ядра нет, но есть несколько особенностей. Первая и самая важная: ожидать на объекте синхронизации можно только при IRQL строго меньше DISPATCH_LEVEL! Это связано с тем, что планировщик потоков сам работает на IRQL = DISPATCH_LEVEL. Поэтому, если заставить поток ждать на занятом объекте при IRQL >= DISPATCH_LEVEL, то планировщик не сможет предоставить процессор потоку, использующему занятый объект, а значит, этот поток никогда не сможет его освободить, и ожидание будет продолжаться бесконечно. Вторая особенность в том, что в режиме ядра функции ожидания принимают указатель на объект, а не описатель объекта как в режиме пользователя.

Для ожидания используются две функции: KeWaitForSingleObject - ожидает один объект и KeWaitForMultipleObjects - ожидает несколько объектов. Эти функции ждут, когда объект перейдет в свободное состояние.



12.2 Исходный текст драйвера TimerWorks


 ;@echo off
 ;goto make

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;
 ;  TimerWorks - Создаем поток и таймер.
 ;               Поток по таймеру делает свою работу, а мы подождем когда он её закончит
 ;
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .386
 .model flat, stdcall
 option casemap:none

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                               В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 include \masm32\include\w2k\ntstatus.inc
 include \masm32\include\w2k\ntddk.inc
 include \masm32\include\w2k\ntoskrnl.inc
 include \masm32\include\w2k\hal.inc

 includelib \masm32\lib\w2k\ntoskrnl.lib
 includelib \masm32\lib\w2k\hal.lib

 include \masm32\Macros\Strings.mac

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .const

 CCOUNTED_UNICODE_STRING "\\Device\\TimerWorks", g_usDeviceName, 4
 CCOUNTED_UNICODE_STRING "\\DosDevices\\TimerWorks", g_usSymbolicLinkName, 4

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                     Н Е И Н И Ц И А Л И З И Р О В А Н Н Ы Е    Д А Н Н Ы Е                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .data?

 g_pkThread   PVOID  ?   ; PTR KTHREAD
 g_fStop     BOOL    ?

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                           К О Д                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                        ThreadProc                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 ThreadProc proc Param:DWORD

 Local dwCounter:DWORD
 local pkThread:PVOID         ; PKTHREAD
 local status:NTSTATUS
 local kTimer:KTIMER
 local liDueTime:LARGE_INTEGER

     and dwCounter, 0

     invoke DbgPrint, $CTA0("\nTimerWorks: Entering ThreadProc\n")

     ;:::::::::::::::::::::::::::::::::::::::::::::::::::::
     ; Для образовательных целей посмотрим, какой у нас IRQL
     ; и поиграемся с приоритетом потока

     invoke KeGetCurrentIrql
     invoke DbgPrint, $CTA0("TimerWorks: IRQL = %d\n"), eax


     invoke KeGetCurrentThread
     mov pkThread, eax
     invoke KeQueryPriorityThread, eax
     push eax
     invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax

     pop eax
     inc eax
     inc eax
     invoke KeSetPriorityThread, pkThread, eax

     invoke KeQueryPriorityThread, pkThread
     invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax

     ;:::::::::::::::::::::::::::::::::::::::::::::::::::::

     invoke KeInitializeTimerEx, addr kTimer, SynchronizationTimer

     ; Установим относительный (т.е. от настоящего момента) интервал времени,
     ; через который таймер начнет срабатывать, равным 5 секундам. А период
     ; последующего срабатывания зададим равным одной секунде.

     or liDueTime.HighPart, -1
     mov liDueTime.LowPart, -50000000

     invoke KeSetTimerEx, addr kTimer, liDueTime.LowPart, liDueTime.HighPart, 1000, NULL

     invoke DbgPrint, $CTA0("TimerWorks: Timer is set. It starts counting in 5 seconds\n")

     .while dwCounter < 10
         invoke KeWaitForSingleObject, addr kTimer, Executive, KernelMode, FALSE, NULL

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

         inc dwCounter
         invoke DbgPrint, $CTA0("TimerWorks: Counter = %d\n"), dwCounter

         ; Если флаг g_fStop установлен, значит кто-то вызвал DriverUnload - пора прекращать работу.

         .if g_fStop
             invoke DbgPrint, $CTA0("TimerWorks: Stop counting to let the driver to be uloaded\n")
             .break
         .endif

     .endw

     invoke KeCancelTimer, addr kTimer

     invoke DbgPrint, $CTA0("TimerWorks: Timer is canceled. Leaving ThreadProc\n")
     invoke DbgPrint, $CTA0("\nTimerWorks: Our thread is about to terminate\n")

     invoke PsTerminateSystemThread, STATUS_SUCCESS

     ret

 ThreadProc endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       DriverUnload                                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DriverUnload proc pDriverObject:PDRIVER_OBJECT

     invoke DbgPrint, $CTA0("\nTimerWorks: Entering DriverUnload\n")

     mov g_fStop, TRUE   ; Break the timer loop if it's counting

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

     invoke DbgPrint, $CTA0("\nTimerWorks: Wait for thread exits...\n")
        
     invoke KeWaitForSingleObject, g_pkThread, Executive, KernelMode, FALSE, NULL
    
     ; Единственная причина, по которой ожидание может быть удовлетворено
     ; это завершение работы потока. Поэтому, нет смысла проверять возвращаемое значение.

     invoke ObDereferenceObject, g_pkThread

     invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

     mov eax, pDriverObject
     invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

     invoke DbgPrint, $CTA0("\nTimerWorks: Leaving DriverUnload\n")

     ret

 DriverUnload endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code INIT

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       StartThread                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 StartThread proc

 local status:NTSTATUS
 local hThread:HANDLE

     invoke DbgPrint, $CTA0("\nTimerWorks: Entering StartThread\n")

     invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadProc, NULL
     mov status, eax
     .if eax == STATUS_SUCCESS

         invoke ObReferenceObjectByHandle, hThread, THREAD_ALL_ACCESS, NULL, KernelMode, \
                                           addr g_pkThread, NULL

         invoke ZwClose, hThread
         invoke DbgPrint, $CTA0("TimerWorks: Thread created\n")
     .else
         invoke DbgPrint, $CTA0("TimerWorks: Can't create Thread. Status: %08X\n"), eax
     .endif

     invoke DbgPrint, $CTA0("\nTimerWorks: Leaving StartThread\n")

     mov eax, status
     ret

 StartThread endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       DriverEntry                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

 local status:NTSTATUS
 local pDeviceObject:PDEVICE_OBJECT

     mov status, STATUS_DEVICE_CONFIGURATION_ERROR

     invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, \
                   FILE_DEVICE_UNKNOWN, 0, TRUE, addr pDeviceObject
     .if eax == STATUS_SUCCESS
         invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
         .if eax == STATUS_SUCCESS
             invoke StartThread
             .if eax == STATUS_SUCCESS
                 and g_fStop, FALSE          ; Явно сбросим флаг, хотя он и так равен нулю.
                 mov eax, pDriverObject
                 mov (DRIVER_OBJECT PTR [eax]).DriverUnload, offset DriverUnload
                 mov status, STATUS_SUCCESS
             .else
                 invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
                 invoke IoDeleteDevice, pDeviceObject
             .endif
         .else
             invoke IoDeleteDevice, pDeviceObject
         .endif
     .endif

     mov eax, status
     ret

 DriverEntry endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                                                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 end DriverEntry

 :make

 set drv=TimerWorks

 \masm32\bin\ml /nologo /c /coff %drv%.bat
 \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj

 del %drv%.obj

 echo.
 pause



12.3 Создаем системный поток

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


 local hThread:HANDLE
 . . .
     invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadProc, NULL

Переменная hThread получит описатель созданного потока. Третий параметр - указатель на структуру OBJECT_ATTRIBUTES - он равен NULL, т.к. эта структура может быть полезна, в данном случае, только для помещения описателя потока в таблицу описателей ядра (см. предыдущую статью), для того, чтобы он был доступен в контексте любого процесса. Но нам этого не требуется, т.к. сразу после создания потока мы закроем его описатель. Почему? Об этом чуть позже. В случае если всё же необходимо поместить описатель потока в таблицу описателей ядра следует поступить таким образом:


 local oa:OBJECT_ATTRIBUTES
 local hThread:HANDLE
 . . .
     InitializeObjectAttributes addr oa, NULL, OBJ_KERNEL_HANDLE, NULL, NULL
     invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, addr oa, NULL, NULL, ThreadProc, NULL

Четвертый и пятый параметры функции PsCreateSystemThread - это описатель процесса и указатель на структуру CLIENT_ID - предназначены для создания потока в контексте определенного процесса, но мы их не используем за ненадобностью. Шестой параметр - указатель на процедуру, которую будет выполнять созданный поток. Т.е. это стартовый адрес потока. Эта процедура имеет такой прототип:


 ThreadProc proc Param:DWORD

Единственный параметр будет равен значению последнего параметра, переданного в функцию PsCreateSystemThread. Используя его, потоковой процедуре можно передать какие-то данные. Например, указатель на какую-нибудь структуру. В нашем случае никаких входных данных потоковой процедуре не требуется и последний параметр функции PsCreateSystemThread равен NULL.


     .if eax == STATUS_SUCCESS

         invoke ObReferenceObjectByHandle, hThread, THREAD_ALL_ACCESS, NULL, KernelMode, \
                                           addr g_pkThread, NULL
         invoke ZwClose, hThread
     .endif

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



12.4 Указатель на объект

Мы уже несколько раз вскользь касались темы указателей и очень много раз эти самые указатели использовали. Например, в процедуре DriverEntry мы получаем от системы указатель на объект "драйвер", а, создав объект "устройство", вызовом функции IoCreateDevice, получаем указатель на этот объект. В прошлой статье я уже упоминал функцию ObReferenceObjectByHandle. Сейчас нам без неё уже не обойтись, поэтому затронем этот вопрос подробнее.

Семейство ObReferenceObjectXxx функций возвращает указатель на объект, по какой-либо другой его характеристике, в том числе и по самому указателю. Например, недокументированная функция ObReferenceObjectByName возвращает указатель, используя имя объекта, а документированная ObReferenceObjectByHandle - используя его описатель. Имея указатель на объект, мы можем обращаться по нему в адресном пространстве любого процесса.

Каждый объект в недрах системы представлен структурой. Например, объекту "поток" соответствует недокументированная структура KTHREAD (см. w2kundoc.inc), а объект "таймер" описывает документированная структура KTIMER (см. ntddk.inc). Структура, описывающая объект, - это тело объекта. У каждого объекта есть еще и заголовок. У объектов всех типов заголовок описывается недокументированной структурой OBJECT_HEADER.


 OBJECT_HEADER STRUCT                        ; sizeof = 018h
     PointerCount            SDWORD      ?   ; 0000h
     union
         HandleCount         SDWORD      ?   ; 0004h
         SEntry              PVOID       ?   ; 0004h PTR SINGLE_LIST_ENTRY
     ends
     _Type                   PVOID       ?   ; 0008h PTR OBJECT_TYPE  (original name Type)
     NameInfoOffset          BYTE        ?   ; 000Ch
     HandleInfoOffset        BYTE        ?   ; 000Dh
     QuotaInfoOffset         BYTE        ?   ; 000Eh
     Flags                   BYTE        ?   ; 000Fh
     union
         ObjectCreateInfo    PVOID       ?   ; 0010h PTR OBJECT_CREATE_INFORMATION
         QuotaBlockCharged   PVOID       ?   ; 0010h
     ends
     SecurityDescriptor      PVOID       ?   ; 0014h
 ;   Body                    QUAD        <>  ; 0018h
 OBJECT_HEADER ENDS

В памяти заголовок располагается всегда сразу перед телом объекта. Имея указатель на объект, отнимите от этого значения 18h и получите адрес заголовка. Поле HandleCount хранит количество описателей объекта, а поле PointerCount - количество ссылок на объект (остальные поля структуры OBJECT_HEADER достаточно подробно описаны в книге Свена Шрайбера "Недокументированные возможности Windows 2000"). До тех пор, пока оба эти поля не равны нулю объект не будет удален, т.к. это означает, что кто-то еще пользуется объектом. Каждому описателю соответствует, по крайней мере, одна ссылка. Функции ObReferenceObjectXxx кроме возвращения указателя увеличивают значение поля PointerCount на единицу. Т.о. система помнит, что выдала кому-то еще один указатель. Если указатель больше не нужен, необходимо уменьшить значение счетчика ссылок, вызвав функцию ObDereferenceObject или ObfDereferenceObject.

С помощью команды SoftICE proc с ключом -o можно вывести список всех объектов используемых процессом - это фактически содержимое таблицы описателей процесса.

Командой proc получаем список процессов:


 :proc
 Process     KPEB      PID  Threads  Pri  User Time  Krnl Time  Status
 *System     818A89E0    8       22    8   00000000   00001214  Running
  smss       81359400   8C        6    B   00000001   0000003C  Idle
  csrss      8133F840   A4        A    D   0000005B   00001BF4  Ready
 . . .

Звездочка напротив процесса System означает, что в данный момент мы находимся в его адресном контексте (я выполнил команду proc, находясь в процедуре StartThread).

Используя ключ -o получаем список объектов процесса (в данном случае процесса System):


 :proc -o 818A89E0
 Process     KPEB      PID  Threads  Pri  User Time  Krnl Time  Status
 *System     818A89E0    8       22    8   00000000   00001214  Running

     ---- Handle Table Information ----

     Handle Table:    818CD508  Handle Array: E1002000  Entries:   75

     Handle  Ob Hdr *  Object *  Type
     0000    00000000  00000018  ?
     0004    818A89C8  818A89E0  Process
     . . .
     0140    811C3F70  811C3F88  File
     0148    E2D03288  E2D032A0  Key
     014C    810C5888  810C58A0  Thread

Этот снимок сделан сразу после вызова функции PsCreateSystemThread. Последний описатель (014C) в таблице описателей процесса System соответствует только что созданному потоку. В столбце Object * SoftICE любезно предоставляет нам адрес тела объекта, а в столбце Ob Hdr * адрес его заголовка (нам даже не нужно производить сложные математические операции :) ).

Посмотрим значения количества описателей и указателей на наш поток:


 :d 810C5888
 0010:810C5888 00000003  00000001  818A8E40  22000000      ........@......"
 0010:810C5898 00000001  E1000598  006C0006  00000000      ..........l.....

Как видите, количество описателей равно одному - это тот самый описатель, который мы получили в переменной hThread. Количество указателей равно трем: один из них соответствует описателю, два других получены системой для внутреннего использования.

После вызова функции ObReferenceObjectByHandle заголовок выглядит так:


 :d 810C5888
 0010:810C5888 00000004  00000001  818A8E40  22000000      ........@......"
 0010:810C5898 00000001  E1000598  006C0006  00000000      ..........l.....

А после закрытия описателя, вызовом ZwClose, так:


 :d 810C5888
 0010:810C5888 00000003  00000000  818A8E40  22000000      ........@......"
 0010:810C5898 00000001  E1000598  006C0006  00000000      ..........l.....



12.5 Процедура потока

Итак, поток создан. Рано или поздно планировщик предоставит ему процессор, и он начнет выполнять процедуру ThreadProc.


     and dwCounter, 0

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


     invoke KeGetCurrentIrql
     invoke DbgPrint, $CTA0("TimerWorks: IRQL = %d\n"), eax

     invoke KeGetCurrentThread
     mov pkThread, eax
     invoke KeQueryPriorityThread, pkThread
     push eax
     invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax

     pop eax
     inc eax
     inc eax
     invoke KeSetPriorityThread, pkThread, eax

     invoke KeQueryPriorityThread, pkThread
     invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax

Эти строки нужны исключительно для образовательных целей. Проанализировав эти сообщения, вы убедитесь в том что, во-первых, поток выполняется на IRQL = PASSIVE_LEVEL, во-вторых, его приоритет равен 8, что соответствует приоритету потока по умолчанию. Пользовательские потоки имеют такой же приоритет. Эксперимента ради, повысим приоритет на две единицы. Имейте только в виду, что потоки с приоритетом в диапазоне 16-31, работающие в режиме ядра, не вытесняются. Например, выполнив такой код, вы блокируете, на однопроцессорной машине, выполнение всех других потоков с приоритетом меньше 16, а таких потоков подавляющее большинство.


 invoke KeGetCurrentThread
 invoke KeSetPriorityThread, eax, LOW_REALTIME_PRIORITY

 @@:
 jmp @B

Некоторые системные потоки имеют более высокий приоритет. Например, приоритет одного из потоков системного процесса csrss (Client-Server Runtime Subsystem), выполняющего функцию RawInputThread (обрабатывает очередь ввода клавиатуры и мыши) в модуле win32k.sys, равен 19. Поэтому, ввод от клавиатуры и мыши еще будет работать. А выполнение такого кода уже намертво "вешает" однопроцессорную систему.


 invoke KeGetCurrentThread
 invoke KeSetPriorityThread, eax, HIGH_PRIORITY

 @@:
 jmp @B

Перейдем к содержательной части процедуры ThreadProc.


     invoke KeInitializeTimerEx, addr kTimer, SynchronizationTimer

В одном из предыдущих примеров мы уже использовали таймер (IoInitializeTimer, IoStartTimer, IoStopTimer). Но тот таймер обладал рядом ограничений. Во-первых, он жестко ассоциирован с объектом "устройство" и создать второй такой таймер невозможно. Во-вторых, он срабатывает раз в секунду, и изменить этот интервал нельзя. В-третьих, процедура таймера выполняется при IRQL = DISPATCH_LEVEL. Таймер, создаваемый функцией KeInitializeTimerEx полностью лишен этих недостатков.

Мы создаем синхронизирующий таймер (synchronization timer). В режиме пользователя ему соответствует таймер с автоматическим сбросом (auto-reset timer), создаваемый функцией CreateWaitableTimer. Отличительная особенность такого таймера в том, что если его ожидают несколько потоков, то когда таймер перейдет в свободное состояние, ожидание только одного потока будет удовлетворено и таймер тут же опять автоматически перейдет в занятое состояние. Это избавляет от необходимости повторно устанавливать таймер.

Функция KeInitializeTimer только лишь заполняет структуру KTIMER. Для запуска таймера используется функция KeSetTimerEx, прототип которой выглядит так:


 BOOLEAN
   KeSetTimerEx(
     IN PKTIMER        Timer,
     IN LARGE_INTEGER  DueTime,
     IN LONG           Period   OPTIONAL,
     IN PKDPC          Dpc      OPTIONAL
     );

Эта функция настолько гибка, что позволяет задать аж два временных интервала: DueTime - время (в 100-наносекундных интервалах), по истечении которого таймер сработает первый раз. После чего он будет срабатывать через временной интервал (в миллисекундах), заданный в параметре Period. Обратите внимание на тип параметра DueTime - это не указатель на структуру LARGE_INTEGER, а сама эта структура, т.е. на самом деле функция KeSetTimerEx принимает не четыре, а пять параметров. Для LARGE_INTEGER сначала передается младшая половина, а потом старшая. У параметра DueTime есть и ещё одна особенность - время, задаваемое им, может быть абсолютным или относительным.

Абсолютное время задается в 100-наносекундных интервалах от даты 1 января 1601 года. Это не шутка. Такая странная дата выбрана в связи с циклом високосных лет и позволяет упростить математические преобразования из одного временного формата в другой. В случае если задается абсолютный интервал, значение DueTime должно быть положительным. Например, чтобы назначить дату запуска таймера в полночь 1 января 2010 года, надо выполнить такой код:


 local liDueTime:LARGE_INTEGER
 local tf:TIME_FIELDS

 mov tf.Year,         2010
 mov tf.Month,        01
 mov tf.Day,          01
 mov tf.Hour,         00
 mov tf.Minute,       00
 mov tf.Second,       00
 mov tf.Milliseconds, 00
 mov tf.Weekday,      5   ; Пятница

 invoke RtlTimeFieldsToTime, addr tf, addr liDueTime
 invoke RtlLocalTimeToSystemTime,  addr liDueTime, addr liDueTime

Относительное время задается от момента вызова KeSetTimerEx и значение DueTime, в этом случае, должно быть отрицательным.

Имейте в виду, что задать интервал срабатывания таймера равным 100 наносекундам (и даже значительно больше) не удастся. Точнее, вы можете его задать, но это не приведет к срабатыванию именно через 100 наносекунд. Операционная система Windows не является системой реального времени. Внутренне, все взведенные таймеры связаны в двусвязный список KiTimerTableListHead, который периодически опрашивается системой. Если текущее системное время превышает время в поле KTIMER.DueTime (даже если вы задаете относительное время оно переводится в абсолютное), значит, таймер должен сработать. Система удаляет его из двусвязного списка, устанавливает поле KTIMER.Header.SignalState в TRUE и переводит поток (потоки) из состояния ожидания (waiting) в состояние готовности (ready). Момент, когда этот поток получит процессор и выйдет из функции ожидания, зависит от загрузки системы, приоритета патока, количества процессоров и т.п.


     or liDueTime.HighPart, -1
     mov liDueTime.LowPart, -50000000

     invoke KeSetTimerEx, addr kTimer, liDueTime.LowPart, liDueTime.HighPart, 1000, NULL

Устанавливаем относительный интервал времени, через который таймер начнет срабатывать, равным 5 секундам. А период последующего срабатывания зададим равным одной секунде. Десять раз крутим цикл, который имитирует выполнение потоком какой-то полезной работы.


     .while dwCounter < 10
         invoke KeWaitForSingleObject, addr kTimer, Executive, KernelMode, FALSE, NULL

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

Первый параметр функции KeWaitForSingleObject - это указатель на объект синхронизации. Если этот объект находится в свободном состоянии, функция немедленно возвращает управление. Если в занятом - поток переводится в состояние ожидания, до перехода объекта в свободное состояние. Структура, описывающая объект, должна располагаться в неподкачиваемой памяти. Наш объект "таймер" расположен в стеке, который, вообще говоря, может сбрасываться в файл подкачки - как стек режима пользователя, так и стек режима ядра. Наш поток работает только в режиме ядра, т.е. у него нет стека пользовательского режима. Память, отведенная под этот стек, также может быть подкачиваемой. Для того чтобы запретить сброс стека на диск, мы передаем в третьем параметре значение KernelMode. Насчет второго параметра, к сожалению, не могу сказать ничего умного, кроме того, что он должен быть равен Executive. Четвертый параметр определяет, должен ли поток ждать в тревожном состоянии (alertable wait state). Нам, слава богу, это не нужно и поэтому передаем в этом параметре FALSE. Последний параметр определяет длительность ожидания. Если он равен NULL, то ожидание будет длиться до перехода объекта в свободное состояние, сколько бы времени на это не потребовалось. Если нужен таймаут, то всё то, что я говорил о параметре DueTime функции KeSetTimerEx, применимо и к этому параметру, за тем исключением, что это указатель на структуру LARGE_INTEGER.


         inc dwCounter
         invoke DbgPrint, $CTA0("TimerWorks: Counter = %d\n"), dwCounter

Увеличиваем счетчик проделанных работ, чтобы показать работу потока.


         .if g_fStop
             .break
         .endif
     .endw

Для того чтобы остановить цикл до прохождения 10 итераций, проверяем флаг g_fStop. Если он установлен, значит, кто-то вызвал DriverUnload - пора прекращать работу.


     invoke KeCancelTimer, addr kTimer

Останавливаем таймер.


     invoke PsTerminateSystemThread, STATUS_SUCCESS

     ret

Прекращаем работу потока. Обратите внимание на то, что функция PsTerminateSystemThread принимает только код завершения потока. Указания, какой именно поток нужно завершить нет. Это значит, что PsTerminateSystemThread завершает тот поток, в контексте которого она вызвана. В DDK написано, что эта функция возвращает NTSTATUS, но это не так. Эта функция вообще не возвращает управления в поток, что, кстати, вполне естественно. Обратное было бы в высшей степени не логично, т.к. абсурдно продолжать выполнять только что завершенный поток. Так что инструкция ret использована просто для красоты.



12.6 Процедура DriverUnload

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

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

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


     mov g_fStop, TRUE

Если поток ещё занят своей работой, установка флага g_fStop просигнализирует ему о том, что пора отдыхать.


     invoke KeWaitForSingleObject, g_pkThread, Executive, KernelMode, FALSE, NULL

Теперь просто ждем, когда поток завершит работу. Именно для этого мы и получали указатель на поток в процедуре StartThread.

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


     invoke ObDereferenceObject, g_pkThread

     invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

     mov eax, pDriverObject
     invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

Как обычно убираем за собой. Вызов ObDereferenceObject уменьшает счетчик ссылок не объект и балансирует вызов ObReferenceObjectByHandle, который мы сделали в процедуре StartThread. Это позволяет системе вернуть себе ресурсы, отведенные на создание потока.

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

2002-2013 (c) wasm.ru