第1章では、UEFI仕様書から機能を調べ、UEFIアプリケーションの作成・実行までの流れを説明しました。この章では、キー入力の取得方法を紹介し、簡単なシェルもどきを作ってみます。
キー入力を取得する関数は"EFI_SIMPLE_TEXT_INPUT_PROTOCOL"の中にあります。EFI_SIMPLE_TEXT_INPUT_PROTOCOLも、前の章でテキスト出力のために使用したEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLと同じく、SystemTableのメンバです(図2.1)。
図2.1: EFI_SYSTEM_TABLEの定義(一部)(再掲)
EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義はリスト2.1の通りです。リスト2.1では使用する関数のみ定義しています。EFI_SIMPLE_TEXT_INPUT_PROTOCOLの全体は、仕様書の"11.3 Simple Text Input Protocol(P.420)"を参照してください。
リスト2.1: EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義
struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL {
unsigned long long _buf;
unsigned long long (*ReadKeyStroke)(
struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
struct EFI_INPUT_KEY *Key);
};
キー入力は、EFI_SIMPLE_TEXT_INPUT_PROTOCOLの"ReadKeyStroke"関数で取得できます(仕様書"11.3 Simple Text Input Protocol(P.423)"参照)。ReadKeyStroke関数はノンブロッキングの関数で、関数実行時にキー入力が無ければエラーのステータスを返します。
引数の意味は以下の通りです(第1引数は省略)。
なお、EFI_INPUT_KEY構造体の定義はリスト2.2の通りです。
リスト2.2: EFI_INPUT_KEYの定義
struct EFI_INPUT_KEY {
unsigned short ScanCode;
unsigned short UnicodeChar;
};
また、リスト2.2のメンバの意味は以下の通りです。
ReadKeyStroke関数を使用して、取得した文字を画面へ出力する「エコーバック」のサンプルをリスト2.3に示します。サンプルのディレクトリは"sample2_1_echoback"です。
リスト2.3: sample2_1_echoback/main.c
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.2: エコーバックサンプル実行の様子
リスト2.3では、while()内でReadKeyStroke関数が成功するまで、ReadKeyStroke関数を何度も呼び出しています。しかし、キー入力が得られるまでCPUを休ませてあげた方がCPUに優しいです。
そのためにEFI_SIMPLE_TEXT_INPUT_PROTOCOLには"WaitForKey"というメンバ変数があります(リスト2.4、仕様書"11.3 Simple Text Input Protocol(P.421)")。
リスト2.4: WaitForKeyの定義
struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL {
unsigned long long _buf;
unsigned long long (*ReadKeyStroke)(
struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
struct EFI_INPUT_KEY *Key);
void *WaitForKey;
};
"void *"は、UEFIでイベントを指す"EFI_EVENT"型の実体で、仕様書上はEFI_EVENT WaitForKeyと定義されています。仕様書P.421のWaitForKeyの説明にも記載の通り、WaitForKeyはWaitForEvent関数で使用できます。
WaitForEventは指定したイベントの発生を待つ関数です。SystemTable->BootServices内で定義されています。BootServicesはEFI_BOOT_SERVICESという構造体で、主にブートローダー向けにUEFIが提供している関数(サービス)を持ちます(詳細は次の章で説明します)。WaitForEventの定義はリスト2.5の通りです。
リスト2.5: WaitForEventの定義
unsigned long long (*WaitForEvent)(
unsigned long long NumberOfEvents,
void **Event,
unsigned long long *Index);
引数の意味は以下の通りです。
WaitForKeyとWaitForEventを使用して、リスト2.6の様にキー入力を待つことができます。
リスト2.6: WaitForKeyとWaitForEventを使用する例
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等をグローバル変数へ格納する枠組みになっています。
リスト2.7: sample2_2_shell/efi.c
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の通りです。
リスト2.8: sample2_2_shell/shell.c
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: }
リスト2.9: sample2_2_shell/main.c
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の通りです。
図2.3: シェルもどき実行の様子
実はUEFIアプリケーション起動時、ウォッチドッグタイマがセットされています。その時間は5分ですので、何もしないでいると、UEFIアプリケーションが起動してから5分後に再起動することになります。ウォッチドッグタイマはSystemTable->BootServices->SetWatchdogTimer関数で解除できます。
SetWatchdogTimer関数の定義はリスト2.10の通りです(仕様書"6.5 Miscellaneous Boot Services(P.201)")。
リスト2.10: SetWatchdogTimerの定義
unsigned long long (*SetWatchdogTimer)(
unsigned long long Timeout,
unsigned long long WatchdogCode,
unsigned long long DataSize,
unsigned short *WatchdogData);
また、引数の意味は以下の通りです。
ウォッチドッグタイマー無効化のコード例はリスト2.11の通りです。
リスト2.11: ウォッチドッグタイマ無効化
ST->BootServices->SetWatchdogTimer(0, 0, 0, NULL);