コンピュータの電源ボタンを押した瞬間に何が起こるのか疑問に思ったことはありませんか? 画面が点灯する前のその短い休止の背後で、一連の複雑なプロセスが進行しています。この記事では、ファームウェアの魅力的な世界に飛び込み、ブート プロセス中にさまざまなコンポーネントがどのように相互作用するかを探ります。
これらのつながりを理解することで、システムを動かす基礎要素をより明確に把握できるようになります。ここでは主にIntel x86 アーキテクチャに焦点を当てますが、多くの原則は他のアーキテクチャにも当てはまります。
このシリーズの第 1 部を見逃した方は、 こちらをクリックしてご覧ください。それでは、ファームウェアの背後にある謎を解き明かしましょう。
ファームウェア コンポーネントがどのように相互作用するかを理解するために、接続されているすべての部分を含むアーキテクチャ全体を調べます。下の図に示す実行フローは、第 1 ステージ ブートローダの一部であるリセット ベクターから始まります。そこから、さまざまなファームウェア ステージを経て進行します。
ファームウェアまたは BIOS は、通常、2 つの主要な部分に分けられ、その間のインターフェースは通常最小限です。
プラットフォーム ファームウェアの設計は、ハードウェアの初期化とブート機能を組み合わせたモノリシックにすることも、モジュール式の段階的なブート フローに従うこともできます。設計の選択はシステム要件によって異なり、特定のデバイスに適している場合があります。
次の図は、さまざまなファームウェア コンポーネントがどのように相互作用し、ブート プロセスをサポートするために一緒に使用されるかを示しています (矢印は実行の順序を示します)。
これらの図が今は複雑に思えても心配しないでください。この記事を読んだ後にもう一度見直すと、より明確になります。
このファームウェアは、最小限のハードウェア初期化に重点を置いてコンピューターと組み込みシステムを初期化するように設計されています。つまり、絶対に必要なことだけを実行し、制御を第 2 ステージ ブートローダーに渡してオペレーティング システムを起動します。FSBLは、フラッシュ チップ以外のストレージ メディアからオペレーティング システムをロードしません。基礎となるハードウェアを初期化するだけで、ハード ドライブ、SSD、USB フラッシュ ドライブなどのブート メディアは処理しないため、実際にオペレーティング システムを起動するには別のソフトウェアが必要です。
FSBLの主な責任:
コンピューティングの初期の頃は、オープンソース ソフトウェアは広く普及しておらず、ほとんどの BIOS 実装は独自のものでした。Super PC/Turbo XT BIOSやGLaBIOSなど、BIOS POST ソース コードを提供するオープン ソリューションはわずかしかありません。これらのプロジェクトは、IBM 5150/5155/5160 システムとほとんどの XT クローンで動作するように設計されました。
ただし、 OpenBIOSやSeaBIOSなどのよく知られているオープンソースの BIOS 実装は、ベアハードウェア上で実行することを目的としていないため、ハードウェアの初期化を実行しません。ただし、これらは第 2 ステージ ブートローダとして広く使用されており、QEMU や Bochs などの仮想環境でネイティブに実行されます。
いずれにせよ、これらの初期の BIOS を直接操作したり、その詳細を深く調べる必要はほとんどありません。ただし、探索することに興味がある場合は、前述のリポジトリが適切な出発点になります。
現在の開発動向から判断すると、独自の BIOS ソリューションの開発は進行中ではないようで、このようなプロジェクトは最新の代替手段の登場により時代遅れになっています。
ブート プロセスは、次の図に示すように、左から始まり右に進む段階的なフローに従います。プラットフォーム ブート プロセスのタイムラインは、黄色のボックスで示されるように、次のフレーズに分割されます。
ExitBootServices()
呼び出しで終了します。
このプロセスとその実行フェーズは、UEFI プラットフォーム初期化 (PI) 仕様でカバーされています。ただし、 UEFI インターフェイス(図の太い青い線で示されています) もあります。これは前のドキュメントの一部ではなく、 UEFI 仕様で説明されています。UEFIの名前と頻繁な使用は混乱を招く可能性がありますが、これら 2 つのドキュメントの焦点は異なります。
基本的に、どちらの仕様もインターフェースに関するものですが、レベルが異なります。詳細については、 UEFI フォーラムの Web サイトで両方の仕様にアクセスできます。
UEFI PI は当初、第 1 ステージ ブートローダと第 2 ステージ ブートローダの区別を考慮せずに、統合ファームウェア ソリューションとして設計されました。ただし、 UEFIを第 1 ステージ ブートローダと呼ぶ場合、 SEC 、 PEI 、および初期の DXEフェーズが含まれます。DXE を初期段階と後期段階に分ける理由は、初期化プロセスにおける役割が異なるためです。
初期の DXEフェーズでは、ドライバーは通常、重要な CPU/PCH/ボードの初期化を実行し、DXE フェーズをプラットフォーム固有のハードウェアから分離するのに役立つDXE アーキテクチャ プロトコル (AP)も生成します。APはプラットフォーム固有の詳細をカプセル化し、後期の DXEフェーズがハードウェアの詳細とは独立して動作できるようにします。
Coreboot の仕組みに関する詳細な記事が近日公開されます。私のソーシャル メディアをフォローしてください。近日中に公開されます。
初期のハードウェア セットアップが完了すると、第 2段階が始まります。その主な役割は、オペレーティング システムとプラットフォーム ファームウェアの間にソフトウェア インターフェイスを設定し、OS がシステム リソースを管理し、ハードウェア コンポーネントと対話できるようにすることです。
SSBL は、ハードウェアのバリエーションを可能な限り隠し、ハードウェア レベルのインターフェイスのほとんどを処理することで OS とアプリケーションの開発を簡素化することを目的としています。この抽象化により、開発者は基盤となるハードウェアの違いを気にすることなく、より高レベルの機能に集中できます。
SSBLの主な責任:
プラットフォーム情報の取得: メモリ マッピング、SMBIOS、ACPI テーブル、SPI フラッシュなど、プラットフォーム固有の情報を第 1 段階ブートローダから取得します。
プラットフォームに依存しないドライバーを実行します: SMM、SPI、PCI、SCSI/ATA/IDE/DISK、USB、ACPI、ネットワーク インターフェイスなどのドライバーが含まれます。
サービス実装 (別名インターフェイス) : オペレーティング システムとハードウェア コンポーネント間の通信を容易にする一連のサービスを提供します。
セットアップ メニュー: システム構成のセットアップ メニューを提供し、ユーザーは起動順序、ハードウェア設定、その他のシステム パラメータに関連する設定を調整できます。
ブート ロジック: 利用可能なブート メディアからペイロード (おそらくオペレーティング システム) を見つけてロードするメカニズム。
BIOS のインターフェースは、BIOS サービス/関数/割り込み呼び出しと呼ばれます。これらの関数は、ハードウェア アクセス用の一連のルーチンを提供しますが、システムの特定のハードウェア上でどのように実行されるかという具体的な詳細は、ユーザーには表示されません。
16 ビットリアル モードでは、 INT x86 アセンブリ言語命令を介してソフトウェア割り込みを呼び出すことで簡単にアクセスできます。32 ビットプロテクト モードでは、セグメント値の処理方法が異なるため、ほぼすべての BIOS サービスが利用できません。
このインターフェイスの使用方法の例として、シリンダ ヘッド セクター (CHS)アドレス指定を使用してセクター ベースのハード ディスクとフロッピー ディスクの読み取りおよび書き込みサービスを提供するディスク サービス( INT 13h
) を取り上げます。2 つのセクター (1024 バイト) を読み取り、メモリ アドレス0x9020にロードするとすると、次のコードを実行できます。
mov $0x02, %ah # Set BIOS read sector routine mov $0x00, %ch # Select cylinder 0 mov $0x00, %dh # Select head 0 [has a base of 0] mov $0x02, %cl # Select sector 2 (next after the # boot sector) [has a base of 1] mov $0x02, %al # Read 2 sectors mov $0x00, %bx # Set BX general register to 0 mov %bx, %es # Set ES segment register to 0 mov $0x9020, %bx # Load sectors to ES:BX (0:0x9020) int $0x13 # Start reading from drive jmp $0x9020 # Jump to loaded code
このサービスが SeaBios でどのように記述されているかに興味がある場合は、 src/disk.cを参照してください。
デバイスから最初の 512 バイトのセクター(セクター 0) を読み取ることによって、起動可能なデバイス(ハード ドライブ、CD-ROM、フロッピー ディスクなど) の検索を開始します。
BIOS のブートストラップ シーケンスは、見つかった最初の有効なマスター ブート レコード (MBR)を、コンピューターの物理メモリの物理アドレス0x7C00にロードします (ヒント: 0x0000:0x7c00と0x7c0:0x0000 は同じ物理アドレスを参照します)。
BIOS はペイロードの最初の 512 バイトに制御を移します。このロードされたセクターはペイロード コード全体を格納するには小さすぎますが、ブート可能なデバイスからペイロードの残りをロードする目的で使用されます。この時点で、ペイロードは BIOS によって公開されたインターフェイスを使用できます。
初期の頃にはBIOS 仕様が存在しなかったことは注目に値します。BIOS は事実上の標準であり、1980 年代の実際の IBM PC で動作していたのと同じように動作します。他のメーカーは、IBM 互換の BIOS をリバース エンジニアリングして作成しただけです。その結果、BIOS メーカーが新しい BIOS 機能を発明したり、重複する機能を持たせたりすることを防ぐ規制はありませんでした。
前述のように、UEFI 自体は単なる仕様であり、多くの実装があります。最も広く使用されているのは、UEFI および PI 仕様のオープンソースリファレンス実装であるTianoCore EDK IIです。EDKII だけでは完全に機能するブート ファームウェアを作成するには不十分ですが、ほとんどの商用ソリューションの強固な基盤を提供します。
さまざまなファーストステージ ブートローダをサポートし、UEFI インターフェイスを提供するために、 UEFI ペイロードプロジェクトが使用されます。これは、実行された初期セットアップとブート ファームウェアによって提供されるプラットフォーム情報に依存して、システムを UEFI 環境用に準備します。
UEFI ペイロードは、プラットフォームに依存しないDXEフェーズとBDSフェーズを使用します。さまざまなプラットフォームに適応できる汎用ペイロードを提供します。ほとんどの場合、カスタマイズやプラットフォーム固有の調整は必要なく、ファーストステージ ブートローダーからプラットフォーム情報を取得してそのまま使用できます。
UEFI ペイロードのバリアント:
レガシー UEFI ペイロード: 必要な実装固有のプラットフォーム情報を抽出するための解析ライブラリが必要です。ブートローダが API を更新する場合は、ペイロードも更新する必要があります。
ユニバーサル UEFI ペイロード:ユニバーサル スケーラブル ファームウェア (USF) 仕様に準拠し、共通イメージ形式として実行可能およびリンク可能形式 (ELF)またはフラット イメージ ツリー (FIT)を使用します。ペイロード自体を解析するのではなく、ペイロード エントリでハンドオフ ブロック (HOB)を受信することを想定しています。
レガシー UEFI ペイロードは問題なく動作しますが、EDK2 コミュニティは業界をユニバーサル UEFI ペイロードへと移行させようとしています。ペイロードの選択は、ファームウェア コンポーネントによって異なります。たとえば、私のパッチなしでは、 Slim Bootloaderで SMM サポート付きのレガシー ペイロードを実行することはできません。一方、コアブートでユニバーサルペイロードを使用するには、コアブート テーブルをHOBに変換するためのシム レイヤーが必要です。これは、 StarLabs EDK2 フォークでのみ利用可能な機能です。
すべての UEFI 準拠システムは、UEFI 環境で実行されるすべてのコード (ドライバー、アプリケーション、OS ローダー) に渡されるシステム テーブルを提供します。このデータ構造により、UEFI 実行可能ファイルは、 ACPI 、 SMBIOS、 UEFI サービスのコレクションなどのシステム構成テーブルにアクセスできます。
テーブル構造はMdePkg/Include/Uefi/UefiSpec.hに記述されています。
typedef struct { EFI_TABLE_HEADER Hdr; CHAR16 *FirmwareVendor; UINT32 FirmwareRevision; EFI_HANDLE ConsoleInHandle; EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; EFI_HANDLE ConsoleOutHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_HANDLE StandardErrorHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; // // A pointer to the EFI Runtime Services Table. // EFI_RUNTIME_SERVICES *RuntimeServices; // // A pointer to the EFI Boot Services Table. // EFI_BOOT_SERVICES *BootServices; UINTN NumberOfTableEntries; EFI_CONFIGURATION_TABLE *ConfigurationTable; } EFI_SYSTEM_TABLE;
サービスには、ブート サービス、ランタイム サービス、プロトコルによって提供されるサービスなどのタイプが含まれます。
UEFI は、UEFI プロトコルを設定することでデバイスへのアクセスを抽象化します。これらのプロトコルは関数ポインターを含むデータ構造であり、他のモジュールがそれらを見つけて使用できるようにするグローバル一意識別子 (GUID)によって識別されます。これらはブート サービスを通じて検出できます。
UEFI ドライバーはこれらのプロトコルを生成し、実際の関数 (ポインターではありません!) はドライバー自体に含まれています。このメカニズムにより、UEFI 環境内のさまざまなコンポーネントが相互に通信できるようになり、OS が独自のドライバーをロードする前にデバイスと対話できるようになります。
一部のプロトコルは UEFI 仕様で事前定義および説明されていますが、ファームウェア ベンダーは独自のカスタム プロトコルを作成してプラットフォームの機能を拡張することもできます。
ブートサービス
起動時にのみ使用できる関数を提供します。これらのサービスはEFI_BOOT_SERVICES.ExitBootServices()
関数が呼び出されるまで利用可能です ( MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c )。
すべてのブート サービスへのポインターは、ブート サービス テーブル( MdePkg/Include/Uefi/UefiSpec.h ) に格納されます。
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_MEMORY_MAP GetMemoryMap; EFI_ALLOCATE_POOL AllocatePool; EFI_FREE_POOL FreePool; ... EFI_HANDLE_PROTOCOL HandleProtocol; ... EFI_EXIT_BOOT_SERVICES ExitBootServices; ... } EFI_BOOT_SERVICES;
ランタイムサービス
オペレーティング システムの実行中も、最小限のサービス セットにアクセスできます。ブート サービスとは異なり、これらのサービスは、任意のペイロード (OS ブートローダーなど) がEFI_BOOT_SERVICES.ExitBootServices()
の呼び出しによってプラットフォームを制御した後も有効です。
すべてのランタイム サービスへのポインターは、ランタイム サービス テーブル( MdePkg/Include/Uefi/UefiSpec.h ) に格納されます。
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_TIME GetTime; EFI_SET_TIME SetTime; ... EFI_GET_VARIABLE GetVariable; EFI_GET_NEXT_VARIABLE_NAME GetNextVariableName; EFI_SET_VARIABLE SetVariable; ... EFI_GET_NEXT_HIGH_MONO_COUNT GetNextHighMonotonicCount; EFI_RESET_SYSTEM ResetSystem; ... } EFI_RUNTIME_SERVICES;
下の図は、ブート サービスとランタイム サービスのタイムラインを示しており、各サービスがいつアクティブになるかを正確に確認できます。
UEFI 仕様では、UEFI ブート マネージャーと呼ばれるブート ポリシー エンジンが定義されています。これは、特定の順序でUEFI アプリケーションをロードしようとします。この順序とその他の設定は、グローバルNVRAM (不揮発性ランダム アクセス メモリ) 変数を変更することで構成できます。最も重要な変数について説明します。
Boot####
( ####
は一意の 16 進数値に置き換えられます) — ブート/ロード オプション。BootCurrent
— 現在実行中のシステムを起動するために使用されるブート オプション。BootNext
— 次回の起動のみの起動オプション。これは、1 回の起動のみBootOrder
を置き換え、最初の使用後にブート マネージャーによって削除されます。これにより、 BootOrder
を変更せずに、次回の起動の動作を変更できます。BootOrder
— 順序付けられたブート オプション ロード リスト。ブート マネージャーは、このリストの最初のアクティブ オプションのブートを試行します。失敗した場合は、次のオプションを試行します。BootOptionSupport
— ブート マネージャーによってサポートされるブート オプションの種類。Timeout
- BootNext
またはBootOrder
から起動値を自動的に選択するまでの、ファームウェアのブート マネージャーのタイムアウト (秒単位)。
これらの変数は、efibootmgr(8)を使用してLinuxから簡単に取得できます。
[root@localhost ~]# efibootmgr BootCurrent: 0000 Timeout: 5 seconds BootOrder: 0000,0001,2001,2002,2003 Boot0000* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi) Boot0001* Windows Boot Manager HD(1,GPT,6f185443-09fc-4f15-afdf-01c523565e52,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)57a94e544f5753000100000088900100780000004200430044039f0a42004a004500430054003d007b00390064006500610038003600320063002d1139006300640064002d0034006500370030102d0061006300630031002d006600330032006200330034003400640034003700390035007d00000033000300000710000000040000007fff0400 Boot0002* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000) Boot2001* EFI USB Device RC Boot2002* EFI DVD/CDROM RC Boot2003* EFI Network RC
上記のコード スニペットを参考にして、ブートを見てみましょう。UEFI はBootOrder
リストの反復処理を開始します。リスト内の各エントリについて、対応するBoot####
変数を検索します (0000 の場合はBoot0000
の場合はBoot2003
など)。変数が存在しない場合は、次のエントリに進みます。変数が存在する場合は、変数の内容を読み取ります。各ブート オプション変数には、可変長フィールドのバイト パック バッファーであるEFI_LOAD_OPTION
記述子が含まれています (これは単なるデータ構造です)。
データ構造は[MdePkg/Include/Uefi/UefiSpec.h][ https://github.com/tianocore/edk2/blob/edk2-stable202405/MdePkg/Include/Uefi/UefiSpec.h#L2122 )に記載されています。
typedef struct _EFI_LOAD_OPTION { /// The attributes for this load option entry. UINT32 Attributes; /// Length in bytes of the FilePathList. UINT16 FilePathListLength; /// The user readable description for the load option. /// Example: 'ARCHLINUX' / 'Windows Boot Manager' / `EFI USB Device` // CHAR16 Description[]; /// A packed array of UEFI device paths. /// Example: 'HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi)' // EFI_DEVICE_PATH_PROTOCOL FilePathList[]; /// The remaining bytes in the load option descriptor are a binary data buffer that is passed to the loaded image. /// Example: '57a9...0400' in Boot0001 variable // UINT8 OptionalData[]; } EFI_LOAD_OPTION;
この時点で、ファームウェアはデバイス パス( EFI_DEVICE_PATH_PROTOCOL ) を調べます。ほとんどの場合、コンピューターはストレージ デバイス (ハード ドライブ/SSD/NVMe など) から起動されます。そのため、デバイス パスにはHD(Partition Number, Type, Signature, Start sector, Size in sectors)
ノードが含まれます。
注: 他のパスを変換する方法に興味がある場合は、 UEFI 仕様 v2.10、10.6.1.6 テキスト デバイス ノード リファレンスを参照してください。
UEFI はディスクを調べて、ノードに一致するパーティションがあるかどうかを確認します。存在する場合は、 EFI システム パーティション (ESP)としてマークする特定のグローバル一意識別子 (GUID)でラベル付けされている必要があります。これは、 FAT ファイル システムの特定のバージョンに基づいて仕様が決められ、 EFI ファイル システムと呼ばれるファイル システムでフォーマットされています。実際は、通常のFAT12/16/32です。
File(\Path\To\The\File.efi)
が含まれている場合、UEFI はその特定のファイルを検索します。たとえば、 Boot0000
オプションにはFile(\EFI\ARCHLINUX\grubx64.efi)
が含まれています。\EFI\BOOT\BOOT{arch}.EFI
( amd64の場合はBOOTx64.EFI
、 i386 / IA32の場合はBOOTia32.EFI
)) を使用します。このメカニズムにより、ブート可能なリムーバブル メディア(USB ドライブなど) を UEFI で動作させることができます。フォールバック ブート パスのみが使用されます。たとえば、 Boot0002
オプションはこのメカニズムを使用します。
注:上記のすべてのBoot####
オプションは、 efibootmgrの出力例に表示されるブート オプションを指します。
どちらの場合も、 UEFI ブート マネージャーはUEFI アプリケーション( OS ブートローダー、UEFI シェル、ユーティリティ ソフトウェア、システム セットアップなど) をメモリにロードします。この時点で、制御はUEFI アプリケーションのエントリ ポイントに転送されます。BIOS とは異なり、 UEFI アプリケーションはファームウェアに制御を返すことができます (アプリケーションがシステムの制御を引き継ぐ場合を除く)。このような状況が発生した場合、または何か問題が発生した場合、ブート マネージャーは次のBoot####
エントリに移動し、まったく同じプロセスに従います。
仕様では、ブート マネージャーがデータベース変数を自動的に維持できることが述べられています。これには、参照されていない、または解析できないロード オプション変数の削除が含まれます。さらに、順序付きリストを書き換えて、対応するロード オプション変数のないロード オプションを削除することもできます。
上記のテキストはUEFI ブートについて説明しています。また、UEFI ファームウェアは、BIOS をエミュレートする互換性サポート モジュール (CSM)モードで実行できます。
ファームウェア (通常は第 2 ステージ ブートローダ) によって起動され、そのインターフェイスを使用してOS カーネルをロードするソフトウェア。OS と同じくらい複雑で、次のような機能を提供します。
これらのプログラムの一般的な設計については、この記事の範囲外です。一般的な OS ブートローダーの詳細な比較については、 ArchLinux wikiとWikipedia の記事を参照してください。
Windows システムは、Windows ブート マネージャー (BOOTMGR)と呼ばれる独自の OS ブートローダーを使用します。
ファームウェアはもはや小さくて複雑なコードではありません。膨大な量の複雑なコードになり、現在のトレンドはこれに拍車をかけています。ファームウェア上でDoomやTwitterなど、多くの興味深いアプリケーションを実行できます。
全体的なアーキテクチャを理解することで、これらのコンポーネントを頭の中で整理しやすくなります。既存のファームウェアの設計を調べることで、コンピューターの電源を入れるたびに展開される魅力的なプロセスについて理解を深めることができます。このトップダウンの視点は、各部分の役割を明確にするだけでなく、最新のファームウェア システムの高度で進化する性質も強調します。