Windows驱动跑在核心态(Kernel mode),驱动的调用者跑在用户态。如何使用户态进程与核心态驱动共享内存呢 ?
我们知道32位Windows中,默认状态下虚拟空间有4G,前2G是每个进程私有的,也就是说在进程切换的时候会变化,后2G是操作系统的,所以是固定的。既然用户态进程和核心态驱动在同一个进程空间里,是不是只要直接传个内存地址过来,就可以访问了?理论上可以但实际上不行,因为用户态的进程在不断地切换,使驱动运行时没法保证前面的用户态进程是哪个,也就不确定前2G虚拟地址空间的映射情况,那么用户态进程传来的地址也许不是合法的。
比较常用的做法是通过MDL进行内存的重映射。简单地说就是将同一块物理内存同时映射到用户态空间和核心态空间。
具体来说,可以有两种做法:用户态进程分配空间,内核态去映射。另一种是内核态分配空间,用户态进程去映射。
前者伪码:
// assume uva is a virtual address in user space, uva_size is its size
MDL * mdl = IoAllocateMdl(uva, uva_size, FALSE, FALSE, NULL);
ASSERT(mdl);
__try {
MmProbeAndLockPages(mdl, UserMode, IoReadAccess);
} __except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("error code = %d", GetExceptionCode);
}
PVOID kva = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);
// use kva
// …
MmUnlockPages(mdl);
IoFreeMdl(mdl);
*记得在driver unload之前把mdl unlock和free掉,否则会BSoD。
后者伪码:
PVOID kva = ExAllocatePoolWithTag(NonPagedPool, 1024, (ULONG)'PMET');
MDL * mdl = IoAllocateMdl(uva, uva_size, FALSE, FALSE, NULL);
ASSERT(mdl);
__try {
MmBuildMdlForNonPagedPool(mdl);
} __except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("error code = %d", GetExceptionCode);
}
PVOID uva = MmMapLockedPagesSpecifyCache(mdl, UserMode, MmCached, NULL, FALSE, NormalPagePriority);
*如果kva是分配在nonpagedpool,那这些物理页本身就是被lock住的,因此用的是MmBuildMdlForNonPagedPool,如果是分配在paged pool里的用MmProbeAndLockPages。
除了这种最原始的方式,Windows还提供了两种称为DO_BUFFERED_IO 和DO_DIRECT_IO的方式,前者中系统自动将用户态空间内存拷贝到了到核心态空间(Associated-Irp.SystemBuffer),后者由系统自动生成MDL(Irp->MdlAddress)。其实这两种方法本质都是系统帮忙做了上面的部分流程,从而可以让程序员省了那些操作。
前面提到了一个关键数据结构MDL(memorydescriptor list ),系统用它来描述虚拟空间对应物理内存的layout。MDL分为两部分:固定长部分和变长部分,固定长部分结构如下:
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
Next: 指向下一个MDL结构,从而构成链表,有时一个IRP会包含多个MDL
Size:MDL本身的大小,注意包含了定长部分和变长两部分的size
MdlFlags:属性标记,如所描述的物理页有没有被lock住等
Process:顾名思义,指向该包含该虚拟地址的地址空间的对应进程结构
MappedSystemVa:内核态空间中的对应地址
StartVa:用户或者内核地址空间中的虚拟地址,取决于在哪allocate的,该值是页对齐的
ByteCount:MDL所描述的虚拟地址段的大小,byte为单位
ByteOffset:起始地址的页内偏移,因为MDL所描述的地址段不一定是页对齐的
如allocate出来的虚拟地址为0xac004010,则StartVa为0xac004000,ByteOffset为0x10,MmGetMdlVirtualAddress给出StartVa + ByteOffset。
变长部分包含了物理页编号数组,可以用
PPFN_NUMBER pfn = MmGetMdlPfnArray(mdl)
来得到,注意里面只包含了pfn,不包含页内偏移量。数组的元素个数可以由ADDRESS_AND_SIZE_TO_SPAN_PAGES得到。
jpg 改 rar