如果程序運行時出現段錯誤,用gdb
可以很容易定位到究竟是哪一行引發的段錯誤,例如這個小程序:
調試過程如下:
$ 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
函數都沒辦法用好,更沒有辦法保證寫出正確的程序。