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映射到用户态地址空间去,用户态程序甚至可以直接读取内核态的数据。