mm学习笔记_06:缺页异常处理
mm学习笔记_06缺页异常处理基于 ReactOS ARM3 MM 源码ntoskrnl/mm/ARM3/pagfault.c1. pagfault.c 概述文件位置d:\reactos\ntoskrnl\mm\ARM3\pagfault.c职责处理所有类型的缺页异常Page Fault包括请求零页故障Demand Zero Fault分页文件故障Page File Fault转换故障Transition Fault原型 PTE 故障Proto PTE Fault / Copy on Write用户栈自动扩展Stack Overflow访问权限检查Access Check入口点MmArmAccessFault由 CPU 缺页中断KiTrap0E调用2. MmArmAccessFault——缺页异常入口完整路径文件pagfault.c:1698CPU 检测到缺页CR2 故障地址错误码在栈上 │ ▼ KiTrap0E (x86 汇编中断处理程序) │ ▼ MmArmAccessFault(FaultCode, Address, Mode, TrapInformation)2.1 第一阶段高 IRQL 故障处理MmArmAccessFault(ULONG FaultCode,PVOID Address,...){OldIrqlKeGetCurrentIrql();if(OldIrqlAPC_LEVEL){// 高 IRQL APC_LEVEL下的缺页处理// 只允许系统页表paged pool 页表的缺页if(MI_IS_SYSTEM_PAGE_TABLE_ADDRESS(Address))MiSynchronizeSystemPde(...);// 检查 PXE/PPE/PDE/PTE 是否有效if(任何一级页表无效){// 无效 → 返回错误触发 KERNEL_DATA_INPAGE_ERROR 或 ATTEMPTED_WRITE_TO_READONLY_MEMORYreturnSTATUS_IN_PAGE_ERROR|0x10000000;}// 检查是否写只读页if(MI_IS_WRITE_ACCESS(FaultCode)!PTE_READWRITE)KeBugCheck(ATTEMPTED_WRITE_TO_READONLY_MEMORY);returnSTATUS_SUCCESS;// 无实际错误的缺页}关键点高 IRQL 下不允许复杂的缺页处理只能处理页表本身的缺页。2.2 第二阶段内核地址空间故障if(AddressMmSystemRangeStart){// 内核地址空间缺页// 不允许用户态触发内核缺页if(ModeUserMode)returnSTATUS_ACCESS_VIOLATION;// 检查 PXE/PPE/PDE 是否有效if(任何一级无效)KeBugCheck(PAGE_FAULT_IN_NONPAGED_AREA);// 如果 PTE 有效检查写只读页或 NX 违规if(TempPte.u.Hard.Valid){if(写只读)KeBugCheck(ATTEMPTED_WRITE_TO_READONLY_MEMORY);if(执行不可执行)KeBugCheck(ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY);returnSTATUS_SUCCESS;}// 获取工作集系统缓存或会话空间WorkingSetMmSystemCacheWs;// 或 MmSessionSpace-...// 锁定工作集 → 重新检查 PTE → 分派故障MiLockWorkingSet(CurrentThread,WorkingSet);if(TempPte.u.Hard.Valid){/* 已解决 */}else{// 处理原型 PTE 或分派故障StatusMiDispatchFault(FaultCode,Address,PointerPte,ProtoPte,FALSE,CurrentProcess,...);}MiUnlockWorkingSet(CurrentThread,WorkingSet);if(StatusSTATUS_NO_MEMORY){MmRebalanceMemoryConsumersAndWait();gotoRetryKernel;}returnStatus;}2.3 第三阶段用户地址空间故障UserFaultUserFault: CurrentProcess PsGetCurrentProcess(); MiLockProcessWorkingSet(CurrentProcess, CurrentThread); // 1. 检查各级页表PXE → PPE → PDE → PTE有效性 // 对无效的页表项通过 MiResolveDemandZeroFault 按需分配页表页 // 2. 读取 PTE TempPte *PointerPte; // 3. 如果 PTE 有效Valid1 if (TempPte.u.Hard.Valid) { if (写操作) { if (COW) { // → 分配新页 → 复制内容 → 写时复制COW } if (只读) { return STATUS_ACCESS_VIOLATION; } } return STATUS_SUCCESS; // 其他线程已解决此故障 } // 4. 如果 PTE 是 Demand Zero 格式 if (是 DemandZero 格式) { return MiResolveDemandZeroFault(Address, PointerPte, ...); } // 5. 如果 PTE 0从未映射 if (TempPte.u.Long 0) { // 查找 VADMiCheckVirtualAddress // 如果是 Guard Page → 调用 MiCheckForUserStackOverflow栈扩展 // 否则 → 分配零页 } // 6. 对原型 PTE 等复杂情况调用 MiDispatchFault Status MiDispatchFault(FaultCode, Address, PointerPte, ProtoPte, ...); ExitUser: MiUnlockProcessWorkingSet(CurrentProcess, CurrentThread); if (Status STATUS_NO_MEMORY) { MmRebalanceMemoryConsumersAndWait(); goto UserFault; // 重试 } return Status;完整流程图MmArmAccessFault(FaultCode, Address, Mode, TrapInfo) │ ├─ [高 IRQL ( APC_LEVEL)] │ ├─ 检查页表有效性 │ ├─ 同步系统 PDE │ └─ 返回 STATUS_SUCCESS 或 BugCheck │ ├─ [内核地址 (Address MmSystemRangeStart)] │ ├─ 检查用户态违规 │ ├─ 检查 PDE/PPE/PXE 有效性 │ ├─ 锁定系统工作集 │ ├─ 重新检查 PTE │ ├─ MiDispatchFault → 故障分派 │ └─ 解锁工作集 │ └─ [用户地址 (UserFault 标签)] ├─ MiLockProcessWorkingSet ├─ 检查/分配页表页PDE/PPE/PXE ├─ [PTE 有效] → COW 处理 / 访问违规 ├─ [DemandZero PTE] → MiResolveDemandZeroFault ├─ [PTE 0] → 查找 VAD → GuardPage? 栈扩展 / 分配零页 ├─ [其他] → MiDispatchFault └─ MiUnlockProcessWorkingSet3. MiDispatchFault——分派逻辑文件pagfault.c:1338功能根据 PTE 内容判断故障类型分派到对应的解析函数。PTE 状态判断顺序NTSTATUS NTAPIMiDispatchFault(...){TempPte*PointerPte;// 顺序1有原型 PTE 吗if(PointerProtoPte){// 锁定 PFN 数据库检查原型 PTE 的状态// → MiResolveProtoPteFault}// 顺序2Transition PTEif(TempPte.u.Soft.Transition){// → MiResolveTransitionFault}// 顺序3页面在分页文件中if(TempPte.u.Soft.PageFileHigh!0){// → MiResolvePageFileFault}// 顺序4只能是 Demand Zero PTEASSERT(TempPte.u.Hard.Valid0);ASSERT(TempPte.u.Soft.Prototype0);ASSERT(TempPte.u.Soft.Transition0);ASSERT(TempPte.u.Soft.PageFileHigh0);ASSERT(TempPte.u.Long!0);// → MiResolveDemandZeroFault}判断流程图读取 PTE │ ├─ 有原型 PTE? → MiResolveProtoPteFault │ ├─ Transition 位1? → MiResolveTransitionFault │ ├─ PageFileHigh ≠ 0? → MiResolvePageFileFault │ └─ 默认软件 PTE, 非零→ MiResolveDemandZeroFault4. MiResolveDemandZeroFault——请求零页故障文件pagfault.c:599触发条件访问的页面已被预留但从未提交/访问PTE 为软件格式Demand Zero。处理流程MiResolveDemandZeroFault(Address, PointerPte, Protection, Process, OldIrql) │ ├─ 确定是否需要零页NeedZero │ ├─ 用户模式进程 → NeedZero TRUE需要已清零页 │ └─ 内核/会话 → 根据 OldIrql 判断 │ ├─ 锁定 PFN 数据库如尚未锁定 │ ├─ 取页策略 │ ├─ 有颜色 → MiRemoveZeroPageSafe(Color) 优先取零页 │ │ └─ 失败 → MiRemoveAnyPage(Color) NeedZero TRUE │ └─ 无颜色 → MI_GET_NEXT_COLOR() MiRemoveAnyPage / MiRemoveZeroPage │ ├─ 初始化 PFNMiInitializePfn(PageFrameNumber, PointerPte, TRUE) │ ├─ 递增 Demand Zero 计数MmDemandZeroCount │ ├─ 释放 PFN 锁 │ ├─ 如需清零MiZeroPfn(PageFrameNumber)通过超空间映射清零 │ ├─ 构建硬件 PTE │ ├─ 用户地址 → MI_MAKE_HARDWARE_PTE_USER含用户标志位 │ └─ 内核地址 → MI_MAKE_HARDWARE_PTE │ ├─ 写脏位如页面可写 │ └─ MI_WRITE_VALID_PTE(PointerPte, TempPte)数据结构变化分配前 分配后 ┌─────────┐ ┌─────────┐ │ PointerPte → 软件 PTE │ PointerPte → 硬件 PTEValid1, PFNX │ (Valid0, ProtectionXX)│ └─────────┘ └─────────┘ PFN X: PageLocation: Free/Zeroed → ActiveAndValid ReferenceCount: 0 → 1 ShareCount: 0 → 1 PteAddress: → PointerPte OriginalPte: → 保存原 PTE5. MiResolvePageFileFault——分页文件故障文件pagfault.c:900触发条件访问的页面已被换出到分页文件PTE 中记录了分页文件号和偏移量。PTE 格式分页文件状态// PageFileHigh 分页文件中的偏移量高位// PageFileLow 分页文件索引号低位// Protection 保护属性TempPte.u.Soft.PageFileHigh → 偏移量 TempPte.u.Soft.PageFileLow → 文件索引 TempPte.u.Soft.Protection → 保护属性处理流程MiResolvePageFileFault(StoreInstruction, FaultingAddress, PointerPte, Process, OldIrql) │ ├─ 解析 PTE 中的交换条目PageFileIndex, PageFileOffset, Protection │ ├─ 取一空闲物理页MiRemoveAnyPage(Color) │ ├─ 初始化 PFNMiInitializePfn(Page, PointerPte, StoreInstruction) │ ├─ 设置 ReadInProgress 1标记 I/O 进行中 │ ├─ 写 Transition PTE标记页面正在 I/O保护原内容 │ MI_MAKE_TRANSITION_PTE(TempPte, Page, Protection) │ MI_WRITE_INVALID_PTE(PointerPte, TempPte) │ ├─ 释放 PFN 锁 → 发起读盘 I/O │ Status MiReadPageFile(Page, PageFileIndex, PageFileOffset) │ ├─ 重新锁定 PFN 锁 │ ├─ 检查 I/O 结果 │ ├─ 成功构建硬件 PTE写为有效 │ ├─ 失败标记 InPageError │ └─ 唤醒等待者若有事件 │ └─ 返回 Status关键点I/O 期间 PTE 被写为 Transition 格式其他线程访问同一地址时会等待使用Pfn1-u1.Event事件对象实现等待队列链6. MiResolveTransitionFault——转换故障文件pagfault.c:992触发条件PTE 的 Transition 位 1页面物理上存在但已被暂时解除映射位于备用/已修改列表中。PTE 格式Transition 状态// Valid0, Transition1, Prototype0// PageFrameNumber 物理页帧号// Protection 保护属性TempPte.u.Trans.PageFrameNumber → PFN TempPte.u.Trans.Protection → 保护属性 TempPte.u.Trans.Transition →1处理流程MiResolveTransitionFault(StoreInstruction, FaultingAddress, PointerPte, Process, OldIrql) │ ├─ 获取 PFN 和 PFN 条目 │ ├─ 递增转换故障计数MmTransitionCount │ ├─ 检查页面是否正在进行 I/OReadInProgress / WriteInProgress │ ├─ 是 → 设置 InPageBlock 事件让调用者等待 │ └─ 否 → 继续处理 │ ├─ 检查页面位置 │ ├─ ActiveAndValid → 已在使用中仅做检查 │ └─ 其他列表 → MiUnlinkPageFromList 引用计数递增 │ ├─ 设置 ShareCount、PageLocation ActiveAndValid │ ├─ 构建最终 PTE │ ├─ 从原 PTE 取 PFN 和属性位 │ ├─ 从 MmProtectToPteMask 取硬件保护位 │ └─ 根据 Modified 位设置 Dirty/Clean │ └─ MI_WRITE_VALID_PTE(PointerPte, TempPte)关键点Transition 故障是软故障soft fault不需要磁盘 I/O页面已在备用/已修改列表中只是被临时解除映射这是最快的故障类型之一页内容仍在物理内存中7. MiResolveProtoPteFault——原型 PTE 故障文件pagfault.c:1124触发条件进程的 PTE 指向一个原型 PTEPrototype PTE即该页面被多个进程共享Section 映射。处理流程MiResolveProtoPteFault(StoreInstruction, Address, PointerPte, PointerProtoPte, OutPfn, ..., Process, OldIrql) │ ├─ 读取原型 PTE共享的底层 PTE │ ├─ [原型 PTE 有效Valid1] │ ├─ 递增 PFN 的 ShareCount │ ├─ 递增转换计数 │ └─ MiCompleteProtoPteFault构建进程私有 PTE指向共享物理页 │ ├─ [原型 PTE 为零未提交的 Section] │ └─ STATUS_ACCESS_VIOLATION │ ├─ [原型 PTE 为 Transition] │ └─ MiResolveTransitionFault先在原型层面恢复页 │ ├─ [原型 PTE 为 Demand Zero] │ └─ MiResolveDemandZeroFault先在原型层面分配页 │ └─ [写时复制 COW] ├─ 先按读操作解析原型 PTE ├─ 分配新物理页 ├─ MiCopyPfn复制原页内容到新页 ├─ MiDeletePte删除指向共享页的 PTE ├─ MiInitializePfn初始化新页 ├─ 构建 PTE保护属性去掉 WRITECOPY加上 READWRITE └─ MI_WRITE_VALID_PTE写时复制COW详细流程进程 A 写入共享页 → 缺页 │ ├─ 原型 PTE 有效指向物理页 X标记为 COW │ ├─ 先按读操作完成故障建立只读映射 │ ├─ 分配新物理页 Y │ ├─ MiCopyPfn(Y, X) → 复制内容 │ ├─ 删除原 PTE → 递减共享计数 │ ├─ MiInitializePfn(Y, PointerPte, TRUE) │ ├─ 构建新 PTE去掉 WRITECOPY → READWRITE │ └─ 写有效 PTE指向新物理页 YCOW 后的状态COW 前 COW 后 进程 A PTE → [Proto] 进程 A PTE → 物理页 Y私有, RW 进程 B PTE → [Proto] 进程 B PTE → 物理页 X共享, RO │ 实际未改变 └→ 物理页 X共享, RO8. MiCheckForUserStackOverflow——用户栈自动扩展文件pagfault.c:34触发条件访问用户栈的 Guard Page保护页导致 STATUS_GUARD_PAGE_VIOLATION。处理流程MiCheckForUserStackOverflow(Address, TrapInformation) │ ├─ 检查线程状态已附加进程 ACP已禁 → 不允许栈扩展 │ ├─ 读取 TEB 中的栈信息 │ StackBase Teb-NtTib.StackBase栈基址 │ DeallocationStack Teb-DeallocationStack栈底 │ GuaranteedSize Teb-GuaranteedStackBytes保证大小 │ ├─ 计算下一个栈地址 │ NextStackAddress PAGE_ALIGN(Address) - PAGE_SIZE │ ├─ 如果快到栈底了距 DeallocationStack 不足一页 │ ├─ 在 DeallocationStack 之上提交一页 │ ├─ 更新 StackLimit │ └─ 返回 STATUS_STACK_OVERFLOW │ ├─ 否则正常扩展 │ ├─ 更新 StackLimit │ ├─ 在下一页提交带 PAGE_GUARD 的内存 │ │ ZwAllocateVirtualMemory(..., MEM_COMMIT, PAGE_READWRITE | PAGE_GUARD) │ └─ 返回 STATUS_PAGE_FAULT_GUARD_PAGE栈扩展示意图栈方向高地址 → 低地址 StackBase (高地址) ←── 栈顶部 ... ┌───────────────────────┐ │ 已提交的栈空间 │ ├───────────────────────┤ │ Guard Page当前 │ ← 本次触发了缺页 ├───────────────────────┤ ← StackLimit 更新位置 │ 新提交的 Guard Page │ ← 新保护页 ├───────────────────────┤ │ 未提交的栈空间 │ └───────────────────────┘ DeallocationStack (低地址) ←── 栈底部9. MiAccessCheck / MiIsAccessAllowed——权限检查9.1 MiIsAccessAllowed文件pagfault.c:142FORCEINLINE BOOLEANMiIsAccessAllowed(ULONG ProtectionMask,BOOLEAN Write,BOOLEAN Execute){// 32 字节查表法staticconstUCHAR AccessAllowedMask[2][2]{// 读访问{_BYTE_MASK(0,1,1,1,1,1,1,1),// 只读_BYTE_MASK(0,0,1,1,0,0,1,1)},// 可执行读// 写访问{_BYTE_MASK(0,0,0,0,1,1,1,1),// 写_BYTE_MASK(0,0,0,0,0,0,1,1)},// 可执行写};ProtectionMaskMM_PROTECT_ACCESS;return(AccessAllowedMask[Write][Execute]ProtectionMask)1;}权限表基于保护码低 3 位查表保护码只读写可执行读可执行写0 (NOACCESS)✗✗✗✗1 (READONLY)✓✗✓✗2 (EXECUTE)✗✗✓✗3 (EXECUTE_READ)✓✗✓✗4 (READWRITE)✓✓✓✓5 (WRITECOPY)✓✓(COW)✓✓(COW)6 (EXECUTE_READWRITE)✓✓✓✓7 (EXECUTE_WRITECOPY)✓✓(COW)✓✓(COW)9.2 MiAccessCheck文件pagfault.c:172NTSTATUS NTAPIMiAccessCheck(PMMPTE PointerPte,BOOLEAN StoreInstruction,KPROCESSOR_MODE PreviousMode,ULONG_PTR ProtectionMask,PVOID TrapFrame,BOOLEAN LockHeld){// 1. 检查用户态访问内核地址 → STATUS_ACCESS_VIOLATIONif((PreviousModeUserMode)(PointerPteMiHighestUserPte))returnSTATUS_ACCESS_VIOLATION;// 2. 如果 PTE 有效if(TempPte.u.Hard.Valid){// 写操作时检查可写性或 COWif(StoreInstruction){if(MI_IS_PAGE_WRITEABLE(TempPte)||MI_IS_PAGE_COPY_ON_WRITE(TempPte))returnSTATUS_SUCCESS;returnSTATUS_ACCESS_VIOLATION;}returnSTATUS_SUCCESS;// 读操作总是允许}// 3. 如果 PTE 无效查权限表if(!MiIsAccessAllowed(ProtectionMask,StoreInstruction,FALSE))returnSTATUS_ACCESS_VIOLATION;// 4. 检查 Guard Pageif((ProtectionMaskMM_PROTECT_SPECIAL)MM_GUARDPAGE){// 移除 Guard Page 位TempPte.u.Soft.ProtectionProtectionMask~MM_GUARDPAGE;MI_WRITE_INVALID_PTE(PointerPte,TempPte);returnSTATUS_GUARD_PAGE_VIOLATION;}returnSTATUS_SUCCESS;}附录pagfault.c 函数一览函数行号功能MiCheckForUserStackOverflow34用户栈自动扩展处理MiIsAccessAllowed142查表法权限判断MiAccessCheck172访问权限验证含 Guard Page 检测MiCheckVirtualAddress240检查虚拟地址是否有效查找 VADMiCheckPdeForPagedPool421确保分页池页表有效MiCheckPdeForSessionSpace341确保会话空间页表有效MiZeroPfn487通过系统 PTE 清零一个物理页MiCopyPfn533在两个物理页之间复制内容MiResolveDemandZeroFault599请求零页故障处理MiCompleteProtoPteFault775完成原型 PTE 故障构建MiResolvePageFileFault900分页文件故障处理磁盘 I/OMiResolveTransitionFault992转换故障处理备用/修改列表取回MiResolveProtoPteFault1124原型 PTE 故障处理含 COWMiDispatchFault1338故障分派主逻辑MmArmAccessFault1698缺页异常入口函数MmGetExecuteOptions2651获取 NX 执行选项MmSetExecuteOptions2693设置 NX 执行选项