浅谈MDL(壹)(Windows内核学习笔记)
内存描述符表(MDL)是一种用于在内核态管理内存映射的结构体。它通过物理页帧号数组描述虚拟内存范围,并支持锁定特定页面以避免不可预测的地址映射问题。MDL在Win7和Windows 10中实现了类似的功能,但细节略有不同。使用MDL可以解决线程调度导致的页面不可交换问题,并支持对用户态或内核态虚拟地址进行映射。尽管建立和撤销MDL有一定开销,但对于较大缓冲区来说是有效的方法之一。相关函数包括IoAllocateMdl用于分配并初始化MDL,以及MmInitializeMdl用于配置其属性等。
当爱和责任合二为一,则恩典便与你同在。
先来了解下MDL是什么吧。查看MSDN官方定义:
//Win10下的定义
//0x1c bytes (sizeof)
struct _MDL
{
struct _MDL* Next; //0x0 MDL队列中的下一个成员
SHORT Size; //0x4 整个MDL列表的长度,包括MDL结构体和物理页帧号所占的内存 物理页帧号起始地址紧跟在结构体后
SHORT MdlFlags; //0x6 标志,设置一些内存属性
struct _EPROCESS* Process; //0x8 缓冲区所属进程
VOID* MappedSystemVa; //0xc 映射之后的系统空间虚拟地址
VOID* StartVa; //0x10 缓冲区所在第一个页面的虚拟地址
ULONG ByteCount; //0x14 字节数,虚拟地址的大小
ULONG ByteOffset; //0x18 StartVa + ByteOffset缓冲区开始的地址
};
//Win7的Windbg版本
0: kd> dt nt!_MDL
+0x000 Next : Ptr32 _MDL
+0x004 Size : Int2B
+0x006 MdlFlags : Int2B
+0x008 Process : Ptr32 _EPROCESS
+0x00c MappedSystemVa : Ptr32 Void
+0x010 StartVa : Ptr32 Void
+0x014 ByteCount : Uint4B
+0x018 ByteOffset : Uint4B
好像在Win7和Win下的定义,并没有什么变化。。。
为什么要使用MDL?
我们都知道在内核中运行的代码,可以访问用户层的内存空间,但是当线程调度发生,当前的页面映射表不再是之前的用户层进程的页面映射,但是CPU仍然访问原本的用户层虚拟地址,就会发生不可预知的错误,因为用户层的低2G虚拟空间空间是独立的。也就是说,我们访问到的地址可能并不是真正的目标地址。解决办法之一,就是使用MDL映射一份对应的物理内存到系统空间的虚拟地址中来,并且被MDL锁定之后,页面不会被换出。MDL只能在内核态使用,它可以指定对内核虚拟地址或者用户虚拟地址的映射。
接下来说一下MDL的使用:
对于很小的缓冲区来说,使用MDL不太划算,毕竟建立和撤销一个新的映射需要一定的开销,对于大一点的缓冲区可以使用。
之前我说过的一个代码,用的是内核态映射用户态的虚拟地址,使用MDL进行缓冲区数据拷贝。看一些常用的MDL操作函数:
//该宏未初始化MappedSystemVa,因为还没有进行映射
#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; \
}
PMDL
NTAPI
IoAllocateMdl(IN PVOID VirtualAddress,
IN ULONG Length,
IN BOOLEAN SecondaryBuffer,
IN BOOLEAN ChargeQuota,
IN PIRP Irp)
{
PMDL Mdl = NULL, p;
ULONG Flags = 0;
ULONG Size;
/*断言长度有效*/
ASSERT(Length != 0);
/*超过地址空间的一半,肯定无法映射,因为MDL存在于内核中,且不能被换出*/
if (Length & 0x80000000) return NULL;
/*计算缓冲区跨过的页面数量*/
Size = ADDRESS_AND_SIZE_TO_SPAN_PAGES(VirtualAddress, Length);
if (Size > 23)
{
/*超过23个页面,根据实际大小估算MDL的大小*/
Size *= sizeof(PFN_NUMBER);//sizeof(PFN_NUMBER)物理页帧号的大小
Size += sizeof(MDL);
if (Size > MAXUSHORT) return NULL;
}
else
{
/*不超过23,按照标准大小23计算大小*/
Size = (23 * sizeof(PFN_NUMBER)) + sizeof(MDL);
Flags |= MDL_ALLOCATED_FIXED_SIZE;
/*Lookaside防止内存空洞,相当于一个内存管理器一样*/
Mdl = IopAllocateMdlFromLookaside(LookasideMdlList);//现成的数据结构分配内存,效率较高
}
/**/
if (!Mdl)
{
/*若失败,非分页内存池中申请内存*/
Mdl = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_MDL);
if (!Mdl) return NULL;
}
/*初始化MDL*/
MmInitializeMdl(Mdl, VirtualAddress, Length);
Mdl->MdlFlags |= Flags;
/* Check if an IRP was given too */
if (Irp)
{
/* Check if it came with a secondary buffer */
if (SecondaryBuffer)
{
/* Insert the MDL at the end */
p = Irp->MdlAddress;
while (p->Next) p = p->Next;
p->Next = Mdl;
}
else
{
/* Otherwise, insert it directly */
Irp->MdlAddress = Mdl;
}
}
/* Return the allocated mdl */
return Mdl;
}
当前篇幅较为冗长, 建议下次再深入分析剩余的经典MDL功能. 具体包括以下几点: MmProbeAndLockPages, MmMapLockedPagesSpecifyCache, MmUnmapLockedPages, MmUnlockPages, IoFreeMdl等.
当爱与责任融为一体时,则恩典将与您共存.”
参考书籍: 《Windows内核情景分析》
