mm学习笔记_05:虚拟内存核心与系统调用

📅 2026/7/22 22:41:08 👁️ 阅读次数 📝 编程学习
mm学习笔记_05:虚拟内存核心与系统调用

mm学习笔记_05:虚拟内存核心与系统调用

基于 ReactOS ARM3 MM 源码(ntoskrnl/mm/ARM3/virtual.c)


1. virtual.c 概述

文件位置d:\reactos\ntoskrnl\mm\ARM3\virtual.c

职责:虚拟内存管理的核心,实现以下关键系统调用:

  • NtAllocateVirtualMemory— 分配虚拟内存
  • NtFreeVirtualMemory— 释放虚拟内存
  • NtProtectVirtualMemory— 修改内存保护属性
  • MmGetPhysicalAddress— 虚拟地址转物理地址
  • MmCopyVirtualMemory— 跨进程内存拷贝

2. NtAllocateVirtualMemory——完整路径

文件:virtual.c:4457

2.1 函数签名

NTSTATUS NTAPINtAllocateVirtualMemory(IN HANDLE ProcessHandle,IN OUT PVOID*BaseAddress,IN ULONG_PTR ZeroBits,IN OUT PSIZE_T RegionSize,IN ULONG AllocationType,IN ULONG Protect);

2.2 步骤一:参数验证

// 1. ZeroBits 检查(不能超过最大值)if(ZeroBits>MI_MAX_ZERO_BITS)returnSTATUS_INVALID_PARAMETER_3;// 2. AllocationType 合法性检查(必须包含 MEM_COMMIT / MEM_RESERVE / MEM_RESET)if(AllocationType&~(MEM_COMMIT|MEM_RESERVE|MEM_RESET|...))returnSTATUS_INVALID_PARAMETER_5;if(!(AllocationType&(MEM_COMMIT|MEM_RESERVE|MEM_RESET)))returnSTATUS_INVALID_PARAMETER_5;// 3. MEM_RESET 独占检查if((AllocationType&MEM_RESET)&&(AllocationType!=MEM_RESET))returnSTATUS_INVALID_PARAMETER_5;// 4. MEM_LARGE_PAGES 必须带 MEM_COMMITif((AllocationType&MEM_LARGE_PAGES)&&!(AllocationType&MEM_COMMIT))returnSTATUS_INVALID_PARAMETER_5;// 5. MEM_WRITE_WATCH 必须带 MEM_RESERVEif((AllocationType&MEM_WRITE_WATCH)&&!(AllocationType&MEM_RESERVE))returnSTATUS_INVALID_PARAMETER_5;// 6. MEM_PHYSICAL 必须带 MEM_RESERVE 且 Protect == PAGE_READWRITEif((AllocationType&MEM_PHYSICAL)&&...)...// 7. 保护属性验证ProtectionMask=MiMakeProtectionMask(Protect);if(ProtectionMask==MM_INVALID_PROTECTION)returnSTATUS_INVALID_PAGE_PROTECTION;// 8. 用户态参数捕获(SEH 保护)_SEH2_TRY{if(PreviousMode!=KernelMode){ProbeForWritePointer(UBaseAddress);ProbeForWriteSize_t(URegionSize);}PBaseAddress=*UBaseAddress;PRegionSize=*URegionSize;}_SEH2_EXCEPT(...);// 9. 地址范围检查if(PBaseAddress>MM_HIGHEST_VAD_ADDRESS)returnSTATUS_INVALID_PARAMETER_2;if(!PRegionSize)returnSTATUS_INVALID_PARAMETER_4;

2.3 步骤二:进程获取与附加

// 如果是当前进程,直接使用if(ProcessHandle==NtCurrentProcess())Process=CurrentProcess;else{// 否则以 PROCESS_VM_OPERATION 权限引用目标进程Status=ObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_OPERATION,PsProcessType,...);// 如果目标进程不是当前进程,附加到目标进程地址空间if(CurrentProcess!=Process){KeStackAttachProcess(&Process->Pcb,&ApcState);Attached=TRUE;}}

2.4 步骤三:MEM_RESERVE 主路径

当未指定基址或指定 MEM_RESERVE 时:

1. 配额计费 PsChargeProcessNonPagedPoolQuota(sizeof(MMVAD_LONG)) 2. 分配 VAD:ExAllocatePoolWithTag(NonPagedPool, sizeof(MMVAD_LONG), 'SdaV') 3. 初始化 VAD: - Vad->u.VadFlags.PrivateMemory = 1 - Vad->u.VadFlags.Protection = ProtectionMask - if (AllocationType & MEM_COMMIT) Vad->u.VadFlags.MemCommit = 1 4. 插入 VAD(自动查找空闲地址): Status = MiInsertVadEx(Vad, &StartingAddress, PRegionSize, HighestAddress, MM_VIRTMEM_GRANULARITY, AllocationType) 5. 写回用户参数(SEH 保护): *URegionSize = PRegionSize; *UBaseAddress = (PVOID)StartingAddress;

2.5 步骤四:MEM_COMMIT 主路径

当指定已有地址(即对已预留区进行提交):

1. 锁定地址空间:MmLockAddressSpace(AddressSpace) 2. 检查进程是否将终止 3. 查找冲突/匹配 VAD: Result = MiCheckForConflictingNode(StartVpn, EndVpn, &Process->VadRoot, ...) 4. 验证 VAD 类型(不能是 VadAwe/VadDevicePhysicalMemory/VadLargePages) 5. 验证地址范围在 VAD 内 6. 如果是非私有内存(Section 映射): - 获取原型 PTE - 锁定 MmSectionCommitMutex - 遍历 PTE 写入模板 PTE - 更新 NumberOfCommittedPages 7. 如果是私有内存: - 更新 CommitCharge 和 Process->CommitCharge - MiLockProcessWorkingSetUnsafe - 遍历每个 PTE: - MiMakePdeExistAndMakeValid(确保页表存在) - 写入 DemandZero 格式的 PTE(保护属性 + 软件 PTE 标记) - MiUnlockProcessWorkingSetUnsafe

完整流程图

NtAllocateVirtualMemory │ ├─ 参数验证 │ ├─ ZeroBits 范围检查 │ ├─ AllocationType 组合检查 │ ├─ Protect 合法性检查 │ ├─ 用户态参数 Probe │ └─ 地址范围检查 │ ├─ 进程获取与附加(KeStackAttachProcess) │ ├─ [MEM_RESERVE 分支] │ ├─ 配额计费 │ ├─ 分配 VAD(ExAllocatePoolWithTag) │ ├─ VAD 初始化 │ └─ MiInsertVadEx(自动搜索 + Avl 插入) │ ├─ [MEM_COMMIT 分支] │ ├─ MmLockAddressSpace │ ├─ 查找已有 VAD(MiCheckForConflictingNode) │ ├─ 验证 VAD 类型 │ ├─ [Section 内存] → 写原型 PTE → 更新段提交计数 │ ├─ [私有内存] → 更新 CommitCharge │ │ └─ 锁定工作集 │ │ └─ 遍历 PTE,确保 PDE 有效,写 DemandZero PTE │ └─ 解锁地址空间 │ └─ 返回 BaseAddress + STATUS_SUCCESS

3. NtFreeVirtualMemory——完整路径

3.1 MEM_RELEASE 分支(完整 VAD 释放)

四种释放情况

情况1:释放整个 VAD(BaseAddress == VAD.StartingVpn, Size == VAD 全范围) ─ 直接删除 VAD,释放所有物理页 情况2:释放 VAD 头部部分(BaseAddress == VAD.StartingVpn, Size < VAD 全范围) ─ 调整 VAD.StartingVpn = 原起始地址 + Size ─ 释放对应 PTE 情况3:释放 VAD 尾部部分(BaseAddress > VAD.StartingVpn, 释放到结束) ─ 调整 VAD.EndingVpn = BaseAddress - 1 ─ 释放对应 PTE 情况4:释放 VAD 中间部分(释放中间一块,两边保留) ─ 需要创建新 VAD 保存剩余后半部分 ─ 原 VAD 缩小到前半部分
释放前: ┌──────────────────────────────┐ │ 原始 VAD │ └──────────────────────────────┘ 情况1: 情况2: ┌──────────────────┐ ┌─────┐ ┌──────────┐ │ 全部释放 → 删除 │ │释放 │ │ 保留 │ └──────────────────┘ └─────┘ └──────────┘ Base StartingVpn 调整 情况3: 情况4: ┌──────────┐ ┌─────┐ ┌─────┐ ┌──────┐ ┌─────┐ │ 保留 │ │释放 │ │释放 │ │ 保留1│ │保留2│ └──────────┘ └─────┘ └─────┘ └──────┘ └─────┘ (需要新建 VAD)

3.2 MEM_DECOMMIT 分支(取消提交)

// 1. 锁定地址空间// 2. 查找 VAD,确认地址范围有效// 3. 调用 MiDecommitPages 取消提交:// - 遍历指定范围的 PTE// - 对每个有效 PTE → 释放物理页、更新 PFN// - 将 PTE 写为 decommit 状态// 4. 更新进程 CommitCharge

4. NtProtectVirtualMemory 路径

通过MiProtectVirtualMemory实现:

1. 验证参数(保护属性合法性) 2. 锁定地址空间 3. 查找覆盖整个范围的 VAD 4. 调用 MiCheckSecuredVad 检查安全 VAD 限制 5. 锁定工作集 6. 遍历范围内的每个 PTE: a. 如果 PTE 有效: - 修改硬件 PTE 的保护位 - 刷新 TLB(KeFlushCurrentTb) b. 如果 PTE 无效: - 修改软件 PTE 的保护位 c. 写时复制(COW)处理: - 如果新保护为只读但原为可写,清除脏位 7. 更新 VAD 的保护属性 8. 解锁

5. MiDeletePte——底层 PTE 删除逻辑

文件:virtual.c:369

功能:删除单个 PTE,释放其引用的物理页。

VOID NTAPIMiDeletePte(IN PMMPTE PointerPte,IN PVOID VirtualAddress,IN PEPROCESS CurrentProcess,IN PMMPTE PrototypePte){TempPte=*PointerPte;if(TempPte.u.Hard.Valid==0){// 无效 PTE:可能是 Transition 或 PageFile 状态if(TempPte.u.Soft.Transition){// Transition PTE → 从列表中拆除,递减引用MiUnlinkPageFromList(Pfn1);MiDecrementReferenceCount(Pfn1,PageFrameIndex);}// 忽略已换出(PageFile)的 PTE}else{// 有效 PTEPageFrameIndex=PFN_FROM_PTE(&TempPte);Pfn1=MiGetPfnEntry(PageFrameIndex);if(Pfn1->u3.e1.PrototypePte==1){// 原型 PTE → 递减页表共享计数和 PFN 共享计数MiDecrementShareCount(PageTablePfn,...);MiDecrementShareCount(Pfn1,PageFrameIndex);}else{// 普通 PTE → 标记删除,递减共享计数MI_SET_PFN_DELETED(Pfn1);MiDecrementShareCount(Pfn1,PageFrameIndex);}// 擦除 PTEMI_ERASE_PTE(PointerPte);}KeFlushCurrentTb();}

6. MiDeleteVirtualAddresses——批量 PTE 删除

文件:virtual.c:530

功能:批量删除指定虚拟地址范围内的所有 PTE。

VOID NTAPIMiDeleteVirtualAddresses(IN ULONG_PTR Va,IN ULONG_PTR EndingAddress,IN OPTIONAL PMMVAD Vad){while(Va<=EndingAddress){// 逐级检查 PXE → PPE → PDE → PTE 的有效性// 跳过无效的大页表区域(AddressGap)// 对每个有效 PTE 调用 MiDeletePte// 递减页表引用计数,必要时删除 PDEVa+=PAGE_SIZE;}}

关键特点

  • 支持多级页表(x86 PAE / x86-64)的地址间隙跳过
  • 对 Section VAD 的间隙处理:通过MI_GET_PROTOTYPE_PTE_FOR_VPN跳到下一个原型 PTE
  • 批量删除过程中保持 PFN 锁定

7. MiDecommitPages——页面取消提交

功能:将已提交的页面取消提交,释放物理内存但保留 VAD 结构。

1. 遍历指定地址范围的每个 PTE 2. 对每个 PTE: a. 如果 PTE 有效(Valid=1): - 获取 PFN - 标记 PFN 已删除(MI_SET_PFN_DELETED) - 递减 ShareCount - 擦除 PTE b. 如果 PTE 是 Transition 状态: - 从列表中拆除 PFN - 递减引用计数 c. 如果 PTE 是 DemandZero 状态: - 直接擦除 PTE 3. 更新进程/系统提交计数 4. 刷新 TLB

8. MmGetPhysicalAddress——虚拟地址转物理地址

文件:virtual.c:5681

PHYSICAL_ADDRESS NTAPIMmGetPhysicalAddress(PVOID Address){// 1. 检查 PDE 是否有效(对大页 SPI 还检查 PPE/PXE)if(MiAddressToPde(Address)->u.Hard.Valid){// 2. 检查是否为大页(LargePage)TempPde=*MiAddressToPde(Address);if(TempPde.u.Hard.LargePage){// 大页偏移 = 地址低 22 位(4MB 大页)PhysicalAddress.QuadPart=(ULONG64)TempPde.u.Hard.PageFrameNumber<<PAGE_SHIFT;PhysicalAddress.QuadPart+=((ULONG_PTR)Address&(PAGE_SIZE*PTE_PER_PAGE-1));returnPhysicalAddress;}// 3. 检查 PTE 是否有效TempPte=*MiAddressToPte(Address);if(TempPte.u.Hard.Valid){// 物理地址 = PFN << 12 + 页内偏移PhysicalAddress.QuadPart=(ULONG64)TempPte.u.Hard.PageFrameNumber<<PAGE_SHIFT;PhysicalAddress.QuadPart+=((ULONG_PTR)Address&(PAGE_SIZE-1));returnPhysicalAddress;}}// 4. 无效地址,返回 0PhysicalAddress.QuadPart=0;returnPhysicalAddress;}

9. MmCopyVirtualMemory——跨进程内存拷贝

文件:virtual.c:1269

两种策略,基于数据量选择:

9.1 大拷贝(> 512 字节):MDL 映射方式MiDoMappedCopy

BufferSize > MI_POOL_COPY_BYTES (512) → MiDoMappedCopy 流程: 1. 循环拷贝,每次最大 14 页(MI_MAPPED_COPY_PAGES * PAGE_SIZE = 56KB) 2. 附加到源进程 → ProbeForRead → MmProbeAndLockPages → MmMapLockedPagesSpecifyCache 3. 分离源进程 4. 附加到目标进程 → ProbeForWrite → RtlCopyMemory 5. 分离目标进程 6. MmUnmapLockedPages → MmUnlockPages 7. 继续下一批数据

9.2 小拷贝(≤ 512 字节):池缓冲方式MiDoPoolCopy

BufferSize <= MI_POOL_COPY_BYTES (512) → MiDoPoolCopy 流程: 1. 如果 BufferSize <= 512,使用栈上缓冲(StackBuffer),否则分配 NonPagedPool 2. 附加到源进程 → ProbeForRead → RtlCopyMemory(池, 源) 3. 分离源进程 4. 附加到目标进程 → ProbeForWrite → RtlCopyMemory(目标, 池) 5. 分离目标进程 6. 重复直到所有数据拷贝完毕

入口函数

NTSTATUS NTAPIMmCopyVirtualMemory(PEPROCESS SourceProcess,PVOID SourceAddress,PEPROCESS TargetProcess,PVOID TargetAddress,SIZE_T BufferSize,KPROCESSOR_MODE PreviousMode,PSIZE_T ReturnSize){if(!BufferSize)returnSTATUS_SUCCESS;// 获取目标进程的 rundown protectionif(!ExAcquireRundownProtection(&Process->RundownProtect))returnSTATUS_PROCESS_IS_TERMINATING;if(BufferSize>MI_POOL_COPY_BYTES)Status=MiDoMappedCopy(SourceProcess,SourceAddress,...);elseStatus=MiDoPoolCopy(SourceProcess,SourceAddress,...);ExReleaseRundownProtection(&Process->RundownProtect);returnStatus;}

附录:virtual.c 函数一览

函数行号功能
MiCalculatePageCommitment40计算指定地址范围内的已提交页数
MiMakeSystemAddressValid181确保系统页表地址有效(触发缺页)
MiDeleteSystemPageableVm273删除可分页系统内存
MiDeletePte367删除单个 PTE
MiDeleteVirtualAddresses528批量删除 PTE
MiDoMappedCopy793MDL 方式跨进程拷贝
MiDoPoolCopy1018池缓冲方式跨进程拷贝
MmCopyVirtualMemory1269跨进程内存拷贝入口
MiProtectVirtualMemory22 (extern)修改内存保护属性
MiGetPageProtection1359获取页面的保护属性
NtAllocateVirtualMemory4457虚拟内存分配系统调用
MmGetPhysicalAddress5681虚拟地址→物理地址转换