個人用雑記

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

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

続・GUIのようなもの

マウスのようなものを作ってみる・・・はずが

晴れて任意の画素に色を付けることができるようになったので、今度は描画したドットをマウスで動かしてみます。なんとUEFIには最初からマウスの移動を取得できるEFI_SIMPLE_POINTER_PROTOCOLがあるのです。UEFIすげぇ・・・
しかし、このSIMPLE_POINTER_PROTOCOLは、自分の環境ではQEMUに対応してませんでした。設定を変えたりするのがめんどくさいという理由でUSBからブートしてこなかったのがここにきて響いてくるとは・・・とりあえず実装だけはして、本当に必要になったら試すことにしました。(SIMPLE_POINTER_PROTOCOLはマウスの移動・左クリック・右クリックに対応しているものの、スクロールには対応してないようです・・・?(試してないのでもしかしたら違うかもしれません。ごめんなさい。))

EFI_SIMPLE_POINTER_PROTOCOL

とりあえず、実装はしておきます。
このプロトコルも前のグラフィックスプロトコルの時と同様にGUIDを使ってLocateProtocolで先頭アドレスを取得します。このプロトコルのGUIDは以下です。

EFI::EFI_GUID SimplePointerProtocol_GUID = {
    0x31878c87, 0xb75, 0x11d5,
   {0x9a, 0x4f, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d}
};

EFI_SIMPLE_POINTER_PROTOCOLにはReset関数、GetState関数、WaitForInputが存在しています。基本的な使い方の流れは、最初にReset関数でマウスをリセットして置いたら、キー入力と同様にWaitForInputを引数に渡し、WaitForEvent関数を使ってマウスの入力を待機します。マウスの入力が起きたらGetState関数を呼び出すことでその入力を取得することができます。
QEMUではそもそもWaitForEvent関数が終了しなかったのでおそらくマウスの入力自体受け付けてもらえませんでした・・・ 本当であればGetState関数の引数に渡したEFI_SIMPLE_POINTER_STATE構造体の変数にマウスの移動値が格納され、その値をもとにマウスの座標を再計算し、描画という流れで動くはずだったんですけどね~
別の方法でマウスの入力を受け付けるか、何らかの方法でマウスの代替を作るしかなさそうです。

さて、何も進捗が無いように見えますがとりあえずこれでSIMPLE_POINTER_PROTOCOLの使い方は分かったので次に進めます。中途半端に前回が長くて今回が短い記事になってしまいましたが、仕方ないですね。

次回以降の目標

次回はファイルの読み書き周りのプロトコルです。それが終わるとひとまず参考にさせていただいた本の内容は踏襲できた形になるので、そこからが本当の意味で自作OS、始まります!になりそうですね。
その前にとりあえずロードマップのようなものを作ってみるのもアリかなと漠然と思ってます。必要そうだったら都度更新していって、記事ができたらリンクを貼っていくという感じの。という妄想は置いといて、とりあえず30日OSの方で基礎を勉強して来いという話ですね。

30日でできる!OS自作入門(Day 10)

メモリ管理の続き

確保するメモリのサイズを考える

Day 9で実装したセグメントによるメモリ管理では必要なバイト数ちょうどだけ確保して割り当てていました。しかし、これでは例えば数バイト程度のメモリ割り当てが頻繁に行われると、虫食いだらけでどんどん使えるメモリが減ってしまいます。
そこで、割り当てるメモリの最小単位を設定してしまえば、すくなくとも最小単位のサイズで空きができるため、一見すると無駄にメモリを割り当ててしまっているようにも見えますが、大局的にメモリの無駄がなくなります。

今回はそのサイズとして4KB単位、つまり0x1000バイト単位で管理することを考えます。必要なメモリのバイト数が4KB以下の場合は4KBを割り当てる。8KB以下なら8KBを・・・という風に必要量に対して最も近い4KBの倍数のメモリを割り当てることができればよさそうです。これには必要なメモリ量を4KB単位で切り上げてあげればよさそうです。
プログラミングを少しでもやったことがある人はきっとこの切り上げ方法についてすぐ思いつくと僕は思います。それは以下のようなコードです。

size = (size + 0xfff) & 0xfffff000;

右辺におけるsizeが必要なメモリ量を持っているとすると、その値に0xfff(=4095)を足してから下位のbitを強制的に0にしています。余談にはなりますが、このような計算は十進数での四捨五入などにも使われて、その場合は5を足してから切り捨てたりしますね。
話を戻すと、上記のおかげで見事4KBずつ割り当てることができるようになったので、あとは既存のセグメントで割り当てる際にメモリ量を変化させるだけです。

重ね合わせ処理

マウスの描画の続き

まだなおマウスの描画には問題が残っているのでした。
前に残っていた課題を確認すると、マウスが通った後の部分が再描画されていない問題のようです。

下敷きを考える

ここではマウスやウィンドウ、タスクバーなど全ての描画しているものはそれぞれ透明な下敷きに絵を描いて重ね合わせているようなイメージで進めるようです。下の方の下敷きに描画したウィンドウの上に別の下敷きに描画したウィンドウを載せて・・・を繰り返すということですね。当然、この中にはマウスも含まれます。
下敷きを扱うために、まず1枚の下敷きをそれぞれ構造体で表し、さらにそれらのたくさんの下敷き構造体を管理する母体を用意します。下敷き構造体には描画場所や高さ、幅などといった固有の情報を持たせ、管理にはそれらを配列で管理させればよさそうです。
下敷きにはきちんとそれぞれが何番目に描画されるかという情報が必要で、かつ選択や削除によってそれらを順番を守ったまま並べ替える必要があります。そこで、データそのものを持つ構造体と、それらの順番を保った状態で各構造体の番地に対するポインタを持った構造体の2種類を用意します。すると、中身はポインタの構造体から番地を辿れば見ることが可能で、また削除や移動はポインタの順番を全てずらすことで解決できそうです。結果的にデータそのものの構造体の順番はめちゃくちゃなままとなります。

ここで、自分で実装する場合どんなアルゴリズムを使うかをちょっと考えてみたのですが、ある程度大きな固定枚数で配列を作成しても、もしその枚数を超えたら怖いと考えるタイプなので、最大値は動的に増やしたい欲が出てきます。となると追加や順番の入れ替えが高速な双方向リストとかになるのでしょうか・・・まぁこの辺はおいおい再考しましょう。

順番さえ守られていれば、更新のたびに下の層から上の層に向けて描画することで重ね合わせを守ることができるようになります。

高速化

これだけでも重ね合わせ処理自体は完成しており、問題ないように見えますが、簡単な実装をした場合、更新する必要が出るたびに全ての画素を書き換えることになります。これがもしフルHDや4Kなんかの解像度だととんでもないことになりそうですね・・・
そこで、いろいろな高速化が提案されますが、最終的な部分だけ見てしまえば、次のような感じでしょうか。
1. 更新する範囲だけを描画
2. 重なっている部分だけを描画
一つ目に関しては移動前と移動後の座標を元にfor文を回して、あとは高さの低い下敷きから描画すれば達成できそうです。二つ目に関しては下敷きの中である一部だけ更新範囲に入った場合です。どの部分が更新されたかを丁寧に場合分けすれば問題なく達成できそうです。
これらの高速化のおかげでずいぶん快適なマウス操作を達成することができます。先人の努力はすごいですね・・・

Day 11へ

今回は結構学ぶ内容が多くて記事も長くなりましたね。Day 11はいつの間にかウィンドウが表示されていて、すごくOSっぽい見た目をしています。やっぱりGUIは直感的にOSっぽい!となれるのでいいですね。

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

GUIっぽいものを作る準備

文字出力→キー入力→矩形描画(=任意のピクセルに色を付ける)という流れを見るとUEFIすごい・・・ってなりますね。
今回使用するプロトコルEFI_GRAPHICS_OUTPUT_PROTOCOLです。ちなみに自分のミスが原因で3日間修正まで時間がかかりました。。。

EFI_GRAPHICS_OUTPUT_PROTOCOL

その名の通りグラフィックを出力するための関数を提供するプロトコルです。しかし、これまでの文字出力やキー入力とは違ってSystemTableのメンバから直接関数を呼び出すことはできないようです。
UEFIプロトコルは、SystemTableのメンバから呼び出せるほうが少なく、基本的にはSystemTable->BootServices内のLocateProtocol関数を使って呼び出したいプロトコルの先頭アドレスを取得することで関数を呼び出すようです。LocateProtocol関数ではGUIDと呼ばれるプロトコルを区別するための一位の値を使って取得したいプロトコルを取得するようです。今回のEFI_GRAPHICS_OUTPUT_PROTOCOLのGUIDは以下のような感じです。

EFI::EFI_GUID GraphicsOutputProtocol_GUID = {
    0x9042a9de, 0x23dc, 0x4a38,
   {0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a}
};

このGUIDと取得したプロトコルの先頭アドレスを格納するための変数を用いて以下のようにLocateProtocol関数を使うとめでたくプロトコルの先頭アドレスを取得できます。(見やすさのために改行してます)

    SystemTable->BootServices->LocateProtocol(
        &GraphicsOutputProtocol_GUID, 
         nullptr, 
         reinterpret_cast<void **>(&this->GraphicsOutputProtocol)
    );

一つ目の引数に取得したいプロトコルのGUID、二つ目の引数にはOPTIONALでRegistration(というものを取得できるようなのですが、NULLにすれば無視されるとのことなのでnullptrにしています。)、三つ目の引数に格納先をそれぞれ引数として渡します。自分の場合はEFIクラスのメンバに*SystemTableや*GraphicsOutputProtocolを置いているのでthis->で格納しています。

描画してみる

上記のようにプロトコルの先頭アドレスを取得できると、その内部に描画先であるFrameBufferの先頭アドレスを持った変数FrameBufferBaseがあります。UEFIでは画面の左上を(0,0)座標として水平方向にx、垂直方向にyの値を使用するため、ある(x1, y1)という座標に対して描画をしたい場合、このFrameBufferBaseの値に対して(画面の横幅*y1+x1)の値を足せばいいということがわかります。画面の横幅についても取得したプロトコルの内部に変数が存在するため、その値を取得すれば問題ありません。
本の通りに(10, 10)という座標から縦200横100の長方形を描画させてみましょう。描画自体は単純にその方向にループを回しながらこの座標への描画を繰り返すだけです。 f:id:rei_624:20200219224130p:plain
だけでは、なかったのですか・・・?
冒頭の3日かかったミスがこの画面です。

以前一番最初にUEFIアプリケーションを実行しようとした時も似たエラー画面に遭遇しました。その時はC++のマングルによってエントリポイントとして指定した関数名と実態が異なっていたことが原因でしたが今回はそうではありません。
原因は以下のコードに含まれていました。

    typedef struct _EFI_GRAPHICS_OUTPUT_PROTOCOL
    {
        EFI_STATUS (EFIAPI QueryMode)(
            IN _EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
            IN UINT32 ModeNumber,
            OUT UINTN *SizeOfInfo,
            OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info);
        EFI_STATUS (EFIAPI SetMode)(
            IN _EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
            IN UINT32 ModeNumber);
        EFI_STATUS (EFIAPI Blt)(
            IN _EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
            IN OUT EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer,
            IN EFI_GRAPHICS_OUTPUT_BLT_OPERATION BitOperation,
            IN UINTN SourceX,
            IN UINTN SourceY,
            IN UINTN DestinationX,
            IN UINTN DestinationY,
            IN UINTN Width,
            IN UINTN Height,
            IN UINTN Delta);
        EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
    } EFI_GRAPHICS_OUTPUT_PROTOCOL;

一見すると何も問題がないように見えます。仕様書と照らし合わせても変数名、構造体名、一言一句同じはずです。でもImageBase(本来あるべきアドレス)とEntryPoint(実際のアドレス)が違うと怒られています。
アドレス値を比較してみるとEntryPointが大きくなっています。つまり、本来の仕様書通りのアドレスよりも余分なものが入っていることがわかります。引き延ばしても意味がないので、結論を書いてしまいますが、上記のコードは関数"ポインタ"になっていなかったのです。
正しくコードを直すと以下のようになります。

    typedef struct _EFI_GRAPHICS_OUTPUT_PROTOCOL
    {
        EFI_STATUS (EFIAPI *QueryMode)(
            IN _EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
            IN UINT32 ModeNumber,
            OUT UINTN *SizeOfInfo,
            OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info);
        EFI_STATUS (EFIAPI *SetMode)(
            IN _EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
            IN UINT32 ModeNumber);
        EFI_STATUS (EFIAPI *Blt)(
            IN _EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
            IN OUT EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer,
            IN EFI_GRAPHICS_OUTPUT_BLT_OPERATION BitOperation,
            IN UINTN SourceX,
            IN UINTN SourceY,
            IN UINTN DestinationX,
            IN UINTN DestinationY,
            IN UINTN Width,
            IN UINTN Height,
            IN UINTN Delta);
        EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
    } EFI_GRAPHICS_OUTPUT_PROTOCOL;

QueryModeやSetMode、Bltという部分に*がついただけです。ついただけですが、意味が大きく変わってきます。ポインタであることによって占めるアドレスのサイズが違うのですから。ポインタにしてないのですから当然本来あるべきアドレスよりも実際のアドレスが大きくなってしまうかもしれません。(そして、実際そうだったわけです。)

ということで、気を取り直して再度実行した結果が以下です。 f:id:rei_624:20200219224523j:plain
これまでとは急に違う見た目の画面になりましたが、それでも矩形は表示できていますね、やったね。

WSLの罠?

急に画面が変わったのはこれがWSL2上のLinuxではなくWindowsQEMUで起動しているからです。そしてWSL2+Linuxでは直した後も矩形は出ませんでした。これはおそらくQEMUのオプションに-nographicをつけているからっぽいのですが、そもそもWSL2にグラフィックできるものが存在しないため、このオプションを外してしまうと実行自体ができなくなってしまいます。確かにWSL2+LinuxCUIベースですし、別ウィンドウで開くことはできないのも納得できなくはないですが、抜け道のようなものもあるんでしょうかね?
どうやらここでWSL2とはお別れのようです・・・今後はWindowsで実行しなければならないようです。まぁコンパイルはWSL2側で行うんですけど
もしWSL2のQEMUでもグラフィックを描画できる方法をご存じの方はぜひ教えてください。お願いします。

次回以降の目標

長くなってしまったので、いったんここで記事を切ります。次回以降はGUIっぽいものとして"マウスのようななにか"を実装するところですね。晴れてバグもなくなったので清々しい気持ちで取り掛かりたいですね。

30日でできる!OS自作入門(Day 9)

メモリ管理

そもそも、セグメント方式

最近作成を始めたUEFIの自作OSの方は、今後メモリ管理を実装する際はページングにしようと思ってるので(ページングを習った時めちゃめちゃ感動したので)セグメントとは結構違う部分も出てきそうなのがネックですね・・・
もちろん知識として知っておくに越したことはないので、今日もやっていきます。

メモリ容量のチェック

おおまかなメモリ容量のチェック方法は、キャッシュの無効化→一定範囲のメモリに対して適当な値を書き込み→同じ箇所を今度は読み込み→値が同じままならメモリ容量がある、違っていたらないという判断を行う。という流れです。
本ではキャッシュがあるかどうかを先に調べてから無効にするかどうかを行っていますが、今時のCPUなら100%キャッシュがあるとしていいでしょうね。
次のメモリチェック本体については単純にforループで書き込む先を変えながら、データの退避→書き込み→データの反転→反転できているかどうかのチェック→再度反転→元の値と同じかのチェック→データを戻すといった具合で、非常にシンプルでした。しかし、一見無駄に見えるこれらの動作のせいで、コンパイラの最適化が入って何も行わなくなってしまう可能性があるようです。当然、最適化を切ってしまえば解決する問題ですが、メモリチェックのためだけに最適化を切るのは無駄が多いので今回はアセンブラで書いたようです。
試してないですが、今ならvolatile変数とかにすれば問題はなさそうですね。

メモリ管理

ここは、うん。セグメントしてますね。。。

それぞれの番地に対して空いてるor空いてないを割り当ててしまうと管理テーブルだけでメモリを大きく使ってしまうことになるため、ある番地から◯バイト空いているという情報を持たせるようにします。すると確保する時も必要量よりも空いているバイトが大きい物を探して、確保する分だけ番地をずらし、空き容量を減らすだけでいいことになります。非常にシンプルな実装ができそうですね。(当然、それがセグメントのメリットでもあるのですが)

方式はなんであれ、メモリの総容量を管理して、それぞれに必要な量のメモリを割り当てるという動作はいかにもOSらしくて”自作している”という感が強く得られますね。

Day 10へ

記事としてはサクッと終わらせてしまいましたが、簡単なもので良ければメモリ管理はこう実装できるということが学べてよかった気がします。 Day 10はメモリ管理の続きを少しと、描画周りの修正ですね。

自作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なんかの)環境の調整が必要かなと使いながら感じています。それが終わり次第本の続きに行きたいですね。次はもう画面に絵を描くそうです。

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

はじめに

30日OSのブログでUEFIの勉強も並列して行っていると書いたので、そちらも進捗が出次第ブログにしていきたいなと思い、書き始めました。 ソースコードGitHub上にあげておきますが、自環境での動作のみ保証しているため、正常に動かない場合があります。

参考資料

※その他たくさんの既存の実装を参考にさせていただいています。

まずはUEFIをつかって文字を出すところから

30日OSの本でもそうでしたが、まずはBIOSを使って画面に文字を表示するところからです。今回はレガシーBIOSではなくUEFI BIOSを使って文字を表示していきます。

UEFIについて

そもそも、UEFIとはUnified Extensible Firmware Interfaceの略でその名の通り統一された拡張性のあるファームウェアのインターフェース(直訳)を提供するものです。最近のPCであれば多くはこのUEFIを内蔵しているかと思います。(今年中にレガシーBIOSをなくすという発表もあるため)
今回はQEMUを使って作っていくため、PC事態にUEFIが内蔵されていなくても問題はありませんが・・・

環境構築

あいにくと自作OSでは使いやすそうなLinuxmacも所持していないため、wsl2+Ubuntuで環境を作りました。(wsl2にするのに1日消費しました、やっぱりWindowsは・・・)
wsl2上で起動したUbuntuに、以下のコマンドを用いてQEMUQEMU上でUEFIを利用するためのファームウェアをインストールします。 (実はダウンロードしたbios64.binファイルを内蔵しているため、ovmfのインストールは不必要です)

$ sudo apt install qemu-system-x86
$ sudo apt install ovmf

また、UEFIを用いたアプリケーションはクロスコンパイルで実行形式をPE32+にする必要があるため、mingwもインストールします。今回は少し頑張ってC++で実装していきたいため、g++をインストールしていますが、C言語を用いる場合はg++をgccに置き換えてください。

$ sudo apt install g++-mingw-w64-x86-64

上記をインストールしたら環境構築は終了です。

UEFIで文字を出すには?

さきほどUEFIはインターフェースであると言った通り、UEFIを使いたい場合はそのインターフェースを適切に利用する実装を行う必要があります。
それぞれの関数の実装方法についてはUEFIの仕様書から確認ができるため、これを見ながら実装を進めていくことになります。
今回のように画面に文字を出力するだけであればEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLを用いればよさそうです。

UEFIはそれぞれの関数をプロトコルという単位で分けており、それぞれに構造体が用意されています。そしてこの構造体の中にそれぞれの関数の関数ポインタが格納されており、関数を使いたい場合は構造体→関数ポインタから関数を呼び出しという手順で使うことになります。

また、UEFIでは通常通りのmain関数ではなく適切な引数を持った関数をエントリポイントとして指定する必要があります。

UEFIに従って実装

さて、C++EFI_SIMPLE_TEXT_OUTPUT_PROTOCOLの一部分を実装してみると以下のようになるでしょうか。

#define IN
#define EFIAPI

typedef unsigned short CHAR16;
typedef unsigned long long EFI_STATUS;
typedef void *EFI_HANDLE;

struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

typedef EFI_STATUS (EFIAPI *EFI_TEXT_STRING)(
    IN struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
    IN CHAR16 *String
);

typedef EFI_STATUS (EFIAPI *EFI_TEXT_CLEAR_SCREEN)(
    IN struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This
);

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
{
    unsigned long long buf;
    EFI_TEXT_STRING OutputString;
    unsigned long long buf2[4];
    EFI_TEXT_CLEAR_SCREEN ClearScreen;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

typedef struct
{
    char buf[52];
    EFI_HANDLE ConsoleOutHandle;
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
} EFI_SYSTEM_TABLE;

extern "C"{
    EFI_STATUS EFIAPI efi_main(
        IN EFI_HANDLE ImageHandle __attribute__ ((unused)),
        IN EFI_SYSTEM_TABLE *SystemTable
        )
    {
        SystemTable->ConOut->ClearScreen(SystemTable->ConOut);
        SystemTable->ConOut->OutputString(SystemTable->ConOut, (CHAR16 *)L"Hello KizunaOS! in C++\n");
        while (1);
        return 0;
    }
}

エントリポイントとして指定するefi_main関数をextern"C"で囲っているのがC++で実行する上でのミソです。

今回のようにmain関数から始まらないようにするためには、エントリポイントとして-eオプションをつけて開始の関数を指定してからコンパイルするわけなんですが、C++では(オーバーロードによって)複数の同じ名前の関数を定義出来てしまうため、マングルと呼ばれる関数を引数などをもとに一意の名前の関数に変更する動作がコンパイル時に入るようです。そのため、通常通り-eオプションでefi_mainを指定してもコンパイル中に別の名前になってしまうため正常にエントリポイントを定義することができません。そこで、このefi_main関数はC言語の関数であると認識させるためにextern"C"で囲うと、このマングルが行われず-eオプションでefi_mainと指定できるようになるというわけです。

実行

ソースコードもできたのでいよいよ実行です。
QEMUでは指定したフォルダをハードディスクと見なしてブートする機能があるため、これを利用します。UEFIEFI/BOOTフォルダ内のEFIファイルをもとに実行するようなので、例えばApp/EFI/BOOT/BOOTX64.EFIという階層で作成した場合はQEMUのオプションにfat:Appを追加します。
また、今回wsl2でQEMUを使う場合、-nographicオプションと-cursesオプションをつけないと正常に動きませんでした。あ、コンパイル時のオプションやQEMU実行時のオプションはソースコード内のMakefileを参照してください。(全然きれいではないのですが)

ということでQEMUで実行した結果が以下の画像です。

f:id:rei_624:20200213013853p:plain
Hello KizunaOS!
どうやら正常に文字列が表示できたようで何よりです。 QEMUの終了はCtrl+A→xです。 ちなみになんでKizunaOSという名前にしたのかは、僕がKizunaAIちゃんが好きだからです。
そのうちぴょこぴょこが画面上を動いてたりそういった実装ができたらなという妄想だけはあります。

次回以降の目標

当面はフルスクラッチで作る!UEFIベアメタルプログラミングの内容に沿って理解を深めていく予定です。
今はめちゃめちゃモチベが高いのでこういう時に一気に進められるといいですね。

30日でできる!OS自作入門(Day 8)

はじめに

f:id:rei_624:20200211231603p:plain
何ですか、これは。
1日進めるために287日かかりました。この間何をしてたかといえば、講義でOSとかアーキテクチャとかいろんなことを学んでいました。満を持してのOS自作本というわけです。ちなみにOSの講義でめっちゃ好きになったのがOS自作では取り扱わないページングとCopy-On-Writeでした。

ところで、最近のOSの流行りはUEFIらしいですよ?

マウスを動かしたい

昨日(287日前)マウスからの情報を割り込み処理で受け取ることができたので、その割り込みで得られた情報を解読するところからです。マウスの割り込みでは一回の動作ごとに3バイトずつデータが送られてくるそうなので、それぞれが何を表しているかを知る必要があるというわけです。ちなみに、大雑把に言えばマウスのボタンクリックの有無、左右の動き、上下の動きで3バイトだそうです。

マウス、動く

マウスから送られてくるデータが解読できさえすれば後は描画するだけって感じですね。

・・・だけって感じではなかったですね。今のままだとマウスが通った後の部分が再描画されないので、マウスがゴジラみたいになってますね。
解決するのはDay 10です、マウスって難しいですね。

32ビットモード

実は裏で行われてて解説のなかったOSブート時の16ビットモードから32ビットモードへの移行の解説がここで来ました。ちなみに、UEFIなら最初から64ビットで動かせますね。
32ビットモードへの移行で必須な設定は、移行終了まで割り込みの禁止とメモリを1MB以上使えるようにする設定、使用する命令の拡張とページングを使わないプロテクトモードへの切り替え、と盛りだくさんです。(さようならページング)

Day 9へ

内容を見返したんですけど、恐ろしくペラッペラでした。というのも、並行してUEFIの方も勉強し始めたのでこの辺でやっていることは一通り何をしているかだけ把握しておいて、後々必要になった場合は再度確認しようと思ったわけです。
ちゃんとUEFIの方もまとめられたらまとめたいですね。
ちなみにDay 9はメモリ管理についてなんですけど、セグメント方式のメモリ管理を学ぶ必要は、あるんですかね・・・(もちろん実装は楽なんですけど)