pDriverObject^.MajorFunction[IRP_MJ_CREATE] := @DispatchCreateClose; pDriverObject^.MajorFunction[IRP_MJ_CLEANUP] := @DispatchCleanup; pDriverObject^.MajorFunction[IRP_MJ_CLOSE] := @DispatchCreateClose; pDriverObject^.MajorFunction[IRP_MJ_DEVICE_CONTROL] := @DispatchControl; pDriverObject^.DriverUnload := @DriverUnload;
g_pSharedMemory := ExAllocatePool(NonPagedPool, PAGE_SIZE); if g_pSharedMemory <> nil then begin
ExAllocatePool返回系统空间中的地址,也就是说驱动程序是与当前上下文无关的。现在需要将这块内存映射到这个进程的地址空间中去,使之被共享。我们的驱动程序是单层的,所以对IRP_MJ_DEVICE_CONTROL的处理我们想放在我们应用程序的地址上下文中。在我们将分配的一个内存页映射到进程地址空间之前必须先分配MDL(Memory Descriptor List。)
MDL是一个结构体,用于描述一片内存区域中的物理内存页。其定义如下:
PMDL = ^TMDL; TMDL=packed record Next: PMDL; Size: CSHORT; MdlFlags: CSHORT; Process: PEPROCESS; MappedSystemVa: PVOID; StartVa: PVOID; ByteCount: ULONG; ByteOffset: ULONG; end;
g_pMdl := IoAllocateMdl(g_pSharedMemory, PAGE_SIZE,false, false, nil);
函数IoAllocateMdl为MDL分配内存并初始化首部。
MmBuildMdlForNonPagedPool(g_pMdl);
如果我们将要调用的函数MmMapLockedPagesSpecifyCache的参数AccessMode为UserMode且调用失败,系统会抛出一个异常(这是DDK公开说明的),这个异常我们能够处理,所以我们建立SEH-frame用于捕获异常。
MmMapLockedPagesSpecifyCache函数将MDL所描述的内存映射到我们应用程序的地址空间中。
MDL的第一个参数为描述所要映射的内存区域的MDL。第二个参数定义了是否要从用户模式下访问这块内存。第三个参数定义了这块内存被处理器缓存的方式。如果第四个参数为NULL,则系统会自己从用户空间中挑选虚拟地址。第五个参数定义了如果万一系统不能完成请求,是否要出现BSOD,但是这只用在第二个参数为KernelMode时。我们可不想让系统死掉,于是将这个参数赋值为FALSE。最后一个参数定义了成功调用MmMapLockedPagesSpecifyCache的重要性。
借助于MDL,在用户地址空间中只能映射锁定的内存,即位于非分页池中的内存(对于使用分页内存的所有情况我并不全都知道)。这是使用非分页内存的第一个理由。
映射的内存不能少于一页,所以我们需要完整的一个内存页,但是实际上总共只用其中的几个字节。
g_pUserAddress := MmMapLockedPagesSpecifyCache(g_pMdl,UserMode, MmCached, nil, 0, NormalPagePriority); if g_pUserAddress <> nil then begin DbgPrint('SharingMemory: Memory mapped into user space at address %08X'#13#10,g_pUserAddress); pSystemBuffer := p_Irp^.AssociatedIrp.SystemBuffer; PVOID(pSystemBuffer^) := g_pUserAddress;
为了直观起见,函数UpdateTime将把当前系统时间放在我们的内存页中。
{更新系统时间到共享内存} procedure UpdateTime; stdcall; var SysTime:LARGE_INTEGER; begin KeQuerySystemTime(@SysTime); ExSystemTimeToLocalTime(@SysTime, g_pSharedMemory); end;
if IoInitializeTimer(p_DeviceObject, @TimerRoutine,@dwContext) = STATUS_SUCCESS then Begin
IoStartTimer(p_DeviceObject); g_fTimerStarted := true;
if p_Irp^.IoStatus.Status <> STATUS_SUCCESS then begin DbgPrint('SharingMemory: Something went wrong:'#13#10); Cleanup(p_DeviceObject); end;
{清理过程--释放资源} procedure Cleanup(pDeviceObject:PDEVICE_OBJECT); stdcall; begin if g_fTimerStarted then begin IoStopTimer(pDeviceObject); DbgPrint('SharingMemory: Timer stopped'#13#10); end; if (g_pUserAddress <> nil) and (g_pMdl <> nil) then begin MmUnmapLockedPages(g_pUserAddress, g_pMdl); DbgPrint('SharingMemory: Memory at address %08X unmapped'#13#10, g_pUserAddress); g_pUserAddress := nil; end; if g_pMdl <> nil then begin IoFreeMdl(g_pMdl); DbgPrint('SharingMemory: MDL at address %08X freed'#13#10, g_pMdl); g_pMdl := nil; end; if g_pSharedMemory <> nil then begin ExFreePool(g_pSharedMemory); DbgPrint('SharingMemory: Memory at address %08X released'#13#10, g_pSharedMemory); g_pSharedMemory := nil; end; end;
Cleanup过程进行的工作都是很显然的,不用过多解释。唯一的奥妙在于将内存映射到用户空间和还原操作是借助于MmUnmapLockedPages函数实现的,应该在进程定义的地址上下文中进行,这是很自然的。
以上就是整个的驱动程序,利用内核计时器大约每1秒钟读取一次系统时间并写入到共享内存中供用户程序读取。
户端程序非常简单,首先是加载驱动程序,然后启动一个计时器,每隔一秒钟去读取一下共享内存里的时间信息,然后显示出来。如果驱动程序被正常启动,我们就向其发送控制代码IOCTL_GIVE_ME_YOUR_MEMORY。驱动将地址返回到变量pSharedMemory中,这个地址就是驱动程序映射内存缓冲区的地址。对其大小我们这里不感兴趣,足够我们用的。其中头8个字节为当前时间,每一秒钟由驱动程序更新一次。程序其他的地方都很好理解,我们主要来看一下计时器过程
procedure TForm1.Timer1Timer(Sender: TObject); var stime: SYSTEMTIME; buffer: string; begin if pSharedMemory <> nil then begin FileTimeToSystemTime(pSharedMemory^, stime); buffer := Format('%2.2d:%2.2d:%2.2d', [stime.wHour, stime.wMinute, stime.wSecond]); Label1.Caption := buffer; end; end;
计时器过程的任务是将当前时间格式化为小时:分钟:秒钟的形式并将其输出。
这样驱动程序每秒钟向分配的内存页写一次当前时间,将其虚拟地址视为系统地址空间的地址,而应用程序每秒钟一次地获取此信息,将虚地址视为用户地址空间的地址。但是物理上是同一个内存页。这样时钟每秒滴答一次。顺便说一句,函数KeQuerySystemTime取得当前时间,同时在内核和用户模式页间共享,这个内存页在内核模式下地址为0FFDF0000h,而在用户模式下为7FFE0000h(用户函数GetSystemTime和内核函数KeQuerySystemTime读取的都是这个字节),之后函数将其写入KUSER_SHARED_DATA结构体。从这个结构体的名字可以看出,它是由内核模式与用户模式共享的。
当驱动程序收到IRP_MJ_CLEANUP并随后收到IRP_MJ_CLOSE而进行清理时,最主要的就是解除对用户地址空间的内存映射。在这些操作中甚至可能会没有异常处理。如果应用程序崩溃,系统就要自己关闭所有打开的句柄和设备句柄。我们在对IRP_MJ_CLEANUP的处理中解除我们的内存共享仅仅是希望能将过去可能分配过的资源全部释放掉。在本例中这项工作还可以在对IRP_MJ_CLOSE的处理中进行。一般情况下,MmUnmapLockedPages应该在用户进程中止后调用。
本例与上例的差别是,这里我们有两个线程使用共享的内存资源。这时我们就应该考虑同步的问题了。读线程工作在用户模式下,因而总是处于IRQL = PASSIVE_LEVEL下。写线程位于系统进程空间并执行TimerRoutine函数,其地址定义在IoInitializeTimer调用中。TimerRoutine函数调用系统函数的环境是IRQL = DISPATCH_LEVEL(DDK中有准确的叙述)并由idle进程的线程执行,在我所试验过的所有情况下,都是由这个线程执行的。它的优先级要比用户线程的优先级低,所以在从共享内存页读取数据时它不可能使应用程序中断。在IRQL = DISPATCH_LEVEL下调度线程不执行,这样在系统向共享内存页中写入当前时间时用户线程不可能使系统中断。所以在单处理器机器上应该不会出现任何同步上的问题。在多处理器机器上这些线程则有可能同时工作。所以在类似的情形下需要考虑同步问题。在本例中我们就不在这上下功夫了,在后面有文章专门讨论。这个程序最不好的一点是时间上有误差,不过在这里不算什么。