クロスコンパイルとは
クロスコンパイルとは、プログラムを動かす機械(ターゲット)とは別の機械(ホスト)の上でビルドする手法です。組込みLinuxではターゲットがArmなどの非力なボードのことが多く、その上で直接ビルドすると遅い・メモリが足りない・そもそもコンパイラが載らない、といった問題が起きます。そこで高性能なPC(x86_64のホスト)でArm向けの実行ファイルを生成し、できあがったバイナリだけをターゲットへ転送して実行します。ホストのCPUとターゲットのCPUが異なる点が、通常のネイティブコンパイルとの決定的な違いです。
クロスコンパイル(cross compile)とは、プログラムを実際に動かす機械と、そのプログラムをビルドする機械が別々である、というビルドの形を指します。動かす側の機械をターゲット(target)、ビルドする側の機械をホスト(host)と呼びます。ふだん私たちがPC上で gcc hello.c と打ってそのPCで動く実行ファイルを作る——あの当たり前のやり方は、ホストとターゲットが同じ「ネイティブコンパイル」です。クロスコンパイルはその当たり前を崩し、ホストとターゲットでCPUの種類(アーキテクチャ)が違う状態でビルドします。この一点が、クロスコンパイルを理解するうえでの出発点であり、すべての約束事の源になっています。
なぜそんな回り道が必要なのでしょうか。組込みLinuxの世界では、ターゲットがArm(アーム)系の小さなボードであることが多く、CPUは非力でメモリも数百MB、ストレージも限られます。そういうボードの上で大きなソフトを直接ビルドしようとすると、いくつもの壁にぶつかります。まずビルドが極端に遅い。Linuxカーネルや大きなアプリのコンパイルは、高速なPCでも数分から数十分かかる重い作業で、これを非力なボードでやれば何時間もかかり、開発のたびに待たされて作業が前に進みません。次にメモリが足りず、コンパイルやリンクの途中でメモリ不足になり、プロセスがカーネルに強制終了されてビルドが完走しないこともあります。そもそもターゲットにコンパイラ(gcc)一式が入っていない、容量の制約で入れる空きすらない、という構成も珍しくありません。製品出荷時のボードには、動作に必要なものだけを載せてコンパイラのような開発用の道具はあえて入れない、という設計がむしろ一般的です。こうした事情が積み重なって、「ターゲット上で直接ビルドする」という素朴なやり方は、組込みでは現実的でなくなるのです。
ホストとターゲットという2つの世界
この問題を解いてくれるのがクロスコンパイルです。考え方はシンプルで、非力なターゲットの代わりに、高性能なPC(多くは x86_64 のホスト)の上でターゲット向けの実行ファイルを作ってしまう、というものです。ホストには潤沢なCPUとメモリがあるので、ビルドは速く、大きなソフトでも余裕で扱えます。できあがるのはターゲットのCPU(たとえばArm)で動くバイナリで、これをネットワーク転送やSDカード経由でターゲットへ運び、ターゲット側ではただ実行するだけ——という役割分担になります。「重い作業はホストで、実行はターゲットで」と覚えておくと、全体像がつかみやすくなります。
ここで大切なのが、ホストとターゲットという2つの世界を頭の中できちんと分けることです。ホストは x86_64 で動くLinux PC、ターゲットはArmで動く組込みボード。両者はCPUが違うので、ホスト向けにビルドしたバイナリはターゲットでは1命令も実行できませんし、その逆もまた然りです。CPUが理解する機械語そのものが違うからで、これは翻訳すべき言語が根本から異なるのに似ています。クロスコンパイルにまつわるトラブルの多くは、この2つの世界を取り違えること——たとえばうっかりホスト向けのバイナリを作ってしまう、あるいはホスト用のライブラリをターゲット向けのビルドに混ぜてしまう——から生まれます。逆に言えば、この区別さえぶれなければ、クロスコンパイルの大半の落とし穴は避けられます。だからこそ、いま自分が「どちらの世界向けに」ビルドしているのかを常に意識する姿勢が、この分野では何より効いてきます。本トラックの用語でいうクロスコンパイル(cross compile)とは、まさにこのホストとターゲットがずれた状態でのビルドを指す言葉です。
実際の流れと、この先で学ぶこと
クロスコンパイルの基本的な流れを言葉にすると、こうなります。ホストのPC上で、ターゲット用に用意された特別なコンパイラを使ってビルドし、生成された実行ファイルがちゃんとターゲット向けになっているかを確認し、それをターゲットへ転送して実行する。たとえばホストで作った hello というバイナリは、ホスト上で ./hello と打っても「実行形式エラー」になって動きません。アーキテクチャが違うからです。それをArmボードへ scp などで転送してはじめて、本来の動作をします。この「ホストでは動かないのが正しい」という感覚は、最初は奇妙に思えますが、慣れるとクロスコンパイルが正しくできている証拠として頼もしく見えてきます。
もう少し全体を俯瞰しておくと、組込みLinuxの開発では、このクロスコンパイルが単発のプログラムだけでなく、ブートローダ・カーネル・各種アプリ・それらを束ねるルートファイルシステムまで、システムを構成するほぼすべての要素に対して行われます。つまりクロスコンパイルは、組込みLinux開発の例外的な手法ではなく、むしろ日常そのものだということです。本トラックでは小さな hello から始めますが、その先には製品まるごとをホストでビルドする世界が広がっています。最初の一歩で身につける考え方が、そのまま大規模なビルドにも通用するので、ここで土台を固める意味は大きいと言えます。
ターゲット用の特別なコンパイラのことを、ツールチェーン(toolchain)と呼びます。これはコンパイラ単体ではなく、ターゲット向けのバイナリを作るために必要な道具一式をまとめた呼び名で、次のトピックで詳しく扱います。また、そのコンパイラがどのターゲット向けなのかは、arm-linux-gnueabihf のようなトリプレットという識別子で表されます。この先のトピックでは、ツールチェーンの中身、トリプレットとABIの読み方、実際に arm-linux-gnueabihf-gcc でビルドする手順、file や readelf で生成物を確認する方法、外部ライブラリを使うときの sysroot、そして Make や configure で大規模なプロジェクトをクロスビルドする CROSS_COMPILE の作法、という順で一歩ずつ深めていきます。まずは「ホストでターゲット用を作る」という核を、しっかり腹に落としておきましょう。