ターゲット向けgccでビルドする
実際のビルドは、ネイティブの gcc の代わりにトリプレット付きのクロスコンパイラを呼ぶだけです。たとえば arm-linux-gnueabihf-gcc -o hello hello.c とすれば、Arm向けの実行ファイル hello ができます。コマンドのオプション(-o で出力名、-static で静的リンク、-O2 で最適化)はネイティブの gcc とまったく同じ流儀で使えます。違うのは生成されるバイナリのアーキテクチャだけです。できあがった hello はホスト上では実行できず(アーキが違うため)、ターゲットへ転送してはじめて動きます。
ここまでで、ホストとターゲットという考え方、ツールチェーンの中身、トリプレットの読み方を見てきました。いよいよ実際にビルドしてみましょう。身構える必要はありません。クロスコンパイルのビルドそのものは、拍子抜けするほど簡単です。やることは一つだけ——ふだん使うネイティブの gcc の代わりに、トリプレット付きのクロスコンパイラを呼ぶ。これだけです。コマンドの構造も、渡すオプションも、ネイティブの gcc とまったく同じ流儀が通用します。違うのは、できあがるバイナリのアーキテクチャだけです。
最初の一歩 — hello をArm向けにビルドする
簡単なCのソース hello.c があるとします。ネイティブなら gcc -o hello hello.c でホスト用の実行ファイルができます。これをArm向けにするには、コマンドの gcc を arm-linux-gnueabihf-gcc に置き換えるだけです。すなわち arm-linux-gnueabihf-gcc -o hello hello.c。たったこれだけで、Armで動く実行ファイル hello が生成されます。-o hello は出力ファイル名の指定で、これもネイティブと同じ意味です。コンパイラがArm向けのコードを吐き、Arm向けのアセンブラ(arm-linux-gnueabihf-as)とリンカ(arm-linux-gnueabihf-ld)が——いずれも gcc から内部で自動的に呼ばれます——結合してくれるので、利用者の側はコンパイラ名を変えるという一手間だけで、ターゲット向けのビルドが完了します。前のトピックで見たツールチェーンが、ここで一斉に連携して働いているわけです。64bit Arm向けなら aarch64-linux-gnu-gcc -o hello hello.c となり、トリプレットが変わるだけで考え方はまったく同じです。
オプションはネイティブのgccと同じ
クロスコンパイラだからといって、特別なオプションを覚え直す必要はありません。最適化レベルを上げる -O2、デバッグ情報を埋め込む -g、警告を厳しくする -Wall、プリプロセッサにマクロを渡す -D、追加のヘッダ検索パスを指定する -I、ライブラリの検索パスを足す -L、ライブラリを指定する -l——これらはすべてネイティブの gcc と同じように使えます。たとえば arm-linux-gnueabihf-gcc -O2 -Wall -o app main.c util.c のように、最適化と警告を有効にして複数のソースをまとめてビルドできますし、-c を付けてオブジェクトファイル(.o)だけを作り、後でまとめてリンクする、という分割ビルドのやり方も同じです。gcc というツールの使い方の知識が、そっくりそのままクロスコンパイルに引き継げるわけです。これが「コンパイラ名を変えるだけ」と言える理由であり、ネイティブで gcc に慣れている人ほど、クロスコンパイルへの移行はなめらかになります。
静的リンクという実務上の選択
クロスコンパイルでよく使うオプションの一つに、静的リンクを指示する -static があります。arm-linux-gnueabihf-gcc -static -o hello hello.c とすると、プログラムが必要とするライブラリ(標準Cライブラリなど)の中身を実行ファイルの中に全部抱え込ませた、自己完結したバイナリができます。なぜこれが便利かというと、ターゲット側に対応するライブラリが揃っているとは限らないからです。動的リンク(既定)のバイナリは、実行時にターゲット上の共有ライブラリ(.so ファイル)を探して読み込みますが、ターゲットにそのライブラリが無かったりバージョンが違ったりすると「ライブラリが見つからない」と起動に失敗します。組込みのターゲットはライブラリが最小限しか入っていないことが多いので、この失敗は実際よく起こります。静的リンクならその心配がなく、バイナリ1つを転送すればとりあえず動く、という確実さが得られます。引き換えにファイルサイズは大きくなるので、容量と確実さを天秤にかけて選びます。実運用では容量を抑えるために動的リンクを基本としつつ、動作確認や使い捨てのツール、あるいはトラブルの切り分けではまず -static で確実に動かす、という使い分けが現場の定石です。ライブラリ依存をどう解決するかという、より本格的な話題は、後の sysroot のトピックで扱います。
できたバイナリはホストでは動かない
ここで、クロスコンパイル初心者がほぼ全員一度は驚くポイントに触れておきます。いま作った hello を、ビルドしたホスト上で ./hello と打って実行しようとすると、動きません。bash なら「実行形式エラー(Exec format error)」のようなメッセージが出ます。これは失敗ではなく、むしろ正しくクロスコンパイルできている証拠です。hello はArm向けの機械語でできており、ホストの x86_64 CPU はその命令を解釈できないので、当然そのままでは走らないのです。アーキテクチャが違えば、命令の符号化はもちろん、データのバイト並び順であるエンディアンが異なる場合すらあり、機械語のレベルでは完全な別物になります。動かすには、このバイナリをArmボード(ターゲット)へ scp などで転送し、ターゲット上で実行します。「ホストでビルドし、ホストでは動かず、ターゲットで動く」——この流れこそがクロスコンパイルの本質です。もしホストでそのまま動いてしまったら、それはArm向けに作れておらず、うっかりネイティブの gcc でビルドしてしまった疑いが濃厚です。だからこそ、ビルドした直後に生成物が本当にターゲット向けになっているかを確認する習慣が大切で、その確認方法——file や readelf でアーキやエンディアンを読み取る手順——を次のトピックで詳しく見ていきます。