借り初めのひみつきち

仮ブログです。

最小ステップで作る UEFI OS v0.0

UEFI で OS ローダーを作るために必要な最小ステップを考えてみました。

github.com

1. ACPI テーブルの取得

bootinfo.acpi = efi_find_config_table(&efi_acpi_20_table_guid);

対応するハードウェアが完全に既知でそれ以外対応しないなら不要な気もしますが、いまどきの OS で ACPI 対応しないのはありえないですよね。
BIOSと大きく違うところは最初のテーブルの探し方が違うだけで、テーブルが見つかったあとは BIOS と変わりありません。
具体的には efi_main のパラメーターで渡される EFI_SYSTEM_TABLE の ConfigurationTable の中から探します。 ACPI 以外にも SMBIOS 等の構成情報が入ってることがあるのでスキャンします。 -- SMBIOS って必要?

static void* efi_find_config_table(CONST EFI_GUID* guid) {
    for (int i = 0; i < gST->NumberOfTableEntries; i++) {
        EFI_CONFIGURATION_TABLE* tab = gST->ConfigurationTable + i;
        if (IsEqualGUID(&tab->VendorGuid, guid)) {
            return tab->VendorTable;
        }
    }
    return NULL;
}

1.5 カーネルの読み込み

最重要機能と思わせて、実は UEFI ローダーとリンクして同じバイナリにしてしまえばこの処理は不要になります。
一応 UEFI はこのあたりの仕組みがかなり拡張されていて、いろんなデバイスに対応したりファイルシステムを使った高度なアクセスに対応したり PE の再配置もよしなにやってくれます。

2. GOP の取得

status = gBS->LocateProtocol(&EfiGraphicsOutputProtocolGuid, NULL, (void**)&gop);

これも対応するハードウェアが完全に既知だったり画面出力を全く行わなければ不要ですが、ふつうの OS では必須になります。
BIOS の時代は CGA や MDA と互換性あるテキストモードが標準になっていて、とりあえずテキスト流し込むだけで簡単に文字描画できましたが、 UEFI ではテキストモードは利用できません。
多くの場合は 32bit フルカラーでディスプレイの画素数と一致する画面モードに設定されていますが、ごく稀に変な画面モードになっていたり、期待する解像度ではないモードで起動することがあります。本来は画面モードをスキャンして適切なモードに設定したほうがいいですが、今回のプログラムは起動時によしなにやってくれることを期待して何もしません。
UEFIAPI で単純なテキストを表示することができますが、次のステップでそれも使用できなくなるので、 OS 起動後は自力でビットマップ描画しなければなりません。

3. ExitBootServices

do {
    status = gBS->GetMemoryMap(&mmapsize, mmap, &mapkey, &descriptorsize, &descriptorversion);
    while (status == EFI_BUFFER_TOO_SMALL) {
        if (mmap) {
            gBS->FreePool(mmap);
        }
        status = gBS->AllocatePool(EfiLoaderData, mmapsize, (void**)&mmap);
        status = gBS->GetMemoryMap(&mmapsize, mmap, &mapkey, &descriptorsize, &descriptorversion);
    }
    status = gBS->ExitBootServices(image, mapkey);
} while (EFI_ERROR(status));

UEFI で最も重要な概念です。 UEFI には BootServices 環境と RuntimeServices 環境という2つの動作環境があります。
BootServices 環境では UEFI が簡易的な OS としてコンピューターを制御し、キーボードやマウス等のデバイスドライバを提供したり、システム設定 UI を操作したり、シェル上で UEFI アプリケーションを起動して簡単な作業をしたりできます。 BootServices 環境では他の OS を起動することができません。
RuntimeServices 環境は UEFI が一部の API を残して完全にいなくなった状態で、他の OS がコンピューターを制御できます。
BootServices 環境から Runtime Services 環境へは ExitBootServices API を利用して一度だけ移行することができます。
OS を起動するために ExitBootServices API を一度だけ実行する必要がありますが、その後はほとんどの API が使えなくなるので起動処理の一番最後に行う必要があります。
BootServices 環境で使える API と RuntimeServices 環境で使える API の区別は EFI_SYSTEM_TABLE の BootServices と RuntimeServices どちらに乗っているかで区別できます。

実は ExitBootServices API は初回の呼び出しで必ず失敗する環境が存在しますが、その際は適切なリトライ処理が必要で、一度実行したら中断して通常の UEFI 環境に戻ることはできません。ここがちょっと面倒です。
なお、 ExitBootServices API を適切に呼び出すとおまけでメモリーマップも取得できます。他のタイミングで取得することもできますが、 ExitBootServices API の呼び出し中に変更される可能性があるので ExitBootServices API の呼び出しで使ったメモリマップを使うのがベストです。

4. カーネルの起動

start_kernel(&bootinfo);

ここまで集めた構成情報をカーネルに渡し、通常の OS の起動処理に移行します。 BIOS と大きく違うのはここまでの処理で一度もリアルモードに戻ったりアセンブリ言語の関数を呼び出す必要がありません。とはいえ、カーネルを起動したらシステム制御命令のために時々アセンブリ言語の関数を呼び出す必要はあります。

ここまでの4ステップで UEFI から OS を起動することができました。実際のコードはエラー処理とかでもうちょっとだけ長くなります。

f:id:neriring16:20180929185810p:plain

多分これが一番かんたんだと思います。