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

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

第 5 章第 6 章 の最終目標は一時的なシステム環境を構築することです。 この一時的なシステムはシステム構築のための十分なツール類を有していて、ホストシステムとは切り離されたものです。 この環境へは chroot によって移行します。この環境は 第 8 章 において、クリーンでトラブルのない 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 におけるクロスコンパイラーの実装方法

[注記]

注記

ほぼすべてのビルドシステムにおいては、cpu-vendor-kernel-os という形式のマシントリプレット(triplet)と呼ばれる名称が用いられます。 お気づきのことと思いますが、なぜ "トリプレット" といいながら 4 つの項目からなる名前なのでしょう。 その理由はこれまでの経緯にあります。 当初は 3 つの項目による名前を使っていれば、マシンを間違いなく特定できるものでした。 しかし新たなマシン、新たなシステムが登場するようになって、これでは不十分であることがわかりました。 "トリプレット" という語だけが残ったわけです。 マシンのトリプレットを確認する一番簡単な方法は、config.guess スクリプトを実行することです。 これは多くのパッケージのソースに含まれています。 binutils のソースを伸張(解凍)し、この ./config.guess スクリプトを実行して、その出力を確認してください。 たとえば 32 ビットのインテルプロセッサーであれば、i686-pc-linux-gnu と出力されます。 64 ビットシステムであれば x86_64-pc-linux-gnu となります。

またプラットフォームのダイナミックリンカーの名前にも注意してください。 これはダイナミックローダーとも呼ばれます。 (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_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 ライブラリを用いています。 このライブラリは lfs マシン向けにコンパイルされたものでなければなりません。 つまりクロスコンパイラー cc1 を使うということです。 しかしコンパイラーには内部ライブラリというものがあって、アセンブラー命令セットだけでは利用できない複雑な命令が含まれます。 その内部ライブラリは libgcc と呼ばれ、完全に機能させるには glibc ライブラリにリンクさせなければなりません。 さらに C++ (libstdc++) に対する標準ライブラリも、glibc にリンクさせる必要があります。 このようなニワトリと卵の問題を解決するには、まず libgcc に基づいた低機能版の cc1 をビルドします。 この cc1 にはスレッド処理や例外処理といった機能が含まれていません。 その後に、この低機能なコンパイラーを使って glibc をビルドします。 (glibc 自体は低機能ではありません。) そして libstdc++ をビルドします。 libstdc++ もやはり、libgcc と同じく機能がいくつか欠如しています。

これで話が終わるわけではありません。 上の段落における結論は以下のようになります。 cc1 からは完全な libstdc++ はビルドできないということです。 しかし第 2 段階においては、C/C++ ライブラリをビルドできる唯一のコンパイラーです。 もちろん第 2 段階においてビルドされるコンパイラー cc-lfs は、それらライブラリをビルドできます。 しかし (1) GCC ビルドシステムは、それが pc 上で利用できるかどうかわかりません、そして (2) pc 上にてそれを使うと pc 内のライブラリにリンクしてしまうリスクがあります。 なぜなら cc-lfs はネイティブコンパイラーであるからです。 そこで libstdc++ は、後々 chroot 環境内でビルドしなければならないのです。

その他の手順詳細

クロスコンパイラーは、他から切り離された $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 を実行すると、検索するライブラリのパスとその検索順を示してくれます。 ダミープログラムをコンパイルして ld--verbose オプションをつけてリンクを行うと、どのファイルがリンクされたが分かります。 例えば $LFS_TGT-gcc dummy.c -Wl,--verbose 2>&1 | grep succeeded と実行すれば、リンカーの処理中にオープンに成功したファイルがすべて表示されます。

次にインストールするのは 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 を実行します。

さらに詳細な情報を知りたいときは、ダミープログラムをコンパイルする際に -v オプションをつけて実行します。 例えば gcc -v dummy.c と入力すると、プリプロセッサー、コンパイル、アセンブルの各処理工程が示されますが、さらに 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 ビルドでは、libstdc++ や不必要なライブラリは省略します。 GCC の configure スクリプトにはハードコーディングされている部分があるので、CC_FOR_TARGET はホストのターゲットが同じであれば cc になります。 しかしビルドシステムにおいては異なります。 そこで configure オプションには CC_FOR_TARGET=$LFS_TGT-gcc を明示的に指定するようにしています。

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