PK
パーネルカニック研究所
今日もどこかでkernel panic。
root@pk-lab:~#

ツールチェーンとは何か:ソースコードを「実行可能ファイル」に変える魔法の道具セット

連載ナビ

この連載は「Linuxを使えるようになる」ではなく、「Linuxがどう動いているのか」を理解するための実践シリーズです。

今回は Linuxを理解する 〜 LFSで学ぶLinux内部構造 〜 の Part 3 / 第2回として、仕組みのつながりを意識しながら学びます。

—

ツールチェーン = コンパイラ + Cライブラリ + バイナリツールの「連結」

結論から言います。ツールチェーンとは、ソースコードを実行可能ファイルに変換するために連携して動く一連のプログラム群のことで、Linuxの標準的な構成は次の3つです。


ツールチェーン
  = GCC      (コンパイラ:  Cなどのソースをアセンブリ言語に変換する)
  + Binutils (バイナリツール: アセンブラ as とリンカ ld を含む)
  + Glibc    (標準Cライブラリ: printf などの実装と、カーネルへの窓口)

「チェーン(鎖)」と呼ばれるのは、前のツールの出力が、次のツールの入力になるからです。GCCが出したアセンブリコードを as が機械語に変換し、その出力を ld がGlibcと結合して実行ファイルにする——この連結が1か所でも切れると、実行ファイルは1つも作れません。LFSで最初に長い時間をかけてツールチェーンを構築するのは、これが以降のすべてのビルド作業の前提だからです。

gcc コマンド1つの裏で、複数のプログラムがリレーしているから

なぜ「3点セット」を意識する必要があるのか。理由は、普段 gcc hello.c -o hello と1コマンドで済ませている処理が、実際には別々のプログラムによるリレーであり、エラーが出たときに「どの走者で落ちたか」を読めないと対処できないからです。

gcc コマンドの正体は、各プログラムを順に呼び出す「ドライバ」です。-v オプションを付けると、裏で呼ばれるプログラムが表示されます。


gcc -v hello.c -o hello

出力の中に、次のような行が見つかります(パスやバージョンは環境によって異なります)。

  • cc1 — Cコンパイラ本体。プリプロセスとコンパイルを行い、アセンブリコードを出力する(GCCの一部)
  • as — アセンブラ。アセンブリコードを機械語のオブジェクトファイルに変換する(Binutilsの一部)
  • collect2 — リンカ ld を呼び出すラッパー。オブジェクトファイルとGlibcを結合する(ld はBinutilsの一部)

つまり gcc と打った時点で、すでにGCC・Binutils・Glibcの3つ全部が仕事をしているのです。

hello.c が実行ファイルになるまでを、中間生成物ごと確認する

リレーの各区間を、実際に途中で止めて確認してみます。


cat > /tmp/hello.c <<'EOF'
#include <stdio.h>
int main(void) { puts("hello, world"); return 0; }
EOF
cd /tmp

# 区間1: C言語 → アセンブリ言語(GCCの仕事)
gcc -S hello.c
head -n 20 hello.s        # 人間がぎりぎり読めるアセンブリコード

# 区間2: アセンブリ言語 → オブジェクトファイル(Binutilsのasの仕事)
gcc -c hello.c
file hello.o

# オブジェクトファイルの中のシンボル(関数名など)を確認
nm hello.o

# 区間3: オブジェクトファイル + Glibc → 実行ファイル(Binutilsのldの仕事)
gcc hello.o -o hello
file hello
./hello

file hello.o の出力は、x86-64環境なら ELF 64-bit LSB relocatable, x86-64 ... となります(M1 Mac上のLinux VMなら ARM aarch64 と表示されます)。この relocatable(再配置可能) という表示が重要で、「機械語にはなったが、まだメモリ上のどこに置かれるか決まっていない部品」であることを意味します。

さらに nm hello.o の出力に注目してください。


0000000000000000 T main
                 U puts

T main は「main 関数の本体がこのファイルにある」、U puts は「puts を使っているが本体はここにない(Undefined)」という意味です。この U を解決する、つまりGlibcの中にある puts の実体と結び付けるのがリンカ ld の仕事です。リンクを終えた hello は file コマンドで executable(または pie executable)と表示され、初めて実行できるファイルになります。

リレーのどこで落ちたかは、エラーメッセージに書いてある

区間を意識できると、ビルドエラーの読み方が変わります。たとえば、本体が存在しない関数を呼ぶコードをビルドすると、次のようなエラーが出ます。


/usr/bin/ld: /tmp/ccXXXXXX.o: in function `main':
main.c:(.text+0x...): undefined reference to `helper'
collect2: error: ld returned 1 exit status

行頭をよく見ると /usr/bin/ld: とあります。これはGCCのエラーではなく、リンカ ld(Binutils)のエラーです。コンパイル(区間1〜2)は成功していて、シンボル解決(区間3)で失敗した、と読めます。したがって疑うべきは文法ミスではなく、「リンク対象のオブジェクトファイルやライブラリの指定漏れ(-l オプションの不足など)」です。

一方、hello.c:2:1: error: expected ';' のような形式ならコンパイラ cc1 のエラーで、疑うべきはソースコードの文法です。どのツールが出したエラーかを行頭で見分ける——これがツールチェーンを理解している人の読み方です。

3点セットを最初に作るのは、すべてのビルドの前提だから

ツールチェーンとは、GCC(コンパイル)・Binutils(アセンブルとリンク)・Glibc(標準ライブラリ)がリレー形式で連携し、ソースコードを実行ファイルへ変換する仕組みの総称です。gcc コマンド1つの裏で3つすべてが動いており、エラーメッセージの行頭を見れば、どのツールで失敗したかまで特定できます。

LFSではこの3つを Binutils → GCC → Glibc の順にソースからビルドします。以降の回で、この3つを1つずつ解剖していきます。次回はリレーの第一走者、GCCです。gcc コマンドの内部で実は4段階の処理が行われていることを、中間ファイルを取り出しながら確認します。

コマンドでツールチェーンの存在を確認する

あなたのシステムにインストールされているツールチェーンの3点セットを確認してみましょう。


# コンパイラGCCのバージョンを確認
gcc --version

# リンカldのバージョンを確認(ldはBinutilsの一部。GNU Binutilsと表示される)
ld --version

# アセンブラasのバージョンを確認(これもBinutilsの一部)
as --version

# 標準Cライブラリ(Glibc)のバージョンを確認
ldd --version

# gccが実際に呼び出すプログラムの正体を確認
gcc -print-prog-name=cc1
gcc -print-prog-name=as
gcc -print-prog-name=ld

チェックポイント

  • ツールチェーンが「ソースコードを実行可能ファイルに変換する一連のプログラム群」であることを説明できる
  • GCC・Binutils・Glibcのそれぞれが、リレーのどの区間を担当しているかを言える
  • nm の出力の T と U の意味を説明できる
  • undefined reference エラーがリンカ(ld)の段階のエラーだと見分けられる

今回理解できたこと

  • ツールチェーンは、GCC(コンパイラ)・Binutils(アセンブラ/リンカ)・Glibc(標準Cライブラリ)で構成されるビルドの基盤であること
  • gcc コマンドの正体はドライバで、裏で cc1 → as → ld のリレーが行われていること
  • オブジェクトファイルは「シンボル未解決の部品」であり、リンカがGlibcと結合して初めて実行可能になること
  • エラーメッセージの行頭を見れば、ツールチェーンのどの段階で失敗したか特定できること