一 MDL是什么
在MSDN中有这样的定义
内存描述符列表 (MDL) 是一个系统定义的结构,通过一系列物理地址描述缓冲区。执行直接 I/O 的驱动程序从 I/O 管理器接收一个 MDL 的指针,并通过 MDL 读写数据。一些驱动程序在执行直接 I/O 来满足设备 I/O 控制请求时也使用 MDL。
http://msdn.microsoft.com/zh-cn/windows/hardware/gg463193.aspx这里有完整的内容,但该文章是机器人翻译过来的,所以看起来有点头疼.
因此通俗的解释一下,MDL仅仅运用于内核中,在应用层并不会涉及这个结构,由于内核中的驱动有跟应用层程序通信的需要,因此可能会接收到来自进程空间的虚拟地址,而在windows的分页机制下,进程空间中的任何一个虚拟地址所属的页面都有可能被内存管理器从RAW置换到页文件中,或者,进程被释放或是取消地址的映射。这些都会导致严重的错误发生。因此内核创建一个MDL,并将其与来自进程空间的虚拟地址相关联,当需要对这些虚拟地址进行读写的时候调用相关的内核函数,锁定这些虚拟地址对应的物理页面和逻辑页面,防止物理页面被置换,逻辑页面被修改或者释放。
另外一种情况下一个驱动程序在执行纯内核任务中也可以使用MDL,特别的仅仅调用非分页内存的话,这些页面是不会置换到页文件中的,因此不需要考虑锁定页面的问题。
二 MDL的内容
先看看wdm.h中MDL的定义:
typedef __struct_bcount(Size) struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
先大概说明一下爱各个字段:
Next:MDL可以连接成一个单链表,因此可以将分散的虚拟机地址串接起来。
Size:一个MDL并不单单包含结构里这些东西,在内存中紧接着一个MDL结构,存着这个MDL对应的各个物理页面编号,由于一个物理页面一定是4KB对齐的,所以这个编号相当于一个物理页面起始地址的高20位。Size的值减去sizeof(MDL),等于存放编号的区域的大小。比如该MDL需要三个物理页面来映射虚拟地址空间,则Size-sizeof(MDL)==4*3==12;
MdlFlags:很重要的字段,用于描述和操控虚拟地址的各种属性。
Process:如果虚拟地址是某一进程的用户地址空间,那么MDL代表的这块虚拟地址必须是从属于某一个进程,这个成员指向从属进程的结构
MappedSystemVa:该MDL结构对应的物理页面可能被映射到内核地址空间,这个成员代表这个内核地址空间下的虚拟地址。对MmBuildMdlForNonPagedPool的逆向表明,MappedSystemVa=StartVa+ByteOffset。这是因为这个函数的输入MDL,其StartVa是由ExAllocatePoolWithTag决定的,所以已经从内核空间到物理页面建立了映射,MappedSystemVa自然就可以这样算。 可以猜测,如果是调用MmProbeAndLockPages 返回,则MappedSystemVa不会与StartVa有这样的对应关系,因为此时对应的物理页面还没有被映射到内核空间。(此处未定,MmProbeAndLockPages 是否会到PDE与PTE中建立映射,未知。)
StartVa:虚拟地址空间的首地址,当这块虚拟地址描述的是一个用户进程地址空间的一块时,这个地址从属于某一个进程。
ByteCount:虚拟地址块的大小,字节数
ByteOffset:StartVa+ByteCount等于缓冲区的开始地址
由于WDK文档中表述得比较模糊,上面的说明有些仅仅是猜测。我们可以通过DBG调试一个驱动,观察它的内存原始数据来证实我们的推测。
下面是取自tdifw中的一段代码,它为一个IPV4地址ctx->tai 分配一个非分页内存块,然后调用IoAllocateMdl创建一个针对这个虚拟地址的MDL,最后调用MmBuildMdlForNonPagedPool来建立虚拟地址与物理页面直接的映射。
ctx->tai = (TDI_ADDRESS_INFO *)malloc_np(TDI_ADDRESS_INFO_MAX);
if (ctx->tai == NULL) {
KdPrint(("[tdi_fw] tdi_create_addrobj_complete: malloc_np!\n"));
status = STATUS_INSUFFICIENT_RESOURCES;
goto done;
}
可以看到tai的首地址是0x82040928,这是一个非分页内存中的虚拟地址,长度是0x55。
mdl = IoAllocateMdl(ctx->tai, TDI_ADDRESS_INFO_MAX, FALSE, FALSE, NULL);
if (mdl == NULL) {
KdPrint(("[tdi_fw] tdi_create_addrobj_complete: IoAllocateMdl!\n"));
status = STATUS_INSUFFICIENT_RESOURCES;
goto done;
}
分配MDL之后的内存:
其中MDL的首地址是0x82010210,size是32,这是MDL结构本身的大小,mdlflags是8,mappedsystemva的值是0xf8c9oa9c
startva是0x82040000,这说明startva目前表示的是tai所指向的虚拟地址的页起始地址,bytecount是55,这代表了虚拟地址的大小,byteoffset是0x929,因此这个字节表示的是虚拟地址相对于页的偏移地址。
MmBuildMdlForNonPagedPool(mdl);
调用此函数之后:
仅有3个字段发生了变化
mdlflags变成了12,mappedsystemva真正指向了虚拟地址,process被置0,说明这是一个非分页地址,它不属于任何一个进程的地址空间。
实际上size的大小并不等于MDL结构的大小,因为在MDL后面紧跟着一个表示物理页面的数组,只是没有在结构体中表现出来,这应该是为了避免一般的驱动程序直接修改这些物理数组,因为虚拟地址和物理页面的映射只应该由内存管理器来维护。在刚才的调试中观察内存发现,在MDL后面只有一个物理页面编号。此编号在调用IoAllocateMdl的时候并未初始化,而是在 MmBuildMdlForNonPagedPool(mdl)中被赋的值。有些人认为 MmBuildMdlForNonPagedPool是把物理页面映射到系统地址空间中,这种说法应该是错误的,因为对于非分页内存,在调用ExAllocatePool系列函数的时候,内存管理器就建立了映射关系,否则这些内存根本无法使用,实际上, MmBuildMdlForNonPagedPool的作用是把这种映射保存到MDL中,使其变得不透明,以满足某些驱动的需求。
三,MDL的使用
典型的,当运行在内核中的一个驱动向另一个驱动发送请求的时候,其中一种数据传输方式将运用到MDL。
首先调用IoAllocateMdl对你需要传递的数据生成一个MDL,它会返回一个MDL结构的指针,然后调用MmBuildMdlForNonPagedPool来更新MDL的内容,最后把这个MDL指针传递给IRP中的MdlAddress成员。
顺便说一下,我们不可以直接访问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解锁内存页 |
对于I/O管理器执行的Direct方式的读写操作,其过程可以想象为下面代码:
KPROCESSOR_MODE mode; // either KernelMode or UserMode PMDL mdl = IoAllocateMdl(uva, length, FALSE, TRUE, Irp); MmProbeAndLockPages(mdl, mode, reading ? IoWriteAccess : IoReadAccess); <code to send and await IRP> MmUnlockPages(mdl); ExFreePool(mdl); |
I/O管理器首先创建一个描述用户缓冲区的MDL。IoAllocateMdl的第三个参数(FALSE)指出这是一个主数据缓冲区。第四个参数(TRUE)指出内存管理器应把该内存充入进程配额。最后一个参数(Irp)指定该MDL应附着的IRP。在内部,IoAllocateMdl把Irp->MdlAddress设置为新创建MDL的地址,以后你将用到这个成员,并且I/O管理器最后也使用该成员来清除MDL。
这段代码的关键地方是调用MmProbeAndLockPages(以粗体字显示)。该函数校验那个数据缓冲区是否有效,是否可以按适当模式访问。如果我们向设备写数据,我们必须能读缓冲区。如果我们从设备读数据,我们必须能写缓冲区。另外,该函数锁定了包含数据缓冲区的物理内存页,并在MDL的后面填写了页号数组。在效果上,一个锁定的内存页将成为非分页内存池的一部分,直到所有对该页内存加锁的调用者都对其解了锁。
在Direct方式的读写操作中,对MDL你最可能做的事是把它作为参数传递给其它函数。例如,DMA传输的MapTransfer步骤需要一个MDL。另外,在内部,USB读写操作总使用MDL。所以你应该把读写操作设置为DO_DIRECT_IO方式,并把结果MDL传递给USB总线驱动程序。
顺便提一下,I/O管理器确实在stack->Parameters联合中保存了读写请求的长度,但驱动程序应该直接从MDL中获得请求数据的长度。
ULONG length = MmGetMdlByteCount(mdl); |