最後の一歩で止まったビルド
勤怠管理のプログラムをビルドすると、undefined reference to printf と出ました。同僚は C のコードを1行ずつ読み始めます。
でも、コードに誤りはありません。このエラーはビルドの最後の段階から出ています。その段階は、C のコードを読みません。段階を知っていれば、1時間を無駄にせずに済みました。
このレッスンのゴール
メモ
コンパイラがプログラムを通す段階の名前を言える。
重要な言葉
| ことば | いみ | れい |
|---|---|---|
| ドライバ | ビルド用のほかのプログラムを、順に動かすプログラム | gcc |
| プリプロセッサ | ファイルを貼り込み、マクロを置き換える段階 | |
| アセンブラ | アセンブリを機械語に変えるプログラム | as |
| オブジェクトファイル | まだ1つのプログラムになっていない機械語 | hello.o |
| シンボル | リンカが探す名前。関数の名前など | |
| リンカ | オブジェクトファイルとライブラリをつなぐプログラム | ld |
学生が入力するセルは、このプレビューでは空白です。
gcc はほかのプログラムを動かす
gcc hello.c と打っても、gcc 自身はほとんど何もしません。gcc はドライバです。ほかのプログラムを順に動かし、前のプログラムが書いたファイルを次に渡します。
オプション -### を付けると、gcc は動かすはずのコマンドを表示するだけで、何も動かしません。下の2つの Run で、ラボのフォルダに入り、各コマンドが起動するプログラムの名前だけを取り出します。
コンパイラのフォルダに入る
cd ~/compilerここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
gcc が動かすはずのプログラム
gcc -### -o hello hello.c 2>&1 | grep "^ " | cut -d " " -f 2ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
例: 3つのプログラムと4つの段階
| プログラム | 受け持つ段階 | 書くもの |
|---|---|---|
| cc1 | 前処理とコンパイル | アセンブリ(.s ファイル) |
| as | アセンブル | オブジェクトファイル(.o) |
| collect2 | リンク(中で ld を動かす) | 実行できるプログラム |
学生が入力するセルは、このプレビューでは空白です。
プリプロセッサはマクロを置き換える
hello.c には #define GREETING の行があります。これはマクロで、名前を文字列に置き換える約束です。
gcc -E は最初の段階で止まり、結果を画面に出します。#include の1行が数百行に増えるので、全部は読みません。grep で、squared を含む行だけを取り出します。
前処理のあとの printf の行
gcc -E hello.c | grep squaredここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
すべての段階のファイルを残す
ふつう、段階の間のファイルは一時フォルダに置かれ、終わると消されます。-save-temps を付けると、ソースの隣に残ります。
1回のビルドで4つのファイルができます。前処理のあとの hello.i、コンパイルのあとの hello.s、アセンブルのあとの hello.o、リンクのあとの hello です。file はファイルの先頭を読んで、種類を答えます。
1回ビルドして、全部残す
gcc -save-temps -o hello hello.cここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
それぞれ何のファイルか
file hello.i hello.s hello.o helloここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
例: file の答え
| ファイル | 作った段階 | file の答え(共通の部分) |
|---|---|---|
| hello.i | プリプロセッサ | C source, ASCII text |
| hello.s | コンパイラ | assembler source, ASCII text |
| hello.o | アセンブラ | ELF 64-bit LSB relocatable |
| hello | リンカ | ELF 64-bit LSB pie executable |
学生が入力するセルは、このプレビューでは空白です。
オブジェクトファイルには穴がある
hello.o には、square と main の機械語が入っています。でも main が呼ぶ printf は、このファイルにありません。C のライブラリにあります。
nm は、オブジェクトファイルのシンボルを並べます。T は中身がここにあるという印です。U は未定義で、リンカが埋める穴です。
次に、C のライブラリを渡さずに、リンカの ld を自分で動かします。何と言うかを読みましょう。
hello.o のシンボル
nm hello.oここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ライブラリなしでリンクする
ld hello.o -o brokenここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
ビルドのエラーは、どれも自分のコードの誤りだと思いがちです。undefined reference はリンカから出ます。コードのコンパイルは、もう済んでいます。シンボルの中身が、最後の段階に渡されていないのです。C の行ではなく、何をリンクしたかを調べます。
printf はどこから来るか
ldd helloここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
さらに挑戦: 2つの段階のエラー
自分で打ちましょう。sed "s/square(7)/cube(7)/" hello.c > cube.c で、どこにもない関数 cube を呼ぶコピーを作ります。gcc -c cube.c を実行し、どの段階が答えたかを言いましょう。
次に、main の上に int cube(int n); の1行を足して、もう一度 gcc -c cube.c を実行します。nm cube.o を読み、最後に gcc cube.c を実行します。2つのエラーが違う段階から出る理由を説明しましょう。