GDB實戰

程序中除了一目瞭然的Bug以外都須要必定的調試手段來分析到底錯在哪。到目前爲止咱們的調試手段只有一種:根據程序執行時的出錯現象假設錯誤緣由,而後在代碼中適當的位置插入 printf ,執行程序並分析打印結果,若是結果和預期的同樣,就基本上證實了本身假設的錯誤緣由,就能夠動手修Bug了,若是結果和預期的不同,就根據結果作進一步的假設和分析。html

本章咱們介紹一種很強大的調試工具 gdb ,能夠徹底操控程序的運行,使得程序就像你手裏的玩具同樣,叫它走就走,叫它停就停,而且隨時能夠查看程序中全部的內部狀態,好比各變量的值、傳給函數的參數、當前執行的代碼行等。掌握了 gdb 的用法以後,調試手段就更加豐富了。但要注意,即便調試手段豐富了,調試的基本思想仍然是「分析現象→假設錯誤緣由→產生新的現象去驗證假設」這樣一個循環,根據現象如何假設錯誤緣由,以及如何設計新的現象去驗證假設,這都須要很是嚴密的分析和思考,若是由於手裏有了強大的工具就濫用而忽略了分析過程,每每會治標不治本地修正Bug,致使一個錯誤現象消失了但Bug仍然存在,甚至是把程序越改越錯。本章經過初學者易犯的幾個錯誤實例來說解如何使用 gdb 調試程序,在每一個實例後面總結一部分經常使用的 gdb 命令。python

 

gdb基本命令1
命令 描述
backtrace(或bt) 查看各級函數調用及參數
finish 連續運行到當前函數返回爲止,而後停下來等待命令
frame(或f) 幀編號 選擇棧幀
info(或i) locals 查看當前棧幀局部變量的值
list(或l) 列出源代碼,接着上次的位置往下列,每次列10行
list 行號 列出從第幾行開始的源代碼
list 函數名 列出某個函數的源代碼
next(或n) 執行下一行語句
print(或p) 打印表達式的值,經過表達式能夠修改變量的值或者調用函數
quit(或q) 退出 gdb 調試環境
set var 修改變量的值
start 開始執行程序,停在 main 函數第一行語句前面等待命令
step(或s) 執行下一行語句,若是有函數調用則進入到函數中
gdb基本命令2
命令 描述
break(或b) 行號 在某一行設置斷點
break 函數名 在某個函數開頭設置斷點
break ... if ... 設置條件斷點
continue(或c) 從當前位置開始連續運行程序
delete(或d)breakpoints 斷點號 刪除斷點
display 變量名 跟蹤查看某個變量,每次停下來都顯示它的值
disable breakpoints 斷點號 禁用斷點
enable breakpoints 斷點號 啓用斷點
info(或i) breakpoints 查看當前設置了哪些斷點
run(或r) 從頭開始連續運行程序
undisplay 跟蹤顯示號 取消跟蹤顯示
gdb基本命令3
命令 描述
watch 設置觀察點
info(或i) watchpoints 查看當前設置了哪些觀察點
x 從某個位置開始打印存儲單元的內容,所有當成字節來看,而不區分哪一個字節屬於哪一個變量

 

10.1. 單步執行和跟蹤函數調用

看下面的程序:linux

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int add_range(int low, int high) { int i, sum; for (i = low; i <= high; i++) sum = sum + i; return sum; } int main(void) { int result[1000]; result[0] = add_range(1, 10); result[1] = add_range(1, 100); printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]); return 0; } 

add_range 函數從 low 加到 high ,在 main 函數中首先從1加到10,把結果保存下來,而後從1加到100,再把結果保存下來,最後打印的兩個結果是:redis

result[0]=55 result[1]=5105 

第一個結果正確,第二個結果顯然不正確 [1] ,在小學咱們就據說太高斯小時候的故事,從1加到100應該是5050。一段代碼,第一次運行結果是對的,第二次運行卻不對,這是很常見的一類錯誤現象,這種狀況一方面要懷疑代碼,另外一方面更要懷疑數據:第一次和第二次運行的都是同一段代碼,若是代碼是錯的,那第一次的結果爲何能對呢?因此極可能是第二次運行時相關的狀態和數據錯了,錯誤的數據致使了錯誤的結果。在動手調試以前,讀者先試試只看代碼能不能看出錯誤緣由,只要前面幾章學得紮實就應該能看出來。express

[1] 若是你編譯運行這個程序的環境和個人環境(Ubuntu 12.04 LTS 32位x86)不一樣,也許在你的機器上跑不出這個結果,那也不要緊,重要的是學會本章介紹的思想方法。另外你也能夠嘗試修改程序,總有辦法獲得相似的結果,上例中故意定義了一個很大的數組 result[1000] ,修改數組的大小就會改變各局部變量的存儲空間的位置,運行結果就可能會不一樣。

在編譯時要加上 -g 選項,生成的可執行文件才能用 gdb 進行源碼級調試:ubuntu

$ gcc -g main.c -o main
$ gdb main
GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /home/akaedu/main...done.
(gdb)

-g 選項的做用是在可執行文件中加入源文件的信息,便可執行文件 main 中的第幾條機器指令對應源文件 main.c 的第幾行,但並非把整個源文件嵌入到可執行文件中,因此在調試時必須保證 gdb 能找到源文件 main.c 。 gdb 提供一個相似Shell的命令行環境,上面的 (gdb) 就是提示符,在這個提示符下輸入 help 能夠查看命令的類別:小程序

(gdb) help
List of classes of commands:

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
internals -- Maintenance commands
obscure -- Obscure features
running -- Running the program
stack -- Examining the stack
status -- Status inquiries
support -- Support facilities
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands

Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

也能夠進一步查看某一類別中有哪些命令,例如查看 files 類別下有哪些命令可用:數組

(gdb) help files
Specifying and examining files.

List of commands:

add-symbol-file -- Load symbols from FILE
add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file
cd -- Set working directory to DIR for debugger and program being debugged
core-file -- Use FILE as core dump for examining memory and registers
directory -- Add directory DIR to beginning of search path for source files
edit -- Edit specified file or function
exec-file -- Use FILE as program for getting contents of pure memory
file -- Use FILE as program to be debugged
forward-search -- Search for regular expression (see regex(3)) from last line listed
generate-core-file -- Save a core file with the current state of the debugged process
list -- List specified function or line
...

如今試試用 list 命令從第一行開始列出源代碼:函數

(gdb) list 1
1    #include <stdio.h>
2
3    int add_range(int low, int high)
4    {
5            int i, sum;
6            for (i = low; i <= high; i++)
7                    sum = sum + i;
8            return sum;
9    }
10

一次只列10行,若是要從第11行開始繼續列源代碼能夠再輸入一次:工具

(gdb) list

也能夠什麼都不輸直接敲回車, gdb 提供了一個很方便的功能,在提示符下直接敲回車表示重複上一條命令:

(gdb) (直接回車)
11   int main(void)
12   {
13           int result[1000];
14           result[0] = add_range(1, 10);
15           result[1] = add_range(1, 100);
16           printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
17           return 0;
18   }

gdb 的不少經常使用命令有簡寫形式,例如 list 命令能夠寫成 l ,要列一個函數的源代碼也能夠用函數名作參數:

(gdb) l add_range
1    #include <stdio.h>
2
3    int add_range(int low, int high)
4    {
5            int i, sum;
6            for (i = low; i <= high; i++)
7                    sum = sum + i;
8            return sum;
9    }
10

如今退出 gdb 的環境:

(gdb) quit

咱們作一個實驗,把源代碼更名或移到別處再用 gdb 調試,這樣就列不出源代碼了:

$ mv main.c mian.c
$ gdb main
...
(gdb) l
5    main.c: No such file or directory.

可見 gcc 的 -g 選項並非把源代碼嵌入到可執行文件中,在調試時也須要源文件。如今把源代碼恢復原樣,咱們繼續調試。首先用 start 命令開始執行程序:

$ gdb main
...
(gdb) start
Temporary breakpoint 1 at 0x8048415: file main.c, line 14.
Starting program: /home/akaedu/main

Temporary breakpoint 1, main () at main.c:14
14           result[0] = add_range(1, 10);
(gdb)

gdb 停在 main 函數中變量定義以後的第一條語句處等待咱們發命令( gdb 在提示符以前最後列出的語句老是「即將執行的下一條語句」)。咱們能夠用 next 命令(簡寫爲 n )控制這些語句一條一條地執行:

(gdb) n
15           result[1] = add_range(1, 100);
(gdb) (直接回車)
16           printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
(gdb) (直接回車)
result[0]=55
result[1]=5105
17           return 0;

用 n 命令依次執行兩行賦值語句和一行打印語句,在執行打印語句時結果馬上打出來了,而後停在 return 語句以前等待咱們發命令。雖然咱們徹底控制了程序的執行,但仍然看不出哪裏錯了,由於錯誤不在 main 函數中而在 add_range 函數中,如今用 start 命令從新來過,此次用 step 命令(簡寫爲 s )鑽進 add_range 函數中去跟蹤執行:

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x8048415: file main.c, line 14.
Starting program: /home/akaedu/main

Temporary breakpoint 2, main () at main.c:14
14           result[0] = add_range(1, 10);
(gdb) s
add_range (low=1, high=10) at main.c:6
6            for (i = low; i <= high; i++)

此次停在了 add_range 函數中變量定義以後的第一條語句處。在函數中有幾種查看狀態的辦法, backtrace 命令(簡寫爲 bt )能夠查看函數調用的棧幀:

(gdb) bt #0 add_range (low=1, high=10) at main.c:6 #1 0x08048429 in main () at main.c:14

可見當前的 add_range 函數是被 main 函數調用的, main 傳進來的參數是 low=1, high=10 。 main 函數的棧幀編號爲1, add_range 的棧幀編號爲0。如今能夠用 info 命令(簡寫爲 i )查看 add_range 函數局部變量的值:

(gdb) i locals
i = 0
sum = 0

若是想查看 main 函數當前局部變量的值也能夠作到,先用 frame 命令(簡寫爲 f )選擇1號棧幀而後再查看局部變量:

(gdb) f 1
#1  0x08048429 in main () at main.c:14
14           result[0] = add_range(1, 10);
(gdb) i locals
result = {0 <repeats 471 times>, 1184572, 0 <repeats 11 times>, -1207961512, -1073746088, 1249268, -1073745624, 1142336,
...

注意到 result 數組中不少元素的值是雜亂無章的,咱們知道未經初始化的局部變量具備不肯定的值,到目前爲止一切正常。用 s 或 n 往下走幾步,而後用 print 命令(簡寫爲 p )打印出變量 sum 的值:

(gdb) s
7                    sum = sum + i;
(gdb) (直接回車)
6            for (i = low; i <= high; i++)
(gdb) (直接回車)
7                    sum = sum + i;
(gdb) (直接回車)
6            for (i = low; i <= high; i++)
(gdb) p sum
$1 = 3

第一次循環 i 是1,第二次循環 i 是2,加起來是3,沒錯。這裏的 $1 表示 gdb 保存着這些中間結果,$後面的編號會自動增加,在命令中能夠用 $1 、 $2 、 $3 等編號代替相應的值。因爲咱們原本就知道第一次調用的結果是正確的,再往下跟也沒意義了,能夠用 finish 命令讓程序一直運行到從當前函數返回爲止:

(gdb) finish
Run till exit from #0  add_range (low=1, high=10) at main.c:6
0x08048429 in main () at main.c:14
14           result[0] = add_range(1, 10);
Value returned is $2 = 55

返回值是55,當前正準備執行賦值操做,用 n 命令執行賦值操做後查看 result 數組:

(gdb) n
15           result[1] = add_range(1, 100);
(gdb) p result
$3 = {55, 0 <repeats 470 times>, 1184572, 0 <repeats 11 times>, -1207961512, -1073746088, 1249268, -1073745624, 1142336,
...

第一個值55確實賦給了 result 數組的第0個元素。下面用 s 命令進入第二次 add_range 調用,進入以後首先查看參數和局部變量:

(gdb) s
add_range (low=1, high=100) at main.c:6
6            for (i = low; i <= high; i++)
(gdb) bt
#0  add_range (low=1, high=100) at main.c:6
#1  0x08048441 in main () at main.c:15
(gdb) i locals
i = 11
sum = 55

因爲局部變量 i 和 sum 沒初始化,因此具備不肯定的值,又因爲兩次調用是挨着的, i 和 sum 正好取了上次調用時的值,回顧一下咱們講過的 驗證局部變量存儲空間的分配和釋放 那個例子,其實和如今這個例子是同樣的道理,只不過我此次舉的例子設法讓局部變量 sum 在第一次調用時初值爲0而第二次調用時初值不爲0。 i 的初值不肯定倒不要緊,在 for 循環中首先會把 i 賦值爲 low ,但 sum 若是初值不是0,累加獲得的結果就錯了。好了,咱們已經找到錯誤緣由,能夠退出 gdb 修改源代碼了。若是咱們不想浪費此次調試機會,能夠在 gdb 中立刻把 sum 的初值改成0繼續運行,看看這一處改了以後還有沒有別的Bug:

(gdb) set var sum=0
(gdb) finish
Run till exit from #0  add_range (low=1, high=100) at main.c:6
0x08048441 in main () at main.c:15
15           result[1] = add_range(1, 100);
Value returned is $4 = 5050
(gdb) n
16           printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
(gdb) (直接回車)
result[0]=55
result[1]=5050
17           return 0;

這樣結果就對了。修改變量的值除了用 set 命令以外也能夠用 print 命令,由於 print 命令後面跟的是表達式,而咱們知道賦值和函數調用也都是表達式,因此也能夠用 print 命令修改變量的值或者調用函數:

(gdb) p result[2]=33
$5 = 33
(gdb) p printf("result[2]=%d\n", result[2])
result[2]=33
$6 = 13

咱們講過, printf 的返回值表示實際打印的字符數,因此 $6 的結果是13。最後總結一下本節用到的 gdb 命令:

gdb基本命令1
命令 描述
backtrace(或bt) 查看各級函數調用及參數
finish 連續運行到當前函數返回爲止,而後停下來等待命令
frame(或f) 幀編號 選擇棧幀
info(或i) locals 查看當前棧幀局部變量的值
list(或l) 列出源代碼,接着上次的位置往下列,每次列10行
list 行號 列出從第幾行開始的源代碼
list 函數名 列出某個函數的源代碼
next(或n) 執行下一行語句
print(或p) 打印表達式的值,經過表達式能夠修改變量的值或者調用函數
quit(或q) 退出 gdb 調試環境
set var 修改變量的值
start 開始執行程序,停在 main 函數第一行語句前面等待命令
step(或s) 執行下一行語句,若是有函數調用則進入到函數中

習題

  1. 用 gdb 一步一步跟蹤 遞歸 講的 factorial 函數,對照着 factorial(3)的調用過程 查看各層棧幀的變化狀況,練習本節所學的各類 gdb 命令。

10.2. 斷點

看如下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#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
^C(按Ctrl-C退出程序)
$

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

$ gdb main
...
(gdb) start
Temporary breakpoint 1 at 0x804843d: file main.c, line 4.
Starting program: /home/akaedu/main

Temporary breakpoint 1, main () at main.c:4
4    {
(gdb) n
5            int sum = 0, i = 0;

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

(gdb) display sum
1: sum = 1466933
(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 = 0;
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 0x8048459: 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 0x80484b2: file main.c, line 12.
(gdb) i breakpoints
Num     Type           Disp Enb Address    What
2       breakpoint     keep y   0x08048459 in main at main.c:9
        breakpoint already hit 1 time
3       breakpoint     keep y   0x080484b2 in main at main.c:12

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

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

有時候一個斷點暫時不用能夠禁用掉而沒必要刪除,這樣之後想用的時候能夠直接啓用,而沒必要從新從代碼裏找應該在哪一行設斷點:

(gdb) disable breakpoints 3
(gdb) i breakpoints
Num     Type           Disp Enb Address    What
3       breakpoint     keep n   0x080484b2 in main at main.c:12
(gdb) enable 3
(gdb) i breakpoints
Num     Type           Disp Enb Address    What
3       breakpoint     keep y   0x080484b2 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 4 at 0x8048459: file main.c, line 9.
(gdb) i breakpoints
Num     Type           Disp Enb Address    What
4       breakpoint     keep y   0x08048459 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 4, main () at main.c:9
9                    scanf("%s", input);
1: sum = 123

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

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

習題

  1. 看下面的程序:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #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 跟蹤循環,找出錯誤緣由並改正。

10.3. 觀察點

繼續修改上一節的程序。通過調試咱們得出結論,對於這個程序來講, sum 賦不賦初值不重要,重要的是在 while (1) 循環體的開頭加上 sum 0; ,這才能保證每次循環從0開始累加。咱們把程序改爲這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

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

在這裏我故意把 sum 、 i 、 input 定義成全局變量, sum 賦初值而 i 和 input 不賦初值,這是爲了比較容易產生本節要講的錯誤現象。仍是那句話,若是你的運行環境和我不一樣,在你機器上可能跑不出書上說的結果。你能夠先看書,在理解了基本原理以後本身改改程序看能不能跑出相似的結果:變量定義在全局仍是局部做用域,在定義時是否初賦了初值,這些都會影響變量所佔的存儲空間的位置,從而影響本程序的運行結果。

使用 scanf 函數是很是兇險的,即便修正了上一節的Bug也還存在不少問題。若是輸入的字符串超長了會怎麼樣?咱們知道數組訪問越界是不會被檢查的,因此 scanf 會把 input 數組寫越界。現象是這樣的:

$ ./main
1234
input=1234
1234567
input=1234567
12345678
input=123456740

輸入1234567其實已經訪問越界了,但程序還能給出正確結果。而輸入12345678時程序給出一個很是詭異的結果,下面咱們用調試器看看這個詭異的結果是怎麼出來的:

$ gdb main
...
(gdb) start
Temporary breakpoint 1 at 0x804843d: file main.c, line 9.
Starting program: /home/akaedu/main

Temporary breakpoint 1, main () at main.c:9
9                    sum = 0;
(gdb) n
10                   scanf("%s", input);
(gdb) (直接回車)
12345678
11                   for (i = 0; input[i] != '\0'; i++)
(gdb) p input
$1 = "12345"

在這裏 gdb 知道 input 數組的長度是5,因此用 p 命令查看時只顯示5個字符。咱們換一種辦法查看就能夠看到其實已經寫越界了:

(gdb) p printf("%x %x %x %x %x %x %x %x %x\n", input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7], input[8])
31 32 33 34 35 36 37 38 0
$2 = 26

這條命令從 input 數組的第一個字節開始連續打印9個字節,打印的正是 '1' 到 '8' 的十六進制ASCII碼,還有一個 '\0' ,因此 scanf 實際上寫越界了四個字符:'6' 、 '7' 、 '8' 、 '\0' 。 printf 的轉換說明 %x 表示按16進制打印。

根據運行結果「123456740」,用戶輸入的前7個字符轉成數字都沒錯,第8個錯了,也就是 i 從0到6的循環都沒錯,咱們設一個條件斷點從 i 等於7開始單步調試:

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

Breakpoint 2, main () at main.c:12
12                           sum = sum*10 + input[i] - '0';
(gdb) p sum
$3 = 1234567

如今 sum 是1234567沒錯,咱們推測即將進行的下一步計算確定要出錯,調試的結果出乎意料,下一步計算並無出錯:

(gdb) p input[i]
$4 = 56 '8'
(gdb) n
11                   for (i = 0; input[i] != '\0'; i++)
(gdb) p sum
$5 = 12345678

input[i] 是 '8' ,減去 '0' 等於8,把 sum 的當前值1234567乘以10再加上8,確實獲得了12345678。那爲何打印的結果卻不是這一步算出的12345678呢?只有一個解釋:這一步計算以後並無跳出循環去執行 printf ,而是繼續下一輪循環:

(gdb) n
12                           sum = sum*10 + input[i] - '0';
(gdb) p i
$6 = 8
(gdb) p input[i]
$7 = 8 '\b'
(gdb) n
11                   for (i = 0; input[i] != '\0'; i++)
(gdb) p sum
$8 = 123456740
(gdb) n
13                   printf("input=%d\n", sum);
(gdb) p i
$9 = 9
(gdb) p input[9]
$10 = 0 '\000'

先前咱們明明打印出 input[8] 是 '\0' ,何時變成 '\b' 的呢?這一變,循環的控制條件 input[8] != '\0'又獲得知足了,本來應該跳出循環的,如今又進循環了,把sum累加成了12345678*10 + ‘b’ - ‘0’ = 123456740 ( '\b' 的ASCII碼是8, '0' 的ASCII碼是48)。而後 input[9] 確實是0,跳出循環,打印,終於得出了那個詭異的結果!

如今咱們要弄清楚 input[8] 究竟是何時變的,能夠用觀察點(Watchpoint)來跟蹤。咱們知道斷點是當程序執行到某一代碼行時中斷,而觀察點是當程序訪問某個存儲單元時中斷。若是咱們不知道某個存儲單元是被哪一行代碼改動的,觀察點就很是有用了。下面刪除原來設的斷點,從頭執行程序,重複上次的輸入,用 watch 命令設置觀察點,跟蹤 input[8] 的存儲單元:

(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 3 at 0x804843d: file main.c, line 9.
Starting program: /home/akaedu/main

Temporary breakpoint 3, main () at main.c:9
9                    sum = 0;
(gdb) n
10                   scanf("%s", input);
(gdb) (直接回車)
12345678
11                   for (i = 0; input[i] != '\0'; i++)
(gdb) watch input[8]
Hardware watchpoint 4: input[8]
(gdb) i watchpoints
Num     Type           Disp Enb Address    What
4       hw watchpoint  keep y              input[8]
(gdb) c
Continuing.
Hardware watchpoint 4: input[8]

Old value = 0 '\000'
New value = 1 '\001'
0x0804849f in main () at main.c:11
11                   for (i = 0; input[i] != '\0'; i++)
(gdb) c
Continuing.
Hardware watchpoint 4: input[8]

Old value = 1 '\001'
New value = 2 '\002'
0x0804849f in main () at main.c:11
11                   for (i = 0; input[i] != '\0'; i++)

已經很明顯了,每次都是回到 for 循環開頭的時候改變了 input[8] 的值,並且是每次加1--這不就是循環變量 i 麼?原來循環變量 i 就位於 input[8] 的位置。 input[5] 、 input[6] 、 input[7] 雖然也是訪問越界,但還不算嚴重,反正也沒有別的變量佔用這塊存儲空間,而 input[8] 這個訪問越界就嚴重了,直接訪問到變量 i 的頭上了。其實用 x 命令能夠清楚地看到這一點,只不過爲了防止「劇透」我一開始沒有這麼作:

(gdb) x/12bx input
0x804a024 <input>:   0x31    0x32    0x33    0x34    0x35    0x36    0x37    0x38
0x804a02c <i>:       0x02    0x00    0x00    0x00

x 命令打印指定的存儲單元裏保存的內容,後綴 8bx 是打印格式,12表示打印12組,b表示每一個字節一組,x表示按十六進制格式打印 [2] ,咱們能夠看到在 input 的存儲單元的起始位置加8個字節處正是變量 i 的存儲單元。

[2] 打印結果最左邊的一長串數字是內存地址,在 內存與地址 詳細解釋,目前能夠無視。

修正這個Bug對初學者來講有必定難度。若是你發現了這個Bug卻沒想到數組訪問越界這一點,也許一時想不出緣由,就會先去處理另一個更容易修正的Bug:若是輸入的不是數字而是字母或別的符號也能算出結果來,這顯然是不對的,能夠在循環中加上判斷條件檢查非法字符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
while (1) { sum = 0; 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); } 

而後你會驚喜地發現,不只輸入字母會報錯,輸入超長也會報錯:

$ ./main
123a
Invalid input!
input=-1
dead
Invalid input!
input=-1
1234578
Invalid input!
input=-1
1234567890abcdef
Invalid input!
input=-1
23
input=23

彷佛是兩個Bug一塊兒解決掉了,但這是治標不治本的解決方法。看起來輸入超長的錯誤是不出現了,但只要沒有找到根本緣由就不可能真的解決掉,等到條件一變,它可能又冒出來了,在下一節你會看到它又以一種新的形式冒出來了。如今請思考一下爲何加上檢查非法字符的代碼以後輸入超長也會報錯。

最後總結一下本節用到的 gdb 命令:

gdb基本命令3
命令 描述
watch 設置觀察點
info(或i) watchpoints 查看當前設置了哪些觀察點
x 從某個位置開始打印存儲單元的內容,所有當成字節來看,而不區分哪一個字節屬於哪一個變量

10.4. 程序崩潰

若是程序運行時出現段錯誤,用 gdb 能夠很容易定位到到底是哪一行引起的段錯誤,例如這個小程序:

1
2
3
4
5
6
7
8
#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.
0x00180a93 in _IO_vfscanf () from /lib/i386-linux-gnu/libc.so.6
(gdb) bt
#0  0x00180a93 in _IO_vfscanf () from /lib/i386-linux-gnu/libc.so.6
#1  0x0018747b in __isoc99_scanf () from /lib/i386-linux-gnu/libc.so.6
#2  0x0804842a in main () at main.c:6

在 gdb 中運行,遇到段錯誤會自動停下來,這時能夠用命令查看當前執行到哪一行代碼了。 gdb 顯示段錯誤出如今 _IO_vfscanf 函數中,用 bt 命令能夠看到這個函數是被 main.c 的第6行間接調用的,也就是 scanf 這行代碼引起的段錯誤。仔細觀察程序發現是 man 前面少了個&。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#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
12345678
input=12345678
*** stack smashing detected ***: ./main terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0xf4cdd5]
/lib/i386-linux-gnu/libc.so.6(+0xffd8a)[0xf4cd8a]
./main[0x8048592]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xe664d3]
./main[0x8048421]
======= Memory map: ========
00138000-00158000 r-xp 00000000 08:01 394133     /lib/i386-linux-gnu/ld-2.15.so
00158000-00159000 r--p 0001f000 08:01 394133     /lib/i386-linux-gnu/ld-2.15.so
00159000-0015a000 rw-p 00020000 08:01 394133     /lib/i386-linux-gnu/ld-2.15.so
00c97000-00c98000 r-xp 00000000 00:00 0          [vdso]
00e0f000-00e2b000 r-xp 00000000 08:01 394174     /lib/i386-linux-gnu/libgcc_s.so.1
00e2b000-00e2c000 r--p 0001b000 08:01 394174     /lib/i386-linux-gnu/libgcc_s.so.1
00e2c000-00e2d000 rw-p 0001c000 08:01 394174     /lib/i386-linux-gnu/libgcc_s.so.1
00e4d000-00fec000 r-xp 00000000 08:01 394153     /lib/i386-linux-gnu/libc-2.15.so
00fec000-00fee000 r--p 0019f000 08:01 394153     /lib/i386-linux-gnu/libc-2.15.so
00fee000-00fef000 rw-p 001a1000 08:01 394153     /lib/i386-linux-gnu/libc-2.15.so
00fef000-00ff2000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 439349     /home/akaedu/main
08049000-0804a000 r--p 00000000 08:01 439349     /home/akaedu/main
0804a000-0804b000 rw-p 00001000 08:01 439349     /home/akaedu/main
09c65000-09c86000 rw-p 00000000 00:00 0          [heap]
b7780000-b7781000 rw-p 00000000 00:00 0
b778e000-b7793000 rw-p 00000000 00:00 0
bfb0c000-bfb2d000 rw-p 00000000 00:00 0          [stack]
Aborted (core dumped)

咱們輸入12345678,計算結果12345678都打印完了,卻在最後爆出整整一屏錯誤信息。準確地說這是另一種形式的程序崩潰而不是段錯誤,不過咱們能夠按一樣的方法用 gdb 調試看看:

$ gdb main
...
(gdb) r
Starting program: /home/akaedu/main
12345678
input=12345678
*** stack smashing detected ***: /home/akaedu/main terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0x232dd5]
/lib/i386-linux-gnu/libc.so.6(+0xffd8a)[0x232d8a]
/home/akaedu/main[0x8048592]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x14c4d3]
/home/akaedu/main[0x8048421]
======= Memory map: ========
00110000-00130000 r-xp 00000000 08:01 394133     /lib/i386-linux-gnu/ld-2.15.so
00130000-00131000 r--p 0001f000 08:01 394133     /lib/i386-linux-gnu/ld-2.15.so
00131000-00132000 rw-p 00020000 08:01 394133     /lib/i386-linux-gnu/ld-2.15.so
00132000-00133000 r-xp 00000000 00:00 0          [vdso]
00133000-002d2000 r-xp 00000000 08:01 394153     /lib/i386-linux-gnu/libc-2.15.so
002d2000-002d4000 r--p 0019f000 08:01 394153     /lib/i386-linux-gnu/libc-2.15.so
002d4000-002d5000 rw-p 001a1000 08:01 394153     /lib/i386-linux-gnu/libc-2.15.so
002d5000-002d8000 rw-p 00000000 00:00 0
002d8000-002f4000 r-xp 00000000 08:01 394174     /lib/i386-linux-gnu/libgcc_s.so.1
002f4000-002f5000 r--p 0001b000 08:01 394174     /lib/i386-linux-gnu/libgcc_s.so.1
002f5000-002f6000 rw-p 0001c000 08:01 394174     /lib/i386-linux-gnu/libgcc_s.so.1
08048000-08049000 r-xp 00000000 08:01 439349     /home/akaedu/main
08049000-0804a000 r--p 00000000 08:01 439349     /home/akaedu/main
0804a000-0804b000 rw-p 00001000 08:01 439349     /home/akaedu/main
0804b000-0806c000 rw-p 00000000 00:00 0          [heap]
b7fed000-b7fee000 rw-p 00000000 00:00 0
b7ffb000-b8000000 rw-p 00000000 00:00 0
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]

Program received signal SIGABRT, Aborted.
0x00132416 in __kernel_vsyscall ()
(gdb) bt
#0  0x00132416 in __kernel_vsyscall ()
#1  0x001611ef in raise () from /lib/i386-linux-gnu/libc.so.6
#2  0x00164835 in abort () from /lib/i386-linux-gnu/libc.so.6
#3  0x0019c2fa in ?? () from /lib/i386-linux-gnu/libc.so.6
#4  0x00232dd5 in __fortify_fail () from /lib/i386-linux-gnu/libc.so.6
#5  0x00232d8a in __stack_chk_fail () from /lib/i386-linux-gnu/libc.so.6
#6  0x08048592 in main () at main.c:20

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

想要寫出Bug-free的程序是很是不容易的,即便 scanf 讀入字符串這麼一個簡單的函數調用都會隱藏着各類各樣的錯誤。有些錯誤現象是咱們暫時無法解釋的,在後續章節中都會解釋清楚。其實如今講 scanf 這個函數爲時過早,讀者還不具有充足的基礎知識,並且這個函數的用法也確實是至關複雜,要用得準確無誤是挺難的,本書將在 格式化I/O函數 詳細解釋這個函數。如今早早地引入這個函數是爲了讓讀者能夠早早地開始寫有用的程序,畢竟,一個只能輸出( printf )而不能輸入( scanf )的程序算不上什麼有用的程序。

轉自: http://songjinshan.com/akabook/zh/gdb.html

相關文章
相關標籤/搜索