unoh.github.com

gdbの使い方

2008-04-04 10:22:50 +0000

今年の2月にマカーになったbokkoです。どうも僕の使っているフォントがほかの人には見づらいらしく、「そのフォントはねぇよw」と言われたり、外付けのキーボードを使っているせいか、「MacBookの意味なし!」と社内で言われてたりしています。

今日はgdbのお話です。gdbは非常に広く使われているデバッガで、特にC、C++のプログラムをデバッグするのによく使われています。

デバッガの使い方



プログラムをデバッグする際、例えば以下の方法が挙げられます。

1. ソースコードを読む
2. ソースコードに出力関数を仕込む(例えばprintf)
3. ソースコードを書き換えて実行してみる

これで十分な場合もありますが、そうでない場合もあります。これらの方法ではプログラムを実行している最中にこちらからソースコードレベルでのアクションを起こすことが難しいので、例えば、プログラムをある時点で止めて変数の内容を確認するといったことが困難です。2と3でもできないことはないですが、CやC++だと何か別のことをやろうとする度にコンパイルし直す必要があります。そこでデバッガの出番です。デバッガを使えばプログラムを1行ずつ実行しながら変数の内容を確認したり、あるループ内の処理がどう実行されてどのようにそのループを抜けたのか確認するのがやりやすくなります。


プログラムにデバッグ情報を持たせる



例えば、以下のように(ものすごく単純に)配列をランダムにソートするCのプログラムがあるとします。

/* random_sort.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define COUNT(arr) (sizeof(arr)/sizeof(arr[0]))
void random_sort(int arr[], int n);
void random_sort(int arr[], int n){
  int i, rnd, temp;
  srand(time(NULL));
  for(i=0;i<n;i++){
    rnd = rand() % n;
    temp = arr[i];
    arr[i] = arr[rnd];
    arr[rnd] = temp;
  }
}
int main(int argc, char *argv[]){
  int i, n;
  int nums[] = {1, 2, 3, 4, 5};
  n = COUNT(nums);
  random_sort(nums, n);
  for(i=0;i<n;i++){
    printf("%d ", nums[i]);
  }
  printf("\n");
  return 0;
}



このプログラムをgccでコンパイルしてgdbで実行するためには例えば、

$ gcc -g random_sort.c -o random_sort


とします。-gはプログラムにデバッグ情報を含ませるためのオプションです。(実際にはCでプログラムを書く際は警告を見逃さないように、もっとオプションを付け足したりするのですが、ここでは割愛します)。作成した実行プログラムをgdbに引数で渡してやります。

$ gdb ./random_sort                         7165 (c/tips/random_sort)
GNU gdb 6.3.50-20050815 (Apple version gdb-768) (Tue Oct  2 04:07:49 UTC 2007)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-apple-darwin"...Reading symbols for shared libraries ... done
(gdb)


これでgdbを使う準備ができました。次はよく使うコマンドについて解説します。

プログラムを実行する



プログラムを実行するにはrunコマンドを使います。

(gdb) run
Starting program: /Users/bokkko/programming/c/tips/random_sort/random_sort
Reading symbols for shared libraries ++. done
4 3 2 5 1
Program exited normally.
(gdb)


ただ単にrunコマンドを実行してもプログラムが正常、もしくは異常終了したり、ユーザからの入力待ちになったりするだけなので、この後は自分が挙動を調べたい関数にブレークポイントを設定したりします。

ブレークポイントを設定する



ブレークポイントを設定するにはbreakコマンドを使います。

(gdb) break random_sort
Breakpoint 1 at 0x1ef0: file random_sort.c, line 13.
(gdb)


関数だけでなく、行単位で設定することもできます。

(gdb) b 15
Breakpoint 1 at 0x1f04: file random_sort.c, line 15.
(gdb) r
Starting program: /Users/bokkko/programming/c/tips/random_sort/random_sort
Reading symbols for shared libraries ++. done
Breakpoint 1, random_sort (arr=0xbfffed24, n=5) at random_sort.c:15
15        for(i=0;i<n;i++){
(gdb)


プログラムを1行ずつ実行する



プログラムを1行ずつ実行するにはnextコマンドを使います。

(gdb) b main
Breakpoint 1 at 0x1f77: file random_sort.c, line 26.
(gdb) r
Starting program: /Users/bokkko/programming/c/tips/random_sort/random_sort
Reading symbols for shared libraries ++. done
Breakpoint 1, main (argc=1, argv=0xbfffed74) at random_sort.c:26
26        int nums[] = {1, 2, 3, 4, 5};
(gdb) next
27        n = COUNT(nums);
(gdb) next
28        random_sort(nums, n);
(gdb) next
30        for(i=0;<n;i++){
(gdb)


ソースコードを表示する



listコマンドを使うと一部のソースコードを表示することができます。
引数なしで実行した場合、現在の行の前後合わせて10行を表示します。

(gdb) b main
Breakpoint 1 at 0x1f77: file random_sort.c, line 26.
(gdb) r
Starting program: /Users/bokkko/programming/c/tips/random_sort/random_sort
Reading symbols for shared libraries ++. done
Breakpoint 1, main (argc=1, argv=0xbfffed74) at random_sort.c:26
26        int nums[] = {1, 2, 3, 4, 5};
(gdb) l
21      }
22
23      int main(int argc, char *argv[]){
24       
25        int i, n;
26        int nums[] = {1, 2, 3, 4, 5};
27        n = COUNT(nums);
28        random_sort(nums, n);
29
30        for(i=0;i<n;i++){
(gdb)


引数には関数や行番号を指定することもできます。

変数の内容を表示する



変数の内容を表示するにはprintコマンドを使います。

(gdb) print i
$5 = 0
(gdb)


また、単に変数を表示するだけでなく、計算もできます。

(gdb) pirnt i + 10
$5 = 10
(gdb)


配列の中身も以下のように表示できます。

(gdb) print nums
$1 = {1, 2, 3, 4, 5}
(gdb)


上記のソースコードにはありませんが、構造体の内容を表示することもできます。例えば、以下のような構造体とソースコードがあった場合、

typedef struct list {
  struct list *next;
  int data;
} List;


       ・
       ・
       ・
  List *l;
  l = (List *)malloc(sizeof(List));
  l->next = NULL;
  l->data = (int)NULL;
> return l;
       ・
       ・
       ・


//>で↓を実行
(gdb) p *l
  $1 = {
  next = 0x0,
  data = 0
}
(gdb)


pはprintのエイリアスです。gdbでは長かったり、よく使うコマンドにはエイリアスが用意されています。

また、環境によっては構造体の内容が上記のように改行されず、1行で表示されてしまっているかもしれません。この場合、ホームディレクトリに.gdbinitというファイルを作成して、

set print pretty on


と記述すれば幸せになれるかもしれません。ただ、構造体のメンバの数が多い場合、画面からはみ出ししまうことがあります。上記の設定はgdbを実行している途中で切り替えることもできるので、状況にあった表示を選択するといいでしょう。

(gdb) set print pretty off
(gdb) p *l
$1 = {next = 0x0, data = 0}
(gdb) set print pretty on
(gdb) p *l
$2 = {
  next = 0x0,
  data = 0
}
(gdb) 


バックトレースを表示する



自分が今、プログラムのどの関数を実行しているのか知りたい場合、backtraceコマンドを使います。以下はステップ実行してrandom_sort関数に入ってbacktraceを実行した様子です。

(gdb) backtrace
#0  random_sort (arr=0xbfffed24, n=5) at random_sort.c:15
#1  0x00001fb3 in main (argc=1, argv=0xbfffed74) at random_sort.c:28
(gdb)


このように自分が実行している関数がどこから呼び出されているのかもわかります。

変数の内容を監視する



変数の内容を表示するには先述したprintコマンドが使えますが、nextコマンドなどでプログラムをステップ実行しながら毎回指定した変数の内容を表示するにはdisplayコマンドを使います。

以下はforループ内でrand関数で生成した数値を監視している様子です。

(gdb) b random_sort
Breakpoint 1 at 0x1ef0: file random_sort.c, line 13.
(gdb) r
Starting program: /Users/bokkko/programming/c/tips/random_sort/random_sort
Reading symbols for shared libraries ++. done
Breakpoint 1, random_sort (arr=0xbfffed24, n=5) at random_sort.c:13
13        srand(time(NULL));
(gdb) display rnd
1: rnd = 0
(gdb) n
15        for(i=0;i<n;i++){
1: rnd = 0
(gdb)
16          rnd = rand() % n;
1: rnd = 0
(gdb)
17          temp = arr[i];
1: rnd = 4
(gdb)
18          arr[i] = arr[rnd];
1: rnd = 4
(gdb)
19          arr[rnd] = temp;
1: rnd = 4
(gdb)
15        for(i=0;i<n;i++){
1: rnd = 4
(gdb)
16          rnd = rand() % n;
1: rnd = 4
(gdb)
17          temp = arr[i];
1: rnd = 2
(gdb)


このように変数の内容がどこでどう変更されたかがわかります。nはnextコマンドのエイリアスで、プログラムの次の行を実行します。

Emacs上でgdbを使う




gdbはコマンドラインで使っていると不便に思うことがあります。例えば、実際のコードのどこが実行されているかわかりにくいですし、listコマンドは使う度に頭の中でコンテキストスイッチをしているようなもので、あまりいいものではありません。なので僕は普段、gdbはEmacs上で使っています。Emacs上でgdbを起動するにはM-x gdbを実行します。実行している様子は以下のような感じです。

gdb.png


このようにlistコマンドを使うことなく、ソースコードを見ながらデバッグできます。Visual StudioやEclipseのデバッグ機能を利用したことがある人はこっちの方がなじみやすいかもしれません。プログラム書く時はvi(m)だけど、「gdb使う時はEmacs上で」という人もいるそうです。

参考文献



GDBデバッギング入門
GDBデバッギング入門
posted with amazlet at 08.04.04
リチャード ストールマン ローランド ペシュ
アスキー
売り上げランキング: 42902
おすすめ度の平均: 4.0
5 Debugging with GDB
3 入門書ではありません



GDBハンドブック
GDBハンドブック
posted with amazlet at 08.04.04
アーノルド ロビンス
オライリージャパン
売り上げランキング: 55079
おすすめ度の平均: 4.0
5 単なるリファレンスとして使うもの
3 開発慣れしてる人でGDB忘れた人.お勧めです






追記:(2008/04/07)


上記の配列をランダムに並び替えるプログラムですが、ある方から上記のコードでは均等にランダムにならない、とのご指摘を頂きました。均等にランダムになるには関数random_sortの↓の部分を、

  for(i=0;i<n;i++){                                                                            
    rnd = rand() % n;                                                                          
    temp = arr[i];                                                                             
    arr[i] = arr[rnd];                                                                         
    arr[rnd] = temp;                                                                           
  }                                                                                            


↓のようにするのが正しいです。すみませんでした。

  for(i=0;i<n-1;i++){
    rnd = rand() % (n - i) + i;
    temp = arr[i];
    arr[i] = arr[rnd];
    arr[rnd] = temp;
  }


また、修正にあたり、こちらを参考にさせて頂きました。


この場を借りて御二方に感謝します。ありがとうございました。