6. デバッガ / gdb & DDD

C言語によるプログラミングでデバッガを使うことで効率的にバグを見つけ出すことができる. gcc とペアでよく使われるデバッガが gdb である.

なお,演習室のLinux計算機環境では, gdb をより使いやすくするための, DDD (Data Display Debugger) というフロントエンドをインストールしている. 実行中プログラムのデータ可視化機能も備えた強力なツールなので,ぜひ使いこなしてほしい. (ただし,DDDは日本語のコメントを含むソースコードでは誤動作することがある.)

6.1. クイック実行

以下は,segv_prog.c というプログラムをデバッグするための簡単な例である. ポイントは,

  1. コンパイルの際に,-g オプションを加える

  2. 実行の際に,gdb を利用する

  3. gdb のコンソール内で,run をすると,プログラムが実行される

  4. gdbquit で終了する.

ことである.

$ gcc -g segv_prog.c
$ gdb ./a.out
(gdb) run

run を行った後,Segmentation faultが起きると,その行を表示して,停止するはずである.

6.2. コンパイル方法

デバッグ用のコンパイルオプション -g をつける必要がある. また,最適化のオプションも無効化 -O0 を付けたほうが良い(大文字のオーと数字のゼロ). 例えば, kadai3.c というソースファイルから kadai3-dbg という実行ファイルを作る場合は,

$ gcc -g -O0 -o kadai3-dbg kadai3.c

※コマンド例の先頭に書かれている $ はプロンプトである.自身で入力する必要はない.

6.3. デバッガの操作

6.3.1. デバッガによるプログラムの起動

kadai3-dbg という実行ファイルのデバッグを開始するには,以下のように実行する.

$ gdb kadai3-dbg
(gdb) run

gdb実行後の (gdb) はGDBのプロンプトである.(※プロンプト=入力を待ち受けている) このプロンプトに対して, run を入力する.

6.4. よく使うコマンド

GDBでよく使うコマンドをまとめる.

6.4.1. 起動と終了

r, run

デバッグを開始する.

q, quit

デバッグを終了する

(gdb) r                # コマンドを引数無しで実行
(gdb) q                # 終了する
(gdb) r 2 sample.csv   # 2 と sample.csv という2つの引数を与えてコマンドを実行

6.4.2. 関数間の移動,変数表示

通常,実行中にエラーが発生すると,GDBはそこで停止する. 停止した箇所,あるいは,停止した箇所から逆戻って,変数等の情報を確認することができる.

bt, backtrace

関数の呼び出し履歴を表示する.

f, frame

関数の呼び出し元に戻る.引数を与えることで指定の場所(スタックフレーム)に移動することができる.

(gdb) bt       # 関数の呼び出し履歴を表示する
(gdb) f 1      # btで表示された一覧の#1に戻る
p, print

指定した変数を表示する.

x

指定したアドレスが持つ値を表示する.通常,フォーマット指定等をつけて実行する.

例: char str[10] の中身を表示する場合

(gdb) p str        # 配列変数 str を表示する.
(gdb) x/10xb str   # 配列変数 str の先頭アドレスからバイト単位(b)で10個(10)を16進数(x)で表示する.

6.4.3. ブレークポイント

ブレークポイントを設定すると,実行途中の任意の行でプログラムを停止させることができる. 通常,ブレークポイントは run の前に用意しておく.

b, break

第1引数が数字ならばその行数,文字列ならばその関数名でブレークポイントを設置する.

n, next

1行実行.

s, step

1行実行.ただし,関数呼び出しがある場合は,その関数の中に入る.

c, continue

停止した箇所からの処理を再開する

(gdb) b 10         # 10行目にブレークポイントを設置する.
(gdb) b subst      # substという名前の関数にブレークポイントを設置する.
(gdb) r            # (ブレークポイントを設置してから,runする)
# 10行目,あるいは,subst関数で停止する
(gdb) n            # 1行だけ処理をする
(gdb) c            # 処理を再開する

6.5. 例題

以下のプログラムを gdb でデバッグしてみよ.

6.5.1. 例題1

リスト 15 GDB 例題1
1#include <stdio.h>
2
3int main()
4{
5    int x = 0;
6    int *p = 0;
7    *p = 100; // Segmentation fault
8    printf("%d\n", *p);
9}
  1. リスト 15 のプログラムを実行し,Segmentation faultが発生することを確認せよ.

  2. gdb 上で実行し,Segmentatin faultが発生した箇所で停止させよ.

  3. p コマンドを使って,例えば,p x, p &xp p の実行結果を確認してみよ.

  4. 以上の結果から,このプログラムは何が問題だったといえるか?

  • ヒント:上記のプログラムは6行目を int *p = &x; とすれば動作するだろう.何が違うのか?

6.5.2. 例題2

リスト 16 GDB 例題2
 1#include <stdio.h>
 2
 3int main()
 4{
 5    int i = 0;
 6
 7    printf("number: ");
 8    scanf("%d", i); // Segmentation fault
 9    printf("%d\n", i);
10
11    return 0;
12}
  1. リスト 16 のプログラムを実行し,Segmentation faultが発生することを確認せよ.

  2. gdb 上で実行し,Segmentatin faultが発生した箇所で停止させよ.

  3. bt コマンドを使って,自身のプログラムの何行目でエラーが発生しているのか,特定せよ.

  4. 特定した行を含むスタックフレームへ f で移動し,変数 i を表示してみよ. (例えば,#3 がそのスタックフレームであるならば,f 3 で移動できる.)

  5. 以上の結果から,このプログラムは何が問題だったといえるか.

6.6. DDDの使い方

DDDを用いることで,例えば,以下のようなGUIインタフェース上での gdb デバッグをすることができる.

../_images/gdb_screenshot_ddd.png

図 8 DDD (Data Display Debugger) によるデバッグ中の画面例

6.6.1. コンパイルと実行の方法

基本は,上述のgdbの使い方と同じである. gcc -g -O0 によるコンパイルを行い,その実行ファイルを ddd コマンド経由で実行する.

$ gcc -g -O0 -o test3 test3.c
$ ddd ./test3

6.6.2. 基本操作方法

DDDを使う際は,最低限の操作として,以下は覚えておこう.

  • ソースコード左端あたりを右クリックして,ブレークポイントを設定することができる

  • F2キーで run (プログラムの実行)できる

  • 緑の矢印は停止箇所(矢印の行はまだ実行していない.直前で止まっている.)

  • F5キーでブレーク個所からの step 実行(関数があればその中に入る)ができる

  • F6キーでブレーク個所からの next 実行(関数があれば実行して次の行)ができる

  • 実行途中で,ソースコードをダブルクリックすると,Data Displayに変数の情報が表示される

6.7. 付録1:Valgrindによるメモリ関連処理の診断

演習室の環境では Valgrind というデバッグ用ツールをインストールしている. このツールを用いることで,メモリに関する様々なトラブルを発見することができる. 例えば,バッファオーバーランやメモリリークなどの問題を検出することができる.

6.7.1. コンパイルと実行の方法

基本は,上述のgdbの使い方と同じである. gcc -g -O0 によるコンパイルを行い,その実行ファイルを valgrind 経由で実行する.

$ gcc -g -O0 -o test3 test3.c
$ valgrind ./test3

6.7.2. 例題3

リスト 17 Valgrindの練習(例題3)
 1#include <stdio.h>
 2#include <string.h>
 3
 4int main()
 5{
 6    char str1[] = "ABCDEFG";
 7    char* p_str;
 8
 9    p_str = malloc(8);     // <-- Change here, 8 --> 7, for Ex.1-2
10    strcpy(p_str, str1);   // <-- Buffer overrun, for Ex.1-2
11
12    printf("%s\n", p_str);
13
14    free(p_str);  // <-- Comment out here, for Ex.1-3
15
16    return 0;  // <-- Memory leak (Allocated memory should be free.), for Ex.1-3
17}

STEP1

  1. リスト 17 のプログラムをコンパイルし,通常通りプログラムを実行し,エラー無くプログラムが終了することを確認せよ.

  2. valgrind経由で上記のプログラムを実行し,エラー無しの場合のvalgrindのメッセージを確認せよ.

STEP2

  1. 9行目で,確保するバッファサイズを8以下(例えば7)にして,コンパイルせよ.

  2. 通常通りプログラムを実行し,(おそらく)エラー無くプログラムが終了することを確認せよ.

  3. valgrind経由で変更後のプログラムを実行し,そのメッセージを確認せよ.

  4. そして,その問題を解決せよ.(エラーメッセージを頼りに修正を試みること.)

※コンパイルオプション (gcc -g -O0) を忘れずに.

STEP3

  1. 14行目で,敢えて malloc したメモリの free を忘れ(例えば,該当行をコメントアウト)て,コンパイルせよ.

  2. 通常通りプログラムを実行し,(おそらく)エラー無くプログラムが終了することを確認せよ.

  3. valgrind経由で変更後のプログラムを実行し,そのメッセージを確認せよ.

  4. そして,その問題を解決せよ.(エラーメッセージを頼りに修正を試みること.)