FreeBSDのブートプロセス

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

#前提
* FreeBSD 11.1-RELEASE-p9 amd64[^1]

[^1]: i386でもほぼ同様のはず

#用語
ディスク全体を論理的に区分した各領域を、この記事ではIBM PC互換機の慣例に基づき「[パーティション](https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B7%E3%83%A7%E3%83%B3)」と呼ぶ[^2]。

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

#概要
電源投入後まず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）が配置されており、この形式を持ったものを[マスターブートレコード](https://ja.wikipedia.org/wiki/マスターブートレコード)([Master Boot Record](https://en.wikipedia.org/wiki/Master_boot_record))と呼んでいる[^3]。したがってブートコードは最大446バイトとなる。オリジナルのIBM PCのCPUは8088だったので、ブートコードはそれと互換性のある「[リアルモード](https://ja.wikipedia.org/wiki/%E3%83%AA%E3%82%A2%E3%83%AB%E3%83%A2%E3%83%BC%E3%83%89)」で呼び出される。

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

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

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

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

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

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

* [gpart](https://www.freebsd.org/cgi/man.cgi?query=gpart&sektion=8) bootcode
* [fdisk](https://www.freebsd.org/cgi/man.cgi?query=fdisk&sektion=8) -B
* [boot0cfg](https://www.freebsd.org/cgi/man.cgi?query=boot0cfg&sektion=8) -B

##MBRディスクの場合
3ステージブートを行う。公式の情報源としては、FreeBSDハンドブックの[12.2. FreeBSD Boot Process](https://www.freebsd.org/doc/handbook/boot-introduction.html)、より詳しくはFreeBSD Architecture Handbookの[Chapter 1. Bootstrapping and Kernel Initialization](https://www.freebsd.org/doc/en/books/arch-handbook/boot.html)を参照。

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

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

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

###ステージ1とステージ2
ステージ1ローダはパーティションの先頭セクタ([Partition Boot Record](https://en.wikipedia.org/wiki/Partition_boot_record), PBR)に置かれており、ステージ2ローダを読み込む働きをしている。一方ステージ2ローダは[プロテクトモード](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%86%E3%82%AF%E3%83%88%E3%83%A2%E3%83%BC%E3%83%89)に移行して、ファイルシステムからステージ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](https://svnweb.freebsd.org/base/head/stand/i386/boot2/boot1.S?view=markup)と呼び、これは上記パーティションの先頭16セクタを読み込んでステージ2に移行する。ZFSの場合は[zfsldr](https://svnweb.freebsd.org/base/head/stand/i386/zfsboot/zfsldr.S?view=markup)と呼び、1024番セクタ以降を固定長（バージョンによって異なる）で読み込んでステージ2に移行する。
* ステージ2  
[BTX](https://svnweb.freebsd.org/base/head/stand/i386/btx/btx/btx.S?view=markup)という簡素なプロテクトモード環境に移行して、ファイルシステムからELFバイナリを読み込んで実行する。UFSの場合は[boot2](https://svnweb.freebsd.org/base/head/stand/i386/boot2/boot2.c?view=markup)と呼び、`/boot/loader`を読み込む。ZFSの場合は[zfsboot](https://svnweb.freebsd.org/base/head/stand/i386/zfsboot/zfsboot.c?view=markup)と呼び、`/boot/zfsloader`を読み込む。どちらも動作を`/boot.config`ファイルや対話的操作で変更することが可能。

`/boot/boot`の書き込みは`bsdlabel -B`で行えるが、`/boot/zfsboot`の書き込みは`dd`を使う必要がある（[zfsboot(8)](https://www.freebsd.org/cgi/man.cgi?query=zfsboot&sektion=8)を参照）。

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

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

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

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

`/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)](https://www.freebsd.org/cgi/man.cgi?query=gptboot&sektion=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)](https://www.freebsd.org/cgi/man.cgi?query=gptzfsboot&sektion=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)](https://www.freebsd.org/cgi/man.cgi?query=uefi&sektion=8)を参照。

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

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

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

###boot1.efi
`boot1.efi`はプロテクトモードで動作し、ファイルシステムを理解してその中からステージ3ブートローダを読み込んで制御を移す。つまりレガシーBIOS経由のブートプロセスではステージ2に相当する。ソースコードは[stand/efi/boot1](https://svnweb.freebsd.org/base/head/stand/efi/boot1/boot1.c?view=markup)にある。 

簡便のため、このファイルを/EFI/BOOT/BOOTX86.EFIという名前で含んだFAT32のイメージファイル`/boot/boot1.efifat`が用意されており、これをESPとして書き込むことでブートの準備ができる。既存のESPに`boot1.efi`をコピーして[efivar(8)](https://www.freebsd.org/cgi/man.cgi?query=efivar&sektion=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](https://svnweb.freebsd.org/base/head/stand/efi/loader)にある。