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

Дата публикации 5 апр 2004

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



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

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

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

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

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

Код (Text):
  1.  
  2.  
  3.  KTIMER STRUCT
  4.      Header            DISPATCHER_HEADER
  5.  . . .
  6.  KTIMER ENDS
  7.  
  8.  KTHREAD STRUCT
  9.      Header            DISPATCHER_HEADER
  10.  . . .
  11.  KTHREAD ENDS
  12.  
  13.  

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

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

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



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

Код (Text):
  1.  
  2.  
  3.  ;@echo off
  4.  ;goto make
  5.  
  6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  7.  ;
  8.  ;  TimerWorks - Создаем поток и таймер.
  9.  ;               Поток по таймеру делает свою работу, а мы подождем когда он её закончит
  10.  ;
  11.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  12.  
  13.  .386
  14.  .model flat, stdcall
  15.  option casemap:none
  16.  
  17.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  18.  ;                               В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы
  19.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  20.  
  21.  include \masm32\include\w2k\ntstatus.inc
  22.  include \masm32\include\w2k\ntddk.inc
  23.  include \masm32\include\w2k\ntoskrnl.inc
  24.  include \masm32\include\w2k\hal.inc
  25.  
  26.  includelib \masm32\lib\w2k\ntoskrnl.lib
  27.  includelib \masm32\lib\w2k\hal.lib
  28.  
  29.  include \masm32\Macros\Strings.mac
  30.  
  31.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  32.  ;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е                                
  33.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  34.  
  35.  .const
  36.  
  37.  CCOUNTED_UNICODE_STRING "\\Device\\TimerWorks", g_usDeviceName, 4
  38.  CCOUNTED_UNICODE_STRING "\\DosDevices\\TimerWorks", g_usSymbolicLinkName, 4
  39.  
  40.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  41.  ;                     Н Е И Н И Ц И А Л И З И Р О В А Н Н Ы Е    Д А Н Н Ы Е                                
  42.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  43.  
  44.  .data?
  45.  
  46.  g_pkThread   PVOID  ?   ; PTR KTHREAD
  47.  g_fStop     BOOL    ?
  48.  
  49.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  50.  ;                                           К О Д                                                  
  51.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  52.  
  53.  .code
  54.  
  55.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  56.  ;                                        ThreadProc                                                
  57.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  58.  
  59.  ThreadProc proc Param:DWORD
  60.  
  61.  Local dwCounter:DWORD
  62.  local pkThread:PVOID         ; PKTHREAD
  63.  local status:NTSTATUS
  64.  local kTimer:KTIMER
  65.  local liDueTime:LARGE_INTEGER
  66.  
  67.      and dwCounter, 0
  68.  
  69.      invoke DbgPrint, $CTA0("\nTimerWorks: Entering ThreadProc\n")
  70.  
  71.      ;:::::::::::::::::::::::::::::::::::::::::::::::::::::
  72.      ; Для образовательных целей посмотрим, какой у нас IRQL
  73.      ; и поиграемся с приоритетом потока
  74.  
  75.      invoke KeGetCurrentIrql
  76.      invoke DbgPrint, $CTA0("TimerWorks: IRQL = %d\n"), eax
  77.  
  78.  
  79.      invoke KeGetCurrentThread
  80.      mov pkThread, eax
  81.      invoke KeQueryPriorityThread, eax
  82.      push eax
  83.      invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax
  84.  
  85.      pop eax
  86.      inc eax
  87.      inc eax
  88.      invoke KeSetPriorityThread, pkThread, eax
  89.  
  90.      invoke KeQueryPriorityThread, pkThread
  91.      invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax
  92.  
  93.      ;:::::::::::::::::::::::::::::::::::::::::::::::::::::
  94.  
  95.      invoke KeInitializeTimerEx, addr kTimer, SynchronizationTimer
  96.  
  97.      ; Установим относительный (т.е. от настоящего момента) интервал времени,
  98.      ; через который таймер начнет срабатывать, равным 5 секундам. А период
  99.      ; последующего срабатывания зададим равным одной секунде.
  100.  
  101.      or liDueTime.HighPart, -1
  102.      mov liDueTime.LowPart, -50000000
  103.  
  104.      invoke KeSetTimerEx, addr kTimer, liDueTime.LowPart, liDueTime.HighPart, 1000, NULL
  105.  
  106.      invoke DbgPrint, $CTA0("TimerWorks: Timer is set. It starts counting in 5 seconds\n")
  107.  
  108.      .while dwCounter < 10
  109.          invoke KeWaitForSingleObject, addr kTimer, Executive, KernelMode, FALSE, NULL
  110.  
  111.          ; Единственная причина, по которой, в данном случае, ожидание может быть удовлетворено
  112.          ; - это срабатывание таймера. Поэтому проверять возвращаемое значение не имеет смысла.
  113.  
  114.          inc dwCounter
  115.          invoke DbgPrint, $CTA0("TimerWorks: Counter = %d\n"), dwCounter
  116.  
  117.          ; Если флаг g_fStop установлен, значит кто-то вызвал DriverUnload - пора прекращать работу.
  118.  
  119.          .if g_fStop
  120.              invoke DbgPrint, $CTA0("TimerWorks: Stop counting to let the driver to be uloaded\n")
  121.              .break
  122.          .endif
  123.  
  124.      .endw
  125.  
  126.      invoke KeCancelTimer, addr kTimer
  127.  
  128.      invoke DbgPrint, $CTA0("TimerWorks: Timer is canceled. Leaving ThreadProc\n")
  129.      invoke DbgPrint, $CTA0("\nTimerWorks: Our thread is about to terminate\n")
  130.  
  131.      invoke PsTerminateSystemThread, STATUS_SUCCESS
  132.  
  133.      ret
  134.  
  135.  ThreadProc endp
  136.  
  137.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  138.  ;                                       DriverUnload                                                
  139.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  140.  
  141.  DriverUnload proc pDriverObject:PDRIVER_OBJECT
  142.  
  143.      invoke DbgPrint, $CTA0("\nTimerWorks: Entering DriverUnload\n")
  144.  
  145.      mov g_fStop, TRUE   ; Break the timer loop if it's counting
  146.  
  147.      ; Мы не можем позволить выгрузить драйвер до тех пор, пока наш поток работает,
  148.      ; т.к. он выполняет процедуру находящуюся в теле драйвера. Поэтому, мы будем ждать
  149.      ; ценой приостаноки системного потока выполняющего процедуру DriverUnload.
  150.  
  151.      invoke DbgPrint, $CTA0("\nTimerWorks: Wait for thread exits...\n")
  152.        
  153.      invoke KeWaitForSingleObject, g_pkThread, Executive, KernelMode, FALSE, NULL
  154.    
  155.      ; Единственная причина, по которой ожидание может быть удовлетворено
  156.      ; это завершение работы потока. Поэтому, нет смысла проверять возвращаемое значение.
  157.  
  158.      invoke ObDereferenceObject, g_pkThread
  159.  
  160.      invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
  161.  
  162.      mov eax, pDriverObject
  163.      invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
  164.  
  165.      invoke DbgPrint, $CTA0("\nTimerWorks: Leaving DriverUnload\n")
  166.  
  167.      ret
  168.  
  169.  DriverUnload endp
  170.  
  171.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  172.  ;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д                  
  173.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  174.  
  175.  .code INIT
  176.  
  177.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  178.  ;                                       StartThread                                                
  179.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  180.  
  181.  StartThread proc
  182.  
  183.  local status:NTSTATUS
  184.  local hThread:HANDLE
  185.  
  186.      invoke DbgPrint, $CTA0("\nTimerWorks: Entering StartThread\n")
  187.  
  188.      invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadProc, NULL
  189.      mov status, eax
  190.      .if eax == STATUS_SUCCESS
  191.  
  192.          invoke ObReferenceObjectByHandle, hThread, THREAD_ALL_ACCESS, NULL, KernelMode, \
  193.                                            addr g_pkThread, NULL
  194.  
  195.          invoke ZwClose, hThread
  196.          invoke DbgPrint, $CTA0("TimerWorks: Thread created\n")
  197.      .else
  198.          invoke DbgPrint, $CTA0("TimerWorks: Can't create Thread. Status: %08X\n"), eax
  199.      .endif
  200.  
  201.      invoke DbgPrint, $CTA0("\nTimerWorks: Leaving StartThread\n")
  202.  
  203.      mov eax, status
  204.      ret
  205.  
  206.  StartThread endp
  207.  
  208.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  209.  ;                                       DriverEntry                                                
  210.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  211.  
  212.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
  213.  
  214.  local status:NTSTATUS
  215.  local pDeviceObject:PDEVICE_OBJECT
  216.  
  217.      mov status, STATUS_DEVICE_CONFIGURATION_ERROR
  218.  
  219.      invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, \
  220.                    FILE_DEVICE_UNKNOWN, 0, TRUE, addr pDeviceObject
  221.      .if eax == STATUS_SUCCESS
  222.          invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
  223.          .if eax == STATUS_SUCCESS
  224.              invoke StartThread
  225.              .if eax == STATUS_SUCCESS
  226.                  and g_fStop, FALSE          ; Явно сбросим флаг, хотя он и так равен нулю.
  227.                  mov eax, pDriverObject
  228.                  mov (DRIVER_OBJECT PTR [eax]).DriverUnload, offset DriverUnload
  229.                  mov status, STATUS_SUCCESS
  230.              .else
  231.                  invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
  232.                  invoke IoDeleteDevice, pDeviceObject
  233.              .endif
  234.          .else
  235.              invoke IoDeleteDevice, pDeviceObject
  236.          .endif
  237.      .endif
  238.  
  239.      mov eax, status
  240.      ret
  241.  
  242.  DriverEntry endp
  243.  
  244.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  245.  ;                                                                                                  
  246.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  247.  
  248.  end DriverEntry
  249.  
  250.  :make
  251.  
  252.  set drv=TimerWorks
  253.  
  254.  \masm32\bin\ml /nologo /c /coff %drv%.bat
  255.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj
  256.  
  257.  del %drv%.obj
  258.  
  259.  echo.
  260.  pause
  261.  
  262.  


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

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

Код (Text):
  1.  
  2.  
  3.  local hThread:HANDLE
  4.  . . .
  5.      invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadProc, NULL
  6.  
  7.  

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

Код (Text):
  1.  
  2.  
  3.  local oa:OBJECT_ATTRIBUTES
  4.  local hThread:HANDLE
  5.  . . .
  6.      InitializeObjectAttributes addr oa, NULL, OBJ_KERNEL_HANDLE, NULL, NULL
  7.      invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, addr oa, NULL, NULL, ThreadProc, NULL
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.  ThreadProc proc Param:DWORD
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.      .if eax == STATUS_SUCCESS
  4.  
  5.          invoke ObReferenceObjectByHandle, hThread, THREAD_ALL_ACCESS, NULL, KernelMode, \
  6.                                            addr g_pkThread, NULL
  7.          invoke ZwClose, hThread
  8.      .endif
  9.  
  10.  

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



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

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

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

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

Код (Text):
  1.  
  2.  
  3.  OBJECT_HEADER STRUCT                        ; sizeof = 018h
  4.      PointerCount            SDWORD      ?   ; 0000h
  5.      union
  6.          HandleCount         SDWORD      ?   ; 0004h
  7.          SEntry              PVOID       ?   ; 0004h PTR SINGLE_LIST_ENTRY
  8.      ends
  9.      _Type                   PVOID       ?   ; 0008h PTR OBJECT_TYPE  (original name Type)
  10.      NameInfoOffset          BYTE        ?   ; 000Ch
  11.      HandleInfoOffset        BYTE        ?   ; 000Dh
  12.      QuotaInfoOffset         BYTE        ?   ; 000Eh
  13.      Flags                   BYTE        ?   ; 000Fh
  14.      union
  15.          ObjectCreateInfo    PVOID       ?   ; 0010h PTR OBJECT_CREATE_INFORMATION
  16.          QuotaBlockCharged   PVOID       ?   ; 0010h
  17.      ends
  18.      SecurityDescriptor      PVOID       ?   ; 0014h
  19.  ;   Body                    QUAD          ; 0018h
  20.  OBJECT_HEADER ENDS
  21.  
  22.  

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

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

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

Код (Text):
  1.  
  2.  
  3.  :proc
  4.  Process     KPEB      PID  Threads  Pri  User Time  Krnl Time  Status
  5.  *System     <font color="#0000FF">818A89E0</font>    8       22    8   00000000   00001214  Running
  6.   smss       81359400   8C        6    B   00000001   0000003C  Idle
  7.   csrss      8133F840   A4        A    D   0000005B   00001BF4  Ready
  8.  . . .
  9.  
  10.  

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

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

Код (Text):
  1.  
  2.  
  3.  :proc -o <font color="#0000FF">818A89E0</font>
  4.  Process     KPEB      PID  Threads  Pri  User Time  Krnl Time  Status
  5.  *System     818A89E0    8       22    8   00000000   00001214  Running
  6.  
  7.      ---- Handle Table Information ----
  8.  
  9.      Handle Table:    818CD508  Handle Array: E1002000  Entries:   75
  10.  
  11.      Handle  Ob Hdr *  Object *  Type
  12.      0000    00000000  00000018  ?
  13.      0004    818A89C8  818A89E0  Process
  14.      . . .
  15.      0140    811C3F70  811C3F88  File
  16.      0148    E2D03288  E2D032A0  Key
  17.      <font color="#0000FF">014C    810C5888  810C58A0  Thread</font>
  18.  
  19.  

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

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

Код (Text):
  1.  
  2.  
  3.  :d 810C5888
  4.  0010:810C5888 <font color="#0000FF">00000003  00000001</font>  818A8E40  22000000      ........@......"
  5.  0010:810C5898 00000001  E1000598  006C0006  00000000      ..........l.....
  6.  
  7.  

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

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

Код (Text):
  1.  
  2.  
  3.  :d 810C5888
  4.  0010:810C5888 <font color="#0000FF">00000004  00000001</font>  818A8E40  22000000      ........@......"
  5.  0010:810C5898 00000001  E1000598  006C0006  00000000      ..........l.....
  6.  
  7.  

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

Код (Text):
  1.  
  2.  
  3.  :d 810C5888
  4.  0010:810C5888 <font color="#0000FF">00000003  00000000</font>  818A8E40  22000000      ........@......"
  5.  0010:810C5898 00000001  E1000598  006C0006  00000000      ..........l.....
  6.  
  7.  


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

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

Код (Text):
  1.  
  2.  
  3.      and dwCounter, 0
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.      invoke KeGetCurrentIrql
  4.      invoke DbgPrint, $CTA0("TimerWorks: IRQL = %d\n"), eax
  5.  
  6.      invoke KeGetCurrentThread
  7.      mov pkThread, eax
  8.      invoke KeQueryPriorityThread, pkThread
  9.      push eax
  10.      invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax
  11.  
  12.      pop eax
  13.      inc eax
  14.      inc eax
  15.      invoke KeSetPriorityThread, pkThread, eax
  16.  
  17.      invoke KeQueryPriorityThread, pkThread
  18.      invoke DbgPrint, $CTA0("TimerWorks: Thread Priority = %d\n"), eax
  19.  
  20.  

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

Код (Text):
  1.  
  2.  
  3.  invoke KeGetCurrentThread
  4.  invoke KeSetPriorityThread, eax, LOW_REALTIME_PRIORITY
  5.  
  6.  @@:
  7.  jmp @B
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.  invoke KeGetCurrentThread
  4.  invoke KeSetPriorityThread, eax, HIGH_PRIORITY
  5.  
  6.  @@:
  7.  jmp @B
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.      invoke KeInitializeTimerEx, addr kTimer, SynchronizationTimer
  4.  
  5.  

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

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

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

Код (Text):
  1.  
  2.  
  3.  BOOLEAN
  4.    KeSetTimerEx(
  5.      IN PKTIMER        Timer,
  6.      IN LARGE_INTEGER  DueTime,
  7.      IN LONG           Period   OPTIONAL,
  8.      IN PKDPC          Dpc      OPTIONAL
  9.      );
  10.  
  11.  

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

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

Код (Text):
  1.  
  2.  
  3.  local liDueTime:LARGE_INTEGER
  4.  local tf:TIME_FIELDS
  5.  
  6.  mov tf.Year,         2010
  7.  mov tf.Month,        01
  8.  mov tf.Day,          01
  9.  mov tf.Hour,         00
  10.  mov tf.Minute,       00
  11.  mov tf.Second,       00
  12.  mov tf.Milliseconds, 00
  13.  mov tf.Weekday,      5   ; Пятница
  14.  
  15.  invoke RtlTimeFieldsToTime, addr tf, addr liDueTime
  16.  invoke RtlLocalTimeToSystemTime,  addr liDueTime, addr liDueTime
  17.  
  18.  

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

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

Код (Text):
  1.  
  2.  
  3.      or liDueTime.HighPart, -1
  4.      mov liDueTime.LowPart, -50000000
  5.  
  6.      invoke KeSetTimerEx, addr kTimer, liDueTime.LowPart, liDueTime.HighPart, 1000, NULL
  7.  
  8.  

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

Код (Text):
  1.  
  2.  
  3.      .while dwCounter < 10
  4.          invoke KeWaitForSingleObject, addr kTimer, Executive, KernelMode, FALSE, NULL
  5.  
  6.  

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

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

Код (Text):
  1.  
  2.  
  3.          inc dwCounter
  4.          invoke DbgPrint, $CTA0("TimerWorks: Counter = %d\n"), dwCounter
  5.  
  6.  

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

Код (Text):
  1.  
  2.  
  3.          .if g_fStop
  4.              .break
  5.          .endif
  6.      .endw
  7.  
  8.  

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

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

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

Код (Text):
  1.  
  2.  
  3.      invoke PsTerminateSystemThread, STATUS_SUCCESS
  4.  
  5.      ret
  6.  
  7.  

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



12.6 Процедура DriverUnload

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

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

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

Код (Text):
  1.  
  2.  
  3.      mov g_fStop, TRUE
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.      invoke KeWaitForSingleObject, g_pkThread, Executive, KernelMode, FALSE, NULL
  4.  
  5.  

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

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

Код (Text):
  1.  
  2.  
  3.      invoke ObDereferenceObject, g_pkThread
  4.  
  5.      invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
  6.  
  7.      mov eax, pDriverObject
  8.      invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
  9.  
  10.  

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

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

© Four-F

0 1.678
archive

archive
New Member

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