自作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の長方形を描画させてみましょう。描画自体は単純にその方向にループを回しながらこの座標への描画を繰り返すだけです。
だけでは、なかったのですか・・・?
冒頭の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という部分に*がついただけです。ついただけですが、意味が大きく変わってきます。ポインタであることによって占めるアドレスのサイズが違うのですから。ポインタにしてないのですから当然本来あるべきアドレスよりも実際のアドレスが大きくなってしまうかもしれません。(そして、実際そうだったわけです。)
ということで、気を取り直して再度実行した結果が以下です。
これまでとは急に違う見た目の画面になりましたが、それでも矩形は表示できていますね、やったね。
WSLの罠?
急に画面が変わったのはこれがWSL2上のLinuxではなくWindowsのQEMUで起動しているからです。そしてWSL2+Linuxでは直した後も矩形は出ませんでした。これはおそらくQEMUのオプションに-nographicをつけているからっぽいのですが、そもそもWSL2にグラフィックできるものが存在しないため、このオプションを外してしまうと実行自体ができなくなってしまいます。確かにWSL2+LinuxはCUIベースですし、別ウィンドウで開くことはできないのも納得できなくはないですが、抜け道のようなものもあるんでしょうかね?
どうやらここでWSL2とはお別れのようです・・・今後はWindowsで実行しなければならないようです。まぁコンパイルはWSL2側で行うんですけど
もしWSL2のQEMUでもグラフィックを描画できる方法をご存じの方はぜひ教えてください。お願いします。
次回以降の目標
長くなってしまったので、いったんここで記事を切ります。次回以降はGUIっぽいものとして"マウスのようななにか"を実装するところですね。晴れてバグもなくなったので清々しい気持ちで取り掛かりたいですね。