原文:Exercise 31: Debugging Codehtml
譯者:飛龍git
我已經教給你一些關於個人強大的調試宏的技巧,而且你已經開始用它們了。當我調試代碼時,我使用debug()
宏,分析發生了什麼以及跟蹤問題。在這個練習中我打算教給你一些使用gdb的技巧,用於監視一個不會退出的簡單程序。你會學到如何使用gdb附加到運行中的進程,並掛起它來觀察發生了什麼。在此以後我會給你一些用於gdb的小提示和小技巧。程序員
我主要按照一種「科學方法」的方式來調試,我會提出可能的全部緣由,以後排除它們或證實它們致使了缺陷。許多程序員擁有的問題是它們對解決bug的恐慌和急躁使他們以爲這種方法會「拖慢」他們。它們並無注意到,它們已經失敗了,而且在收集無用的信息。我發現日誌(調試輸出)會強迫我科學地解決bug,而且在更多狀況下易於收集信息。github
此外,使用調試輸出來做爲個人首要調試工具的理由以下:編程
你可使用變量的調試輸出,來看到程序執行的整個軌跡,它讓你跟蹤變量是如何產生錯誤的。使用gdb的話,你必須爲每一個變量放置查看和調試語句,而且難以得到執行的實際軌跡。服務器
調試輸出存在於代碼中,當你須要它們是你能夠從新編譯使它們回來。使用gdb的話,你每次調試都須要從新配置相同的信息。app
當服務器工做不正常時,它的調試日誌功能易於打開,而且在它運行中能夠監視日誌來查看哪裏不對。系統管理員知道如何處理日誌,他們不知道如何使用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,由於你須要將假寫爲打印語句。
我將在這個練習中調試下面這個程序,它只有一個不會正常終止的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 --args
一般gdb
得到你提供的變量並假設它們用於它本身。使用--args
來向程序傳遞它們。
thread apply all bt
轉儲全部線程的執行軌跡,很是有用。
gdb --batch --ex r --ex bt --ex q --args
運行程序,當它崩潰時你會獲得執行軌跡。
?
若是你有其它技巧,在評論中寫下它吧。
找到一個圖形化的調試器,將它與原始的gdb
相比。它們在本地調試程序時很是有用,可是對於在服務器上調試沒有任何意義。
你能夠開啓OS上的「核心轉儲」,當程序崩潰時你會獲得一個核心文件。這個核心文件就像是對程序的解剖,便於你瞭解崩潰時發生了什麼,以及由什麼緣由致使。修改ex31.c
使它在幾個迭代以後崩潰,以後嘗試獲得它的核心轉儲並分析。