Skip to content

Instantly share code, notes, and snippets.

@RKX1209
Created November 5, 2017 06:19
Show Gist options
  • Save RKX1209/5f756da55deb675085a799e5ff1faaa1 to your computer and use it in GitHub Desktop.
Save RKX1209/5f756da55deb675085a799e5ff1faaa1 to your computer and use it in GitHub Desktop.
bitvisor source code internal

BitVisorのなかみ(BitVisor internal) 1.ビルド 各ディレクトリごとにoutput.oを生成して、トップディレクトリのoutput.oにリンク。 このoutput.oからbitvisor.elfを生成。 makeするとbuild-allルールで$(CONFIG),defconfigターゲットのルール実行。 これによりmake -f Makefile.config update-configが実行。update-configは.configを読み込んで.configに書き込む(?) .configはmake configすると、./config.shからwhiptailを使ってコンフィグ画面が起動。これで.configに書き込み。 その後make -f Makefile.buildでmake build-targetされる。 Makefile.buildでは$(DIR)$(target)(i.e. dir/$(outo))がビルドされる。これはdefoutoのエイリアス。

  1. ブート

bootloader

install.shが0x7c00から始まる先頭にカーネル全体のサイズ(length),モジュール1,2のoffsetを埋め込む. これらはbootloader内で最初にスタックに対比しておいて次のentry.sで使う。

multiboot specificationを見る。

[ref: BitVisor本体のブート仕様 http://qiita.com/hdk_2/items/b73161f08fefce0d99c3] core/entry.sがヘッダでリンク時に先頭8192バイト以内に置かれる。 entry:のポリモーフィックコードでbios(16bit)かmulti(32bit)かuefi(64bit)を判断。

BIOSのブートローダー(boot/loader)から来た(16bit)

1:に飛んで、[realmode_entry,realmode_entry_end]をREALMODE_ENTRY_OFFSET(0x8000)にコピーして飛ぶ。 飛んだ先でA20の設定、call 1fで、CR0_PE_BITの有効(i.e. 32bitプロテクトモードに移行),GDTを仮の4:に設定。0x18:0xf000(esi)に読み込んでおいたELFを0x18:0x100000(edi)にコピー。gdt0x18はオフセット0になっている。(4:参照) 読み込んだ後のesi,ediをスタックに積んでおく(うまく行くとmovswで2byte単位でcx(0x10000/2)回転送したので0x10000バイト読み込まれている) 5:で残ったセクタを読み込む。読み込むべきセクタが無くなったらjbe 2fで2:に飛ぶ。 real_mode_entry_endの直後

そのあとmultiboot_entryにジャンプ。32bitだとPAE有効化。GDTをentry_gdtに設定。 entry16,entry16_2でcheck_cpuidで64bit環境ならロングモードにMSR設定して移行。callmain64にジャンプ。 callmain64ではBSP(Bootstrap Processor)ならvmm_mainに、APならapinitproc0に飛ぶ。

物理アドレス=仮想アドレス&0xff + 0xf000になっている。ただしこの計算によってでた物理アドレスがELF内のオフセットに一致するとは限らないのでこれ変えたほうが良いのでは... ex). (virt) 0x4010100c => (phys) 0x1000c != (actual offset+0xf000) 0x2000c

  1. printf putcharで1文字ずつ表示するだけ。putchar.c内で登録されたputchar関数が使われる。これはtty_putchar(tty.c)でVRAMに直接書いている。

  2. vramwrite VRAMに描画するためのルーチン群。フォントデータもハードコードされてる(vramwrite.c) 基本的にはVRAMアドレスに直接書き込むだけだが、たまにBIOSのフォント情報の読み込みなどでcallreamode_*(callrealmode.c)を使う。 またVGAの情報も使う(vga.c) インラインアセンブリを書く必要があるときは共通してasm.h,asm.cを使う。ttyは保護ドメイン(後述)とメッセージをやり取りするためのハンドラを登録している。tty_init_msg

  3. 保護ドメイン BitVisorはVMM内にプロセスを生める。それらはprocess/以下にある。このプロセスはnewprocessで生まれる。newprocessも含めVMM内にはシステムコールがいくつか用意されていてprocessから使えるようになっている。 これら保護ドメイン下に置かれたプロセスはVMMのコア部分(ring0)とメッセージでやり取りする。(msg.c) sysenterに対するエントリーはproess.cのprocess_init_global->setup_syscallentry内でMSRを設定する事でprocess_sysenter.s内の各エントリーに設定されている。

  4. vmm_main http://qiita.com/deep_tkkn/items/a05f87749ee352547e40#bsp-%E3%81%AE%E6%97%85 いろいろやってBSPならbsp_procに、APならap_proceに飛ぶ。"pcpu"のinitでVT-x関係の処理が始まって(create_pass_vm)後は戻ってこない(VMEnter,Exitの繰り返しループに入る vt_main.c) create_pass_vmではVT-xの初期化vt_vminit(vt_init.c)がある。 vt_vminitではvt__vmcs_initでVMCSの初期化を行う。MSR_IA32_VMX_ENTRY_CTLSなどから情報を読みだしそれに合わせて初期化を行っていく。(例えばMSR_IA32_VMX_PROCBASED_CTLS2にはEPTやVPIDのON/OFFがビットで設定されている。これを読んで有効ならvpid_initやept_initをする) またVMCSの(GUEST|HOST)RIPが0xdeadbeefに設定されているが、GUEST_RIPはこの後のcopy_bootsectorで、HOST_RIPはvmlaunch,vmresumeが実行されるasm.s内で設定される。 create_pass_vmに戻る。vminitが終わればvmmcall_boot_enable(vmmcall_boot.c)でbsp_init_threadスレッドを作成して実行する。bsp_init_threadの実行が終わるまでvmmcall_boot_enableは実行が止まる(schedule()で自発的に他のスレッドに切り替える) bsp_init_thread内ではload_bootsector(loadbootsector.c)のtry*bootでゲストOSのブートを行う。load_bootsector呼び出しの前にget_tmpbufでtmpbufのアドレスを取得する。 このtmpbufはcallrealmode_endofcodeaddr関数で得たcallrealmode_endのアドレスの直後に設定される。try_hddbootではcallrealmode_disk_readmbrでtmpbufaddrに起動ディスクのMBRを読み込む。 mapmem_hphysでtmpbufaddrを mapmem_hphys,mapmem_gphysはそれぞれmapmemにフラグを渡して制御。mapmemはBitVisorの静的物理アドレス割当関数。0x80000000の仮想アドレスに与えられた物理アドレスをマップする。 例えば今回なら、0x80000000+tmpbufにtmpbufのアドレスを割り当てる。

|c| loadbuf = alloc (loadsize); p = mapmem_hphys (tmpbufaddr, loadsize, 0); memcpy (loadbuf, p, loadsize); unmapmem (p, loadsize); ||<

[参照: BitVisorの仮想メモリーマップ http://qiita.com/hdk_2/items/6c7aaa72f5dcfcfda342] loadbufをallocして先ほどMBRを読み込んだtmpbufaddr(phys addr)を静的物理アドレス(virt addr)に割り当てる。そしてloadbuf(virt addr)にコピーする。(仮想アドレス同士でコピーしたいからこうなってる)

この後copy_bootsectorでは

|c| p = mapmem_hphys (loadaddr, loadsize, MAPMEM_WRITE); memcpy (p, loadbuf, loadsize); unmapmem (p, loadsize); free (loadbuf); loadbuf = NULL; current->vmctl.write_realmode_seg (SREG_SS, 0); current->vmctl.write_general_reg (GENERAL_REG_RSP, 0x1000); current->vmctl.write_realmode_seg (SREG_CS, jmpseg); current->vmctl.write_ip (jmpoff); current->vmctl.write_general_reg (GENERAL_REG_RDX, loaddrive); ||<

loadaddr(phys addr:0x7c00)を仮想アドレスに割り当てて先ほど読み込んだloadbufをコピーする。これで物理アドレス0x7c00にMBRが読み込まれる。 そしてvmctl.write_ipによってjmpoff(0x7c00)をGUEST_RIPに設定する事でVMEnter時にゲストOSのMBRに飛ぶ。

初期化終了後、vt_start_vm->vt_mainloopに入る。mainloop内ではvt__vm_run()でVMEnterしVMExitするとこの関数からretしてきて、次のvt__exit_reason()を実行する。 vt__vm_runはcall_vt__vmresume->asm_vmresume_regs(asm.s)を呼び出す。 asm_vmresume_regs_32ではvmwriteでHOST_RIPを1:に設定している。このラベルはホストの情報を戻してasm_vmresume_regsからreturnするだけ。

EPT violationの流れ。 vt__exit_reason(vt_main.c)からdo_ept_violationを実行。EXIT_QUALからREAD/WRITEのどちらで落ちたかをチェックしてvt_paging_npf(vt_paging.c)に飛び、vt_ept_violation(vt_ept.c)を呼ぶ。この中でcurrent->u.vt.eptでvt_ept構造体を取得する、がこの.eptの初期化は同ファイル内のvt_ept_init内でのみ。(おそらくずっと同じ?) MMIOフックとかはこの関数内のmmio_access_pageから実行される。 またmmio_acces_memoryフックハンドラが呼ばれるっぽい。 vt_ept_map_pageでgphysのEPTマッピングを行う。 実際はvt_ept_map_page_subが行う。

struct vt_ept { int cnt; int cleared; void ncr3tbl; [] phys_t ncr3tbl_phys; [] void tbl[NUM_OF_EPTBL]; [] phys_t tbl_phys[NUM_OF_EPTBL]; [] struct { int level; [] phys_t gphys; [] u64 entry[EPT_LEVELS]; [] } cur; };

static void vt_ept_map_page_sub (struct vt_ept *ept, bool write, u64 gphys) { bool fakerom; u64 hphys; u32 hattr; u64 *p;

cur_move (ept, gphys); [*]
p = cur_fill (ept, gphys, 0); [*]
hphys = current->gmm.gp2hp (gphys, &fakerom) & ~PAGESIZE_MASK;
if (fakerom && write)
	panic ("EPT: Writing to VMM memory.");
hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
	EPTE_READEXEC | EPTE_WRITE;
if (fakerom)
	hattr &= ~EPTE_WRITE;
*p = hphys | hattr; [*]

}

static void vt_ept_map_page (struct vt_ept ept, bool write, u64 gphys) { if (ept->cleared) vt_ept_map_page_clear_cleared (ept); [] vt_ept_map_page_sub (ept, write, gphys); if (ept->cleared) vt_ept_map_page_clear_cleared (ept); }

vt_ept構造体はcurメンバが現在見ているEPTエントリー情報を持っている。 []のncr3tbl(_phys)がEPTPが指すアドレス(と物理アドレス) []のtbl(_phys)は1024個のポインタ郡で、ここからページテーブル1つを確保する。(つまりポインタ一つが一つのページテーブルの先頭アドレスに相当する。これらはalloc_page(virt,phys)(core/mm.c)で確保してそれらの仮想|物理アドレスがvirt,physに代入される。

[]のlevelが現在見ているEPTテーブルのレベル(全4レベル)、[]のentry[level]が現在参照中のGPAに対応する各levelごとのエントリー内にある次エントリーへのアドレス(仮想アドレス)。つまりEPTから余計なパーミッション情報とか取り除いてポインタ情報だけ取り出したもの。

[]のcur_moveでcur->u.vt.eptのvt_eptのcurメンバを設定する。cuur_moveは最初に一番左のレベルのエントリを見て、ポインタpに設定。あとはこのpに入っている値(つまり次のレベルへのアドレス)をwhileで見ていって、そこにgphysのシフトした部分を入れていく。phys_to_virtはpの値は物理アドレスなのでポインタとしてアクセスするために一旦仮想アドレスにしているだけ。 これでept->curの各エントリが現在見ているgphysに対応するEPTテーブルのエントリになっている。 []のcur_fillはcur_moveで設定したアドレスを辿って、それぞれのエントリーを実際にテーブルのアドレスで埋めていく。で最後のPDEのアドレスを返す。 [*]でこのPDEのアドレスにホストの物理アドレスとattributionを設定して終わり。

[*]でept->clearedが1の場合(つまり1024個のtblが枯渇してEPTに割り当てられなくなったので全てクリアされた状態)、vt_ept_map_page_clear_clearedで必須の部分だけ割当。 static void vt_ept_map_page_clear_cleared (struct vt_ept *ept) { u32 n, nn; u64 base, len, size; phys_t next_phys;

ept->cleared = 0;
n = 0;
for (nn = 1; nn; n = nn) {
	nn = current->gmm.getforcemap (n, &base, &len); [*]
	...
}
if (ept->cleared)
	panic ("%s: error", __func__);

}

static void vt_ept_map_page (struct vt_ept *ept, bool write, u64 gphys) { if (ept->cleared) vt_ept_map_page_clear_cleared (ept); vt_ept_map_page_sub (ept, write, gphys); if (ept->cleared) vt_ept_map_page_clear_cleared (ept); }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment