ゲストOSのブートローダをホストOS上で動作するプログラムに翻訳する
このブログエントリはカーネル/VM Advent Calendar 2013 8日目の記事、および第九回カーネル/VM探検隊の発表の補足記事です。
発表資料と動画
このエントリだけでも理解できるように書いたつもりですが、はじめにこちらをみて頂いた方が分かりやすいかもしれません。
BHyVeでOSvを起動したい 〜BIOSがなくてもこの先生きのこるには〜 // Speaker Deck
BHyVeって何? OSvって何?
何がしたいのか?
BHyVeはKVM+QEMUやVMwareと異なり、BIOSやUEFIなどの標準的なファームウェアを持ちません。
そのため、MBRのブートセクタからのブートやUEFIブートローダからのブートを行う事ができません。
代わりに、ホストOSで動くプログラム(ゲストOSローダ)でゲストマシン上にゲストOSをロードして実行します。
この時、BIOSに依存している通常のブートローダの実行は一切行わず、ブートローダが行っているような処理をゲストOSローダで代わりに実行します。
全てのお膳立てをゲストOSローダが行い、ゲストマシンは最初の命令が実行される時点で既にページテーブルが設定済みでプロテクテッドモードになっています。
これは、サスペンド&レジュームでメモリやレジスタの内容をサスペンド時の状態に復帰することにより、OSの起動処理を飛ばしていきなりOS実行中の状態から再開するのに少し似ています。
レジューム処理が電源を落とす直前の実行中のOSの状態へ復帰するのに対し、ゲストOSローダでは「ブートローダがカーネルをロードして実行しようとした状態」をゲストマシン上に作り出し、そこからゲストマシンを実行します。
このような仕組みのブート方法をとるのは何もBHyVeだけではありません。
XenのPV guestはほぼ同様の手法でゲストOSをロードしていますし、QEMU(KVM)ではqemu -kernel vmlinuz -initrd initrd.imgのような引数を与えることによりLinuxカーネルを直接起動することが可能です。
(但し、これらのハイパーバイザはBIOSを使って標準的なブート方法で起動する方式もサポートしています。その場合はQEMUが必須になっており、BHyVeにこの辺の機能が足りないのはユーザランドにQEMUを使わない選択をしているからだとも考えられます)
ゲストOSローダの実装方法についての考え方
カーネル/VMの発表だけ聞くと誤解しかねない気がしてきたのでここで補足しますが、上述の通りゲストOSローダは「ブートローダがカーネルをロードして実行しようとした状態」をゲストマシン上に作り出すのが役割です。
ブートローダが行っている全ての処理を移植してくる必要はありません。
ブートローダを実行した結果だけを作り出せば事足ります。
どういうことかというと、例えばRAXレジスタに0x1000という値を代入するという命令がブートローダの途中で実行されていたとして、ブートローダの実行が終了するまでに更に別の命令が実行され値が0x2000へ書き換わるのであれば、ゲストOSローダは単にRAXレジスタを0x2000に書き換えればよく、途中で0x1000だったという事は考慮する必要がありません。
このため、ブートローダを解析してゲストOSローダを実装するときには、最終的にどのような状態が生み出されているかに注目してみていく必要があります。
具体的な実装方法
カーネル/VMの発表でも例示した通り、ブートローダで行われている処理は以下のように置き換え可能です。
コンソールへの文字列表示(ブートローダではINT10h)
コンソールへの文字列表示は単に標準出力へのprintで代替できます。
多くの場合、出力はそもそも不要でしょう。
printf("str\n");
ディスクの読み込み(ブートローダではINT13h)
ディスクイメージファイルをopenしてreadすればINT13hと同じ効果が得られます。
char buf[BUF_SIZ]; int fd = open(disk_image, O_RDWR); read(fd, buf, BUF_SIZ);
メモリへの書き込み
ゲストメモリ空間へのポインタをBHyVeから得るために、libvmmapiを使用します。
vm_map_gpa()の内側では/dev/vmm/
struct vmctx *ctx = vm_open(vm_name); void *ptr = vm_map_gpa(ctx, offset, len); memcpy(ptr, data, len);
レジスタへの書き込み(セグメントレジスタ以外)
ゲストのレジスタへのアクセスにもlibvmmapiを使用します。
vm_set_register()の内側では/dev/vmm/
struct vmctx *ctx = vm_open(vm_name); 

vm_set_register(ctx, cpuno, VM_REG_GUEST_RFLAGS, val);
レジスタへの書き込み(セグメントレジスタ)
セグメントレジスタは通常レジスタと扱いが異なります。
ブートローダ上でセグメントレジスタへ書き込みを行う時とは異なり、各フィールドを別々に指定する必要があります。
これは、VT-xのVMCS上のセグメントレジスタのフォーマットがこうなっているのが由来だと思われます。
struct vmctx *ctx = vm_open(vm_name); vm_set_desc(ctx, cpuno, VM_REG_GUEST_CS, base, limit, access); 
vm_set_register(ctx, cpuno, VM_REG_GUEST_CS, selector);
boot16.Sの解析
まずOSvのブートセクタを読んでみます。
ここでは以下のような処理が行われています:
BIOSコールとして以下のものを呼んでいます:
OSvではブート高速化のために、汎用OSでの作法を無視して以下のような固定的なレイアウトでカーネルELFファイルやカーネル引数をディスク上に配置しています。
かなり強引な手法ですが、これにより汎用OSでよく見られる多段ブート方式を用いずMBRのブートセクタから直接カーネルELFファイルをロード&実行しています。
boot16.S
# Copyright (C) 2013 Cloudius Systems, Ltd. # # This work is open source software, licensed under the terms of the # BSD license as described in the LICENSE file in the top-level directory. .code16 tmp = 0x80000 ←loader.elfの一時的なロード先 bootsect = 0x7c00 ←このプログラムの先頭アドレス cmdline = 0x7e00 ←cmdlineのロード先 target = 0x200000 ←カーネルELFファイルの最終的なロード先 entry = 24+target ←カーネルELFファイルのELFヘッダ上のe_entryフィールドのアドレス ここにカーネルのエントリアドレスが格納されている mb_info = 0x1000 ← multiboot headerの置き場 カーネルのmain()関数の引数として渡される mb_cmdline = (mb_info + 16) ← cmdlineのアドレスはmb_infoのcmdlineメンバ経由で渡される mb_mmap_len = (mb_info + 44) ← e820で得たメモリマップの長さはmb_infoのmmap_lenメンバ経由で渡される mb_mmap_addr = (mb_info + 48) ← e820で得たメモリマップのアドレスはmb_infoのmmap_addrメンバ経由で渡される e820data = 0x2000 ← e820で得るメモリマップの置き場 .text start: ← エントリポイント ljmp $0, $init ← initへジャンプ # Be conservative, we should not be in the 10th byte so far, but better safe than sorry .org 0x10 count32: .short 4096 # in 32k units, 4096=128MB int1342_struct: ← カーネルELFファイルロード用のDAP .byte 0x10 .byte 0 .short 0x40 # 32k ← 32k分ロード .short 0 .short tmp / 16 ← 一時的ロード先(0x80000) lba: .quad 128 ← カーネルELFファイルのオフセット(128セクタ目) int1342_boot_struct: # for command line ← cmdline用のDAP .byte 0x10 .byte 0 .short 0x3f # 31.5k ← 31.5k分ロード .short cmdline ← 0x7e00へロード .short 0 .quad 1 ← cmdlineのオフセット(1セクタ目) xfer: .long target ← targetへのポインタ gdt: ← プロテクトモード切り替え用のGDT .short gdt_size - 1 .short gdt .long 0 .quad 0x00cf9b000000ffff # 32-bit code segment .quad 0x00cf93000000ffff # 32-bit data segment .quad 0x00009b000000ffff # 16-bit code segment .quad 0x000093000000ffff # 16-bit data segment gdt_size = . - gdt init: xor %ax, %ax mov %ax, %ds mov %ax, %es mov %ax, %ss mov $0x7c00, %sp lea int1342_boot_struct, %si ← DS:SIでDAPを指定 mov $0x42, %ah mov $0x80, %dl int $0x13 ← INT 13h AH=42h: Extended Read Sectors From Drive movl $cmdline, mb_cmdline ← mb_info->mb_cmdlineにcmdlineのアドレスを代入 read_disk: ← ここではINT13hコールで32KBごとディスクを読んでプロテクテッドモードへ切り替えhighmem領域であるtargetへコピーしている。 これを繰り返すことにより、BIOSを使いながらリアルモードのメモリ空間の制約から逃れて大きなカーネルELFファイルをロードしている。 lea int1342_struct, %si ← DS:SIでDAPを指定 mov $0x42, %ah mov $0x80, %dl int $0x13 ← INT 13h AH=42h: Extended Read Sectors From Drive jc done_disk # in case of errors, we don't really know what to do. cli ← 割り込み禁止(ここからしばらくプロテクテッドモードへの切り替え) lgdtw gdt ← GDTのロード mov $0x11, %ax ← CR0にセットしたい値をロード lmsw %ax ← CR0に値をセット(PEを1にする) ljmp $8, $1f ←CSレジスタに32bitセグメントをセット 1: .code32 mov $0x10, %ax mov %eax, %ds mov %eax, %es mov $tmp, %esi mov xfer, %edi mov $0x8000, %ecx rep movsb ← tmpからxferが指し示すtargetへデータをコピー mov %edi, xfer mov $0x20, %al mov %eax, %ds mov %eax, %es ljmpw $0x18, $1f 1: .code16 mov $0x10, %eax mov %eax, %cr0 ← CR0からPEをクリア ljmpw $0, $1f 1: xor %ax, %ax mov %ax, %ds mov %ax, %es sti addl $(0x8000 / 0x200), lba ←読み出すオフセット値を加算 decw count32 jnz read_disk ← count32が0になるまで繰り返す(4096回 × 32KB = 128MB) done_disk: mov $e820data, %edi mov %edi, mb_mmap_addr ← mb_info->mb_mmap_addrにe820dataのアドレスを代入 xor %ebx, %ebx ← Continuation(デフォルトは0) more_e820: mov $100, %ecx ← Buffer Size mov $0x534d4150, %edx mov $0xe820, %ax ← Signature 'SMAP' add $4, %edi int $0x15 ← INT 15h, AX=E820h - Query System Address Map jc done_e820 ← もう続きが無かったらループを抜ける mov %ecx, -4(%edi) add %ecx, %edi test %ebx, %ebx jnz more_e820 ← Continuationをチェックして続きがあるなら繰り返す done_e820: sub $e820data, %edi mov %edi, mb_mmap_len ← mb_info->mb_mmap_lenにe820dataのサイズを代入 cli ← 割り込み禁止(ここからプロテクテッドモードへの切り替え) mov $0x11, %ax lmsw %ax ← CR0へPEビット書き込み ljmp $8, $1f ← CSレジスタに32bitセグメントセット 1: .code32 mov $0x10, %ax mov %eax, %ds mov %eax, %es mov %eax, %gs mov %eax, %fs mov %eax, %ss mov $target, %eax ← targetをEAXへ代入 mov $mb_info, %ebx ← mb_infoをEBXへ代入 jmp *entry ← カーネルELFファイルのELFヘッダのe_entryフィールドに書かれているエントリポイントアドレスへジャンプ .org 0x1b8 .byte 0x56, 0x53, 0x4F, 0, 0, 0 .org 0x1be .space 16, 0 .org 0x1ce .space 16, 0 .org 0x1de .space 16, 0 .org 0x1ee, .space 16, 0 .org 0x1fe .byte 0x55, 0xaa
ここまでをゲストOSローダとして翻訳すると、概ね以下のようになります。
OSローダ(boot16.Sまでの分)
#define ADDR_CMDLINE 0x7e00 #define ADDR_TARGET 0x200000 #define ADDR_MB_INFO 0x1000 #define ADDR_E820DATA 0x1100 struct multiboot_info_type { uint32_t flags; uint32_t mem_lower; uint32_t mem_upper; uint32_t boot_device; uint32_t cmdline; uint32_t mods_count; uint32_t mods_addr; uint32_t syms[4]; uint32_t mmap_length; uint32_t mmap_addr; uint32_t drives_length; uint32_t drives_addr; uint32_t config_table; uint32_t boot_loader_name; uint32_t apm_table; uint32_t vbe_control_info; uint32_t vbe_mode_info; uint16_t vbe_mode; uint16_t vbe_interface_seg; uint16_t vbe_interface_off; uint16_t vbe_interface_len; } __attribute__((packed)); struct e820ent { uint32_t ent_size; uint64_t addr; uint64_t size; uint32_t type; } __attribute__((packed)); extern struct vmctx *ctx; extern int disk_fd; int osv_load(struct loader_callbacks *cb, uint64_t mem_size) { struct multiboot_info_type mb_info; struct e820ent e820data[3]; char cmdline[0x3f * 512]; void *target; bzero(&mb_info, sizeof(mb_info)); mb_info.cmdline = ADDR_CMDLINE; mb_info.mmap_addr = ADDR_E820DATA; mb_info.mmap_length = sizeof(e820data); printf("sizeof e820data=%lx\n", sizeof(e820data)); if (cb->copyin(NULL, &mb_info, ADDR_MB_INFO, sizeof(mb_info))) { perror("copyin"); return (1); } cb->setreg(NULL, VM_REG_GUEST_RBX, ADDR_MB_INFO); e820data[0].ent_size = 20; e820data[0].addr = 0x0; e820data[0].size = 654336; e820data[0].type = 1; e820data[1].ent_size = 20; e820data[1].addr = 0x100000; e820data[1].size = mem_size - 0x100000; e820data[1].type = 1; e820data[2].ent_size = 20; e820data[2].addr = 0; e820data[2].size = 0; e820data[2].type = 0; if (cb->copyin(NULL, e820data, ADDR_E820DATA, sizeof(e820data))) { perror("copyin"); return (1); } if (cb->diskread(NULL, 0, 1 * 512, cmdline, 63 * 512, &resid)) { perror("diskread"); } printf("cmdline=%s\n", cmdline); if (cb->copyin(NULL, cmdline, ADDR_CMDLINE, sizeof(cmdline))) { perror("copyin"); return (1); } target = calloc(1, 0x40 * 512 * 4096); if (!target) { perror("calloc"); return (1); } #if 0 /* XXX: Why this doesn't work? */ if (cb->diskread(NULL, 0, 0x40 * 512 * 4096, target, 128 * 512, &resid)) { perror("diskread"); return (1); } #endif siz = pread(disk_fd, target, 0x40 * 512 * 4096, 128 * 512); if (siz < 0) perror("pread"); if (cb->copyin(NULL, target, ADDR_TARGET, 0x40 * 512 * 4096)) { perror("copyin"); return (1); } cb->setreg(NULL, VM_REG_GUEST_RAX, ADDR_TARGET);
boot.Sの解析
次にカーネルのエントリポイントを読んでみます。
ここではGDT、ページテーブルなどを用意してlong modeへ切り替えを行っています。
この記事で作ろうとしているゲストOSローダでは、ここで出てくるstart64()からゲストマシンの実行を開始しようとしています。
どう実装すればそのような事が可能になるか考えていきましょう。
(ここから先はBIOSコールが無いプロテクテッドモードなのだから、このエントリポイントから全てゲストマシンで実行すれば良いじゃないか、という考え方もあろうかと思います。
そのような事も可能なのですが、ページングが無効な状態でVT-xを動作させることが出来るCPUが限られており、かつ試してみたらうまく行かなかった、という理由でここではstart64()からゲストマシンを実行する事としています。)
boot.S
# Copyright (C) 2013 Cloudius Systems, Ltd. # # This work is open source software, licensed under the terms of the # BSD license as described in the LICENSE file in the top-level directory. #include "processor-flags.h" .text .code32 .data .align 4096 ident_pt_l4: ← 以下ページテーブルの定義 .quad ident_pt_l3 + 0x67 .rept 511 .quad 0 .endr ident_pt_l3: .quad ident_pt_l2 + 0x67 .rept 511 .quad 0 .endr ident_pt_l2: index = 0 .rept 512 .quad (index << 21) + 0x1e7 index = index + 1 .endr gdt_desc: ← GDTデスクリプタの定義 .short gdt_end - gdt - 1 .long gdt .align 8 gdt = . - 8 ← GDTの定義 .quad 0x00af9b000000ffff # 64-bit code segment .quad 0x00cf93000000ffff # 64-bit data segment .quad 0x00cf9b000000ffff # 32-bit code segment gdt_end = . .align 8 . = . + 4 # make sure tss_ist is aligned on a quad boundary .bss .align 16 . = . + 4096*4 init_stack_top = . . = . + 4096*10 .global interrupt_stack_top interrupt_stack_top = . .text #define BOOT_CR0 ( X86_CR0_PE \ ← 64bit用CR0レジスタ値の定義 | X86_CR0_WP \ | X86_CR0_PG ) #define BOOT_CR4 ( X86_CR4_DE \ ← 64bit用CR4レジスタ値の定義 | X86_CR4_PSE \ | X86_CR4_PAE \ | X86_CR4_PGE \ | X86_CR4_PCE \ | X86_CR4_OSFXSR \ | X86_CR4_OSXMMEXCPT ) .globl start32 start32: ← カーネルELFファイルのエントリポイント。boot16.Sからここへジャンプしてくる mov %eax, %ebp ← EAXに代入されていたtargetのアドレスをEBPへコピー lgdt gdt_desc ← GDTをロード mov $0x10, %eax mov %eax, %ds ←以下のmovで各セグメントレジスタを「64-bit data segment」にセット mov %eax, %es mov %eax, %fs mov %eax, %gs mov %eax, %ss ljmp $0x18, $1f ←CSを「32-bit code segment」にセット 1: and $~7, %esp mov $BOOT_CR4, %eax mov %eax, %cr4 ← CR4レジスタで64bitメモリ空間などを有効化 lea ident_pt_l4, %eax mov %eax, %cr3 ← ページテーブルをロード mov $0xc0000080, %ecx mov $0x00000900, %eax xor %edx, %edx wrmsr ← EFERでLMEビットを有効化 mov $BOOT_CR0, %eax mov %eax, %cr0 ← CR0レジスタでページングを有効化 ljmpl $8, $start64 ← CSを「64-bit code segment」にセット、start64へジャンプ .code64 .global start64 start64: ← ここからゲストマシンを開始したい!
ページテーブル・GDT・GDTデスクリプタは同様の内容なものをlomem領域の空いている場所に配置してロードしておけば同じ動作が得られると考えられます。
このため、CR3はゲストOSローダがゲストメモリ空間に作成したページテーブルを、GDTRはゲストOSローダがゲストメモリ空間に作成したGDTデスクリプタを指すようにします。
スタックについても同様にゲストOSローダ側で用意してみましょう。
その他のレジスタ(EBP, 各セグメントレジスタ, CR4, CR0, EFER)は値をそのままコピーしてきます。
最後に、start64()のアドレスをRIPレジスタに設定すれば完成です。が、このアドレスは特にリンカで固定されていません。
このため、ELFヘッダを読みに行ってアドレスを取得する必要があります。
詳しい解説は省きますが、このためにシンボル名からアドレスを取得する簡易的なELFパーサを書いてみました。
int elfparse_open_memory(char *image, size_t size, struct elfparse *ep); int elfparse_close(struct elfparse *ep); uintmax_t elfparse_resolve_symbol(struct elfparse *ep, char *name);
これを使って"start64"というシンボルのアドレスを取得してRIPにセットすることとします。
そんな方針で、先ほどのコードに必要な処理を書き加えた完成版がこちらになります。
OSローダ(完成版)
/*- * Copyright (c) 2011 NetApp, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY NETAPP, INC ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL NETAPP, INC OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD: user/syuu/bhyve_standalone_guest/usr.sbin/bhyveload/bhyveload.c 253922 2013-08-04 01:22:26Z syuu $ */ /*- * Copyright (c) 2011 Google, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD: user/syuu/bhyve_standalone_guest/usr.sbin/bhyveload/bhyveload.c 253922 2013-08-04 01:22:26Z syuu $ */ #include <sys/cdefs.h> __FBSDID("$FreeBSD: user/syuu/bhyve_standalone_guest/usr.sbin/bhyveload/bhyveload.c 253922 2013-08-04 01:22:26Z syuu $"); #include <sys/stat.h> #include <sys/param.h> #include <machine/specialreg.h> #include <machine/vmm.h> #include <x86/segments.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <vmmapi.h> #include "elfparse.h" #include "userboot.h" #define BSP 0 #define DESC_UNUSABLE 0x00010000 #define ADDR_CMDLINE 0x7e00 #define ADDR_TARGET 0x200000 #define ADDR_MB_INFO 0x1000 #define ADDR_E820DATA 0x1100 #define ADDR_STACK 0x1200 #define ADDR_PT4 0x2000 #define ADDR_PT3 0x3000 #define ADDR_PT2 0x4000 #define ADDR_GDTR 0x5000 struct multiboot_info_type { uint32_t flags; uint32_t mem_lower; uint32_t mem_upper; uint32_t boot_device; uint32_t cmdline; uint32_t mods_count; uint32_t mods_addr; uint32_t syms[4]; uint32_t mmap_length; uint32_t mmap_addr; uint32_t drives_length; uint32_t drives_addr; uint32_t config_table; uint32_t boot_loader_name; uint32_t apm_table; uint32_t vbe_control_info; uint32_t vbe_mode_info; uint16_t vbe_mode; uint16_t vbe_interface_seg; uint16_t vbe_interface_off; uint16_t vbe_interface_len; } __attribute__((packed)); struct e820ent { uint32_t ent_size; uint64_t addr; uint64_t size; uint32_t type; } __attribute__((packed)); #define MSR_EFER 0xc0000080 #define CR4_PAE 0x00000020 #define CR4_PSE 0x00000010 #define CR0_PG 0x80000000 #define CR0_PE 0x00000001 /* Protected mode Enable */ #define CR0_NE 0x00000020 /* Numeric Error enable (EX16 vs IRQ13) */ #define PG_V 0x001 #define PG_RW 0x002 #define PG_U 0x004 #define PG_PS 0x080 typedef u_int64_t p4_entry_t; typedef u_int64_t p3_entry_t; typedef u_int64_t p2_entry_t; #define GUEST_GDTR_LIMIT (4 * 8 - 1) int osv_load(struct loader_callbacks *cb, uint64_t mem_size); static void setup_osv_gdt(uint64_t *gdtr) { gdtr[0] = 0x0; gdtr[1] = 0x00af9b000000ffff; gdtr[2] = 0x00cf93000000ffff; gdtr[3] = 0x00cf9b000000ffff; } extern struct vmctx *ctx; extern int disk_fd; int osv_load(struct loader_callbacks *cb, uint64_t mem_size) { int i; struct multiboot_info_type mb_info; struct e820ent e820data[3]; char cmdline[0x3f * 512]; void *target; size_t resid; struct elfparse ep; uint64_t start64; int error; uint64_t desc_base; uint32_t desc_access, desc_limit; uint16_t gsel; ssize_t siz; p4_entry_t PT4[512]; p3_entry_t PT3[512]; p2_entry_t PT2[512]; uint64_t gdtr[3]; bzero(&mb_info, sizeof(mb_info)); mb_info.cmdline = ADDR_CMDLINE; mb_info.mmap_addr = ADDR_E820DATA; mb_info.mmap_length = sizeof(e820data); printf("sizeof e820data=%lx\n", sizeof(e820data)); if (cb->copyin(NULL, &mb_info, ADDR_MB_INFO, sizeof(mb_info))) { perror("copyin"); return (1); } cb->setreg(NULL, VM_REG_GUEST_RBX, ADDR_MB_INFO); e820data[0].ent_size = 20; e820data[0].addr = 0x0; e820data[0].size = 654336; e820data[0].type = 1; e820data[1].ent_size = 20; e820data[1].addr = 0x100000; e820data[1].size = mem_size - 0x100000; e820data[1].type = 1; e820data[2].ent_size = 20; e820data[2].addr = 0; e820data[2].size = 0; e820data[2].type = 0; if (cb->copyin(NULL, e820data, ADDR_E820DATA, sizeof(e820data))) { perror("copyin"); return (1); } if (cb->diskread(NULL, 0, 1 * 512, cmdline, 63 * 512, &resid)) { perror("diskread"); } printf("cmdline=%s\n", cmdline); if (cb->copyin(NULL, cmdline, ADDR_CMDLINE, sizeof(cmdline))) { perror("copyin"); return (1); } target = calloc(1, 0x40 * 512 * 4096); if (!target) { perror("calloc"); return (1); } #if 0 /* XXX: Why this doesn't work? */ if (cb->diskread(NULL, 0, 0x40 * 512 * 4096, target, 128 * 512, &resid)) { perror("diskread"); return (1); } #endif siz = pread(disk_fd, target, 0x40 * 512 * 4096, 128 * 512); if (siz < 0) perror("pread"); if (cb->copyin(NULL, target, ADDR_TARGET, 0x40 * 512 * 4096)) { perror("copyin"); return (1); } if (elfparse_open_memory(target, 0x40 * 512 * 4096, &ep)) { return (1); } start64 = elfparse_resolve_symbol(&ep, "start64"); printf("start64:0x%lx\n", start64); desc_base = 0; desc_limit = 0; desc_access = 0x0000209B; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_CS, desc_base, desc_limit, desc_access); if (error) goto done; desc_access = 0x00000093; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_DS, desc_base, desc_limit, desc_access); if (error) goto done; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_ES, desc_base, desc_limit, desc_access); if (error) goto done; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_FS, desc_base, desc_limit, desc_access); if (error) goto done; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_GS, desc_base, desc_limit, desc_access); if (error) goto done; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_SS, desc_base, desc_limit, desc_access); if (error) goto done; /* * XXX TR is pointing to null selector even though we set the * TSS segment to be usable with a base address and limit of 0. */ desc_access = 0x0000008b; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_TR, 0, 0, desc_access); if (error) goto done; error = vm_set_desc(ctx, BSP, VM_REG_GUEST_LDTR, 0, 0, DESC_UNUSABLE); if (error) goto done; gsel = GSEL(1, SEL_KPL); if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_CS, gsel)) != 0) goto done; gsel = GSEL(2, SEL_KPL); if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_DS, gsel)) != 0) goto done; if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_ES, gsel)) != 0) goto done; if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_FS, gsel)) != 0) goto done; if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_GS, gsel)) != 0) goto done; if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_SS, gsel)) != 0) goto done; /* XXX TR is pointing to the null selector */ if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_TR, 0)) != 0) goto done; /* LDTR is pointing to the null selector */ if ((error = vm_set_register(ctx, BSP, VM_REG_GUEST_LDTR, 0)) != 0) goto done; bzero(PT4, PAGE_SIZE); bzero(PT3, PAGE_SIZE); bzero(PT2, PAGE_SIZE); /* * This is kinda brutal, but every single 1GB VM memory segment * points to the same first 1GB of physical memory. But it is * more than adequate. */ for (i = 0; i < 512; i++) { /* Each slot of the level 4 pages points to the same level 3 page */ PT4[i] = (p4_entry_t) ADDR_PT3; PT4[i] |= PG_V | PG_RW | PG_U; /* Each slot of the level 3 pages points to the same level 2 page */ PT3[i] = (p3_entry_t) ADDR_PT2; PT3[i] |= PG_V | PG_RW | PG_U; /* The level 2 page slots are mapped with 2MB pages for 1GB. */ PT2[i] = i * (2 * 1024 * 1024); PT2[i] |= PG_V | PG_RW | PG_PS | PG_U; } cb->copyin(NULL, PT4, ADDR_PT4, sizeof(PT4)); cb->copyin(NULL, PT3, ADDR_PT3, sizeof(PT3)); cb->copyin(NULL, PT2, ADDR_PT2, sizeof(PT2)); cb->setreg(NULL, VM_REG_GUEST_RFLAGS, 0x2); cb->setreg(NULL, VM_REG_GUEST_RBP, ADDR_TARGET); cb->setreg(NULL, VM_REG_GUEST_RSP, ADDR_STACK); cb->setmsr(NULL, MSR_EFER, 0x00000d00); cb->setcr(NULL, 4, 0x000007b8); cb->setcr(NULL, 3, ADDR_PT4); cb->setcr(NULL, 0, 0x80010001); setup_osv_gdt(gdtr); cb->copyin(NULL, gdtr, ADDR_GDTR, sizeof(gdtr)); cb->setgdt(NULL, ADDR_GDTR, sizeof(gdtr)); cb->setreg(NULL, VM_REG_GUEST_RIP, start64); return (0); done: return (error); }
このコードで、実際にOSvが起動して、BHyVeにデバイスが足りないというエラーでクラッシュするところまでの動作を確認できました。
沢山コードを書いているように見えなくもないですが、実際のところかなりのコードはFreeBSDローダのコピーペーストで出来ているので、一から書いたのは僅かなものです。
こんな感じで、ブートコードをきちんと解析出来れば割とシンプルなコードでブートローダ相当のゲストOSローダを実装出来るという事がおわかり頂けましたでしょうか。
参照したブートコード&実装したOSローダのURL
お知らせ
ここではOSv専用のゲストOSローダを実装しましたが、BHyVeプロジェクトでは汎用ブートローダ(grub2)をゲストOSローダとして移植してくるプロジェクトが進行中です。
grub2-bhyveと言います。
いまのところLinuxとOpenBSDの動作確認がとれています。
仕組みは今回説明したものと同様なので、こちらも眺めてみるとおもしろいと思います。