/home/tnishinaga/TechMEMO

日々行ったこと、面白かったことを書き留めます。

UEFIで任意のProtocolを使ってプログラムを書くためにはどうすればいいのかメモ

ふと、UEFIアプリでシリアルを直接さわって通信がしたくなりました。

UEFI 2.0にはEFI SERIAL IO PROTOCOLというプロトコルがあり、これを用いればシリアル通信をハードウェアを直接触ること無く行えるようです。

早速、このプロトコルを使ってシリアル通信をするプログラムを作ろう……としたのですが、大変お恥ずかしいことにその時の私はUEFIで任意のプロトコルを呼び出して制御を行う方法がイマイチわかっておらず、すぐに作ることができませんでした。

その後、KernelVM探検隊のみなさまにUEFIについて質問したところ、参考になるサイトやプログラムを教えていただき、以下のシリアルの入力をそのまま出力に返すプログラムを作ることができました。

baremetal_hikeyboard/uefi_serial_echo_sample at master · tnishinaga/baremetal_hikeyboard · GitHub

このプログラムを作ったことでUEFIアプリで任意のプロトコルを使うために、なんとなくどのようなことをすべきかが見えるようになってきたので、初心者の心を忘れないうちに書き残しておこうと思います。

注意

  • 本記事はどうすれば目的のプロトコルを取得し、インターフェースを使えるかに注目します。UEFIのすべてを解説する記事ではありません
  • 一部筆者の理解が足りていないため、説明に誤りがある場合があります。コメントで教えていただけると嬉しいです。

任意のプロトコルの取得と利用方法

UEFIの提供する様々な機能はインターフェースを介して利用できるようになっており、 インターフェースは「プロトコル(Protocol)」と呼ばれる組でグルーピングされて提供されています。

プロトコルの取得は、以下の手順で行なえます。

  1. BootService(BS)のLocateHandleで目的のプロトコルを取得できるハンドラを見つける
  2. BSのHandleProtocolで先程取得したハンドラから目的のプロトコルを取得する

ハンドラの取得

プログラム上でのプロトコルは、各インターフェースの関数ポインタを格納した構造体であり、メモリ上の何処かに置かれています。

そのため、目的のインターフェースを使うためには、プロトコルの先頭アドレスを取得し、プロトコルの構造体にキャストし、そこから関数ポインタを使って目的のインターフェースを使うということが必要になります。

プロトコルを取得するためには、そのプロトコルを取得できるハンドル(Handle)を見つける必要があります。 この作業を行う関数がLocateHandleです。 プロトコルにはGUIDというIDがあるため、LocateHandleに目的のプロトコルのGUIDを渡すことで、ハンドルを見つけてきてくれます。

詳しい使い方は以下を参照。

EFI BOOT SERVICES - PhoenixWiki

今回作ったサンプルでは、以下の部分がハンドラの取得部分になります。

    EFI_GUID serial_io_protocol = SERIAL_IO_PROTOCOL;
    EFI_STATUS efi_status;

    // ... skip ...

    // search handler
    UINTN handlers_size = 0;
    EFI_HANDLE *handlers = NULL;
    efi_status = uefi_call_wrapper(
        BS->LocateHandle,
        5,
        ByProtocol,
        &serial_io_protocol,
        0,
        &handlers_size,
        handlers
    );
    if (efi_status == EFI_BUFFER_TOO_SMALL) {
        efi_status = uefi_call_wrapper(BS->AllocatePool,
            3,
            EfiBootServicesData,
            handlers_size,
            (VOID **)&handlers
        );
        efi_status = uefi_call_wrapper(
            BS->LocateHandle,
            5,
            ByProtocol,
            &serial_io_protocol,
            0,
            &handlers_size,
            handlers
        );
    }
    if (handlers == NULL || EFI_ERROR(efi_status)) {
        FreePool(handlers);
        return efi_status;
    }

このコードではEFI SERIAL IO PROTOCOLを取得できるハンドラを探しています。

このプロトコルにはシリアルポートを選択するインターフェースが無いため、ポートの数だけ与えられたハンドラを切り替えて使って、シリアルポートの選択を行うようです。

そのため、LocateHandleを1度実行した後、ハンドラのサイズが足りない場合はAllocatePool(UEFI版のmallocのようなもの)でメモリを確保し、再度LocateHandleを実行するようになっています。

プロトコルの取得

先程取得したハンドラと目的のプロトコルのGUIDより、HandleProtocolを使ってプロトコルを取得します。

HandleProtocolの使い方は以下を参照。

EFI BOOT SERVICES - PhoenixWiki

今回作ったサンプルでは、以下の部分がハンドラからのプロトコル取得部分になります。

 efi_status = uefi_call_wrapper(
        BS->HandleProtocol,
        3,
        handlers[0],
        &serial_io_protocol,
        (VOID **)&serialio
    );

    if (EFI_ERROR(efi_status)) {
        Print(L"efi_status is not EFI_SUCCESS\n");
        return efi_status;
    }

インターフェースの利用

プロトコルのもつインターフェースの利用は、構造体のメンバにアロー演算子でアクセスして使うだけです。

今回作ったサンプルでは、以下の部分が該当します。

efi_status = uefi_call_wrapper(serialio->GetControl, 2, serialio, &control);

このコードではEFI SERIAL IO PROTOCOLのもつGetConrolインターフェースを呼び出し、シリアルポートのインターフェース情報を取得しています。

大体こんな感じでUEFIのハンドラ取得、プロトコル取得、インターフェース利用が行なえます。

質問と回答コーナー

LocateHandle使わずにLocateProtocolで直接Protocol取得してるひともいるけど、違いは何?

僕もよくわかってません。 ハンドル取得してからプロトコル取得が正しい道だとは思うのですが……

uefi_call_wrapperってなに? なぜ使う必要があるの? 引数のマジックナンバーの意味は何?

後で追記します……