この記事は BitVisor Advent Calender 2017,20 日目の記事(大遅刻)です。
UEFI 向けのローダーでも読んで記事書くかーって思ってたら先にやられてしまってウンウン唸ってましたが, 彼の記事の続きを勝手に書けば良いことに気がつきました。
では元気よく,UEFI の 1st stage loader から
いきなり
で,その後,32-bit なら
でももう書き始めちゃったのでこのまま書いちゃいます。
そのあとは,
ここからは簡単で,セグメントレジスタを全部スタックに積んで,
さて,その後設定した PML4 テーブルのアドレスを
まずは 1st stage loader で
頻りに
これによって,60 行目まで
108 行目では改めて UEFI の機能で確保したページに BitVisor の読み込んだバイナリをコピーしてます。続く 112 行目から続くループで,64 KiB しかロードしてなかった
その後,ロードが完了したら
479 行目から 485 行目ではメモリの初期化をしており,486 行目から 492 行目は
とりあえず,BitVisor 本体の処理に飛ぶまでの,VMM Loader(2nd stage)を読んできました。間違って読んでる箇所があったら教えてください。 BitVisor はなんとなくどういうことに使われてるかは知ってたけどちゃんとした処理は知らなかったので,最初のローダーの部分だけでも,後々の切り替えや UEFI に戻るための仕掛けなんかがとてもなるほどと感心しました。すごい。どうせなのでこの後の部分も色々読もうと思います。
UEFI 向けのローダーでも読んで記事書くかーって思ってたら先にやられてしまってウンウン唸ってましたが, 彼の記事の続きを勝手に書けば良いことに気がつきました。
では元気よく,UEFI の 1st stage loader から
entry_func
に飛んだ後を追っていきましょう。いきなり
find
と grep
を使っても良いのですが,折角手掛りがあるのでまずリンカスクリプト, bitvisor.lds
を読んでみます。1st stage loader を読む記事に因ると,bitvisor.elf
を 0x10000
だけ読んで,paddr
(おそらく物理アドレス,physical address の略でしょう)に読んで,そこからエントリポイントを計算して,entry_func
に飛んでるらしいです。bitvisor.lds
リンカスクリプト 2 行目,phys = 0x00100000;
となっており,11 行目, .entry : AT (phys + (code - head)) {
をみるとこのアドレスは bitvisor.elf
の .entry
というセクションになってるらしいので,このセクションを探せば良さそう。ついでに,このスクリプトから bitvisor.elf
がメモリに置かれるときの仮想アドレスの開始位置もわかります。core/entry.s
.entry
セクションをみると,まず multiboot のヘッダが書いてあって,次に 32-bit のコードが始まってます。test
命令の結果,もしリアルモードであるならば,jc
によってリアルモード用の初期化コードに飛んでますが,今回は割愛。で,その後,32-bit なら
jnc
命令で分岐せず multiboot のためのコードにジャンプ,ロングモードなら uefi64_entry
に分岐しているわけですが,この分岐のやりかたが結構トリッキーというか,あんまりちゃんとなんでこれでいいのかわかってないなあと思ったら,
BitVisor本体のブート仕様 2 年前のえいらくさんの記事にしっかり書いてあった……。やだ,もしかしてこの記事を書く必要なかった……?でももう書き始めちゃったのでこのまま書いちゃいます。
core/entry.s
まず後々 uefi_init
関数の引数として 1st stage loader から渡された引数を渡すために,整数として渡されてる rcx, rdx
を ポインタとして扱うため rdi, rsi
へ,r8
は rdx
にコピーしておいて,準備してますね。そのあとは uefi_init
を呼び出すまでページテーブル準備してるだけっぽいですね。core/entry.s
entry_pd
とかのページディレクトリとかの定義はこの通り。まあこれも見たとおり。uefi64_entry
17 行目(この Gist での行数。以下,注釈なく行数が書かれた場合基本的に Gist に貼ったコードでの行数とする。)で ecx
レジスタはゼロにされており,また,18 行目でページディレクトリのアドレスを ebx
レジスタに(entry_pd
ラベルは相対アドレスなので,インストラクションポインタに足して絶対アドレスを取ってる,で合ってますよね……?→えいらくさんから解説頂きました。
mov a, %rax と
mov a(%rip), %rax は同じだけど前者が絶対アドレス,後者が
%rip 相対アドレスになるそうです。),19 行目でこの行に続く 20 行目のラベルの位置の仮想アドレス計算して,mov a,%rax と mov a(%rip),%rax は同じアドレスのアクセスなんですが、前者は絶対アドレス、後者は %rip 相対アドレスに変換されるみたいです。%rip 相対アドレスのほうが再配置に適し、命令長を抑えられるのと、64bit の絶対アドレスが使える命令は限られるので、よく相対アドレスが使われます。
— Hideki EIRAKU (@hdk_2) 2017年12月22日
eax
レジスタにロードしているようです。ここで出てくる head
は,リンカスクリプトの 9 行目で定義されてるアドレスですね。21 行目〜25 行目のループで,さきほど計算した仮想アドレスを格納している rax
レジスタとページディレクトリの物理アドレスを格納している rbx
を利用して,ecx
をループカウンタにしてエントリを 512 コほど設定しています。そのあとは,
entry_pd
の先頭アドレスから 7 バイト目を entry_pd
の最初に設定したり,add
を利用して PML4 テーブルと PDP テーブルを設定してますね。また,この時,rax
レジスタの中身が,head-0x100000
にページテーブルのぶんのアドレスを足したアドレスになってるので,ここから head-0x100000
を引いたアドレスを uefi_entry_physoff
に入れて,このエントリポイントの物理アドレスのオフセットを格納しているようです。ここからは簡単で,セグメントレジスタを全部スタックに積んで,
cr3
のアドレスとスタックポインタのアドレスをそれぞれ uefi_entry_cr3
と uefi_entry_rsp
に記録し,また,uefi_entry_ret
という関数の終了処理をする部分のアドレスを uefi_entry_ret_addr
に記録しています。これは,おそらく,えいらくさんのスライドを参考にするに VMM から UEFI に戻るときにこれらの記録したアドレスをひっぱり出してまた UEFI に帰ってくるためではないでしょうか?さて,その後設定した PML4 テーブルのアドレスを
cr3
レジスタに設定し,スタックのアドレスを設定し,57 行目のラベルにジャンプします。ここで,uefi_init
関数をコールしています。core/uefi.c
uefi_init
関数はこの通り。まずは 1st stage loader で
entry_func
の引数として渡されている UEFI ローダーの引数とブートオプションをこの関数の引数として受けとってます。頻りに
uefi_entry_pcpy
を呼び出してますが,これは,core/entry.s
をみると一瞬ページテーブルを UEFI に戻して,第二引数に渡した UEFI で扱う SystemTable とかの情報を第一引数で渡した変数の物理アドレスに書き込んでから元のページテーブルに戻してるっぽい。uefi_entry_pcpy
で add
とか sub
とかを rdx
レジスタにしてるのは書き込みたい変数のサイズを確認してるからで,movsq
や movsb
でオペランドが指定されてないのは,この関数の第一引数と第二引数として既に rdi
と rsi
が設定されてるから第二引数から第一引数へデータがコピーされてる,んですよね?これによって,60 行目まで
SystemTable
と BootServices
が持つ重要な関数のアドレスや情報をひっぱってきてます。
その後,起動時のオプションから,bitvisor.elf
のファイルハンドルと,読み込んだアドレスと,そのサイズを取得しています。その後,
core/mm.c
これを使って BitVisor のサイズを取得してから,86 行目でこのサイズに必要なぶんだけ UEFI の機能でページを確保しています。
そのあと,96 行目(_PRINT ("Load");
)から 107 行目(_PRINT ("ing");
)の間でアライメントして余計なページを開放してるみたいですね(ちょっと本当にそうなのか自信がないです)。しかしこの“Loading”の表示のしかたちょっと面白くて好きです。108 行目では改めて UEFI の機能で確保したページに BitVisor の読み込んだバイナリをコピーしてます。続く 112 行目から続くループで,64 KiB しかロードしてなかった
bitvisor.elf
の残りのデータも全部ロードしています。その後,ロードが完了したら
uefi_entry_start
を呼び出し,core/entry.s
に戻ってきています。
core/entry.s
まずこの関数には引数として改めてロードした BitVisor の確保されているアドレスの先頭位置が rdi
レジスタに保存されています。
また,関数のプロローグでまたページテーブルやスタックを UEFI のものに戻したあとに,DIFFPHYS
に設定されている値 0x40000000
,つまりいままで 2nd stage loader が使ってた仮想アドレスのローダーが読まれてた位置と BitVisor がロードされているアドレスを足した値を entry_pml4
などのラベルから引いたアドレスに,rdi
の値を設定しています。これで,読み込み直して BitVisor の物理アドレスがズレた分だけ動かした値をページテーブルに改めて設定しているようですね。466 行であらためて cr3
レジスタに新しいページテーブルを登録した後に,BitVisor のロードされている先頭アドレスを vmm_start_phys
に保存して,BitVisor がロードされている物理アドレスを保存しておきます。また,GDT
や IDT
,セグメントレジスタ,UEFI のページテーブルへのアドレスといったものは,全部 calluefi_uefi_*
へ保存しています。これの実体は core/calluefi_asm.s
にあります。479 行目から 485 行目ではメモリの初期化をしており,486 行目から 492 行目は
cr4
レジスタの設定と cr3, cr4
レジスタの保存をして,GDTR
のアドレスをロードしてから,496 行目で callmain64
を呼び出しています。core/entry.s
ds,es,ss
を 0x20,fs,gs
をゼロに設定し,スタックポインタを設定,最初は bspinit_done
に何も触れてないので jne
で分岐せずにそのまま処理を続け,vmm_main
に飛ぶことで,ようやく BitVisor 本体の処理が始まります。なお,
マルチコアで,BSP にならなかったコアだと,bspinit_done が既に設定されている状態で jne で分岐し,apinitproc0 の呼び出しのほうに行くと思われます
とりあえず,BitVisor 本体の処理に飛ぶまでの,VMM Loader(2nd stage)を読んできました。間違って読んでる箇所があったら教えてください。 BitVisor はなんとなくどういうことに使われてるかは知ってたけどちゃんとした処理は知らなかったので,最初のローダーの部分だけでも,後々の切り替えや UEFI に戻るための仕掛けなんかがとてもなるほどと感心しました。すごい。どうせなのでこの後の部分も色々読もうと思います。