当前位置 博文首页 > KOOKNUT的博客:浅谈MDL(壹)(Windows内核学习笔记)
先说说MDL是个啥东西,看一下MSDN的定义:
Memory descriptor list. An opaque structure, defined by the Memory Manager, that uses an array of physical page frame numbers (PFNs) to describe the pages that back a virtual memory range.
内存描述符表。由内存管理器定义的一个不透明结构体,使用物理页帧号数组来描述支持虚拟内存的页。
听起来可高大上的一个解释,先来看看它的定义,分别在Windbg和**中的定义:
//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
“When love and duty are one, then grace is within you.
当爱和责任合二为一,恩典便与你同在。”
参考书籍:
《Windows内核情景分析》