わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

デバッグしよう(3)〜リンクエラーに対処する

前回,構文エラーを退治した時点でのソースを書いておきましょう.

#include <stdio.h>

#define relatively_prime(a, b) (gcd((a), (b)) == 1)

int gcd(int a, int b);

int main(void)
{
  int x = 180, y = 120;
  int flag_rp = relativelyprime(x, y);

  if (flag_rp) {
    printf("%d and %d are relatively prime.\n", x, y);
  } else {
    printf("%d and %d are not relatively prime.\n", x, y);
  }

  return 0;
}

int gdc(int a, int b)
{
  int r = a % b;

  if (r == 0) {
    return a;
  }

  return gcd(b, r);
}

コンパイルすると…

$ env LC_ALL=C gcc relatively_prime.c
/cygdrive/c/(略)/ccNZgtpS.o:relatively_prime.c:(.text+0x46): undefined reference to `_relativelyprime'
/cygdrive/c/(略)/ccNZgtpS.o:relatively_prime.c:(.text+0xc6): undefined reference to `_gcd'
collect2: ld returned 1 exit status

ではデバッグしましょう.
前回は,何行目を見ればいいというのを教えてくれたのですが,今回はその行数が出ていません.これはなぜかというと,リンク時のエラーだからです.
もう少し詳しく言いますと,前回は,コンパイラが,ソースファイルを構文解析する際に見つけたエラーでした.なので,何行目のどこで「破たんした!」と教えてくれたわけです.
今回は,その構文解析は一応問題なくできました,しかし,実行ファイルを作る工程の一つに「リンク」というのがあり,そこで関数呼び出しの名前を解決します*1が,ここでストップがかかったのでした.もとのプログラムにある関数名が,どこにも作られていなければ,「関数が見つからない」ということで,このようなエラーとなったわけです.
行数こそわかりませんが,今回もエラーメッセージをよく読むと,原因追究のヒントを教えてくれています.すなわち `_relativelyprime' と `_gcd' です.シングルクオートと,最初の下線記号を取り除いた,relativelyprime,gcd.この二つに注意して,プログラムを見直しましょう.
Emacsで,関数名に限らず特定の文字列を見つけるには,インクリメンタルサーチを使います.Emacsをアクティブにしたら,以後マウスは使用しません.Ctrlを押しながら,sを押し,両方を離します(この操作は,「コントロールエス」と呼ばれ,「C-s」と表記するのは,授業で教わりましたね?).そして面倒でもrelativelyprimeと押すと,ミニバッファにこの文字列が出てきて,プログラムのどこにこの文字列があるかを教えてくれます.もしFailingとか出てきたら,検索対象(ここではrelativelyprime)を打ち間違えていないかよく見て,間違いがあれば,C-gを押して御破算にしてから,改めてC-sでやり直してください.間違っていないなら,読み進めてください.
ここにありますよ,と教えてくれているときに,もう一度C-sとすると,そのカーソル位置から下で,次はどこにその文字列があるかを教えてくれます.ここで「Failing I-search: relativelyprime」と出てきても,もう一度,C-sを押すと,今度はメッセージが変わります.「Overwrapped I-search: relativelyprime」です.「Wraped I-search: relativelyprime」となるかもしれません.
これは何かというと,ファイル(Emacsの用語で言うと,バッファ)の先頭に戻って,relativelyprimeを探したところ,見つかりましたよという意味です.なのですが,このケースでは,何回C-sを押しても,ミニバッファのメッセージは周期的に変わりつつも,カーソルの位置は変わりません.
操作を長々説明しましたが,「このプログラムで,relativelyprimeという文字列は,1回しか出現していない」ということが分かったわけです.
ここで,インクリメンタルサーチを止めましょう.サーチ前のカーソルにするには,C-gなのですが,ここは,発見した文字列の位置にカーソルを置いておくほうが都合がいいので,こういうときは,右矢印,そして左矢印という裏技が有効です.
これはデバッグというより検索の技術なのですが,「relativelyprime」で調べると1箇所しか見つからないのなら,この文字列の一部だけで検索すると,もっと多い箇所に出現があるんじゃないか考えます.そこで再びC-sとして,今度は,relがどこにあるかを調べてみましょう.すると,

  • 3行目の,#define relatively_prime(a, b) 以下略
  • 10行目の,int flag_rp = relativelyprime(x, y);
  • 13行目の,printf("%d and %d are relatively prime.\n", x, y);
  • 15行目の,printf("%d and %d are not relatively prime.\n", x, y);

の4か所であることがわかります.13行目,15行目は,文字列の中ですから,出力メッセージに影響こそすれ,関数呼び出しとは関係ないので,無視しましょう.3行目と10行目ですが…はい,3行目は「relatively_prime」という名前の関数形式マクロを定義していて,10行目は「relativelyprime」という呼び出しをしています.識別子が一致しておらず,ここがエラーのもと,ということです.
10行目のところを,「relatively_prime」に変更しましょう.
もう一つのエラーのもとは,gcdでした.C-sを使って同様にインクリメンタルサーチをすると,

  • 3行目の,#define relatively_prime(a, b) 以下略
  • 5行目の,int gcd(int a, int b);
  • 29行目の,return gcd(b, r);

の3か所です.3行目は,gcd関数の呼び出し,5行目は,関数プロトタイプ,29行目は,gcd関数の再帰呼び出しです.…
関数定義の最初の行を,見つけていません.そこをスキップしています.インクリメンタルサーチは終了して,上下のキーで,21行目を見ると…「int gdc(int a, int b)」となっています.
他はgcd,ここだけgdc
そうです.gdcという名前の関数を定義していて,コンパイル時には(gcdの関数プロトタイプがあるので)gcdという関数がどこかで定義されているものとして構文解析が行われ,リンク時に,いやgcdという関数はどこにもないよということで,エラーになったわけです.
もちろん,21行目のgdcを,gcdに変更しておきましょう.最大公約数を英語で書くと,greatest common divisor,その頭文字がGCDですから.
そして再びコンパイル…メッセージが一つも出なくなりました.lsでファイルを見ると,実行ファイルができています.
では次のステップのデバッグに移りましょう.バグがあるのですか? ええ,あるのです.

  • 知っておこう英語表現
    • undefined reference to `...': 「...」という未定義のものを参照しています,「...」は定義されていません
    • ld: リンクのコマンド*2コンパイラが内部で呼び出すコマンドの一つ.
    • exit status: 終了ステータス.Unixでは,0が正常終了,0以外が異常終了を意味します.
    • I-search: インクリメンタルサーチ(incremental search)
  • 知っておこうキーストローク
    • C-sで,インクリメンタルサーチ開始.検索対象文字列を打ち込んでいきます.
    • 文字列を打ち込んでから,C-sで,次の該当位置にカーソルが移ります.代わりにC-rだと,前の該当位置にカーソルが移ります.
    • ミニバッファに「Failed」と出ても,そこでC-sと打てば,ファイルの先頭から検索し直します.
    • C-gでインクリメンタルサーチを終了し,サーチ前の位置に戻ります.
    • カーソルキーを押しても,インクリメンタルサーチを終了します.この場合は,見つけた文字列の周辺にカーソルが移動します.
    • relatively_primeのような長い文字列は,いちいち打ち込むと,タイプミス,そしてリンクエラーになりやすいので,テキストエディタの補完機能を活用しましょう.Emacsでは,動的略語展開というのが便利です.ためしに,プログラムの末尾にカーソルを移動して,relまでタイプしてから,M-/(Altキーを押しながら,/キーを押して,すぐに両方離す)を押すと,「relatively」まで出てきます.検索中のC-sと同様に,M-/を何度も押すと,効果(補完内容)が変わります.
    • 繰り返しC-sを打ちたいときは,Ctrlを押したままにして,sだけを繰り返しタイプします.繰り返しM-/も同様に,Altを押したまま,/だけをタイプします.

*1:外部変数という可能性もあります.関数・変数は,ライブラリからだけでなく,自作のものをリンクすることもあります.そうなってくると,おそらく,ソースファイルは複数からなり,小分けにコンパイルして,最終的にリンクして実行ファイルを作るという,「分割コンパイル」をとっていることになるでしょう.

*2:「リンカ(linker)」とも呼ばれます.