BitVisor の UEFI 向けのブートローダーを読む(2nd stage)

この記事は BitVisor Advent Calender 2017,20 日目の記事(大遅刻)です。

UEFI 向けのローダーでも読んで記事書くかーって思ってたら先にやられてしまってウンウン唸ってましたが, 彼の記事の続きを勝手に書けば良いことに気がつきました。

では元気よく,UEFI の 1st stage loader から entry_func に飛んだ後を追っていきましょう。
いきなり findgrep を使っても良いのですが,折角手掛りがあるのでまずリンカスクリプト, bitvisor.lds を読んでみます。1st stage loader を読む記事に因ると,bitvisor.elf0x10000 だけ読んで,paddr (おそらく物理アドレス,physical address の略でしょう)に読んで,そこからエントリポイントを計算して,entry_func に飛んでるらしいです。

bitvisor.lds
ENTRY(entry)
virt = 0x40100000;
phys = 0x00100000;
PHDRS {
all PT_LOAD;
}
SECTIONS {
. = virt;
head = .;
. = virt + SIZEOF_HEADERS;
.entry : AT (phys + (code - head)) {
code = .;
*(.entry)
*(.entry.text)
*(.entry.data)
} :all
.text : AT (phys + (text - head)) {
text = .;
*(.text)
codeend = .;
}
.data : AT (phys + (data - head)) {
data = .;
*(.data)
. = ALIGN (8);
__initfunc_start = .;
*(.initfunc)
__initfunc_end = .;
__process_builtin = .;
*(.processes)
__process_builtin_end = .;
dataend = .;
}
.bss : AT (phys + (bss - head)) {
bss = .;
*(.bss)
*(COMMON)
. = ALIGN (4096);
}
end = .;
/DISCARD/ : {
*(.gomi)
}
}
view raw bitvisor.lds hosted with ❤ by GitHub
リンカスクリプト 2 行目,phys = 0x00100000; となっており,11 行目, .entry : AT (phys + (code - head)) { をみるとこのアドレスは bitvisor.elf.entry というセクションになってるらしいので,このセクションを探せば良さそう。ついでに,このスクリプトから bitvisor.elf がメモリに置かれるときの仮想アドレスの開始位置もわかります。

core/entry.s
######## ENTRY SECTION START
# This section is placed at the start of the .text section.
# See the linker script.
.section .entry
# GRUB Multiboot Header
.align 4
.long MULTIBOOT_HEADER_MAGIC
.long MULTIBOOT_HEADER_FLAGS
.long CHECKSUM
# 32bit code start
.code32
.globl entry
entry:
# boot/loader jumps to here in real-address mode
test $0xF9F9F9F9,%eax # (test;stc;stc in 16bit mode)
jc 1f # must be short jump for 16bit & 32bit
shl %al
inc %eax # (rex prefix in 64bit mode)
rcr %al # undo %al
jnc uefi64_entry
jmp multiboot_entry
view raw entry.s hosted with ❤ by GitHub
.entry セクションをみると,まず multiboot のヘッダが書いてあって,次に 32-bit のコードが始まってます。test 命令の結果,もしリアルモードであるならば,jc によってリアルモード用の初期化コードに飛んでますが,今回は割愛。
で,その後,32-bit なら jnc 命令で分岐せず multiboot のためのコードにジャンプ,ロングモードなら uefi64_entry に分岐しているわけですが,この分岐のやりかたが結構トリッキーというか,あんまりちゃんとなんでこれでいいのかわかってないなあと思ったら, BitVisor本体のブート仕様 2 年前のえいらくさんの記事にしっかり書いてあった……。やだ,もしかしてこの記事を書く必要なかった……?
でももう書き始めちゃったのでこのまま書いちゃいます。

core/entry.s
uefi64_entry:
.if longmode
# The entry_pd that will not be used in 64bit mode is used for
# a page table during this routine
push %rbx
push %rsi
push %rdi
mov %rcx,%rdi
mov %rdx,%rsi
mov %r8,%rdx
push %rbp
push %r12
push %r13
push %r14
push %r15
push %rcx
xor %ecx,%ecx
lea entry_pd(%rip),%ebx
lea head-0x100000+0x3(%rip),%eax
1:
mov %rax,(%rbx,%rcx,8)
add $0x1000,%eax
add $1,%ecx
cmp $512,%ecx
jb 1b
lea 7(%ebx),%eax
mov %rax,(%rbx)
lea head-0x100000(%rip),%rax
add %rax,entry_pml4(%rip)
add %rax,entry_pdp+0(%rip)
add %rax,entry_pdp+8(%rip)
addq $entry_pd-entry_pd0,entry_pdp+8(%rip)
sub $head-0x100000,%rax
mov %rax,uefi_entry_physoff(%rip)
pop %rcx
mov %es,%eax
push %rax
mov %ss,%eax
push %rax
mov %ds,%eax
push %rax
mov %fs,%eax
push %rax
mov %gs,%eax
push %rax
mov %cr3,%rax
mov %rax,uefi_entry_cr3(%rip)
mov %rsp,uefi_entry_rsp(%rip)
lea uefi_entry_ret(%rip),%rax
mov %rax,uefi_entry_ret_addr(%rip)
lea entry_pml4(%rip),%rax
cli
mov %rax,%cr3
mov $start_stack,%rsp
mov $1f,%rax
jmp *%rax
1:
call uefi_init
mov %rax,1b(%rip)
mov uefi_entry_physoff(%rip),%rax
call uefi_entry_rip_plus_rax
mov uefi_entry_rsp(%rip),%rsp
mov uefi_entry_cr3(%rip),%rax
mov %rax,%cr3
mov 1b(%rip),%rax
uefi_entry_ret:
pop %rbx
mov %ebx,%gs
pop %rbx
mov %ebx,%fs
pop %rbx
mov %ebx,%ds
pop %rbx
mov %ebx,%ss
pop %rbx
mov %ebx,%es
pop %r15
pop %r14
pop %r13
pop %r12
pop %rbp
pop %rdi
pop %rsi
pop %rbx
ret
view raw entry.s hosted with ❤ by GitHub
まず後々 uefi_init 関数の引数として 1st stage loader から渡された引数を渡すために,整数として渡されてる rcx, rdx を ポインタとして扱うため rdi, rsi へ,r8rdx にコピーしておいて,準備してますね。そのあとは uefi_init を呼び出すまでページテーブル準備してるだけっぽいですね。

core/entry.s
# Provisional page tables
.align PAGESIZE
entry_pml4: # 7654321|76543210
.long entry_pdp-DIFFPHYS+0x7 # 0x0000000000000000
.long 0 #
.space 4096-8*1 # 0x0000008000000000-
.globl entry_pdp
entry_pdp:
.long entry_pd0-DIFFPHYS+0x7 # 0x0000000000000000
.long 0 #
.long entry_pd0-DIFFPHYS+0x7 # 0x0000000040000000
.long 0 #
.long entry_pd0-DIFFPHYS+0x7 # 0x0000000080000000
.long 0 #
.quad 0 # 0x00000000C0000000
.space 4096-8*4 # 0x0000000100000000-
.globl entry_pd0
entry_pd0:
.rept 512 # +0x00000000-
.quad 0x83 + (. - entry_pd0) / 8 * 0x200000
.endr
.globl entry_pd
entry_pd: # Page directory for PAE OFF # 7654321|76543210
.rept 3 # +0x00000000-
1:
.rept 256
.long 0x83 + (. - 1b) / 4 * 0x400000
.endr
.endr
.space 1024 # +0xC0000000-
# Stack for initialization
.align PAGESIZE
.space PAGESIZE
start_stack:
######## ENTRY SECTION END
view raw entry.s hosted with ❤ by GitHub
entry_pd とかのページディレクトリとかの定義はこの通り。まあこれも見たとおり。uefi64_entry 17 行目(この Gist での行数。以下,注釈なく行数が書かれた場合基本的に Gist に貼ったコードでの行数とする。)で ecx レジスタはゼロにされており,また,18 行目でページディレクトリのアドレスを ebx レジスタに(entry_pd ラベルは相対アドレスなので,インストラクションポインタに足して絶対アドレスを取ってる,で合ってますよね……?→えいらくさんから解説頂きました。mov a, %raxmov a(%rip), %rax は同じだけど前者が絶対アドレス,後者が %rip 相対アドレスになるそうです。 ),19 行目でこの行に続く 20 行目のラベルの位置の仮想アドレス計算して,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_cr3uefi_entry_rsp に記録し,また,uefi_entry_ret という関数の終了処理をする部分のアドレスを uefi_entry_ret_addr に記録しています。これは,おそらく,えいらくさんのスライドを参考にするに VMM から UEFI に戻るときにこれらの記録したアドレスをひっぱり出してまた UEFI に帰ってくるためではないでしょうか?
さて,その後設定した PML4 テーブルのアドレスを cr3 レジスタに設定し,スタックのアドレスを設定し,57 行目のラベルにジャンプします。ここで,uefi_init 関数をコールしています。

core/uefi.c
int SECTION_ENTRY_TEXT
uefi_init (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab, void **boot_options)
{
EFI_BOOT_SERVICES *uefi_boot_services;
EFI_FILE_HANDLE file;
u64 loadaddr, loadsize;
u64 uefi_read;
u64 alloc_addr64, readsize;
ulong alloc_addr;
u32 vmmsize, align, ret, loadedsize, blocksize, npages;
int freesize;
extern u8 dataend[];
EFI_SIMPLE_TEXT_IN_PROTOCOL *conin;
EFI_SIMPLE_TEXT_OUT_PROTOCOL *conout;
struct bitvisor_boot bitvisor_opt;
u64 boot_opt_addr;
uefi_boot_param_ext_addr = (ulong)boot_options;
uefi_image_handle = (ulong)image;
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_conin),
&systab->ConIn, sizeof uefi_conin);
conin = uefi_conin;
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_conin_read_key_stroke),
&conin->ReadKeyStroke,
sizeof uefi_conin_read_key_stroke);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_conout),
&systab->ConOut, sizeof uefi_conout);
conout = uefi_conout;
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_conout_output_string),
&conout->OutputString,
sizeof uefi_conout_output_string);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_boot_services),
&systab->BootServices, sizeof uefi_boot_services);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_allocate_pages),
&uefi_boot_services->AllocatePages,
sizeof uefi_allocate_pages);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_free_pages),
&uefi_boot_services->FreePages,
sizeof uefi_free_pages);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_wait_for_event),
&uefi_boot_services->WaitForEvent,
sizeof uefi_wait_for_event);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_get_memory_map),
&uefi_boot_services->GetMemoryMap,
sizeof uefi_get_memory_map);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_locate_handle_buffer),
&uefi_boot_services->LocateHandleBuffer,
sizeof uefi_locate_handle_buffer);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_free_pool),
&uefi_boot_services->FreePool, sizeof uefi_free_pool);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_open_protocol),
&uefi_boot_services->OpenProtocol,
sizeof uefi_open_protocol);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_close_protocol),
&uefi_boot_services->CloseProtocol,
sizeof uefi_close_protocol);
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_disconnect_controller),
&uefi_boot_services->DisconnectController,
sizeof uefi_disconnect_controller);
read_configuration_table (systab);
if (!boot_options) {
_PRINT ("Fatal: Boot options not found\n");
return 0;
}
boot_opt_addr = boot_param_get_phys (&boot_opt_uuid);
if (boot_opt_addr == 0x0) {
_PRINT ("Fatal: Cannot find boot handler\n");
return 0;
}
uefi_entry_pcpy (uefi_entry_virttophys (&bitvisor_opt),
(void *)(ulong)boot_opt_addr,
sizeof (bitvisor_opt));
loadaddr = bitvisor_opt.loadaddr;
loadsize = bitvisor_opt.loadsize;
file = bitvisor_opt.file;
uefi_entry_pcpy (uefi_entry_virttophys (&uefi_read),
&file->Read, sizeof uefi_read);
uefi_init_get_vmmsize (&vmmsize, &align);
vmmsize = (vmmsize + PAGESIZE - 1) & ~PAGESIZE_MASK;
alloc_addr64 = 0xFFFFFFFF;
npages = (vmmsize + align - 1) >> PAGESIZE_SHIFT;
ret = uefi_entry_call (uefi_allocate_pages, 0,
1 /* AllocateMaxAddress */,
8 /* EfiUnusableMemory */,
npages, uefi_entry_virttophys (&alloc_addr64));
if (ret) {
_PRINT ("AllocatePages failed ");
_printhex (ret, 8);
_PRINT ("\n");
return 0;
}
_PRINT ("Load");
alloc_addr = alloc_addr64;
if (alloc_addr % align)
alloc_addr += align - (alloc_addr % align);
freesize = (alloc_addr - alloc_addr64) >> PAGESIZE_SHIFT;
if (freesize > 0)
uefi_entry_call (uefi_free_pages, 0, alloc_addr64, freesize);
freesize = npages - freesize - (vmmsize >> PAGESIZE_SHIFT);
if (freesize > 0)
uefi_entry_call (uefi_free_pages, 0, alloc_addr + vmmsize,
freesize);
_PRINT ("ing ");
uefi_entry_pcpy ((u8 *)alloc_addr + 0x100000, (u8 *)(ulong)loadaddr,
loadsize);
loadedsize = loadsize;
blocksize = (((dataend - head) / 64 + 511) / 512) * 512;
do {
_putchar ('.');
readsize = dataend - head - loadedsize;
if (readsize > blocksize)
readsize = blocksize;
ret = uefi_entry_call (uefi_read, 0, file,
uefi_entry_virttophys (&readsize),
(void *)(alloc_addr + 0x100000 +
loadedsize));
if (ret) {
_PRINT ("\nRead error.\n");
return 0;
}
loadedsize += readsize;
} while (dataend - head > loadedsize && readsize > 0);
_PRINT ("\n");
if (dataend - head > loadedsize) {
_PRINT ("Load failed\n");
return 0;
}
uefi_entry_start (alloc_addr);
}
view raw uefi.c hosted with ❤ by GitHub
uefi_init 関数はこの通り。
まずは 1st stage loader で entry_func の引数として渡されている UEFI ローダーの引数とブートオプションをこの関数の引数として受けとってます。
頻りに uefi_entry_pcpy を呼び出してますが,これは,

core/entry.s
uefi_entry_pcpy:
mov uefi_entry_physoff(%rip),%rax
call uefi_entry_rip_plus_rax
mov uefi_entry_cr3(%rip),%rax
mov %rax,%cr3
lea entry_pml4(%rip),%rax
cld
1:
sub $8,%rdx
jb 1f
movsq
jne 1b
mov %rax,%cr3
ret
1:
add $8,%edx
.byte 0xA8 # test $imm,%al
1:
movsb
sub $1,%edx
jnb 1b
mov %rax,%cr3
ret
uefi_entry_rip_plus_rax:
add %rax,(%rsp)
ret
view raw entry.s hosted with ❤ by GitHub
をみると一瞬ページテーブルを UEFI に戻して,第二引数に渡した UEFI で扱う SystemTable とかの情報を第一引数で渡した変数の物理アドレスに書き込んでから元のページテーブルに戻してるっぽい。uefi_entry_pcpyadd とか sub とかを rdx レジスタにしてるのは書き込みたい変数のサイズを確認してるからで,movsqmovsb でオペランドが指定されてないのは,この関数の第一引数と第二引数として既に rdirsi が設定されてるから第二引数から第一引数へデータがコピーされてる,んですよね?
これによって,60 行目まで SystemTableBootServices が持つ重要な関数のアドレスや情報をひっぱってきてます。 その後,起動時のオプションから,bitvisor.elf のファイルハンドルと,読み込んだアドレスと,そのサイズを取得しています。その後,

core/mm.c
void __attribute__ ((section (".entry.text")))
uefi_init_get_vmmsize (u32 *vmmsize, u32 *align)
{
*vmmsize = VMMSIZE_ALL;
*align = 0x400000;
}
view raw mm.c hosted with ❤ by GitHub
これを使って 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
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
mov %edi,vmm_start_phys
sgdtq calluefi_uefi_gdtr
sidtq calluefi_uefi_idtr
sldt calluefi_uefi_ldtr
mov %es,calluefi_uefi_sregs+0
mov %cs,calluefi_uefi_sregs+2
mov %ss,calluefi_uefi_sregs+4
mov %ds,calluefi_uefi_sregs+6
mov %fs,calluefi_uefi_sregs+8
mov %gs,calluefi_uefi_sregs+10
mov uefi_entry_cr3(%rip),%rax
mov %rax,calluefi_uefi_cr3
mov $bss,%edi # Clear BSS
mov $end+3,%ecx #
sub %edi,%ecx #
shr $2,%ecx #
xor %eax,%eax #
cld #
rep stosl #
mov %cr4,%rcx
or $(CR4_PAE_BIT|CR4_PGE_BIT),%rcx
and $~CR4_MCE_BIT,%rcx
mov %cr3,%rax
mov %rcx,entry_cr4 # Save CR4
mov %rax,vmm_base_cr3 # Save CR3
mov %rcx,%cr4
lgdtq entry_gdtr # Load GDTR
ljmpl *1f
1:
.long callmain64
.long ENTRY_SEL_CODE64
.else
ret
.endif
view raw entry.s hosted with ❤ by GitHub
まずこの関数には引数として改めてロードした BitVisor の確保されているアドレスの先頭位置が rdi レジスタに保存されています。 また,関数のプロローグでまたページテーブルやスタックを UEFI のものに戻したあとに,DIFFPHYS に設定されている値 0x40000000,つまりいままで 2nd stage loader が使ってた仮想アドレスのローダーが読まれてた位置と BitVisor がロードされているアドレスを足した値を entry_pml4 などのラベルから引いたアドレスに,rdi の値を設定しています。これで,読み込み直して BitVisor の物理アドレスがズレた分だけ動かした値をページテーブルに改めて設定しているようですね。466 行であらためて cr3 レジスタに新しいページテーブルを登録した後に,BitVisor のロードされている先頭アドレスを vmm_start_phys に保存して,BitVisor がロードされている物理アドレスを保存しておきます。また,GDTIDT,セグメントレジスタ,UEFI のページテーブルへのアドレスといったものは,全部 calluefi_uefi_* へ保存しています。これの実体は core/calluefi_asm.s にあります。
479 行目から 485 行目ではメモリの初期化をしており,486 行目から 492 行目は cr4 レジスタの設定と cr3, cr4 レジスタの保存をして,GDTR のアドレスをロードしてから,496 行目で callmain64 を呼び出しています。

core/entry.s
callmain64:
mov $ENTRY_SEL_DATA64,%eax
xor %ebx,%ebx
mov %eax,%ds
mov %eax,%es
mov %ebx,%fs
mov %ebx,%gs
mov %eax,%ss
mov $start_stack,%esp
mov $bspinit_done,%eax
cmpb $0,(%rax) # BSP?
jne 1f # No-
movb $1,(%rax)
# BSP
mov $entry_ebx,%eax
mov (%rax),%edi
call vmm_main
cli
hlt
1:
# AP
call apinitproc0
cli
hlt
view raw entry.s hosted with ❤ by GitHub
ds,es,ss を 0x20,fs,gs をゼロに設定し,スタックポインタを設定,最初は bspinit_done に何も触れてないので jne で分岐せずにそのまま処理を続け,vmm_main に飛ぶことで,ようやく BitVisor 本体の処理が始まります。なお, マルチコアで,BSP にならなかったコアだと,bspinit_done が既に設定されている状態で jne で分岐し,apinitproc0 の呼び出しのほうに行くと思われます


とりあえず,BitVisor 本体の処理に飛ぶまでの,VMM Loader(2nd stage)を読んできました。間違って読んでる箇所があったら教えてください。 BitVisor はなんとなくどういうことに使われてるかは知ってたけどちゃんとした処理は知らなかったので,最初のローダーの部分だけでも,後々の切り替えや UEFI に戻るための仕掛けなんかがとてもなるほどと感心しました。すごい。どうせなのでこの後の部分も色々読もうと思います。