プログラミングなどなど

プログラミングに関係しそうな内容を

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;
}

コンパイル

デバッグ用にオプションをつけておきます。

$ gcc -Wall -g3 main.c -o main

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

うまく行っていそうです。