練習(xí)31:代碼調(diào)試
譯者:飛龍
我已經(jīng)教給你一些關(guān)于我的強(qiáng)大的調(diào)試宏的技巧蹋宦,并且你已經(jīng)開始用它們了。當(dāng)我調(diào)試代碼時猫十,我使用debug()
宏,分析發(fā)生了什么以及跟蹤問題。在這個練習(xí)中我打算教給你一些使用gdb的技巧,用于監(jiān)視一個不會退出的簡單程序累提。你會學(xué)到如何使用gdb附加到運行中的進(jìn)程,并掛起它來觀察發(fā)生了什么磁浇。在此之后我會給你一些用于gdb的小提示和小技巧斋陪。
調(diào)試輸出、GDB或Valgrind
我主要按照一種“科學(xué)方法”的方式來調(diào)試置吓,我會提出可能的所有原因无虚,之后排除它們或證明它們導(dǎo)致了缺陷。許多程序員擁有的問題是它們對解決bug的恐慌和急躁使他們覺得這種方法會“拖慢”他們衍锚。它們并沒有注意到友题,它們已經(jīng)失敗了,并且在收集無用的信息戴质。我發(fā)現(xiàn)日志(調(diào)試輸出)會強(qiáng)迫我科學(xué)地解決bug度宦,并且在更多情況下易于收集信息。
此外告匠,使用調(diào)試輸出來作為我的首要調(diào)試工具的理由如下:
- 你可以使用變量的調(diào)試輸出戈抄,來看到程序執(zhí)行的整個軌跡,它讓你跟蹤變量是如何產(chǎn)生錯誤的后专。使用gdb的話划鸽,你必須為每個變量放置查看和調(diào)試語句,并且難以獲得執(zhí)行的實際軌跡行贪。
- 調(diào)試輸出存在于代碼中漾稀,當(dāng)你需要它們是你可以重新編譯使它們回來模闲。使用gdb的話,你每次調(diào)試都需要重新配置相同的信息崭捍。
- 當(dāng)服務(wù)器工作不正常時尸折,它的調(diào)試日志功能易于打開,并且在它運行中可以監(jiān)視日志來查看哪里不對殷蛇。系統(tǒng)管理員知道如何處理日志实夹,他們不知道如何使用gdb。
- 打印信息更加容易粒梦。調(diào)試器通常由于它奇特的UI和前后矛盾顯得難用且古怪亮航。
debug("Yo, dis right? %d", my_stuff);
就沒有那么麻煩。 - 編寫調(diào)試輸出來發(fā)現(xiàn)缺陷匀们,強(qiáng)迫你實際分析代碼缴淋,并且使用科學(xué)方法。你可以認(rèn)為它是泄朴,“我假設(shè)這里的代碼是錯誤的”重抖,你可以運行它來驗證你的假設(shè),如果這里沒有錯誤那么你可以移動到其它地方祖灰。這看起來需要更長時間钟沛,但是實際上更快,因為你經(jīng)歷了“鑒別診斷”的過程局扶,并排除所有可能的原因恨统,直到你找到它。
- 調(diào)試輸入更適于和單元測試一起運行三妈。你可以實際上總是編譯調(diào)試語句畜埋,單元測試時可以隨時查看日志。如果你用gdb沈跨,你需要在gdb中重復(fù)運行單元測試由捎,并跟蹤他來查看發(fā)生了什么兔综。
- 使用Valgrind可以得到和調(diào)試輸出等價的內(nèi)存相關(guān)的錯誤饿凛,所以你并不需要使用類似gdb的東西來尋找缺陷。
盡管所有原因顯示我更傾向于debug
而不是gdb
软驰,我還是在少數(shù)情況下回用到gdb
涧窒,并且我認(rèn)為你應(yīng)該選擇有助于你完成工作的工具。有時锭亏,你只能夠連接到一個崩潰的程序并且四處轉(zhuǎn)悠纠吴。或者慧瘤,你得到了一個會崩潰的服務(wù)器戴已,你只能夠獲得一些核心文件來一探究竟固该。這些貨少數(shù)其它情況中,gdb是很好的辦法糖儡。你最好準(zhǔn)備盡可能多的工具來解決問題伐坏。
接下來我會通過對比gdb、調(diào)試輸出和Valgrind來詳細(xì)分析握联,像這樣:
- Valgrind用于捕獲所有內(nèi)存錯誤桦沉。如果Valgrind中含有錯誤或Valgrind會嚴(yán)重拖慢程序,我會使用gdb金闽。
- 調(diào)試輸出用于診斷或修復(fù)有關(guān)邏輯或使用上的缺陷纯露。在你使用Valgrind之前,這些共計90%的缺陷代芜。
- 使用gdb解決剩下的“謎之bug”埠褪,或如要收集信息的緊急情況。如果Valgrind不起作用挤庇,并且我不能打印出所需信息组橄,我就會使用gdb開始四處搜索。這里我僅僅使用gdb來收集信息罚随。一旦我弄清發(fā)生了什么玉工,我會回來編程單元測試來引發(fā)缺陷,之后編程打印語句來查找原因淘菩。
調(diào)試策略
這一過程適用于你打算使用任何調(diào)試技巧遵班,無論是Valgrind、調(diào)試輸出潮改,或者使用調(diào)試器狭郑。我打算以使用gdb
的形式來描述他,因為似乎人們在使用調(diào)試器是會跳過它汇在。但是應(yīng)當(dāng)對每個bug使用它翰萨,直到你只需要在非常困難的bug上用到。
- 創(chuàng)建一個小型文本文件叫做
notes.txt
糕殉,并且將它用作記錄想法亩鬼、bug和問題的“實驗記錄”。 - 在你使用
gdb
之前阿蝶,寫下你打算修復(fù)的bug雳锋,以及可能的產(chǎn)生原因。 - 對于每個原因羡洁,寫下你所認(rèn)為的玷过,問題來源的函數(shù)或文件,或者僅僅寫下你不知道。
- 現(xiàn)在啟動
gdb
并且使用file:function
挑選最可能的因素辛蚊,之后在那里設(shè)置斷點粤蝎。 - 使用
gdb
運行程序,并且確認(rèn)它是否是真正原因袋马。查明它的最好方式就是看看你是否可以使用set
命令诽里,簡單修復(fù)問題或者重現(xiàn)錯誤。 - 如果它不是真正原因飞蛹,則在
notes.txt
中標(biāo)記它不是谤狡,以及理由。移到下一個可能的原因卧檐,并且使最易于調(diào)試的墓懂,之后記錄你收集到的信息。
這里你并沒有注意到霉囚,它是最基本的科學(xué)方法捕仔。你寫下一些假設(shè),之后調(diào)試來證明或證偽它們盈罐。這讓你洞察到更多可能的因素榜跌,最終使你找到他。這個過程有助于你避免重復(fù)步入同一個可能的因素盅粪,即使你發(fā)現(xiàn)它們并不可能钓葫。
你也可以使用調(diào)試輸出來執(zhí)行這個過程。唯一的不同就是你實際在源碼中編寫假設(shè)來推測問題所在票顾,而不是notes.txt
中础浮。某種程度上,調(diào)試輸出強(qiáng)制你科學(xué)地解決bug奠骄,因為你需要將假寫為打印語句豆同。
使用 GDB
我將在這個練習(xí)中調(diào)試下面這個程序,它只有一個不會正常終止的while
循環(huán)含鳞。我在里面放置了一個usleep
調(diào)用影锈,使它循環(huán)起來更加有趣。
#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)
在你希望暫停之星的地方設(shè)置斷點。你可以提供行號或者函數(shù)名稱,來在文件中的那個地方暫停垂攘。
run ARGS
運行程序维雇,使用ARGS
作為命令行參數(shù)。
cont
繼續(xù)執(zhí)行程序晒他,直到斷點或錯誤吱型。
step
單步執(zhí)行代碼,但是會進(jìn)入函數(shù)內(nèi)部陨仅。使用它來跟蹤函數(shù)內(nèi)部津滞,來觀察它做了什么。
next
就像是step
灼伤,但是他會運行函數(shù)并步過它們触徐。
backtrace (or bt)
執(zhí)行“跟蹤回溯”,它會轉(zhuǎn)儲函數(shù)到當(dāng)前執(zhí)行點的執(zhí)行軌跡狐赡。對于查明如何執(zhí)行到這里非常有用撞鹉,因為它也打印出傳給每個函數(shù)的參數(shù)。它和Valgrind報告內(nèi)存錯誤的方式很接近颖侄。
set var X = Y
將變量X
設(shè)置為Y
鸟雏。
print X
打印出X
的值,你通忱雷妫可以使用C的語法來訪問指針的值或者結(jié)構(gòu)體的內(nèi)容孝鹊。
ENTER
重復(fù)上一條命令。
quit
退出gdb
展蒂。
這些都是我使用gdb
時的主要命令惶室。你現(xiàn)在的任務(wù)是玩轉(zhuǎn)它們和ex31
,你會對它的輸出更加熟悉玄货。
一旦你熟悉了gdb
之后皇钞,你會希望多加使用它。嘗試在更復(fù)雜的程序松捉,例如devpkg
上使用它夹界,來觀察你是否能夠改函數(shù)的執(zhí)行或分析出程序在做什么。
附加到進(jìn)程
gdb
最實用的功能就是附加到運行中的程序隘世,并且就地調(diào)試它的能力可柿。當(dāng)你擁有一個崩潰的服務(wù)器或GUI程序,你通常不需要像之前那樣在gdb
下運行它丙者。而是可以直接啟動它复斥,希望它不要馬上崩潰,之后附加到它并設(shè)置斷點械媒。練習(xí)的這一部分中我會向你展示怎么做目锭。
當(dāng)你退出gdb
之后评汰,如果你停止了ex31
我希望你重啟它,之后開啟另一個中斷窗口以便于啟動gdb
并附加痢虹。進(jìn)程附加就是你讓gdb
連接到已經(jīng)運行的程序被去,以便于你實時監(jiān)測它。它會掛起程序來讓你單步執(zhí)行奖唯,當(dāng)你執(zhí)行完之后程序會像往常一樣恢復(fù)運行惨缆。
下面是一段會話,我對ex31
做了上述事情丰捷,單步執(zhí)行它坯墨,之后修改while
循環(huán)并使它退出。
$ 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
的進(jìn)程ID少漆。
gdb:5
我使用gdb ./ex31 PID
來附加到進(jìn)程,其中PID
替換為我所擁有的進(jìn)程ID硼被。
gdb:6-19
gdb
打印出了一堆關(guān)于協(xié)議的信息示损,接著它讀取了所有東西。
gdb:21
程序被附加嚷硫,并且在當(dāng)前執(zhí)行點上停止检访。所以現(xiàn)在我在文件中的第8行使用break
設(shè)置了斷點。我假設(shè)我這么做的時候仔掸,已經(jīng)在這個我想中斷的文件中了脆贵。
gdb:24
執(zhí)行break
的更好方式,是提供file.c line
的格式起暮,便于你確保定位到了正確的地方卖氨。我在這個break
中這樣做。
gdb:27
我使用cont
來繼續(xù)運行负懦,直到我命中了斷點筒捺。
gdb:30-31
我已到達(dá)斷點,于是gdb
打印出我需要了解的變量(argc
和argv
)纸厉,以及停下來的位置系吭,之后打印出斷點的行號。
gdb:33-34
我使用print
的縮寫p
來打印出i
變量的值颗品,它是0肯尺。
gdb:36
繼續(xù)運行來查看i
是否改變沃缘。
gdb:42
再次打印出i
,顯然它沒有變化蟆盹。
gdb:45-55
使用list
來查看代碼是什么孩灯,之后我意識到它不可能退出闺金,因為我沒有自增i
逾滥。
gdb:57
確認(rèn)我的假設(shè)是正確的,即i
需要使用set
命令來修改為i = 200
败匹。這是gdb
最優(yōu)秀的特性之一寨昙,讓你“修改”程序來讓你快速知道你是否正確。
gdb:59
打印i
來確保它已改變掀亩。
gdb:62
使用next
來移到下一段代碼舔哪,并且我發(fā)現(xiàn)命中了ex31.c:12
的斷點,所以這意味著while
循環(huán)已退出槽棍。我的假設(shè)正確捉蚤,我需要修改i
。
gdb:67
使用cont
來繼續(xù)運行炼七,程序像往常一樣退出缆巧。
gdb:71
最后我使用quit
來退出gdb
。
GDB 技巧
下面是你可以用于GDB的一些小技巧:
gdb --args
通常gdb
獲得你提供的變量并假設(shè)它們用于它自己豌拙。使用--args
來向程序傳遞它們陕悬。
thread apply all bt
轉(zhuǎn)儲所有線程的執(zhí)行軌跡,非常有用按傅。
gdb --batch --ex r --ex bt --ex q --args
運行程序捉超,當(dāng)它崩潰時你會得到執(zhí)行軌跡。
?
如果你有其它技巧唯绍,在評論中寫下它吧拼岳。
附加題
- 找到一個圖形化的調(diào)試器,將它與原始的
gdb
相比况芒。它們在本地調(diào)試程序時非常有用惜纸,但是對于在服務(wù)器上調(diào)試沒有任何意義。 - 你可以開啟OS上的“核心轉(zhuǎn)儲”牛柒,當(dāng)程序崩潰時你會得到一個核心文件堪簿。這個核心文件就像是對程序的解剖,便于你了解崩潰時發(fā)生了什么皮壁,以及由什么原因?qū)е峦指P薷?code>ex31.c使它在幾個迭代之后崩潰,之后嘗試得到它的核心轉(zhuǎn)儲并分析蛾魄。