# 練習31:代碼調試
> 原文:[Exercise 31: Debugging Code](http://c.learncodethehardway.org/book/ex31.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
我已經教給你一些關于我的強大的調試宏的技巧,并且你已經開始用它們了。當我調試代碼時,我使用`debug()`宏,分析發生了什么以及跟蹤問題。在這個練習中我打算教給你一些使用gdb的技巧,用于監視一個不會退出的簡單程序。你會學到如何使用gdb附加到運行中的進程,并掛起它來觀察發生了什么。在此之后我會給你一些用于gdb的小提示和小技巧。
## 調試輸出、GDB或Valgrind
我主要按照一種“科學方法”的方式來調試,我會提出可能的所有原因,之后排除它們或證明它們導致了缺陷。許多程序員擁有的問題是它們對解決bug的恐慌和急躁使他們覺得這種方法會“拖慢”他們。它們并沒有注意到,它們已經失敗了,并且在收集無用的信息。我發現日志(調試輸出)會強迫我科學地解決bug,并且在更多情況下易于收集信息。
此外,使用調試輸出來作為我的首要調試工具的理由如下:
+ 你可以使用變量的調試輸出,來看到程序執行的整個軌跡,它讓你跟蹤變量是如何產生錯誤的。使用gdb的話,你必須為每個變量放置查看和調試語句,并且難以獲得執行的實際軌跡。
+ 調試輸出存在于代碼中,當你需要它們是你可以重新編譯使它們回來。使用gdb的話,你每次調試都需要重新配置相同的信息。
+ 當服務器工作不正常時,它的調試日志功能易于打開,并且在它運行中可以監視日志來查看哪里不對。系統管理員知道如何處理日志,他們不知道如何使用gdb。
+ 打印信息更加容易。調試器通常由于它奇特的UI和前后矛盾顯得難用且古怪。`debug("Yo, dis right? %d", my_stuff);`就沒有那么麻煩。
+ 編寫調試輸出來發現缺陷,強迫你實際分析代碼,并且使用科學方法。你可以認為它是,“我假設這里的代碼是錯誤的”,你可以運行它來驗證你的假設,如果這里沒有錯誤那么你可以移動到其它地方。這看起來需要更長時間,但是實際上更快,因為你經歷了“鑒別診斷”的過程,并排除所有可能的原因,直到你找到它。
+ 調試輸入更適于和單元測試一起運行。你可以實際上總是編譯調試語句,單元測試時可以隨時查看日志。如果你用gdb,你需要在gdb中重復運行單元測試,并跟蹤他來查看發生了什么。
+ 使用Valgrind可以得到和調試輸出等價的內存相關的錯誤,所以你并不需要使用類似gdb的東西來尋找缺陷。
盡管所有原因顯示我更傾向于`debug`而不是`gdb`,我還是在少數情況下回用到`gdb`,并且我認為你應該選擇有助于你完成工作的工具。有時,你只能夠連接到一個崩潰的程序并且四處轉悠。或者,你得到了一個會崩潰的服務器,你只能夠獲得一些核心文件來一探究竟。這些貨少數其它情況中,gdb是很好的辦法。你最好準備盡可能多的工具來解決問題。
接下來我會通過對比gdb、調試輸出和Valgrind來詳細分析,像這樣:
+ Valgrind用于捕獲所有內存錯誤。如果Valgrind中含有錯誤或Valgrind會嚴重拖慢程序,我會使用gdb。
+ 調試輸出用于診斷或修復有關邏輯或使用上的缺陷。在你使用Valgrind之前,這些共計90%的缺陷。
+ 使用gdb解決剩下的“謎之bug”,或如要收集信息的緊急情況。如果Valgrind不起作用,并且我不能打印出所需信息,我就會使用gdb開始四處搜索。這里我僅僅使用gdb來收集信息。一旦我弄清發生了什么,我會回來編程單元測試來引發缺陷,之后編程打印語句來查找原因。
## 調試策略
這一過程適用于你打算使用任何調試技巧,無論是Valgrind、調試輸出,或者使用調試器。我打算以使用`gdb`的形式來描述他,因為似乎人們在使用調試器是會跳過它。但是應當對每個bug使用它,直到你只需要在非常困難的bug上用到。
+ 創建一個小型文本文件叫做`notes.txt`,并且將它用作記錄想法、bug和問題的“實驗記錄”。
+ 在你使用`gdb`之前,寫下你打算修復的bug,以及可能的產生原因。
+ 對于每個原因,寫下你所認為的,問題來源的函數或文件,或者僅僅寫下你不知道。
+ 現在啟動`gdb`并且使用`file:function`挑選最可能的因素,之后在那里設置斷點。
+ 使用`gdb`運行程序,并且確認它是否是真正原因。查明它的最好方式就是看看你是否可以使用`set`命令,簡單修復問題或者重現錯誤。
+ 如果它不是真正原因,則在`notes.txt`中標記它不是,以及理由。移到下一個可能的原因,并且使最易于調試的,之后記錄你收集到的信息。
這里你并沒有注意到,它是最基本的科學方法。你寫下一些假設,之后調試來證明或證偽它們。這讓你洞察到更多可能的因素,最終使你找到他。這個過程有助于你避免重復步入同一個可能的因素,即使你發現它們并不可能。
你也可以使用調試輸出來執行這個過程。唯一的不同就是你實際在源碼中編寫假設來推測問題所在,而不是`notes.txt`中。某種程度上,調試輸出強制你科學地解決bug,因為你需要將假寫為打印語句。
## 使用 GDB
我將在這個練習中調試下面這個程序,它只有一個不會正常終止的`while`循環。我在里面放置了一個`usleep`調用,使它循環起來更加有趣。
```c
#include <unistd.h>
int main(int argc, char *argv[])
{
int i = 0;
while(i < 100) {
usleep(3000);
}
return 0;
}
```
像往常一樣編譯,并且在`gdb`下啟動它,例如:`gdb ./ex31`。
一旦它運行之后,我打算讓你使用這些`gdb`命令和它交互,并且觀察它們的作用以及如何使用它們。
help COMMAND
獲得`COMMAND`的簡單幫助。
break file.c:(line|function)
在你希望暫停之星的地方設置斷點。你可以提供行號或者函數名稱,來在文件中的那個地方暫停。
run ARGS
運行程序,使用`ARGS`作為命令行參數。
cont
繼續執行程序,直到斷點或錯誤。
step
單步執行代碼,但是會進入函數內部。使用它來跟蹤函數內部,來觀察它做了什么。
next
就像是`step`,但是他會運行函數并步過它們。
backtrace (or bt)
執行“跟蹤回溯”,它會轉儲函數到當前執行點的執行軌跡。對于查明如何執行到這里非常有用,因為它也打印出傳給每個函數的參數。它和Valgrind報告內存錯誤的方式很接近。
set var X = Y
將變量`X`設置為`Y`。
print X
打印出`X`的值,你通常可以使用C的語法來訪問指針的值或者結構體的內容。
ENTER
重復上一條命令。
quit
退出`gdb`。
這些都是我使用`gdb`時的主要命令。你現在的任務是玩轉它們和`ex31`,你會對它的輸出更加熟悉。
一旦你熟悉了`gdb`之后,你會希望多加使用它。嘗試在更復雜的程序,例如`devpkg`上使用它,來觀察你是否能夠改函數的執行或分析出程序在做什么。
## 附加到進程
`gdb`最實用的功能就是附加到運行中的程序,并且就地調試它的能力。當你擁有一個崩潰的服務器或GUI程序,你通常不需要像之前那樣在`gdb`下運行它。而是可以直接啟動它,希望它不要馬上崩潰,之后附加到它并設置斷點。練習的這一部分中我會向你展示怎么做。
當你退出`gdb`之后,如果你停止了`ex31`我希望你重啟它,之后開啟另一個中斷窗口以便于啟動`gdb`并附加。進程附加就是你讓`gdb`連接到已經運行的程序,以便于你實時監測它。它會掛起程序來讓你單步執行,當你執行完之后程序會像往常一樣恢復運行。
下面是一段會話,我對`ex31`做了上述事情,單步執行它,之后修改`while`循環并使它退出。
```sh
$ ps ax | grep ex31
10026 s000 S+ 0:00.11 ./ex31
10036 s001 R+ 0:00.00 grep ex31
$ gdb ./ex31 10026
GNU gdb 6.3.50-20050815 (Apple version gdb-1705) (Fri Jul 1 10:50:06 UTC 2011)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done
/Users/zedshaw/projects/books/learn-c-the-hard-way/code/10026: No such file or directory
Attaching to program: `/Users/zedshaw/projects/books/learn-c-the-hard-way/code/ex31', process 10026.
Reading symbols for shared libraries + done
Reading symbols for shared libraries ++........................ done
Reading symbols for shared libraries + done
0x00007fff862c9e42 in __semwait_signal ()
(gdb) break 8
Breakpoint 1 at 0x107babf14: file ex31.c, line 8.
(gdb) break ex31.c:11
Breakpoint 2 at 0x107babf1c: file ex31.c, line 12.
(gdb) cont
Continuing.
Breakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8
8 while(i < 100) {
(gdb) p i
$1 = 0
(gdb) cont
Continuing.
Breakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8
8 while(i < 100) {
(gdb) p i
$2 = 0
(gdb) list
3
4 int main(int argc, char *argv[])
5 {
6 int i = 0;
7
8 while(i < 100) {
9 usleep(3000);
10 }
11
12 return 0;
(gdb) set var i = 200
(gdb) p i
$3 = 200
(gdb) next
Breakpoint 2, main (argc=1, argv=0x7fff677aabd8) at ex31.c:12
12 return 0;
(gdb) cont
Continuing.
Program exited normally.
(gdb) quit
$
```
> 注
> 在OSX上你可能會看到輸入root密碼的GUI輸入框,并且即使你輸入了密碼還是會得到來自`gdb`的“Unable to access task for process-id XXX: (os/kern) failure.”的錯誤。這種情況下,你需要停止`gdb`和`ex31`程序,并重新啟動程序使它工作,只要你成功輸入了root密碼。
我會遍歷整個會話,并且解釋我做了什么:
gdb:1
使用`ps`來尋找我想要附加的`ex31`的進程ID。
gdb:5
我使用`gdb ./ex31 PID`來附加到進程,其中`PID`替換為我所擁有的進程ID。
gdb:6-19
`gdb`打印出了一堆關于協議的信息,接著它讀取了所有東西。
gdb:21
程序被附加,并且在當前執行點上停止。所以現在我在文件中的第8行使用`break`設置了斷點。我假設我這么做的時候,已經在這個我想中斷的文件中了。
gdb:24
執行`break`的更好方式,是提供`file.c line`的格式,便于你確保定位到了正確的地方。我在這個`break`中這樣做。
gdb:27
我使用`cont`來繼續運行,直到我命中了斷點。
gdb:30-31
我已到達斷點,于是`gdb`打印出我需要了解的變量(`argc`和`argv`),以及停下來的位置,之后打印出斷點的行號。
gdb:33-34
我使用`print`的縮寫`p`來打印出`i`變量的值,它是0。
gdb:36
繼續運行來查看`i`是否改變。
gdb:42
再次打印出`i`,顯然它沒有變化。
gdb:45-55
使用`list`來查看代碼是什么,之后我意識到它不可能退出,因為我沒有自增`i`。
gdb:57
確認我的假設是正確的,即`i`需要使用`set`命令來修改為`i = 200`。這是`gdb`最優秀的特性之一,讓你“修改”程序來讓你快速知道你是否正確。
gdb:59
打印`i`來確保它已改變。
gdb:62
使用`next`來移到下一段代碼,并且我發現命中了`ex31.c:12`的斷點,所以這意味著`while`循環已退出。我的假設正確,我需要修改`i`。
gdb:67
使用`cont`來繼續運行,程序像往常一樣退出。
gdb:71
最后我使用`quit`來退出`gdb`。
## GDB 技巧
下面是你可以用于GDB的一些小技巧:
gdb --args
通常`gdb`獲得你提供的變量并假設它們用于它自己。使用`--args`來向程序傳遞它們。
thread apply all bt
轉儲所有線程的執行軌跡,非常有用。
gdb --batch --ex r --ex bt --ex q --args
運行程序,當它崩潰時你會得到執行軌跡。
?
如果你有其它技巧,在評論中寫下它吧。
## 附加題
+ 找到一個圖形化的調試器,將它與原始的`gdb`相比。它們在本地調試程序時非常有用,但是對于在服務器上調試沒有任何意義。
+ 你可以開啟OS上的“核心轉儲”,當程序崩潰時你會得到一個核心文件。這個核心文件就像是對程序的解剖,便于你了解崩潰時發生了什么,以及由什么原因導致。修改`ex31.c`使它在幾個迭代之后崩潰,之后嘗試得到它的核心轉儲并分析。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死