一 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); |