ii. ツールチェーンの技術的情報

本節ではシステムをビルドする原理や技術的な詳細について説明します。 この節のすべてをすぐに理解する必要はありません。 この先、実際の作業を行っていけば、いろいろな情報が明らかになってくるはずです。 各作業を進めながら、いつでもこの節に戻って読み直してみてください。

第 5 章第 6 章 の最終目標は一時的なシステム環境を構築することです。 この一時的なシステムはシステム構築のための十分なツール類を有していて、ホストシステムとは切り離されたものです。 この環境へは chroot によって移行します。この環境は 第 8 章 において、クリーンでトラブルのない LFS システムの構築を行う土台となるものです。 構築手順の説明においては、初心者の方であっても失敗を最小限にとどめ、同時に最大限の学習材料となるように心がけています。

ビルド過程は クロスコンパイル を基本として行います。 通常クロスコンパイルとは、ビルドを行うマシンとは異なるマシン向けにコンパイラーや関連ツールチェーンをビルドすることです。 これは厳密には LFS に必要なものではありません。 というのも新たに作り出すシステムは、ビルドに使ったマシンと同一環境で動かすことにしているためです。 しかしクロスコンパイルには大きな利点があって、クロスコンパイルによってビルドしたものは、ホスト環境上にはまったく依存できないものとなります。

クロスコンパイルについて

[注記]

注記

LFS はクロスツールチェーン(あるいはネイティブツールチェーン)のビルドを説明する書ではなく、その説明は行っていません。 クロスツールチェーンは、LFS のビルドとは異なる別の目的で用いるものであるため、何を行っているのかが十分に分かっていないまま、クロスチェーン向けのコマンドを利用することは避けてください。

クロスコンパイルには必要な捉え方があって、それだけで 1 つの節を当てて説明するだけの価値があるものです。 初めて読む方は、この節を読み飛ばしてかまいません。 ただしビルド過程を十分に理解するためには、後々この節に戻ってきて読んで頂くことをお勧めします。

ここにおいて取り上げる用語を定義しておきます。

ビルド(build)

ビルド作業を行うマシンのこと。 このマシンは "ホスト(host)" と呼ぶこともあります。

ホスト(host)

ビルドされたプログラムを実行するマシンまたはシステムのこと。 ここでいう "ホスト" とは、他の節でいうものと同一ではありません。

ターゲット(target)

コンパイラーにおいてのみ用いられます。 コンパイラーの生成コードを必要とするマシンのこと。 これはビルドやホストとは異なることもあります。

例として以下のシナリオを考えてみます。 (これはよく "カナディアンクロス(Canadian Cross)" とも呼ばれるものです。) コンパイラーが低速なマシン上にだけあるとします。 これをマシン A と呼び、コンパイラーは ccA とします。 これとは別に高速なマシン(マシン B)があって、ただしそこにはコンパイラーがありません。 そしてここから作り出すプログラムコードは、まったく別の低速マシン(マシン C)向けであるとします。 マシン C 向けにコンパイラーをビルドするためには、以下の 3 つの段階を経ることになります。

段階 ビルド ホスト ターゲット 作業
1 A A B マシン A 上の ccA を使い、クロスコンパイラー cc1 をビルド。
2 A B C マシン A 上の cc1 を使い、クロスコンパイラー cc2 をビルド。
3 B C C マシン B 上の cc2 を使い、コンパイラー ccC をビルド。

マシン C 上で必要となる他のプログラムは、高速なマシン B 上において cc2 を用いてコンパイルすることができます。 マシン B がマシン C 向けのプログラムを実行できなかったとすると、マシン C そのものが動作するようにならない限り、プログラムのビルドやテストは一切できないことになります。 たとえば ccC においてテストスイートを実行するには、以下の 4 つめの段階が必要になります。

段階 ビルド ホスト ターゲット 作業
4 C C C マシン C 上にて ccC を使い ccC そのものの再ビルドとテストを実施。

上の例において cc1 と cc2 だけがクロスコンパイラーです。 つまりこのコンパイラーは、これを実行しているマシンとは別のマシンに対するコードを生成できるものです。 これに比べて ccA と ccC というコンパイラーは、実行しているマシンと同一マシン向けのコードしか生成できません。 そういうコンパイラーのこをネイティブ コンパイラーと呼びます。

LFS におけるクロスコンパイラーの実装方法

[注記]

注記

本書におけるクロスコンパイルパッケージは、すべて autoconf ベースのビルドシステムを利用しています。 この autoconf ベースのビルドシステムは、システムのタイプとして cpu-vendor-kernel-os という形式のシステムトリプレット (system triplet) を用いています。 ベンダー項目はたいていは正しくないため、autoconf では無視されます。

よく考えてみると、4 つの項目からなる名前なのに、どうして3 つの組 (triplet)というのでしょう。 カーネルと OS の各項目は、元はシステム (system)項目に由来しています。 したがって 3 つの項目からなる書式が、今も有効に扱われるシステムもあります。 たとえば x86_64-unknown-freebsd です。 異なる 2 システム間で同一カーネルを共有することも不可能ではありませんが、それにしても同一トリプレットとするには、あまりにも差異がありすぎます。 たとえば携帯端末で動作する Android は ARM64 サーバー上で動作する Ubuntu とでは、確かに同一の CPU タイプ (ARM64) であり、同一カーネル (Linux) を使っているとは言っても、明らかに違います。

エミュレーション層を設けない限り、携帯端末上にサーバー向け実行モジュールを起動することはできません。 この逆の場合も同様です。 そこでシステム (system)フィールドは kernel と os に分けられ、システムを明確に指定するようになりました。 たとえば上の例における Android システムは aarch64-unknown-linux-android と指定され、Ubuntu システムは aarch64-unknown-linux-gnu と指定されるようになります。

トリプレット という名前が辞書に残っただけです。 システムのトリプレットを確認する一番簡単な方法は、config.guess スクリプトを実行することです。 これは多くのパッケージのソースに含まれています。 binutils のソースを伸張(解凍)し、この ./config.guess スクリプトを実行して、その出力を確認してください。 たとえば 32 ビットのインテルプロセッサーであれば、i686-pc-linux-gnu と出力されます。 64 ビットシステムであれば x86_64-pc-linux-gnu となります。 ほとんどの Linux システムでは、より簡単に gcc -dumpmachine コマンドを実行すれば、同様の情報を得ることができます。

またプラットフォームのダイナミックリンカーの名前にも注意してください。 これはダイナミックローダーとも呼ばれます。 (binutils の一部である標準リンカー ld とは別ものですから混同しないでください。) ダイナミックリンカーは glibc パッケージによって提供されているもので、何かのプログラムが必要とする共有ライブラリを検索しロードします。 そして実行できるような準備を行って、実際に実行します。 32 ビットインテルマシンに対するダイナミックリンカーの名前は ld-linux.so.2 となります。 (64 ビットシステムであれば ld-linux-x86-64.so.2 となります。) ダイナミックリンカーの名前を確実に決定するには、何でもよいのでホスト上の実行モジュールを調べます。 readelf -l <name of binary> | grep interpreter というコマンドを実行することです。 出力結果を見てください。 どのようなプラットフォームであっても確実な方法は、shlib-versions というファイルを見てみることです。 これは glibc ソースツリーのルートに存在しています。

LFS ではクロスコンパイルに似せた作業を行うため、ホストのトリプレットを多少調整します。 LFS_TGT 変数において "vendor" 項目を変更します。 またクロスリンカーやクロスコンパイラーを生成する際には --with-sysroot オプションを利用します。 これはホスト内に必要となるファイルがどこにあるかを指示するものです。 第 6 章 においてビルドされる他のプログラムが、ビルドマシンのライブラリにリンクできないようにするためです。 以下の 2 段階は必須ですが、最後の 1 つはテスト用です。

段階 ビルド ホスト ターゲット 作業
1 pc pc lfs pc 上の cc-pc を使い、クロスコンパイラー cc1 をビルド。
2 pc lfs lfs pc 上の cc1 を使い、クロスコンパイラー cc-lfs をビルド。
3 lfs lfs lfs lfs 上の cc-lfs を使い cc-lfs そのものの再ビルドとテストを実施。

上の表において "pc 上の" というのは、すでにそのディストリビューションにおいてインストールされているコマンドを実行することを意味します。 また "lfs 上の" とは、chroot 環境下にてコマンドを実行することを意味します。

話はまだまだあります。 C 言語というと単にコンパイラーがあるだけではなく、標準ライブラリも定義しています。 本書では glibc と呼ぶ GNU C ライブラリを用いています(別の選択肢として "musl" があります)。 このライブラリは lfs マシン向けにコンパイルされたものでなければなりません。 つまりクロスコンパイラー cc1 を使うということです。 しかしコンパイラーには内部ライブラリというものがあって、アセンブラー命令セットだけでは利用できない複雑なサブルーチンが含まれます。 その内部ライブラリは libgcc と呼ばれ、完全に機能させるには glibc ライブラリにリンクさせなければなりません。 さらに C++ (libstdc++) に対する標準ライブラリも、glibc にリンクさせる必要があります。 このようなニワトリと卵の問題を解決するには、まず libgcc に基づいた低機能版の cc1 をビルドします。 この cc1 にはスレッド処理や例外処理といった機能が含まれていません。 その後に、この低機能なコンパイラーを使って glibc をビルドします。 (glibc 自体は低機能ではありません。) そして libstdc++ をビルドします。 libstdc++ もやはり、libgcc の機能がいくつか欠如しています。

上の段落における結論は以下のようになります。 グレードの落ちた libgcc を使っている以上、cc1 からは完全な libstdc++ はビルドできないということです。 しかし第 2 段階においては、C/C++ ライブラリをビルドできる唯一のコンパイラーです。 第 2 段階でビルドしたコンパイラー cc-lfs を、そういったライブラリビルド用として即座には利用しない理由が 2 つあります。

  • 一般的に cc-lfs は PC(ホストシステム)上で動作させることはできません。 PC と LFS のトリプレットに互換性があったとしても、LFS 向けの実行ファイルは Glibc-2.38 に依存していなければなりません。 一方ホストディストロは、異なる libc 実装(たとえば musl)や古い Glibc(たとえば glibc-2.13)を利用しているかもしれません。

  • PC 上において cc-lfs が動作できたとしても、それを使い続けると、その PC 上のライブラリにリンクしてしまうリスクがあります。 これは cc-lfs がネイティブコンパイラーであるからです。

そこで libstdc++ は、2 回めの gcc の一部として再ビルドしないといけません。 そこで GCC 2 回めのビルドにあたっては、cc1 を使って libgcc と libstdc++ を再ビルドするように指示します。 ただしこのとき、libstdc++ がリンクされるのは、デグレードした古い libgcc ではなく、新たに再ビルドされた libgcc です。 こうして libstdc++ は再ビルドによって完全な機能を備えることになります。

第 8 章 (つまり3 回め) において、LFS システムに必要なパッケージがすべてビルドされます。 それまでの章において、特定のパッケージがたとえ LFS システムにインストールされていても、再ビルドをし続けます。 そのようにしてパッケージを再ビルドする最大の理由は、そのパッケージを安定させるためです。 完全に仕上がった LFS システムに、どれかの LFS パッケージを再インストールしたとしたら、その際にインストールされる内容は、第 8 章 において初めてインストールされるものと、全く同一でなければなりません。 第 6 章第 7 章 においてインストールする一時的なパッケージでは、この要件を満たしません。 なぜならそういったものに対しては、任意の依存パッケージを含めずにビルドしているからです。 また 第 6 章 において autoconf が行う機能チェックは、クロスコンパイルが原因で一部が適切に行われません。 そうなると一時パッケージには、オプション機能がコンパイルされなかったり、最適化が不十分なコードルーチンが用いられたりすることがあります。 さらにパッケージ再ビルドのもう一つの理由は、テストスイートを実行するためです。

その他の手順詳細

クロスコンパイラーは、他から切り離された $LFS/tools ディレクトリにインストールされます。 このクロスコンパイラーは、最終システムに含めるものではないからです。

binutils をまず初めにインストールします。 この後の gcc や glibc の configure スクリプトの実行ではアセンブラーやリンカーに対するさまざまな機能テストが行われるためで、そこではどの機能が利用可能または利用不能であるかが確認されます。 ただ重要なのは binutils を一番初めにビルドするという点だけではありません。 gcc や glibc の configure が正しく処理されなかったとすると、ツールチェーンがわずかながらも不完全な状態で生成されてしまいます。 この状態は、すべてのビルド作業を終えた最後になって、大きな不具合となって現れてくることになります。 テストスイートを実行することが欠かせません。 これを実行しておけば、この先に行う多くの作業に入る前に不備があることが分かるからです。

Binutils はアセンブラーとリンカーを二箇所にインストールします。 $LFS/tools/bin$LFS/tools/$LFS_TGT/bin です。 これらは一方が他方のハードリンクとなっています。 リンカーの重要なところはライブラリを検索する順番です。 ld コマンドに --verbose オプションをつけて実行すれば詳しい情報が得られます。 例えば $LFS_TGT-ld --verbose | grep SEARCH を実行すると、検索するライブラリのパスとその検索順を示してくれます。 (この例は見て分かるように lfs ユーザーでログインしている場合のみ実行することができます。 この後にもう一度このページに戻ってきたときには、$LFS_TGT-ld を単に ld と置き換えてください。)

次にインストールするのは gcc です。 configure の実行時には以下のような出力が行われます。

checking what assembler to use... /mnt/lfs/tools/i686-lfs-linux-gnu/bin/as
checking what linker to use... /mnt/lfs/tools/i686-lfs-linux-gnu/bin/ld

これを示すのには重要な意味があります。 gcc の configure スクリプトは、利用するツール類を探し出す際に PATH ディレクトリを参照していないということです。 しかし gcc の実際の処理にあたっては、その検索パスが必ず使われるわけでもありません。 gcc が利用する標準的なリンカーを確認するには gcc -print-prog-name=ld を実行します。 (上でも述べたように、後でこのページに戻ってきたときは $LFS_TGT- の部分を取り除いてください。)

gcc からさらに詳細な情報を知りたいときは、プログラムをコンパイルする際に -v オプションをつけて実行します。 たとえば $LFS_TGT-gcc -v example.c と (あるいは後にここに戻ってきたときは $LFS_TGT- 部分を除いて) 入力すると、プリプロセッサー、コンパイル、アセンブルの各処理工程が示されますが、さらに gcc がインクルードヘッダーを検索するパスとその読み込み順も示されます。

次に健全化された (sanitized) Linux API ヘッダーをインストールします。 これにより、標準 C ライブラリ (glibc) が Linux カーネルが提供する機能とのインターフェースを可能とします。

次のパッケージは glibc です。 glibc 構築の際に気にかけるべき重要なものは、コンパイラー、バイナリツール、カーネルヘッダーです。 コンパイラーについては、一般にはあまり問題にはなりません。 glibc は常に configure スクリプトにて指定される --host パラメーターに関連づけしたコンパイラーを用いるからです。 我々の作業においてそのコンパイラーとは $LFS_TGT-gcc になります。 バイナリツールとカーネルヘッダーは多少複雑です。 従って無理なことはせずに有効な configure オプションを選択することが必要です。 configure 実行の後は build ディレクトリにある config.make ファイルに重要な情報が示されているので確認してみてください。 なお CC="$LFS_TGT-gcc" とすれば、($LFS_TGT が展開されて)どこにある実行モジュールを利用するかを制御でき -nostdinc-isystem を指定すれば、コンパイラーに対してインクルードファイルの検索パスを制御できます。 これらの指定は Glibc パッケージの重要な面を示しています。 glibc がビルドされるメカニズムは自己完結したビルドが行われるものであり、ツールチェーンのデフォルト設定には基本的に依存しないことを示しています。

すでに述べたように、標準 C++ ライブラリはこの後にコンパイルします。 そして 第 6 章 では、自己依存性を持ったプログラムをビルドできるように、その依存性を無視するためにクロスコンパイル行っていきます。 そのようなパッケージのインストール手順においては DESTDIR 変数を使って、LFS ファイルシステム内にインストールするようにします。

第 6 章 の最後には、LFS のネイティブコンパイラーをインストールします。 はじめに DESTDIR ディレクトリを使って binutils 2 回めをビルドし、他のプログラムにおいても同じようにインストールを行います。 2 回めとなる gcc ビルドでは、不必要なライブラリは省略します。 gcc の configure スクリプトにはハードコーディングされている部分があるので、CC_FOR_TARGET はホストのターゲットが同じであれば cc になります。 しかしビルドシステムにおいては異なります。 そこで configure オプションには CC_FOR_TARGET=$LFS_TGT-gcc を明示的に指定するようにしています。

第 7 章での chroot による環境下では、各種プログラムのインストールを、ツールチェーンを適切に操作しながら実施していきます。 これ以降、コアとなるツールチェーンは自己完結していきます。 そしてシステムの全機能を動作させるための全パッケージの最終バージョンを、ビルドしテストしインストールします。