練習31:代碼調試
譯者:飛龍
我已經教給你一些關于我的強大的調試宏的技巧,并且你已經開始用它們了。當我調試代碼時,我使用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
調用,使它循環起來更加有趣。
#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
循環并使它退出。
$ 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
使它在幾個迭代之后崩潰,之后嘗試得到它的核心轉儲并分析。