Windows驅動之MDL
在驅動開發中,驅動程序訪問應用程序數據緩衝區有三種方法三種方法:
- 在
buffered
方式中,I/O管理器先創建一個與用戶模式數據緩衝區大小相等的系統緩衝區。而你的驅動程序將使用這個系統緩衝區工作。I/O管理器負責在系統緩衝區和用戶模式緩衝區之間複製數據。 - 在
direct
方式中,I/O管理器鎖定了包含用戶模式緩衝區的物理內存頁,並創建一個稱爲MDL(內存描述符表)的輔助數據結構來描述鎖定頁。因此你的驅動程序將使用MDL工作。 - 在
neither
方式中,I/O管理器僅簡單地把用戶模式的虛擬地址傳遞給你。而使用用戶模式地址的驅動程序應十分小心。
其中緩衝模式指定的代碼如下:
NTSTATUS AddDevice(...)
{
PDEVICE_OBJECT fdo;
IoCreateDevice(..., &fdo);
fdo->Flags |= DO_BUFFERED_IO; //buffered 模式
fdo->Flags |= DO_DIRECT_IO; //direcr 模式
fdo->Flags |= 0; //neither 模式
}
本文我們來探討一下MDL的結構和使用原理。
1. MDL結構
MDL是一個結構體,保存着一個需要通過MDL來共享訪問一段內存的信息,這個結構體定義如下:
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
有一個初始化的宏,可以比較明確的看出每個成員的作用:
#define BYTE_OFFSET(Va) \
((ULONG) ((ULONG_PTR) (Va) & (PAGE_SIZE - 1)))
#define PAGE_ALIGN(Va) \
((PVOID) ((ULONG_PTR)(Va) & ~(PAGE_SIZE - 1)))
#define MmInitializeMdl(_MemoryDescriptorList, \
_BaseVa, \
_Length) \
{ \
(_MemoryDescriptorList)->Next = (PMDL) NULL; \
(_MemoryDescriptorList)->Size = (CSHORT) (sizeof(MDL) + \
(sizeof(PFN_NUMBER) * ADDRESS_AND_SIZE_TO_SPAN_PAGES(_BaseVa, _Length))); \
(_MemoryDescriptorList)->MdlFlags = 0; \
(_MemoryDescriptorList)->StartVa = (PVOID) PAGE_ALIGN(_BaseVa); \
(_MemoryDescriptorList)->ByteOffset = BYTE_OFFSET(_BaseVa); \
(_MemoryDescriptorList)->ByteCount = (ULONG) _Length; \
}
其中:
Size
: 表示結構體的大小。MappedSystemVa
: 映射的系統地址。StartVa
: 成員給出了用戶緩衝區的虛擬地址,這個地址僅在擁有數據緩衝區的用戶模式進程上下文中才有效。ByteCount
: 是緩衝區的字節長度。ByteOffset
: 是緩衝區起始位置在一個頁幀中的偏移值。Pages
: 數組沒有被正式地聲明爲MDL結構的一部分,在內存中它跟在MDL的後面,包含用戶模式虛擬地址映射爲物理頁幀的個數。
在這裏有個奇怪的成員就是Pages
, 這個成員在MDL
中並沒有定義出來,但是被真實的使用了,那麼這個是幹什麼用的呢?想要明白這個東西,那麼需要先掌握一個東西,MDL
是怎麼樣使用Direct 模式的呢?
其實Direct模式,也可以理解成爲共享內存模式,共享內存的方案如下:
從上圖我們可以看到,StartVa
虛擬內存對應的物理內存映射表放在了Pages
中,普通情況下,內存尋址都是通過CR3尋找PDE,然後在通過PDE,PTE查找到物理內存。但是在MDL中,我們通過MDL後面的Pages
查找物理內存,並且兩個物理內存是一樣的,這樣就無需考慮數據了。
Windows對於MDL提供了宏和訪問函數
宏或函數 | 描述 |
---|---|
IoAllocateMdl |
創建MDL |
IoBuildPartialMdl |
創建一個已存在MDL的子MDL |
IoFreeMdl |
銷燬MDL |
MmBuildMdlForNonPagedPool |
修改MDL以描述內核模式中一個非分頁內存區域 |
MmGetMdlByteCount |
取緩衝區字節大小 |
MmGetMdlByteOffset |
取緩衝區在第一個內存頁中的偏移 |
MmGetMdlVirtualAddress |
取虛擬地址 |
MmGetSystemAddressForMdl |
創建映射到同一內存位置的內核模式虛擬地址 |
MmGetSystemAddressForMdlSafe |
與MmGetSystemAddressForMd l相同,但Windows 2000首選 |
MmInitializeMdl |
(再)初始化MDL以描述一個給定的虛擬緩衝區 |
MmPrepareMdlForReuse |
再初始化MDL |
MmProbeAndLockPages |
地址有效性校驗後鎖定內存頁 |
MmSizeOfMdl |
取爲描述一個給定的虛擬緩衝區的MDL所佔用的內存大小 |
MmUnlockPages |
爲該MDL解鎖內存頁 |
2. MDL的使用
對於WriteFile
的Direct方式,有如下代碼:
NTSTATUS
NTAPI
NtWriteFile(IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL)
{
//...
Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
//...
Mdl = IoAllocateMdl(Buffer, Length, FALSE, TRUE, Irp);
MmProbeAndLockPages(Mdl, PreviousMode, IoReadAccess);
//...
}
- 對於
IoAllocateMdl
這個函數的作用是創建一個MDL結構,並把Irp->MdlAddress
設置爲新創建MDL的地址,以後你將用到這個成員,並且I/O管理器最後也使用該成員來清除MDL。 MmProbeAndLockPages
:該函數校驗那個數據緩衝區是否有效,是否可以按適當模式訪問;另外,該函數鎖定了包含數據緩衝區的物理內存頁,並在MDL的後面填寫了頁號數組。在效果上,一個鎖定的內存頁將成爲非分頁內存池的一部分,直到所有對該頁內存加鎖的調用者都對其解了鎖。
在我們的驅動程序中,就可以使用MmGetSystemAddressForMdlSafe
相關函數來操作MDL了。
如果我們需要自己使用MDL來共享內存,那麼也可以使用IoAllocateMdl
來創建並初始化一個DML,然後使用MmProbeAndLockPages
鎖定物理內存頁。