1年生に送るデバッガ(gdb)の使い方(入門編)
長い夏休みが終わって、1年生はプログラミングⅡを履修すると思います。
プログラミングⅡではポインタ、ファイル入出力といった新しい概念を学んでいくことになります。
しかし、これらの発展的な内容は便利であると共にエンバグしやすい部分でもあります。
例えば「segmentation faultって出てプログラムが止まった…」「2回目以上呼び出した時の関数の挙動が怪しい…」といったバグを埋め込んだ時、ソースコードとにらめっこするかTAを呼んでないでしょうか。
それ、デバッガを使えば解決できるんですよ。
以下のソースコードを見ていきましょう。
#include <stdio.h> int main(void) { int i; int *ptr = NULL; for ( i = 0; i < 128; i++ ) { printf("%d", ptr[i]); } return 0; }
見るからにおかしな部分がありますが、コンパイルして実行してみましょう。
$ ./a.out Segmentation fault: 11
はい。セグメンテーション違反を起こしました。このエラーはプログラムがアクセスしてはいけないメモリにアクセスしようとした際にOSが強制的に実行を停止させるエラーです。
このエラーが出た場合、コンパイルエラーとは異なりどこに原因があるのか分からないので、困り果てる人が多いと思います(流石にこのプログラムであれば分かると思いますが…)。
そこで、以下のようなコマンドを実行します。
$ gcc -g segfault.c $ ulimit -c unlimited $ ./a.out Segmentation fault (core dumped)
同じくセグメンテーション違反を起こしましたが、core dumpedというメッセージが増えました。lsコマンドを実行すると同じディレクトリにcore.xxxxxというファイルがあるのが確認できると思います(macでは/coresというディレクトリにあります)。これがコアダンプです。コアダンプとはあるプロセスの実行時のメモリの状態をファイルに書き出したものです。このコアを使ってデバッガ(gdb)からエラーの原因を探してみましょう。以下のコマンドを実行してください。
$ gdb a.out core.xxxxx ~略~ Core was generated by `./a.out'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x0000000000400583 in main () at segfault.c:8 15 printf("%d", ptr[i]);
といった出力が得られました。ざっくり見るとsegfault.cの8行目のmain関数でセグメンテーション違反が起きたことが分かります。余談ですが、セグメンテーション違反を起こした場合はSISSEGVというシグナルを受け取るんだなぁと覚えておくといつか役に立つかもしれません。このプログラムではptr[i]という(NULL+i)番地にアクセスしようとしたため、セグメンテーション違反を起こして終了しています(そういえば説明無しでptrを配列として使ってますが、配列の糖衣構文とか授業でやりますよね?)。
ちなみに、ある関数の中でセグメンテーション違反を起こした場合、バックトレースによってどの関数から呼び出されたのかを追っていくことができます。以下のプログラムを例にあげます。
#include <stdio.h> void func1(void) { int i; int *ptr = NULL; for ( i = 0; i < 128; i++ ) { printf("%d", ptr[i]); } } void func2(void) { func1(); } int main(void) { func2(); return 0; }
これに先ほどと同じ処理を行い、今度はgdbでbacktraceというコマンドを実行します。ちなみにcoreファイルは毎回xxxxxが別の番号になっていると思うので、その都度最新のものを選んでください(ちなみにこの番号は実行時のプロセスのpidです)。
$ gcc -g segfault.c $ ./a.out $ gdb a.out core.xxxxx ~略~ Program terminated with signal SIGSEGV, Segmentation fault. #0 0x0000000000400583 in func1 () at segfault.c:8 8 printf("%d", ptr[i]); (gdb) backtrace #0 0x0000000000400583 in func1 () at segfault.c:8 #1 0x00000000004005ab in func2 () at segfault.c:13 #2 0x00000000004005b6 in main () at segfault.c:17 (gdb)
このように、どの関数からどの関数を呼び出し、最終的にどこでエラーが起きたかが分かります。便利ですね。
これで簡単にですがセグメンテーション違反が起きた場合に原因がどこにあるかを探せるようになりました。TAや先生を呼ばなくて済むので捗りますね。
次回はある時点でプログラムを停止して実行中の変数を表示したりする予定です。