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 参数解析
struct kvm_vcpu *vcpu
:指向发生页错误的虚拟CPU。gpa_t cr2_or_gpa
:触发错误的地址(可能是GVA或GPA,具体取决于映射模式)。u64 error_code
:错误码,包含错误类型(如权限错误、保留位错误等)。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 关键设计点
-
直接映射 vs 影子页表
- 直接映射(如EPT/NPT)下,
cr2_or_gpa
是GPA;否则为GVA(需地址转换)。
- 直接映射(如EPT/NPT)下,
-
MMIO与保留位错误
- 利用保留位(RSVD)标记MMIO区域,触发快速路径处理。
-
嵌套虚拟化优化
- 直接解除页面保护而非完整模拟,减少性能开销。
-
指令重试机制
- 通过
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
)
- EPT模式(Intel CPU):指向
2.2. 参数解析
参数 | 类型 | 作用 |
---|---|---|
vcpu | struct kvm_vcpu* | 当前虚拟CPU的上下文 |
cr2_or_gpa | gpa_t | 客户机物理地址(GPA)或CR2寄存器值(客户机虚拟地址) |
lower_32_bits(error_code) | u32 | 错误码的低32位(包含缺页类型:写/读/执行权限等) |
false | bool | 通常表示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)的意义
返回值 | 宏定义 | 含义 |
---|---|---|
0 | RET_PF_CONTINUE | 处理成功 |
1 | RET_PF_RETRY | 需要重试(如异步页故障) |
2 | RET_PF_EMULATE | 需要模拟指令 |
3 | RET_PF_INVALID | 无效请求(如非法地址) |
2.6、完整调用流程示例
- 触发缺页:客户机访问未映射的GPA,触发VM-Exit。
- 分发处理:KVM通过
vcpu->arch.mmu->page_fault
调用具体处理函数。 - 物理页分配:若需要,调用
kvm_mmu_map_page()
分配主机物理页(HPA)。 - 更新EPT:通过原子操作(如
cmpxchg64
)写入EPT页表项。 - 返回客户机:若成功,客户机恢复执行;否则注入异常(如#PF)。
2.7、调试技巧
- Tracepoints:
echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_mmu_page_fault/enable cat /sys/kernel/debug/tracing/trace_pipe
- 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, ®ion);
KVM收到此信息后:
- 建立EPT顶层页表结构(PML4)
- 将
HVA→HPA
的映射关系暂存,但EPT页表项(PTE)此时为空
3.1.3. 触发EPT页表动态填充
当虚拟机首次访问某块内存时:
- CPU检测到EPT页表缺失(EPT_VIOLATION)
- KVM捕获该异常,调用
kvm_tdp_page_fault()
- KVM根据QEMU注册的
HVA→GPA
映射,结合宿主机页表(HVA→HPA)填充EPT页表项 - 更新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/hugepages | KVM直接填充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完成:
- 配置内存布局:定义客户机物理地址空间(GPA)
- 注册内存区域:通过
KVM_API
将HVA→GPA映射告知KVM - 响应缺页事件:处理需要软件模拟的内存访问
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. 注意事项
- 权限控制:确保修改后的权限不会破坏虚拟机隔离性。
- 同步问题:修改EPT时需暂停虚拟机(vCPU pause)。
- 错误处理:错误映射会导致虚拟机崩溃或主机不稳定。
- 兼容性:不同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 参数详解
-
gpa
(Guest Physical Address)- 虚拟机物理地址,表示客户机(Guest)尝试访问的物理地址。
- 用途:定位EPT页表项对应的客户机内存区域,例如调试内存访问冲突(如EPT Violation)。
-
level
(页表层级)- 映射的页表层级(4级EPT结构中的某一级),取值范围:1~4。
- 1级:1GB大页(EPT PDPT直接指向1GB页)
- 2级:2MB大页(EPT PD直接指向2MB页)
- 3级:4KB页(EPT PT指向4KB页)
- 4级:保留(部分架构可能扩展)
- 用途:分析大页拆分或合并行为,优化内存映射效率。
- 映射的页表层级(4级EPT结构中的某一级),取值范围:1~4。
-
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
:对应的主机物理页帧号。
注意事项
- 性能影响:跟踪点会引入额外开销,生产环境中需谨慎启用。
- 权限要求:需root权限或
CAP_SYS_ADMIN
能力。 - 内核版本差异:跟踪点名称或参数可能随内核版本变化(建议核对内核文档)。
扩展工具
ftrace
:结合其他KVM MMU事件(如kvm_mmu_get_page
、kvm_mmu_sync_page
)综合分析。crash
工具:直接解析EPT页表内容(需内核转储文件)。
通过此跟踪点,开发者可以深入理解KVM的内存管理行为,快速诊断虚拟化环境中的内存映射问题。