4. 段錯誤

如果程序運行時出現段錯誤,用gdb可以很容易定位到究竟是哪一行引發的段錯誤,例如這個小程序:

例 10.4. 段錯誤調試實例一

#include <stdio.h>

int main(void)
{
	int man = 0;
	scanf("%d", man);
	return 0;
}

調試過程如下:

$ gdb main
...
(gdb) r
Starting program: /home/akaedu/main 
123

Program received signal SIGSEGV, Segmentation fault.
0xb7e1404b in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0  0xb7e1404b in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6
#1  0xb7e1dd2b in scanf () from /lib/tls/i686/cmov/libc.so.6
#2  0x0804839f in main () at main.c:6

gdb中運行,遇到段錯誤會自動停下來,這時可以用命令查看當前執行到哪一行代碼了。gdb顯示段錯誤出現在_IO_vfscanf函數中,用bt命令可以看到這個函數是被我們的scanf函數調用的,所以是scanf這一行代碼引發的段錯誤。仔細觀察程序發現是man前面少了個&。

繼續調試上一節的程序,上一節最後提出修正Bug的方法是在循環中加上判斷條件,如果不是數字就報錯退出,不僅輸入字母可以報錯退出,輸入超長的字元串也會報錯退出。表面上看這個程序無論怎麼運行都不出錯了,但假如我們把while (1)循環去掉,每次執行程序只轉換一個數:

例 10.5. 段錯誤調試實例二

#include <stdio.h>

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

	scanf("%s", input);
	for (i = 0; input[i] != '\0'; i++) {
		if (input[i] < '0' || input[i] > '9') {
			printf("Invalid input!\n");
			sum = -1;
			break;
		}
		sum = sum*10 + input[i] - '0';
	}
	printf("input=%d\n", sum);
	return 0;
}

然後輸入一個超長的字元串,看看會發生什麼:

$ ./main 
1234567890
Invalid input!
input=-1

看起來正常。再來一次,這次輸個更長的:

$ ./main 
1234567890abcdef
Invalid input!
input=-1
Segmentation fault

又出段錯誤了。我們按同樣的方法用gdb調試看看:

$ gdb main
...
(gdb) r
Starting program: /home/akaedu/main 
1234567890abcdef
Invalid input!
input=-1

Program received signal SIGSEGV, Segmentation fault.
0x0804848e in main () at main.c:19
19	}
(gdb) l
14			}
15			sum = sum*10 + input[i] - '0';
16		}
17		printf("input=%d\n", sum);
18		return 0;
19	}

gdb指出,段錯誤發生在第19行。可是這一行什麼都沒有啊,只有表示main函數結束的}括號。這可以算是一條規律,如果某個函數的局部變數發生訪問越界,有可能並不立即產生段錯誤,而是在函數返回時產生段錯誤

想要寫出Bug-free的程序是非常不容易的,即使scanf讀入字元串這麼一個簡單的函數調用都會隱藏着各種各樣的錯誤,有些錯誤現象是我們暫時沒法解釋的:為什麼變數i的存儲單元緊跟在input數組後面?為什麼同樣是訪問越界,有時出段錯誤有時不出段錯誤?為什麼訪問越界的段錯誤在函數返回時才出現?還有最基本的問題,為什麼scanf輸入整型變數就必須要加&,否則就出段錯誤,而輸入字元串就不要加&?這些問題在後續章節中都會解釋清楚。其實現在講scanf這個函數為時過早,讀者還不具備充足的基礎知識。但還是有必要講的,學完這一階段之後讀者應該能寫出有用的程序了,然而一個只有輸出而沒有輸入的程序算不上是有用的程序,另一方面也讓讀者認識到,學C語言不可能不去瞭解底層計算機體繫結構和操作系統的原理,不瞭解底層原理連一個scanf函數都沒辦法用好,更沒有辦法保證寫出正確的程序。