本篇及下篇教程我们将讲述内核同步对象。同步是一个涉及面非常广的主题,系统提供了多种同步对象,因此两篇文章也仅能让您对其有个大致的了解。
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. 
10.3 创建线程
通常,每个驱动程序都有一个主线程负责执行DriverEntry、DriverUnload加载或者卸载驱动,通过DispacthXxx分派用户请求。一般情况下,它是不需要别的线程的,当然了,如果需要,您可以使用PsCreateSystemThread函数创建一个新的线程。
代码:
var
  hThread: THANDLE;
  ……
status := PsCreateSystemThread(@hThread,
                            THREAD_ALL_ACCESS, nil, 0,
                            nil, @ThreadProc, nil); 
变量hThread保存线程句柄,第三个参数是一个指向OBJECT_ATTRIBUTES的指针,在这里我们将其置为nil是因为在这个例子中我们并不需要使用它。如果需要使用它,首先要调用InitializeObjectAttributes函数创建一个OBJECT_ATTRIBUTES的实例,然后再调用PsCreateSystemThread创建线程。 InitializeObjectAttributes的用法可以参考前面的教程。
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); 
上面的代码用于显示线程缺省时的优先级,然后我们试着将线程的优先级提升了2。大家知道,必须IRQL=PASSIVE_LEVEL时线程方可运行,系统缺省的线程优先级是8
代码:
KeInitializeTimerEx(@_kTimer, SynchronizationTimer); 
接下来,我们来创建定时器。在前面的教程中我们也使用过定时器 (IoInitializeTimer, IoStartTimer, IoStopTimer)。那个定时器与我们在这里使用的定时器(watchdog)相比有几个缺点:首先,它严格的与设备对象相关联并且用它只能创建一个秒级的定时器;其次,它每秒触发一次,你无法改变这个触发时间;其三,它必须运行在IRQL = DISPATCH_LEVEL。而用KeInitializeTimerEx创建的定时器则没有这些限制。
KeInitializeTimer函数仅填充KTIMER结构,此时,定时器处于非信号状态,它还没有开始倒计时,在这样的定时器上等待的线程永远得不到唤醒。为了启动定时器倒计时,我们调用KeSetTimerEx函数,原型如下:
代码:
function KeSetTimerEx(
    Timer: PKTIMER;
    DueTime_LowPart, DueTime_HighPart: DWORD;
    Period: LONG;
    Dpc: PKDPC): BOOLEAN; stdcall;
duetime是一个64位的时间值,单位为100纳秒。如果该值为正,则表示一个从1601年1月1日算起的绝对时间。如果该值为负,则它是相对于当前时间的一段时间间隔。period是周期超时值,单位为毫秒(ms),dpc是一个可选的指向KDPC对象的指针。这种定时器在第一次倒计时时使用duetime时间,到期后再使用period值重复倒计时。

返回值如果为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; 
KeWaitForSingleObject函数的原型如下:
代码:
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释放线程对象。