Windows驅動之MDL

Windows驅動之MDL

在驅動開發中,驅動程序訪問應用程序數據緩衝區有三種方法三種方法:

  1. buffered方式中,I/O管理器先創建一個與用戶模式數據緩衝區大小相等的系統緩衝區。而你的驅動程序將使用這個系統緩衝區工作。I/O管理器負責在系統緩衝區和用戶模式緩衝區之間複製數據。
  2. direct方式中,I/O管理器鎖定了包含用戶模式緩衝區的物理內存頁,並創建一個稱爲MDL(內存描述符表)的輔助數據結構來描述鎖定頁。因此你的驅動程序將使用MDL工作。
  3. 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 MmGetSystemAddressForMdl相同,但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);
    //...
}
  1. 對於IoAllocateMdl這個函數的作用是創建一個MDL結構,並把Irp->MdlAddress設置爲新創建MDL的地址,以後你將用到這個成員,並且I/O管理器最後也使用該成員來清除MDL。
  2. MmProbeAndLockPages :該函數校驗那個數據緩衝區是否有效,是否可以按適當模式訪問;另外,該函數鎖定了包含數據緩衝區的物理內存頁,並在MDL的後面填寫了頁號數組。在效果上,一個鎖定的內存頁將成爲非分頁內存池的一部分,直到所有對該頁內存加鎖的調用者都對其解了鎖。

在我們的驅動程序中,就可以使用MmGetSystemAddressForMdlSafe相關函數來操作MDL了。

如果我們需要自己使用MDL來共享內存,那麼也可以使用IoAllocateMdl來創建並初始化一個DML,然後使用MmProbeAndLockPages鎖定物理內存頁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章