(2016/12/10: 文章などを少し修正)
はじめに
BitVisor がブートするときに通っているソースコードを辿ってみたいと思います.
BitVisor にもいろいろなブートの仕方がありますし,32bit環境と64bit環境, CPU がIntel のものか AMD のものかで通るパスが違います.
この記事では,Multiboot,64bit環境,Intel CPU という環境でブートするときのコードをターゲットにしたいと思います.
core/entry.s のentry: から core/vt_main.c のvt_mainloop() まで辿ってみたいと思います.
あくまで,ブート時の本流を辿るだけで,細かいコードの解説はないです.
ブートローダからCへの旅
core/entry.s entry:
ブートローダからのエントリーポイントです.
core/entry.s のentry: からスタートするようです.
ここで,multiboot_entry やuefi64_entry に飛びます.
このあたりのことは @hdk_2 さんの http://qiita.com/hdk_2/items/b73161f08fefce0d99c3 に詳しく書かれてます.
core/entry.s multiboot_entry:
CR レジスタの設定などを行っているみたいですね.
core/entry.s cpuinit_start:, cpuinit_tmpstack:
セグメントの設定っぽいことをやっています.
core/entry.s entry16:, entry16_2
EFER とか MSR の設定をしているように見えます.
最後にcallmain64 (32bit環境ならcallmain32)にジャンプしてますかね.
core/entry.s callmain64
セグメント色々設定して,BSPならvmm_main()
, APならapinitproc0()
を呼んでいるみたいですね.
BSP の旅
entry.s からCへのエントリーポイントはBSPとAPでは異なるようです.
まずは,BSPの方を追ってみましょう.
core/main.c vmm_main()
asmlinkage void
vmm_main (struct multiboot_info *mi_arg)
{
:
(中略)
:
call_initfunc ("global");
start_all_processors (bsp_proc, ap_proc);
}
最終的には start_all_processors ();
を呼んでいます.
直接 start_all_processors ()
を見る前に,少しcall_initfunc()
絡みの説明をしたいと思います.
この関数は,BitVisor の初期化の処理でよく見る関数ですので.
call_initfunc と INITFUNC マクロについて
core/ 以下や driver/ 以下にあるソースコードで時々 INITFUNC("string0", funcname)
みたいなマクロがあると思います.
これは,BitVisor の初期化時に呼び出される関数を登録するためのマクロです.
では,いつ呼び出されるのか?
それは,call_initfunc()
が呼ばれたときです.
ただ,call_initfunc()
を一度呼ぶと,登録したすべての関数が一度に呼ばれるというわけではありません.
call_initfunc("hoge")
とすると INITFUNC("hoge0, funcname)
のように,マクロの第一引数の数字以外の部分がマッチしたものが呼ばれます.
また,hoge0
の数字の意味は,関数が呼び出される順序の制御です.
数字が小さいものが先に呼ばれるようになっています.
例えば,INITFUNC("hoge0, funcname0)
とINITFUNC("hoge1, funcname1)
と登録して,call_initfunc("hoge")
を呼ぶと,funcname0()
の次にfuncname1()
が呼び出されます.
上記のvmm_main()
内のcall_initfunc("global")
が呼ぶ関数を見てみましょう.
$ find . -name "*.[chsS]" | xargs grep "INITFUNC.*\"global"
./core/process.c:INITFUNC ("global3", process_init_global);
./core/msg.c:INITFUNC ("global4", msg_init_global);
./core/time.c:INITFUNC ("global3", time_init_global);
./core/int.c:INITFUNC ("global1", int_init_global);
./core/acpi.c:INITFUNC ("global3", acpi_init_global);
./core/mm.c:INITFUNC ("global2", mm_init_global);
./core/printf.c:INITFUNC ("global0", printf_init_global);
./core/sleep.c:INITFUNC ("global0", sleep_init_global);
./core/thread.c:INITFUNC ("global3", thread_init_global);
./core/tty.c:INITFUNC ("global0", tty_init_global);
./core/tty.c:INITFUNC ("global3", tty_init_global2);
./core/seg.c:INITFUNC ("global0", segment_init_global);
./core/panic.c:INITFUNC ("global0", panic_init_global);
./core/panic.c:INITFUNC ("global3", panic_init_global3);
./core/vmmcall_dbgsh.c:INITFUNC ("global3", vmmcall_dbgsh_init_global);
./core/vmmcall_status.c:INITFUNC ("global3", vmmcall_status_init_global);
./core/cpu_mmu_spt.c:INITFUNC ("global4", cpu_mmu_spt_init_global);
./core/main.c:INITFUNC ("global1", print_boot_msg);
./core/main.c:INITFUNC ("global3", copy_minios);
./core/main.c:INITFUNC ("global3", get_shiftflags);
./core/callrealmode.c:INITFUNC ("global0", callrealmode_init_global);
なんかたくさん呼ばれてますね.
これ全部見てたら今年中にこの記事が書き終わらないので,興味がある人は見てくださいということで.
core/ap.c start_all_processors ();
では,本流にもどりましょう.
void
start_all_processors (void (*bsp_initproc) (void), void (*ap_initproc) (void))
{
initproc_bsp = bsp_initproc;
initproc_ap = ap_initproc;
bsp_continue (bspinitproc1);
}
次の飛び先は bsp_continue()
ですね.
core/ap.c bsp_continue (bspinitproc1)
static void
bsp_continue (asmlinkage void (*initproc_arg) (void))
{
void *newstack;
newstack = alloc (VMM_STACKSIZE);
currentcpu->stackaddr = newstack;
asm_wrrsp_and_jmp ((ulong)newstack + VMM_STACKSIZE, initproc_arg);
}
最後の asm_wrrsp_and_jmp ((ulong)newstack + VMM_STACKSIZE, initproc_arg);
ってのでinitproc_arg
に飛びます.
これは引数で渡されてくるもので,元をたどると,bspinitproc1()
になります.
core/ap.c bspinitproc1()
(修正 2015/12/27: if文を読み間違えて,いろいろ間違えていたので訂正)
static asmlinkage void
bspinitproc1 (void)
{
printf ("Processor 0 (BSP)\n");
:
(中略)
:
if (!uefi_booted) {
apinit_addr = 0xF000;
ap_start ();
} else {
:
(中略)
:
initproc_bsp ();
panic ("bspinitproc1");
}
ap_start(); っていうのが,APをキックしてるみたいです.
BSP の処理は initproc_bsp()
に続いているようです.
initproc_bsp()
は実は関数ポインタで,実態は2つ前くらいの関数 start_all_processors ()
でセットされています.
追ってもらえばわかると思いますが,これは core/main.c
の bsp_proc()
です.
core/main.c bsp_proc()
また,main.c に戻ってきました.
static void
bsp_proc (void)
{
call_initfunc ("bsp");
call_parallel ();
call_initfunc ("pcpu");
}
パッと見不思議なコードです.どこにも飛んで行ってる気配がないからです.
それがなぜ不思議かというと,この関数を読んだ bspinitproc1()
はこの関数から戻ってきたらすかさず panic()
するようになっています.
これはつまり,正常にBitVisor が動けばbsp_proc()
は呼び出しから戻ってこないはず,というコードです.
なのに,このコード,パッと見普通に戻ってしまいそうなコードではありませんか.
call_initfunc ("pcpu")
が最後に呼ぶ関数が怪しそうです.
調べてみましょう.
$ find . -name "*.[chsS]" | xargs grep "INITFUNC.*\"pcpu"
./core/nmi_pass.c:INITFUNC ("pcpu0", nmi_pass_init_pcpu);
./core/time.c:INITFUNC ("pcpu4", time_init_pcpu);
./core/cache.c:INITFUNC ("pcpu4", cache_init_pcpu);
./core/thread.c:INITFUNC ("pcpu0", thread_init_pcpu);
./core/panic.c:INITFUNC ("pcpu3", panic_init_pcpu);
./core/cpu_mmu_spt.c:INITFUNC ("pcpu0", cpu_mmu_spt_init_pcpu);
./core/main.c:INITFUNC ("pcpu2", virtualization_init_pcpu);
./core/main.c:INITFUNC ("pcpu5", create_pass_vm);
どうも,create_pass_vm()
というのが最後に呼ばれるようです.
名前もそれっぽいので,きっとこれでしょう.
core/main.c create_pass_vm()
static void
create_pass_vm (void)
{
:
(中略)
:
current->vmctl.start_vm ();
panic ("VM stopped.");
}
なんか途中でいろいろやってますが,最後は current->vmctl.start_vm ();
を呼びます.
これは,Intel CPU なら vt_start_vm()
, AMD CPU なら svm_start_vm()
になるかと思います.
core/vt_main.c vt_start_vm()
void
vt_start_vm (void)
{
current->exint.int_enabled ();
vt_paging_start ();
vt_mainloop ();
}
次は vt_mainloop()
みたいですね.
core/vt_main.c vt_mainloop()
ここが,終着点です.
static void
vt_mainloop (void)
{
enum vmmerr err;
ulong cr0, acr;
u64 efer;
for (;;) {
schedule ();
vt_vmptrld (current->u.vt.vi.vmcs_region_phys);
:
(中略)
:
vt__vm_run ();
:
(中略)
:
vt__exit_reason ();
}
}
}
本当はとても長い関数なのですが,いろいろ略してしまいました.
BitVisor は上でOSが動いてるあいだ,この無限ループをぐるぐるしています.
vt__vm_run()
でVMENTERして,VMEXITすると,この関数から戻ってきます.
vt__vm_run()
から戻ってくると,vt__exit_reason()
という関数を呼びます.
これは,VMEXITした原因に合わせて必要な処理を行うものです.
例えば,EPT violation が起きたなら,必要なEPTの処理をします.
フックすべきI/Oを発行されて,VMEXIT してきたなら,フック処理を行います.
AP の旅
(修正 2015/12/26: いろいろ間違えていたので訂正)
これまでは,BSP の旅でした.
AP の方はどうでしょうか?
BSP がブートする途中で bspinitproc1()
という関数を通った時に,localapic_delayed_ap_start ap_start);
apstart()
という関数を呼びました.
この関数は,あとで(具体的には,ゲストOSがAPを起動しようとしたとき)にap_start()
を呼ぶように登録しておく関数です.
で,このap_start()
は最終的にStart IPI を送信してAPを起動します.
たぶん,APもentry.s
の entry:
から実行を始めるのではないかとおもいます(僕もよく知らないのですけども...).
core/entry.s
callmain64
の章で説明した通り,APはapinitproc0()
からCのコードに入ります.
ここから追ってみましょう.
@hdk_2 さんからご指摘のコメントいただきました.ありがとうございます.
APはentry:じゃなくてですね、cpuinit_start:というところから始まるコードを、1MiB未満のアドレスにコピーしたものを実行します。コピーはap_start()関数でやっていますので見てみてください。
とのことですので,ap_start() から見てみましょう.
core/ap.c ap_start()
static void
ap_start (void)
{
volatile u32 *num;
u8 *apinit;
u32 tmp;
int i;
u8 buf[5];
u8 *p;
u32 apinit_segment;
apinit_segment = (apinit_addr - APINIT_OFFSET) >> 4;
/* Put a "ljmpw" instruction to the physical address 0 */
p = mapmem_hphys (0, 5, MAPMEM_WRITE);
memcpy (buf, p, 5);
p[0] = 0xEA; /* ljmpw */
p[1] = APINIT_OFFSET & 0xFF;
p[2] = APINIT_OFFSET >> 8;
p[3] = apinit_segment & 0xFF;
p[4] = apinit_segment >> 8;
apinit = mapmem (MAPMEM_HPHYS | MAPMEM_WRITE, apinit_addr,
APINIT_SIZE);
ASSERT (apinit);
memcpy (apinit, cpuinit_start, APINIT_SIZE);
num = (volatile u32 *)APINIT_POINTER (apinit_procs);
apinitlock = (spinlock_t *)APINIT_POINTER (apinit_lock);
*num = 0;
spinlock_init (apinitlock);
i = 0;
ap_start_addr (0, ap_start_loopcond, &i);
for (;;) {
spinlock_lock (&ap_lock);
tmp = num_of_processors;
spinlock_unlock (&ap_lock);
if (*num == tmp)
break;
usleep (1000000);
}
unmapmem ((void *)apinit, APINIT_SIZE);
memcpy (p, buf, 5);
unmapmem (p, 5);
ap_started = true;
}
ざっくり処理の流れを書くと
- 0x0番地にlong jump 命令のコードを置く (
p[0] = ...
からp[4] = ...
あたり) - long jump の飛び先に
cpuinit_start
のコードを置く(memcpy (apinit, cpuinit_start, APINIT_SIZE);
) - Startup IPI をAPに送信する,エントリーポイントに0x0番地を指定(
ap_start_addr (0, ap_start_loopcond, &i);
).
core/entry.s cpuinit_start: ~ callmain64:
この間の処理BSPと大差ないと思います.
最後,Cのコードへのエントリポイントが,BSPなら vmm_main()
だったところをAPだと apinitproc0()
になるところで,BSP とAP の処理の流れが変わります.
というわけで,次はapinitproc0()
を見ます.
core/ap.c apinitproc0()
asmlinkage void
apinitproc0 (void)
{
newstack_tmp = alloc (VMM_STACKSIZE);
asm_wrrsp_and_jmp ((ulong)newstack_tmp + VMM_STACKSIZE, apinitproc1);
}
apinitproc1()を呼んでます.
core/ap.c apinitproc1()
static asmlinkage void
apinitproc1 (void)
{
void (*proc) (void);
:
(中略)
:
proc = initproc_ap;
printf ("Processor %d (AP)\n", num_of_processors);
:
(中略)
:
proc ();
}
initproc_ap()
を呼んでます.
これは,BSP がcore/ap.c
start_all_processors ();
の中でセットしていました.
追うと,core/main.c
ap_proc()
だということがわかります.
core/main.c ap_proc()
static void
ap_proc (void)
{
call_initfunc ("ap");
call_parallel ();
call_initfunc ("pcpu");
}
これもBSP同様,call_initfunc ("pcpu");
の最後でcreate_pass_vm()
を呼びます.
ここから先は,BSPと同じですね.
おわりに
entry.s からvt_main() のvt_mainloop()に至るまでの流れを追ってみました.
あまり脇道にそれずに本流だけ追って書いてみても結構な量になりました.
本当はこの途中でドライバの初期化だったり,EPTの初期化だったりいろいろやっています.
ブートの流れを追うのはたいてい面倒な作業なので,そういう作業の助けになれば幸いです.