入門 Linux From Scratch
はじめに
普段私たちが利用している Ubuntu などの Linux ディストリビューションは、インストーラーやパッケージマネージャーによって構築、管理が自動化されています。ユーザーが複雑な設定を意識する必要はなくなりますが、その反面、構築、管理として具体的に何が行われているかが見えにくくなっています。
本記事では、Linux From Scratch (LFS) の手順に従い、与えられたソースコードのみから Linux システムを構築します。日本語訳も用意されていますが、それを読んでなお分かりづらい部分があったので、詰まった部分やややこしい部分などを共有しながら、LFS を解説したいと思います。
LFS は、既存ディストロに頼らず、上流のソースコードから Linux システム一式を自分で組み上げてみるためのドキュメントです。本記事はその全手順をなぞるものではなく、特に以下のポイントにフォーカスした補助的な解説になっています。
- ビルド/ホスト/ターゲットの整理
- なぜアーキテクチャが同じなのにクロスコンパイルをするのか
- クロスツールチェーン(binutils, glibc, GCC まわり)の依存関係と、その循環をどうやって解消しているか
- 2 回目の binutils/GCC のビルドの意味と、どの時点からホスト環境への依存が切れるのか
- 最後に BIOS ではなく UEFI ブートをしたいときに、LFS と BLFS のどこを見ればよいか、バージョン違いでハマりやすいポイント
想定している読者
- LFS の名前は知っていて、一度やってみたいと思っている人
- LFS 日本語訳を読み進めていて、クロスツールチェーンや chroot まわりの説明で迷子になった人
- 自作 Linux やディストロの成り立ちに興味があり、クロスツールチェーンの全体像を一度きちんと整理しておきたい人
- UEFI 環境で LFS を起動したくて、LFS 本編と BLFS のどちらをどう参照すればよいか知りたい人
この記事でやること/やらないこと
-
やること
- LFS 12.4 の構成に沿って、クロスツールチェーン → chroot 環境 → 最終システム構築の流れを図や独自の記法を使って整理します。
- 各ツールが、どのマシン上でビルドされ、どのマシンで動くバイナリなのか、を意識できるように、ビルド/ホスト/ターゲットを明確にして説明します。
- UEFI ブートをしたい場合に、どこまでが LFS 本編の範囲で、どこから BLFS の手順を読む必要があるのかを簡単に触れます。
- UEFI ブートをしたい場合に、
grub.cfgでハマりやすい点を紹介します。
-
やらないこと
- 各パッケージの
./configure && make && make installの具体的なオプションや、パーティションレイアウトなどの細かい手順は、本家ドキュメントに任せます。 - 本記事だけを読んで LFS を完遂できるようにすることは目的にしていません。あくまで LFS ブックを読み進めるときの理解を補うためのメモです。
- 各パッケージの
読み方の目安
- まずは LFS 本家または日本語訳を軽く通読し、どんな流れでシステムを構築するのかを把握してください。
- クロスツールチェーンや chroot の章を読み進めていて、
- 今は pc 側の話をしているのか、これから作る lfs 側の話をしているのか分からなくなってきた
- この binutils/GCC のビルドは何回目で、どこが違うのか整理したい
と感じたタイミングで、本記事の「詳細」以降を読むと理解しやすくなると思います。
- UEFI ブートをしたい場合は、LFS 本編を grub 設定前まで一通り終えたあとで、最後の「UEFI ブートの設定」の章と BLFS の該当ページを一緒に確認してください。
LFS とは?
Linux From Scratch (LFS) is a project that provides you with step-by-step instructions for building your own custom Linux system, entirely from source code.
LFS (Linux From Scratch) は、特定のディストリビューションに依存せず、上流のソースコードから直接 Linux システムを構築するための手順を示したドキュメントです。
ドキュメントに沿って作業を進めると、コンパイラ、ライブラリ、カーネルなど、システムを構成するツールをすべて自分自身の手でコンパイルし、インストールしていくことになります。
普段 apt や pacman のようなパッケージマネージャを使ってツールをインストールするのとは全く異なり、依存関係の解決も含めてすべて手動で行います。これにより、Linux システムがどのようにして成り立っているかについて生きた知識を得ることができます。
派生として、グラフィック環境まで用意する BLFS(Beyond LFS) などもあります。
自分は試していませんが、ホストのブートローダーの設定を汚さずに qemu で行う方法もあるようなので、ホストのブートローダーは触りたくないが予備のディスクを持っていない人などは、こちらを参考にすると良いかもしれません。
準備
まず、ドキュメントを見つけるのが少し難しいです。Linux From Scratch で検索するとホームページが見つかりますが、Home, Wiki, Website Mirrors などには最新のドキュメントへのリンクは見つかりません。一番わかりやすいのは、News にあるリンクを踏むことです。
本当にありがたいことに LFS には日本語訳があります。手順や操作は基本的にこちらを参照すれば全く問題ありません。おそらく日本語情報を用意することに重きをおいているため、直訳っぽい言葉になっていて意図が不明確な箇所が一部存在します。そのようなときは原文の英語を確認しに行きましょう。
それでは、ドキュメントを見ながら LFS をやってみましょう。
現在の最新版である LFS 12.4 に沿って説明します。sysvinit 版(systemd 版ではない方)を前提に話をします(クロスツールチェーン周りの流れはどちらもほぼ同じです)。本記事の構成は基本的に本家の構成と対応しています。
全体像
LFS のブック全体は大きく見ると、
- ホスト環境の準備
- 一時的なクロスツールチェーンの構築
- chroot 後のベースシステム構築
- システム設定とブートローダのインストール
という流れになっています。本記事では、特に 2. と 3. のあたりを中心に補足していきます。
まず、普段利用しているディストリビューション上に作業用ディレクトリやパーティションを用意し、必要なソースコード一式を展開します。ここで使っている既存の環境が、LFS でいうところの「ホストシステム」に相当します。
次に、ホストに依存しない LFS システムを作るために、一時的なクロスツールチェーンを構築します。アーキテクチャ自体は同じであっても、ホストとは別の環境向けにビルドしているとみなすことで、最終的にホストから独立したシステムを得る、というのが LFS の重要な考え方です。この段階では binutils や glibc, GCC などのツールを、機能を一部制限しながら用意していきます。
そのクロスツールチェーンを使って最低限必要なツール群をビルドし、chroot でこれから起動する LFS システムの中に入ったつもりになって作業を続けます。ここで binutils や GCC をビルドし直すことで、ビルドに必要なツールチェーンがホスト側に依存しなくなり、LFS システムの内側だけで完結するようになります。この境界が分かりづらいので、本記事ではビルド/ホスト/ ターゲットを整理しながら説明します。
その後は、bash や coreutils などの基本的なツールや各種ライブラリを順にビルドしていき、LFS システム上でテストを実行して、環境が正しく構築できているかを確認します。ここまでくると、普段ディストリビューションが裏側でやってくれているベースシステムの構築を自分の手で一通りなぞったことになります。
最後に、各種設定ファイルを整え、カーネルとブートローダをインストールすれば、ディスクから起動できる Linux システムが完成します。本家 LFS のブックは BIOS ブートを前提としていますが、UEFI での起動を行いたい場合は BLFS の該当章も合わせて参照する必要があります。このあたりの具体的な注意点については、記事の後半で触れます。
詳細
LFS ブックを読み、書いてあるとおりに操作をすれば、作業自体は間違いなく進められますが、なぜその操作を行うのか?本当に必要な操作なのか?などの本質的な理解を得ることができません。
この章では、そこに焦点を当てて説明を行います。
用語の説明
まずは、ここから説明の際に用いる用語や記法などについて定義しておきます。
ビルド、ホストはプログラム全般に対して定義される用語です。
- ビルド
そのプログラムのビルド、コンパイルを行うマシン - ホスト
コンパイルの結果できたバイナリが動作するマシン
ターゲットは、コンパイラのみに対して定義される用語とされていますが、本記事では説明を簡単にするため、リンカやアセンブラのようなバイナリを扱うツールに対しても定義されるとします。
- ターゲット
コンパイル(、リンク、アセンブル)の結果できるバイナリが動作するマシン
つまり、プログラムの視点から
- ネイティブコンパイラによってコンパイルされたので、ビルドとホストが一致する
- クロスコンパイラによってコンパイルされたので、ビルドとホストが異なる
という状況をコンパイラの視点から見ると、
- ネイティブコンパイラは、ホストとターゲットが一致する
- クロスコンパイラは、ホストとターゲットが異なる
となります。これはネイティブコンパイラとクロスコンパイラの定義でもありますね。
実際には x86_64-pc-linux-gnu のようなトリプレットという文字列で ビルド/ホスト/ターゲットが表現されます。LFS では $LFS_TGT として x86_64-lfs-linux-gnu のような値を使い、これがターゲットを表します。
また、これからの説明には以下のような表記を用います。
- LFS を行う環境、つまり普段使っている環境を示すときは、
pcと表記します。 - LFS 完了後に起動する環境、つまり今から構築する LFS 環境を示すときは
lfsと表記します。 - ツール
AがツールBに依存していることをA → Bのように表記します。(e.g.,glibc → gcc)- 実際には mermaid 図を用いていますが、その図の矢印も同じ意味で使っています。
- ツール
AのホストがマシンBであることをA(B)と表記します。(e.g.,glibc(pc)) - コンパイラ
Cのホストがhostで、ターゲットがtargetのとき、C(host→target)のように表記します。(e.g.,gcc(pc→lfs))
| 記法 | 意味 |
|---|---|
glibc(pc) |
pc 上で動く glibc |
gcc(pc→lfs) |
pc で動いて lfs 向けバイナリを吐く gcc |
bash(pc) → glibc(pc) |
pc 上で動く bash は pc 上の glibc に依存する |
クロスツールチェーンの構築
この節のすべてをすぐに理解する必要はありません。 この先、実際の作業を行っていけば、いろいろな情報が明らかになってくるはずです。
とありますが、ここで理解しないとただコマンドを叩き続けるだけの作業になってしまいます。
LFS の説明を軽く読むだけでは理解するのは難しいと思います。特に、ドキュメントの説明が今ホスト環境について言及しているのかターゲット環境について言及しているのかが分かりづらく迷子になりやすいと思いました。ここではそれをより明確にして説明をします。
ここで必要なツールチェーンは C/C++ で書かれているものがほとんどなので、以下の議論は C/C++ に限っています。
ここでは区別のため、大文字の GCC をパッケージ、gcc をコマンド名として書き分けます。
なぜクロスツールチェーンが必要か
クロスツールチェーンとは、クロスコンパイルをするために必要なツール群のことを指します。具体的には、リンカ、標準ライブラリ、コンパイラなどです。
まず最初に、なぜクロスツールチェーンを作る必要があるのかを理解するべきです。個人で LFS を行う場合、LFS を行う環境と使う環境でアーキテクチャが異なることはほぼないと思われます。ではクロスツールチェーンは必要ないのでしょうか?
そんなことはありません。LFS の最終的な目標を思い出すと、今動いている環境に依存しない、ゼロから作られた独立した環境を作ることです。つまり LFS では、実際にはアーキテクチャとしてホストとターゲットが一致していても、一致していないと思い込みます。そして"見せかけ"のクロスコンパイルを行います。そうしないとホストの環境に依存しているということになってしまうからです。
依存関係は循環しないのか
この章の目的を具体的なツール名を用いて一言で表すと、binutils(pc→lfs), glibc(lfs), GCC(pc→lfs)(gcc(pc→lfs), libgcc(lfs), libstdc++(lfs)) を用意することです。
これらのツールのビルド時、実行時の依存関係を考えます。
前提として binutils(pc→pc), glibc(pc), gcc(pc→pc), libgcc(pc), libstdc++(pc) は存在するものとします。
- binutils: ld, as などが含まれる
- glibc: C 標準ライブラリ
- gcc: C/C++ のコンパイラ。GCC パッケージに含まれる
- libgcc: gcc が内部で使うライブラリ。GCC パッケージに含まれる
- libstdc++: C++ 標準ライブラリ。GCC パッケージに含まれる
from scratch と言いつつ強い仮定だと思ったかもしれませんが、上流のソースコードをコンパイルする必要はあるので、必要な仮定です。
用意する binutils(pc→lfs) について、リンカ ld(pc→lfs) などが含まれますが、このようなツールは glibc(lfs) に依存しないため、そこまで難しい話ではありません。gcc(pc→pc) でビルドする際に適切なオプションをつけて、ライブラリなどの探索パスを lfs 上のパスに設定し、lfs 向けのコードを生成するように設定するだけで良いです。
-
--with-sysroot=$LFS: ライブラリの探索などを$LFSなどから行うようにします。 -
--target=$LFS_TGT: ld などが$LFS_TGT向けのコードを生成するようにします。
残りのパッケージは単純に考えると依存関係が循環してしまいます。glibc(lfs) は gcc(pc→lfs) によってビルドします。 gcc(pc→lfs) を完全に機能させるには libgcc(lfs), libstdc++(lfs) が必要です。しかしどちらも通常 glibc(lfs) の機能に依存しており、libstdc++(lfs) は libgcc(lfs) にも依存しています。
また、そもそも実行時の動的リンクや共有ライブラリという仕組み自体、glibc があるから成り立つものです。
つまり、gcc(pc→lfs) の lfs 上の依存関係は以下のようになっています。
これでは依存関係が循環してしまいクロスツールチェーンを用意できません。
しかし実は、gcc(pc→pc) によって GCC(pc→lfs) をビルドするときに特定のオプションをつけると、gcc(pc→lfs) と libgcc(lfs) の機能を制限する代わりにこの依存関係の循環を解消することができます。
-
--with-newlib: libgcc の glibc 依存部分を使用しない -
--disable-shared: libgcc を gcc に静的リンクする -
--disable-libstdcxx: そもそも libstdc++ を作らない
これらのオプションによって、上の依存関係は以下のように変化します。
これで依存関係が循環することなく、機能は不完全ですが gcc(pc→lfs), libgcc(lfs) を用意することができます。具体的にはスレッドや例外処理などの機能が欠けています。コンパイラが動くようになれば、あとは Linux API headers と呼ばれるカーネルの C 言語 API を示すヘッダファイルを配置すれば glibc(lfs) はビルドすることができます。意外にも、gcc(pc→lfs), libgcc(lfs) の機能が不完全でも glibc(lfs) の機能は不完全とはなりません。glibc 自体のビルドは、スレッドや例外などの一部の高度な機能がなくても通るように設計されているためです。あとは GCC パッケージから libstdc++(lfs) を取り出し、gcc(pc→lfs), libgcc(lfs) とは独立にビルドすれば良いです。こちらは機能制限の影響を受けます。
これらの手順を踏むことで、クロスツールチェーンをすべて用意することができます。
ただし、binutils(pc→lfs), gcc(pc→lfs) は pc に依存しているため、また、libgcc(lfs), libstdc++(lfs) は不完全であるため、これらは最終的な LFS 環境には含めません。これを解消するのが次のステップです。
chroot 環境の構築
クロスツールチェーンを用意した目的は、pc から独立した環境を作ること、つまり、lfs がホストのバイナリで環境を作り、それだけで完結することです。現状では、binutils(pc), gcc(pc→lfs) が pc に依存しています。
この章の目的は、binutils(lfs→lfs) を作ったうえで、不完全な gcc(pc→lfs), libgcc(lfs), libstdc++(lfs) から完全な gcc(lfs→lfs), libgcc(lfs), libstdc++(lfs) を用意することです。
Linux From Scratch の説明を順番に読むと、gcc(lfs→lfs) の依存関係である M4(lfs) や、次の chroot 環境を作るために必要なパッケージ bash(lfs) 等を先に準備していますが、それら個別の説明は読めばわかることなので本記事では説明しません。
重要なのは、2回目の binutils, GCC のビルドです。と言っても先程ほど複雑ではありません。
binutils のビルド
既に完全な glibc(lfs) が用意されているため、共有ライブラリを作成、使用することができます。gcc(pc→lfs) でビルドすることで、共有ライブラリも含めて binutils(lfs→lfs) を作ることができます。
これらはこれから chroot する最終的な LFS 環境に含まれます。
GCC のビルド
既に完全な glibc(lfs) が用意されているため、gcc(pc→lfs) による GCC(lfs→lfs) のコンパイル時に完全な libgcc(lfs) を作成することができます。libstdc++(lfs) は新しく作られた完全な libgcc(lfs) を使用するように設定することで完全な状態になります。これらのライブラリが完全な状態になれば、完全な gcc(lfs→lfs) ができます。
-
LDFLAGS_FOR_TARGET=-L$PWD/$LFS_TGT/libgcc: libstdc++ が今回ビルドされた新しい libgcc を使うようにする
以上の処理を行うことで、ビルドに必要な全てのツールチェーンから pc への依存がなくなり、完全に隔離された chroot 環境に入る準備が整います。
最終システムの構築
ここまで丁寧にやってようやく純粋に自身のシステムで依存が完結する環境が構築できます。パッケージマネージャーはないため、あとは必要なパッケージを依存関係の順番通りに一つ一つ真心を込めて構築するのみです。
LFS ブックを見ると、binutils(lfs→lfs), glibc(lfs), GCC(lfs→lfs) など、既にあるものを再コンパイルしていることに疑問を抱くかもしれません。これには主に2つの理由があります。
1つは、既にあるこれらのツールはクロスコンパイルによって作られたものなので、システムの安定化のためにネイティブコンパイラでコンパイルし直すという理由です。
もう1つは、lfs 環境が正しく構築できているかについて、ここまでは簡単なチェックしかなかったものを、ようやく全体的なテストができるから、そのテストを行うためにビルドし直すという理由です。ここで初めてミスが発覚したらかなり絶望ですが、やらないわけにはいかないですね…
ここまで順調に行けば、あとはシステム設定をするのみです。ここは本家にも十分な説明があるため、本記事では割愛します。
UEFI ブートの設定
LFS では BIOS ブートのみ説明されていて、UEFI ブートの手順は BLFS に委ねられているので、BLFS 側の対応するページを見に行かなければいけません。
基本的には BLFS 側のみを参照していれば良いです。grub.cfg の記法に関しての説明は LFS 側でなされているので、適宜参照すると良いです。
ここで一応頭の片隅に入れておいてほしいのは、LFS と BLFS で使用している Linux のバージョンが一致しているとは限らないということです。今回説明の基準にしている 12.4 では問題ないと思いますが、自分が LFS をやったバージョン 12.3 では、LFS は Linux-6.13.4 で BLFS は Linux-6.13.2 でした。grub.cfg にこれをハードコードする箇所があるので、BLFS のみを参考にして grub.cfg を書くとブートできずに困った記憶があります。
また、LFS にも注意書きがありますが、/boot を別パーティションで作る場合は、grub.cfg の menuentry 内に記述するパスが /boot 基準になることにも注意しましょう。
ここまでの手順をすべて乗り越えて最後に再起動するときが、一番手に汗握る瞬間ですね...。無事起動できれば、LFS は完了です。恒例の neofetch(fastfetch) がすぐにできないのはちょっともどかしいですが、今までと同様にインストールすれば良いだけですので、ぜひやってみましょう!(自分はやっていません)
再起動した結果残念ながら起動しなくても、最終システムの構築時に行ったテストがだいたいうまくいっていれば、原因はそこではなくブートプロセスにあるでしょう。落ち着いて Linux や grub の設定を見直しましょう。
おわりに
本記事では、LFS の流れに沿いながら、ビルド/ホスト/ターゲットの整理、見せかけのクロスコンパイルを行う理由、クロスツールチェーンの依存関係、二回目の binutils/GCC のビルドの意味、そして最後の UEFI ブートまわりの注意点までをまとめて説明しました。特に、どのツールが pc 側に依存していて、どこから lfs 側だけで完結するようになるのかを意識できるように書いたつもりです。
実際にやってみたときも、一番迷ったのはクロスツールチェーンの章と、ブートローダ設定の細かい部分でした。ホストとターゲットのどちらの話をしているのか、今作っているバイナリがどこで動くのかが見えなくなると一気にわからなくなるので、そのあたりを補うことを意識しています。
本記事の内容が、LFS ブックを読み進めるときの補助として、単に手順をなぞるだけでなく、なぜそのコマンドを実行しているのかを考えるきっかけになれば幸いです。
Discussion