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

GCCとは何か:コードを機械語に翻訳する、ツールチェーンの心臓部

連載ナビ

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

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

—

GCCの正体は「4段階の処理を束ねるドライバ」

GCC(GNU Compiler Collection)は、C・C++・Fortranなど複数の言語に対応したコンパイラ集です。名前が Compiler "Collection" なのはこのためで、C言語用のフロントエンドが gcc、C++用が g++ です。

そして結論はこうです。私たちが打つ gcc コマンドの正体は、コンパイラ本体そのものではなく、「プリプロセス → コンパイル → アセンブル → リンク」という4段階の処理を、適切なプログラムに順番に依頼していくドライバです。この4段階と、それぞれを担当するプログラムを知っていると、ビルドエラーが出たときに「どの段階の・何のエラーか」を即座に切り分けられるようになります。

段階 途中で止めるオプション 生成物 実際に処理するプログラム
1. プリプロセス gcc -E 展開済みソース プリプロセッサ(cc1に内蔵)
2. コンパイル gcc -S アセンブリコード(.s) cc1(コンパイラ本体)
3. アセンブル gcc -c オブジェクトファイル(.o) as(Binutils)
4. リンク (指定なし) 実行可能ファイル ld(Binutils、collect2 経由)

段階ごとに、担当プログラムもエラーの種類も違うから

なぜ4段階を区別する必要があるのか。理由は、各段階で「入力・出力・失敗のしかた」がまったく違うからです。「コンパイルエラー」と一括りにされがちなものは、実際には次の3種類に分かれます。

  • プリプロセスの失敗: ヘッダファイルが見つからない。

`text

hello.c:1:10: fatal error: stdio.h: No such file or directory

`

疑うべきはソースコードではなく、開発用ヘッダのインストール漏れ(Ubuntuなら build-essential など)やインクルードパス(-I オプション)です。

  • コンパイルの失敗: 文法や型の誤り。

`text

hello.c:5:3: error: expected ';' before 'return'

`

これは cc1 が構文解析で検出したエラーで、疑うべきはソースコードそのものです。

  • リンクの失敗: シンボルが解決できない。

`text

/usr/bin/ld: … undefined reference to `sqrt'

collect2: error: ld returned 1 exit status

`

ソースは正しくても、実体を含むライブラリをリンクし忘れると起きます(この例なら数学ライブラリの指定 -lm が不足)。修正すべきはコードではなくビルドコマンドです。

同じ「ビルドが通らない」でも、原因の場所と直し方が段階ごとに違う——これが4段階を意識する実用上の理由です。

4段階を1つずつ止めて、中間生成物を観察する

hello.c を題材に、各段階の生成物を実際に取り出してみます(記事末尾のコマンド一式で再現できます)。

段階1(gcc -E) の出力は、それでもC言語のソースコードです。ただし #include <stdio.h> が stdio.h の実際の中身(数百行の関数宣言)に置き換わり、#define したマクロもすべて展開されています。出力には # 1 "/usr/include/stdio.h" のような行が挟まっており、「どのファイルの何行目由来か」の記録も残ります。

段階2(gcc -S) で生成される hello.s は、CPUの命令にほぼ一対一で対応するアセンブリコードです。x86-64環境なら call puts@PLT、Arm64環境(M1 MacのLinux VMなど)なら bl puts のような行が見つかります。C言語の puts(...) という1行が、「puts という名前の場所へ飛べ」という命令に翻訳されたわけです。

段階3(gcc -c) で生成される hello.o は、アセンブリコードを機械語(バイナリ)にしたオブジェクトファイルです。file hello.o は ELF 64-bit LSB relocatable ... と表示します。前回見たとおり、これは「シンボル未解決の部品」の状態です。

段階4(オプションなし) で、GCCはリンカ ld を呼び出し、hello.o とGlibcを結合して実行可能ファイルを完成させます。ここから先の主役はGCCではなくBinutilsとGlibcです(第5回で詳しく扱います)。

gcc -v で、ドライバとしての素顔を見る

「gccはドライバである」ことは、-v オプションで確認できます。


gcc -v -o /tmp/hello /tmp/hello.c

大量の出力の中に、cc1 の起動行、as の起動行、そして collect2(ld を呼び出すラッパー)の起動行が順番に現れます。gcc 自身は翻訳をしておらず、適切な引数を組み立てて各プログラムに渡す調整役に徹していることが分かります。

コンパイラ本体 cc1 の置き場所も確認できます。


gcc -print-prog-name=cc1
# 例: /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 (パスやバージョンは環境による)

cc1 はPATHの通った場所にはなく、gcc ドライバ経由でのみ呼ばれる内部コマンドです。普段まったく意識しないこのプログラムこそが、C言語を理解している唯一の存在です。

エラーの「段階」が読めれば、直す場所が分かる

GCCは4段階のパイプラインを統括するドライバであり、各段階は担当プログラムも失敗のしかたも異なります。fatal error: ...h: No such file or directory ならヘッダとパス、error: expected ... ならソースコード、undefined reference ならリンク対象——エラーメッセージから段階を読み取れれば、修正すべき場所は自動的に決まります。

LFSでは、このGCCそのものをソースからビルドします。その際、cc1 や内部ライブラリがどう構成されているかを嫌でも目にすることになるので、今回の内容が土台になります。次回は、段階4でGCCが結合を依頼していた相手、つまりほぼすべてのLinuxプログラムが依存する標準Cライブラリ「Glibc」を解剖します。

コマンドでGCCの仕事の一部を体験する


# 簡単なCプログラムを作成
cat > /tmp/hello.c <<'EOF'
#include <stdio.h>

#define GREETING "Hello, World!"

int main(void) {
  puts(GREETING);
  return 0;
}
EOF

# 1. プリプロセスだけを実行する
#    GREETINGが"Hello, World!"に、#includeがヘッダの中身に置き換わっている
echo "--- Preprocessing ---"
gcc -E /tmp/hello.c | head -n 10
gcc -E /tmp/hello.c | tail -n 10

# 2. アセンブリ言語への翻訳までを実行し、hello.sを生成する
echo "--- Compiling to Assembly ---"
gcc -S -o /tmp/hello.s /tmp/hello.c
cat /tmp/hello.s | head -n 20

# 3. アセンブルまでを実行し、オブジェクトファイルhello.oを生成する
echo "--- Assembling to Object File ---"
gcc -c -o /tmp/hello.o /tmp/hello.c
file /tmp/hello.o

# 4. 全工程を実行し、実行可能ファイルを生成して実行する
echo "--- Linking and Executing ---"
gcc -o /tmp/hello /tmp/hello.c
file /tmp/hello
/tmp/hello

# おまけ: ドライバが呼び出すプログラムの一覧を観察する
gcc -v -o /tmp/hello /tmp/hello.c 2>&1 | grep -E 'cc1|as |collect2' | head -n 5

チェックポイント

  • GCCの役割を「4段階のパイプラインを統括するドライバ」として説明できる
  • -E / -S / -c がそれぞれどの段階で処理を止めるオプションかを言える
  • 「ヘッダが見つからない」「文法エラー」「undefined reference」がそれぞれどの段階のエラーかを見分けられる
  • gcc -v で cc1 / as / collect2 の呼び出しを確認した

今回理解できたこと

  • GCCは単一のプログラムではなく、プリプロセス → コンパイル → アセンブル → リンクを統括するドライバであること
  • C言語を実際に翻訳しているのは内部コマンド cc1 で、アセンブルとリンクはBinutils(as / ld)の担当であること
  • 各段階の中間生成物(展開済みソース、.s、.o)はオプションで取り出せ、デバッグに役立つこと
  • エラーメッセージの形式から失敗した段階を特定すれば、修正すべき場所(ヘッダ・ソース・リンク指定)が決まること