KVMよりもEPTの使用前提なbhyveのほうがコード読みやすかったのです。Nested Paging in bhyveというFreeBSDのメモリ管理からbhyveでのEPT周りの実装を解説した論文があったのも理由としては大きいですね。
EPTPの設定
vmx_vminit()という関数でeptpの設定をする関数を呼びます。
814 static void * 815 vmx_vminit(struct vm *vm, pmap_t pmap) 816 { 817 uint16_t vpid[VM_MAXCPU]; ~ 略 ~ 827 } 828 vmx->vm = vm; 829 830 vmx->eptp = eptp(vtophys((vm_offset_t)pmap->pm_pml4));
pmap_tはpmap構造体で、PML4のアドレスを保持している(LinuxだとPGD)。pml4_entry_tはu_int64_tの別名(typedef)。
309 /* 310 * The kernel virtual address (KVA) of the level 4 page table page is always 311 * within the direct map (DMAP) region. 312 */ 313 struct pmap { 314 struct mtx pm_mtx; 315 pml4_entry_t *pm_pml4; /* KVA of level 4 page table */ 316 uint64_t pm_cr3; 317 TAILQ_HEAD(,pv_chunk) pm_pvchunk; /* list of mappings in pmap */ 318 cpuset_t pm_active; /* active on cpus */ 319 enum pmap_type pm_type; /* regular or nested tables */ 320 struct pmap_statistics pm_stats; /* pmap statistics */ 321 struct vm_radix pm_root; /* spare page table pages */ 322 long pm_eptgen; /* EPT pmap generation id */ 323 int pm_flags; 324 struct pmap_pcids pm_pcids[MAXCPU]; 325 };
eptp()はこのような処理。
195 uint64_t 196 eptp(uint64_t pml4) 197 { 198 uint64_t eptp_val; 199 200 eptp_val = pml4 | (EPT_PWLEVELS - 1) << 3 | PAT_WRITE_BACK; 201 if (ept_enable_ad_bits) 202 eptp_val |= EPT_ENABLE_AD_BITS; 203 204 return (eptp_val); 205 }
この関数はExtended-Page-Table Pointer (EPTP)の設定をしています。EPTPはIntel SDM Vol3の24.6.11 Extended-Page-Table Pointer (EPTP)に説明があります。
まず200行目を見ます。EPT_PWLEVELSは4で、PAT_WRITE_BACKは0x6です。そうすると、以下のようなbit列になります。
>>> bin(3 << 3 | 0x6) '0b11110'
EPTPのbit2:0は0もしくは6を設定する仕様です。ここではPAT_WRITE_BACKで6を設定しています。次にbit5:3がEPT page-walk lengthとなっています。bit5:3は0b11なので3です。 202行目のEPT_ENABLE_AD_BITSは(1 << 6)です。1<<6は2進数で0b1000000なので6bit目を1にしてます。EPTPの6bit目はAccess/Dirty flagを有効にする設定です。EPTPのbit11:7は予約済みで、bit N-1:12はSDMには「Bits N–1:12 of the physical address of the 4-KByte aligned EPT PML4 table 3」とあります。Nは「N is the physical-address width supported by the logical processor.」とのことです。bit63:Nは予約済みです。なので、bit6:0までを設定するのがEPTPの設定ですね。
EPTの設定
vmx_init()からept_init()を呼びます。
ここでvmx_init()までをφ(..)メモメモ
初期化の関数はvmm_ops構造体のinit変数にvmx_init()を設定する。
3423 struct vmm_ops vmm_ops_intel = { 3424 vmx_init,
呼び出し方はVMM_INITマクロで定義されている。
169 static struct vmm_ops *ops; 170 #define VMM_INIT(num) (ops != NULL ? (*ops->init)(num) : 0)
vmm_init()からvmx_init()を呼び出している。
321 static int 322 vmm_init(void) 323 { 324 int error; 325 326 vmm_host_state_init(); 327 328 vmm_ipinum = lapic_ipi_alloc(&IDTVEC(justreturn)); 329 if (vmm_ipinum < 0) 330 vmm_ipinum = IPI_AST; 331 332 error = vmm_mem_init(); 333 if (error) 334 return (error); 335 336 if (vmm_is_intel()) 337 ops = &vmm_ops_intel; 338 else if (vmm_is_amd()) 339 ops = &vmm_ops_amd; 340 else 341 return (ENXIO); 342 343 vmm_resume_p = vmm_resume; 344 345 return (VMM_INIT(vmm_ipinum)); 346 }
で、本題に戻ってept_init()。
77 int 78 ept_init(int ipinum) 79 { 80 int use_hw_ad_bits, use_superpages, use_exec_only; 81 uint64_t cap; 82 83 cap = rdmsr(MSR_VMX_EPT_VPID_CAP); 84 85 /* 86 * Verify that: 87 * - page walk length is 4 steps 88 * - extended page tables can be laid out in write-back memory 89 * - invvpid instruction with all possible types is supported 90 * - invept instruction with all possible types is supported 91 */ 92 if (!EPT_PWL4(cap) || 93 !EPT_MEMORY_TYPE_WB(cap) || 94 !INVVPID_SUPPORTED(cap) || 95 !INVVPID_ALL_TYPES_SUPPORTED(cap) || 96 !INVEPT_SUPPORTED(cap) || 97 !INVEPT_ALL_TYPES_SUPPORTED(cap)) 98 return (EINVAL); 99 100 ept_pmap_flags = ipinum & PMAP_NESTED_IPIMASK; 101 102 use_superpages = 1; 103 TUNABLE_INT_FETCH("hw.vmm.ept.use_superpages", &use_superpages); 104 if (use_superpages && EPT_PDE_SUPERPAGE(cap)) 105 ept_pmap_flags |= PMAP_PDE_SUPERPAGE; /* 2MB superpage */ 106 107 use_hw_ad_bits = 1; 108 TUNABLE_INT_FETCH("hw.vmm.ept.use_hw_ad_bits", &use_hw_ad_bits); 109 if (use_hw_ad_bits && AD_BITS_SUPPORTED(cap)) 110 ept_enable_ad_bits = 1; 111 else 112 ept_pmap_flags |= PMAP_EMULATE_AD_BITS; 113 114 use_exec_only = 1; 115 TUNABLE_INT_FETCH("hw.vmm.ept.use_exec_only", &use_exec_only); 116 if (use_exec_only && EPT_SUPPORTS_EXEC_ONLY(cap)) 117 ept_pmap_flags |= PMAP_SUPPORTS_EXEC_ONLY; 118 119 return (0); 120 }
最初にMSRからEPTをVPIDのケーパビリティを読み出します。これはSDM Vol3のA.10 VPID AND EPT CAPABILITIESに説明があります。そして、必要な機能が使えるかチェックしてます。あとはLinuxで言うところのsysctlで設定されたデータの読み出しと、フラグの設定ですね。
amd64/vmm/intel/ept.cにはもう一つ名前にinitが付く関数があります。それはept_pinit()です。この関数はept_vmspace_alloc()の処理から呼ばれます。
181 struct vmspace * 182 ept_vmspace_alloc(vm_offset_t min, vm_offset_t max) 183 { 184 185 return (vmspace_alloc(min, max, ept_pinit)); 186 } 187
vmspace_alloc()はメモリ管理サブシステムの関数です。3番目の引数にept_pinit()を渡すことで、初期化処理の関数としてept_pinit()を呼ぶようにしています。これは論文によるとVMMをサポートするためにこのような形になったようです。
ept_pinit()はこのような関数です。こちらもメモリ管理サブシステムのほうの関数を呼びます。
174 static int 175 ept_pinit(pmap_t pmap) 176 { 177 178 return (pmap_pinit_type(pmap, PT_EPT, ept_pmap_flags)); 179 }
pmap_pinit_type()はbyhveの追加時に新規に作られた関数とのことです。こちらもEPTのためですね。
2407 /* 2408 * Initialize a preallocated and zeroed pmap structure, 2409 * such as one in a vmspace structure. 2410 */ 2411 int 2412 pmap_pinit_type(pmap_t pmap, enum pmap_type pm_type, int flags) 2413 { 2414 vm_page_t pml4pg; 2415 vm_paddr_t pml4phys; 2416 int i; 2417 2418 /* 2419 * allocate the page directory page 2420 */ 2421 while ((pml4pg = vm_page_alloc(NULL, 0, VM_ALLOC_NORMAL | 2422 VM_ALLOC_NOOBJ | VM_ALLOC_WIRED | VM_ALLOC_ZERO)) == NULL) 2423 VM_WAIT; 2424 2425 pml4phys = VM_PAGE_TO_PHYS(pml4pg); 2426 pmap->pm_pml4 = (pml4_entry_t *)PHYS_TO_DMAP(pml4phys); 2427 CPU_FOREACH(i) { 2428 pmap->pm_pcids[i].pm_pcid = PMAP_PCID_NONE; 2429 pmap->pm_pcids[i].pm_gen = 0; 2430 } 2431 pmap->pm_cr3 = ~0; /* initialize to an invalid value */ 2432 2433 if ((pml4pg->flags & PG_ZERO) == 0) 2434 pagezero(pmap->pm_pml4); 2435 2436 /* 2437 * Do not install the host kernel mappings in the nested page 2438 * tables. These mappings are meaningless in the guest physical 2439 * address space. 2440 */ 2441 if ((pmap->pm_type = pm_type) == PT_X86) { 2442 pmap->pm_cr3 = pml4phys; 2443 pmap_pinit_pml4(pml4pg); 2444 } 2445 2446 pmap->pm_root.rt_root = 0; 2447 CPU_ZERO(&pmap->pm_active); 2448 TAILQ_INIT(&pmap->pm_pvchunk); 2449 bzero(&pmap->pm_stats, sizeof pmap->pm_stats); 2450 pmap->pm_flags = flags; 2451 pmap->pm_eptgen = 0; 2452 2453 return (1); 2454 }
pm_typeとしてPT_EPTを渡しているので2441行目のところは実行されません。ここは通常のページテーブルの設定の場合のみに実行ですね。それ以外はpmap構造体の設定でpml4の物理アドレを設定したりとかしてます。
次に気になるのはept_vmspace_alloc()が何時呼ばれるのか?ですね。
ept_vmspace_alloc()の呼ばれ方
vmx_init()と同様にvmm_ops構造体に関数を設定しています。設定先の変数はvmspace_allocです。この関数も直接は呼び出さないでマクロのVMSPACE_ALLOCマクロから呼ばれます。
178 #define VMSPACE_ALLOC(min, max) \ 179 (ops != NULL ? (*ops->vmspace_alloc)(min, max) : NULL)
vm_create()がVMSPACE_ALLOCマクロを使っています。
422 int 423 vm_create(const char *name, struct vm **retvm) 424 { 425 struct vm *vm; 426 struct vmspace *vmspace; 427 428 /* 429 * If vmm.ko could not be successfully initialized then don't attempt 430 * to create the virtual machine. 431 */ 432 if (!vmm_initialized) 433 return (ENXIO); 434 435 if (name == NULL || strlen(name) >= VM_MAX_NAMELEN) 436 return (EINVAL); 437 438 vmspace = VMSPACE_ALLOC(0, VM_MAXUSER_ADDRESS); 439 if (vmspace == NULL) 440 return (ENOMEM); 441 442 vm = malloc(sizeof(struct vm), M_VM, M_WAITOK | M_ZERO); 443 strcpy(vm->name, name); 444 vm->vmspace = vmspace; 445 mtx_init(&vm->rendezvous_mtx, "vm rendezvous lock", 0, MTX_DEF); 446 447 vm_init(vm, true); 448 449 *retvm = vm; 450 return (0); 451 }
vm_create()を呼んでいるのはsysctl_vmm_create()です。なのでVMの作成時に初期化処理の流れで呼ばれる感じですね。
まとめ
EPTPはeptl()で設定します。EPTに使うページテーブルの設定はept_init()でケーパビリティのチェックやフラグの設定をしてからpmap_pinit_type()でpmap構造体の設定を行うことで設定しています。
(´-`).。oO(BSD系のコードは初見でも読みやすい
( ´ー`)フゥー...
はじめてUNIXで仕事をする人が読む本 (アスキードワンゴ)
- 作者: 木本雅彦,松山直道,稲島大輔
- 出版社/メーカー: ドワンゴ
- 発売日: 2018/07/23
- メディア: Kindle版
- この商品を含むブログを見る