UEFIからのメモリマップとGOPフレームバッファ情報の取得

UEFI Boot Servicesを使ってメモリマップとGOPフレームバッファの情報を取得し、これらを自作カーネルへ引き継ぐための構造を設計します。カーネルへの実際のハンドオフはまだ先の作業になりますが、そこで必要になる情報の集め方を先に固めておきます。

メモリマップの取得

UEFIファームウェアは、物理アドレス空間のどの範囲がどの用途に使われているか(空き領域か、ファームウェア自身が使っている領域か、ACPIテーブル用領域か、MMIO領域かなど)を記述した一覧をBoot Services経由で提供しています。レガシーBIOSにおけるINT 15h, AX=E820hの相当機能ですが、UEFIではGetMemoryMapという関数呼び出しとして提供されます。

この一覧は、MemoryDescriptorという構造体の配列として返されます。それぞれのディスクリプタは種別(type)、物理開始アドレス(physical_start)、ページ数(number_of_pages)、属性(attribute)を持ち、1ページ=4KiB単位で領域を表現します。

GetMemoryMapの呼び出しには、サイズの見積もりと実際の取得の2段階が必要です。マップ全体のサイズは実行中も変動するため、まず必要なバッファサイズを問い合わせ、そのサイズ分のバッファを確保してから改めて取得する、という手順を踏みます。ZigではBootServices.getMemoryMapInfo()がサイズ問い合わせ、BootServices.getMemoryMap()が実際の取得に対応します。

// boot/main.zig
const mmap_info = boot_services.getMemoryMapInfo() catch
    halt(con_out, std.unicode.utf8ToUtf16LeStringLiteral("Failed to query memory map size\r\n"));
const mmap_buffer_size = (mmap_info.len + 4) * mmap_info.descriptor_size;
const mmap_buffer = boot_services.allocatePool(.loader_data, mmap_buffer_size) catch
    halt(con_out, std.unicode.utf8ToUtf16LeStringLiteral("Failed to allocate memory map buffer\r\n"));
const mmap = boot_services.getMemoryMap(mmap_buffer) catch
    halt(con_out, std.unicode.utf8ToUtf16LeStringLiteral("Failed to get memory map\r\n"));

バッファサイズには問い合わせ結果より4エントリ分余分に確保しています。これは、直後のallocatePoolによるメモリ確保自体がメモリマップに新しいエントリを追加しうるためです。問い合わせ時点のサイズぴったりで確保すると、確保後に取得しようとした際にバッファ不足で失敗する可能性があります。

halt()は、エラー発生時にメッセージを表示したまま無限ループへ入るヘルパー関数です。戻り値の型はnoreturnにしており、catchの右辺としてそのまま使えます(catchの両辺の型は最終的に一致する必要がありますが、noreturnはどちらの型にも合流できるため、エラー処理を1行で書けます)。

メモリマップをExitBootServices呼び出し前に取得しておく必要があるのは、ExitBootServices自体が呼び出し時にmap_keyという引数を要求するためです。このmap_keyは直前のGetMemoryMap呼び出しで得られた値と完全に一致している必要があり、その間に別のBoot Services呼び出し(メモリ確保など)でメモリマップが変化していると値が古くなり、ExitBootServicesが失敗します。つまりメモリマップの取得は、単に「ExitBootServices後には取得できなくなるから先に取っておく」というより、ExitBootServicesを呼ぶための前提条件そのものという位置づけです。実際、GetMemoryMapはBoot Services関数の中でも例外的に、ExitBootServicesの呼び出しが一度失敗した直後までは呼び出しが許されています。これは、失敗時にメモリマップを取り直して再試行するための救済措置です。

GOPフレームバッファ情報の取得

Graphics Output Protocol(GOP)は、フレームバッファの物理アドレス・サイズ・解像度・ピクセルフォーマットを取得するためのプロトコルです。UEFI Boot Servicesによるコンソール出力の記事で扱ったSimple Text Output Protocolが文字単位の描画だったのに対し、GOPはピクセル単位で直接描画するための情報を提供します。今回はGOPを使ってピクセルを描画するところまでは行わず、情報の取得と表示にとどめます。

GOPはLocateProtocolでハンドルを取得し、modeフィールド経由で情報にアクセスします。

const gop = (boot_services.locateProtocol(uefi.protocol.GraphicsOutput, null) catch null) orelse
    halt(con_out, std.unicode.utf8ToUtf16LeStringLiteral("Graphics Output Protocol not found\r\n"));
const mode_info = gop.mode.info;

gop.mode.frame_buffer_basegop.mode.frame_buffer_sizeがフレームバッファ本体のアドレスとサイズ、mode_info.horizontal_resolution / vertical_resolution / pixel_format / pixels_per_scan_lineが解像度とピクセルの並び方に関する情報です。

GOPの情報もExitBootServices呼び出し前に取得しておく必要がありますが、その理由はメモリマップとは異なります。検索に使っているLocateProtocol自体がBoot Services関数であり、ExitBootServices後はBoot Services全般とともに呼び出せなくなるためです。一方で、frame_buffer_baseが指す物理メモリ領域への書き込み自体は、ExitBootServices後も引き続き可能です。単なる物理メモリ(またはGPU側のMMIO領域)へのアクセスであり、Boot Servicesの生死とは関係がないためです。ExitBootServices後に失われるのは、そのアドレスや解像度をファームウェアに問い合わせる手段だけであり、値さえ事前に控えておけば描画自体は続けられます。

カーネルへの受け渡し方式の設計

これらの情報は、ExitBootServicesを呼んだ後に自作カーネル側で使うことになります。カーネル側はstd.os.uefiを参照しない独立した実行ファイルになる予定のため、取得した情報をそのままUEFI固有の型(MemoryDescriptorGraphicsOutput.Mode)で持ち回るのではなく、UEFIに依存しない形の構造体に詰め替えてから渡す設計にします。

// boot/boot_info.zig
pub const MemoryMapInfo = struct {
    buffer: [*]u8,
    descriptor_size: usize,
    descriptor_version: u32,
    entry_count: usize,
};

pub const FramebufferInfo = struct {
    base: u64,
    size: usize,
    width: u32,
    height: u32,
    pixels_per_scan_line: u32,
    pixel_format: u32,
};

pub const BootInfo = struct {
    memory_map: MemoryMapInfo,
    framebuffer: FramebufferInfo,
};

pixel_formatは本来GraphicsOutput.PixelFormatという列挙型ですが、BootInfo側ではu32として持たせています。カーネル側のビルドでstd.os.uefiをインポートせずに済むようにするための割り切りで、値の意味はカーネル側で改めて定義した列挙型に変換して使う想定です。

この構造体自体は今回main()内で組み立てて画面表示に使っていますが、実際にカーネルへ渡す配線は、ExitBootServicesの実装と合わせて今後の作業で行います。

QEMU+OVMFでの取得結果

UEFI Boot Servicesによるコンソール出力の記事と同様にQEMU上で画面を確認したところ、次の内容が表示されました。

zigos bootloader
Memory map: 119 entries (48 bytes/descriptor)
Conventional memory: 21485 pages (83 MiB)
GOP: 1024x768, blue_green_red_reserved_8_bit_per_color
Framebuffer: 0xc0000000, 3145728 bytes

いずれの値も妥当であることが確認できます。フレームバッファのサイズ(3,145,728バイト)は、解像度1024×768にピクセルあたり4バイト(blue_green_red_reserved_8_bit_per_colorはB, G, R, Reservedの4チャンネルをそれぞれ8bitで持つ形式)を掛けた値と一致しています。Conventional memoryの83 MiBは、QEMUにデフォルトで割り当てられる128MBのRAMから、ファームウェア自身やBoot Servicesが使用中の領域を除いた空き容量として妥当な値です。

参考リンク