Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

BitVisorのUEFIブート時におけるページテーブル切り替え処理

More than 1 year has passed since last update.

先日VirtualBox 6.0がリリースされました.リリースノートには書いてありませんが,ついにNested Virtualizationがサポートされたという噂を耳にしたので,今回はVirtualBox上でBitVisorが動作するのか検証しよう..と思ってましたが,よくよく調べてみると現時点ではAMDのCPU(の一部)をサポートということらしく,あいにく手持ちのAMD CPUが無いので断念しました.AMDなCPUお持ちの方,試してみてはいかがでしょうか1

ということで別の話ということで,BitVisorのUEFIブート時(BIOSブート時もですが)には何回かページテーブルを切り替える処理が存在します.今回はその話です.

1. ブート用ページテーブルへの切り替え

UEFIのファームウェアは64bit環境であればlong modeで開始されます.このときページテーブルは仮想アドレス=物理アドレスとなるIdentity mappingが設定されています.

loadvmm.efiからentry.sのエントリポイントにジャンプした際,以下のようなページテーブルに切り替えます.

image.png

このためのページテーブルがentry_pml4,entry_pdp,entry_pd0,entry_pdです.

BitVisorのコードは0x40100000からのアドレスに配置されるので(リンカスクリプト),そのためのページテーブルを作成しています.このとき2MBページングを使って,一つのPDPTEエントリで0x40000000-0x7FFFFFFFの1GBの領域をカバーするようにしています.

PTPTE[0]のIdentity mappingはマッピング切り替え用に存在します.
x86でのページテーブルの切り替えはmov cr3命令を使うだけですが,問題となるのはmov cr3を実行した直後から仮想アドレスと物理アドレスのマッピングが変わるため,次の命令を実行した際にページフォールトにならないように適切にマッピングを作成しておく必要があるという点です.
UEFIファームウェアはIdentity Mappingで動作しており,さらにloadvmm.efiはBitVisorを物理アドレス1GB以下の領域に配置するので2,このPDPTE[0]のエントリを作成しておけば,このページテーブルをロードしてもページテーブルが発生することはありません.

実際には以下のように絶対アドレスジャンプを利用して,Identity MappingからBitVisorのアドレス空間へ切り替えます.

    mov %rax, %cr3
    mov $start_stack, %rsp
    mov $1f, %rax
    jmp *%rax        // Change RIP 
1:                   // Use 0x40000000- address

ページテーブル作成処理はこのあたりです.

ちなみに,UEFIファームウェアのページテーブルから独自のページテーブルへ切り替えた際は,割り込み無効化状態で動作させます.これはUEFIの仕様に基づくものです.

2. UEFI関数呼び出しのための切り替え (その1)

無事ページテーブルが切り替えられたら,その後はuefi.cのエントリポイントへ飛んでいきますが,ここで何回かUEFIファームウェアの関数を呼び出しています.
このとき,ページテーブルを元のUEFIファームウェアが使っていたidentity mappingなページテーブルに戻す必要があります.

このための関数がuefi_entry_callです.

asm
uefi_entry_call:
    mov uefi_entry_physoff(%rip),%rax
    call    uefi_entry_rip_plus_rax
    mov %rsp,%rsi
    mov uefi_entry_rsp(%rip),%rsp
    and $~0xF,%rsp  # Force 16-byte alignment; see calluefi
    mov uefi_entry_cr3(%rip),%rax
    mov %rax,%cr3
    cld
    sti
    xchg    %rcx,%rdx
    push    %r9
    push    %r8
    push    %rdx
    push    %rcx
    call    *%rdi
    cli
    mov %rsi,%rsp
    lea entry_pml4(%rip),%rsi
    mov %rsi,%cr3
    ret
...
uefi_entry_rip_plus_rax:
    add %rax,(%rsp)
    ret

uefi_entry_physoffには名前から推測するに物理アドレスと仮想アドレスの(負の)オフセットが記録されていると思われるので,call uefi_entry_rip_plus_raxを利用してその値を戻り値に加算してreturnすることで,PDPTE[1]のアドレス空間から,PDPTE[0]の空間へと切り替えることができます.
その後は引数をスタックに積みUEFIの関数を呼び出します.関数呼び出し後は再びページテーブルをBitVisorのものに戻します.

UEFIアドレス空間からのデータコピーをおこなうuefi_entry_pcpyも同様のページテーブル切り替え処理をおこなっています.

ちなみに, uefi_entry_callは以下のように呼ばれています.

uefi_entry_call (uefi_free_pages, 0, alloc_addr64, freesize);

3. メインのエントリ用のページテーブルの切り替え

loadvmm.efiはbitvisor.elfの先頭64KBのみをロードしており,uefi.cのuefi_init()関数でbitvisor.elf全体がロードされます.bitvisor.elfのロードが終わると,その読み込んだ物理アドレスを引数としてuefi_entry_startに飛びます.このあとvmm_main()を呼ぶのですが,その前に,ロードしたbitvisor.elfを適切にマップする必要があります.uefi_entry_startの先頭でこのためのページテーブルの修正をしています.

uefi_entry_start:
.if longmode
    mov uefi_entry_physoff(%rip),%rax
    call    uefi_entry_rip_plus_rax
    mov uefi_entry_cr3(%rip),%rax
    mov %rax,%cr3
    mov uefi_entry_physoff(%rip),%rax
    add $head-0x100000,%rax
    neg %rax
    add %rdi,%rax
    add %rax,entry_pml4-DIFFPHYS(%rdi)
    add %rax,entry_pdp+0-DIFFPHYS(%rdi)
    add %rax,entry_pdp+8-DIFFPHYS(%rdi)
    mov %rdi,%rax
    mov $0x83,%al
    xor %ebx,%ebx
1:
    mov %rax,entry_pd-DIFFPHYS(%rdi,%rbx,8)
    add $0x200000,%rax
    add $1,%ebx
    cmp $512,%ebx
    jb  1b
    lea entry_pml4-DIFFPHYS(%rdi),%rax
    mov %rax,%cr3

ここで,%rdiにはロードされたbitvisor.elfの先頭の物理アドレスが格納されています.またDIFFPHYSは0x40000000です.

4. BitVisorのメインのページテーブルへの切り替え

BitVisorが実際にメインに利用するページテーブルは,初期化の中でも初期段階に実行されるmm_init_global()から呼ばれるcreate_vmm_pd()で作成されます.
64bitで関連するところを抜粋すると以下のようになります.

static void
create_vmm_pd (void)
{
    int i;
    ulong cr3;

    /* map memory areas copied to at 0xC0000000 */
    for (i = 0; i < VMMSIZE_ALL >> PAGESIZE2M_SHIFT; i++)
        vmm_pd[i] =
            (vmm_start_phys + (i << PAGESIZE2M_SHIFT)) |
            PDE_P_BIT | PDE_RW_BIT | PDE_PS_BIT | PDE_A_BIT |
            PDE_D_BIT | PDE_G_BIT;
    entry_pdp[3] = (((u64)(virt_t)vmm_pd) - 0x40000000) | PDPE_ATTR;

    asm_rdcr3 (&cr3);
    asm_wrcr3 (cr3);

    /* make a new page directory */
    vmm_base_cr3 = sym_to_phys (vmm_pdp);
    memcpy (vmm_pd1, entry_pd0, PAGESIZE);
    memset (vmm_pd2, 0, PAGESIZE);
    vmm_pdp[0] = sym_to_phys (entry_pd0) | PDPE_ATTR;
    vmm_pdp[1] = sym_to_phys (vmm_pd)    | PDPE_ATTR;
    vmm_pdp[2] = sym_to_phys (vmm_pd1)   | PDPE_ATTR;
    vmm_pdp[3] = sym_to_phys (vmm_pd2)   | PDPE_ATTR;
    vmm_base_cr3 = sym_to_phys (vmm_pml4);
    vmm_pml4[0] = sym_to_phys (vmm_pdp) | PDE_P_BIT | PDE_RW_BIT |
        PDE_US_BIT;
}

static void
mm_init_global (void)
{
    ...
    if (uefi_booted) {
        create_vmm_pd ();
        asm_wrcr3 (vmm_base_cr3);
    }
    ...
    map_hphys();
    ...
}

コードを見ると,以下のようなページテーブルが作成されることがわかります.
ちなみに,VMMSIZE_ALLは(128*1024*1024), PAGESIZE2M_SHIFTは21なので,VMMSIZE_ALL >> PAGESIZE2M_SHIFT = 64です.

image.png

PDPTE[3]は物理アドレス領域の動的マップに使用するための領域だと思います.
また,map_hphys()で予め物理メモリ領域へアクセスするためのマッピングを作成しています.

メモリの静的・動的割り当てについては参考資料の「BitVisorの仮想メモリーマップ」が参考になります.

5. UEFI関数呼び出しのための切り替え(その2)

正式なBitVisorのアドレス空間に以降した後も,一部の処理でUEFIの関数を呼び出す場面があります(ゲストのトランポリンコード用のリアルアドレス領域のメモリ確保など).

このために利用されるのがcalluefiです.
"UEFI関数呼び出しのための切り替え(その1)"のときは,Identity mapping上のページに対応するコードが存在していたため,オフセットの加算でよかったですが,bitvisor.elf全体はidentity mapping上には存在しないため,今回の場合は単純なオフセット計算ではページテーブルを切り替えることができません.
そこで,calluefiでは,fill_pagetable()を利用して,UEFIファームウェア呼び出し前に,全てのページテーブルが,同じ物理アドレス領域を指す特殊なページテーブルを利用します.これを使って,RIPをcalluefiを実行している物理アドレスに変更することでidentity mappingで動作可能な状況を作成し,UEFI関数を呼び出します.

BitVisorからのUEFIファームウェア呼び出しについては,参考資料の"BitVisorのUEFI対応"に図が書いてあります.

まとめ

UEFI関数呼び出しのための切り替え処理,おもしろいですね.

参考資料


  1. フォーラムをみると前途多難そう (https://forums.virtualbox.org/viewtopic.php?f=1&t=90831

  2. これは,UEFIファームウェアの関数AllocatePages()でAllocateMaxAddressを指定することでおこないます. https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/boot/uefi-loader/loadvmm.c#lines-150 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away