Entry Bits in KVM MMU
SPTE state transition
Bit 11 / Bit 61 / SPTE_MMU_PRESENT_MASK
/ is_shadow_present_pte()
/ Shadow present PTE / KVM
这两个 bit 任何一个 bit 置上都算,这种机制是 TDX patch 引入的,原来只有 bit 11。
注意,即使是 non-leaf SPTE,也是可以 SPTE_MMU_PRESENT_MASK
的,也就是说这个和是不是 leaf SPTE 没有关系。
edea7c4fc215c7ee1cc98363b016ad505cbac9f7: KVM: x86/mmu: Use a dedicated bit to track shadow/MMU-present SPTEs
先搞清楚 shadow/MMU-present SPTE 其实是同一个概念。
Bit 11 是软件定义的 bit,硬件并没有用,ignore 掉了。
MMU-present: A MMU present SPTE is backed by actual memory and may or may not be present in hardware. MMIO SPTEs are not considered present. (并不是 backed by actual memory?),所以看来主要是为了将普通的 SPTE 和 MMIO SPTE 区分开,置上的表示这个 SPTE 指向的 HPA 是 backed by 真实的内存区间的(但是目前有没有在物理内存里是不重要的),而没有置上表示这其实是一个 MMIO,应该把访问映射到对应的 MMIO 中。
请把这个和页表里的 Present bit 区分。页表里的 present 表示这个页在物理内存里(If the present bit is set, the page is available in RAM)。
可以看一下引入以后判断的区别:
static inline bool is_shadow_present_pte(u64 pte)
{
// return (pte != 0) && !is_mmio_spte(pte) && !is_removed_spte(pte);
return !!(pte & SPTE_MMU_PRESENT_MASK);
}
pte != 0
相当于 pte != SHADOW_NONPRESENT_VALUE
(因为此时 SHADOW_NONPRESENT_VALUE
这个还没有被引入)。
可见,一个 MMU present 的,一定也是 shadow present 的,因此,大多数时候,我们判断 is_present = is_shadow_present_pte
具有更严格的条件,隐含了 shadow present 这一要求。反之则不一定。
Bit 63 / SHADOW_NONPRESENT_VALUE
KVM
看这里可以先复习一下 Virtualization Exception^。
#define SHADOW_NONPRESENT_VALUE BIT_ULL(63)
// 我们是需要保证 SHADOW_NONPRESENT_VALUE 和 SPTE_MMU_PRESENT_MASK 不是同一个 bit
static_assert(!(SHADOW_NONPRESENT_VALUE & SPTE_MMU_PRESENT_MASK));
TDX patch 引入的,这个的引入是为了 TDX 开路,原来都是直接 hard-coded 0 的。现在变成了 bit 63 是置上的情况。
引入的 patch 的名字叫做:KVM: x86/mmu: Allow non-zero value for non-present SPTE and removed SPTE
这个 bit 名字就叫 Suppress VE,表示发生 EPT violation 的时候应该发生 VM-exit 还是只生成 VE 就行了。
In VMX case,因为 vMMIO 的 handle 之前都是通过把对应的 EPT entry 置为 W/X
的(SVM 是通过 reserved bit),这样当 guest 想要访问一个 MMIO 的 GPA,在遍历这个 EPT 表的时候,会发生 EPT Misconfiguration 出来(EPT Misconfiguration 一定会 VM-exit),KVM 发现找不到对应的 kvm_memory_slot
,那么说明这是一个 MMIO。从而在 KVM 处进行 emulation。分为三步:
- 设置 MMIO SPTE 应该用的 value,也就是
VMX_EPT_WRITABLE_MASK | VMX_EPT_EXECUTABLE_MASK
,这样才能产生 EPT Misconfiguration 出来。 - 产生 EPT Violation,通过判断 slot 存不存在来看是不是一个 MMIO SPTE,是的话创建一个 MMIO SPTE
- 后续每次 guest 访问这个 GPA,都会产生 EPT Misconfiguration,从而让 KVM 来模拟 MMIO。
// 这是对应第一步
// 设置 mmio 的 bit,注意 EPT 并不是 reserved bit。
u64 __read_mostly shadow_mmio_value;
u64 __read_mostly shadow_mmio_mask;
u64 __read_mostly shadow_mmio_access_mask;
/* The mask to use to trigger an EPT Misconfiguration in order to track MMIO */
#define VMX_EPT_MISCONFIG_WX_VALUE (VMX_EPT_WRITABLE_MASK | VMX_EPT_EXECUTABLE_MASK)
vt_hardware_setup
if (enable_ept)
kvm_mmu_set_ept_masks
// EPT Misconfigurations are generated if the value of bits 2:0
// of an EPT paging-structure entry is 110b (write/execute).
kvm_mmu_set_mmio_spte_mask(VMX_EPT_MISCONFIG_WX_VALUE, VMX_EPT_RWX_MASK | VMX_EPT_SUPPRESS_VE_BIT, 0);
shadow_mmio_value = mmio_value;
shadow_mmio_mask = mmio_mask;
shadow_mmio_access_mask = access_mask;
// 这是对应第二步
// 在 MMIO SPTE 还没有置上之前,第一次访问这个 GPA 会 EPT Violation(page fault)而不是 ept misconfig,
// 我们需要通过有没有 slot 来确定这个 spte 应不应该是一个 MMIO,如果是的话创建 mmio spte,从而让后续访问
// 都触发 ept misconfig,exit 到 KVM 进行模拟。
handle_ept_violation
__vmx_handle_ept_violation
kvm_mmu_page_fault
kvm_mmu_do_page_fault
kvm_tdp_page_fault
kvm_tdp_mmu_page_fault
kvm_tdp_mmu_map
tdp_mmu_map_handle_target_level
if (unlikely(!fault->slot))
make_mmio_spte
spte |= vcpu->kvm->arch.shadow_mmio_value | access;
// 这是对应第三步
// 后续硬件遍历 mmio spte 的时候会发生 ept misconfig,
// KVM 直接当成 MMIO 来处理
handle_ept_misconfig
kvm_mmu_page_fault(vcpu, gpa, PFERR_RSVD_MASK, NULL, 0);
if (unlikely(error_code & PFERR_RSVD_MASK))
r = handle_mmio_page_fault(vcpu, cr2_or_gpa, direct);
TDX 的情况下,我们不期望生成一个 EPT Misconfiguration 从而让 KVM 全权处理,而是在 guest 后续访问的时候产生 VE,因为 TD Guest 期望在其访问 MMIO 的时候能收到一个 VE,这样它就可以 hypercall 给 KVM,把一些 KVM 需要做 MMIO emulation 相关的信息告诉 KVM(VMX 的时候不需要告诉是因为不是 private 的,KVM 直接访问就能拿到了)。总体分为三步:
- 设置 MMIO SPTE 应该用的 value,也就是
SHADOW_NONPRESENT_VALUE
,这是一个合法的值,这样才能不产生 EPT Misconfiguration。 - Guest 第一次访问产生 EPT Violation,并 exit 出来,通过判断 slot 存不存在来看是不是一个 MMIO SPTE,是的话创建一个 MMIO SPTE,也就是 0(因为 suppress VE bit 也 clear 了)。
- 后续每次 guest 访问这个 GPA,都会产生 VE,从而让 Guest TD handle 并 hypercall 给 KVM,传递给 KVM 一些模拟需要的信息。
刚开始的时候,所有 SPTE 的值都是 10000..
也就是 SHADOW_NONPRESENT_VALUE
,当第一次 EPT Violation 发生并 exit 出来后:
- 对于非 MMIO 的 SPTE,我们保持 suppress VE bit 置上,从而每次 ept violation 都会 exit 出来。这不会影响性能因为 现有 VMX 也没有用到 EPT Violation VE 这个 feature。此时映射完成后 SPTE 值变为
10000… | PFN | …
。 - 对于 MMIO 的 SPTE,我们 clear 掉 suppress VE bit,从而在后续的每次访问都会自动生成一个 VE 给 TD Guest,guest 再 hypercall 出来。因为后面 MMIO 的 SPTE 的值变为 0 而不是
SHADOW_NONPRESENT_VALUE
了,所以每次访问这个 GPA 都会产生 VE。
// 这是对应第一步
// 设置 MMIO SPTE default value 为 0 而不是 VNX 用的值,这样才能
// 保证每次产生 VE 而不是 EPT MISCONFIG
tdx_vm_init
/*
* Because guest TD is protected, VMM can't parse the instruction in TD.
* Instead, guest uses MMIO hypercall. For unmodified device driver,
* #VE needs to be injected for MMIO and #VE handler in TD converts MMIO
* instruction into MMIO hypercall.
*
* SPTE value for MMIO needs to be setup so that #VE is injected into
* TD instead of triggering EPT MISCONFIG.
* - RWX=0 so that EPT violation is triggered.
* - suppress #VE bit is cleared to inject #VE.
*/
kvm_mmu_set_mmio_spte_value(kvm, 0);
kvm->arch.shadow_mmio_value = 0;
// 这里对应第二步
// Guest TD 访问 MMIO range,因为还没有 map,同时 suppress VE is 1, 所以出现了 EPT Violation
// 我们把此 SPTE 设置成 shadow mmio value,TDX 情况下这个值是 0,从而每次 guest 访问这个地址
// 都会产生 VE 而不是 EPT MISCONFIG
__tdx_handle_exit
tdx_handle_ept_violation
__vmx_handle_ept_violation
kvm_mmu_page_fault
kvm_mmu_do_page_fault
kvm_tdp_page_fault
kvm_tdp_mmu_page_fault
kvm_tdp_mmu_map
tdp_mmu_map_handle_target_level
if (unlikely(!fault->slot))
make_mmio_spte
spte |= vcpu->kvm->arch.shadow_mmio_value | access;
// 这里对应第三步
// TD Guest handle VE 的代码
DEFINE_IDTENTRY(exc_virtualization_exception)
tdx_handle_virt_exception
handle_halt // 调用 hcall_func(EXIT_REASON_HLT) 来 hlt
read_msr // 调用 hcall_func(EXIT_REASON_MSR_READ)
write_msr // 调用 hcall_func(EXIT_REASON_MSR_WRITE)
handle_cpuid // 调用 hcall_func(EXIT_REASON_CPUID)
// 这个是重点 MMIO
handle_mmio // 告诉 MMIO 的一些信息
TDX Module 永远会打开 EPT-violation VE 这个 VMCS execution bit。
虽然名字叫做 value,但是其实也只是这一个 bit,比如我们可以附加一些额外的信息:
static u64 __private_zapped_spte(u64 old_spte)
{
return SHADOW_NONPRESENT_VALUE | SPTE_PRIVATE_ZAPPED |
(spte_to_pfn(old_spte) << PAGE_SHIFT) |
(is_large_pte(old_spte) ? PT_PAGE_SIZE_MASK : 0);
}
Bit 7 / is_large_pte()
/ is_last_spte()
/ PT_PAGE_SIZE_MASK
/ KVM
一般来说,is_leaf
就是通过这个来判断的:
bool is_leaf = is_present && is_last_spte(new_spte, level);
可以看到,这个 mask 表示这个 spte 表示的
- 是一个 leaf PTE(不是 PSE),
- 但是不是 4K 大小的,所以是 large 的。
//...
// 生成 leaf
make_spte
if (level > PG_LEVEL_4K)
spte |= PT_PAGE_SIZE_MASK;
//...
// 是个大 leaf
static inline bool is_large_pte(u64 pte)
{
return pte & PT_PAGE_SIZE_MASK;
}
// 是个大 leaf 或者是个小 leaf,总之是一个 leaf
static inline bool is_last_spte(u64 pte, int level)
{
return (level == PG_LEVEL_4K) || is_large_pte(pte);
}
0x5a0ULL / Removed SPTE / REMOVED_SPTE
/ KVM
TDP MMU 引入的一个概念。看来主要是为了:
If a thread running without exclusive control of the MMU lock must perform a multi-part operation on an SPTE, it can set the SPTE to REMOVED_SPTE
as a non-present intermediate value. Other threads which encounter this value should not modify the SPTE. 相当于为了避免额外的 lock,直接把 lock 放在 SPTE 的值里了,可以理解这是一个中间值,Freeze 住 SPTE 的值不能更改。
#define REMOVED_SPTE (SHADOW_NONPRESENT_VALUE | 0x5a0ULL)
// 从这里可以看到 REMOVED 不是 MMU PRESENT 的
static_assert(!(REMOVED_SPTE & SPTE_MMU_PRESENT_MASK));
// 从这里可以看到 REMOVED 不能是 PRIVATE_ZAPPED 的
static_assert(!(REMOVED_SPTE & SPTE_PRIVATE_ZAPPED));
在使用的时候一般也是使用:
ret = tdp_mmu_set_spte_atomic(kvm, iter, REMOVED_SPTE);
old_spte = kvm_tdp_mmu_write_spte_atomic(sptep, REMOVED_SPTE);
if (!try_cmpxchg64(sptep, &old_spte, REMOVED_SPTE))
等等原子性的原语,也可以印证这一点。
Bit 62 / SPTE_PRIVATE_ZAPPED
/ __private_zapped_spte()
/ private_zapped_spte()
KVM
看 commit:2f4147ad0ea5bf3ade46cd96bbe79f84ea8de15a,主要是为了优化 TLB shootdown?
从以下两个函数中可以看出来:
- 一个 zapped 的 SPTE,事先一定是已经
SHADOW_NONPRESENT_VALUE
的;毕竟 comment 里写了 Masks that used to track metadata for not-present SPTEs. - 一个 zapped 的 SPTE,是还保存着 PFN 的映射值的,只不过是 zap bit 置 1 了。
- 必须要是 private 的才可以。
static u64 __private_zapped_spte(u64 old_spte)
{
return SHADOW_NONPRESENT_VALUE | SPTE_PRIVATE_ZAPPED |
(spte_to_pfn(old_spte) << PAGE_SHIFT) |
(is_large_pte(old_spte) ? PT_PAGE_SIZE_MASK : 0);
}
static u64 private_zapped_spte(struct kvm *kvm, const struct tdp_iter *iter)
{
if (!kvm_gfn_shared_mask(kvm))
return SHADOW_NONPRESENT_VALUE;
if (!is_private_sptep(iter->sptep))
return SHADOW_NONPRESENT_VALUE;
return __private_zapped_spte(iter->old_spte);
}
static inline bool is_private_zapped_spte(u64 spte)
{
return !!(spte & SPTE_PRIVATE_ZAPPED);
}
- 当一个 SPTE 变成 zapped 时或者
- 进一步清空以 zapped SPTE 到 not present 同时也是 not zapped 时
都会执行这个 SEAMCALL:TDH.MEM.PAGE.REMOVE
。
当然,也是可以一步直接从 normal zap 到清空的,比如 ZAP_PRIVATE_REMOVE
。
那么为什么要留着 PFN 的值呢?
中间的 PSE 可以是 zapped 状态吗?
为什么要引入 zapped 这个中间状态?它和 removed SPTE 有什么关系?