個人用雑記

勉強したことを書いていければなーと

自作OS、始まります! #2

文字表示の次はいきなりキー入力

30日OSの方では文字を表示させた後、とにかく最初のBIOSが使える状態でしなければいけないことをしていく、という流れでした。しかし、UEFIを使う場合既に32bitないし64bitで動いているため32bitモードへ移行する必要もなく、また簡単なキーの入力受付であれば割り込み処理用のGDT/IDT初期化も必要が無いようです。(当然きちんとしたOSにするためには割り込み処理を実装する必要はあるとは思いますが・・・)

エコーバックプログラム

キー入力受付準備

UEFIアプリケーションで文字を表示するにはEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLを使うのでした。同様に、キー入力を取得するために使う関数も定義されています。それがEFI_SIMPLE_TEXT_INPUT_PROTOCOLです。
このEFI_SIMPLE_TEXT_INPUT_PROTOCOL内のReadKeyStroke関数を使用すると簡単にキー入力を取得することができます。ただし、この関数だけを使う場合、関数実行時にキー入力があるまで待機してくれるわけではなくキー入力があったかどうかで返すエラーが異なるだけのようです。(ポーリングのような実装が必要になるということですね)
そのため、キー入力があるまで待機するためのWaitForKeyという関数がUEFIには存在します。この関数は単体で使用するのではなく、指定したイベントが発生するまで待つための関数であるWaitForEvent関数にこの関数を渡すことでキー入力を待機します。今回はこのWaitForEvent関数とWaitForKey関数を実装したうえでReadKeyStroke関数を使用しました。

キー入力受付

ReadKeyStroke関数でキー入力を正常に受け取った際、その入力は引数に指定した構造体に格納されます。この構造体には二つのメンバが存在し、Unicode範囲内の入力を格納する部分とUnicode範囲外の入力を格納する部分に分かれており、基本的にはキー入力を行った場合はどちらかに0がどちらかに入力内容が格納されることになります。 そのため、何らかのキー入力を受け付けたい場合はどちらかから値を取り出す形になります。 ただしQEMU上でUEFIアプリケーションを実行する場合は日本語は完全に非対応のようです・・・

画面にエコーバックする

上記まででキー入力受付の方法はだいたいわかったため、前回の文字表示と組み合わせてキー入力受付→その内容を文字として表示するUEFIアプリケーションを作成してみます。 とはいえUEFIの仕様書を読みながら必要な構造体を実装しておき、それを呼び出すだけではあるのであまり前回の内容と変わらないですが・・・ そのため、前回の全てをエントリポイントとして指定したefi_main関数に書いている状態から一気にそれぞれの役割毎にソースファイルを分割しました。まず、キー入力と文字出力用の標準関数として以下のようにgetcとputc関数を実装しました。ただしこれらの関数では一文字ずつでしか対応できないので、getsやputs関数も実装したいですね・・・

EFI::CHAR16 getc(EFI::EFI_SYSTEM_TABLE *SystemTable)
{
    EFI::EFI_INPUT_KEY key;
    unsigned long long waitidx;
    SystemTable->BootServices->WaitForEvent(1, &(SystemTable->ConIn->WaitForKey), &waitidx);
    SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &key);
    return key.UnicodeChar;
}

void putc(EFI::EFI_SYSTEM_TABLE *SystemTable, EFI::CHAR16 chara)
{
    unsigned short str[3];
    if (chara != L'\r')
    {
        str[0] = chara;
        str[1] = L'\0';
    }
    else
    {
        str[0] = L'\r';
        str[1] = L'\n';
        str[2] = L'\0';
    }
    SystemTable->ConOut->OutputString(SystemTable->ConOut, str);
}

putc関数は改行文字が来たらCR文字の'\r'を付加しています。これは通常改行しただけではカーソル位置が戻らないためです。(まあWindowsは改行文字が\r\nなんですけどね) そして上記の関数を使って以下のようにエコーバックする関数を実装してみます。

EFI::EFI_STATUS EFIAPI efi_boot_loader(
    IN EFI::EFI_HANDLE ImageHandle  __attribute__ ((unused)),
    IN EFI::EFI_SYSTEM_TABLE *SystemTable)
{
    SystemTable->ConOut->ClearScreen(SystemTable->ConOut);
    SystemTable->ConOut->OutputString(SystemTable->ConOut, (EFI::CHAR16 *)L"KizunaOS boot up!\r\n");
    while(1){
        EFI::CHAR16 c = getc(SystemTable);
        putc(SystemTable, c);
    }
    return 0;
}

後々この関数が(きっと)ブートローダとしての役割を持つはずなので、この関数名です。前回のefi_main関数はextern"C"で記述しているため、実行してすぐにこのefi_boot_loader関数を呼び出すようにしました。

make runで実行してみます。(前回から大幅にMakefileが変わっており、ますます自分の環境でのみ動くようになってしまいました(主にlld-linkのバージョン名とファイル場所)。いつか直したいんですけどね・・・)ちなみに今回からg++ではなくclang++になりました。この辺の話は次回以降どこかでしたいです。

さて、実行中の画像は以下のような感じです。 f:id:rei_624:20200215004951p:plain 前回と違って色々入力した文字が画面に映ってます。(ちなみにまだ文字を消すことはできないのでtypoしたらそのままです。また、一文字ずつ処理しているので適当にキーボードを入力するとかなりラグがあり、ここも改善の余地です)

ともあれこれでキー入力を受け付けて、画面に表示することには成功しました。それにしてもUEFIを使うと楽ですね。

次回以降の目標

一気に色々変えようとしてしまったので、もうしばらくは裏側の(Makefileなんかの)環境の調整が必要かなと使いながら感じています。それが終わり次第本の続きに行きたいですね。次はもう画面に絵を描くそうです。