您的位置:首页 > 理论基础 > 计算机网络

探悉Windows 2000/XP Pool分配流程(http://webcrazy.yeah.net)

2005-01-31 19:36 435 查看
 探悉Windows 2000/XP Pool分配流程
            WebCrazy(http://webcrazy.yeah.net)

    对于Driver编写者,最之烦琐的莫过于各种内存缓冲区的使用(谈到缓冲区,你可能还会想到诸如MDL等概念,其实MDL只是对StartVa指定的Pool的Page Frame Number进行组织而已)。在用户态对于小块零星的内存使用牵涉到Heap,Windows 2000/XP在核心态提供了同样的一个机制用于这部分内核态模块的内存需求,区别于用户态,我们将这称之为Pool(内存池)。撇去内核模块与用户模块一些性质,如Pool不能直接由用户模块访问,Heap是进程相关,而Pool是系统相关等的区别,Pool与Heap在组织管理上有些异曲同工之处,本文着重对Pool的组织进行一些初浅的分析。

    我们知道,Windows总的来说Pool分为两类:非分页池(NonPagedPool)与可分页池(PagedPool)。根据是否Aligned,是否MustSuccess等,又区分出部分类型。由ntddk.h中的POOL_TYPE定义,POOL_TYPE是一个enum定义,对于属于NonPagedPool的其总是一个偶数(如NonPagedPool为0),而PagedPool则是一个奇数(如PagedPool是1)。通常我们分配一块内存区域由内核例程ExAllocatePool(WithTag)来完成,她们接受一个POOL_TYPE与一个分配大小的的参数,当我们从Pool中分配大于PAGE_SIZE(更准确的应该说大于PAGE_SIZE-0x10,在x86中PAGE_SIZE为4K)的空间时,分配的结果总是页对齐的(位于页边界),而如果小于PAGE_SIZE时(更准确的应该说小于PAGE_SIZE-0x10),分配总是在某一页中并且总是8字节对齐的,这一点对于我们以下的叙述是非常关键的。

    Windows针对非分页池与分页池各保留了不同的虚拟内存区域用于Pool的分配。内核变量MmPagedPoolStart与MmPagedPoolEnd指定了分页池的范围,通常这是一个从0xE1000000开始的。而非分页池的空间由两块区域组成,常规的区域从MmNonPagedPoolStart开始,另外含一块Expansion区域,由MmNonPagedPoolExpansionStart与MmNonPagedPoolEnd指定,对于Driver开发者,我们一般很容易的根据这些区段判断非分页池与分页池的。

    对于分页池与非分页池,Windows分别由一个POOL_DESCRIPTOR定义来组织(实际上Windows通常还会有另一些POOL_DESCRIPTOR,比如用于会话空间Session的等等,本文不加以讨论),其定义如下:

   +0x000 PoolType         : _POOL_TYPE
   +0x004 PoolIndex        : Uint4B
   +0x008 RunningAllocs    : Uint4B
   +0x00c RunningDeAllocs  : Uint4B
   +0x010 TotalPages       : Uint4B
   +0x014 TotalBigPages    : Uint4B
   +0x018 Threshold        : Uint4B
   +0x01c LockAddress      : Ptr32 Void
   +0x020 PendingFrees     : Ptr32 Void
   +0x024 PendingFreeDepth : Int4B
   +0x028 ListHeads        : [512] _LIST_ENTRY

    而对于非分页池与分页池的两个POOL_DESCRIPTOR,由系统变量PoolVector数组组织,我们可以使用PoolVector[POOL_TYPE&1]得到相应的POOL_DESCRIPTOR。请注意POOL_DESCRIPTOR的末尾有一个512长度的双向链表。对于小于1页的Pool,因为我们分配的Pool都是8字节对齐的,而分配的结果总是在1页中,所以这里使用了一个长为512的双向链表的数组,每个链表分别将系统中已经分配的页的零星大小分别为8至4096的空闲区域组织成双向链表。对于1页中连续的空闲区域,系统总是尽可能的置入最大的链表中去,譬如对于有16字节的区域总是置入ListHeads[1]中,而不会是插入两个节点到ListHead[0]中。有了这样一个链表,系统就很容易的对Pool分配进行管理了。而对于大于PAGE_SIZE的情况,系统将可能牵涉到重新分配System PTE,这将在我讨论完小于4K字节空间分配后,再加以叙述。

    在讨论这些小块Pool之前,我们必须先了解一下Lookaside,Lookaside与通常的Pool的区别是,她仅仅用于分配固定大小的的内存池。ExInitializeNPagedLookasideList与ExInitializePagedLookasideList用于初始化Lookaside,相关的从LookasideList中分配内存池的例程请参考DDK文档。由于不考虑Spinlock同步且分配固定大小的空间使Lookaside相对比通常的Pool分配来得快得多。我们可以这样能理解,通常我们使用ExInitializeNPagedLookasideList初始化Lookaside时,可以提供ALLOCATE_FUNCTION与FREE_FUNCTION参数用于分配大块内存区域(系统默认使用ExAllocatePool与ExFreePool),然后从中进行零星分配。鉴于速度上的考虑,Windows执行体从8至256字节每隔8字节为NonPagedPool与PagedPool各建立一个Lookaside,位于KPRCB中(我通过分析ExAllocatePoolWithTag,在Windows XP Build 2600中他们分别位于偏移0x598与0x698中),对于AllocatePool小于(0x20个8字节,即256字节),执行体函数ExAllocatePool直接从这些Lookaside中分配。至于Lookaside的Depth,内存管理器会定时使用KiAdjustLookasideDepth进行调整。

    现在我们考虑对于0x100(256字节)与0x1000(4K,PAGE_SIZE)之间的Pool分配,假设我们分配0xB18个字节,实际上系统将分配0xB20个字节,多出的8个字节是POOL_HEADER结构,用于对Pool的管理,POOL_HEADER的结构如下:

   +0x000 PreviousSize     : Pos 0, 9 Bits
   +0x000 PoolIndex        : Pos 9, 7 Bits
   +0x002 BlockSize        : Pos 0, 9 Bits
   +0x002 PoolType         : Pos 9, 7 Bits
   +0x000 Ulong1           : Uint4B
   +0x004 ProcessBilled    : Ptr32 _EPROCESS
   +0x004 PoolTag          : Uint4B
   +0x004 AllocatorBackTraceIndex : Uint2B
   +0x006 PoolTagHash      : Uint2B

    对于这一范围的Pool,系统将根据POOL_TYPE,由PoolVector找到相应的POOL_DESCRIPTOR,我们很容易的算出系统将寻找PoolDescriptor.ListHeads[0x164]的双向链表,因为0xB20/8为0x164。如果这一链表不为空,系统将得到这一链表的一个节点,代表一个大小为0xB20的字节空闲Pool,如果这是一个空链表,系统将定位下一个即0x165之后的双向链表,直到找到一个大小在0xB28与0x1000之间的空闲块。这时分配以后余下的空间,系统将插入相应大小的LIST_ENTRY中(这里要考虑碎片合并),等待下一次的分配。当然如果这之间仍找不到符合相应条件的空间,系统将使用MiAllocatePoolPages分配整页内存池使用。

    如果执行到MiAllocatePoolPages的话,其首先判断分配的是分页池或是非分页池,如果是非分页池,系统将首先寻找MmNonPagedPoolFreeListHead,与PoolDescriptor的ListHeads一样,MmNonPagedPoolFreeListHead同样也是一个双向链表数组,其元素个数为4,分别代表1至4页的非分页池的空闲列表(4页以上的空闲空间也存于第四个数组中,这就需要额外的判断,这儿不加描述)。这是由系统内存管理器维护的链表,MiAllocatePoolPages将首先从此处得到空闲列表,如果找到的话,然后修改PFN数据库。如果没有找到,则需要重新Reserve System PTE,这点我会在后面的讨论中继续谈到。而对于分页池则是另外一种情况,她牵涉到系统的另外一个重要的结构:MM_PAGED_POOL_INFO,定义如下:

   +0x000 PagedPoolAllocationMap : Ptr32 _RTL_BITMAP
   +0x004 EndOfPagedPoolBitmap : Ptr32 _RTL_BITMAP
   +0x008 PagedPoolLargeSessionAllocationMap : Ptr32 _RTL_BITMAP
   +0x00c FirstPteForPagedPool : Ptr32 _MMPTE
   +0x010 LastPteForPagedPool : Ptr32 _MMPTE
   +0x014 NextPdeForPagedPoolExpansion : Ptr32 _MMPTE
   +0x018 PagedPoolHint    : Uint4B
   +0x01c PagedPoolCommit  : Uint4B
   +0x020 AllocatedPagedPool : Uint4B

   FirstPteForPagedPool是PagedPool虚拟地址的第一个PTE位置,通常是MmPagedPoolStart(0xE1000000)的PTE位置,也即在(0xE1000000>>a)&0x3ffffc-0x40000000=0xc0384000(具体算法请参阅《小议Windows NT/2000分页机制》),我们很容易通过内核变量MmPagedPoolInfo指向的MM_PAGED_POOL_INFO来验证这一点。同样的对于LastPteForPagedPool也是很容易通过MmPagedPoolEnd得到值。MiAllocatePoolPages就是通过PagedPoolAllocationMap等几个RTL_BITMAP来分配页的。RTL_BITMAP我在《浅议Windows 2000/XP Pagefile组织管理》中详细介绍过,我也说过使用RtlFindSetBitsAndClear与RtlSetBits等相关操作函数来查找相应的空闲比特,同样的MiAllocatePoolPages也使用这一方法。
    
    接下来我们谈谈大于4K的情况,对于分配大于PAGE_SIZE(0x1000字节,实际上大于0xFF0字节,POOL_BLOCK_HEADER占用16个字节)的POOL,将调用MiAllocatePoolPages,则与上面的叙述一致。对于分页池,系统在初始化阶段因为已经对PTE进行了初始化,指向pagefile.sys,虽然因为pagefile.sys的大小可以自动扩展,但这也只涉及到扩展后MMPTE_SOFTWARE的初始化(指向页面文件的PTE,详见《浅议Windows 2000/XP Pagefile组织管理》),余下的即只是RTL_BITMAP的位操作了,存取这些页面出现的FAULT操作,则是int e(x86中)的任务了。所以底下我只对非分页池进行重点的说明。

  对于非分页池,能直接从MmNonPagedPoolFreeListHead得到页面的情况我们已经讨论过了,但如果我们不能从上面提及的MmNonPagedPoolFreeListHead中得到页面时,系统必须调用MiReserveSystemPtes来申请System PTE,继而调用MiReserveAlignedSystemPtes,保留了System PTE(页数从分配大小中得到)以后,我们还必须调用MiChargeCommitmentCantExpand之类的进行物理内存的分配,然后才涉及到对PFN数据库的操作。

    MiReserveAlignedSystemPtes用于保留System PTE,这一步骤其实就是分配系统虚拟地址的过程。他通过MmFirstFreeSystemPte,查找指定分页池的空闲虚拟地址(比如说是0xfb2b6000),得到这一地址相对于MmSystemPteBase的PTE(在Windows XP中MmSystemPteBase的值就为0xC0000000,这样得到的结果就是0xC03ECAD8),根据这个PTE地址存储的空闲页面数(不知道为什么,Microsoft将这一数据存于此),从虚拟地址的尾部分配虚拟地址。注意这里是尾部地址,这样也就不用更新MmFirstFreeSystemPte了,PTE位置存放的空闲页面数只需减少相应的页数,而不用更换位置。

    得到虚拟地址后,我们接下来必须分配实际的物理地址来满足这次调用。这通常是由MiChargeCommitmentCantExpand来完成的,必要时他会调用MiRemoveAnyPage,然后填充由刚保留下的System PTE,完成这样的一次分配工作。

    实际上介绍到此,我已经基本上将ExAllocatePoolWithTag(ExallocatePool只是传递一个Tag为'None'的ExAllocatePoolWithTag的调用)解释了大部分。至于Pool的释放,即ExFreePool的流程,有了这些知识后,也就不难对其进行分析了。ExAllocatePoolWithTag这个执行体例程相对其他例程复杂的多,对于POOL_TYPE提供的很多如Align或是MustSuccess等因素都要于以考虑。另外由于调试上如Driver Verifier或是Poolmon之类的需要,他也要考虑Pool Track(ExpInsertPoolTracker),还有Special Pool等等。我只是将这些基本流程说个大概,但这也许就可能会出现很多不好理解的地方,牵涉到先前我写过的很多关于PFN Database,PTE等等很多知识,更何况我对这部分的内容的掌握程度也不至于可以到向大家说清楚的地步,本文就算是抛砖引玉吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息