本篇及下篇教程我们将讲述内核同步对象。同步是一个涉及面非常广的主题,系统提供了多种同步对象,因此两篇文章也仅能让您对其有个大致的了解。
10.1 同步对象
迄今为止,我们都不需要独占访问某个数据,因为我们仅有一个线程在工作。当有两个或多个线程都需要访问同一个资源时,就需要引入同步机制,否则此资源的状态就无法预知了。比如当两个线程同时访问(在一个多处理器的系统中这种情况很常见)一个保存在共亨内存中的变量。常见的解决方法就是后面的线程等待前一个线程完成数据访问。
为解决此类问题,操作系统提供了一些同步对象的机制:事件(Event)、互斥(Mutex)--在内核中被称为突变体(Mutant)、信号灯(Semaphore)等等。这些同步机制在用户模式下也存在,而且使用方法也大同小异。
所有的同步对象结构的第一个字段均为一个DISPATCHER_HEADER结构用以描述此对象所期望的操作。下面是本章将要用到的两个结构:定时器对象(又叫watchdog)和线程对象。
代码:
_KTIMER = packed record Header: DISPATCHER_HEADER; …… end;
代码:
KTHREAD = packed record Header: DISPATCHER_HEADER; . . . KTHREAD ENDS end;
内核模式与用户模式在同步的管理上并无大的差异,但还是有几个地方需要注意:首先也是最重要的一点就是同步对象的IRQL要比DISPATCH_LEVLE低,也就是说执行在高于或等于DISPATCH_LEVEL级上的代码不能阻塞线程。这个规则表明你只能在DriverEntry函数、AddDevice函数,或驱动程序的分派函数中阻塞当前线程。因为这些函数都执行在PASSIVE_LEVEL级上。没有必要在DriverEntry或AddDevice函数中阻塞当前线程,因为这些函数的工作仅仅是初始化一些数据结构。其次在内核模式下是通过指向同步对象的指针访问该对象的,而在用户模式下则是通过对象句柄访问。
调用KeWaitForSingleObject或KeWaitForMultipleObjects函数可以使代码(以及背景线程)在一个或多个同步对象上等待,等待它们进入信号态。内核为初始化和控制这些对象的状态提供了例程。
10.2 教程源码
代码:
unit TimerWorks; interface uses nt_status, ntoskrnl, hal, native, fcall, macros; function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall; implementation var g_pkThread: PVOID; {PTR KTHREAD} g_fStop: Boolean; g_usDeviceName, g_usSymbolicLinkName: UNICODE_STRING; function ThreadProc(StartContext: PVOID): NTSTATUS; var dwCounter: DWORD; pkThread: PVOID; {PKTHREAD} _kTimer: KTIMER; liDueTime: LARGE_INTEGER; iPriority: KPRIORITY; begin dwCounter := 0; DbgPrint(#13#10'TimerWorks: Entering ThreadProc'#13#10); DbgPrint('TimerWorks: IRQL = %d'#13#10, KeGetCurrentIrql); pkThread := KeGetCurrentThread; iPriority := KeQueryPriorityThread(pkThread); DbgPrint('TimerWorks: Thread Priority = %d'#13#10, iPriority); Inc(iPriority, 2); KeSetPriorityThread(pkThread, iPriority); iPriority := KeQueryPriorityThread(pkThread); DbgPrint('TimerWorks: Thread Priority = %d'#13#10, iPriority); KeInitializeTimerEx(@_kTimer, SynchronizationTimer); liDueTime.HighPart := liDueTime.HighPart or -1; liDueTime.LowPart := $FD050F80; {-50000000} KeSetTimerEx(@_kTimer, liDueTime.LowPart, liDueTime.HighPart, 1000, nil); DbgPrint('TimerWorks: Timer is set. It starts counting in 5 seconds...'#13#10); while dwCounter < 10 do begin KeWaitForSingleObject(@_kTimer, Executive, KernelMode, FALSE, nil); Inc(dwCounter); DbgPrint('TimerWorks: Counter = %d'#13#10, dwCounter); if g_fStop then begin DbgPrint('TimerWorks: Stop counting to let the driver to be uloaded'#13#10); Break; end; end; KeCancelTimer(@_kTimer); DbgPrint('TimerWorks: Timer is canceled. Leaving ThreadProc'#13#10); DbgPrint('TimerWorks: Our thread is about to terminate'#13#10); PsTerminateSystemThread(STATUS_SUCCESS); Result := STATUS_SUCCESS; end; procedure DriverUnload(p_DriverObject:PDRIVER_OBJECT); stdcall; begin DbgPrint(#13#10'TimerWorks: Entering DriverUnload'#13#10); g_fStop := TRUE; {Break the timer loop if it's counting} DbgPrint('TimerWorks: Wait for thread exits...'#13#10); KeWaitForSingleObject(@g_pkThread, Executive, KernelMode, FALSE, nil); ObfDereferenceObject(@g_pkThread); IoDeleteSymbolicLink(@g_usSymbolicLinkName); IoDeleteDevice(p_DriverObject^.DeviceObject); DbgPrint('TimerWorks: Leaving DriverUnload'#13#10); end; function StartThread: NTSTATUS; var status: NTSTATUS; hThread: THANDLE; begin DbgPrint(#13#10'TimerWorks: Entering StartThread'#13#10); status := PsCreateSystemThread(@hThread, THREAD_ALL_ACCESS, nil, 0, nil, @ThreadProc, nil); if status = STATUS_SUCCESS then begin ObReferenceObjectByHandle(hThread, THREAD_ALL_ACCESS, nil, KernelMode, @g_pkThread, nil); ZwClose(hThread); DbgPrint('TimerWorks: Thread created'#13#10); end else begin DbgPrint('TimerWorks: Can''t create Thread. Status: %08X'#13#10, status); end; DbgPrint('TimerWorks: Leaving StartThread'#13#10); Result := status; end; function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall; var status: NTSTATUS; pDeviceObject: TDeviceObject; begin status := STATUS_DEVICE_CONFIGURATION_ERROR; RtlInitUnicodeString(g_usDeviceName, '\Device\TimerWorks'); RtlInitUnicodeString(g_usSymbolicLinkName, '\DosDevices\TimerWorks'); if IoCreateDevice(pDriverObject, 0, @g_usDeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, pDeviceObject) = STATUS_SUCCESS then begin if IoCreateSymbolicLink(@g_usSymbolicLinkName, @g_usDeviceName) = STATUS_SUCCESS then begin if StartThread = STATUS_SUCCESS then begin g_fStop := false; {reset global flag} pDriverObject^.DriverUnload := @DriverUnload; status := STATUS_SUCCESS; end else begin IoDeleteSymbolicLink(@g_usSymbolicLinkName); IoDeleteDevice(@pDeviceObject); end; end else begin IoDeleteDevice(@pDeviceObject); end; end; Result := status; end; end.
通常,每个驱动程序都有一个主线程负责执行DriverEntry、DriverUnload加载或者卸载驱动,通过DispacthXxx分派用户请求。一般情况下,它是不需要别的线程的,当然了,如果需要,您可以使用PsCreateSystemThread函数创建一个新的线程。
代码:
var hThread: THANDLE; …… status := PsCreateSystemThread(@hThread, THREAD_ALL_ACCESS, nil, 0, nil, @ThreadProc, nil);
PsCreateSystemThread函数的第四第五个参数分别是进程句柄和一个指向CLIENT_ID结构的指针,用于指明指定进程的上下文环境,在本例中我们也不使用它们。第六个参数是指向线程函数的指针,此函数完成该线程的相关功能。函数原型如下:
function ThreadProc(StartContext: PVOID): NTSTATUS;
PsCreateSystemThread函数的最后一个参数的值将被赋给线程函数的StartContext参数,通过它,主线程可以向子线程传递任何信息,比如一个指向任意结构的指针。在我们的例子中此参数被置为nil,因为我们的主线程不需要向子线程传递任何信息。
代码:
if status = STATUS_SUCCESS then begin ObReferenceObjectByHandle(hThread, THREAD_ALL_ACCESS, nil, KernelMode, @g_pkThread, nil); ZwClose(hThread); DbgPrint('TimerWorks: Thread created'#13#10); end else begin DbgPrint('TimerWorks: Can''t create Thread. Status: %08X'#13#10, status); end;
当要结束一个线程时,我们需要一个指向该线程对象的指针,这与用户模式下使用线程句柄不同。但是PsCreateSystemThread返回的却是一个不透明的线程句柄,所以我们需要使用ObReferenceObjectByHandle去获取指向线程对象的指针。ObReferenceObjectByHandle函数把你提供的句柄转换成一个指向下层内核对象的指针。一旦有了这个指针,你就可以调用ZwClose关闭那个句柄。在某些地方,你还需要调用ObDereferenceObject函数释放对该线程对象的引用。
10.4 指向对象的指针
在此之前,我们已经不止一次与对象指针打交道了,比如在DriveEntry函数里我们会从系统接收一个指向DRIVER_OBJECT对象的指针,通过这个指针,我们使用IoCreateDevice创建我们的设备,如果想取得指向该对象的指针,我们就可以使用上节介绍过的ObReferenceObjectByHandle函数。
ObreferenceObjectXxx系列函数均返回一个指向对象的指针,只不过每个函数使用的场合不尽相同,比如ObReferenceObjectByName是通过对象名称返回指向该对象的指针,而ObReferenceObjectByHandle则是通过句柄返回指向该对象的指针。
每个对象在系统内部都有一个结构与之对应,比如线程对象有一个未公开的KTHREAD结构,而计时器对象则是KTIMER。
10.5 线程函数
一旦线程成功创建,它就有机会获得执行的权限,这时就会调用ThreadProc函数。
dwCounter := 0;
dwCounter用来限制线程的执行次数,您也可以去掉这个限制,那样的话在您卸载驱动前该线程会一直不知疲惫地运行。
代码:
pkThread := KeGetCurrentThread; iPriority := KeQueryPriorityThread(pkThread); DbgPrint('TimerWorks: Thread Priority = %d'#13#10, iPriority); Inc(iPriority, 2); KeSetPriorityThread(pkThread, iPriority); iPriority := KeQueryPriorityThread(pkThread); DbgPrint('TimerWorks: Thread Priority = %d'#13#10, iPriority);
代码:
KeInitializeTimerEx(@_kTimer, SynchronizationTimer);
KeInitializeTimer函数仅填充KTIMER结构,此时,定时器处于非信号状态,它还没有开始倒计时,在这样的定时器上等待的线程永远得不到唤醒。为了启动定时器倒计时,我们调用KeSetTimerEx函数,原型如下:
代码:
function KeSetTimerEx( Timer: PKTIMER; DueTime_LowPart, DueTime_HighPart: DWORD; Period: LONG; Dpc: PKDPC): BOOLEAN; stdcall;
返回值如果为TRUE,则表明定时器已经启动。(在这种情况下,如果我们再调用KeSetTimerEx函数,则定时器放弃原来的时间重新开始倒计时)
即使定时器开始倒计时,它仍处于非信号态,直到到达指定的时间。在那个时刻,该定时器对象自动变为信号态,所有等待的线程都被释放。
为什么是1601年1月1日呢?每个人都知道可被4整除的年份是闰年。许多人也知道世纪年(如1900年)应例外,虽然这些年份都能被4整除但它们不是闰年。少数人还知道能被400整除的年份(如1600和2000)是例外中的例外,它们也是闰年。而1601年1月1日正好是一个400年周期的开始。如果把它作为时间信息的起点,那么把NT时间信息转换为常规日期表达(或相反)就不用做任何跳跃操作。
本例中我们使用相对时间,也就是每隔指定的时间触发一次,这里我们是每5秒触发一次。要注意的是,使用相对时间必须是负数。
代码:
while dwCounter < 10 do begin KeWaitForSingleObject(@_kTimer, Executive, KernelMode, FALSE, nil); Inc(dwCounter); DbgPrint('TimerWorks: Counter = %d'#13#10, dwCounter); if g_fStop then begin DbgPrint('TimerWorks: Stop counting to let the driver to be uloaded'#13#10); Break; end; end;
代码:
function KeWaitForSingleObject( SObject:Pointer; WaitReason:KWAIT_REASON; WaitMode:TKProcessorMode; Alertable:LongBool; Timeout:PLargeInteger):NTSTATUS; stdcall;
SObject指向你要等待的对象。注意该参数的类型是PVOID,它应该指向某个同步对象。该对象必须在非分页内存中,例如,在设备扩展中或其它从非分页内存池中分配的数据区。在大部分情况下,执行堆栈可以被认为是非分页的。
WaitReason是一个纯粹建议性的值,它是KWAIT_REASON枚举类型。实际上,除非你指定了WrQueue参数,否则任何内核代码都不关心此值。线程阻塞的原因被保存到一个不透明的数据结构中,如果你了解这个数据结构,那么在调试某种死锁时,你也许会从这个原因代码中获得一些线索。通常,驱动程序应把该参数指定为Executive,代表无原因。
WaitMode是MODE枚举类型,该枚举类型仅有两个值:KernelMode和UserMode。
Alertable是一个布尔类型的值。它不同于WaitReason,这个参数以另一种方式影响系统行为,它决定等待是否可以提前终止以提交一个APC。如果等待发生在用户模式中,那么内存管理器就可以把线程的内核模式堆栈换出。如果驱动程序以自动变量(在堆栈中)形式创建事件对象,并且某个线程又在提升的IRQL级上调用了KeSetEvent,而此时该事件对象刚好又被换出内存,结果将产生一个bug check。所以我们应该总把alertable参数指定为FALSE,即在内核模式中等待。
KeCancelTimer(@_kTimer)停止定时器,PsTerminateSystemThread(STATUS_SUCCESS)则中止线程。
最后DriveUnload做些卸载驱动的清理工作,首先要设置g_fStop以通知ThreadProc中止线程,然后调用KeWaitForSingleObject函数等待线程结束,当KeWaitForSingleObject结束后则表示线程已经中止,这时再调用ObfDereferenceObject释放线程对象。