ゲストOSのブートローダをホストOS上で動作するプログラムに翻訳する

このブログエントリはカーネル/VM Advent Calendar 2013 8日目の記事、および第九回カーネルVM探検隊の発表の補足記事です。

発表資料と動画

このエントリだけでも理解できるように書いたつもりですが、はじめにこちらをみて頂いた方が分かりやすいかもしれません。
BHyVeでOSvを起動したい 〜BIOSがなくてもこの先生きのこるには〜 // Speaker Deck

何がしたいのか?

BHyVeはKVM+QEMUVMwareと異なり、BIOSUEFIなどの標準的なファームウェアを持ちません。
そのため、MBRのブートセクタからのブートやUEFIブートローダからのブートを行う事ができません。

代わりに、ホストOSで動くプログラム(ゲストOSローダ)でゲストマシン上にゲストOSをロードして実行します。
この時、BIOSに依存している通常のブートローダの実行は一切行わず、ブートローダが行っているような処理をゲストOSローダで代わりに実行します。
全てのお膳立てをゲストOSローダが行い、ゲストマシンは最初の命令が実行される時点で既にページテーブルが設定済みでプロテクテッドモードになっています。

これは、サスペンド&レジュームでメモリやレジスタの内容をサスペンド時の状態に復帰することにより、OSの起動処理を飛ばしていきなりOS実行中の状態から再開するのに少し似ています。

レジューム処理が電源を落とす直前の実行中のOSの状態へ復帰するのに対し、ゲストOSローダでは「ブートローダカーネルをロードして実行しようとした状態」をゲストマシン上に作り出し、そこからゲストマシンを実行します。

このような仕組みのブート方法をとるのは何もBHyVeだけではありません。
XenのPV guestはほぼ同様の手法でゲストOSをロードしていますし、QEMUKVM)では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/に対するmmapが行われ、mmapされた空間にゲストメモリ空間がマップされています。

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/に対するioctlが行われ、指定したvCPUのレジスタ値が設定されます。

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のブートセクタを読んでみます。
ここでは以下のような処理が行われています:

  • ディスクからカーネル引数をロード
  • ディスクからカーネルのELFバイナリをロード
  • BIOSからメモリマップを取得
  • カーネルの32bitエントリポイントへエントリ

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 = 0x80000loader.elfの一時的なロード先
bootsect = 0x7c00 ←このプログラムの先頭アドレス
cmdline = 0x7e00cmdlineのロード先
target = 0x200000 ←カーネルELFファイルの最終的なロード先
entry = 24+target ←カーネルELFファイルのELFヘッダ上のe_entryフィールドのアドレス ここにカーネルのエントリアドレスが格納されている

mb_info = 0x1000multiboot headerの置き場 カーネルのmain()関数の引数として渡される
mb_cmdline = (mb_info + 16)  ← cmdlineのアドレスはmb_infocmdlineメンバ経由で渡される
mb_mmap_len = (mb_info + 44) ← e820で得たメモリマップの長さはmb_infommap_lenメンバ経由で渡される
mb_mmap_addr = (mb_info + 48) ← e820で得たメモリマップのアドレスはmb_infommap_addrメンバ経由で渡される

e820data = 0x2000e820で得るメモリマップの置き場

.text

start: ← エントリポイント
	ljmp $0, $initinitへジャンプ

# 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 ← 一時的ロード先(0x80000lba:
	.quad 128 ← カーネルELFファイルのオフセット(128セクタ目)

int1342_boot_struct: # for command line ← cmdline用のDAP
	.byte 0x10
	.byte 0
	.short 0x3f   # 31.5k ← 31.5k分ロード
	.short cmdline0x7e00へロード
	.short 0
	.quad 1cmdlineのオフセット(1セクタ目)

xfer: .long targettargetへのポインタ

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, %siDS:SIDAPを指定
	mov $0x42, %ah
	mov $0x80, %dl
	int $0x13INT 13h AH=42h: Extended Read Sectors From Drive
	movl $cmdline, mb_cmdlinemb_info->mb_cmdlinecmdlineのアドレスを代入
read_disk: ← ここではINT13hコールで32KBごとディスクを読んでプロテクテッドモードへ切り替えhighmem領域であるtargetへコピーしている。
これを繰り返すことにより、BIOSを使いながらリアルモードのメモリ空間の制約から逃れて大きなカーネルELFファイルをロードしている。
	lea int1342_struct, %siDS:SIDAPを指定
	mov $0x42, %ah
	mov $0x80, %dl
	int $0x13INT 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 gdtGDTのロード
	mov $0x11, %axCR0にセットしたい値をロード
	lmsw %axCR0に値をセット(PEを1にする)
	ljmp $8, $1fCSレジスタに32bitセグメントをセット
1:
	.code32
	mov $0x10, %ax
	mov %eax, %ds
	mov %eax, %es
	mov $tmp, %esi
	mov xfer, %edi
	mov $0x8000, %ecx
	rep movsbtmpからxferが指し示すtargetへデータをコピー
	mov %edi, xfer
	mov $0x20, %al
	mov %eax, %ds
	mov %eax, %es
	ljmpw $0x18, $1f
1:
	.code16
	mov $0x10, %eax
	mov %eax, %cr0CR0からPEをクリア
	ljmpw $0, $1f
1:
	xor %ax, %ax
	mov %ax, %ds
	mov %ax, %es
	sti
	addl $(0x8000 / 0x200), lba ←読み出すオフセット値を加算
	decw count32
	jnz read_diskcount32が0になるまで繰り返す(4096回 × 32KB128MBdone_disk:

	mov $e820data, %edi
	mov %edi, mb_mmap_addrmb_info->mb_mmap_addre820dataのアドレスを代入
	xor %ebx, %ebxContinuation(デフォルトは0)
more_e820:
	mov $100, %ecxBuffer Size
	mov $0x534d4150, %edx
	mov $0xe820, %axSignature	'SMAP' 
	add $4, %edi
	int $0x15INT 15h, AX=E820h - Query System Address Map
	jc done_e820 ← もう続きが無かったらループを抜ける
	mov %ecx, -4(%edi)
	add %ecx, %edi
	test %ebx, %ebx
	jnz more_e820Continuationをチェックして続きがあるなら繰り返す
done_e820:
	sub $e820data, %edi
	mov %edi, mb_mmap_lenmb_info->mb_mmap_lene820dataのサイズを代入

	cli ← 割り込み禁止(ここからプロテクテッドモードへの切り替え)
	mov $0x11, %ax
	lmsw %axCR0PEビット書き込み
	ljmp $8, $1fCSレジスタに32bitセグメントセット
1:
	.code32
	mov $0x10, %ax
	mov %eax, %ds
	mov %eax, %es
	mov %eax, %gs
	mov %eax, %fs
	mov %eax, %ss
	mov $target, %eaxtargetEAXへ代入
	mov $mb_info, %ebxmb_infoEBXへ代入
	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 = . - 8GDTの定義
    .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, %ebpEAXに代入されていたtargetのアドレスをEBPへコピー
    lgdt gdt_descGDTをロード
    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, $1fCSを「32-bit code segment」にセット
1:
    and $~7, %esp
    mov $BOOT_CR4, %eax
    mov %eax, %cr4CR4レジスタで64bitメモリ空間などを有効化
    lea ident_pt_l4, %eax
    mov %eax, %cr3 ← ページテーブルをロード
    mov $0xc0000080, %ecx
    mov $0x00000900, %eax
    xor %edx, %edx
    wrmsrEFERLMEビットを有効化
    mov $BOOT_CR0, %eax
    mov %eax, %cr0CR0レジスタでページングを有効化
    ljmpl $8, $start64CSを「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と言います。
いまのところLinuxOpenBSDの動作確認がとれています。
仕組みは今回説明したものと同様なので、こちらも眺めてみるとおもしろいと思います。