EPTの設定をbhyveで調べる

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で仕事をする人が読む本 (アスキードワンゴ)

はじめてUNIXで仕事をする人が読む本 (アスキードワンゴ)