CSDN博客

img SDNHELIXIN

MDL

发表于2008/9/30 21:28:00  3337人阅读

 DriverEntry函数:

代码:
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;
        DriverEntry函数与以往的不同,除了通常的IRP_MJ_CREATE、IRP_MJ_CLOSE和IRP_MJ_DEVICE_CONTROL之外,这里还要处理IRP_MJ_CLEANUP。当用户模式代码调用了CloseHandle时,驱动程序开始发出IRP_MJ_CLEANUP,通知系统设备驱动将要关闭句柄。在此之后句柄真正关闭,驱动收到IRP_MJ_CLOSE。在本例中我们希望释放掉之前使用的资源,所以需要处理IRP_MJ_CLEANUP。

代码:
g_pSharedMemory := ExAllocatePool(NonPagedPool, PAGE_SIZE);
if g_pSharedMemory <> nil then
begin
        获得了控制代码IOCTL_GIVE_ME_YOUR_MEMORY,我们来从非分页内存中分配一个内存页。驱动应该将这部分内存映射到用户进程的地址空间中,在接收到请求的进程上下文中,即在我们的应用程序的地址空间中。使用非分页内存以及使用一个内存页的原因后面会介绍到。
        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所必须的。如果MDL不与IRP相关联(我们这里正是这样),则第三个参数就为FALSE。第四个参数定义是否需要减少进程的份额,并只用于位于驱动程序链最上层的驱动程序或是单层的驱动程序(我们这里正是这样)。每一个进程都要获得一定份额的系统资源。当进程为自己分配资源时,这个份额就会减小。如果份额用完,就不能再为其分配相应的资源。我们可不想减少这个用于分配内存的进程份额,所以第四个参数设为FALSE。最后一个参数定义了一个非必要的指向IRP的指针,通过这个指针MDL可以与IRP关联。例如,对于直接I/O,I/O管理器为用户缓冲区建立MDL,并将其地址送至IRP.MdlAddress。我们弄这个MDL可不是用来搞I/O的,所以就没有什么IRP,最后一个参数也就设为NULL。
函数IoAllocateMdl为MDL分配内存并初始化首部。

代码:
MmBuildMdlForNonPagedPool(g_pMdl);
        MmBuildMdlForNonPagedPool填充物理页号并更新MDL首部的某些范围。
如果我们将要调用的函数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;
MmMapLockedPagesSpecifyCache返回我们的内存页映射到用户空间中的地址。我们将这个地址传递到应用程序中。从这一刻起该内存页就成为共享的了,并且驱动程序对其的使用不依赖于当前的地址上下文,而用户进程也能以自己的地址来访问。
为了直观起见,函数UpdateTime将把当前系统时间放在我们的内存页中。

代码:
{更新系统时间到共享内存}
procedure UpdateTime; stdcall;
var
  SysTime:LARGE_INTEGER;
begin
  KeQuerySystemTime(@SysTime);
  ExSystemTimeToLocalTime(@SysTime, g_pSharedMemory);
end;
KeQuerySystemTime取得的是格林威治时间。再用ExSystemTimeToLocalTime将其转换为本地时间。

代码:
if IoInitializeTimer(p_DeviceObject, @TimerRoutine,@dwContext) = STATUS_SUCCESS then
Begin
IoInitializeTimer初始化内核Timer,Timer将与设备对象建立关联。DEVICE_OBJECT结构体中有一个Timer域,其中有指向IO_TIMER结构体的指针。函数IoInitializeTimer的第一个参数定义了Timer要和哪一个设备对象关联。第二个参数是一个指向系统启用Timer时要调用的函数的指针。TimerRoutine函数将调用UpdateTime,在我们的内存页中更新系统时间。TimerRoutine运行在IRQL = DISPATCH_LEVEL(DDK中有记载)。这就是我们使用非分页内存的第一个也是最主要的原因。IoInitializeTimer的最后一个参数是一个指向任意数据的指针。这个指针将被传递到TimerRoutine中。我们这里不需要指定这个值,所以只是随便虚构一个变量。

代码:
IoStartTimer(p_DeviceObject);
  g_fTimerStarted := true;
启动Timer。现在函数TimerRoutine大约每秒被调用一次。这个时间间隔是不能修改的。

代码:
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下调度线程不执行,这样在系统向共享内存页中写入当前时间时用户线程不可能使系统中断。所以在单处理器机器上应该不会出现任何同步上的问题。在多处理器机器上这些线程则有可能同时工作。所以在类似的情形下需要考虑同步问题。在本例中我们就不在这上下功夫了,在后面有文章专门讨论。这个程序最不好的一点是时间上有误差,不过在这里不算什么。

 

 

0 0

相关博文

我的热门文章

img
取 消
img