mm学习笔记_06:缺页异常处理
mm学习笔记_06:缺页异常处理
基于 ReactOS ARM3 MM 源码(ntoskrnl/mm/ARM3/pagfault.c)
1. 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:1698
CPU 检测到缺页(CR2 = 故障地址,错误码在栈上) │ ▼ KiTrap0E (x86 汇编中断处理程序) │ ▼ MmArmAccessFault(FaultCode, Address, Mode, TrapInformation)2.1 第一阶段:高 IRQL 故障处理
MmArmAccessFault(ULONG FaultCode,PVOID Address,...){OldIrql=KeGetCurrentIrql();if(OldIrql>APC_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(Address>=MmSystemRangeStart){// 内核地址空间缺页// 不允许用户态触发内核缺页if(Mode==UserMode)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;}// 获取工作集(系统缓存或会话空间)WorkingSet=&MmSystemCacheWs;// 或 MmSessionSpace->...// 锁定工作集 → 重新检查 PTE → 分派故障MiLockWorkingSet(CurrentThread,WorkingSet);if(TempPte.u.Hard.Valid){/* 已解决 */}else{// 处理原型 PTE 或分派故障Status=MiDispatchFault(FaultCode,Address,PointerPte,ProtoPte,FALSE,CurrentProcess,...);}MiUnlockWorkingSet(CurrentThread,WorkingSet);if(Status==STATUS_NO_MEMORY){MmRebalanceMemoryConsumersAndWait();gotoRetryKernel;}returnStatus;}2.3 第三阶段:用户地址空间故障(UserFault)
UserFault: CurrentProcess = PsGetCurrentProcess(); MiLockProcessWorkingSet(CurrentProcess, CurrentThread); // 1. 检查各级页表(PXE → PPE → PDE → PTE)有效性 // 对无效的页表项,通过 MiResolveDemandZeroFault 按需分配页表页 // 2. 读取 PTE TempPte = *PointerPte; // 3. 如果 PTE 有效(Valid=1): 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) { // 查找 VAD(MiCheckVirtualAddress) // 如果是 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}// 顺序2:Transition PTE?if(TempPte.u.Soft.Transition){// → MiResolveTransitionFault}// 顺序3:页面在分页文件中?if(TempPte.u.Soft.PageFileHigh!=0){// → MiResolvePageFileFault}// 顺序4:只能是 Demand Zero PTEASSERT(TempPte.u.Hard.Valid==0);ASSERT(TempPte.u.Soft.Prototype==0);ASSERT(TempPte.u.Soft.Transition==0);ASSERT(TempPte.u.Soft.PageFileHigh==0);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 │ ├─ 初始化 PFN:MiInitializePfn(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 → 硬件 PTE(Valid=1, PFN=X) │ (Valid=0, Protection=XX)│ └─────────┘ └─────────┘ 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) │ ├─ 初始化 PFN:MiInitializePfn(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 状态):
// Valid=0, Transition=1, Prototype=0// 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/O(ReadInProgress / 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 指向一个原型 PTE(Prototype PTE),即该页面被多个进程共享(Section 映射)。
处理流程:
MiResolveProtoPteFault(StoreInstruction, Address, PointerPte, PointerProtoPte, OutPfn, ..., Process, OldIrql) │ ├─ 读取原型 PTE(共享的底层 PTE) │ ├─ [原型 PTE 有效(Valid=1)]: │ ├─ 递增 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(共享, RO)8. 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:142
FORCEINLINE 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)},// 可执行写};ProtectionMask&=MM_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:172
NTSTATUS NTAPIMiAccessCheck(PMMPTE PointerPte,BOOLEAN StoreInstruction,KPROCESSOR_MODE PreviousMode,ULONG_PTR ProtectionMask,PVOID TrapFrame,BOOLEAN LockHeld){// 1. 检查用户态访问内核地址 → STATUS_ACCESS_VIOLATIONif((PreviousMode==UserMode)&&(PointerPte>MiHighestUserPte))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((ProtectionMask&MM_PROTECT_SPECIAL)==MM_GUARDPAGE){// 移除 Guard Page 位TempPte.u.Soft.Protection=ProtectionMask&~MM_GUARDPAGE;MI_WRITE_INVALID_PTE(PointerPte,TempPte);returnSTATUS_GUARD_PAGE_VIOLATION;}returnSTATUS_SUCCESS;}附录:pagfault.c 函数一览
| 函数 | 行号 | 功能 |
|---|---|---|
MiCheckForUserStackOverflow | 34 | 用户栈自动扩展处理 |
MiIsAccessAllowed | 142 | 查表法权限判断 |
MiAccessCheck | 172 | 访问权限验证(含 Guard Page 检测) |
MiCheckVirtualAddress | 240 | 检查虚拟地址是否有效(查找 VAD) |
MiCheckPdeForPagedPool | 421 | 确保分页池页表有效 |
MiCheckPdeForSessionSpace | 341 | 确保会话空间页表有效 |
MiZeroPfn | 487 | 通过系统 PTE 清零一个物理页 |
MiCopyPfn | 533 | 在两个物理页之间复制内容 |
MiResolveDemandZeroFault | 599 | 请求零页故障处理 |
MiCompleteProtoPteFault | 775 | 完成原型 PTE 故障构建 |
MiResolvePageFileFault | 900 | 分页文件故障处理(磁盘 I/O) |
MiResolveTransitionFault | 992 | 转换故障处理(备用/修改列表取回) |
MiResolveProtoPteFault | 1124 | 原型 PTE 故障处理(含 COW) |
MiDispatchFault | 1338 | 故障分派主逻辑 |
MmArmAccessFault | 1698 | 缺页异常入口函数 |
MmGetExecuteOptions | 2651 | 获取 NX 执行选项 |
MmSetExecuteOptions | 2693 | 设置 NX 执行选项 |
