MM 学习笔记 07池分配器 (Pool Allocator)文件ntoskrnl/mm/ARM3/expool.cntoskrnl/mm/ARM3/pool.c日期2026-07-191. 概述池分配器是内核态的堆管理器提供分页池PagedPool和非分页池NonPagedPool两类内存分配。两个核心文件分工如下文件行数职责expool.c~3100 行Executive 池分配器 —ExAllocatePoolWithTag/ExFreePoolWithTag、标签跟踪系统、热标签缓存、大页表管理、配额定级pool.c~1200 行底层页面管理 —MiAllocatePoolPages/MiFreePoolPages、非分页池初始化、已释放池页面保护分配层次结构ExAllocatePoolWithTag (expool.c) │ ▼ POOL_DESCRIPTOR 空闲列表查找 (expool.c) │ ▼ MiAllocatePoolPages (pool.c) ← 底层页面分配 │ ▼ MiRemoveAnyPage / MiRemoveZeroPage (pfnlist.c) ← PFN 数据库2. POOL_DESCRIPTOR 结构typedefstruct_POOL_DESCRIPTOR{POOL_TYPE PoolType;// 池类型分页/非分页ULONG PoolIndex;// 池索引ULONG RunningAllocs;// 累计分配计数ULONG RunningDeAllocs;// 累计释放计数ULONG TotalPages;// 总页数ULONG TotalBigPages;// 大页面数ULONG Threshold;// 阈值决定何时切换到大页面分配PVOID LockAddress;// 锁地址PVOID PendingFrees;// 待释放链表LONG PendingFreeDepth;// 待释放深度SIZE_T TotalBytes;// 总字节数SIZE_T Spare0;LIST_ENTRY ListHeads[POOL_LISTS_PER_PAGE];// 空闲列表头256个}POOL_DESCRIPTOR,*PPOOL_DESCRIPTOR;// miarm.h 中的定义#definePOOL_LISTS_PER_PAGE(PAGE_SIZE/sizeof(LIST_ENTRY))// 32位: 4096/8 512 个桶 (但只用 256 个)// 64位: 4096/16 256 个桶关键理解ListHeads包含256 个空闲列表头每个对应一个固定大小的块范围桶索引计算NumberOfBytes POOL_BLOCK_SHIFTPOOL_BLOCK_SHIFT 4即 16 字节粒度当请求大小 PoolDesc-Threshold时走大页分配路径3. POOL_HEADER — 池头部每个分配块前有一个 8 字节头部用于链表管理和完整性检查typedefstruct_POOL_HEADER{union{struct{#ifdef_WIN64USHORT PreviousSize:8;// 前一个块的大小POOL_BLOCK_SIZE 为单位USHORT PoolIndex:8;// 池索引USHORT BlockSize:8;// 当前块的大小USHORT PoolType:8;// 池类型#elseUSHORT PreviousSize:9;USHORT PoolIndex:7;USHORT BlockSize:9;USHORT PoolType:7;#endifULONG PoolTag;// 4字节标签};ULONGLONG Reserved;};}POOL_HEADER,*PPOOL_HEADER;布局示意32位PreviousSize:9 | PoolIndex:7 | BlockSize:9 | PoolType:7PoolTag:32相关宏#definePOOL_ENTRY(x)(PPOOL_HEADER)((ULONG_PTR)(x)-sizeof(POOL_HEADER))#definePOOL_BLOCK(x,i)(PPOOL_HEADER)((ULONG_PTR)(x)((i)*POOL_BLOCK_SIZE))#definePOOL_NEXT_BLOCK(x)POOL_BLOCK((x),(x)-BlockSize)#definePOOL_PREV_BLOCK(x)POOL_BLOCK((x),-((x)-PreviousSize))4. 全局变量// expool.c - 核心全局变量POOL_DESCRIPTOR NonPagedPoolDescriptor;// 非分页池描述符全局唯一PPOOL_DESCRIPTOR ExpPagedPoolDescriptor[161];// 分页池描述符每CPU或每节点PPOOL_DESCRIPTOR PoolVector[2];// [0]分页池, [1]非分页池KSPIN_LOCK ExpTaggedPoolLock;// 标签跟踪表锁KSPIN_LOCK ExpLargePoolTableLock;// 大页表锁PPOOL_TRACKER_TABLE PoolTrackTable;// 池标签跟踪哈希表PPOOL_TRACKER_BIG_PAGES PoolBigPageTable;// 大页跟踪表ULONG PoolHitTag;// 热标签命中统计ULONG ExpPoolFlags;// 调试标志特殊池、验证器等// pool.c - 池页面管理全局变量LIST_ENTRY MmNonPagedPoolFreeListHead[MI_MAX_FREE_PAGE_LISTS];// 4个空闲页链表PFN_COUNT MmNumberOfFreeNonPagedPool;// 空闲非分页池页数SLIST_HEADER MiNonPagedPoolSListHead;// 非分页池 SListLookasideSLIST_HEADER MiPagedPoolSListHead;// 分页池 SListBOOLEAN MmProtectFreedNonPagedPool;// 释放池页面保护开关5. ExAllocatePoolWithTag 完整分配路径PVOID NTAPIExAllocatePoolWithTag(IN POOL_TYPE PoolType,IN SIZE_T NumberOfBytes,IN ULONG Tag)函数位置expool.c:19045.1 分配流程图ExAllocatePoolWithTag │ ├─ 1. 检查特殊池/验证器标志 │ └─ ExpPoolFlags POOL_FLAG_SPECIAL_POOL → MmAllocateSpecialPool │ ├─ 2. 获取池描述符 │ └─ PoolDesc PoolVector[PoolType BASE_POOL_TYPE_MASK] │ ├─ 3. 检查 Lookaside 缓存快速路径 │ └─ 如果命中直接返回无需遍历空闲列表 │ ├─ 4. 计算桶索引 │ └─ i NumberOfBytes POOL_BLOCK_SHIFT (4) │ ├─ 5. 遍历空闲列表从 i 开始向上搜索 │ │ │ ├─ 找到足够大的块 → 从链表中移除 │ │ │ ├─ 碎片分割Split │ │ └─ 如果剩余大小 ≥ 最小块大小16字节 │ │ 将剩余部分作为自由块插回链表 │ │ │ └─ 未找到 → 调用 MiAllocatePoolPages 获取新页面 │ ├─ 6. 大页分配路径 │ └─ NumberOfBytes POOL_MAX_ALLOC (约 PAGE_SIZE/8) │ → MiAllocatePoolPages(OriginalType, NumberOfBytes) │ └─ 7. 更新标签跟踪表 └─ ExpInsertPoolTrackerTag → 记录到哈希表5.2 Lookaside 缓存优化Lookaside 是每个 CPU 的快速分配缓存使用 SList单向链表// 初始化InitializeSListHead(MiPagedPoolSListHead);InitializeSListHead(MiNonPagedPoolSListHead);// 默认最大值MiNonPagedPoolSListMaximum4;// 非分页池最多缓存4个条目MiPagedPoolSListMaximum8;// 分页池最多缓存8个条目// 在内存较大的系统上可以扩大if(MmNumberOfPhysicalPages(2*_1GB)/PAGE_SIZE){MiNonPagedPoolSListMaximum*8;MiPagedPoolSListMaximum*8;}5.3 碎片分割示例假设分配 48 字节3 × 16从桶索引 3 开始搜索找到的块大小为 80 字节5 × 16[分配前] |←──── 80 字节 (5 块) ────→| [分配后] |← 48 →|←── 32 (剩余) ──→| ↑分配返回 ↑插回空闲列表6. ExFreePoolWithTag 释放路径VOID NTAPIExFreePoolWithTag(IN PVOID P,IN ULONG TagToFree)函数位置expool.c:24946.1 释放流程图ExFreePoolWithTag │ ├─ 1. 检查特殊池 │ └─ MmIsSpecialPoolAddress → MmFreeSpecialPool │ ├─ 2. 检查大页释放 │ └─ PAGE_ALIGN(P) P → 大页释放路径 │ ├─ ExpFindAndRemoveTagBigPages 查找标签和页数 │ └─ MiFreePoolPages 释放页面 │ ├─ 3. 小/中块释放正常路径 │ │ │ ├─ 获取 POOL_HEADER验证标签和完整性 │ │ │ ├─ 尝试前向合并Forward Combine │ │ └─ 检查下一个块是否是空闲块 │ │ 如果是合并两个块 │ │ │ ├─ 尝试后向合并Backward Combine │ │ └─ 检查上一个块是否是空闲块 │ │ 如果是合并两个块 │ │ │ └─ 将合并后的块插入空闲列表 │ ├─ 4. 尝试释放整页 │ └─ 如果合并后的块占满整个页面 │ 从空闲列表移除 → MiFreePoolPages │ └─ 5. 更新标签跟踪表 └─ ExpRemovePoolTrackerTag6.2 合并示例// 合并前假设块 B 刚被释放[块 A:已分配][块 B:刚刚释放][块 C:已分配]↓// 尝试前向合并检查块 C 是否空闲 → 否// 尝试后向合并检查块 A 是否空闲 → 否// 结果块 B 插入空闲列表// 合并前块 A 和块 B 都是空闲[块 A:空闲][块 B:刚刚释放][块 C:已分配]↓// 前向合并检查块 C → 否// 后向合并检查块 A → 是空闲// 合并后[块 AB: 更大的空闲块] [块 C: 已分配]7. 标签跟踪系统池标签跟踪是一个哈希表用于跟踪每个标签的分配统计信息typedefstruct_POOL_TRACKER_TABLE{ULONG Key;// 标签4字节ASCIIULONG NonPagedAllocs;// 非分页分配次数ULONG NonPagedFrees;// 非分页释放次数SIZE_T NonPagedBytes;// 非分页使用字节数ULONG PagedAllocs;// 分页分配次数ULONG PagedFrees;// 分页释放次数SIZE_T PagedBytes;// 分页使用字节数}POOL_TRACKER_TABLE,*PPOOL_TRACKER_TABLE;大页跟踪表类似typedefstruct_POOL_TRACKER_BIG_PAGES{PVOID Va;// 虚拟地址ULONG Key;// 标签ULONG Pages;// 页数}POOL_TRACKER_BIG_PAGES,*PPOOL_TRACKER_BIG_PAGES;查找逻辑// 计算哈希Hash(Tag4)PoolTrackTableMask;// 线性探测while(PoolTrackTable[Hash].Key!Tag){if(HashPoolTrackTableMask)Hash0;}8. 热标签缓存ExpSeedHotTagsExpSeedHotTags在池系统初始化时预填充跟踪表将常用标签预先固定到哈希表的特定位置提高查找效率。VOID NTAPIExpSeedHotTags(VOID){ULONG i,Key,Hash,Index;PPOOL_TRACKER_TABLE TrackTablePoolTrackTable;ULONG TagList[]{ oI, laH,PldM,LooP,tSbO, prI,bdDN,LprI,pOoI, ldM,eliF,aVMC,dSeS,CFtN,looP,rPCT,bNMC,dTeS,sFtN,TPCT,CPCT, yeK,qSbO,mNoI,aEoI,cPCT,aFtN,0ftN,tceS,SprI,ekoT, eS,lCbO,cScC,lFtN,cAeS,mfSF,kWcC,miSF,CdfA,EdfA,orSF,nftN,PRIU,rFpN,RFpN,aPeS,sUeS,FpcA,MpcA,cSeS,mNbO,sFpN,uLeS,DPcS,nevE,vrqR,ldaV, pP,SdaV, daV,LdaV,FdaV, GIB,};// 遍历所有 64 个常用标签for(i0;i64;i){KeyTagList[i];Hash(Key4)PoolTrackTableMask;// 在线性探测过程中插入...}}这些标签涵盖内核中最频繁分配的标签如ldaVVAD、 oI、 laH等。9. MiAllocatePoolPages 底层页面分配// pool.c - 从非分页池预分配页面PVOID NTAPIMiAllocatePoolPages(IN POOL_TYPE PoolType,IN SIZE_T NumberOfBytes)位置pool.c功能检查是否是分页池调用 → 转发到分页池分配器非分页池从MmNonPagedPoolFreeListHead空闲列表中获取页面支持 4 个空闲列表减少多核竞争按颜色分配分配成功后将新页面分割成适当的块插入 POOL_DESCRIPTOR 的空闲列表更新配额统计// 非分页池空闲列表4个减少锁定争用for(i0;iMI_MAX_FREE_PAGE_LISTS;i){InitializeListHead(MmNonPagedPoolFreeListHead[i]);}10. MiFreePoolPages 底层页面释放将整页池内存归还给系统从 POOL_DESCRIPTOR 空闲列表中移除该页上的所有空闲块将页面插入MmNonPagedPoolFreeListHead空闲列表如果启用了MmProtectFreedNonPagedPool对页面进行保护更新MmNumberOfFreeNonPagedPool计数11. 非分页池初始化流程CODE_SEG(INIT)VOID NTAPIMiInitializeNonPagedPool(VOID)位置pool.c:278初始化步骤MiInitializeNonPagedPool │ ├─ 1. 初始化 SListLookaside 缓存 │ └─ 根据物理内存大小调整最大缓存数 │ ├─ 2. 初始化 4 个空闲列表头 │ ├─ 3. 计算初始池大小 │ └─ PoolPages BYTES_TO_PAGES(MmSizeOfNonPagedPoolInBytes) │ ├─ 4. 创建第一个空闲条目 │ └─ FreeEntry-Size PoolPages │ FreeEntry-Owner FirstEntry │ ├─ 5. 插入到空闲列表 │ └─ InsertHeadList(MmNonPagedPoolFreeListHead[MI_MAX_FREE_PAGE_LISTS-1], ...) │ └─ 6. 初始化阈值和事件 └─ MiInitializeNonPagedPoolThresholds() └─ MiInitializePoolEvents()阈值初始化// 低阈值8MB 或非分页池大小的 1/3MiLowNonPagedPoolThreshold(8*_1MB)PAGE_SHIFT;MiLowNonPagedPoolThresholdmin(MiLowNonPagedPoolThreshold,Size/3);// 高阈值20MB 或非分页池大小的 50%MiHighNonPagedPoolThreshold(20*_1MB)PAGE_SHIFT;MiHighNonPagedPoolThresholdmin(MiHighNonPagedPoolThreshold,Size/2);12. 已释放池页面保护机制当MmProtectFreedNonPagedPool TRUE时启用用于检测 Use-After-Free 错误。VOID NTAPIMiProtectFreeNonPagedPool(IN PVOID VirtualAddress,IN ULONG PageCount){PMMPTE PointerPte,LastPte;MMPTE TempPte;// 遍历页面的所有 PTEPointerPteMiAddressToPte(VirtualAddress);LastPtePointerPtePageCount;do{TempPte*PointerPte;// 标记为无效 PTE设置原型位以识别为池页面TempPte.u.Hard.Valid0;TempPte.u.Soft.Prototype1;MI_WRITE_INVALID_PTE(PointerPte,TempPte);}while(PointerPteLastPte);// 刷新 TLBKeFlushEntireTb(TRUE,TRUE);}对应的解除保护函数MiUnProtectFreeNonPagedPool在需要访问被保护的页面时调用。保护下的链表操作// 插入前临时解除保护MiProtectedPoolUnProtectLinks(ListHead,PoolFlink,PoolBlink);// 执行链表操作InsertHeadList(ListHead,Entry);// 重新保护MiProtectedPoolProtectLinks(PoolFlink,PoolBlink);13. 状态图ExAllocatePoolWithTag │ ▼ ┌─────────────────┐ │ 特殊池检查 │───启用──→ MmAllocateSpecialPool └────────┬────────┘ │ 禁用 ▼ ┌─────────────────┐ │ Lookaside 缓存 │───命中──→ 直接返回 └────────┬────────┘ │ 未命中 ▼ ┌─────────────────┐ │ 计算桶索引 │ └────────┬────────┘ ▼ ┌─────────────────┐ │ 遍历空闲列表 │ └────────┬────────┘ │ │ 找到块 未找到 │ │ ▼ ▼ ┌─────────┐ ┌──────────────┐ │ 碎片分割 │ │ MiAllocate │ │ │ │ PoolPages │ └────┬─────┘ └──────┬───────┘ │ │ ▼ ▼ ┌──────────────────────────┐ │ 更新标签跟踪表 │ │ ExpInsertPoolTrackerTag│ └──────────────────────────┘参考链接[miarm.h 中 POOL_HEADER 和 POOL_DESCRIPTOR 定义](file:///d:/reactos/ntoskrnl/mm/ARM3/miarm.h)[expool.c — Executive 池分配器主文件](file:///d:/reactos/ntoskrnl/mm/ARM3/expool.c)[pool.c — 池页面管理](file:///d:/reactos/ntoskrnl/mm/ARM3/pool.c)[special.c — 特殊池和 Driver Verifier](file:///d:/reactos/ntoskrnl/mm/ARM3/special.c)[Windows Internals 7th Part 1, Chapter 10 “Memory Management” — Pool]
MM 学习笔记 07:池分配器 (Pool Allocator)
MM 学习笔记 07池分配器 (Pool Allocator)文件ntoskrnl/mm/ARM3/expool.cntoskrnl/mm/ARM3/pool.c日期2026-07-191. 概述池分配器是内核态的堆管理器提供分页池PagedPool和非分页池NonPagedPool两类内存分配。两个核心文件分工如下文件行数职责expool.c~3100 行Executive 池分配器 —ExAllocatePoolWithTag/ExFreePoolWithTag、标签跟踪系统、热标签缓存、大页表管理、配额定级pool.c~1200 行底层页面管理 —MiAllocatePoolPages/MiFreePoolPages、非分页池初始化、已释放池页面保护分配层次结构ExAllocatePoolWithTag (expool.c) │ ▼ POOL_DESCRIPTOR 空闲列表查找 (expool.c) │ ▼ MiAllocatePoolPages (pool.c) ← 底层页面分配 │ ▼ MiRemoveAnyPage / MiRemoveZeroPage (pfnlist.c) ← PFN 数据库2. POOL_DESCRIPTOR 结构typedefstruct_POOL_DESCRIPTOR{POOL_TYPE PoolType;// 池类型分页/非分页ULONG PoolIndex;// 池索引ULONG RunningAllocs;// 累计分配计数ULONG RunningDeAllocs;// 累计释放计数ULONG TotalPages;// 总页数ULONG TotalBigPages;// 大页面数ULONG Threshold;// 阈值决定何时切换到大页面分配PVOID LockAddress;// 锁地址PVOID PendingFrees;// 待释放链表LONG PendingFreeDepth;// 待释放深度SIZE_T TotalBytes;// 总字节数SIZE_T Spare0;LIST_ENTRY ListHeads[POOL_LISTS_PER_PAGE];// 空闲列表头256个}POOL_DESCRIPTOR,*PPOOL_DESCRIPTOR;// miarm.h 中的定义#definePOOL_LISTS_PER_PAGE(PAGE_SIZE/sizeof(LIST_ENTRY))// 32位: 4096/8 512 个桶 (但只用 256 个)// 64位: 4096/16 256 个桶关键理解ListHeads包含256 个空闲列表头每个对应一个固定大小的块范围桶索引计算NumberOfBytes POOL_BLOCK_SHIFTPOOL_BLOCK_SHIFT 4即 16 字节粒度当请求大小 PoolDesc-Threshold时走大页分配路径3. POOL_HEADER — 池头部每个分配块前有一个 8 字节头部用于链表管理和完整性检查typedefstruct_POOL_HEADER{union{struct{#ifdef_WIN64USHORT PreviousSize:8;// 前一个块的大小POOL_BLOCK_SIZE 为单位USHORT PoolIndex:8;// 池索引USHORT BlockSize:8;// 当前块的大小USHORT PoolType:8;// 池类型#elseUSHORT PreviousSize:9;USHORT PoolIndex:7;USHORT BlockSize:9;USHORT PoolType:7;#endifULONG PoolTag;// 4字节标签};ULONGLONG Reserved;};}POOL_HEADER,*PPOOL_HEADER;布局示意32位PreviousSize:9 | PoolIndex:7 | BlockSize:9 | PoolType:7PoolTag:32相关宏#definePOOL_ENTRY(x)(PPOOL_HEADER)((ULONG_PTR)(x)-sizeof(POOL_HEADER))#definePOOL_BLOCK(x,i)(PPOOL_HEADER)((ULONG_PTR)(x)((i)*POOL_BLOCK_SIZE))#definePOOL_NEXT_BLOCK(x)POOL_BLOCK((x),(x)-BlockSize)#definePOOL_PREV_BLOCK(x)POOL_BLOCK((x),-((x)-PreviousSize))4. 全局变量// expool.c - 核心全局变量POOL_DESCRIPTOR NonPagedPoolDescriptor;// 非分页池描述符全局唯一PPOOL_DESCRIPTOR ExpPagedPoolDescriptor[161];// 分页池描述符每CPU或每节点PPOOL_DESCRIPTOR PoolVector[2];// [0]分页池, [1]非分页池KSPIN_LOCK ExpTaggedPoolLock;// 标签跟踪表锁KSPIN_LOCK ExpLargePoolTableLock;// 大页表锁PPOOL_TRACKER_TABLE PoolTrackTable;// 池标签跟踪哈希表PPOOL_TRACKER_BIG_PAGES PoolBigPageTable;// 大页跟踪表ULONG PoolHitTag;// 热标签命中统计ULONG ExpPoolFlags;// 调试标志特殊池、验证器等// pool.c - 池页面管理全局变量LIST_ENTRY MmNonPagedPoolFreeListHead[MI_MAX_FREE_PAGE_LISTS];// 4个空闲页链表PFN_COUNT MmNumberOfFreeNonPagedPool;// 空闲非分页池页数SLIST_HEADER MiNonPagedPoolSListHead;// 非分页池 SListLookasideSLIST_HEADER MiPagedPoolSListHead;// 分页池 SListBOOLEAN MmProtectFreedNonPagedPool;// 释放池页面保护开关5. ExAllocatePoolWithTag 完整分配路径PVOID NTAPIExAllocatePoolWithTag(IN POOL_TYPE PoolType,IN SIZE_T NumberOfBytes,IN ULONG Tag)函数位置expool.c:19045.1 分配流程图ExAllocatePoolWithTag │ ├─ 1. 检查特殊池/验证器标志 │ └─ ExpPoolFlags POOL_FLAG_SPECIAL_POOL → MmAllocateSpecialPool │ ├─ 2. 获取池描述符 │ └─ PoolDesc PoolVector[PoolType BASE_POOL_TYPE_MASK] │ ├─ 3. 检查 Lookaside 缓存快速路径 │ └─ 如果命中直接返回无需遍历空闲列表 │ ├─ 4. 计算桶索引 │ └─ i NumberOfBytes POOL_BLOCK_SHIFT (4) │ ├─ 5. 遍历空闲列表从 i 开始向上搜索 │ │ │ ├─ 找到足够大的块 → 从链表中移除 │ │ │ ├─ 碎片分割Split │ │ └─ 如果剩余大小 ≥ 最小块大小16字节 │ │ 将剩余部分作为自由块插回链表 │ │ │ └─ 未找到 → 调用 MiAllocatePoolPages 获取新页面 │ ├─ 6. 大页分配路径 │ └─ NumberOfBytes POOL_MAX_ALLOC (约 PAGE_SIZE/8) │ → MiAllocatePoolPages(OriginalType, NumberOfBytes) │ └─ 7. 更新标签跟踪表 └─ ExpInsertPoolTrackerTag → 记录到哈希表5.2 Lookaside 缓存优化Lookaside 是每个 CPU 的快速分配缓存使用 SList单向链表// 初始化InitializeSListHead(MiPagedPoolSListHead);InitializeSListHead(MiNonPagedPoolSListHead);// 默认最大值MiNonPagedPoolSListMaximum4;// 非分页池最多缓存4个条目MiPagedPoolSListMaximum8;// 分页池最多缓存8个条目// 在内存较大的系统上可以扩大if(MmNumberOfPhysicalPages(2*_1GB)/PAGE_SIZE){MiNonPagedPoolSListMaximum*8;MiPagedPoolSListMaximum*8;}5.3 碎片分割示例假设分配 48 字节3 × 16从桶索引 3 开始搜索找到的块大小为 80 字节5 × 16[分配前] |←──── 80 字节 (5 块) ────→| [分配后] |← 48 →|←── 32 (剩余) ──→| ↑分配返回 ↑插回空闲列表6. ExFreePoolWithTag 释放路径VOID NTAPIExFreePoolWithTag(IN PVOID P,IN ULONG TagToFree)函数位置expool.c:24946.1 释放流程图ExFreePoolWithTag │ ├─ 1. 检查特殊池 │ └─ MmIsSpecialPoolAddress → MmFreeSpecialPool │ ├─ 2. 检查大页释放 │ └─ PAGE_ALIGN(P) P → 大页释放路径 │ ├─ ExpFindAndRemoveTagBigPages 查找标签和页数 │ └─ MiFreePoolPages 释放页面 │ ├─ 3. 小/中块释放正常路径 │ │ │ ├─ 获取 POOL_HEADER验证标签和完整性 │ │ │ ├─ 尝试前向合并Forward Combine │ │ └─ 检查下一个块是否是空闲块 │ │ 如果是合并两个块 │ │ │ ├─ 尝试后向合并Backward Combine │ │ └─ 检查上一个块是否是空闲块 │ │ 如果是合并两个块 │ │ │ └─ 将合并后的块插入空闲列表 │ ├─ 4. 尝试释放整页 │ └─ 如果合并后的块占满整个页面 │ 从空闲列表移除 → MiFreePoolPages │ └─ 5. 更新标签跟踪表 └─ ExpRemovePoolTrackerTag6.2 合并示例// 合并前假设块 B 刚被释放[块 A:已分配][块 B:刚刚释放][块 C:已分配]↓// 尝试前向合并检查块 C 是否空闲 → 否// 尝试后向合并检查块 A 是否空闲 → 否// 结果块 B 插入空闲列表// 合并前块 A 和块 B 都是空闲[块 A:空闲][块 B:刚刚释放][块 C:已分配]↓// 前向合并检查块 C → 否// 后向合并检查块 A → 是空闲// 合并后[块 AB: 更大的空闲块] [块 C: 已分配]7. 标签跟踪系统池标签跟踪是一个哈希表用于跟踪每个标签的分配统计信息typedefstruct_POOL_TRACKER_TABLE{ULONG Key;// 标签4字节ASCIIULONG NonPagedAllocs;// 非分页分配次数ULONG NonPagedFrees;// 非分页释放次数SIZE_T NonPagedBytes;// 非分页使用字节数ULONG PagedAllocs;// 分页分配次数ULONG PagedFrees;// 分页释放次数SIZE_T PagedBytes;// 分页使用字节数}POOL_TRACKER_TABLE,*PPOOL_TRACKER_TABLE;大页跟踪表类似typedefstruct_POOL_TRACKER_BIG_PAGES{PVOID Va;// 虚拟地址ULONG Key;// 标签ULONG Pages;// 页数}POOL_TRACKER_BIG_PAGES,*PPOOL_TRACKER_BIG_PAGES;查找逻辑// 计算哈希Hash(Tag4)PoolTrackTableMask;// 线性探测while(PoolTrackTable[Hash].Key!Tag){if(HashPoolTrackTableMask)Hash0;}8. 热标签缓存ExpSeedHotTagsExpSeedHotTags在池系统初始化时预填充跟踪表将常用标签预先固定到哈希表的特定位置提高查找效率。VOID NTAPIExpSeedHotTags(VOID){ULONG i,Key,Hash,Index;PPOOL_TRACKER_TABLE TrackTablePoolTrackTable;ULONG TagList[]{ oI, laH,PldM,LooP,tSbO, prI,bdDN,LprI,pOoI, ldM,eliF,aVMC,dSeS,CFtN,looP,rPCT,bNMC,dTeS,sFtN,TPCT,CPCT, yeK,qSbO,mNoI,aEoI,cPCT,aFtN,0ftN,tceS,SprI,ekoT, eS,lCbO,cScC,lFtN,cAeS,mfSF,kWcC,miSF,CdfA,EdfA,orSF,nftN,PRIU,rFpN,RFpN,aPeS,sUeS,FpcA,MpcA,cSeS,mNbO,sFpN,uLeS,DPcS,nevE,vrqR,ldaV, pP,SdaV, daV,LdaV,FdaV, GIB,};// 遍历所有 64 个常用标签for(i0;i64;i){KeyTagList[i];Hash(Key4)PoolTrackTableMask;// 在线性探测过程中插入...}}这些标签涵盖内核中最频繁分配的标签如ldaVVAD、 oI、 laH等。9. MiAllocatePoolPages 底层页面分配// pool.c - 从非分页池预分配页面PVOID NTAPIMiAllocatePoolPages(IN POOL_TYPE PoolType,IN SIZE_T NumberOfBytes)位置pool.c功能检查是否是分页池调用 → 转发到分页池分配器非分页池从MmNonPagedPoolFreeListHead空闲列表中获取页面支持 4 个空闲列表减少多核竞争按颜色分配分配成功后将新页面分割成适当的块插入 POOL_DESCRIPTOR 的空闲列表更新配额统计// 非分页池空闲列表4个减少锁定争用for(i0;iMI_MAX_FREE_PAGE_LISTS;i){InitializeListHead(MmNonPagedPoolFreeListHead[i]);}10. MiFreePoolPages 底层页面释放将整页池内存归还给系统从 POOL_DESCRIPTOR 空闲列表中移除该页上的所有空闲块将页面插入MmNonPagedPoolFreeListHead空闲列表如果启用了MmProtectFreedNonPagedPool对页面进行保护更新MmNumberOfFreeNonPagedPool计数11. 非分页池初始化流程CODE_SEG(INIT)VOID NTAPIMiInitializeNonPagedPool(VOID)位置pool.c:278初始化步骤MiInitializeNonPagedPool │ ├─ 1. 初始化 SListLookaside 缓存 │ └─ 根据物理内存大小调整最大缓存数 │ ├─ 2. 初始化 4 个空闲列表头 │ ├─ 3. 计算初始池大小 │ └─ PoolPages BYTES_TO_PAGES(MmSizeOfNonPagedPoolInBytes) │ ├─ 4. 创建第一个空闲条目 │ └─ FreeEntry-Size PoolPages │ FreeEntry-Owner FirstEntry │ ├─ 5. 插入到空闲列表 │ └─ InsertHeadList(MmNonPagedPoolFreeListHead[MI_MAX_FREE_PAGE_LISTS-1], ...) │ └─ 6. 初始化阈值和事件 └─ MiInitializeNonPagedPoolThresholds() └─ MiInitializePoolEvents()阈值初始化// 低阈值8MB 或非分页池大小的 1/3MiLowNonPagedPoolThreshold(8*_1MB)PAGE_SHIFT;MiLowNonPagedPoolThresholdmin(MiLowNonPagedPoolThreshold,Size/3);// 高阈值20MB 或非分页池大小的 50%MiHighNonPagedPoolThreshold(20*_1MB)PAGE_SHIFT;MiHighNonPagedPoolThresholdmin(MiHighNonPagedPoolThreshold,Size/2);12. 已释放池页面保护机制当MmProtectFreedNonPagedPool TRUE时启用用于检测 Use-After-Free 错误。VOID NTAPIMiProtectFreeNonPagedPool(IN PVOID VirtualAddress,IN ULONG PageCount){PMMPTE PointerPte,LastPte;MMPTE TempPte;// 遍历页面的所有 PTEPointerPteMiAddressToPte(VirtualAddress);LastPtePointerPtePageCount;do{TempPte*PointerPte;// 标记为无效 PTE设置原型位以识别为池页面TempPte.u.Hard.Valid0;TempPte.u.Soft.Prototype1;MI_WRITE_INVALID_PTE(PointerPte,TempPte);}while(PointerPteLastPte);// 刷新 TLBKeFlushEntireTb(TRUE,TRUE);}对应的解除保护函数MiUnProtectFreeNonPagedPool在需要访问被保护的页面时调用。保护下的链表操作// 插入前临时解除保护MiProtectedPoolUnProtectLinks(ListHead,PoolFlink,PoolBlink);// 执行链表操作InsertHeadList(ListHead,Entry);// 重新保护MiProtectedPoolProtectLinks(PoolFlink,PoolBlink);13. 状态图ExAllocatePoolWithTag │ ▼ ┌─────────────────┐ │ 特殊池检查 │───启用──→ MmAllocateSpecialPool └────────┬────────┘ │ 禁用 ▼ ┌─────────────────┐ │ Lookaside 缓存 │───命中──→ 直接返回 └────────┬────────┘ │ 未命中 ▼ ┌─────────────────┐ │ 计算桶索引 │ └────────┬────────┘ ▼ ┌─────────────────┐ │ 遍历空闲列表 │ └────────┬────────┘ │ │ 找到块 未找到 │ │ ▼ ▼ ┌─────────┐ ┌──────────────┐ │ 碎片分割 │ │ MiAllocate │ │ │ │ PoolPages │ └────┬─────┘ └──────┬───────┘ │ │ ▼ ▼ ┌──────────────────────────┐ │ 更新标签跟踪表 │ │ ExpInsertPoolTrackerTag│ └──────────────────────────┘参考链接[miarm.h 中 POOL_HEADER 和 POOL_DESCRIPTOR 定义](file:///d:/reactos/ntoskrnl/mm/ARM3/miarm.h)[expool.c — Executive 池分配器主文件](file:///d:/reactos/ntoskrnl/mm/ARM3/expool.c)[pool.c — 池页面管理](file:///d:/reactos/ntoskrnl/mm/ARM3/pool.c)[special.c — 特殊池和 Driver Verifier](file:///d:/reactos/ntoskrnl/mm/ARM3/special.c)[Windows Internals 7th Part 1, Chapter 10 “Memory Management” — Pool]