<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 第?10?章?gdb **目錄** + [1\. 單步執行和跟蹤函數調用](ch10s01.html) + [2\. 斷點](ch10s02.html) + [3\. 觀察點](ch10s03.html) + [4\. 段錯誤](ch10s04.html) 程序中除了一目了然的Bug之外都需要一定的調試手段來分析到底錯在哪。到目前為止我們的調試手段只有一種:根據程序執行時的出錯現象假設錯誤原因,然后在代碼中適當的位置插入`printf`,執行程序并分析打印結果,如果結果和預期的一樣,就基本上證明了自己假設的錯誤原因,就可以動手修正Bug了,如果結果和預期的不一樣,就根據結果做進一步的假設和分析。本章我們介紹一種很強大的調試工具`gdb`,可以完全操控程序的運行,使得程序就像你手里的玩具一樣,叫它走就走,叫它停就停,并且隨時可以查看程序中所有的內部狀態,比如各變量的值、傳給函數的參數、當前執行的代碼行等。掌握了`gdb`的用法之后,調試手段就更加豐富了。但要注意,即使調試手段豐富了,調試的基本思想仍然是“分析現象-&gt;假設錯誤原因-&gt;產生新的現象去驗證假設”這樣一個循環,根據現象如何假設錯誤原因,以及如何設計新的現象去驗證假設,這都需要非常嚴密的分析和思考,如果因為手里有了強大的工具就濫用而忽略了分析過程,往往會治標不治本地修正Bug,導致一個錯誤現象消失了但Bug仍然存在,甚至是把程序越改越錯。本章通過初學者易犯的幾個錯誤實例來講解如何使用`gdb`調試程序,在每個實例后面總結一部分常用的`gdb`命令。 ## 1.?單步執行和跟蹤函數調用 看下面的程序: **例?10.1.?函數調試實例** ``` #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[100]; 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,再把結果保存下來,最后打印的兩個結果是: ``` result[0]=55 result[1]=5105 ``` 第一個結果正確<sup>[[20](#ftn.id2740258)]</sup>,第二個結果顯然不正確,在小學我們就聽說過高斯小時候的故事,從1加到100應該是5050。一段代碼,第一次運行結果是對的,第二次運行卻不對,這是很常見的一類錯誤現象,這種情況不應該懷疑代碼而應該懷疑數據,因為第一次和第二次運行的都是同一段代碼,如果代碼是錯的,那為什么第一次的結果能對呢?然而第一次和第二次運行時相關的數據卻有可能不同,錯誤的數據會導致錯誤的結果。在動手調試之前,讀者先試試只看代碼能不能看出錯誤原因,只要前面幾章學得扎實就應該能看出來。 在編譯時要加上`-g`選項,生成的可執行文件才能用`gdb`進行源碼級調試: ``` $ gcc -g main.c -o main $ gdb main GNU gdb 6.8-debian Copyright (C) 2008 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 "i486-linux-gnu"... (gdb) ``` `-g`選項的作用是在可執行文件中加入源代碼的信息,比如可執行文件中第幾條機器指令對應源代碼的第幾行,但并不是把整個源文件嵌入到可執行文件中,所以在調試時必須保證`gdb`能找到源文件。`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-shared-symbol-files -- Load the symbols from shared objects in the dynamic linker's link map 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[100]; 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. in main.c ``` 可見`gcc`的`-g`選項并不是把源代碼嵌入到可執行文件中的,在調試時也需要源文件。現在把源代碼恢復原樣,我們繼續調試。首先用`start`命令開始執行程序: ``` $ gdb main ... (gdb) start Breakpoint 1 at 0x80483ad: file main.c, line 14. Starting program: /home/akaedu/main 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 Breakpoint 2 at 0x80483ad: file main.c, line 14. Starting program: /home/akaedu/main 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 0x080483c1 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 0x080483c1 in main () at main.c:14 14 result[0] = add_range(1, 10); (gdb) i locals result = {0, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480, ... -1208623680} ``` 注意到`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 0x080483c1 in main () at main.c:14 14 result[0] = add_range(1, 10); Value returned is $2 = 55 ``` 返回值是55,當前正準備執行賦值操作,用`s`命令賦值,然后查看`result`數組: ``` (gdb) s 15 result[1] = add_range(1, 100); (gdb) p result $3 = {55, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480, ... -1208623680} ``` 第一個值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 0x080483db in main () at main.c:15 (gdb) i locals i = 11 sum = 55 ``` 由于局部變量`i`和`sum`沒初始化,所以具有不確定的值,又由于兩次調用是挨著的,`i`和`sum`正好取了上次調用時的值,原來這跟[例?3.7 “驗證局部變量存儲空間的分配和釋放”](ch03s04.html#func.localvalidate)是一樣的道理,只不過我這次舉的例子設法讓局部變量`sum`在第一次調用時初值為0了。`i`的初值不是0倒沒關系,在`for`循環中會賦值為0的,但`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 0x080483db 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`命令: **表?10.1.?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`一步一步跟蹤[第?3?節 “遞歸”](ch05s03.html#func2.recursion)講的`factorial`函數,對照著[圖?5.2 “factorial(3)的調用過程”](ch05s03.html#func2.factorial)查看各層棧幀的變化情況,練習本節所學的各種`gdb`命令。 * * * <sup>[[20](#id2740258)]</sup> 這么說不夠準確,在有些平臺和操作系統上第一個結果也未必正確,如果在你機器上運行第一個結果也不正確,首先檢查一下程序有沒有抄錯,如果沒抄錯那就沒關系了,順著我的講解往下看就好了,結果是多少都無關緊要。 ## 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`跟蹤循環,找出錯誤原因并改正。 ## 3.?觀察點 接著上一節的步驟,經過調試我們知道,雖然`sum`已經賦了初值0,但仍需要在`while (1)`循環的開頭加上`sum = 0;`: **例?10.3.?觀察點調試實例** ``` #include <stdio.h> int main(void) { int sum = 0, i = 0; char input[5]; 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; } ``` 使用`scanf`函數是非常兇險的,即使修正了這個Bug也還存在很多問題。如果輸入的字符串超長了會怎么樣?我們知道數組訪問越界是不會檢查的,所以`scanf`會寫出界。現象是這樣的: ``` $ ./main 123 input=123 67 input=67 12345 input=123407 ``` 下面用調試器看看最后這個詭異的結果是怎么出來的<sup>[[21](#ftn.id2741970)]</sup>。 ``` $ 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; (gdb) n 9 sum = 0; (gdb) (直接回車) 10 scanf("%s", input); (gdb) (直接回車) 12345 11 for (i = 0; input[i] != '\0'; i++) (gdb) p input $1 = "12345" ``` `input`數組只有5個元素,寫出界的是`scanf`自動添的`'\0'`,用`x`命令看會更清楚一些: ``` (gdb) x/7b input 0xbfb8f0a7: 0x31 0x32 0x33 0x34 0x35 0x00 0x00 ``` `x`命令打印指定存儲單元的內容。`7b`是打印格式,`b`表示每個字節一組,7表示打印7組<sup>[[22](#ftn.id2742046)]</sup>,從`input`數組的第一個字節開始連續打印7個字節。前5個字節是`input`數組的存儲單元,打印的正是十六進制ASCII碼的`'1'`到`'5'`,第6個字節是寫出界的`'\0'`。根據運行結果,前4個字符轉成數字都沒錯,第5個錯了,也就是`i`從0到3的循環都沒錯,我們設一個條件斷點從`i`等于4開始單步調試: ``` (gdb) l 6 char input[5]; 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 == 4 Breakpoint 2 at 0x80483e6: 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 $2 = 1234 ``` 現在`sum`是1234沒錯,根據運行結果是123407我們知道即將進行的這步計算肯定要出錯,算出來應該是12340,那就是說`input[4]`肯定不是`'5'`了,事實證明這個推理是不嚴謹的: ``` (gdb) x/7b input 0xbfb8f0a7: 0x31 0x32 0x33 0x34 0x35 0x04 0x00 ``` `input[4]`的確是0x35,產生123407還有另外一種可能,就是在下一次循環中123450不是加上而是減去一個數得到123407。可現在不是到字符串末尾了嗎?怎么會有下一次循環呢?注意到循環控制條件是`input[i] != '\0'`,而本來應該是0x00的位置現在莫名其妙地變成了0x04,因此循環不會結束。繼續單步: ``` (gdb) n 11 for (i = 0; input[i] != '\0'; i++) (gdb) p sum $3 = 12345 (gdb) n 12 sum = sum*10 + input[i] - '0'; (gdb) x/7b input 0xbfb8f0a7: 0x31 0x32 0x33 0x34 0x35 0x05 0x00 ``` 進入下一次循環,原來的0x04又莫名其妙地變成了0x05,這是怎么回事?這個暫時解釋不了,但123407這個結果可以解釋了,是12345*10 + 0x05 - 0x30得到的,雖然多循環了一次,但下次一定會退出循環了,因為0x05的后面是`'\0'`。 `input[4]`后面那個字節到底是什么時候變的?可以用觀察點(Watchpoint)來跟蹤。我們知道斷點是當程序執行到某一代碼行時中斷,而觀察點是當程序訪問某個存儲單元時中斷,如果我們不知道某個存儲單元是在哪里被改動的,這時候觀察點尤其有用。下面刪除原來設的斷點,從頭執行程序,重復上次的輸入,用`watch`命令設置觀察點,跟蹤`input[4]`后面那個字節(可以用`input[5]`表示,雖然這是訪問越界): ``` (gdb) delete breakpoints Delete all breakpoints? (y or n) y (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; (gdb) n 9 sum = 0; (gdb) (直接回車) 10 scanf("%s", input); (gdb) (直接回車) 12345 11 for (i = 0; input[i] != '\0'; i++) (gdb) watch input[5] Hardware watchpoint 2: input[5] (gdb) i watchpoints Num Type Disp Enb Address What 2 hw watchpoint keep y input[5] (gdb) c Continuing. Hardware watchpoint 2: input[5] Old value = 0 '\0' New value = 1 '\001' 0x0804840c in main () at main.c:11 11 for (i = 0; input[i] != '\0'; i++) (gdb) c Continuing. Hardware watchpoint 2: input[5] Old value = 1 '\001' New value = 2 '\002' 0x0804840c in main () at main.c:11 11 for (i = 0; input[i] != '\0'; i++) (gdb) c Continuing. Hardware watchpoint 2: input[5] Old value = 2 '\002' New value = 3 '\003' 0x0804840c in main () at main.c:11 11 for (i = 0; input[i] != '\0'; i++) ``` 已經很明顯了,每次都是回到`for`循環開頭的時候改變了`input[5]`的值,而且是每次加1,而循環變量`i`正是在每次回到循環開頭之前加1,原來`input[5]`就是變量`i`的存儲單元,換句話說,`i`的存儲單元是緊跟在`input`數組后面的。 修正這個Bug對初學者來說有一定難度。如果你發現了這個Bug卻沒想到數組訪問越界這一點,也許一時想不出原因,就會先去處理另外一個更容易修正的Bug:如果輸入的不是數字而是字母或別的符號也能算出結果來,這顯然是不對的,可以在循環中加上判斷條件檢查非法字符: ``` 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`命令: **表?10.3.?gdb基本命令3** | 命令 | 描述 | | --- | --- | | watch | 設置觀察點 | | info(或i) watchpoints | 查看當前設置了哪些觀察點 | | x | 從某個位置開始打印存儲單元的內容,全部當成字節來看,而不區分哪個字節屬于哪個變量 | * * * <sup>[[21](#id2741970)]</sup> 不得不承認,在有些平臺和操作系統上也未必得到這個結果,產生Bug的往往都是一些平臺相關的問題,舉這樣的例子才比較像是真實軟件開發中遇到的Bug,如果您的程序跑不出我這樣的結果,那這一節您就湊合著看吧。 <sup>[[22](#id2742046)]</sup> 打印結果最左邊的一長串數字是內存地址,在[第?1?節 “內存與地址”](ch17s01.html#arch.addr)詳細解釋,目前可以無視。 ## 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`函數都沒辦法用好,更沒有辦法保證寫出正確的程序。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看