linux内核 kvm_mmu_page_fault 分析


int kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gpa_t cr2_or_gpa, u64 error_code,
		       void *insn, int insn_len)
{
	int r, emulation_type = 0;
	bool direct = vcpu->arch.mmu->direct_map;

	/* With shadow page tables, fault_address contains a GVA or nGPA.  */
	if (vcpu->arch.mmu->direct_map) {
		vcpu->arch.gpa_available = true;
		vcpu->arch.gpa_val = cr2_or_gpa;
	}

	r = RET_PF_INVALID;
	if (unlikely(error_code & PFERR_RSVD_MASK)) {
		r = handle_mmio_page_fault(vcpu, cr2_or_gpa, direct);
		if (r == RET_PF_EMULATE)
			goto emulate;
	}

	if (r == RET_PF_INVALID) {
		r = vcpu->arch.mmu->page_fault(vcpu, cr2_or_gpa,
					       lower_32_bits(error_code),
					       false);
		WARN_ON(r == RET_PF_INVALID);
	}

	if (r == RET_PF_RETRY)
		return 1;
	if (r < 0)
		return r;

	/*
	 * Before emulating the instruction, check if the error code
	 * was due to a RO violation while translating the guest page.
	 * This can occur when using nested virtualization with nested
	 * paging in both guests. If true, we simply unprotect the page
	 * and resume the guest.
	 */
	if (vcpu->arch.mmu->direct_map &&
	    (error_code & PFERR_NESTED_GUEST_PAGE) == PFERR_NESTED_GUEST_PAGE) {
		kvm_mmu_unprotect_page(vcpu->kvm, gpa_to_gfn(cr2_or_gpa));
		return 1;
	}

	/*
	 * vcpu->arch.mmu.page_fault returned RET_PF_EMULATE, but we can still
	 * optimistically try to just unprotect the page and let the processor
	 * re-execute the instruction that caused the page fault.  Do not allow
	 * retrying MMIO emulation, as it's not only pointless but could also
	 * cause us to enter an infinite loop because the processor will keep
	 * faulting on the non-existent MMIO address.  Retrying an instruction
	 * from a nested guest is also pointless and dangerous as we are only
	 * explicitly shadowing L1's page tables, i.e. unprotecting something
	 * for L1 isn't going to magically fix whatever issue cause L2 to fail.
	 */
	if (!mmio_info_in_cache(vcpu, cr2_or_gpa, direct) && !is_guest_mode(vcpu))
		emulation_type = EMULTYPE_ALLOW_RETRY;
emulate:
	/*
	 * On AMD platforms, under certain conditions insn_len may be zero on #NPF.
	 * This can happen if a guest gets a page-fault on data access but the HW
	 * table walker is not able to read the instruction page (e.g instruction
	 * page is not present in memory). In those cases we simply restart the
	 * guest, with the exception of AMD Erratum 1096 which is unrecoverable.
	 */
	if (unlikely(insn && !insn_len)) {
		if (!kvm_x86_ops->need_emulation_on_page_fault(vcpu))
			return 1;
	}

	return x86_emulate_instruction(vcpu, cr2_or_gpa, emulation_type, insn,
				       insn_len);
}
EXPORT_SYMBOL_GPL(kvm_mmu_page_fault);

kvm_vcpu_ioctl(KVM_RUN)
    kvm_arch_vcpu_ioctl_run
        vcpu_run
            vcpu_enter_guest
                kvm_x86_ops->handle_exit(vcpu)
                    vmx_handle_exit
                        handle_ept_violation

1. 函数功能

此函数用于处理客户机(Guest)的页错误(Page Fault),是KVM内存管理单元(MMU)的核心逻辑之一。主要任务包括处理直接物理映射、MMIO访问、嵌套虚拟化错误和指令模拟。


1.1 参数解析
  1. struct kvm_vcpu *vcpu:指向发生页错误的虚拟CPU。
  2. gpa_t cr2_or_gpa:触发错误的地址(可能是GVA或GPA,具体取决于映射模式)。
  3. u64 error_code:错误码,包含错误类型(如权限错误、保留位错误等)。
  4. void *insn, int insn_len:触发错误的指令及其长度(用于模拟)。

1.2 关键步骤解析
1.2.1. 初始化与直接映射处理
bool direct = vcpu->arch.mmu->direct_map;
if (vcpu->arch.mmu->direct_map) {
    vcpu->arch.gpa_available = true;
    vcpu->arch.gpa_val = cr2_or_gpa;
}
  • direct_map:表示是否使用直接物理映射(如Intel EPT或AMD NPT)。
  • 若启用直接映射,将cr2_or_gpa保存为GPA,供后续处理使用。

1.2.2. 处理保留位错误(MMIO访问)
r = RET_PF_INVALID;
if (unlikely(error_code & PFERR_RSVD_MASK)) {
    r = handle_mmio_page_fault(vcpu, cr2_or_gpa, direct);
    if (r == RET_PF_EMULATE)
        goto emulate;
}
  • PFERR_RSVD_MASK:表示页表中存在保留位错误,通常由MMIO访问触发。
  • handle_mmio_page_fault:尝试处理MMIO页错误,若需模拟指令(RET_PF_EMULATE),跳转到指令模拟阶段。

1.2.3. 调用MMU的页错误处理
if (r == RET_PF_INVALID) {
    r = vcpu->arch.mmu->page_fault(vcpu, cr2_or_gpa,
                                   lower_32_bits(error_code), false);
    WARN_ON(r == RET_PF_INVALID);
}
  • 若未处理MMIO错误,调用底层MMU的page_fault方法(如tdp_page_fault)。
  • 参数lower_32_bits(error_code)提取错误码的低32位(兼容性处理)。

1.2.4. 错误处理分支
if (r == RET_PF_RETRY) return 1;  // 需重试处理
if (r < 0) return r;              // 返回错误
  • RET_PF_RETRY:需重新执行操作(如TLB刷新后)。
  • r < 0:返回错误码(如KVM_PFN_ERR_*)。

1.2.5. 嵌套虚拟化特殊处理
if (vcpu->arch.mmu->direct_map &&
    (error_code & PFERR_NESTED_GUEST_PAGE) == PFERR_NESTED_GUEST_PAGE) {
    kvm_mmu_unprotect_page(vcpu->kvm, gpa_to_gfn(cr2_or_gpa));
    return 1;
}
  • PFERR_NESTED_GUEST_PAGE:嵌套虚拟化中因只读(RO)页触发的错误。
  • kvm_mmu_unprotect_page:解除页面保护,允许后续访问。

1.2.6. 指令模拟优化
if (!mmio_info_in_cache(vcpu, cr2_or_gpa, direct) && !is_guest_mode(vcpu))
    emulation_type = EMULTYPE_ALLOW_RETRY;
  • EMULTYPE_ALLOW_RETRY:允许处理器重新执行指令(避免重复模拟MMIO)。

1.2.7. 处理AMD平台特殊错误
if (unlikely(insn && !insn_len)) {
    if (!kvm_x86_ops->need_emulation_on_page_fault(vcpu))
        return 1;
}
  • AMD某些情况下可能无法获取指令长度(如Erratum 1096),需检查是否需要强制模拟。

1.2.8. 最终指令模拟
return x86_emulate_instruction(vcpu, cr2_or_gpa, emulation_type, insn, insn_len);
  • 调用x86_emulate_instruction模拟触发页错误的指令。

1.3 关键设计点
  1. 直接映射 vs 影子页表

    • 直接映射(如EPT/NPT)下,cr2_or_gpa是GPA;否则为GVA(需地址转换)。
  2. MMIO与保留位错误

    • 利用保留位(RSVD)标记MMIO区域,触发快速路径处理。
  3. 嵌套虚拟化优化

    • 直接解除页面保护而非完整模拟,减少性能开销。
  4. 指令重试机制

    • 通过EMULTYPE_ALLOW_RETRY避免不必要的模拟,提升性能。

1.4 小结

此函数是KVM处理客户机页错误的核心逻辑,涵盖从错误分类(MMIO、嵌套错误)到指令模拟的全流程,通过条件分支和硬件特性优化性能。

2、vcpu->arch.mmu->page_fault 代码结构解析

r = vcpu->arch.mmu->page_fault(
    vcpu, 
    cr2_or_gpa, 
    lower_32_bits(error_code), 
    false
);
2.1. vcpu->arch.mmu->page_fault
  • 本质:这是一个函数指针,指向当前MMU模式对应的缺页处理函数。
  • 动态绑定:具体指向的函数取决于虚拟机的分页模式:
    • EPT模式(Intel CPU):指向tdp_page_fault()arch/x86/kvm/mmu/tdp_mmu.c
    • 影子页表模式(传统模式):指向FNAME(page_fault)arch/x86/kvm/mmu/paging_tmpl.h
    • 非分页模式:指向nonpaging_page_fault()arch/x86/kvm/mmu/mmu.c
2.2. 参数解析
参数类型作用
vcpustruct kvm_vcpu*当前虚拟CPU的上下文
cr2_or_gpagpa_t客户机物理地址(GPA)或CR2寄存器值(客户机虚拟地址)
lower_32_bits(error_code)u32错误码的低32位(包含缺页类型:写/读/执行权限等)
falsebool通常表示prefetch标志(是否为预取触发的缺页)

2.3、底层实现细节
a. **MMU模式动态分发
  • 初始化绑定:在虚拟机启动时,根据CPU特性(如是否支持EPT)设置vcpu->arch.mmu结构体。
  • 示例绑定流程
    // arch/x86/kvm/mmu/mmu.c
    void kvm_init_mmu(struct kvm_vcpu *vcpu) {
        if (tdp_enabled)
            vcpu->arch.mmu->page_fault = tdp_page_fault;
        else if (is_paging(vcpu))
            vcpu->arch.mmu->page_fault = FNAME(page_fault);
        else
            vcpu->arch.mmu->page_fault = nonpaging_page_fault;
    }
    
b. **典型处理函数:tdp_page_fault(以EPT模式为例)
// arch/x86/kvm/mmu/tdp_mmu.c
int tdp_page_fault(struct kvm_vcpu *vcpu, gpa_t gpa, u32 error_code, bool prefetch) {
    // 1. 检查是否需要处理大页(2M/1G)
    if (try_async_pf(vcpu, prefetch, gpa, &pfn, &map_writable))
        return RET_PF_RETRY;
    
    // 2. 调用核心映射函数
    return kvm_tdp_mmu_map(vcpu, gpa, error_code, map_writable, pfn, prefetch);
}
c. 关键操作:kvm_tdp_mmu_map
  • 遍历EPT页表:从根页表(EPT Level 4)逐级向下查找目标GPA对应的页表项。
  • 修复映射
    • 若中间层页表不存在,调用kvm_tdp_mmu_map_handle_target_level()创建新页表。
    • 最终通过__handle_changed_spte()原子写入最终页表项。

2.4、错误码(error_code)的解码
Bit位含义KVM处理逻辑
0 §Present(页面不存在)分配物理页
1 (W/R)写操作触发检查写权限
2 (U/S)用户模式访问校验用户权限
3 (RSVD)保留位异常处理大页分裂
4 (I/D)指令获取触发NX位校验

通过lower_32_bits(error_code)提取有效位后,KVM会根据这些标志调整页表权限。


2.5、返回值(r)的意义
返回值宏定义含义
0RET_PF_CONTINUE处理成功
1RET_PF_RETRY需要重试(如异步页故障)
2RET_PF_EMULATE需要模拟指令
3RET_PF_INVALID无效请求(如非法地址)

2.6、完整调用流程示例
  1. 触发缺页:客户机访问未映射的GPA,触发VM-Exit。
  2. 分发处理:KVM通过vcpu->arch.mmu->page_fault调用具体处理函数。
  3. 物理页分配:若需要,调用kvm_mmu_map_page()分配主机物理页(HPA)。
  4. 更新EPT:通过原子操作(如cmpxchg64)写入EPT页表项。
  5. 返回客户机:若成功,客户机恢复执行;否则注入异常(如#PF)。

2.7、调试技巧
  1. Tracepoints
    echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_mmu_page_fault/enable
    cat /sys/kernel/debug/tracing/trace_pipe
    
  2. KVM调试日志
    dmesg -w | grep "kvm_mmu"
    

通过理解这一行代码的分发机制,可以深入掌握KVM缺页处理的多模式适配和**动态绑定设计

3、QEMU与KVM的协作流程

步骤QEMU的行为KVM的行为与EPT的关系
1. 内存初始化调用memory_region_init_ram()定义虚拟机内存区域无直接操作为EPT提供客户机物理内存(GPA)布局
2. 内存注册通过kvm_set_user_memory_region()将内存区域注册到KVM创建EPT影子页表框架建立HVA(Host Virtual Address)到GPA的初步映射
3. 虚拟机启动调用ioctl(KVM_RUN)启动VCPU进入VMX非根模式,启用EPT硬件硬件自动使用EPT进行GPA→HPA转换
4. 缺页处理处理KVM_EXIT_MMIO等异常(如设备模拟)捕获EPT_VIOLATION,动态填充EPT页表项按需构建EPT页表

3.1、QEMU间接驱动EPT构建的关键步骤
3.11. 定义虚拟机内存布局

QEMU通过MemoryRegion结构描述客户机物理地址空间(GPA空间):

// 示例:分配4GB内存
ram_size = 4ULL * 1024 * 1024 * 1024;
memory_region_allocate_system_memory(&ram_memory, NULL, "pc.ram", ram_size);
memory_region_add_subregion(system_memory, 0, &ram_memory);

此时仅建立逻辑上的内存布局,尚未涉及EPT

3.1.2. 向KVM注册内存区域

QEMU通过KVM_SET_USER_MEMORY_REGION ioctl将内存区域信息传递给KVM:

struct kvm_userspace_memory_region region = {
    .slot = 0,
    .guest_phys_addr = 0,        // GPA起始地址
    .memory_size = ram_size,     // 内存大小
    .userspace_addr = host_addr, // QEMU进程的虚拟地址(HVA)
};
ioctl(kvm_fd, KVM_SET_USER_MEMORY_REGION, &region);

KVM收到此信息后:

  • 建立EPT顶层页表结构(PML4)
  • HVA→HPA的映射关系暂存,但EPT页表项(PTE)此时为空
3.1.3. 触发EPT页表动态填充

当虚拟机首次访问某块内存时:

  1. CPU检测到EPT页表缺失(EPT_VIOLATION)
  2. KVM捕获该异常,调用kvm_tdp_page_fault()
  3. KVM根据QEMU注册的HVA→GPA映射,结合宿主机页表(HVA→HPA)填充EPT页表项
  4. 更新EPT后,VM继续执行

3.2、技术细节:EPT页表构建的硬件加速
EPT页表层级结构(以4级页表为例)
EPT PML4 (Level 4) → EPT PDPT (Level 3) → EPT PD (Level 2) → EPT PT (Level 1)
  • KVM使用TDX(Two-Dimensional Paging)算法自动管理层级结构
  • 硬件自动缓存EPT页表(EPT TLB),无需软件维护
性能优化机制
机制QEMU的关联操作对EPT的影响
大页(Huge Page)使用-mem-path /dev/hugepagesKVM直接填充2MB/1GB EPT大页项,减少页表层级
脏页跟踪memory_region_set_log(true)KVM通过EPT写保护位监控内存修改
内存去重Balloon驱动交互KVM合并相同EPT项

3.3、调试与观察方法
1. 查看当前EPT状态

通过KVM调试接口获取EPT信息:

# 需内核启用CONFIG_KVM_MMU_AUDIT
echo 1 > /sys/kernel/debug/kvm/print_pte
2. QEMU Trace日志

启用EPT相关事件跟踪:

qemu-system-x86_64 -trace "kvm_mmu_*" ...
3. GDB调试KVM模块
# 在内核中设置断点
b kvm_tdp_page_fault

3.4、特殊场景处理
1. EPT缺页模拟(Emulated MMIO)

当虚拟机访问未映射的GPA时:

  • KVM触发EPT_VIOLATION
  • QEMU收到KVM_EXIT_MMIO事件
  • QEMU模拟设备响应,并调用kvm_set_phys_mem()更新EPT映射
2. 动态内存热插拔

QEMU添加新内存区域后:

kvm_set_user_memory_region(new_region); // 触发KVM更新EPT

3.5小论:

QEMU并不直接构建EPT页表,而是通过以下方式驱动KVM完成:

  1. 配置内存布局:定义客户机物理地址空间(GPA)
  2. 注册内存区域:通过KVM_API将HVA→GPA映射告知KVM
  3. 响应缺页事件:处理需要软件模拟的内存访问

EPT页表的具体填充完全由KVM在硬件辅助下动态完成,整个过程对QEM透明。理解这一分工机制是优化虚拟机内存性能的关键。

4. 修改ept页表

4.1. 理解EPT结构

EPT是四层页表结构:

EPT PML4 → EPT PDPT → EPT PD → EPT PT

每个表项(Entry)包含物理地址映射和权限位(读/写/执行)。


4.2. 定位目标EPT页表项
步骤1:获取目标GPA(Guest Physical Address)

确定需要修改的虚拟机物理地址GPA

步骤2:计算各级索引

对64位地址分段(以4KB页为例):

  • Bits 47:39 → PML4索引
  • Bits 38:30 → PDPT索引
  • Bits 29:21 → PD索引
  • Bits 20:12 → PT索引

4.3. 修改EPT页表项
方法1:通过VMM代码修改(如KVM/QEMU)
// 伪代码示例:在KVM中操作EPT
struct kvm *kvm = ...;          // 目标虚拟机
gpa_t gpa = 0x12345678;         // 目标GPA
u64 new_hpa = 0xabcd0000;       // 新HPA(Host Physical Address)

// 获取EPT页表项指针
u64 *ept_entry = get_ept_entry(kvm, gpa);

// 修改物理地址和权限位
*ept_entry = new_hpa | EPT_READ | EPT_WRITE;
方法2:通过内存直接修改(需Ring0权限)

在内核模块中直接操作EPT页表物理内存:

// 定位EPT页表物理地址
phys_addr_t ept_pml4 = vmcs_read64(EPT_POINTER);

// 逐级遍历页表层级
u64 *pml4_entry = phys_to_virt(ept_pml4) + pml4_index * 8;
u64 pdpt_phys = *pml4_entry & PAGE_MASK;
// ... 递归遍历到目标PTE后修改

4.4. 刷新TLB

修改后需刷新EPT缓存:

INVEPT rax, [inv_ept_descriptor]  ; x86指令

4.5. 注意事项
  1. 权限控制:确保修改后的权限不会破坏虚拟机隔离性。
  2. 同步问题:修改EPT时需暂停虚拟机(vCPU pause)。
  3. 错误处理:错误映射会导致虚拟机崩溃或主机不稳定。
  4. 兼容性:不同CPU代际的EPT实现细节可能不同。

4.6. 调试工具
  • EPT Violation监控:通过VMX_EXIT_REASON_EPT_VIOLATION捕获非法访问。
  • Intel PT:使用处理器跟踪技术分析EPT行为。

此操作需深入理解虚拟化架构,建议在开发/测试环境中验证,谨慎用于生产环境。

5. __direct_map 跟踪pte页表 trace_kvm_mmu_spte_requested

5.1 trace_kvm_mmu_spte_requested(gpa, level, pfn) 功能介绍

trace_kvm_mmu_spte_requested 是 KVM(Kernel-based Virtual Machine)中用于调试和跟踪内存管理单元(MMU)操作的关键跟踪点(tracepoint),主要记录虚拟机在请求映射或更新 **EPT(Extended Page Table)页表项(SPTE)**时的关键信息。它在以下场景中被触发:

  • 虚拟机首次访问某物理地址(GPA)时,触发EPT页表项的分配。
  • 因权限变更(如写保护、执行权限调整)需更新EPT映射。
  • 内存热迁移或大页分裂时,需要重建EPT映射。

5.2 参数详解
  1. gpa(Guest Physical Address)

    • 虚拟机物理地址,表示客户机(Guest)尝试访问的物理地址。
    • 用途:定位EPT页表项对应的客户机内存区域,例如调试内存访问冲突(如EPT Violation)。
  2. level(页表层级)

    • 映射的页表层级(4级EPT结构中的某一级),取值范围:1~4。
      • 1级:1GB大页(EPT PDPT直接指向1GB页)
      • 2级:2MB大页(EPT PD直接指向2MB页)
      • 3级:4KB页(EPT PT指向4KB页)
      • 4级:保留(部分架构可能扩展)
    • 用途:分析大页拆分或合并行为,优化内存映射效率。
  3. pfn(Host Physical Frame Number)

    • 主机物理页帧号,表示GPA最终映射到的主机物理内存页。
    • 用途:验证客户机物理地址到主机物理地址的映射是否正确,排查跨虚拟机内存泄漏或越界访问。

5.3 典型应用场景
1. 调试EPT映射错误
  • 当虚拟机因EPT Violation导致退出(VM-Exit)时,通过该跟踪点可快速定位:
    • 检查gpa是否在预期范围内。
    • 检查pfn是否指向合法的主机内存区域。
    • 验证level是否符合预期(如是否错误拆分了本应保留的大页)。
2. 性能优化
  • 高频触发trace_kvm_mmu_spte_requested可能表明:
    • 过度页分裂:频繁从大页拆分为小页(如2MB→4KB),需优化内存预分配。
    • 内存碎片化:客户机内存访问模式不连续,导致频繁新建EPT项。
3. 安全审计
  • 监控pfn的合法性,防止恶意虚拟机通过EPT映射访问主机或其他虚拟机内存(如“幽灵”漏洞利用)。

5.4 使用方法
步骤1:启用跟踪点
# 查看所有KVM MMU跟踪点
sudo ls /sys/kernel/debug/tracing/events/kvm/kvm_mmu/

# 启用特定跟踪点
echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_mmu/spte_requested/enable
步骤2:捕获跟踪数据

使用perf工具或trace-cmd记录事件:

# 使用perf
sudo perf record -e kvm:kvm_mmu_spte_requested -a

# 使用trace-cmd
sudo trace-cmd record -e kvm:kvm_mmu_spte_requested
步骤3:分析输出

输出格式示例:

kvm_mmu_spte_requested: gpa=0x7f5a4000 level=3 pfn=0x2ac8f00
  • gpa=0x7f5a4000:客户机试图访问的物理地址。
  • level=3:映射为4KB页(EPT PT层级)。
  • pfn=0x2ac8f00:对应的主机物理页帧号。

注意事项
  1. 性能影响:跟踪点会引入额外开销,生产环境中需谨慎启用。
  2. 权限要求:需root权限或CAP_SYS_ADMIN能力。
  3. 内核版本差异:跟踪点名称或参数可能随内核版本变化(建议核对内核文档)。

扩展工具
  • ftrace:结合其他KVM MMU事件(如kvm_mmu_get_pagekvm_mmu_sync_page)综合分析。
  • crash工具:直接解析EPT页表内容(需内核转储文件)。

通过此跟踪点,开发者可以深入理解KVM的内存管理行为,快速诊断虚拟化环境中的内存映射问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值