FreeBSD
BIOS
UEFI

FreeBSDのブートプロセス

動機

気がつくとGPTディスクが当たり前のように普及していて、もちろん自分でも使っているのだが、FreeBSDがそこからどうブートするのか把握していないことに気がついたので情報整理。意外と一ヶ所にまとまった情報がないような気がする。

前提

  • FreeBSD 11.1-RELEASE-p9 amd641

用語

ディスク全体を論理的に区分した各領域を、この記事ではIBM PC互換機の慣例に基づき「パーティション」と呼ぶ2

概要

電源投入後まずBIOSに制御がわたり、設定されている優先順位にしたがってブートローダを探す。ディスクからブートローダを探すやり方は、BIOSの仕様(レガシーBIOSかUEFIか)によって異なる。レガシーBIOSの場合はMBRを読み、UEFIの場合はGPTを読むのが原則。ただしレガシーBIOSでもディスク上に書き込まれている情報次第で(つまりOS側の機能として)GPTディスクから起動させることは可能だし、UEFIの場合も互換性のためMBRから起動が可能になっている場合が多い。いずれにせよ、ディスク中の単純な場所からローダーを何段階かで読み込み、UFSまたはZFSのファイルシステムの中に存在する高機能なカーネルローダーを実行し、カーネルを初期化してユーザープロセスを起動するという段階を踏む。しかしFreeBSDの場合、BIOS・ディスク・ファイルシステムの組み合わせによって、ブートプロセスで使われるバイナリが下表のように異なっている。本記事では順にこれらの機能を見ていく。

BIOS ディスク ルートFS セクタ0 ステージ2 ステージ3
レガシー MBR UFS boot0など boot loader
レガシー MBR ZFS boot0など zfsboot zfsloader
レガシー GPT UFS pmbr gptboot loader
レガシー GPT ZFS pmbr zfsgptboot zfsloader
UEFI GPT UFS/ZFS (pmbr) boot1.efi loader.efi

レガシーBIOS経由

レガシーBIOSはディスクのセクタ0を0:7C00に読み込んで末尾2バイト(+01FE)にシグナチャ(0x55,0xAA)があることを確認する。シグナチャがある時だけ読み込んだセクタ0の先頭(+0000)のブートコードを実行し、シグナチャがなければ優先順位にしたがって他のメディアを探しにいく。BIOSが行うのはこれだけで、これ以降はセクタ0に書かれているブートコード次第でなんでもあり。ただ通常は、シグナチャの直前にパーティションテーブル(16バイト×4)が配置されており、この形式を持ったものをマスターブートレコード(Master Boot Record)と呼んでいる3。したがってブートコードは最大446バイトとなる。オリジナルのIBM PCのCPUは8088だったので、ブートコードはそれと互換性のある「リアルモード」で呼び出される。

アドレス(16進) サイズ 内容
+0000 446 ブートコード
+01BE 16 パーティションテーブル#1
+01CE 16 パーティションテーブル#2
+01DE 16 パーティションテーブル#3
+01EE 16 パーティションテーブル#4
+01FE 2 ブートシグナチャ(0x55,0xAA)

セクタ0のブートコード

FreeBSDでセクタ0に書き込まれるブートコードは以下の4種類ある。多分。

  • /boot/boot0
    簡単なメニューを表示して、選択されたパーティションの先頭セクタを読み込んで制御を移す。ソースコードはstand/i386/boot0にある。
  • /boot/boot0sio
    メニューの表示・選択にシリアルコンソールを利用する以外はboot0と同じ。ソースコードもboot0と共通だがマクロSIOが定義されてアセンブルされている。
  • /boot/mbr
    IBM PCで使われたものとほぼ同等のブートコード。4つのパーティションのうちブータブルフラグの立っているパーティション(アクティブパーティション)の先頭セクタを読み込んで制御を移す。ソースコードはstand/i386/mbrにある。
  • /boot/pmbr
    GPTディスクからの起動を行うブートコード。ソースコードはstand/i386/pmbrにある。

このうち/boot/boot0, /boot/boot0sio, /boot/mbrはMBRディスクから起動するためのOSを問わない汎用ブートコードで、起動パーティションの選択法が異なる以外は共通である。一方、/boot/pmbrはBIOS経由でGPTディスクからの起動を可能にするためのものだが、FreeBSD専用のブートコードである。

セクタ0のブートコードの切り替えには以下のいずれかのコマンドを用いる。詳しいことはそれぞれのmanページを参照のこと。

MBRディスクの場合

3ステージブートを行う。公式の情報源としては、FreeBSDハンドブックの12.2. FreeBSD Boot Process、より詳しくはFreeBSD Architecture HandbookのChapter 1. Bootstrapping and Kernel Initializationを参照。

ステージ0

ステージ0はディスクのセクタ0に書かれた汎用のブートコードのことで、厳密にはFreeBSDのブートプロセスの一部ではなく、他のブートセレクタ(grubとかNTLDRとか)を使っても全く問題がない。

boot0はディスク中の有効なパーティションの一覧をビデオコンソールに表示して、ユーザーからの選択を一定時間待ち、選択がなければデフォルトのパーティションの先頭セクタを読み込んで制御を移す。選択肢はファンクションキーのF1からF4が4つのパーティションに対応し、F5が次のディスク、F6がPXEブートになる。待つ時間やデフォルトのパーティションなどは、boot0cfg(8)で設定することができる。

boot0sioはメニューの表示・選択にシリアルコンソールを利用する点が異なり、ファンクションキーではなく数字の1から6で選択する。mbrにはメニュー機能がなく、暗黙の内にアクティブパーティションの先頭セクタを読み込んで制御を移す。

ステージ1とステージ2

ステージ1ローダはパーティションの先頭セクタ(Partition Boot Record, PBR)に置かれており、ステージ2ローダを読み込む働きをしている。一方ステージ2ローダはプロテクトモードに移行して、ファイルシステムからステージ3ローダを読み込むのが仕事である。ところが、起動パーティションがUFSの場合とZFSの場合とでステージ2ローダの置き場が異なるため、結果的にステージ1ローダも使い分ける必要がある。

UFSの場合、パーティションの先頭16セクタに/boot/bootが書き込まれている。このうち最初のセクタ(PBR)がステージ1ローダ、次のセクタがdisklabel (BSD label)であり、残り14セクタにステージ2ローダが書き込まれている。ZFSの場合は、パーティションの先頭セクタ(PBR)には/boot/zfsbootの先頭512バイトが書き込まれており、これがステージ1ローダになっている。残りの部分はステージ2ローダで、ステージ1ローダの1024セクタ後方に書き込まれている。

  • ステージ1
    起動ディスク中のFreeBSDパーティションのうち、アクティブなもの(それがなければ最初に見付かるもの)を探し、そこからステージ2ローダを読み込む。UFSの場合はboot1と呼び、これは上記パーティションの先頭16セクタを読み込んでステージ2に移行する。ZFSの場合はzfsldrと呼び、1024番セクタ以降を固定長(バージョンによって異なる)で読み込んでステージ2に移行する。
  • ステージ2
    BTXという簡素なプロテクトモード環境に移行して、ファイルシステムからELFバイナリを読み込んで実行する。UFSの場合はboot2と呼び、/boot/loaderを読み込む。ZFSの場合はzfsbootと呼び、/boot/zfsloaderを読み込む。どちらも動作を/boot.configファイルや対話的操作で変更することが可能。

/boot/bootの書き込みはbsdlabel -Bで行えるが、/boot/zfsbootの書き込みはddを使う必要がある(zfsboot(8)を参照)。

ステージ3

ステージ3は設定ファイルとして/boot/loader.rc/boot/loader.confなどを処理し、必要に応じて対話的な処理も行った上で、カーネルを読み込む。FORTH言語に基づくインタプリタ環境であり、かなり柔軟な制御を行うことができる。ZFS対応の有無により/boot/loader/boot/zfsloaderに区別されているが、それ以外はほぼ等価である。詳しいことはloader(8)を参照。

GPTディスクの場合

やはり3ステージブートを行うが、そのステージ分けはMBRディスクの場合とは異なる。なおGPTディスクの場合、規格上セクタ0にブートコードが書かれていることは期待できないが、レガシーBIOS経由で起動する場合にはそこにブートコードがあるものとして実行される。

ステージ1

/boot/boot0/boot/mbrが汎用のステージ0ローダであるのに対し、/boot/pmbrは同じくセクタ0に存在するもののFreeBSDの起動に特化したステージ1ローダである。また/boot/pmbrには/boot/boot0と違ってブートセレクタの機能はないので、ブートセレクタが必要ならば何か他に用意しなければいけない4

/boot/pmbrはGPTからfreebsd-bootパーティション(Type={{83BD6B9D-7F41-11DC-BE0B-001560B84F0F}})を探し、最初に見付かったパーティションの内容を全て読み込んで制御を移す。freebsd-bootパーティションは128番目までに存在している必要があり、またメモリ配置上の制約からサイズが545K以内でなければならない。一方でBIOSから認識可能である限り、ディスク上の場所に制約はない。

ステージ2

freebsd-bootパーティションの内容は、ルートファイルシステムがUFSの場合が/boot/gptboot、ZFSの場合が/boot/gptzfsbootである。これらのファイルの構造は/boot/boot/boot/zfsbootと類似しているが、しかしステージ1ローダである/boot/pmbrがパーティションの全てのセクタをすでに読み込んでいるため、改めてディスクにアクセスすることなくメモリ上で再配置するだけでプロテクトモードに移行し、ファイルシステムからステージ3ローダを読み込む。

gptboot(8)はGPTからfreebsd-ufsパーティション(Type={{516E7CB6-6ECF-11D6-8FF8-00022D09712B}})を探しマウントして/boot/loaderを読み込む。複数ある場合、bootmeアトリビュートがあるものを最初から順に探し、それがなければ単に最初のもの、という優先順位で起動される。また起動テスト用にbootonce/bootfailedアトリビュートが使われている。これはまずbootonceのみのパーティションはbootfailedに書き換える。その上でbootonce+bootmeのパーティションが最優先で見付かった順にbootmeを削除してから起動試行する。(起動失敗した場合、次回起動時にbootfailedに書き換わる)次にbootmeのみのパーティションを見付かった順に起動試行する。これらのフラグは起動後に/etc/rc.d/gptbootで管理される。

gptzfsboot(8)は可能な限り全てのfreebsd-zfsパーティション(Type={{516E7CBA-6ECF-11D6-8FF8-00022D09712B}})を探しimportして/boot/zfsloaderを読み込む。ブートしたディスクだけではなく、BIOSから見える全てのディスクと、そこにあるサポートしている全てのパーティション(GPTとMBR)が対象。まずブートしたディスクからはじめ、その後はBIOSが見つけた順となる。それぞれについて、ディスク全体がZFSになっているのでなければ、GPTの場合はfreebsd-zfsパーティションを、MBRの場合は全パーティションをチェックする。一番最初に見付かったプールがデフォルトプールとなる。そしてデフォルトプールのbootfsプロパティがブートファイルシステムを指定し、その中の/boot/zfsloaderが読み込まれる。

ステージ3

ステージ3については上記MBRディスクの場合と同じである。

UEFI経由

UEFIでは2ステージのブートプロセスとなっている。詳しくはUEFI(8)を参照。

UEFIはプロテクトモードで動作しており、セクタ0にリアルモードのブートコードが書かれていても使われない。代わりにGPTディスクのEFI System Partition (ESP; FAT32)に格納された、PEバイナリのプログラム(UEFIアプリケーション)を実行できる。デフォルトでは/EFI/BOOT/BOOTX64.EFIが実行されるが5、これはNVRAMで上書きでき、またNVRAM上に複数のパスが指定されていればその選択もできる。

2ステージブート

FreeBSDの起動に用いるUEFIアプリケーションは/boot/boot1.efi/boot/loader.efiの2つ
で、それぞれレガシーBIOS経由のステージ2と3に相当する。

boot1.efi

boot1.efiはプロテクトモードで動作し、ファイルシステムを理解してその中からステージ3ブートローダを読み込んで制御を移す。つまりレガシーBIOS経由のブートプロセスではステージ2に相当する。ソースコードはstand/efi/boot1にある。

簡便のため、このファイルを/EFI/BOOT/BOOTX86.EFIという名前で含んだFAT32のイメージファイル/boot/boot1.efifatが用意されており、これをESPとして書き込むことでブートの準備ができる。既存のESPにboot1.efiをコピーしてefivar(8)でNVRAMを設定しても良い。

boot1.efiはシステム中からfreebsd-ufsまたはfreebsd-zfsパーティションを探す。まずブートしたディスクからはじめ、ついでUEFIの優先順位にしたがう。同じディスクにfreebsd-ufsとfreebsd-zfsが両方あった場合にはzfsが優先される。こうして見付かったファイルシステムの中にある/boot/loader.efiを実行する。

loader.efi

loader.efiもUEFIアプリケーションであるが、その機能はステージ3ローダーである/boot/loader/boot/zfsloaderとほぼ同じである。ただしloader.efiはそれ自体がUFSとZFSに両対応していることが異なる。ソースコードはstand/efi/loaderにある。


  1. i386でもほぼ同様のはず 

  2. FreeBSDではMBRで管理された各領域を「スライス」と呼んでその内部をさらに「パーティション」(BSD partition)に区分していたが、GPTディスクではBSD partitionを使う必然性もなくなり各領域をパーティションと呼ぶことが多くなった。本記事にはこれ以降BSD partitionが登場しないので、簡便のためMBR管理下の領域もGPT管理下の領域も「パーティション」に統一している。 

  3. 余談だが、ブートコードの領域に他の管理情報を格納することもできる。たとえばパーティションテーブルを5つ以上配置することも可能で、実際そういう製品もあったようだ。ただ通常のOSやディスク操作ユーティリティはこうした非標準的な管理情報を取り扱えないので、普通は互換性やデータ保護の観点からそういうことはしない。もっともこれを逆手にとって、通常のOSからは存在が見えないパーティションからハイパーバイザを起動して、通常の起動パーティションからゲストOSとして起動する、みたいないたずらが可能だったりする。 

  4. UEFIではブートコードの格納に専用のパーティション(EFI System Partition)を使うため、GPTディスクの場合パーティションの先頭セクタにブートコードがあることを前提にできない。したがってGPT対応のブートセレクタは、MBRディスクの時のようにパーティション先頭セクタに丸投げすることができず、各OSのブートプロセスを把握している必要がある。それではセクタ0に納まりきるはずもなく、OS標準のブートローダとしては荷が重いと言えるだろう。複数あるfreebsd-bootパーティションから選択するくらいならできるかもしれないが…。 

  5. 64bitシステムの場合。32bitシステムでは/EFI/BOOT/BOOTX86.EFIである。