💻

Zig で作ったブートローダーを実機で動かすには

2023/04/18に公開

この記事で書くこと

この記事では Zig で作ったブートローダーから Hello World するまで で作ったブートローダーと自作カーネルを QEMU ではなく実機で動かすための手順を紹介します。
私は Chromebook C302CA で試しました。お使いの PC によっては私の環境と別の手順が必要かもしれません。

ブート用 USB メモリ作成

まずはブート用の USB メモリを作成します。手順は簡単で以下のステップで作成できます。

  1. USB メモリを FAT 形式でフォーマットします。Mac OS だとディスクユーティリティアプリでフォーマットできます。Macのディスクユーティリティでストレージデバイスを消去して再フォーマットする
  2. fs ディレクトリから USB メモリのルートディレクトリに kernel.elf をコピー
  3. fs ディレクトリから USB メモリの /efi/boot ディレクトリに bootx64.efi をコピー

最終的に USB メモリは以下の構成になります。

/(ルート)
|--kernel.elf
└--efi
   └--boot
      └--bootx64.efi

UEFI BIOS からブートローダー起動

つぎに UEFI BIOS の画面を開いて USB メモリからブートローダーを起動します。
PC によって UEFI BIOS の開き方やメニューの表示に違いがあると思いますので、参考に私の PC で実行したときの画像を貼っておきます。

  1. Boot Menu を選択

  2. USB メモリを選択(私は SD カードを使ったので画像では SD Device となっています)

ブートローダーから自作カーネルを起動して HELLO WORLD! が表示される...はずでしたが、ブートローダーでエラーがでてしまいました。

failed to open kernel file: os.uefi.status.Status.NotFound

つまりカーネルファイル(kernel.elf)が見つからないということですね。原因を調べるために print デバッグすると、どうやら openVolume は PC に内蔵されたストレージのルートディレクトリを開いているようでした。カーネルファイルを見つけるには USB メモリのルートディレクトリを開かないといけません。

src/boot.zig
    ...
    // ルートディレクトリを開く
    var root_dir: *uefi.protocols.FileProtocol = undefined;
    status = fs.openVolume(&root_dir);
    if (status != .Success) {
        printf("failed to open root directory: {d}\r\n", .{status});
        return status;
    }
    printf("opened root directory\r\n", .{});
    
    // デバッグのためルートディレクトリ直下のファイル/ディレクトリ一覧を表示
    printf("list files in root directory\r\n", .{});
    status = listDir(root_dir);
    if (status != .Success) {
        printf("failed to list root directory: {d}\r\n", .{status});
        return status;
    }
    ...
}

fn listDir(dir: *uefi.protocols.FileProtocol) uefi.Status {
    var status: uefi.Status = undefined;
    var info_buffer: [1024]u8 align(8) = undefined;

    while (true) {
        var info_size: usize = info_buffer.len;
        status = dir.read(&info_size, info_buffer[0..]);
        if (status != .Success) return status;
        if (info_size == 0) break;

        var info: *uefi.protocols.FileInfo = @ptrCast(*uefi.protocols.FileInfo, &info_buffer);

        status = con_out.outputString(&[_:0]u16{ '-', ' ' });
        if (status != .Success) return status;
        status = con_out.outputString(info.getFileName());
        if (status != .Success) return status;
        status = con_out.outputString(&[_:0]u16{ '\r', '\n' });
        if (status != .Success) return status;
    }

    return uefi.Status.Success;
}

ブートローダー修正

ゼロからのOS自作入門MikanOS のブートローダーを見ると BootServices の OpenProtocol を使って USB メモリのルートディレクトリを開いていました。

uefi.handle はブートローダーを実行したデバイスの機能を提供するインターフェースになっていて、OpenProtocol でそのデバイスのファイルシステム(SimpleFileSystemProtocol)を取得する、ということだと思います。
今回の uefi.handle は USB メモリの機能を提供するインターフェースです。

src/boot.zig
fn openRootDir(root_dir: **uefi.protocols.FileProtocol) uefi.Status {
    var status: uefi.Status = undefined;
    var loaded_image: *uefi.protocols.LoadedImageProtocol = undefined;
    var fs: *uefi.protocols.SimpleFileSystemProtocol = undefined;

    status = bs.openProtocol(
        uefi.handle,
        &uefi.protocols.LoadedImageProtocol.guid,
        @ptrCast(*?*anyopaque, &loaded_image),
        uefi.handle,
        null,
        .{ .by_handle_protocol = true },
    );
    if (status != .Success) {
        printf("failed to open loaded image protocol: {d}\r\n", .{status});
        return status;
    }

    var device_handle = loaded_image.device_handle orelse {
        printf("failed to get device handle\r\n", .{});
        return uefi.Status.Unsupported;
    };
    printf("loaded image: device_handle={*}\r\n", .{device_handle});
    status = bs.openProtocol(
        device_handle,
        &uefi.protocols.SimpleFileSystemProtocol.guid,
        @ptrCast(*?*anyopaque, &fs),
        uefi.handle,
        null,
        .{ .by_handle_protocol = true },
    );
    if (status != .Success) {
        printf("failed to open device handle: {d}\r\n", .{status});
        return status;
    }

    return fs.openVolume(root_dir);
}

新しく作った openRootDir 関数を使うようにブートローダーを修正しました。

src/boot.zig
var con_out: *uefi.protocols.SimpleTextOutputProtocol = undefined;
- var fs: *uefi.protocols.SimpleFileSystemProtocol = undefined;
var gop: *uefi.protocols.GraphicsOutputProtocol = undefined;

pub fn main() uefi.Status {
    ...
-    // SimpleFileSystemProtocol 取得
-    status = bs.locateProtocol(&uefi.protocols.SimpleFileSystemProtocol.guid, null, @ptrCast(*?*anyopaque, &fs));
-    if (status != .Success) {
-        printf("failed to locate file system protocol: {d}\r\n", .{status});
-        return status;
-    }
    ...
    // ルートディレクトリを開く
    var root_dir: *uefi.protocols.FileProtocol = undefined;
-    status = fs.openVolume(&root_dir);
+    status = openRootDir(&root_dir);
    ...

もう一度 UEFI BIOS からブートローダー起動

ブートローダーを再ビルドしたあと USB メモリの bootx64.efi を置き換えます。
そして BootMenu から USB を選択すると...。ブートローダーから自作カーネルが実行できました!

まとめ

実機で確認できると QEMU で実行するのとは違った嬉しさがあると思うのでぜひ試してみてください。
今回の修正内容は以下のリポジトリから確認できます。
ssstoyama/bootstrap_zig

git clone --branch from_device_dir https://github.com/ssstoyama/bootstrap_zig.git

参考

ゼロからのOS自作入門
MikanOS
UEFI Specification 2.10

Discussion