第1章では、UEFI仕様書から機能を調べ、UEFIアプリケーションの作成・実行までの流れを説明しました。この章では、キー入力の取得方法を紹介し、簡単なシェルもどきを作ってみます。
キー入力を取得する関数は"EFI_SIMPLE_TEXT_INPUT_PROTOCOL"の中にあります。EFI_SIMPLE_TEXT_INPUT_PROTOCOLも、前の章でテキスト出力のために使用したEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLと同じく、SystemTableのメンバです(図2.1)。
EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義はリスト2.1の通りです。リスト2.1では使用する関数のみ定義しています。EFI_SIMPLE_TEXT_INPUT_PROTOCOLの全体は、仕様書の"11.3 Simple Text Input Protocol(P.420)"を参照してください。
キー入力は、EFI_SIMPLE_TEXT_INPUT_PROTOCOLの"ReadKeyStroke"関数で取得できます(仕様書"11.3 Simple Text Input Protocol(P.423)"参照)。ReadKeyStroke関数はノンブロッキングの関数で、関数実行時にキー入力が無ければエラーのステータスを返します。
引数の意味は以下の通りです(第1引数は省略)。
なお、EFI_INPUT_KEY構造体の定義はリスト2.2の通りです。
また、リスト2.2のメンバの意味は以下の通りです。
ReadKeyStroke関数を使用して、取得した文字を画面へ出力する「エコーバック」のサンプルをリスト2.3に示します。サンプルのディレクトリは"sample2_1_echoback"です。
1: struct EFI_INPUT_KEY { 2: unsigned short ScanCode; 3: unsigned short UnicodeChar; 4: }; 5: 6: struct EFI_SYSTEM_TABLE { 7: char _buf1[44]; 8: struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL { 9: unsigned long long _buf; 10: unsigned long long (*ReadKeyStroke)( 11: struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, 12: struct EFI_INPUT_KEY *Key); 13: } *ConIn; 14: unsigned long long _buf2; 15: struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { 16: unsigned long long _buf; 17: unsigned long long (*OutputString)( 18: struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, 19: unsigned short *String); 20: unsigned long long _buf2[4]; 21: unsigned long long (*ClearScreen)( 22: struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This); 23: } *ConOut; 24: }; 25: 26: void efi_main(void *ImageHandle __attribute__ ((unused)), 27: struct EFI_SYSTEM_TABLE *SystemTable) 28: { 29: struct EFI_INPUT_KEY key; 30: unsigned short str[3]; 31: SystemTable->ConOut->ClearScreen(SystemTable->ConOut); 32: while (1) { 33: if (!SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, 34: &key)) { 35: if (key.UnicodeChar != L'\r') { 36: str[0] = key.UnicodeChar; 37: str[1] = L'\0'; 38: } else { 39: str[0] = L'\r'; 40: str[1] = L'\n'; 41: str[2] = L'\0'; 42: } 43: SystemTable->ConOut->OutputString(SystemTable->ConOut, 44: str); 45: } 46: } 47: }
リスト2.3では、EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義をEFI_SYSTEM_TABLEに追加しています。
efi_main関数内について、ClearScreen後のwhileの無限ループがエコーバックの処理です。ReadKeyStrokeでキー入力を取得できたら、str配列へヌル文字(L'\0')を付加した文字列として格納し、OutputStringで画面へ表示します。なお、Enterキーの入力時はCR('\r')を取得するため、取得した文字がCRのときはLF('\n')も出力するようにしています。
サンプルを実行すると、入力した文字がそのまま表示されるエコーバック動作を確認できます(図2.2)。
リスト2.3では、while()内でReadKeyStroke関数が成功するまで、ReadKeyStroke関数を何度も呼び出しています。しかし、キー入力が得られるまでCPUを休ませてあげた方がCPUに優しいです。
そのためにEFI_SIMPLE_TEXT_INPUT_PROTOCOLには"WaitForKey"というメンバ変数があります(リスト2.4、仕様書"11.3 Simple Text Input Protocol(P.421)")。
"void *"は、UEFIでイベントを指す"EFI_EVENT"型の実体で、仕様書上はEFI_EVENT WaitForKey
と定義されています。仕様書P.421のWaitForKeyの説明にも記載の通り、WaitForKeyはWaitForEvent関数で使用できます。
WaitForEventは指定したイベントの発生を待つ関数です。SystemTable->BootServices内で定義されています。BootServicesはEFI_BOOT_SERVICESという構造体で、主にブートローダー向けにUEFIが提供している関数(サービス)を持ちます(詳細は次の章で説明します)。WaitForEventの定義はリスト2.5の通りです。
引数の意味は以下の通りです。
WaitForKeyとWaitForEventを使用して、リスト2.6の様にキー入力を待つことができます。
1: struct EFI_INPUT_KEY key; 2: unsigned long long waitidx; 3: 4: /* キー入力取得まで待機 */ 5: SystemTable->BootServices->WaitForEvent(1, 6: &(SystemTable->ConIn->WaitForKey), &waitidx); 7: 8: /* キー入力取得 */ 9: SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &key);
ここまでで、コンソール画面上での文字の入出力ができるようになりました。OSっぽいものを作る上で、まずはシェルっぽいものを作ってみます。サンプルのディレクトリは"sample2_2_shell"です。
このサンプルでは、これからOSっぽいものを作っていく上で土台となるソースコード構成を用意します。ここでは、関数化を適宜行い、ソースコードを以下の様に分けます。
エントリポイントの引数であるSystemTableは、何をするにしても必要になるため、グローバル変数へ格納しておくことにします。その処理を行うのがefi.cの初期化処理(efi_init関数)です(リスト2.7)*1。efi_initではSetWatchdogTimerという関数を呼び出していますが、これについては後述のコラムで説明します。
[*1] EDK2やgnu-efiといった開発環境やツールチェインでも同様に、SystemTable等をグローバル変数へ格納する枠組みになっています。
1: #include "efi.h" 2: #include "common.h" 3: 4: struct EFI_SYSTEM_TABLE *ST; 5: 6: void efi_init(struct EFI_SYSTEM_TABLE *SystemTable) 7: { 8: ST = SystemTable; 9: ST->BootServices->SetWatchdogTimer(0, 0, 0, NULL); 10: }
そして、シェルのソースコードはリスト2.8、エントリポイントのソースコードはリスト2.9の通りです。
1: #include "common.h" 2: #include "shell.h" 3: 4: #define MAX_COMMAND_LEN 100 5: 6: void shell(void) 7: { 8: unsigned short com[MAX_COMMAND_LEN]; 9: 10: while (TRUE) { 11: puts(L"poiOS> "); 12: if (gets(com, MAX_COMMAND_LEN) <= 0) 13: continue; 14: 15: if (!strcmp(L"hello", com)) 16: puts(L"Hello UEFI!\r\n"); 17: else 18: puts(L"Command not found.\r\n"); 19: } 20: }
1: #include "efi.h" 2: #include "shell.h" 3: 4: void efi_main(void *ImageHandle __attribute__ ((unused)), 5: struct EFI_SYSTEM_TABLE *SystemTable) 6: { 7: SystemTable->ConOut->ClearScreen(SystemTable->ConOut); 8: efi_init(SystemTable); 9: 10: shell(); 11: }
リスト2.8では、"OSっぽいもの"ということで、プロンプトに"poiOS"と付けてみました*2。
[*2] "mockOS"とか"OSmodoki"とかも考えていたのですが、Google検索してみると、どちらも既に世の中に存在するようです。
リスト2.8で登場した各種の定数や、関数puts・gets・strcmpは、common.hとcommon.cで定義しています。これまで説明したUEFIの機能の呼び出し方を関数化しただけなので、紙面上では特に紹介しません(独特な実装方法をしているわけでもないので)。気になる方は、GitHubのサンプルコードをダウンロードして見てみてください。
また、リスト2.9では、efi_init関数でUEFIの初期化処理を行い、shell関数を実行することでシェルを起動しています。以降、main.cは書き換えません。
そして、サンプル実行の様子は図2.3の通りです。
実はUEFIアプリケーション起動時、ウォッチドッグタイマがセットされています。その時間は5分ですので、何もしないでいると、UEFIアプリケーションが起動してから5分後に再起動することになります。ウォッチドッグタイマはSystemTable->BootServices->SetWatchdogTimer関数で解除できます。
SetWatchdogTimer関数の定義はリスト2.10の通りです(仕様書"6.5 Miscellaneous Boot Services(P.201)")。
また、引数の意味は以下の通りです。
ウォッチドッグタイマー無効化のコード例はリスト2.11の通りです。