EFI_BOOT_SERVICES内のLoadImage()とStartImage()を使用することで、UEFIアプリケーションをロード・実行できます。ただし、そのためにはUEFIの「デバイスパス」という概念でパスを作成する必要があります。
この章ではデバイスパスを見てみる、作ってみるところから、順を追って説明し、UEFIアプリケーションをロード・実行する方法を紹介します。
なお実は、今のLinuxカーネルはUEFIアプリケーションとしてカーネルイメージを生成する機能があります。そこで、この章の最後では、Linuxカーネルの起動を行ってみます。
LoadImage()でUEFIアプリケーションの実行バイナリをロードするには、実行バイナリへのパスを「デバイスパス」というもので作成する必要があります。
実は、自分自身(EFI/BOOT/BOOTX64.EFI)のデバイスパスを取得する方法がありますので、この章では、既存のデバイスパスを改造して、起動したいUEFIアプリケーションのデバイスパスを作ることにします。
この節では、まず、自分自身のデバイスパスを画面へ表示し、どんなものか見てみます。
サンプルのディレクトリは"030_loaded_image_protocol_file_path"です。
ロード済みのイメージ(UEFIアプリケーション)の情報を取得するには、EFI_LOADED_IMAGE_PROTOCOLを使用します(リスト3.1)。
EFI_LOADED_IMAGE_PROTOCOLは、これまで見てきたプロトコルとは異なり、メンバのほとんどが変数や構造体のポインタで、これらのメンバにロード済みイメージの各種情報が格納されています。リスト3.1を見てみると、"FilePath"というメンバがあります。しかも型が"EFI_DEVICE_PATH_PROTOCOL"という名前なので、ここにロード済みイメージのデバイスパスが格納されていそうです。
そして、ロード済みイメージのEFI_LOADED_IMAGE_PROTOCOLを取得するためにEFI_BOOT_SERVICESのOpenProtocol()を使用します(リスト3.2)。
EFI_LOADED_IMAGE_PROTOCOLを開く場合、OpenProtocol()の第1引数へはEFI_LOADED_IMAGE_PROTOCOLで情報を見たい対象のイメージハンドルを指定し、第4引数へはOpenProtocol()を実行しているUEFIアプリケーションのイメージハンドルを指定します。今回の場合はどちらも、起動時から実行しているUEFIアプリケーションです。
なお通常、UEFIアプリケーションのイメージハンドルは後述するLoadImage()でUEFIアプリケーションをロードする際に取得しますが、起動時から実行しているUEFIアプリケーションについては、エントリ関数の第1引数"ImageHandle"が自分自身のイメージハンドルです。そのため、OpenProtocol()の第1引数と第4引数へはImageHandleを指定します。
また、OpenProtocol()の第6引数"Attributes"へ指定できるモード定数はリスト3.3の通りです。今回の場合、"EFI_OPEN_PROTOCOL_GET_PROTOCOL"を指定します。
ここまでをまとめると、OpenProtocol()はリスト3.4の様に使用します。なお、EFI_LOADED_IMAGE_PROTOCOLのGUID"lip_guid"は、efi.h(リスト3.5)とefi.c(リスト3.6)へ定義を追加しています。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: unsigned long long status; 7: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 8: 9: efi_init(SystemTable); 10: ST->ConOut->ClearScreen(ST->ConOut); 11: 12: status = ST->BootServices->OpenProtocol( 13: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 14: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 15: assert(status, L"OpenProtocol"); 16: 17: while (TRUE); 18: }
1: extern struct EFI_GUID lip_guid;
1: struct EFI_GUID lip_guid = {0x5b1b31a1, 0x9562, 0x11d2, 2: {0x8e, 0x3f, 0x00, 0xa0, 3: 0xc9, 0x69, 0x72, 0x3b}};
EFI_DEVICE_PATH_TO_TEXT_PROTOCOLを使用することで、デバイスパスを画面に表示できるようテキストへ変換できます(リスト3.7)。
前節で取得したEFI_LOADED_IMAGE_PROTOCOLのstruct EFI_DEVICE_PATH_PROTOCOL *FilePathの内容をテキストへ変換して表示してみます(リスト3.8)。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: unsigned long long status; 7: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 8: 9: efi_init(SystemTable); 10: ST->ConOut->ClearScreen(ST->ConOut); 11: 12: status = ST->BootServices->OpenProtocol( 13: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 14: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 15: assert(status, L"OpenProtocol"); 16: 17: /* 追加(ここから) */ 18: puts(L"lip->FilePath: "); 19: puts(DPTTP->ConvertDevicePathToText(lip->FilePath, FALSE, FALSE)); 20: puts(L"\r\n"); 21: /* 追加(ここまで) */ 22: 23: while (TRUE); 24: }
実行すると図3.1の様に表示されます。
図3.1を見ると、起動時から実行している自分自身のデバイスパスとしては、実行バイナリを配置した通り、"\EFI\BOOT\BOOTX64.EFI"というデバイスパスが格納されていることが分かります。
前節では確認のためにデバイスパスをテキストへ変換する関数ConvertDevicePathToText()を紹介しました。
実はその逆に、テキストをデバイスパスへ変換する関数もあります。この節ではその関数を使用してみます。
サンプルのディレクトリは"031_create_devpath_1"です。
テキストをデバイスパスへ変換する関数は、EFI_DEVICE_PATH_FROM_TEXT_PROTOCOLのConvertTextToDevicePath()です(リスト3.9)。
EFI_DEVICE_PATH_FROM_TEXT_PROTOCOLの取得はLocateProtocol()で行います(リスト3.10)。
それでは、ConvertTextToDevicePath()を使用してデバイスパスを作成してみたいと思います。前節で、起動時から実行している自分自身のデバイスパスは"\EFI\BOOT\BOOTX64.EFI"というテキストでした。"\test.efi"というテキストをデバイスパスへ変換すれば、「USBフラッシュメモリ直下のtest.efiというUEFIアプリケーション」を表せそうです。ConvertTextToDevicePath()の使用例はリスト3.11の通りです。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle __attribute__ ((unused)), 5: struct EFI_SYSTEM_TABLE *SystemTable) 6: { 7: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; 8: 9: efi_init(SystemTable); 10: ST->ConOut->ClearScreen(ST->ConOut); 11: 12: dev_path = DPFTP->ConvertTextToDevicePath(L"\\test.efi"); 13: puts(L"dev_path: "); 14: puts(DPTTP->ConvertDevicePathToText(dev_path, FALSE, FALSE)); 15: puts(L"\r\n"); 16: 17: while (TRUE); 18: }
リスト3.11では、確認のために、作成したデバイスパスをConvertDevicePathToText()を使用してテキストへ戻し、画面表示しています。
実行すると図3.2の様に表示されます。
意図した通りのデバイスパスが作成できました。
デバイスパスを作成できたので、LoadImage()でロードしてみます。
サンプルのディレクトリは"032_load_devpath_1"です。
LoadImage()はEFI_BOOT_SERVICES内で定義されています(リスト3.12)。
使用例はリスト3.13の通りです。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle __attribute__ ((unused)), 5: struct EFI_SYSTEM_TABLE *SystemTable) 6: { 7: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; /* 追加 */ 8: unsigned long long status; /* 追加 */ 9: void *image; 10: 11: efi_init(SystemTable); 12: ST->ConOut->ClearScreen(ST->ConOut); 13: 14: dev_path = DPFTP->ConvertTextToDevicePath(L"\\test.efi"); 15: puts(L"dev_path: "); 16: puts(DPTTP->ConvertDevicePathToText(dev_path, FALSE, FALSE)); 17: puts(L"\r\n"); 18: 19: /* 追加(ここから) */ 20: status = ST->BootServices->LoadImage(FALSE, ImageHandle, dev_path, NULL, 21: 0, &image); 22: assert(status, L"LoadImage"); 23: puts(L"LoadImage: Success!\r\n"); 24: /* 追加(ここまで) */ 25: 26: while (TRUE); 27: }
デバイスパスの生成までは前節のままで、生成したデバイスパス"dev_path"をLoadImage()に渡しています。LoadImage()に失敗した場合はassert()でログを出して止まり、成功した場合はputs()で"LoadImage: Success!"が表示されます。
リスト3.13を実行してみます。なお、ロードされる側の"test.efi"は、適当なUEFIアプリケーションをビルドして用意しておけばよいのですが、1点だけビルド時の注意点がありますので、後述のコラムをご覧ください。ここでは、前著"フルスクラッチで作る!UEFIベアメタルプログラミング(パート1)"のサンプルプログラム"sample1_1_hello_uefi"を使用します*1。そしてビルドしたefiバイナリを"test.efi"とリネームし、起動ディスクとして使用するUSBフラッシュメモリのルート直下へ配置したこととします。
実行してみると、結果は図3.3の通りです。
失敗しています。EFI_STATUSの"0x80000000 0000000E"は、最上位の0x8が"エラーである"事を示し、下位の"0xE"が"EFI_NOT_FOUND"を示します*2。
[*2] 詳しくは仕様書の"Appendix D Status Codes"を見てみてください
LoadImage()がイメージを見つけられなかった理由はデバイスパスが完全では無かったからです。実は、デバイスパスにはもう少し付け加えなければならない要素があり、次節から説明します。
LoadImage()でロードされるUEFIアプリケーションの実行バイナリはリロケータブルなバイナリである必要があります。
そのため、Makefileの"fs/EFI/BOOT/BOOTX64.EFI"ターゲットで"x86_64-w64-mingw32-gcc"のオプションを指定している箇所へ"-shared"オプションを追加してください(リスト3.14)。
これまで、"\EFI\BOOT\BOOTX64.EFI"や"\test.efi"のようにパスを指定してみました。ただし、考えてみれば、これでは「どのデバイスであるか」を指定できておらず、例えば、ノートPC本体のハードディスクとUSBフラッシュメモリのどちらを指しているのかが分かりません。そもそも、デバイスをパスの形で指定できるからこそ"デバイスパス"であるにも関わらず、"デバイス"の箇所を指定していませんでした。
それでは、デバイスの部分はどのように指定するのかを確認するために、例として、起動時から実行しているUEFIアプリケーション自身のデバイス部分のパスを見てみます。
サンプルのディレクトリは"033_loaded_image_protocol_device_handle"です。
実は、デバイス部分のパスを得るための情報もEFI_LOADED_IMAGE_PROTOCOLにあります(リスト3.15)。
"Source location of the image"のコメントが書かれている箇所には"FilePath"の他に"DeviceHandle"があります。この"DeviceHandle"を使用してデバイス部分のパスを得ることができます。
デバイスパス(EFI_DEVICE_PATH_PROTOCOL)を得る方法は、EFI_LOADED_IMAGE_PROTOCOLを取得する時と同じく、OpenProtocol()を使用します。そして、OpenProtocol()の第1引数にこの"DeviceHandle"を指定することで、"DeviceHandle"の"EFI_DEVICE_PATH_PROTOCOL"を得ることができます(リスト3.16)。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 7: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; 8: unsigned long long status; 9: 10: efi_init(SystemTable); 11: ST->ConOut->ClearScreen(ST->ConOut); 12: 13: /* ImageHandleのEFI_LOADED_IMAGE_PROTOCOL(lip)を取得 */ 14: status = ST->BootServices->OpenProtocol( 15: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 16: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 17: assert(status, L"OpenProtocol(lip)"); 18: 19: /* lip->DeviceHandleのEFI_DEVICE_PATH_PROTOCOL(dev_path)を取得 */ 20: status = ST->BootServices->OpenProtocol( 21: lip->DeviceHandle, &dpp_guid, (void **)&dev_path, ImageHandle, 22: NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); 23: assert(status, L"OpenProtocol(dpp)"); 24: 25: /* dev_pathをテキストへ変換し表示 */ 26: puts(L"dev_path: "); 27: puts(DPTTP->ConvertDevicePathToText(dev_path, FALSE, FALSE)); 28: puts(L"\r\n"); 29: 30: while (TRUE); 31: }
なお、上記の実装に合わせて、efi.hとefi.cへEFI_DEVICE_PATH_PROTOCOLのGUIDを定義します(リスト3.17、リスト3.18)。
実行すると、図3.4の様に表示されます。
"PciRoot"から始まるテキストが表示されており、「デバイス」の「パス」っぽいですね。本書で扱う範囲では立ち入らずに済むためあまり説明しませんが、UEFIの概念ではこの様に、デバイスをパスの形で指定して扱います。ストレージデバイスだけでなく、PCに接続されるマウス等のデバイスもこの様にパスの形で指定します。
ここまでで、"PciRoot"から始まる正に"デバイス"を指定しているパスと、"\test.efi"といったよく見るファイルのパスの2つのパスを確認しました。実は、この2つを連結することでフルパスとなります。
サンプルのディレクトリは"034_create_devpath_2"です。
パス連結等のパスを操作する関数群を持つのがEFI_DEVICE_PATH_UTILITIES_PROTOCOLです。ここでは、AppendDeviceNode()を使用します(リスト3.19)。
"デバイスノード"は、デバイスパスの部分で、"\"で区切られた1要素のことです。例えば、"\EFI\BOOT\BOOTX64.EFI"の場合、"EFI"や"BOOT"がデバイスノードです。そのため、AppendDeviceNode()は、デバイスパスの末尾に1つのノードを追加する関数、となります*3。
[*3] EFI_DEVICE_PATH_UTILITIES_PROTOCOLの中にはデバイスパスにデバイスパスを連結する関数もあるのですが、使用しないため省略します。
今回の場合、ファイルパスに相当する部分は"test.efi"という単一のデバイスノードで済むため、AppendDeviceNode()を使用します。それに併せて、テキストからデバイスパスを生成する関数群を持つEFI_DEVICE_PATH_UTILITIES_PROTOCOLにはConvertTextToDeviceNode()という関数もあるので、ここではこちらの関数を使用します(リスト3.20)。
以上を踏まえ、デバイスパスとデバイスノードを連結する例はリスト3.21の通りです。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 7: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; 8: struct EFI_DEVICE_PATH_PROTOCOL *dev_node; /* 追加 */ 9: struct EFI_DEVICE_PATH_PROTOCOL *dev_path_merged; /* 追加 */ 10: unsigned long long status; 11: 12: efi_init(SystemTable); 13: ST->ConOut->ClearScreen(ST->ConOut); 14: 15: /* ImageHandleのEFI_LOADED_IMAGE_PROTOCOL(lip)を取得 */ 16: status = ST->BootServices->OpenProtocol( 17: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 18: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 19: assert(status, L"OpenProtocol(lip)"); 20: 21: /* lip->DeviceHandleのEFI_DEVICE_PATH_PROTOCOL(dev_path)を取得 */ 22: status = ST->BootServices->OpenProtocol( 23: lip->DeviceHandle, &dpp_guid, (void **)&dev_path, ImageHandle, 24: NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); 25: assert(status, L"OpenProtocol(dpp)"); 26: 27: /* 追加・変更(ここから) */ 28: /* "test.efi"のデバイスノードを作成 */ 29: dev_node = DPFTP->ConvertTextToDeviceNode(L"test.efi"); 30: 31: /* dev_pathとdev_nodeを連結 */ 32: dev_path_merged = DPUP->AppendDeviceNode(dev_path, dev_node); 33: 34: /* dev_path_mergedをテキストへ変換し表示 */ 35: puts(L"dev_path_merged: "); 36: puts(DPTTP->ConvertDevicePathToText(dev_path_merged, FALSE, FALSE)); 37: puts(L"\r\n"); 38: /* 追加・変更(ここまで) */ 39: 40: while (TRUE); 41: }
実行すると図3.5の様に表示されます。
デバイスパスとデバイスノードは型は分かれておらず、共にEFI_DEVICE_PATH_PROTOCOLです。
どういうことかというと、EFI_DEVICE_PATH_PROTOCOLがデバイスノードで、EFI_DEVICE_PATH_PROTOCOLが連結することでデバイスパスとなります。
それでは、EFI_DEVICE_PATH_PROTOCOLはどういう定義になっているかというと、リスト3.22の様になっています。
リスト3.22を見ると、リンクリストを構成するようなメンバはありません。EFI_DEVICE_PATH_PROTOCOLはメモリ上に連続に並ぶことでパスを構成します。
また、リスト3.22を見ると、ファイルパスで必要となる"ファイル名"といった要素を格納するようなメンバがありません。実は、EFI_DEVICE_PATH_PROTOCOLは各種デバイスノードのヘッダ部分のみで、ボディに当たる部分はEFI_DEVICE_PATH_PROTOCOLに続くメモリ領域へ配置します。そのため、デバイスノードのタイプを"Type"・"SubType"メンバで指定し、ヘッダ・ボディ含めたサイズを"Length"で指定します。
デバイスパス内のノード数を示す要素はどこにもありません。ノード終端を示すデバイスノードを配置することでデバイスノードの終わりを示します。
UEFIでは、デバイスパスやノードの構造やメモリ上の配置は意識せずに使用できるよう、EFI_DEVICE_PATH_FROM_TEXT_PROTOCOLやEFI_DEVICE_PATH_UTILITIES_PROTOCOLといったプロトコルを用意しています。AppendDeviceNode()でデバイスパスへデバイスノードを追加する際も、ノード終端の要素は自動で配置してくれます。そのため、UEFIのファームウェアを仕様書通りに叩く上では特に意識する必要はありません。
それでは、再びデバイスパスのロードを試してみます。
サンプルのディレクトリは"035_load_devpath_2"です。
前節のサンプル(リスト3.21)へリスト3.13で紹介したLoadImage()の処理を追加するだけです(リスト3.23)。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 7: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; 8: struct EFI_DEVICE_PATH_PROTOCOL *dev_node; 9: struct EFI_DEVICE_PATH_PROTOCOL *dev_path_merged; 10: unsigned long long status; 11: void *image; /* 追加 */ 12: 13: efi_init(SystemTable); 14: ST->ConOut->ClearScreen(ST->ConOut); 15: 16: /* ImageHandleのEFI_LOADED_IMAGE_PROTOCOL(lip)を取得 */ 17: status = ST->BootServices->OpenProtocol( 18: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 19: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 20: assert(status, L"OpenProtocol(lip)"); 21: 22: /* lip->DeviceHandleのEFI_DEVICE_PATH_PROTOCOL(dev_path)を取得 */ 23: status = ST->BootServices->OpenProtocol( 24: lip->DeviceHandle, &dpp_guid, (void **)&dev_path, ImageHandle, 25: NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); 26: assert(status, L"OpenProtocol(dpp)"); 27: 28: /* "test.efi"のデバイスノードを作成 */ 29: dev_node = DPFTP->ConvertTextToDeviceNode(L"test.efi"); 30: 31: /* dev_pathとdev_nodeを連結 */ 32: dev_path_merged = DPUP->AppendDeviceNode(dev_path, dev_node); 33: 34: /* dev_path_mergedをテキストへ変換し表示 */ 35: puts(L"dev_path_merged: "); 36: puts(DPTTP->ConvertDevicePathToText(dev_path_merged, FALSE, FALSE)); 37: puts(L"\r\n"); 38: 39: /* 追加(ここから) */ 40: /* dev_path_mergedをロード */ 41: status = ST->BootServices->LoadImage(FALSE, ImageHandle, 42: dev_path_merged, NULL, 0, &image); 43: assert(status, L"LoadImage"); 44: puts(L"LoadImage: Success!\r\n"); 45: /* 追加(ここまで) */ 46: 47: while (TRUE); 48: }
実行してみると、今度はロードに成功しました(図3.6)。
ロードが成功しましたので、いよいよ実行してみます。
サンプルのディレクトリは"036_start_devpath"です。
ロードしたイメージを実行するにはEFI_BOOT_SERVICESのStartImage()を使用します(リスト3.24)。
StartImage()の使用例はリスト3.25の通りです。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 7: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; 8: struct EFI_DEVICE_PATH_PROTOCOL *dev_node; 9: struct EFI_DEVICE_PATH_PROTOCOL *dev_path_merged; 10: unsigned long long status; 11: void *image; 12: 13: efi_init(SystemTable); 14: ST->ConOut->ClearScreen(ST->ConOut); 15: 16: /* ImageHandleのEFI_LOADED_IMAGE_PROTOCOL(lip)を取得 */ 17: status = ST->BootServices->OpenProtocol( 18: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 19: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 20: assert(status, L"OpenProtocol(lip)"); 21: 22: /* lip->DeviceHandleのEFI_DEVICE_PATH_PROTOCOL(dev_path)を取得 */ 23: status = ST->BootServices->OpenProtocol( 24: lip->DeviceHandle, &dpp_guid, (void **)&dev_path, ImageHandle, 25: NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); 26: assert(status, L"OpenProtocol(dpp)"); 27: 28: /* "test.efi"のデバイスノードを作成 */ 29: dev_node = DPFTP->ConvertTextToDeviceNode(L"test.efi"); 30: 31: /* dev_pathとdev_nodeを連結 */ 32: dev_path_merged = DPUP->AppendDeviceNode(dev_path, dev_node); 33: 34: /* dev_path_mergedをテキストへ変換し表示 */ 35: puts(L"dev_path_merged: "); 36: puts(DPTTP->ConvertDevicePathToText(dev_path_merged, FALSE, FALSE)); 37: puts(L"\r\n"); 38: 39: /* dev_path_mergedをロード */ 40: status = ST->BootServices->LoadImage(FALSE, ImageHandle, 41: dev_path_merged, NULL, 0, &image); 42: assert(status, L"LoadImage"); 43: puts(L"LoadImage: Success!\r\n"); 44: 45: /* 追加(ここから) */ 46: /* imageの実行を開始する */ 47: status = ST->BootServices->StartImage(image, NULL, NULL); 48: assert(status, L"StartImage"); 49: puts(L"StartImage: Success!\r\n"); 50: /* 追加(ここまで) */ 51: 52: while (TRUE); 53: }
実行例は図3.7の通りです。
無事に実行できました!これで、今後はUEFIアプリケーションをバイナリ単位で分割し、呼び出す事ができます。
そう言えば、前著(パート1)のsample1_1_hello_uefiでは、CR('\r')が入っていないので、今回のように、別のUEFIアプリケーションから呼び出された場合、改行はしますが、行頭は戻ってないですね。
現在のLinuxカーネルはビルド時の設定(menuconfig)でUEFIバイナリを生成するように設定できます。LinuxカーネルをUEFIバイナリとしてビルドすることで、Linuxのイメージ(bzImage)をこれまで説明したUEFIアプリケーションの実行方法で実行させることができます。
サンプルのディレクトリは"037_start_bzImage"です。
Debian、Ubuntu等のパッケージ管理システムでAPTが使用できる事を前提に説明します。
APTでは"apt-get build-dep <パッケージ名>
"というコマンドで、指定したパッケージ名をビルドするために必要な環境をインストールすることができます。
カーネルイメージは"linux-image-<バージョン>-<アーキテクチャ>"というパッケージ名です。現在使用しているカーネルバージョンとアーキテクチャは"uname -r
"コマンドで取得できるので、結果としては、以下のコマンドでLinuxカーネルをビルドする環境をインストールできます。
$ sudo apt-get build-dep linux-image-$(uname -r)
なお、menuconfigを使用するための"libncurses5-dev"パッケージがbuild-depでインストールされないので、別途インストールする必要があります。
$ sudo apt install libncurses5-dev
ソースコードもAPTでは"apt-get source <パッケージ名>
"というコマンドで取得できます。しかし、せっかくなのでここでは本家であるkernel.orgから最新の安定版をダウンロードして使用してみることにします。
https://www.kernel.org/へアクセスし、"Latest Stable Kernel:"からダウンロードしてください(図3.8)。
ダウンロード後は、展開しておいてください。
$ tar Jxf linux-<バージョン>.tar.xz
Linuxカーネルのビルドの設定を行います。
まず、展開したLinuxカーネルのソースコードディレクトリへ移動し、x86_64向けのデフォルト設定を反映させます。
$ cd linux-<バージョン> $ make x86_64_defconfig
そして、menuconfigで、UEFIバイナリを生成する設定(コンフィグシンボル名"CONFIG_EFI_STUB")を有効化します。
menuconfigを起動させます。
$ make menuconfig
すると、図3.9の画面になります。
リスト3.26の場所にあるCONFIG_EFI_STUBを有効化します。
makeコマンドでビルドします。-jオプションでビルドのスレッド数を指定できます。ここではnprocコマンドで取得したCPUコア数を-jオプションに渡しています。
$ make -j $(nproc)
ビルドには時間がかかりますので、のんびりと待ちましょう。
ビルドが完了すると、"bzImage"というイメージファイルができあがっています。
$ ls arch/x86/boot/bzImage arch/x86/boot/bzImage
arch/x86/boot/bzImageをUSBフラッシュメモリ等のこれまで"test.efi"を配置していた場所に"bzImage.efi"という名前で配置し、リスト3.25の"test.efi"を"bzImage.efi"へ変更するだけで、Linuxカーネルを起動できます図3.10。
Linuxカーネルの起動は始まりますが、ルートファイルシステムを何も準備していないのでカーネルパニックで止まります。
もし、前述の場所に設定項目が無い場合は、menuconfigの検索機能を使ってみてください。
menuconfig画面内で"/(スラッシュ)"キーを押下すると図3.11の画面になります。
この画面で"efi_stub"の様に検索したいキーワードを入力*4し、Enterを押下すると、コンフィギュレーションの依存関係や設定項目の場所等の情報が表示されます(図3.12)。なお、検索結果の画面ではコンフィグシンボル名の"CONFIG_"は省略されています。
図3.12では、"Location"の欄にコンフィグの場所が、"Prompt"の欄に設定項目名が記載されています。
"Location"と"Prompt"が示す場所にも設定項目が無い場合は、特にコンフィグの依存関係("Depends on")を見てみると良いです。図3.12の場合、"Depends on"は、"CONFIG_EFIが有効で、かつCONFIG_X86_USE_3DNOWが無効であること"を示しており、"[]"の中は現在の設定値です。現在の設定値が要求している依存関係と一致していない場合、その設定項目はmenuconfig画面内に現れませんので、先に依存するコンフィグを変更する必要があります。
前節では、Linuxカーネルが起動時に参照するルートファイルシステム("root=")等のオプションを指定していなかったため、カーネルパニックに陥ってしまっていました。そこで、カーネル起動時のオプションを設定してみます。
サンプルのディレクトリは"038_start_bzImage_options"です。
UEFIアプリケーション実行時のオプション(引数)は、EFI_LOADED_IMAGE_PROTOCOLのunsigned int LoadOptionsSize
メンバとvoid *LoadOptions
メンバへ行います(リスト3.27)。
使用例はリスト3.28の通りです。
1: #include "efi.h" 2: #include "common.h" 3: 4: void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable) 5: { 6: struct EFI_LOADED_IMAGE_PROTOCOL *lip; 7: struct EFI_LOADED_IMAGE_PROTOCOL *lip_bzimage; /* 追加 */ 8: struct EFI_DEVICE_PATH_PROTOCOL *dev_path; 9: struct EFI_DEVICE_PATH_PROTOCOL *dev_node; 10: struct EFI_DEVICE_PATH_PROTOCOL *dev_path_merged; 11: unsigned long long status; 12: void *image; 13: unsigned short options[] = L"root=/dev/sdb2 init=/bin/sh rootwait"; 14: /* 追加 */ 15: 16: efi_init(SystemTable); 17: ST->ConOut->ClearScreen(ST->ConOut); 18: 19: /* ImageHandleのEFI_LOADED_IMAGE_PROTOCOL(lip)を取得 */ 20: status = ST->BootServices->OpenProtocol( 21: ImageHandle, &lip_guid, (void **)&lip, ImageHandle, NULL, 22: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 23: assert(status, L"OpenProtocol(lip)"); 24: 25: /* lip->DeviceHandleのEFI_DEVICE_PATH_PROTOCOL(dev_path)を取得 */ 26: status = ST->BootServices->OpenProtocol( 27: lip->DeviceHandle, &dpp_guid, (void **)&dev_path, ImageHandle, 28: NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); 29: assert(status, L"OpenProtocol(dpp)"); 30: 31: /* "bzImage.efi"のデバイスノードを作成 */ 32: dev_node = DPFTP->ConvertTextToDeviceNode(L"bzImage.efi"); 33: 34: /* dev_pathとdev_nodeを連結 */ 35: dev_path_merged = DPUP->AppendDeviceNode(dev_path, dev_node); 36: 37: /* dev_path_mergedをテキストへ変換し表示 */ 38: puts(L"dev_path_merged: "); 39: puts(DPTTP->ConvertDevicePathToText(dev_path_merged, FALSE, FALSE)); 40: puts(L"\r\n"); 41: 42: /* dev_path_mergedをロード */ 43: status = ST->BootServices->LoadImage(FALSE, ImageHandle, 44: dev_path_merged, NULL, 0, &image); 45: assert(status, L"LoadImage"); 46: puts(L"LoadImage: Success!\r\n"); 47: 48: /* 追加(ここから) */ 49: /* カーネル起動オプションを設定 */ 50: status = ST->BootServices->OpenProtocol( 51: image, &lip_guid, (void **)&lip_bzimage, ImageHandle, NULL, 52: EFI_OPEN_PROTOCOL_GET_PROTOCOL); 53: assert(status, L"OpenProtocol(lip_bzimage)"); 54: lip_bzimage->LoadOptions = options; 55: lip_bzimage->LoadOptionsSize = 56: (strlen(options) + 1) * sizeof(unsigned short); 57: /* 追加(ここまで) */ 58: 59: /* imageの実行を開始する */ 60: status = ST->BootServices->StartImage(image, NULL, NULL); 61: assert(status, L"StartImage"); 62: puts(L"StartImage: Success!\r\n"); 63: 64: while (TRUE); 65: }
リスト3.28では、カーネル起動オプションとして"root=/dev/sdb2 init=/bin/sh rootwait"を指定しています。
"root=/dev/sdb2"は、ルートファイルシステムを配置しているパーティションへのデバイスファイルの指定です。筆者が実験で使用しているPCの場合、内蔵のHDDを"sda"として認識し、接続したUSBフラッシュメモリを"sdb"として認識します。そのため、USBフラッシュメモリの第2パーティションを指定するため、"/dev/sdb2"としています。
"init=/bin/sh"は起動時にカーネルが最初に実行する実行バイナリの指定です。何も指定しない場合、"/sbin/init"が実行されますが、起動直後にシェルを立ち上げてしまおうと、"/bin/sh"を指定しています。そのため、後述しますが、USBフラッシュメモリの第2パーティションへ/bin/shを配置しておく必要があります。
"rootwait"は、Linuxカーネルがルートファイルシステムを検出するタイミングを遅らせるオプションです。USBフラッシュメモリ等の場合、デバイスが検出されるタイミングは非同期です。デバドラの検出より先にLinuxカーネルのルートファイルシステム検出を行おうとするとルートファイルシステムの検出に失敗してしまうので、ルートファイルシステムの検出を遅延させるためのオプションです。
実行例は図3.13の通りです。
晴れて、シェルを起動させることができました。
今回の場合の様に「シェルさえ動けば良い」のであれば、ルートファイルシステムの構築には"BusyBox"が便利です。BusyBoxは"組み込みLinuxのスイスアーミーナイフ"と呼ばれるもので、一つの"busybox"という実行バイナリへシンボリックリンクを張ることで、色々なコマンドが使用できるようになるというものです。
Debian、あるいはUbuntu等のAPTを使用できる環境では、"busybox-static"というパッケージをインストールすることで、必要なライブラリが静的に埋め込まれたbusyboxバイナリを取得できます。
$ sudo apt install busybox-static ・・・ $ ls /bin/busybox /bin/busybox
インストールすると、/bin/busyboxへbusyboxの実行バイナリが配置されます。この実行バイナリをUSBフラッシュメモリ第2パーティションへ以下の様に配置すれば良いです。("sh"は"busybox"へのシンボリックリンクです。)
USBフラッシュメモリ第2パーティション └── bin/ ├── busybox └── sh -> busybox
なお、基本的なコマンドはBusyBoxで事足りますが、「aptを使用したい」等の場合はルートファイルシステムの構築が面倒になってきます。
Debianから公開されているdebootstrapというコマンドを使用すると、apt等が使用できる最低限のDebianのルートファイルシステムをコマンド一つで構築できます。
$ sudo debootstrap <debianのバージョン> <作成先のディレクトリ>
例えば、USBフラッシュメモリの第2パーティションを/mnt/storageへマウントしている時、sid(unstable)のDebianルートファイルシステムを/mnt/storageへ構築するコマンドは以下の通りです。
$ sudo debootstrap sid /mnt/storage