X86 nested virtualization: memory management

We were in L1, and L1 calls vmlaunch or vmresume.
It traps to L0, and L0 starts to emulate it to run L2 in nested_vmx_run() function. At the end of the function, it sets nested_run_pending. By setting this flag, L0 will run L2 later somehow :)

nested_vmx_run()
∟ prepare_vmcs02()
∟ nested_ept_init_mmu_context()
∟ kvm_init_shadow_ept_mmu() /* set kvm_mmu struct */
∟ set walk_mmu /* walk_mmu = nested_mmu */
∟ kvm_mmu_reset_context()
∟ init_kvm_mmu()
∟ init_kvm_nested_mmu() /* set kvm_mmu struct */
(init_kvm_tdp_mmu() for L1 guest)
...
vmx->nested.nested_run_pending = 1
  1. kvm_init_shadow_ept_mmu()
context->page_fault = ept_page_fault;
context->gva_to_gpa = ept_gva_to_gpa;
context->root_hpa = INVALID_PAGE;
context->shadow_root_level = kvm_x86_ops->get_tdp_level(); /* 4, which equals to PT64_ROOT_LEVEL */
context->root_level = context->shadow_root_level;

2. walk_mmu

vcpu->arch.walk_mmu = &vcpu->arch.nested_mmu;

이것은 나중에 mmu_is_nested() 함수에서 VM이 nested인지 아닌지를 판단하는데 쓰인다.

이후에 kvm_mmu_reset_context()에서는 init_kvm_mmu()를 호출하며 kvm_mmu struct를 설정한다. (e.g. get_cr3, inject_page_fault 등). 여기서 mmu_is_nested() 를 사용하여 VM or nVM에 따라 init_kvm_tdp_mmu() 또는 init_kvm_nested_mmu()를 호출한다.

nested_run_pending 이것을 설정했기 때문에, 다시 L2로 돌아가게 된다. 이 과정에서 kvm_mmu_load()를 시작으로 eptp를 설정한다. (* direct_map은 실제로 일어나야 하는 address translation과 hardware가 제공하는 # of translation이 같을 때만 true로 set 된다. e.g. KVM guest를 돌리고 있을 때 hardware가 npt나 ept를 제공하는 경우가 되겠다. software적으로 shadow를 만들어야 하면 이건 direct가 될 수 없다 .그러니까 shadow)

vcpu_run()
...
∟ kvm_mmu_reload ()
∟ kvm_mmu_load() /* only when root is not set yet */
∟ mmu_alloc_roots(vcpu) /* this sets root_hpa */
∟ mmu_alloc_shadow_roots /* direct_map is not set for L1? */
∟ vcpu->arch.mmu.set_cr3(vcpu, vcpu->arch.mmu.root_hpa);
∟ vmx_set_cr3()
∟ eptp = construct_eptp(cr3); /* vcpu->arch.mmu.root_hpa */
∟ vmcs_write64(EPT_POINTER, eptp);

자 그럼 이제 L2로 돌아가게 된다.

참고로, paging64_page_fault 같이 paging64 prefix가 붙는 함수들은 paging_tmpl.h에 정의가 되어 있는데, 이 함수들은 init_kvm_softmmu()함수가 불려서 초기화가 되어야지만 사용된다. 즉, HW를 이용한 address translation에서는 쓰이지 않는것 같다. 다만, shadow ETP에 쓰이는 듯. 확인 필요.

EPT violation이란 무었이냐?

guest physical에 해당하는 page가 machine physical에 존재하지 않거나 (not present) 존재하지만 permission을 위반하여 접근한 경우.

EPT viloation이 발생했을 때, 어떻게 다루나?

(일단 x86에는 virtual address / physical address 사이에 linear address라는게 있다. virtual address는 segment + offset으로 이루어지는데, 이걸 해석해서 linear address를 만든다. 즉 모든 segment들의 주소가 linear하게 합쳐져 있다는 뜻)

KVM와 Xen의 page fault handler를 살펴본다.

Xen의 경우, EPT violation handling에 대해서 comment를 달아놓았다.

xen/arch/x86/mm/hap/nested_hap.c

/* AlGORITHM for NESTED PAGE FAULT
*
* NOTATION
* Levels: L0, L1, L2
* Guests: L1 guest, L2 guest
* Hypervisor: L0 hypervisor
* Addresses: L2-GVA, L2-GPA, L1-GVA, L1-GPA, MPA
*
* On L0, when #NPF happens, the handler function should do:
* hap_page_fault(GPA)
* {
* 1. If #NPF is from L1 guest, then we crash the guest VM (same as old
* code)
* 2. If #NPF is from L2 guest, then we continue from (3)
* 3. Get np2m base from L1 guest. Map np2m base into L0 hypervisor address
* space.
* 4. Walk the np2m’s page table
* 5. — if not present or permission check failure, then we inject #NPF
* back to L1 guest and
* re-launch L1 guest (L1 guest will either treat this #NPF as MMIO,
* or fix its p2m table for L2 guest)
* 6. — if present, then we will get the a new translated value L1-GPA
* (points to L1 machine memory)
* 7. * Use L1-GPA to walk L0 P2M table
* 8. — if not present, then crash the guest (should not happen)
* 9. — if present, then we get a new translated value MPA
* (points to real machine memory)
* 10. * Finally, use GPA and MPA to walk nested_p2m
* and fix the bits.
* }
*
*/
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.