在驱动开发中,驱动程序访问应用程序数据缓冲区有三种方法三种方法:
buffered
方式中,I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个系统缓冲区工作。I/O管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。direct
方式中,I/O管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。因此你的驱动程序将使用MDL工作。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的结构和使用原理。
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模式,也可以理解成为共享内存模式,共享内存的方案如下:
从上图我们可以看到,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 |
与MmGetSystemAddressForMd l相同,但Windows 2000首选 |
MmInitializeMdl |
(再)初始化MDL以描述一个给定的虚拟缓冲区 |
MmPrepareMdlForReuse |
再初始化MDL |
MmProbeAndLockPages |
地址有效性校验后锁定内存页 |
MmSizeOfMdl |
取为描述一个给定的虚拟缓冲区的MDL所占用的内存大小 |
MmUnlockPages |
为该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);
//...
}
IoAllocateMdl
这个函数的作用是创建一个MDL结构,并把Irp->MdlAddress
设置为新创建MDL的地址,以后你将用到这个成员,并且I/O管理器最后也使用该成员来清除MDL。MmProbeAndLockPages
:该函数校验那个数据缓冲区是否有效,是否可以按适当模式访问;另外,该函数锁定了包含数据缓冲区的物理内存页,并在MDL的后面填写了页号数组。在效果上,一个锁定的内存页将成为非分页内存池的一部分,直到所有对该页内存加锁的调用者都对其解了锁。在我们的驱动程序中,就可以使用MmGetSystemAddressForMdlSafe
相关函数来操作MDL了。
如果我们需要自己使用MDL来共享内存,那么也可以使用IoAllocateMdl
来创建并初始化一个DML,然后使用MmProbeAndLockPages
锁定物理内存页。