2. 斷點

看以下程序:

例 10.2. 斷點調試實例

#include <stdio.h>

int main(void)
{
	int sum = 0, i = 0;
	char input[5];

	while (1) {
		scanf("%s", input);
		for (i = 0; input[i] != '\0'; i++)
			sum = sum*10 + input[i] - '0';
		printf("input=%d\n", sum);
	}
	return 0;
}

這個程序的作用是:首先從鍵盤讀入一串數字存到字元數組input中,然後轉換成整型存到sum中,然後打印出來,一直這樣循環下去。scanf("%s", input);這個調用的功能是等待用戶輸入一個字元串並回車,scanf把其中第一段非空白(非空格、Tab、換行)的字元串保存到input數組中,並自動在末尾添加'\0'。接下來的循環從左到右掃瞄字元串並把每個數字累加到結果中,例如輸入是"2345",則循環累加的過程是(((0*10+2)*10+3)*10+4)*10+5=2345。注意字元型的'2'要減去'0'的ASCII碼才能轉換成整數值2。下面編譯運行程序看看有什麼問題:

$ gcc main.c -g -o main
$ ./main 
123
input=123
234
input=123234
(Ctrl-C退出程序)
$

又是這種現象,第一次是對的,第二次就不對。可是這個程序我們並沒有忘了賦初值,不僅sum賦了初值,連不必賦初值的i都賦了初值。讀者先試試只看代碼能不能看出錯誤原因。下面來調試:

$ gdb main
...
(gdb) start
Breakpoint 1 at 0x80483b5: file main.c, line 5.
Starting program: /home/akaedu/main 
main () at main.c:5
5		int sum = 0, i = 0;

有了上一次的經驗,sum被列為重點懷疑對象,我們可以用display命令使得每次停下來的時候都顯示當前sum的值,然後繼續往下走:

(gdb) display sum
1: sum = -1208103488
(gdb) n
9			scanf("%s", input);
1: sum = 0
(gdb) 
123
10			for (i = 0; input[i] != '\0'; i++)
1: sum = 0

undisplay命令可以取消跟蹤顯示,變數sum的編號是1,可以用undisplay 1命令取消它的跟蹤顯示。這個循環應該沒有問題,因為上面第一次輸入時打印的結果是正確的。如果不想一步一步走這個循環,可以用break命令(簡寫為b)在第9行設一個斷點(Breakpoint)

(gdb) l
5		int sum = 0, i;
6		char input[5];
7	
8		while (1) {
9			scanf("%s", input);
10			for (i = 0; input[i] != '\0'; i++)
11				sum = sum*10 + input[i] - '0';
12			printf("input=%d\n", sum);
13		}
14		return 0;
(gdb) b 9
Breakpoint 2 at 0x80483bc: file main.c, line 9.

break命令的參數也可以是函數名,表示在某個函數開頭設斷點。現在用continue命令(簡寫為c)連續運行而非單步運行,程序到達斷點會自動停下來,這樣就可以停在下一次循環的開頭:

(gdb) c
Continuing.
input=123

Breakpoint 2, main () at main.c:9
9			scanf("%s", input);
1: sum = 123

然後輸入新的字元串準備轉換:

(gdb) n
234
10			for (i = 0; input[i] != '\0'; i++)
1: sum = 123

問題暴露出來了,新的轉換應該再次從0開始累加,而sum現在已經是123了,原因在於新的循環沒有把sum歸零。可見斷點有助于快速跳過沒有問題的代碼,然後在有問題的代碼上慢慢走慢慢分析,“斷點加單步”是使用調試器的基本方法。至于應該在哪裡設置斷點,怎麼知道哪些代碼可以跳過而哪些代碼要慢慢走,也要通過對錯誤現象的分析和假設來確定,以前我們用printf打印中間結果時也要分析應該在哪裡插入printf,打印哪些中間結果,調試的基本思路是一樣的。一次調試可以設置多個斷點,用info命令可以查看已經設置的斷點:

(gdb) b 12
Breakpoint 3 at 0x8048411: file main.c, line 12.
(gdb) i breakpoints
Num     Type           Disp Enb Address    What
2       breakpoint     keep y   0x080483c3 in main at main.c:9
	breakpoint already hit 1 time
3       breakpoint     keep y   0x08048411 in main at main.c:12

每個斷點都有一個編號,可以用編號指定刪除某個斷點:

(gdb) delete breakpoints 2
(gdb) i breakpoints 
Num     Type           Disp Enb Address    What
3       breakpoint     keep y   0x08048411 in main at main.c:12

有時候一個斷點暫時不用可以禁用掉而不必刪除,這樣以後想用的時候可以直接啟用,而不必重新從代碼裡找應該在哪一行設斷點:

(gdb) disable breakpoints 3
(gdb) i breakpoints 
Num     Type           Disp Enb Address    What
3       breakpoint     keep n   0x08048411 in main at main.c:12
(gdb) enable 3
(gdb) i breakpoints 
Num     Type           Disp Enb Address    What
3       breakpoint     keep y   0x08048411 in main at main.c:12
(gdb) delete breakpoints 
Delete all breakpoints? (y or n) y
(gdb) i breakpoints
No breakpoints or watchpoints.

gdb的斷點功能非常靈活,還可以設置斷點在滿足某個條件時才激活,例如我們仍然在循環開頭設置斷點,但是僅當sum不等於0時才中斷,然後用run命令(簡寫為r)重新從程序開頭連續運行:

(gdb) break 9 if sum != 0
Breakpoint 5 at 0x80483c3: file main.c, line 9.
(gdb) i breakpoints 
Num     Type           Disp Enb Address    What
5       breakpoint     keep y   0x080483c3 in main at main.c:9
	stop only if sum != 0
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/akaedu/main 
123
input=123

Breakpoint 5, main () at main.c:9
9			scanf("%s", input);
1: sum = 123

結果是第一次執行scanf之前沒有中斷,第二次卻中斷了。總結一下本節用到的gdb命令:

表 10.2. gdb基本命令2

命令描述
break(或b) 行號在某一行設置斷點
break 函數名在某個函數開頭設置斷點
break ... if ...設置條件斷點
continue(或c)從當前位置開始連續運行程序
delete breakpoints 斷點號刪除斷點
display 變數名跟蹤查看某個變數,每次停下來都顯示它的值
disable breakpoints 斷點號禁用斷點
enable 斷點號啟用斷點
info(或i) breakpoints查看當前設置了哪些斷點
run(或r)從頭開始連續運行程序
undisplay 跟蹤顯示號取消跟蹤顯示

習題

1、看下面的程序:

#include <stdio.h>

int main(void)
{
	int i;
	char str[6] = "hello";
	char reverse_str[6] = "";

	printf("%s\n", str);
	for (i = 0; i < 5; i++)
		reverse_str[5-i] = str[i];
	printf("%s\n", reverse_str);
	return 0;
}

首先用字元串"hello"初始化一個字元數組str(算上'\0'共6個字元)。然後用空字元串""初始化一個同樣長的字元數組reverse_str,相當於所有元素用'\0'初始化。然後打印str,把str倒序存入reverse_str,再打印reverse_str。然而結果並不正確:

$ ./main 
hello

我們本來希望reverse_str打印出來是olleh,結果什麼都沒有。重點懷疑對象肯定是循環,那麼簡單驗算一下,i=0時,reverse_str[5]=str[0],也就是'h'i=1時,reverse_str[4]=str[1],也就是'e',依此類推,i=0,1,2,3,4,共5次循環,正好把h,e,l,l,o五個字母給倒過來了,哪裡不對了?用gdb跟蹤循環,找出錯誤原因並改正。