第5回:GDBでデバッグ

ホーム ] 上へ ] 第1回:基本作法 ] 第2回:データ型 ] 第3回:関数 ] 第4回:分割コンパイル ] [ 第5回:GDBでデバッグ ] 第6回:ポインタ初級編 ] 第7回:配列とポインタ ] 第8回:構造体とポインタ ] 第9回:文字列とポインタ ] 番外編:配列の様に使えるリスト ]

最終更新日: 03/02/21     藤岡研外部からのアクセス数: 

 

GDBとは?

   GNU が開発・配布している、フリーのソースレベルデバッガです。プログラムのソースコードを見ながら、実行を途中で止めたり、変数の値を見たり、1行づつ実行したり、といったことができます。

 

GDBを使う際の注意

   過信しないこと。普通に実行するとエラーが出るんだけど、GDB 上で実行すると、ちゃんと実行される。などということが良くあります。基本は、あくまでも、printf などを使ったデバッグ法です。GDB は、あくまで、それを便利にするためのツールに過ぎません。

 

GDBを使うための準備

コンパイルを行う際、-g (または、-ggdb) オプションを付けてコンパイルする。

サンプルプログラム1 (lecture4-1.c) をコンパイルしてみよ。

また、以下の説明でも、適宜、サンプルプログラムを用いて練習せよ。

 

起動・終了

bulletMule (Emacs) 上で、デバッグする場合

ESC, x, gdb, Return, <program name>, Return

で起動。<program name> の部分には、デバッグしたいプログラムの実行ファイル名を指定。

GDBウィンドウで、quit, Return と入力すれば、終了。

bulletコマンドライン上で、デバッグする場合

% gdb <program name>

で起動、

(gdb) quit

で終了。

   Mule 上でデバッグした方がなにかと便利なので、以下では、Mule 上でデバッグする場合についてのみ説明する。

 

簡単な使い方 (初級編)

bulletブレークポイントの設定 (プログラムの実行を、ある部分で一時停止するようにしておく)

(gdb) break <function name>

<function name> の部分には、実行を止めたい関数名を指定する。

以上の様に指定した後、次の run コマンドを実行すると、その関数の最初の行で一時停止する。

bulletプログラムの実行

(gdb) run

プログラムが引数を必要とする場合は、run の後に引数を記述。

ブレークポイントに出会うと、プログラムの実行は一時停止され、プロンプト (gdb) が現れる。

bulletステップ実行

(gdb) next  または  (gdb)  n   または   C-x, C-a, C-n

一時停止中の行を実行し、次の行で、再び停止する。

bullet変数の表示

(gdb) print <variable name>  または   (gdb) p <variable name>

<variable name> の部分には、表示したい変数名を指定する。

bullet実行再開

(gdb) continue  または (gdb) c    または   C-x, C-a, C-r

ちょっと進んだ使い方 (中級編)

bulletブレークポイントの設定 (関数中の任意の箇所で実行を停止する)

一時停止したい箇所が含まれるソースファイルを開き、その箇所へカーソルを移動 (Mule で、複数ファイルを開く方法は、ここ (Mule で複数ファイルを扱う) を参照)。

ESC, x, gud-break, Return   または   C-x, Space    または  C-x, C-a, C-b

bulletブレークポイントの一覧表示

(gdb) info breakpoints

bulletブレークポイントの削除

(gdb) delete <number>

<number> の部分には、ブレークポイントの番号 (info breakpoints を実行した時に、左端に表示される) を指定。

bullet一時停止箇所のソースコードの表示

ブレークポイントで一時停止すると、自動的に、その箇所のソースファイルが表示され、停止箇所に => が表示される。その後、ソースファイルを閉じてしまい、再度、一時停箇所のソースコードを表示シたい場合は、

ESC, x, gud-refresh  または  C-x, C-a, C-l

bullet変数の表示

ソースコードの変数名が書かれている部分にカーソルを移動し、

ESC, x, gud-print, Return  または  C-x, C-a, C-p

としても、変数の値を表示できる。また、

(gdb) print <expression>

として、式の計算も可能。<expression> の部分に、計算したい式を記述する。

ポインタ変数の場合は、

(gdb) print *<pointer>

とすれば、ポインタの指し示すメモリの内容が表示される。<pointer> の部分に、ポインタ変数名を指定。

print コマンドで表示した値は、$1, $2, $3, ... という、特殊な変数に記録されていく。これらの変数を、後で式に使うこともできる。

bullet関数呼び出しの内部もステップ実行

現在停止中の行に関数呼出しが含まれる場合でも、next コマンドを実行すると、その次の行で再停止する (その関数の内部では停止しない)。

(gdb) step   または   (gdb) s   または C-x, C-a, C-s

とすると、その関数の最初の行で停止する。

bullet関数の最後まで実行

(gdb) finish   または  (gdb) f   または C-x, C-a, C-f

現在停止している箇所を含む関数を、最後まで実行し、その次の行で停止する。

bullet

コマンド、関数名、変数名の補完

(gdb) のプロンプトに続いて、コマンド、関数名、変数名を入力中、TAB キーにより、これらの名前を補完 (名前の残りの部分を自動的に入力) することができる。

 

さらに進んだ使い方 (上級編)

bullet条件つきブレークポイント

前述の方法でブレークポイントを設定し、

(gdb) condition <number> <expression>    または   (gdb) cond <number> <expression>

<number> の部分にブレークポイントの番号、<expression> の部分に条件式を指定。プログラムの事項がブレークポイントに到達し、条件式が成立した時のみ、一時停止する。

例えば、次のような サンプルプログラム1 (lecture4-1.c) を考える。

#include <stdio.h>
int main(void)
{
  int i;
  int a;
  for(i=0; i<1000; i++)
    {
      if( i%100 == 0 )
        {
=>        a = i/100;
          printf("%d\n", a);
        }
    }
  return 0;
}

のようなコードがあり、=> の行に1番のブレークポイントが設定されているとする。ここで、

(gdb) cond  1  i == 500

として、プログラムを実行すると、ループの500回目 (i == 500 の時) のみ、一時停止する。

bulletウォッチポイント

(gdb) watch <variable name>

<variable name> の部分に、変数名を指定。変数の値が変化した時に実行が停止される。いつの間にか、変数の値が変わってしまっている?! という時などに便利。但し、実行が非常に遅くなる。

bullet変数の値を変える

(gdb) print <variable name>=<expression>    または
(gdb) set var <variable name>=<expression>

<variable name> の部分に変数名、<expression> の部分に値または式を指定。

bullet関数を呼び出す

(gdb) call <function name>( <param1>,   <param2>,  .....  )

<function name> の部分に関数名、<param1>, <param2>, ..... の部分にパラメータを指定。

但し、関数内でグローバル変数を書き換えるなど、プログラムの他の部分に影響を与えるコードがある場合、その後の実行結果が変わってくるので、注意。

bulletフレーム (注目している関数) の変更

main()  →  sub()  →  subsub()   と、関数呼び出しが行われており、subsub() の内部で実行が一時停止しされている場合を考える。

このとき、参照できる (print コマンドで表示するなど) のは、グローバル変数と subsub() のローカル変数のみである。main() や sub() のローカル変数は参照できない。

sub() のローカル変数を参照するためには、まず

(gdb) up

として、注目関数を一つ上の sub() に移動する。GDB では、この注目関数のことをフレームと読んでいる。このとき、プログラムの実行は、subsub() の内部で止まったままである。

さらに、

(gdb) up

とすれば、フレームは main() に移り、

(gdb) down

とすれば、subsub() に戻る。

フレームがどこか分からなくなった時は、以下のようにすれば表示される。

(gdb) frame

また、以下のようにすると、main() からどのような順序で関数が呼び出されたかが表示される。

(gdb) where

#0 subsub () at tmp1.c:5
#1 0x400a68 in sub () at tmp1.c:14
#2 0x400b14 in main () at tmp1.c:26

上記の例は、main()  →  sub()  →  subsub() と関数が呼び出され、現在 subsub() の中 (tmp1.c の 5 行目) で実行が停止されていることを示している。

 

Segmentation fault のデバッグ

プログラムを実行すると、Segmentation fault と表示されてプログラムが以上終了してしまう場合がある。たいていは、ポインタの値が間違っていることが原因である。

Segmentation fault は、通常の実行では、どこでエラーが発生しているのか分からないので、デバッグするのが難しい。しかし、デバッガを使えば、簡単にエラー箇所を見つけることができる。

次のような サンプルプログラム2 (lecture4-2.c) を考える。

#include <stdio.h>

int g;

int *subsub()
{
  int *p;

  p = &g;
  p = NULL;

  return p;
}

int sub()
{
  int a;
  int *ptr;

  ptr = subsub();
  a = *ptr;

  return a * 10;
}

main()
{
  g = 3;
  printf ("%d\n", sub());

  return 0;
}

これを実行すると、Segmentation fault が発生する。この原因を突き止めるには、 

  1. GDB でプログラムを実行する。
  2. Segmentation fault が発生すると、

  3. Program received signal SIGSEGV, Segmentation fault.
    0x400ad4 in sub () at lecture4-2.c:21

  4. の様に表示される。関数 sub() の内部、lecture4-2.c の 21 行目でエラーが出ていることが分かる。同時に、ソースファイルが表示され、この箇所に => が表示されているはずである。
  5. この行で参照されているポインタ変数の値を表示する。この場合、int *ptr が怪しいので、これを表示する。

    (gdb) print ptr
    $1 = (int *) 0x0

  6. 大抵は、ポインタの値が 0 になっているはずである。本来なら、ここに、何らかの値が表示されなければいけない。
  7. ptr に値を設定している部分を見てみると、20行目で ptr = subsub(); としている。
  8. subsub() の最後の return 部分を見てみると、12行目 return p; となっている。
  9. p の値を設定している部分を見てみると、10行目 p = NULL; となっている (NULL は 0 である)。これが、原因である。
  10. この行は、Segmentation fault を出すためにわざと入れた行で、本当は、その前の行 p = &g; によって、p の値が設定されていなければならない。p = NULL; を削除してコンパイルすれば、このプログラムは動作する。
 

Mail to: < miura@ise.eng.osaka-u.ac.jp >