WDK tips (10) MDL是描述内存的列表(memory descriptor list)

先讲个题外话,不知是出于炫耀的目的还是什么恶趣味作怪,程序员给自己的项目起名字时喜欢玩一些“递归”的小把戏,比如GNU的全称是GNU is Not Unix,再比如WINE的全称是WINE Is Not Emulator。这种小玩笑在开源界很常见,但是微软以及跟着微软混的人则很少这么干,MSND这种严肃古板的产品文档上更是不太可能出现。MDL却是个例外,我们来看看MSDN是怎么描述MDL的:

MDL
An MDL structure is a partially-opaque structure that represents a memory descriptor list (MDL).

MDL是一种MDL,跟没说一样。这句话给我的感受是这样的:一个阳光明媚的星期天下午我哼着小曲愉快的去加班,老板往我脸上摔了一坨需求说:客户就是要MDL没MDL他们不给钱赶紧给我去搞定它,我怯生生的问:啥是MDL啊我是文盲我不懂,这时老板怒气值满点给我发了个大招:MDL意思就是MDL啦这都不懂星期一打包走人!看到这么戏剧性的场面我只能说:导播给我左脸画三条黑线的特效。

言归正传。那么如果让我来解释什么叫MDL,我会怎么说呢?我会这么说:

MDL

MDL提供了一套机制,程序员可以手动的将一段连续虚拟地址与一段物理内存地址绑定在一起。

显而易见的,在了解MDL之前你需要有虚拟内存相关的背景知识,你得知道虚拟地址是怎么映射到物理地址上的,这部分内容有点大我们放到下次讲,但是有两个原则你需要现在就知道:1. 从虚拟地址大物理地址的翻译由操作系统接管,对程序员是透明的。2. 物理内存的地址空间可以比虚拟内存的地址空间小,虚拟内存对应的内容并不一定在物理内存里,并且虚拟地址映射到的物理地址是可变,这次在这里,下次就跑别的地方去了。MDL的出现打破了以上两个原则,当你用MDL将某段地址绑定好之后,1. 虚拟地址到屋里地址的翻译对程序员就变得不透明了 2. 这段虚拟内存里的内容一定会在物理内存中(也就是不会page out),并且所在位置也不会变。

MDL只能在内核态使用,但它指定的虚拟内存即可以是内核态地址也可以是用户态地址。如果是用户态的地址你必须要自行弄清地址所在的进程上下文,因为不同的进程拥有不同的地址空间,即使地址的值一模一样它们包含的数据也一定完全不同。如果是内核态的地址那么事情会变的稍微简单点,因为在内核态地址空间是共享的,同一个地址里包含的数据一定是一样的。

MDL本身的结构在DDK里有写,但它属于undocument结构,也就是说微软想改就改不需要事先通知你,所以你最好不要对它做任何假设。不过看一眼当然是没有问题的,又不会怀孕。以下就是MDL数据结构的定义:

// An MDL describes pages in a virtual buffer in terms

// of physical pages. The pages associated with the

// buffer are described in an array that is allocated

// just after the MDL header structure itself.

//

// One simply calculates the base of the array by

// adding one to the base MDL pointer:

//

// Pages = (PPFN_NUMBER) (Mdl + 1);

//

// Notice that while in the context of the subject

// thread, the base virtual address of a buffer mapped

// by an MDL may be referenced using the following:

//

// Mdl->StartVa | Mdl->ByteOffset

//

typedef struct _MDL {

    struct _MDL *Next;

    CSHORT Size;

    CSHORT MdlFlags;

    struct _EPROCESS *Process;

    PVOID MappedSystemVa;

    PVOID StartVa;

    ULONG ByteCount;

    ULONG ByteOffset;

} MDL, *PMDL;

从注释中我们可以看到MDL实际上是一个变长数据结构,在这个结构后面会跟一个数组,把映射到的物理内存的地址都记录下来。而虚拟地址的信息则记录在StartVa中,ByteCount表征它的大小,ByteOffset表征它在页内的偏移。想获得正确的虚存地址,你得用类似 Mdl->StartVa | Mdl->ByteOffset 的方法去获得。想要使用MDL之前你必须先申请一个MDL数据结构。如上面所说MDL是一个变长结构,你不能光申请一个struct _MDL就完事了,你需要自行计算后面所跟数组的大小,以及里面各个域的值,考虑到还有页对齐等一系列恶心烦人的事,我建议你不要手动创建struct _MDL,而是用IoAllocateMdl函数来帮你做这些事情。IoAllocateMdl函数的定义如下:

PMDL

IoAllocateMdl(

IN PVOID VirtualAddress,

IN ULONG Length,

IN BOOLEAN SecondaryBuffer,

IN BOOLEAN ChargeQuota,

IN OUT PIRP Irp OPTIONAL

);

第一个参数为虚存地址;第二个参数为虚存大小;第三个参数与最后一个参数配合使用,如果你在调用IoAllocateMdl时指定了一个IRP,并且SecondaryBuffer为TRUE,那么这个函数会自动把新生成的MDL附加到IRP的MDL列表的最后,如果指定了IRP并且SecondaryBuffer为FALSE那么这个函数会把Irp->MdlAddress设置为新生成的MDL;ChangeQuota一般为FALSE,只有那些会生出新的IRP并往下传的顶层driver才会把它置为TRUE.

值得注意的是IoAllocateMdl函数就跟它的名字一样只负责分配数据结构所需的内存,真正把虚存和物理内存绑在一起的工作它是不负责的,后续工作由另外一批函数负责,比如检测权限和锁定物理内存不让别人占用等事情就由MmProbeAndLockPages函数来完成。这个函数的定义如下:

VOID

MmProbeAndLockPages (

__inout PMDL MemoryDescriptorList,

__in KPROCESSOR_MODE AccessMode,

__in LOCK_OPERATION Operation

);

第一个参数为刚刚生成的MDL,第二个参数指定是用户态的虚存还是内核态的虚存,第三个参数指定访问权限,有IoReadAccess, IoWriteAccess, 和 IoModifyAccess 三种类型可选(事实上 IoWriteAccess和IoModifyAccess 是一模一样的...)。

聪明的同学已经发现问题了:所谓检测权限,那必然是有成功有失败,这个函数怎么不返回任何错误码呢,难道这些个AccessMode的参数传进去都是装装样子的,其实什么事也没做?答案是在权限匹配失败的情况下,MmProbeAndLockPages函数会抛异常,你必须用__try __except之类的SEH关键字给它包起来。在这里我又不得不吐下槽了:真的有好好设计过吗你们?考虑到MSDN关于这块内容的文档质量,再加上这些奇葩的API设计,我怀疑这堆东西根本就是后面打补丁打上去的,而且项目截止日期一定是国庆长假前一天。

事情做到这儿一般来讲已经差不多了,你已经获得了一块永远不会被page out,永远跟指定的虚拟内存一一对应的物理内存。good job!祝你用的开心。假如你生性事多“一般”情况满足不了你(对不起我不该这么说,因为需求永远是多变的),下面还有个函数可以给你点新内容:MmMapLockedPagesSpecifyCache函数可以让你从指定的MDL里生成出新的虚拟地址空间来。假设你的MDL在某个进程上下文里生产,但主要使用场所却是别的进程或是没有进程上下文的地方(比如DPC,内核线程等),那么这个函数会很有用,因为进程切换后或者压根没进程的话,同一个虚拟地址代表的内容是不一样的。

MmMapLockedPagesSpecifyCache函数的定义如下

PVOID

MmMapLockedPagesSpecifyCache (

__in PMDL MemoryDescriptorList,

__in KPROCESSOR_MODE AccessMode,

__in MEMORY_CACHING_TYPE CacheType,

__in_opt PVOID RequestedAddress,

__in ULONG BugCheckOnFailure,

__in MM_PAGE_PRIORITY Priority

);

大家也看到了,返回值是PVOID类型,也就是新的虚拟地址,如果你指定了RequestedAddress,那么返回值跟这个参数应该是一样的,当然,系统也可能没办法满足你指定的地址,在这种情况下假如BugCheckOnFailure参数为TRUE,那么系统就立刻BSOD了。AccessMode可以指定为KernelMode或者UserMode,如果是UserMode,那么该函数会把MDL映射到用户态地址空间去,用户态程序甚至可以直接读取内核态的数据。

以上就是跟MDL有关的大致内容,我有意忽略了大部分跟虚拟内存管理有关的内容(要么直接不讲,要么含糊其词,其实你已经看出来了...),如前面所言,这部分内容我们下次再讲。

 

posted @ 2013-01-02 01:43  gussing  阅读(3104)  评论(0编辑  收藏  举报