Kernel Mode Drivers

Part 12: Basic Technique: Directories and Files



I'll make a reservation right away. Synchronization is such an extensive topic, and the number of synchronization objects is so great that within the framework of one or two articles it can only be covered very superficially.



12.1 Synchronization objects

Until now, we did not need to monopolize access to any data, since we only had one stream. Obviously, as soon as there are two (or more) threads accessing the same resource, their work needs to be synchronized somehow. Otherwise, the resource will sooner or later find itself in an unpredictable state. For example, when both threads will simultaneously (on a multiprocessor system in the literal sense of the word) write something to a shared memory area. Another common problem that synchronization solves is waiting for an operation to complete.

To solve such problems, the operating system provides a very extensive set of synchronization objects (dispatcher objects): an event (event), a mutex (mutex) - in the kernel, this object is called a mutant (mutant), a semaphore (semaphore), etc., as well as tools for managing these objects. Most synchronization objects are used in both kernel and user mode. More specifically, almost all user-mode synchronization objects are wrappers for the corresponding kernel-mode objects. In the kernel, however, the set of synchronization mechanisms is somewhat richer.

All synchronizing objects have the DISPATCHER_HEADER structure as the first member of their structures, through which the system manages the wait. This is how the structure of the object "Timer" (timer object) and "flow" (thread object) - these objects we will be using today.

 KTIMER STRUCT
     Header            DISPATCHER_HEADER <>
 . . .
 KTIMER ENDS

 KTHREAD STRUCT
     Header            DISPATCHER_HEADER <>
 . . .
 KTHREAD ENDS

The logic of the work of each object differs from the logic of the work of its counterparts, which is quite natural. Which object to use in a particular case depends on its nature. I will not dwell on this in detail, as I assume that you have already worked enough with these objects in user mode. Let me just remind you that each synchronization object can be in one of two states: free (signaled) or busy (nonsignaled). The words free and busy are terribly bad at reflecting the essence of some objects, but these are well-established terms.

There is no fundamental difference in managing synchronization objects in user mode and kernel mode, but there are several peculiarities. First and foremost: you can expect synchronization on an object only if the IRQL is strictly less than DISPATCH_LEVEL! This is because the thread scheduler itself runs on IRQL = DISPATCH_LEVEL. Therefore, if you force a thread to wait on a busy object with IRQL> = DISPATCH_LEVEL, then the scheduler will not be able to provide the processor to the thread using the occupied object, which means that this thread will never be able to free it, and the wait will continue indefinitely. The second feature is that in kernel mode wait functions take a pointer to an object, not an object handle as in user mode.

Two functions are used for waiting: KeWaitForSingleObject - expects one object and KeWaitForMultipleObjects - expects multiple objects. These functions wait for the object to go free.



12.2 TimerWorks Driver Source Code

 ;@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 Create a system thread

So far, we have only had one thread. Either it is a system thread that executes DriverEntry and DriverUnload routine code , or a custom driver control program thread that executes dispatch routine code DispatchXxx . Usually, drivers do not need to create additional threads. However, if necessary, this can be done by calling the PsCreateSystemThread function .

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

The hThread variable will receive a handle to the created thread. The third parameter is a pointer to the OBJECT_ATTRIBUTES structure - it is NULL, since this structure can be useful, in this case, only for placing the thread descriptor in the kernel descriptor table (see the previous article), so that it is available in the context of any process. But we do not need this, because immediately after creating a stream, we will close its handle. Why? More on this later. If it is nevertheless necessary to place a thread descriptor in the kernel descriptor table, proceed as follows:

 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

The fourth and fifth parameters of the PsCreateSystemThread function are a process handle and a pointer to the CLIENT_ID structure; they are designed to create a thread in the context of a specific process, but we do not use them as unnecessary. The sixth parameter is a pointer to the procedure that the created thread will execute. Those. this is the start address of the stream. This routine has the following prototype:

 ThreadProc proc Param:DWORD

The only parameter will be equal to the value of the last parameter passed to the PsCreateSystemThread function . Using it, you can pass some data to the streaming procedure. For example, a pointer to some structure. In our case, the threading procedure does not require any input data, and the last parameter of the PsCreateSystemThread function is NULL.

     .if eax == STATUS_SUCCESS

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

We are going to wait for the thread to finish, and for this we need a pointer to the "thread" object, since in kernel mode wait functions operate on pointers rather than handles as in user mode. Therefore, by calling ObReferenceObjectByHandle , using the hThread descriptor we have at our disposal, we get a pointer to the "stream" object, and then close the descriptor, because we don't need it anymore. Calling ObReferenceObjectByHandle need, of course, before the closing handle.



12.4 Pointer to object

We have already casually touched upon the topic of pointers several times and used these same pointers many times. For example, in the DriverEntry procedure , we get a pointer to the "driver" object from the system, and by creating a "device" object, by calling the IoCreateDevice function , we get a pointer to this object. In the last article, I already mentioned the ObReferenceObjectByHandle function . Now we cannot do without it, so we will touch on this issue in more detail.

The ObReferenceObjectXxx family of functions returns a pointer to an object, according to some other of its characteristics, including the pointer itself. For example, the undocumented ObReferenceObjectByName function returns a pointer using the object name, and the documented ObReferenceObjectByHandle using its handle. Having a pointer to an object, we can refer to it in the address space of any process.

Each object in the depths of the system is represented by a structure. For example, the stream object corresponds to the undocumented KTHREAD structure (see w2kundoc.inc), and the timer object describes the documented KTIMER structure (see ntddk.inc). The structure that describes the object is the body of the object. Each object also has a title. For objects of all types, the header is described by the undocumented OBJECT_HEADER structure.

 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

In memory, the header is always located immediately before the body of the object. Given a pointer to an object, subtract 18h from this value to get the header address. The HandleCount field stores the number of object handles, and the PointerCount field stores the number of references to the object (the remaining fields of the OBJECT_HEADER structure are described in some detail in the book "Undocumented Windows 2000 Features" by Sven Schreiber). As long as both of these fields are not equal to zero, the object will not be deleted, because this means that someone else is using the object. Each descriptor has at least one link associated with it. ObReferenceObjectXxx Functionsin addition to returning a pointer, the value of the PointerCount field is increased by one. That. the system remembers that it has given someone another pointer. If the pointer is no longer needed, you must decrease the reference count by calling the ObDereferenceObject or ObfDereferenceObject function .

Using the SoftICE proc command with the -o switch, you can display a list of all objects used by a process - this is actually the contents of the process descriptor table.

With the proc command, we get a list of processes:

 :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
 . . .

The asterisk opposite the System process means that we are currently in its address context (I executed the proc command while in the StartThread procedure ).

Using the -o switch, we get a list of process objects (in this case, the System process):

 :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

This snapshot was taken immediately after the call to the PsCreateSystemThread function . The last descriptor (014C) in the System descriptor table corresponds to the newly created thread. In the Object * column, SoftICE kindly provides us with the address of the object's body, and in the Ob Hdr * column, the address of its header (we don't even need to perform complex mathematical operations :)).

Let's see the values ​​of the number of descriptors and pointers to our stream:

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

As you can see, the number of descriptors is equal to one - this is the same descriptor that we got in the hThread variable. The number of pointers is three: one of them corresponds to the descriptor, the other two are obtained by the system for internal use.

After calling the ObReferenceObjectByHandle function, the header looks like this:

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

And after closing the handle, by calling ZwClose , like this:

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



12.5 Flow procedure

So the stream has been created. Sooner or later, the scheduler will provide it with a processor and it will start executing the ThreadProc procedure .

     and dwCounter, 0

Let's reset the counter of the number of passes. I use it to limit the amount of hypothetical work a thread needs to do. You can remove this limitation and the thread will be constantly busy until the driver is unloaded.

     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

These lines are for educational purposes only. After analyzing these messages, you will make sure that, firstly, the thread is executed at IRQL = PASSIVE_LEVEL, and secondly, its priority is 8, which corresponds to the default thread priority. User threads have the same priority. For experiment's sake, let's increase the priority by two. Just keep in mind that threads with a priority in the 16-31 range running in kernel mode are not preempted. For example, by executing such code, you block, on a uniprocessor machine, the execution of all other threads with a priority less than 16, and such threads are the overwhelming majority.

 invoke KeGetCurrentThread
 invoke KeSetPriorityThread, eax, LOW_REALTIME_PRIORITY

 @@:
 jmp @B

Some system threads have higher priority. For example, the priority of one of the threads of the csrss system process (Client-Server Runtime Subsystem) that executes the RawInputThread function (processes the keyboard and mouse input queue) in the win32k.sys module is 19. Therefore, keyboard and mouse input will still work. And the execution of such code is already tightly "hangs" the uniprocessor system.

 invoke KeGetCurrentThread
 invoke KeSetPriorityThread, eax, HIGH_PRIORITY

 @@:
 jmp @B

Let's move on to the substantive part of the ThreadProc procedure .

     invoke KeInitializeTimerEx, addr kTimer, SynchronizationTimer

In one of the previous examples, we already used a timer ( IoInitializeTimer , IoStartTimer , IoStopTimer ). But that timer had a number of limitations. First, it is strictly associated with the device object and it is impossible to create a second such timer. Secondly, it fires once a second, and this interval cannot be changed. Third, the timer routine is executed with IRQL = DISPATCH_LEVEL. The timer created by the KeInitializeTimerEx function is completely devoid of these drawbacks.

We create a synchronization timer . In user mode, this corresponds to an auto-reset timer created by the CreateWaitableTimer function . A distinctive feature of such a timer is that if several threads are waiting for it, then when the timer goes into an idle state, the wait for only one thread will be satisfied and the timer will immediately again automatically go into a busy state. This eliminates the need to re-set the timer.

The KeInitializeTimer function just fills in the KTIMER structure. To start the timer, the KeSetTimerEx function is used , the prototype of which looks like this:

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

This function is so flexible that it allows you to set as many as two time intervals: DueTime - the time (in 100-nanosecond intervals) after which the timer will work for the first time. After that, it will be triggered after a time interval (in milliseconds) specified in the Period parameter. Pay attention to the type of the DueTime parameter - it is not a pointer to the LARGE_INTEGER structure, but this structure itself, i.e. in fact, the KeSetTimerEx function takes not four, but five parameters. For LARGE_INTEGER, the low half is passed first, and then the high half. The DueTime parameter has one more peculiarity - the time specified by it can be absolute or relative.

The absolute time is specified in 100-nanosecond intervals from January 1, 1601. It is not joke. This strange date was chosen due to the cycle of leap years and allows for easier mathematical conversions from one time format to another. If an absolute interval is specified, the DueTime value must be positive. For example, to set the start date for the timer at midnight on January 1, 2010, you would run the following code:

 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

The relative time is set from the moment KeSetTimerEx is called and the DueTime value, in this case, must be negative.

Keep in mind that you won't be able to set the timer interval to 100 nanoseconds (or even much longer). More precisely, you can set it, but this will not trigger exactly after 100 nanoseconds. The Windows operating system is not a real-time system. Internally, all cocked timers are linked into a doubly linked list KiTimerTableListHead, which is periodically polled by the system. If the current system time exceeds the time in the KTIMER.DueTime field (even if you specify a relative time, it is converted to absolute), then the timer should work. The system removes it from the doubly linked list, sets the KTIMER.Header.SignalState field to TRUE, and switches the stream (s) from the waiting state to the ready state. The moment when this thread receives the processor and exits the wait function,

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

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

We set the relative time interval after which the timer will start working, equal to 5 seconds. And the period of the subsequent actuation is set equal to one second. We turn the loop ten times, which simulates the execution of some useful work by the thread.

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

We are waiting for the timer to work. The only reason the wait can be satisfied in this case is for the timer to fire. Therefore, the return value need not be checked.

The first parameter of the KeWaitForSingleObject functionis a pointer to a synchronization object. If this object is in a free state, the function returns immediately. If busy, the thread is put into the waiting state, until the object transitions to the free state. The structure describing the object must be located in non-swapped memory. Our timer object resides on a stack, which, generally speaking, can be paged into the paging file — both the user-mode stack and the kernel-mode stack. Our thread only runs in kernel mode, i.e. it doesn't have a user mode stack. The memory allocated for this stack can also be swapped. In order to prohibit flushing the stack to disk, we pass the KernelMode value in the third parameter. Unfortunately, I can't say anything smart about the second parameter, except that it should be equal to Executive.alarm state (alertable wait state). We, thank God, do not need this and therefore pass FALSE in this parameter. The last parameter defines the waiting time. If it is NULL, then the wait will continue until the object transitions to a free state, no matter how long it takes. If you need a timeout, then all the things that I talked about setting functions DueTime KeSetTimerEx , applies to this parameter, except that it is a pointer to a structure LARGE_INTEGER.

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

We increase the counter of the work done to show the work of the flow.

         .if g_fStop
             .break
         .endif
     .endw

To stop the loop before passing 10 iterations, check the g_fStop flag. If it is installed, then someone called DriverUnload - it's time to stop working.

     invoke KeCancelTimer, addr kTimer

We stop the timer.

     invoke PsTerminateSystemThread, STATUS_SUCCESS

     ret

We terminate the work of the thread. Note that the PsTerminateSystemThread function only accepts the thread termination code. There is no indication of which thread to terminate. This means that PsTerminateSystemThread terminates the thread in the context of which it is called. The DDK says that this function returns NTSTATUS, but it doesn't. This function does not return control to the thread at all, which, by the way, is quite natural. The opposite would be highly illogical, since it is absurd to continue executing the thread you just finished. So the ret instruction is just for show.



12.6 DriverUnload Procedure

Before we allow unloading the driver, we need to make sure that the thread has exited, because the thread procedure is in the body of the driver and unloading it prematurely will lead to a system crash.

To prevent unloading the driver, you can do it simply - reset the DRIVER_OBJECT.DriverUnload field, restoring it when you can unload the driver. We will do it differently - we will wait for the thread to complete its work. This is not very good because we will have to block one of the system threads executing the DriverUnload procedure, but it will allow us to practice synchronization once again.

A thread, like a timer, is also an expected object. While a thread is executing, it is in a busy state and goes free when it terminates. So we will wait.

     mov g_fStop, TRUE

If the thread is still busy with its work, setting the g_fStop flag will signal it that it is time to rest.

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

Now we just wait for the thread to exit. That is why we got the thread pointer in the StartThread procedure .

When KeWaitForSingleObject returns, the thread has already ceased to exist and the driver can be safely unloaded. Here, too, the only reason the wait can be satisfied is for the thread to terminate. Therefore, it is not necessary to check the return code.

     invoke ObDereferenceObject, g_pkThread

     invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

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

As usual, we clean up after ourselves. The ObDereferenceObject call decrements the non-object reference count and balances the ObReferenceObjectByHandle call we made in the StartThread procedure . This allows the system to reclaim the resources allocated to create the stream.

The source code of the driver in the archive .