Драйверы режима ядра: Часть 12: Базовая техника. Синхронизация: Таймер и системный поток — Архив WASM.RU
Сразу оговорюсь. Синхронизация столь обширная тема, а количество объектов синхронизации столь велико, что в рамках одной-двух статей её можно осветить только очень поверхностно.
12.1 Объекты синхронизации
До сих пор нам не требовалось монополизировать доступ к каким-либо данным, т.к. мы имели всего один поток. Очевидно, что как только появляется два (и более) потока, обращающихся к одному и тому же ресурсу, их работу нужно как-то синхронизировать. Иначе ресурс рано или поздно окажется в непредсказуемом состоянии. Например, когда оба потока будут одновременно (на многопроцессорной системе в буквальном смысле слова) что-то записывать в разделяемую область памяти. Другой часто встречающейся задачей, которую решает синхронизация, является ожидание завершения какой-то операции.
Для решения подобных задач операционная система предоставляет весьма обширный набор объектов синхронизации (dispatcher objects): событие (event), мьютекс (mutex) - в ядре этот объект называется мутантом (mutant), семафор (semaphore) и др., а также средства управления этими объектами. Большинство объектов синхронизации используется как в ядре, так и в режиме пользователя. Точнее говоря, почти все объекты синхронизации режима пользователя являются оболочками соответствующих объектов режима ядра. В ядре, правда, набор механизмов синхронизации несколько богаче.
Все синхронизирующие объекты первым членом своих структур имеют структуру DISPATCHER_HEADER, через которую система управляет ожиданием. Вот как выглядят структуры объектов "таймер" (timer object) и "поток" (thread object) - эти объекты мы будем сегодня использовать.
Код (Text):
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
Код (Text):
;@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.
Код (Text):
local hThread:HANDLE . . . invoke PsCreateSystemThread, addr hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadProc, NULLПеременная hThread получит описатель созданного потока. Третий параметр - указатель на структуру OBJECT_ATTRIBUTES - он равен NULL, т.к. эта структура может быть полезна, в данном случае, только для помещения описателя потока в таблицу описателей ядра (см. предыдущую статью), для того, чтобы он был доступен в контексте любого процесса. Но нам этого не требуется, т.к. сразу после создания потока мы закроем его описатель. Почему? Об этом чуть позже. В случае если всё же необходимо поместить описатель потока в таблицу описателей ядра следует поступить таким образом:
Код (Text):
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 - предназначены для создания потока в контексте определенного процесса, но мы их не используем за ненадобностью. Шестой параметр - указатель на процедуру, которую будет выполнять созданный поток. Т.е. это стартовый адрес потока. Эта процедура имеет такой прототип:
Код (Text):
ThreadProc proc Param:DWORDЕдинственный параметр будет равен значению последнего параметра, переданного в функцию PsCreateSystemThread. Используя его, потоковой процедуре можно передать какие-то данные. Например, указатель на какую-нибудь структуру. В нашем случае никаких входных данных потоковой процедуре не требуется и последний параметр функции PsCreateSystemThread равен NULL.
Код (Text):
.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.
Код (Text):
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 получаем список процессов:
Код (Text):
:proc Process KPEB PID Threads Pri User Time Krnl Time Status *System <font color="#0000FF">818A89E0</font> 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):
Код (Text):
:proc -o <font color="#0000FF">818A89E0</font> 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 <font color="#0000FF">014C 810C5888 810C58A0 Thread</font>Этот снимок сделан сразу после вызова функции PsCreateSystemThread. Последний описатель (014C) в таблице описателей процесса System соответствует только что созданному потоку. В столбце Object * SoftICE любезно предоставляет нам адрес тела объекта, а в столбце Ob Hdr * адрес его заголовка (нам даже не нужно производить сложные математические операции ).
Посмотрим значения количества описателей и указателей на наш поток:
Код (Text):
:d 810C5888 0010:810C5888 <font color="#0000FF">00000003 00000001</font> 818A8E40 22000000 ........@......" 0010:810C5898 00000001 E1000598 006C0006 00000000 ..........l.....Как видите, количество описателей равно одному - это тот самый описатель, который мы получили в переменной hThread. Количество указателей равно трем: один из них соответствует описателю, два других получены системой для внутреннего использования.
После вызова функции ObReferenceObjectByHandle заголовок выглядит так:
Код (Text):
:d 810C5888 0010:810C5888 <font color="#0000FF">00000004 00000001</font> 818A8E40 22000000 ........@......" 0010:810C5898 00000001 E1000598 006C0006 00000000 ..........l.....А после закрытия описателя, вызовом ZwClose, так:
Код (Text):
:d 810C5888 0010:810C5888 <font color="#0000FF">00000003 00000000</font> 818A8E40 22000000 ........@......" 0010:810C5898 00000001 E1000598 006C0006 00000000 ..........l.....
12.5 Процедура потока
Итак, поток создан. Рано или поздно планировщик предоставит ему процессор, и он начнет выполнять процедуру ThreadProc.
Код (Text):
and dwCounter, 0Сбросим значение счетчика числа проходов. Я использую его для ограничения количества гипотетических работ, которые поток должен проделать. Можно убрать это ограничение и поток будет постоянно занят до выгрузки драйвера.
Код (Text):
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, а таких потоков подавляющее большинство.
Код (Text):
invoke KeGetCurrentThread invoke KeSetPriorityThread, eax, LOW_REALTIME_PRIORITY @@: jmp @BНекоторые системные потоки имеют более высокий приоритет. Например, приоритет одного из потоков системного процесса csrss (Client-Server Runtime Subsystem), выполняющего функцию RawInputThread (обрабатывает очередь ввода клавиатуры и мыши) в модуле win32k.sys, равен 19. Поэтому, ввод от клавиатуры и мыши еще будет работать. А выполнение такого кода уже намертво "вешает" однопроцессорную систему.
Код (Text):
invoke KeGetCurrentThread invoke KeSetPriorityThread, eax, HIGH_PRIORITY @@: jmp @BПерейдем к содержательной части процедуры ThreadProc.
Код (Text):
invoke KeInitializeTimerEx, addr kTimer, SynchronizationTimerВ одном из предыдущих примеров мы уже использовали таймер (IoInitializeTimer, IoStartTimer, IoStopTimer). Но тот таймер обладал рядом ограничений. Во-первых, он жестко ассоциирован с объектом "устройство" и создать второй такой таймер невозможно. Во-вторых, он срабатывает раз в секунду, и изменить этот интервал нельзя. В-третьих, процедура таймера выполняется при IRQL = DISPATCH_LEVEL. Таймер, создаваемый функцией KeInitializeTimerEx полностью лишен этих недостатков.
Мы создаем синхронизирующий таймер (synchronization timer). В режиме пользователя ему соответствует таймер с автоматическим сбросом (auto-reset timer), создаваемый функцией CreateWaitableTimer. Отличительная особенность такого таймера в том, что если его ожидают несколько потоков, то когда таймер перейдет в свободное состояние, ожидание только одного потока будет удовлетворено и таймер тут же опять автоматически перейдет в занятое состояние. Это избавляет от необходимости повторно устанавливать таймер.
Функция KeInitializeTimer только лишь заполняет структуру KTIMER. Для запуска таймера используется функция KeSetTimerEx, прототип которой выглядит так:
Код (Text):
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 года, надо выполнить такой код:
Код (Text):
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). Момент, когда этот поток получит процессор и выйдет из функции ожидания, зависит от загрузки системы, приоритета патока, количества процессоров и т.п.
Код (Text):
or liDueTime.HighPart, -1 mov liDueTime.LowPart, -50000000 invoke KeSetTimerEx, addr kTimer, liDueTime.LowPart, liDueTime.HighPart, 1000, NULLУстанавливаем относительный интервал времени, через который таймер начнет срабатывать, равным 5 секундам. А период последующего срабатывания зададим равным одной секунде. Десять раз крутим цикл, который имитирует выполнение потоком какой-то полезной работы.
Код (Text):
.while dwCounter < 10 invoke KeWaitForSingleObject, addr kTimer, Executive, KernelMode, FALSE, NULLЖдем, когда сработает таймер. Единственная причина, по которой, в данном случае, ожидание может быть удовлетворено - это срабатывание таймера. Поэтому возвращаемое значение можно и не проверять.
Первый параметр функции KeWaitForSingleObject - это указатель на объект синхронизации. Если этот объект находится в свободном состоянии, функция немедленно возвращает управление. Если в занятом - поток переводится в состояние ожидания, до перехода объекта в свободное состояние. Структура, описывающая объект, должна располагаться в неподкачиваемой памяти. Наш объект "таймер" расположен в стеке, который, вообще говоря, может сбрасываться в файл подкачки - как стек режима пользователя, так и стек режима ядра. Наш поток работает только в режиме ядра, т.е. у него нет стека пользовательского режима. Память, отведенная под этот стек, также может быть подкачиваемой. Для того чтобы запретить сброс стека на диск, мы передаем в третьем параметре значение KernelMode. Насчет второго параметра, к сожалению, не могу сказать ничего умного, кроме того, что он должен быть равен Executive. Четвертый параметр определяет, должен ли поток ждать в тревожном состоянии (alertable wait state). Нам, слава богу, это не нужно и поэтому передаем в этом параметре FALSE. Последний параметр определяет длительность ожидания. Если он равен NULL, то ожидание будет длиться до перехода объекта в свободное состояние, сколько бы времени на это не потребовалось. Если нужен таймаут, то всё то, что я говорил о параметре DueTime функции KeSetTimerEx, применимо и к этому параметру, за тем исключением, что это указатель на структуру LARGE_INTEGER.
Код (Text):
inc dwCounter invoke DbgPrint, $CTA0("TimerWorks: Counter = %d\n"), dwCounterУвеличиваем счетчик проделанных работ, чтобы показать работу потока.
Код (Text):
.if g_fStop .break .endif .endwДля того чтобы остановить цикл до прохождения 10 итераций, проверяем флаг g_fStop. Если он установлен, значит, кто-то вызвал DriverUnload - пора прекращать работу.
Код (Text):
invoke KeCancelTimer, addr kTimerОстанавливаем таймер.
Код (Text):
invoke PsTerminateSystemThread, STATUS_SUCCESS retПрекращаем работу потока. Обратите внимание на то, что функция PsTerminateSystemThread принимает только код завершения потока. Указания, какой именно поток нужно завершить нет. Это значит, что PsTerminateSystemThread завершает тот поток, в контексте которого она вызвана. В DDK написано, что эта функция возвращает NTSTATUS, но это не так. Эта функция вообще не возвращает управления в поток, что, кстати, вполне естественно. Обратное было бы в высшей степени не логично, т.к. абсурдно продолжать выполнять только что завершенный поток. Так что инструкция ret использована просто для красоты.
12.6 Процедура DriverUnload
Прежде чем мы позволим выгрузить драйвер надо убедиться, что поток завершил работу, ведь потоковая процедура находится в теле драйвера и его преждевременная выгрузка приведет к краху системы.
Для предотвращения выгрузки драйвера можно поступить просто - обнулить поле DRIVER_OBJECT.DriverUnload, восстановив его, когда можно будет выгрузить драйвер. Мы поступим по-другому - будем ждать, когда поток завершит работу. Это не очень хорошо, т.к. нам придется блокировать один из системных потоков, выполняющих процедуру DriverUnload, но зато позволит лишний раз попрактиковаться в синхронизации.
Поток, как и таймер, также является ожидаемым объектом. Пока поток выполняется, он находится в занятом состоянии и переходит в свободное после своего завершения. Значит, будем ждать.
Код (Text):
mov g_fStop, TRUEЕсли поток ещё занят своей работой, установка флага g_fStop просигнализирует ему о том, что пора отдыхать.
Код (Text):
invoke KeWaitForSingleObject, g_pkThread, Executive, KernelMode, FALSE, NULLТеперь просто ждем, когда поток завершит работу. Именно для этого мы и получали указатель на поток в процедуре StartThread.
Когда KeWaitForSingleObject вернет управление, поток уже прекратит свое существование и драйвер можно безопасно выгружать. Здесь также единственная причина, по которой, ожидание может быть удовлетворено - это завершение работы потока. Поэтому проверять код возврата не обязательно.
Код (Text):
invoke ObDereferenceObject, g_pkThread invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName mov eax, pDriverObject invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObjectКак обычно убираем за собой. Вызов ObDereferenceObject уменьшает счетчик ссылок не объект и балансирует вызов ObReferenceObjectByHandle, который мы сделали в процедуре StartThread. Это позволяет системе вернуть себе ресурсы, отведенные на создание потока.
Исходный код драйвера в архиве.
© Four-F
Драйверы режима ядра: Часть 12: Базовая техника. Синхронизация: Таймер и системный поток
Дата публикации 5 апр 2004