はじめに (対象読者・この記事でわかること)

この記事は、C言語などでMakefileを用いたビルドを行っており、「未定義の参照」というエラーに悩んでいる開発者の方々を対象としています。特に、複数のソースファイルやライブラリをリンクする際に、このエラーに遭遇し、リンク順序を修正しても解決しないという状況にある方に向けて、その根本原因と具体的な解決策を解説します。

この記事を読むことで、以下のことがわかるようになります。

  • Makefileにおける「未定義の参照」エラーが、リンク順序だけでは解決しない場合の根本原因
  • コンパイルとリンクのプロセスにおける、シンボルの解決順序の重要性
  • 依存関係の誤りがエラーを引き起こすメカニズム
  • 具体的なMakefileの修正方法と、エラーを未然に防ぐためのベストプラクティス

Makefileでのビルドは、開発効率を大きく向上させる強力なツールですが、その内部の依存関係を正確に理解していないと、今回のような原因不明のエラーに陥ることがあります。この記事が、皆様のビルド環境の安定化に貢献できれば幸いです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • C言語の基本的なコンパイル・リンクの概念(コンパイラ、リンカの役割)
  • Makefileの基本的な記法(ターゲット、依存関係、レシピ)
  • 複数のソースファイルから実行ファイルを生成する基本的な流れ

C言語ビルドにおける「未定義の参照」エラー:リンク順序だけでは解決しない原因

Makefileで複数のソースファイルやライブラリをリンクする際、「undefined reference to '...'」といったエラーは、リンカがシンボル(関数や変数の実体)を見つけられない場合に発生します。多くの場合、このエラーはオブジェクトファイルやライブラリのリンク順序を正しく設定することで解決できます。しかし、それでもエラーが解消されない、あるいは意図した通りにリンクできないというケースが存在します。

なぜリンク順序だけでは解決しないのか?

リンカは、指定されたオブジェクトファイルやライブラリを順番に処理し、各ファイルで定義されているシンボルを解決していきます。このとき、あるファイルが参照しているシンボルが、後続のファイルで定義されていても、そのシンボルがまだリンカのシンボルテーブルに登録されていない場合、リンカは「未定義」と判断してしまいます。

例えば、main.cutils.cで定義された関数helper_function()を呼び出しているとします。Makefileで以下のような設定をしていたとしましょう。

Makefile
# 誤った例 program: main.o utils.o gcc main.o utils.o -o program

この場合、main.oが先にリンクされます。main.ohelper_function()を参照しますが、この時点ではutils.oはまだリンクされていない(あるいは、リンカがutils.oを処理する前にmain.oのシンボル解決を試みている)ため、helper_function()は「未定義」とみなされてしまいます。

通常、この問題はmain.outils.oの順序を入れ替えるか、あるいはutils.oを先にリンクすることで解決します。

Makefile
# 正しい例 (順序を入れ替えた場合) program: utils.o main.o gcc utils.o main.o -o program

しかし、それでもエラーが解消されない場合、根本的な原因は依存関係の誤りや、コンパイル時の設定ミス、あるいはライブラリのリンク方法に起因している可能性が非常に高いのです。

依存関係の誤りとシンボルの解決

Makefileにおける依存関係の定義は、ビルドの順序を決定するだけでなく、コンパイラやリンカに渡される情報にも影響します。例えば、あるモジュールが別のモジュールで定義された関数や構造体を利用している場合、Makefileはその依存関係を正しく記述する必要があります。

1. 宣言と定義の不一致: C言語では、ヘッダーファイル (.h) で関数の宣言を行い、ソースファイル (.c) でその定義(実装)を行います。もし、ヘッダーファイルに記述された関数宣言が、実際のソースファイルでの定義と一致していない(引数の型や数が違うなど)場合、コンパイルは通っても、リンク時に「未定義の参照」エラーが発生することがあります。リンカは宣言されたシグネチャでシンボルを探しますが、定義されたシンボルとは一致しないためです。

2. ライブラリのリンク漏れ: 外部ライブラリ(例: math.hの関数群はlibm.alibm.soで提供される)を使用している場合、そのライブラリを明示的にリンクする必要があります。Makefileで-lmのようなオプションを付け忘れると、main.oや他のオブジェクトファイルがlibm内の関数を参照しても、リンカはそのシンボル定義を見つけられず、「未定義の参照」エラーとなります。

3. 順序依存性の高いシンボル: 特に、静的ライブラリ (.a) を複数リンクする場合、ライブラリ間の依存関係が重要になります。ライブラリAがライブラリBで定義されたシンボルを参照している場合、ライブラリBをライブラリAよりも後にリンクすると、エラーが発生する可能性があります。 リンカは、ライブラリAを処理する際にライブラリBのシンボルを参照しますが、ライブラリBはまだ処理されていないためです。

Makefile
# 静的ライブラリ間での依存関係の例 # libA.a が libB.a のシンボルを参照している場合 # 誤った例 (libB.a が後にリンクされる) program: gcc main.o -L. -lA -lB -o program # 正しい例 (libB.a が先にリンクされる) program: gcc main.o -L. -lB -lA -o program

4. マクロ定義やプリプロセッサの利用: #ifdef#ifといったプリプロセッサディレクティブを多用している場合、特定の条件でのみコンパイルされるコードブロックが存在します。もし、これらの条件が意図しない形で評価され、本来定義されるべきシンボルがコンパイル時に除外されてしまうと、リンク時に「未定義の参照」エラーとして現れることがあります。これは、コンパイルオプション(-Dオプションなど)で定義されるマクロが正しく設定されていない場合に起こりやすいです。

5. クロスコンパイル環境での問題: クロスコンパイル環境では、ターゲットアーキテクチャとホストアーキテクチャが異なるため、使用するコンパイラ、リンカ、ライブラリがターゲット環境のものと一致している必要があります。これらの設定に誤りがあると、ビルドプロセス全体が不整合を起こし、「未定義の参照」エラーなどの予期せぬ問題が発生します。

具体的なエラー原因と解決策:Makefileのデバッグ

ここからは、具体的なエラーシナリオと、それに対応するMakefileの修正方法を解説します。

シナリオ1:静的ライブラリ間で依存関係がある

問題: 複数の静的ライブラリ(例: libcommon.a, libnetwork.a)があり、libnetwork.alibcommon.aで定義された関数や変数を使用しているにも関わらず、「未定義の参照」エラーが発生する。

Makefileの例 (誤):

Makefile
LDFLAGS = -L. -lnetwork -lcommon LDLIBS = program: main.o $(CC) main.o $(LDFLAGS) $(LDLIBS) -o $@ main.o: main.c common.h network.h $(CC) $(CFLAGS) -c $< -o $@

この例では、-lnetwork-lcommonより先に指定されているため、libnetwork.aを処理する際に、libcommon.aで定義されているシンボルが見つからず、エラーとなります。

解決策: 依存関係のあるライブラリは、参照される側(libcommon.a)を先にリンクするように順序を修正します。

Makefileの例 (正):

Makefile
LDFLAGS = -L. -lcommon -lnetwork # libcommon.a を先にリンク LDLIBS = program: main.o $(CC) main.o $(LDFLAGS) $(LDLIBS) -o $@ main.o: main.c common.h network.h $(CC) $(CFLAGS) -c $< -o $@

LDFLAGSLDLIBSでライブラリを指定する際は、通常「依存されるライブラリ → 依存するライブラリ」の順序で指定するのがセオリーです。

シナリオ2:外部ライブラリのリンク漏れ

問題: math.hsqrt()関数やsin()関数など、標準Cライブラリの数学関数を使用しているにも関わらず、「undefined reference to 'sqrt'」のようなエラーが発生する。

Makefileの例 (誤):

Makefile
program: main.o $(CC) main.o -o $@ # -lm オプションがない main.o: main.c $(CC) $(CFLAGS) -c $< -o $@

解決策: 数学ライブラリ(libm)をリンクするために、リンカオプションに-lmを追加します。

Makefileの例 (正):

Makefile
program: main.o $(CC) main.o -lm -o $@ # -lm オプションを追加 main.o: main.c $(CC) $(CFLAGS) -c $< -o $@

$(CC)コマンド(通常はgcc)に-lmオプションを渡すことで、数学ライブラリがリンクされます。この-lmは、通常、他のオブジェクトファイルやライブラリの後に置くのが一般的です。

シナリオ3:コンパイル時とリンク時で異なる設定がされている

問題: 特定のヘッダーファイルやマクロ定義をコンパイル時には含めているが、リンク時にはそれらが正しく参照されない。

Makefileの例 (誤):

Makefile
# コンパイル時のみ USE_FEATURE を定義 CFLAGS = -DUSE_FEATURE program: main.o utils.o $(CC) main.o utils.o -o $@ main.o: main.c $(CC) $(CFLAGS) -c $< -o $@ utils.o: utils.c utils.h $(CC) $(CFLAGS) -c $< -o $@

このMakefileでは、programターゲットのレシピでCFLAGSが使われていません。もしutils.c#ifdef USE_FEATUREブロック内で定義される関数をmain.cから呼び出している場合、main.oのリンク時にはUSE_FEATUREマクロが定義されていないため、未定義参照エラーになる可能性があります。

解決策: リンク時にも必要なマクロ定義が有効になるように、LDFLAGSやレシピ内でCFLAGS(あるいはCPPFLAGS)を適切に指定します。CPPFLAGSはプリプロセッサフラグをまとめるのに便利です。

Makefileの例 (正):

Makefile
CPPFLAGS = -DUSE_FEATURE # プリプロセッサフラグをまとめる program: main.o utils.o $(CC) main.o utils.o $(CPPFLAGS) -o $@ # リンク時にもCPPFLAGSを適用 main.o: main.c $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ utils.o: utils.c utils.h $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

CPPFLAGSはコンパイル時とリンク時の両方で使われることが多いので、ここに定義しておくと管理が楽になります。

デバッグのヒント

  • -v オプションの活用: gccclang-v オプションを付けてコンパイル・リンクを実行すると、リンカがどのようなコマンドを実行しているか、どのようなライブラリを検索しているかが詳細に表示されます。これを調べることで、リンク順序やライブラリのパスに関する問題の糸口が見つかることがあります。 bash gcc -v main.o utils.o -o program -L. -lcommon -lnetwork
  • nm コマンドでのシンボル確認: nm コマンドを使うと、オブジェクトファイルやライブラリ内のシンボル(定義されているもの、参照されているもの)を確認できます。 bash nm utils.o | grep helper_function # utils.o に helper_function が定義されているか確認 nm main.o | grep helper_function # main.o が helper_function を参照しているか確認 「U」と表示されるシンボルは未定義(Undefined)、「T」や「W」は定義済み(Text/Weak)を示します。
  • Makefile の -d オプション: make -d を実行すると、Makefileの実行過程を詳細に出力してくれます。依存関係の解決がどのように行われているかを確認するのに役立ちます。

まとめ

本記事では、Makefileにおける「未定義の参照」エラーが、単純なリンク順序の誤りだけでは解決しない場合の、より深い原因について解説しました。

  • 依存関係の誤り: 特に静的ライブラリ間での依存関係の解決順序は、リンカの動作に直接影響します。
  • コンパイル・リンク設定の不一致: ヘッダーファイル、マクロ定義、外部ライブラリのリンク漏れなどが原因となり得ます。
  • デバッグ手法: -v オプションやnmコマンドなどを活用することで、問題の特定が容易になります。

この記事を通して、皆様がMakefileで「未定義の参照」エラーに遭遇した際に、より迅速かつ正確に原因を特定し、解決できるようになることを願っています。 今後は、より複雑なビルドシステム(CMakeなど)との比較や、共有ライブラリ(.so)のリンクにおける注意点についても記事にする予定です。

参考資料