UEFIアプリケーションのビルド設定
UEFIアプリケーションとしてビルドできる状態を作り、QEMU+OVMF上でそのバイナリが正しく実行されることを検証するところまでを扱います。開発環境の導入手順(Zig・QEMU+OVMF)は付録記事に切り出しており、本記事では導入済みであることを前提とします。
対象環境と設計制約
対象実機はIntel Celeron 4205U(Whiskey Lake世代のx86_64ノートPC向けチップ)を搭載したMousePro CR-I1U01です。BIOSはAmerican Megatrends(AMI Aptio系)で、Secure Bootは無効化した状態で運用します。この機体には2つの制約があり、以降の設計判断に直結します。
- シリアルポートが無い:デバッグ出力の経路としてシリアルコンソールを前提にできません
- レガシーVGAテキストモードを提供しない:多くの現代UEFI機に共通する制約で、文字出力であっても最初からピクセル単位の描画として実装する必要があります
この2点から、テキスト表示を含めてデバッグ出力全般をGOPフレームバッファへの直接描画で行う方針を採ります。実機テストはUSBメモリ(\EFI\BOOT\BOOTX64.EFIを配置したリムーバブルメディア構成)で行い、内蔵ストレージには書き込みません。
ディレクトリ構成
カーネルとブートローダーをリポジトリ単位では分割せず、モノレポ内でモジュールとして分離します。
| ディレクトリ | 役割 |
|---|---|
boot/ |
UEFIブートローダー本体 |
arch/ |
GDT/IDT/割り込みなどCPU制御層 |
kernel/ |
メモリ管理・スケジューラなどコア機能 |
drivers/ |
キーボード・グラフィックスなど周辺機器 |
現時点ではboot/にのみ実体があり、他のディレクトリは今後の作業で実装が入ります。
UEFIターゲットへのビルド設定
エントリーポイントは、成功ステータスを返すだけの最小構成にします。コンソールへの文字出力はこの後の作業で扱います。
// boot/main.zig
const std = @import("std");
const uefi = std.os.uefi;
pub fn main() uefi.Status {
return .success;
}通常の実行ファイルと同様にpub fn main()を定義するだけで、UEFIアプリケーションとして成立します。これはZig標準ライブラリのstart.zigが担っている処理で、ビルドターゲットのOSがuefiの場合、EfiMainというラッパー関数が自動生成されます。UEFIファームウェアから渡されるhandleとsystem_tableをそのラッパーがstd.os.uefi.handle / std.os.uefi.system_tableへ格納した上で、こちらが定義したmain()を呼び出す構造になっています。main()の戻り値はvoid・uefi.Status・uefi.Error!voidのいずれかを受け付けます。EFIアプリケーションのエントリポイント仕様(EFI_IMAGE_ENTRY_POINT)を直接扱う必要はなく、ABIの橋渡しはZig側に委ねられています。
build.zigではターゲットをx86_64・OS uefiとして指定します。
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const uefi_target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .uefi,
.abi = .msvc,
});
const boot = b.addExecutable(.{
.name = "BOOTX64",
.root_module = b.createModule(.{
.root_source_file = b.path("boot/main.zig"),
.target = uefi_target,
.optimize = optimize,
}),
});
b.installArtifact(boot);
}abiに.msvcを明示していますが、ZigのTarget.zigではOSがuefiの場合のデフォルトABIがもともと.msvcに解決されるため、省略しても結果は変わりません。それでも明示しているのは、なぜWindows系のABI名が出てくるのかという疑問への回答をコード上に残すためです。UEFIのイメージフォーマットはPE/COFF(Windows実行ファイルと同じ系統のフォーマット)であり、Target.zigでもuefiとwindowsはオブジェクトフォーマットとして揃って.coffに分類されています。Linux向けのELFとは別系統の出力になる、という点を押さえておけば、.msvcというABI名は自然に理解できます。
ビルドすると.efi本体に加えて.pdb(PE系のデバッグ情報ファイル)も生成されます。
$ zig build
$ find zig-out -type f
zig-out/bin/BOOTX64.efi
zig-out/bin/BOOTX64.pdb実機・QEMUどちらのブートでも使用するのはBOOTX64.efiのみで、.pdbはESPには含めません。
QEMU + OVMFによる起動検証
.efiファイルをESP(EFI System Partition)と同じディレクトリ構成に置き、QEMU+OVMFで起動を確認します。イメージファイルを都度作成する必要がないため、ビルドと起動確認のループを高速に回せます。
今回実装したmain()は画面に何も描画せず成功ステータスを返すだけなので、目視で確認できる変化はありません。そこでQMP(QEMU Machine Protocol)経由でVMの実行状態を問い合わせたところ、起動プロセスが例外なく進行し、数秒経過後も実行中の状態を維持していることを確認できました。main()内で未定義動作を踏んだ場合や、UEFIのイメージフォーマットが不正な場合は、この時点で例外が発生してファームウェアがリセットループに入るか、VMがshutdown状態に落ちます。
ただし、この確認方法には限界があります。ESPにBOOTX64.efiを配置しない状態で起動しても、ファームウェア自身のブートマネージャーが起動したまま同様の状態になるため、「用意したUEFIアプリケーションが実際に読み込まれ実行されたこと」までは判別できません。
より直接的な根拠としては、OVMFが内部で出力するデバッグログを確認する方法もありますが、aptで導入したovmfパッケージはRELEASEビルドであり、EDK II内部のDEBUG()マクロによるログ出力がコンパイル時に除去されているため、この経路からは情報が得られませんでした。
そのため現時点では、起動プロセスが例外なく進行することの確認にとどめます。UEFIアプリケーションが実際に実行された証跡は、コンソール文字列出力を実装したうえでこの後の作業にて取得します。
実機USBブートの構成
QEMUでの確認と同じEFI/BOOT/BOOTX64.EFIという配置をそのままUSBメモリ(FAT32フォーマット)に書き込めば、実機側の追加設定は不要になります。UEFIファームウェアはリムーバブルメディアの既定パスとして\EFI\BOOT\BOOTX64.EFIを自動的に探索する仕様のため、ブートエントリの手動登録は要りません。詳細な手順は別途ドキュメント化しており、実機での動作確認はコンソール出力を実装するこの後の作業で行います。
今後は、このboot/main.zigにUEFI Boot Servicesを使ったコンソール文字列出力を実装し、QEMU+OVMFでの目視確認と実機USBブートでの動作確認まで進めます。
参考リンク
- OSDev Wiki: UEFI —— UEFI開発全般のリファレンス。ブートプロセスやプロトコルの概要を掴むのに使えます
- UEFI Specification(UEFI Forum) ——
GetMemoryMapや各種プロトコルの正確な仕様を確認する一次情報源です - TianoCore EDK II —— OVMFのベースになっているリファレンス実装。ファームウェア側の挙動を追う際に参照します
- Zig `std.os.uefi` のソースコード —— この記事で触れた
EfiMainラッパーや各種UEFI型の定義本体です - rust-osdev/uefi-rs —— 他言語でのUEFIバインディング実装例。プロトコルの扱い方を比較する際の参考になります