上篇教程我们学习了通过Section在用户进程和内核驱动程序之间共享信息的方法,但是这种方法有一个缺点,就是驱动程序被硬性限制在具体进程的地址上下文中,即驱动程序所使用的虚拟地址位于此进程的地址空间中。我们在本例中使用的方法将没有这个缺点。对于驱动程序来说,这种方法更为自然些。
首先来看看驱动程序。
代码:
unit SharingMemory; interface uses nt_status, ntoskrnl, native, winioctl, fcall, macros; function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall; implementation uses seh; var g_pSharedMemory: PVOID; g_pMdl: PVOID; g_pUserAddress: PVOID; g_fTimerStarted: boolean; g_usDeviceName, g_usSymbolicLinkName: UNICODE_STRING; {更新系统时间到共享内存} procedure UpdateTime; stdcall; var SysTime:LARGE_INTEGER; begin KeQuerySystemTime(@SysTime); ExSystemTimeToLocalTime(@SysTime, g_pSharedMemory); end; procedure TimerRoutine(pDeviceObject:PDEVICE_OBJECT; pContext:PVOID); stdcall; begin UpdateTime; 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; function DispatchCleanup(pDeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall; begin DbgPrint(#13#10'SharingMemory: Entering DispatchCleanup'#13#10); Cleanup(pDeviceObject); p_Irp^.IoStatus.Status := STATUS_SUCCESS; p_Irp^.IoStatus.Information := 0; IofCompleteRequest(p_Irp, IO_NO_INCREMENT); DbgPrint('SharingMemory: Leaving DispatchCleanup'#13#10); result := STATUS_SUCCESS; end; function DispatchCreateClose(p_DeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall; begin p_Irp^.IoStatus.Status := STATUS_SUCCESS; p_Irp^.IoStatus.Information := 0; IofCompleteRequest(p_Irp, IO_NO_INCREMENT); result := STATUS_SUCCESS; end; function DispatchControl(p_DeviceObject: PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall; label SafePlace; var dwContext:DWORD; psl:PIO_STACK_LOCATION; IOCTL_GIVE_ME_YOUR_MEMORY: DWORD; pSystemBuffer: PVOID; begin DbgPrint(#13#10'SharingMemory: Entering DispatchControl'#13#10); IOCTL_GIVE_ME_YOUR_MEMORY := CTL_CODE(FILE_DEVICE_UNKNOWN, $800, METHOD_BUFFERED, FILE_READ_ACCESS); p_Irp^.IoStatus.Status := STATUS_UNSUCCESSFUL; p_Irp^.IoStatus.Information := 0; psl := IoGetCurrentIrpStackLocation(p_Irp); {取IRP的stack location的指针} if psl^.Parameters.DeviceIoControl.IoControlCode = IOCTL_GIVE_ME_YOUR_MEMORY then begin {是我们控制码就开始处理} if psl^.Parameters.DeviceIoControl.OutputBufferLength >= sizeof(PVOID) then begin g_pSharedMemory := ExAllocatePool(NonPagedPool, PAGE_SIZE); if g_pSharedMemory <> nil then begin DbgPrint('SharingMemory: %X bytes of nonpaged memory allocated at address %08X'#13#10, PAGE_SIZE, g_pSharedMemory); g_pMdl := IoAllocateMdl(g_pSharedMemory, PAGE_SIZE, false, false, nil); if g_pMdl <> nil then begin DbgPrint('SharingMemory: MDL allocated at address %08X'#13#10, g_pMdl); MmBuildMdlForNonPagedPool(g_pMdl); {安装SEH} asm push offset DefaultExceptionHandler push fs:[0] mov fs:[0], esp mov sseh.SafeEip, offset SafePlace mov sseh.PrevEbp, ebp mov sseh.PrevEsp, esp end; 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; if IoInitializeTimer(p_DeviceObject, @TimerRoutine, @dwContext) = STATUS_SUCCESS then begin IoStartTimer(p_DeviceObject); g_fTimerStarted := true; DbgPrint('SharingMemory: Timer started'); p_Irp^.IoStatus.Information := sizeof(PVOID); p_Irp^.IoStatus.Status := STATUS_SUCCESS; end; end; SafePlace: {发生异常安全退出的地方} asm pop fs:[0] add esp, 4 end; end; end; end else begin p_Irp^.IoStatus.Status := STATUS_BUFFER_TOO_SMALL; end; end else begin p_Irp^.IoStatus.Status := STATUS_INVALID_DEVICE_REQUEST; end; if p_Irp^.IoStatus.Status <> STATUS_SUCCESS then begin DbgPrint('SharingMemory: Something went wrong:'#13#10); Cleanup(p_DeviceObject); end; IofCompleteRequest(p_Irp, IO_NO_INCREMENT); DbgPrint('SharingMemory: Leaving DispatchControl'#13#10); result := p_Irp^.IoStatus.Status; end; procedure DriverUnload(p_DriverObject:PDRIVER_OBJECT); stdcall; begin IoDeleteSymbolicLink(@g_usSymbolicLinkName); IoDeleteDevice(p_DriverObject^.DeviceObject); end; {驱动程序进入点} function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; var status: NTSTATUS; pDeviceObject: TDeviceObject; begin status := STATUS_DEVICE_CONFIGURATION_ERROR; g_pSharedMemory := nil; g_pMdl := nil; g_pUserAddress := nil; g_fTimerStarted := false; RtlInitUnicodeString(g_usDeviceName, '\Device\SharingMemory'); RtlInitUnicodeString(g_usSymbolicLinkName, '\DosDevices\SharingMemory'); 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 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; status := STATUS_SUCCESS; end else begin IoDeleteDevice(@pDeviceObject); end; end; result := status; end; end.
代码:
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;
更准确地讲,MDL结构体是一个首部(header)。紧随首部之后的是许多物理页的页号(page frame number, PFN)。但是MDL所描述的内存区域在虚拟地址空间中是连续不间断的,而它们所占据的物理页所在的物理内存却可能是按任意的顺序排列。正是因为如此再加上页的数量较大,我们需要维护一个表来记录该内存区中所有的物理页。同时这也用在了直接内存访问(Direct Memory Access, DMA)中。在我们这里物理页总共就一个。
代码:
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秒钟读取一次系统时间并写入到共享内存中供用户程序读取。下面我们来看看用户程序。
代码:
unit main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls; type TForm1 = class(TForm) Timer1: TTimer; Label1: TLabel; procedure FormActivate(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses WinSvc, nt_status, macros; const _DELETE = $10000; var hDevice: THANDLE; pSharedMemory: PFILETIME; hSCManager: THANDLE; hService: THANDLE; {$R *.dfm} procedure TForm1.FormActivate(Sender: TObject); var acModulePath: string; dwBytesReturned: DWORD; lpTemp: PChar; IOCTL_GIVE_ME_YOUR_MEMORY: DWORD; begin IOCTL_GIVE_ME_YOUR_MEMORY := CTL_CODE(FILE_DEVICE_UNKNOWN, $800, METHOD_BUFFERED, FILE_READ_ACCESS); hSCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS); if hSCManager <> 0 then begin acModulePath := GetCurrentDir + '\' + ExtractFileName('SharingMemory.sys'); hService := CreateService(hSCManager, 'SharingMemory', 'Another way how to share memory', SERVICE_START or SERVICE_STOP or _DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, PChar(acModulePath), nil, nil, nil, nil, nil); if hService <> 0 then begin if StartService(hService, 0, lpTemp) then begin; hDevice := CreateFile(PChar('\\.\SharingMemory'), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0); if hDevice <> INVALID_HANDLE_VALUE then begin if DeviceIoControl(hDevice, IOCTL_GIVE_ME_YOUR_MEMORY, nil, 0, @pSharedMemory, sizeof(PVOID), dwBytesReturned, nil) then begin if dwbytesReturned = sizeof(pSharedMemory) then begin Timer1.Enabled := true; {激活用户端计时器,每隔1秒读取一次共享内存} end; end else begin ShowMessage('Can''t send control code to device.'); end; end else begin ShowMessage('Device is not present.'); end; end else begin ShowMessage('Can''t start driver.'); end; end else begin ShowMessage('Can''t register driver.'); end; end else begin ShowMessage('Can''t connect to Service Control Manager.'); end; end; {退出前卸载驱动,释放资源} procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); var _ss: SERVICE_STATUS; begin CloseHandle(hDevice); ControlService(hService, SERVICE_CONTROL_STOP, _ss); DeleteService(hService); CloseServiceHandle(hService); CloseServiceHandle(hSCManager); end; {计时器过程} 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; end.
如果驱动程序被正常启动,我们就向其发送控制代码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下调度线程不执行,这样在系统向共享内存页中写入当前时间时用户线程不可能使系统中断。所以在单处理器机器上应该不会出现任何同步上的问题。在多处理器机器上这些线程则有可能同时工作。所以在类似的情形下需要考虑同步问题。在本例中我们就不在这上下功夫了,在后面有文章专门讨论。这个程序最不好的一点是时间上有误差,不过在这里不算什么。