C言語:GDBによるデバッグ
0からnまでの整数の和を求める例を題材にデバッガ(gdb)の使用例を紹介したいと思います。バグがある状態から始めます。
main.c(バグ有り)
int sum(int n) { int acc = 0; int i; for (i = 0; i < n; i++) { acc += i; } return acc; } int main(void) { int n = 10; int wa = sum(n); return wa; }
GDB配下でのプログラム実行
gdbは起動中のプログラムにアタッチす方法も有りますが、今回はgdbの引数にプログラムを指定して起動します。
$ gdb -q main Reading symbols from main...done. (gdb) run Startping program: /home/makoto/blog/22_Cdebug/main [Inferior 1 (process 4924) exited with code 055] (gdb) p $_exitcode $1 = 45
1から10までの和なので55になってほしいのですが、45になっています。
ステップ実行していく
ひとまず関数のトップであるmainから調べていくことにします。
mainで止まるようにブレークポイントを設定します。
(gdb) break main Breakpoint 1 at 0x555555554630: file main.c, line 15. (gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x0000555555554630 in main at main.c:15 breakpoint already hit 1 time
main.cの15行目にブレークポイントが設定されています。再度プログラム実行します。
(gdb) run Starting program: /home/makoto/blog/22_Cdebug/main
main関数でプログラムが止まります。
Breakpoint 1, main () at main.c:15 15 int n = 10;
まだ、「int n = 10」は実行されていません。試しに変数nの値を調べてみます。
(gdb) print n $2 = 0
1文進めます。($Xの部分は気にしなくて大丈夫です。)
(gdb) next 16 int wa = sum(n);
今度はnが10で初期化されているはずです。
(gdb) print n $3 = 10
もう1文進めます。
(gdb) next 18 return wa; (gdb) print wa $4 = 45
結果が45になっています。sumが悪そうです。mainのブレークポイントを削除して、今度はsumに設定してみます。
(gdb) delete 1 (gdb) break sum Breakpoint 2 at 0x555555554601: file main.c, line 3. (gdb) info breakpoints Num Type Disp Enb Address What 2 breakpoint keep y 0x0000555555554601 in sum at main.c:3
再度最初から実行します。
(gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/makoto/blog/22_Cdebug/main Breakpoint 2, sum (n=10) at main.c:3 3 int acc = 0;
今度はsum(main.cの3行目)で止まります。1文実行してみます。
(gdb) next 6 for (i = 0; i < n; i++) (gdb) print acc $8 = 0
accが0で初期化されています。for文のなかまで進めてみます。
(gdb) next 8 acc += i;
最初のループを終え、行が戻ります。
(gdb) next 6 for (i = 0; i < n; i++)
ここで各種値を確認してみます。
(gdb) print acc $5 = 0 (gdb) print i $6 = 0
このタイミングではまだiはインクリメントされていません。継続条件も確認してみます。
(gdb) print i < n $3 = 1
成立しています。もう一文進めてみます。
(gdb) next 8 acc += i;
i++が実行されているはずです。
(gdb) print i $5 = 1
iが1なので今度はaccに1が加えられるはずです。
(gdb) next 6 for (i = 0; i < n; i++) (gdb) print acc $6 = 1
加えられています。ループを一気に進めてみます。
(gdb) until 10 return acc; (gdb) print acc $7 = 45
accが45となっていて10足りません。この時のiと各種値を見てみます。
(gdb) print i $10 = 10 (gdb) print n $11 = 10 (gdb) print i < n $12 = 0
iは10になっていますが、i < n が成立せずにループを終了していそうです。もう少し詳しく見てみます。再度実行します。
(gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/makoto/blog/22_Cdebug/main Breakpoint 2, sum (n=10) at main.c:3 3 int acc = 0;
直前のiが9の部分で止まるようにして、そこから詳しく見てみたいと思います。
(gdb) watch i == 9 Hardware watchpoint 3: i == 9
実行を進めます。
(gdb) continue Continuing. Hardware watchpoint 3: i == 9 Old value = 0 New value = 1 0x000055555555461b in sum (n=10) at main.c:6 6 for (i = 0; i < n; i++)
値を確認します。
(gdb) print i $10 = 9 (gdb) print acc $11 = 36
i++が実行された直後ですね。
(gdb) next 8 acc += i; (gdb) next 6 for (i = 0; i < n; i++) (gdb) print acc $13 = 45
36に9が加えられて45になっています。更に実行を継続してみます。
(gdb) continue Continuing. Hardware watchpoint 7: i == 9 Old value = 1 New value = 0 0x000055555555461b in sum (n=10) at main.c:6 6 for (i = 0; i < n; i++) (gdb) print i $29 = 10 (gdb) print n $30 = 10 (gdb) print i < n $31 = 0
i == 9の条件が破れたところで再度止まってくれました。各種値を確認してみると、i,nが10となりi < nの継続条件が成り立っていません。C言語からはインクリメントと継続条件のどちらが先か見えませんが、このような場合は逆アセンブルしてみると見えてきます。(私はアセンブラの理解が怪しいので誤っていたらすみません。)
(gdb) disassemble Dump of assembler code for function sum: 0x00005555555545fa <+0>: push %rbp 0x00005555555545fb <+1>: mov %rsp,%rbp 0x00005555555545fe <+4>: mov %edi,-0x14(%rbp) 0x0000555555554601 <+7>: movl $0x0,-0x8(%rbp) 0x0000555555554608 <+14>: movl $0x0,-0x4(%rbp) 0x000055555555460f <+21>: jmp 0x55555555461b <sum+33> 0x0000555555554611 <+23>: mov -0x4(%rbp),%eax 0x0000555555554614 <+26>: add %eax,-0x8(%rbp) 0x0000555555554617 <+29>: addl $0x1,-0x4(%rbp) => 0x000055555555461b <+33>: mov -0x4(%rbp),%eax 0x000055555555461e <+36>: cmp -0x14(%rbp),%eax 0x0000555555554621 <+39>: jl 0x555555554611 <sum+23> 0x0000555555554623 <+41>: mov -0x8(%rbp),%eax 0x0000555555554626 <+44>: pop %rbp 0x0000555555554627 <+45>: retq End of assembler dump.
+29でインクリメントされていて+36の部分で継続条件を判定しているようです。
rbp-0x4の場所が変数iの場所ですね。
(gdb) print *(int *)($rbp-0x4) $32 = 10
nが10の場合はaccに加えたいので、継続条件をi <= nに修正することにします。
int sum(int n) { int acc = 0; int i; for (i = 0; i <= n; i++) { acc += i; } return acc; }
コンパイルして再度実行してみます。(コンパイルば別のターミナルから実行してgdbは別ターミナルで実行したままにしています。)
(gdb) run Starting program: /home/makoto/blog/22_Cdebug/main warning: Probes-based dynamic linker interface failed. Reverting to original interface. Breakpoint 2, sum (n=10) at main.c:3 3 int acc = 0; (gdb) until 6 for (i = 0; i <= n; i++) (gdb) 8 acc += i; (gdb) 6 for (i = 0; i <= n; i++) (gdb) 10 return acc; (gdb) print acc $33 = 55
うまく行っていそうです。