ExitBootServicesと自作カーネルへのハンドオフ

今回は、ExitBootServicesを呼び出してBoot Servicesが使える世界を抜け、自作カーネルへ処理を引き継ぐところまでを扱います。ブートローダーとしての役割はここで終わり、以後は自作カーネルが直接ハードウェアを扱う番になります。

ハンドオフの設計:同一バイナリ内での関数呼び出し

「カーネルへのハンドオフ」と聞くと、ブートローダーが別のカーネルバイナリをディスクから読み込み、そのエントリポイントへジャンプする構成を思い浮かべるかもしれません。しかし今回はその構成を採らず、boot/kernel/を同じ実行ファイルにまとめ、ブートローダー側からkernel.kmain()を通常の関数呼び出しとして呼ぶだけにしています。

これは手を抜いているわけではなく、意図した設計です。別バイナリをロードする構成にすると、PEやELFの実行ファイルフォーマットを解析して読み込む「自前のローダー」が新たに必要になり、この段階で扱う範囲を大きく超えます。関数呼び出しも、アセンブリレベルではcall命令によるジャンプであり、「ハンドオフ」の本質(それまでの処理から抜けて、別のコードに制御を渡す)は満たしています。モノレポ内をboot/ arch/ kernel/ drivers/というモジュールで分けるという、このプロジェクトの設計方針とも合致します。

Zigのビルドシステム上は、kernel/を独立したモジュールとしてbuild.zigに定義し、bootモジュールからの依存として追加しています。

// build.zig(抜粋)
const kernel_module = b.createModule(.{
    .root_source_file = b.path("kernel/kernel.zig"),
    .target = uefi_target,
    .optimize = optimize,
});

const boot_module = b.createModule(.{
    .root_source_file = b.path("boot/main.zig"),
    .target = uefi_target,
    .optimize = optimize,
});
boot_module.addImport("kernel", kernel_module);

Zig 0.16では、モジュールのルートディレクトリ外へ相対パス(../)で@importすることができません。UEFIからのメモリマップとGOPフレームバッファ情報の取得の記事で作ったboot_info.zigboot/kernel/の両方から参照したかったため、kernel/側に置いたうえでkernel.zigからpub const boot_info = @import("boot_info.zig");として再エクスポートし、boot/main.zig側は@import("kernel")経由でkernel.boot_infoにアクセスする形にしています。

ExitBootServicesの呼び出しと失敗時の対処

ExitBootServicesは、直前のGetMemoryMapで得たmap_keyを引数として要求します。このmap_keyは、その後に別のBoot Services呼び出し(メモリ確保など)が挟まると古くなり、ExitBootServices自体が失敗します。この失敗に対処するため、GetMemoryMapExitBootServicesをセットでリトライするループを実装しました。

// boot/main.zig
const final_mmap = while (true) {
    const mmap = boot_services.getMemoryMap(mmap_buffer) catch
        halt(con_out, std.unicode.utf8ToUtf16LeStringLiteral("Failed to get memory map\r\n"));
    if (boot_services.exitBootServices(uefi.handle, mmap.info.key)) |_| {
        break mmap;
    } else |_| {}
};

GetMemoryMapは、Boot Services関数の中でも例外的に、ExitBootServicesの呼び出しが一度失敗した直後まで呼び出しが許されています。これは、失敗時にメモリマップを取り直して再試行するためのものです。今回のループはこの仕様に沿ったもので、map_keyが失効していれば単に取り直して再挑戦し、成功すればそのメモリマップをbreakで返します。

このバッファをUEFIからのメモリマップとGOPフレームバッファ情報の取得の記事よりも多め(問い合わせ結果より8エントリ分)に確保しているのは、GOPの取得やコンソール出力など、ExitBootServicesより前に行う他のBoot Services呼び出しがメモリマップを変化させる余地があるためです。

カーネル側の実装

ExitBootServicesが成功すると、con_outを含むBoot Services全般が使えなくなります。したがって、ハンドオフ後の動作確認には、UEFIからのメモリマップとGOPフレームバッファ情報の取得の記事で取得したGOPフレームバッファへの直接書き込みを使います。ピクセルフォーマットの判定には、boot_info.zigに新たに追加したPixelFormat列挙型を使用しています。std.os.uefiGraphicsOutput.PixelFormatと同じ並び順(rgb=0, bgr=1, bit_mask=2, blt_only=3)に合わせておく必要がある点は、コード上にコメントを残しています。

// kernel/kernel.zig
pub fn kmain(info: *const boot_info.BootInfo) noreturn {
    const fb = info.framebuffer;
    const pixels: [*]volatile u32 = @ptrFromInt(fb.base);
    const width: usize = fb.width;
    const height: usize = fb.height;
    const stride: usize = fb.pixels_per_scan_line;

    const color: u32 = switch (@as(boot_info.PixelFormat, @enumFromInt(fb.pixel_format))) {
        .rgb => 0x000000ff,
        .bgr => 0x00ff0000,
        else => 0x00ffffff,
    };

    var y: usize = 0;
    while (y < height) : (y += 1) {
        var x: usize = 0;
        while (x < width) : (x += 1) {
            pixels[y * stride + x] = color;
        }
    }

    while (true) {}
}

フレームバッファの各ピクセルは4バイトで、その並び順はUEFIからのメモリマップとGOPフレームバッファ情報の取得の記事で取得したpixel_formatred_green_blue_reserved_8_bit_per_colorblue_green_red_reserved_8_bit_per_color)によって変わります。今回は赤一色で塗りつぶすことを目標に、フォーマットに応じて正しいバイト位置に0xFFを置くよう色の値を出し分けています。

QEMU+OVMFでの確認

QEMU上で画面を確認したところ、UEFIからのメモリマップとGOPフレームバッファ情報の取得の記事まで表示されていたコンソールのログ(zigos bootloaderMemory map: ...など)は消え、画面全体が赤一色で塗りつぶされた状態になりました。

この結果は、次のことをまとめて裏付けています。

  • ExitBootServicesが実際に成功したこと(失敗していればハンドオフ自体が起きず、コンソールのログが残ったままになるか、ファームウェアがエラー処理に入るはずです)
  • 制御が確かにkernel.kmain()へ渡ったこと
  • 渡されたBootInfoのフレームバッファアドレス・解像度・ストライドの値が正しく、実際に書き込みが機能したこと

コンソール出力を使わずにここまで確認できたのは、UEFIからのメモリマップとGOPフレームバッファ情報の取得の記事でGOPフレームバッファの情報をあらかじめ取得しておいたおかげです。

実機(MousePro CR-I1U01)のUSBブートでも同様に画面全体が赤一色になることを確認しました。ファームウェア実装(AMI Aptio系)が異なる環境でも、ExitBootServicesのリトライ処理とハンドオフ後のフレームバッファ書き込みが機能したことになります。

参考リンク