Tuesday, September 04, 2007

Windows Driver programming[3]

MDL (memory descriptor list)

Programming the microsoft windows driver model 對於 MDL 的描述不算多,它只有在需要的時候才會提出一些操作 MDL 的 function。 相信大多數人在對於 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;

MDL 只是一個對實體記憶體的描述,但是因為系統跟 Driver 都是使用虛擬記憶體,所以 MDL 就是把虛擬記憶體『映射』到實體記憶體(from DDK)。

這樣講是很模糊的,其實 MDL 的作用很簡單:當 Driver 要存取某個記憶體位置時,確保 MDL 所描述的記憶體位置不會引起 page fault。 為甚麼?因為『虛擬記憶體』的關係。Driver 存取的是虛擬記憶體,所以當系統丟給 Driver 一塊虛擬記憶體位置,而 Driver 如何知道這塊記憶體所代表的資料到底是在 RAM 還是 Disk(被 paged out) 內?

在 NDIS 裡面,Driver 是不需要去關心系統丟給 Driver 的記憶體到底是如何,反正一定是在 RAM 裡面就是了。但是在 I/O Control 內的 Direct I/O method 裡面, 這可不一定,因為這個記憶體位置可能是指到某個應用程式的虛擬記憶體,而應用程式的虛擬記憶體是非常有可能被 paged out。 所以 Driver 得呼叫 MmGetSystemAddressForMdlSafe() 來鎖定這塊記憶體(系統得把把資料由 Disk 載入到 RAM)。 寫個小程式就可以證明這一點:

PVOID    buf = NULL;
PMDL     mdl = NULL;
NDIS_STATUS    ndis_status;

ndis_status = NdisAllocateMemoryWithTag(&buf, 1000, 0x1111L);
if (ndis_status != NDIS_STATUS_SUCCESS){
    DbgPrint("NetVMini : Can not allocate test Memory for MDL.\n");
} else {
    mdl = IoAllocateMdl(buf, 1000, FALSE, FALSE, NULL);
    if (mdl != NULL) {
        MmBuildMdlForNonPagedPool(mdl);
        DbgPrint("buf = 0x%p\n", buf);
        DbgPrint("MDL.Size = 0x%X\n", mdl->Size);
        DbgPrint("MDL.MdlFlags = 0x%X\n", mdl->MdlFlags);
        DbgPrint("MDL.MappedSystemVa = 0x%p\n", mdl->MappedSystemVa);
        DbgPrint("MDL.StartVa = 0x%p\n", mdl->StartVa);
        DbgPrint("MDL.ByteCount = 0x%X\n", mdl->ByteCount);
        DbgPrint("MDL.ByteOffset = 0x%X\n", mdl->ByteOffset);
        IoFreeMdl(mdl);
        mdl = NULL;
    }
    NdisFreeMemory(buf, 1000, 0);
}

這個程式只是配置一塊 non-paged 記憶體,然後用一個 MDL 來描述它。 下面是程式的結果:

    00000008    0.00103058    buf = 0x85322008   
    00000009    0.00104175    MDL.Size = 0x20   
    00000010    0.00104985    MDL.MdlFlags = 0xC   
    00000011    0.00106243    MDL.MappedSystemVa = 0x85322008   
    00000012    0.00107919    MDL.StartVa = 0x85322000   
    00000013    0.00109120    MDL.ByteCount = 0x3E8   
    00000014    0.00110321    MDL.ByteOffset = 0x8    

buf = MappedSystemVa = StartVa + ByteOffset. Eureka!

看起來直接用 NdisAllocateMemoryWithTag() 所得到指標還比較好? MDL 還是有好處的啦,至少它可以串起一堆 MDL,當系統需要給 Driver 一堆記憶體區塊,而這些記憶體區塊全都是不連續的,那 MDL 就會很好用(NDIS_BUFFER 就是如此)。 而且在 NDIS 6.0 裡面,NET_BUFFER 只能使用 MDL。

跟 MDL 相關的函式有:

PMDL IoAllocateMdl(
    IN PVOID VirtualAddress,
    IN ULONG Length,
    IN BOOLEAN SecondaryBuffer,
    IN BOOLEAN ChargeQuota,
    IN OUT PIRP Irp OPTIONAL
    );

VirtualAddress:這個 MDL 所描述的虛擬記憶體位置。
Length:這個 MDL 所描述的記憶體大小。

DDK 裡面特別強調,如果 Driver 希望建立的 MDL 是映射到 Driver 自己配置的 Non-Paged 記憶體的話,Driver 還得呼叫 MmBuildMdlForNonPagedPool()。 這是因為 IoAllocateMdl() 只有配置記憶體,但是並沒有 Build MDL(好模糊的說法)。 NDIS 6.0 有提供 NdisAllocateMdl(),它提供一氣呵成的服務。

VOID MmBuildMdlForNonPagedPool(
    IN OUT PMDL MemoryDescriptorList
    );

The MmBuildMdlForNonPagedPool routine receives an MDL that specifies a virtual memory buffer in nonpaged pool, and updates it to describe the underlying physical pages. The MDL virtual address that is input must be within the nonpaged portion of system space, such as memory allocated by ExAllocatePoolWithTag with PoolType = NonPagedPool.

VOID IoBuildPartialMdl(
    IN PMDL SourceMdl,
    IN OUT PMDL TargetMdl,
    IN PVOID VirtualAddress,
    IN ULONG Length
    );
VOID IoFreeMdl(
    IN PMDL Mdl
    );
ULONG MmGetMdlByteCount(IN PMDL  Mdl){
    return Mdl->ByteCount;
}

回傳這個 MDL 的大小。

ULONG MmGetMdlByteOffset(IN PMDL  Mdl) {
    return Mdl->ByteOffset;
}

回傳這個 MDL 離 StarVa 有多遠。

PVOID MmGetMdlVirtualAddress(IN PMDL  Mdl) {
    return ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset));
}

取得 MDL 的虛擬記憶體位置。DDK 特別講明,Lower-Level Driver 不可以直接把這個 Address 拿來使用,因為這有可能是 user-space 的記憶體位置。因此,Driver 必須呼叫 MmGetSystemAddressForMdlSafe() 來取得並鎖定這個 Address 所對應到的 system-space 的記憶體位置。

PVOID MmGetSystemAddressForMdlSafe(IN PMDL Mdl, IN MM_PAGE_PRIORITY Priority) {
    if (Mdl->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA | MDL_SOURCE_IS_NONPAGED_POOL)) {
        return Mdl->MappedSystemVa;
    } else {
        return MmMapLockedPagesSpecifyCache(Mdl, KernelMode, MmCached, NULL, FALSE, Priority);
    }
}

這個函式是個 Macro。程式碼很簡單,如果這個 MDL 已經被鎖定並映射,直接回傳 MappedSystemVa,否則就鎖定並映射它。

VOID MmInitializeMdl(IN PMDL  MemoryDescriptorList, IN PVOID  BaseVa, IN SIZE_T  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);
}

這段程式碼已經對 MDL 說明的很清楚了。MDL 的目的是要串起多個分散的記憶體,而 MnInitializeMdl() 只是串起第一個記憶體位置。

VOID MmPrepareMdlForReuse(IN PMDL  MDL) {
    if ((MDL->MdlFlags & MDL_PARTIAL_HAS_BEEN_MAPPED) != 0) {
        MmUnmapLockedPages( MDL->MappedSystemVa, MDL );
    }
}

Very few drivers call this routine (DDK says).

VOID MmProbeAndLockPages(
    IN OUT PMDL  MemoryDescriptorList,
    IN KPROCESSOR_MODE AccessMode,
    IN LOCK_OPERATION Operation
    );

這似乎是用在 Direct IO method 上。它會映射並鎖定 MDL所描述的虛擬記憶體。

ULONG MmSizeOfMdl(
    IN PVOID Base,
    IN SIZE_T Length
    );

The MmSizeOfMdl routine returns the number of bytes to allocate for an MDL describing a given address range.

VOID MmUnmapLockedPages(
    IN PVOID BaseAddress,
    IN PMDL  MemoryDescriptorList
    );

解除 MmProbeAndLockPages() 或 MmMapLockedPagesSpecifyCache() 所造成的影響。

No comments:

codeblock