Windows驱动之MDL

文章目录

  • Windows驱动之MDL
    • 1. MDL结构
    • 2. MDL的使用

Windows驱动之MDL

在驱动开发中,驱动程序访问应用程序数据缓冲区有三种方法三种方法:

  1. buffered方式中,I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个系统缓冲区工作。I/O管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。
  2. direct方式中,I/O管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。因此你的驱动程序将使用MDL工作。
  3. neither方式中,I/O管理器仅简单地把用户模式的虚拟地址传递给你。而使用用户模式地址的驱动程序应十分小心。

其中缓冲模式指定的代码如下:

NTSTATUS AddDevice(...)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., &fdo);
  fdo->Flags |= DO_BUFFERED_IO;  //buffered 模式
  fdo->Flags |= DO_DIRECT_IO;   //direcr 模式
  fdo->Flags |= 0; 	//neither 模式
}

本文我们来探讨一下MDL的结构和使用原理。

1. MDL结构

MDL是一个结构体,保存着一个需要通过MDL来共享访问一段内存的信息,这个结构体定义如下:

typedef struct _MDL {
    struct _MDL *Next;
    CSHORT Size;
    CSHORT MdlFlags;
    struct _EPROCESS *Process;
    PVOID MappedSystemVa;
    PVOID StartVa;
    ULONG ByteCount;
    ULONG ByteOffset;
} MDL, *PMDL;

有一个初始化的宏,可以比较明确的看出每个成员的作用:

#define BYTE_OFFSET(Va) \
  ((ULONG) ((ULONG_PTR) (Va) & (PAGE_SIZE - 1)))

#define PAGE_ALIGN(Va) \
  ((PVOID) ((ULONG_PTR)(Va) & ~(PAGE_SIZE - 1)))

#define MmInitializeMdl(_MemoryDescriptorList, \
                        _BaseVa, \
                        _Length) \
{ \
  (_MemoryDescriptorList)->Next = (PMDL) NULL; \
  (_MemoryDescriptorList)->Size = (CSHORT) (sizeof(MDL) + \
    (sizeof(PFN_NUMBER) * ADDRESS_AND_SIZE_TO_SPAN_PAGES(_BaseVa, _Length))); \
  (_MemoryDescriptorList)->MdlFlags = 0; \
  (_MemoryDescriptorList)->StartVa = (PVOID) PAGE_ALIGN(_BaseVa); \
  (_MemoryDescriptorList)->ByteOffset = BYTE_OFFSET(_BaseVa); \
  (_MemoryDescriptorList)->ByteCount = (ULONG) _Length; \
}

其中:

  • Size : 表示结构体的大小。
  • MappedSystemVa : 映射的系统地址。
  • StartVa : 成员给出了用户缓冲区的虚拟地址,这个地址仅在拥有数据缓冲区的用户模式进程上下文中才有效。
  • ByteCount : 是缓冲区的字节长度。
  • ByteOffset : 是缓冲区起始位置在一个页帧中的偏移值。
  • Pages : 数组没有被正式地声明为MDL结构的一部分,在内存中它跟在MDL的后面,包含用户模式虚拟地址映射为物理页帧的个数。

在这里有个奇怪的成员就是Pages, 这个成员在MDL中并没有定义出来,但是被真实的使用了,那么这个是干什么用的呢?想要明白这个东西,那么需要先掌握一个东西,MDL是怎么样使用Direct 模式的呢?

其实Direct模式,也可以理解成为共享内存模式,共享内存的方案如下:
Windows驱动之MDL_第1张图片

从上图我们可以看到,StartVa虚拟内存对应的物理内存映射表放在了Pages中,普通情况下,内存寻址都是通过CR3寻找PDE,然后在通过PDE,PTE查找到物理内存。但是在MDL中,我们通过MDL后面的Pages查找物理内存,并且两个物理内存是一样的,这样就无需考虑数据了。

Windows对于MDL提供了宏和访问函数

宏或函数 描述
IoAllocateMdl 创建MDL
IoBuildPartialMdl 创建一个已存在MDL的子MDL
IoFreeMdl 销毁MDL
MmBuildMdlForNonPagedPool 修改MDL以描述内核模式中一个非分页内存区域
MmGetMdlByteCount 取缓冲区字节大小
MmGetMdlByteOffset 取缓冲区在第一个内存页中的偏移
MmGetMdlVirtualAddress 取虚拟地址
MmGetSystemAddressForMdl 创建映射到同一内存位置的内核模式虚拟地址
MmGetSystemAddressForMdlSafe MmGetSystemAddressForMdl相同,但Windows 2000首选
MmInitializeMdl (再)初始化MDL以描述一个给定的虚拟缓冲区
MmPrepareMdlForReuse 再初始化MDL
MmProbeAndLockPages 地址有效性校验后锁定内存页
MmSizeOfMdl 取为描述一个给定的虚拟缓冲区的MDL所占用的内存大小
MmUnlockPages 为该MDL解锁内存页

2. MDL的使用

对于WriteFile的Direct方式,有如下代码:

NTSTATUS
NTAPI
NtWriteFile(IN HANDLE FileHandle,
            IN HANDLE Event OPTIONAL,
            IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
            IN PVOID ApcContext OPTIONAL,
            OUT PIO_STATUS_BLOCK IoStatusBlock,
            IN PVOID Buffer,
            IN ULONG Length,
            IN PLARGE_INTEGER ByteOffset OPTIONAL,
            IN PULONG Key OPTIONAL)
{
    //...
    Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
    //...
    Mdl = IoAllocateMdl(Buffer, Length, FALSE, TRUE, Irp);
    MmProbeAndLockPages(Mdl, PreviousMode, IoReadAccess);
    //...
}
  1. 对于IoAllocateMdl这个函数的作用是创建一个MDL结构,并把Irp->MdlAddress设置为新创建MDL的地址,以后你将用到这个成员,并且I/O管理器最后也使用该成员来清除MDL。
  2. MmProbeAndLockPages :该函数校验那个数据缓冲区是否有效,是否可以按适当模式访问;另外,该函数锁定了包含数据缓冲区的物理内存页,并在MDL的后面填写了页号数组。在效果上,一个锁定的内存页将成为非分页内存池的一部分,直到所有对该页内存加锁的调用者都对其解了锁。

在我们的驱动程序中,就可以使用MmGetSystemAddressForMdlSafe相关函数来操作MDL了。

如果我们需要自己使用MDL来共享内存,那么也可以使用IoAllocateMdl来创建并初始化一个DML,然后使用MmProbeAndLockPages锁定物理内存页。

你可能感兴趣的:(Windows驱动开发)