Windows驅動跑在核心態(Kernel mode),驅動的調用者跑在使用者態。如何使使用者態進程與核心態驅動共用記憶體呢
?
我們知道32位Windows中,預設狀態下虛擬空間有4G,前2G是每個進程私人的,也就是說在進程切換的時候會變化,後2G是作業系統的,所以是固定的。既然使用者態進程和核心態驅動在同一個進程空間裡,是不是只要直接傳個記憶體位址過來,就可以訪問了?理論上可以但實際上不行,因為使用者態的進程在不斷地切換,使驅動運行時沒法保證前面的使用者態進程是哪個,也就不確定前2G虛擬位址空間的映射情況,那麼使用者態進程傳來的地址也許不是合法的。
比較常用的做法是通過MDL進行記憶體的重新對應。簡單地說就是將同一塊實體記憶體同時映射到使用者態空間和核心態空間。
具體來說,可以有兩種做法:使用者態進程分配空間,核心態去映射。另一種是核心態分配空間,使用者態進程去映射。
前者偽碼:
// assume uva is a virtual address in user space, uva_size is its sizeMDL * 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得到。