無(wú)論是多么優(yōu)秀的程序員,都難以保證自己在編寫(xiě)代碼時(shí)不會(huì )出現任何錯誤,因此調試是軟件開(kāi)發(fā)過(guò)程中的一個(gè)必不可少的組成部分。當程序完成編譯之后,它很 可能無(wú)法正常運行,或者會(huì )徹底崩潰,或者不能實(shí)現預期的功能。此時(shí)如何通過(guò)調試找到問(wèn)題的癥結所在,就變成了擺在開(kāi)發(fā)人員面前最嚴峻的問(wèn)題。通常說(shuō)來(lái),軟 件項目的規模越大,調試起來(lái)就會(huì )越困難,越需要一個(gè)強大而高效的調試器作為后盾。對于Linux程序員來(lái)講,目前可供使用的調試器非常多,GDB(GNU DeBugger)就是其中較為優(yōu)秀的。
初識GDB
GDB是自由軟件基金會(huì )(Free Software Foundation,FSF)的軟件工具之一。它的作用是協(xié)助程序員找到代碼中的錯誤。如果沒(méi)有GDB的幫助,程序員要想跟蹤代碼的執行流程,唯一的辦 法就是添加大量的語(yǔ)句來(lái)產(chǎn)生特定的輸出。但這一手段本身就可能會(huì )引入新的錯誤,從而也就無(wú)法對那些導致程序崩潰的錯誤代碼進(jìn)行分析。GDB的出現減輕了開(kāi) 發(fā)人員的負擔,他們可以在程序運行的時(shí)候單步跟蹤自己的代碼,或者通過(guò)斷點(diǎn)暫時(shí)中止程序的執行。此外,他們還能夠隨時(shí)察看變量和內存的當前狀態(tài),并監視關(guān) 鍵的數據結構是如何影響代碼運行的。
調試方法
如果想對程序進(jìn)行調試,必須先在用GCC編譯源代碼時(shí)加上-g選項,以便產(chǎn)生GDB所需要的調試符號信息。例如,debugme.c是一個(gè)存在錯誤程序,可以使用如下的命令對其進(jìn)行編譯,同時(shí)產(chǎn)生調試符號:
# gcc -g debugme.c -o debugme
如果愿意的話(huà),還可以在編譯時(shí)使用“-ggdb”選項來(lái)生成更多的調試信息。由于這些調試信息中的相當一部分是GDB所特有的,所以生成的代碼將無(wú)法在 其它調試器中正常調試。對于大多數情況來(lái)說(shuō),普通的-g選項就足夠了。需要注意的是,GCC雖然允許同時(shí)使用-g(調試)和-o(優(yōu)化)選項,但優(yōu)化會(huì )影 響最終生成的代碼,導致程序源代碼和二進(jìn)制代碼之間的關(guān)系變得復雜起來(lái)。如果不想為調試制造障礙,建議不要將-g和-o選項一同使用,并且只在程序徹底調 試完后才開(kāi)始進(jìn)行代碼優(yōu)化。這樣調試過(guò)程將變得相對輕松和愉快。
基本應用
現在可以啟動(dòng)GDB來(lái)調試已經(jīng)生成的可執行程序debugme,命令如下:
# gdb debugme
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
……
(gdb)
如果一切正常,GDB將被啟動(dòng)并在屏幕上輸出版權信息,但如果使用了-q或--quiet選項則不會(huì )顯示它們。啟動(dòng)GDB時(shí)另外一個(gè)有用的命令行選項是“-d dirname”,其中dirname是一個(gè)目錄名。該目錄名告訴GDB應該到哪里去尋找源代碼。
一旦出現GDB的命令提示符(gdb),就表明GDB已經(jīng)準備好接收來(lái)自用戶(hù)的各種調試命令了。如果想在調試環(huán)境下運行這個(gè)程序,可以使用GDB提供的 “run”命令,而程序在正常運行時(shí)所需的各種參數可以作為“run”命令的參數傳入,或者使用單獨的“set args”命令進(jìn)行設置。如果在執行“run”命令時(shí)沒(méi)有給出任何參數,GDB將使用上一次“run”或“set args”命令指定的參數。如果想取消上次設置的參數,可以執行不帶任何參數的“set args”命令。下面嘗試在調試器中運行這個(gè)程序:
(gdb) run
……
Program received signal SIGSEGV, Segmentation fault.
0x4000c6ac in _dl_fini () from /lib/ld-linux.so.2
最后一行輸出表明程序在調用動(dòng)態(tài)鏈接庫/lib/ld-linux.so.2中的_dl_fini()函數時(shí)出現了錯誤,地址是0x4000c6ac。 這些對調試是非常重要的線(xiàn)索。另外還有一種信息對調試也很重要,就是錯誤發(fā)生時(shí)的函數調用層級關(guān)系,可以通過(guò)執行“backtrace”命令來(lái)獲得。在使 用GDB調試命令時(shí),用戶(hù)可以不必輸入完整的命令名稱(chēng),使用任何惟一的縮寫(xiě)都可以。例如“backtrace”命令就可以縮寫(xiě)成“back”甚至 “bt”。GDB還支持很多常用的Shell命令編輯特征,比如可以像在bash或tcsh中那樣按Tab鍵補齊命令。如果相關(guān)命令不惟一的話(huà),則列出所 有可能的匹配項。此外鍵盤(pán)上的方向鍵可用來(lái)翻動(dòng)歷史命令。
GDB是一個(gè)源代碼級的調試器,使用“list”命令可以查看當前調試對象的源代碼。該命令的通用格式為“list [m,n]”,表示顯示從m行開(kāi)始到n行結束的代碼段,而不帶任何參數的“list”命令將顯示最近10行源代碼。
設置斷點(diǎn)
在調試有問(wèn)題的代碼時(shí),在某一點(diǎn)停止運行往往很管用。這樣程序運行到此外時(shí)會(huì )暫時(shí)掛起,等待用戶(hù)的進(jìn)一步輸入。GDB允許在幾種不同的代碼結構上設置斷 點(diǎn),包括行號和函數名等,并且還允許設置條件斷點(diǎn),讓程序只有在滿(mǎn)足一定的條件時(shí)才停止執行。要根據行號設置斷點(diǎn),可以使用“ break linenum”命令。要根據函數名設置斷點(diǎn),則應該使用“break funcname”命令。
在以上兩種情況中,GDB將在 執行指定的行號或進(jìn)入指定的函數之前停止執行程序。此時(shí)可以使用“print”顯示變量的值,或者使用“list”查看將要執行的代碼。對于由多個(gè)源文件 組成的項目,如果想在執行到非當前源文件的某行或某個(gè)函數時(shí)停止執行,可以使用如下形式的命令:
# break filename:linenum
# break filename:funcname
條件斷點(diǎn)允許當一定條件滿(mǎn)足時(shí)暫時(shí)停止程序的執行。它對于調試來(lái)講非常有用。設置條件斷點(diǎn)的正確語(yǔ)法如下:
break linenum if expr
break funcname if expr
其中expr是一個(gè)邏輯表達式。當該表達式的值為真時(shí),程序將在該斷點(diǎn)處暫時(shí)掛起。例如,下面的命令將在debugme程序的第38行設置一個(gè)條件斷點(diǎn)。當程序運行到該行時(shí),如果count的值等于3,就將暫時(shí)停止執行:
(gdb) break 38 if count==3
設置斷點(diǎn)是調試程序時(shí)最常用到的一種手段。它可以中斷程序的運行,給程序員一個(gè)單步跟蹤的機會(huì )。使用命令“ break main”在main函數上設置斷點(diǎn)可以在程序啟動(dòng)時(shí)就開(kāi)始進(jìn)行跟蹤。
接下去使用“continue”命令繼續執行程序,直到遇到下一個(gè)斷點(diǎn)。如果在調試時(shí)設置了很多斷點(diǎn),可以隨時(shí)使用“info breakpoints”命令來(lái)查看設置的斷點(diǎn)。此外,開(kāi)發(fā)人員還可以使用“delete”命令刪除斷點(diǎn),或者使用“disable”命令來(lái)使設置的斷點(diǎn) 暫時(shí)無(wú)效。被設置為無(wú)效的斷點(diǎn)在需要的時(shí)候可以用“enable”命令使其重新生效。
觀(guān)察變量
GDB最有用的特性之 一是能夠顯示被調試程序中幾乎任何表達式、變量或數組的類(lèi)型和值,并且能夠用編寫(xiě)程序所用的語(yǔ)言打印出任何合法表達式的值。查看數據最簡(jiǎn)單的辦法是使用 “print”命令,只需在“print”命令后面加上變量表達式,就可以打印出此變量表達式的當前值,示例如下:
(gdb) print str
$1 = 0x40015360 "Happy new year!\n"
從輸出信息中可以看出,輸入字符串被正確地存儲在了字符指針str所指向的內存緩沖區中。除了給出變量表達式的值外,“print”命令的輸出信息中還 包含變量標號($1)和對應的內存地址(0x40015360)。變量標號保存著(zhù)被檢查數值的歷史記錄,如果此后還想訪(fǎng)問(wèn)這些值,就可以直接使用別名而不 用重新輸入變量表達式。
如果想知道變量的類(lèi)型,可以使用“whatis”命令,示例如下:
(gdb) whatis str
type = char *
對于第一次調試別人的代碼,或者面對的是一個(gè)異常復雜的系統時(shí),“whatis”命令的作用不容忽視。
單步執行
為了單步跟蹤代碼,可以使用單步跟蹤命令“step”,它每次執行源代碼中的一行。
在GDB中可以使用許多方法來(lái)簡(jiǎn)化操作,除了可以將“step”命令簡(jiǎn)化為“s”之外,還可以直接輸入回車(chē)鍵來(lái)重復執行前面一條命令。
除了可以用“step”命令來(lái)單步運行程序之外,GDB還提供了另外一條單步調試命令“next”。兩者功能非常相似,差別在于如果將要被執行的代碼行中包含函數調用,使用step命令將跟蹤進(jìn)入函數體內,而使用next命令則不進(jìn)入函數體內。
在進(jìn)入下一部分之前,使用下面的命令退出GDB:
(gdb) quit
分析核心(core)文件
在程序發(fā)生崩潰時(shí),有時(shí)可能無(wú)法直接運行GDB來(lái)進(jìn)行調試。比如程序可能是在另外一臺機器上運行的,或者因為程序對時(shí)間比較敏感,所以手動(dòng)跟蹤調試會(huì )產(chǎn) 生無(wú)法接受的延遲等。遇到這些情況,就只能等到程序運行結束后才能判斷崩潰的原因了。這時(shí)需要用到Linux提供的core dump機制。當程序中出現內存操作錯誤時(shí),會(huì )發(fā)生崩潰并產(chǎn)生核心文件。使用GDB可以對產(chǎn)生的核心文件進(jìn)行分析,找出程序是在什么時(shí)候崩潰的和在崩潰之 前程序都做了些什么。當然,如果要用GDB來(lái)分析核心文件,也必須在編譯時(shí)加上-g選項來(lái)產(chǎn)生調試符號表。
在分析核心文件之前必須確認系統是否允許生成核心文件,很多Linux發(fā)行版在默認時(shí)禁止生成核心文件。為了生成核心文件,首先必須執行下面的命令:
# ulimit -c unlimited
然后就可以生成核心文件了。這里仍以前面的debugme程序為例,再次執行下面命令將產(chǎn)生核心文件:
# ./debugme
Enter a string to count words:Happy new year!
The number of words is 3.
Segmentation fault (core dumped)
生成的核心文件名根據系統配置的不同會(huì )有所差異。要在GDB中分析核心文件,除了要給出核心文件的文件名外,還必須給出生成該核心文件的可執行程序的名稱(chēng),示例如下:
#gdb debugme core.547
……
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/libc.so.6...done.
……
從GDB的輸出信息中可以看出,產(chǎn)生這個(gè)核心文件的原因是因為程序收到了序號為11的信號。如果想知道程序在崩潰之前運行到了哪里,可以使用“backtrace”或“info stack”命令查看一下堆棧的歷史記錄。示例如下:
(gdb) info stack
#0 0x4000c6ac in _dl_fini () from /lib/ld-linux.so.2
#1 0x40057940 in exit () from /lib/libc.so.6
#2 0x4004291f in _libc_start_main () from /lib/libc.so.6
由上可知,程序崩潰時(shí)正處于_dl_fini()函數之中。但很多時(shí)候程序員感興趣的可能并不是這個(gè),而是exit()或 _libc_start_main()函數,因為它們才可能是問(wèn)題真正的癥結所在。GDB提供的“frame”命令可以用來(lái)在不同的調用上下文中切換。例 如下面的命令可以查看exit()函數在執行時(shí)的狀況:
(gdb) frame 1
#1 0x40057940 in exit () from /lib/libc.so.6
此外還可以用“up”或“down”命令在不同的函數調用上下文中切換。開(kāi)發(fā)人員使用這三條命令可以很輕松地實(shí)現調用棧的遍歷。在分析核心文件時(shí),通過(guò)將遍歷棧的命令和檢查變量值的“print”命令結合起來(lái),就能夠復原程序運行時(shí)的全部景象。
調試其它進(jìn)程
有時(shí)會(huì )遇到一種很特殊的調試需求,對當前正在運行的其它進(jìn)程進(jìn)行調試。這種情況有可能發(fā)生在那些無(wú)法直接在調試器中運行的進(jìn)程身上,例如有的進(jìn)程只能在 系統啟動(dòng)時(shí)運行。另外如果需要對進(jìn)程產(chǎn)生的子進(jìn)程進(jìn)行調試的話(huà),也只能采用這種方式。GDB可以對正在執行的程序進(jìn)行調度,它允許開(kāi)發(fā)人員中斷程序并查看 其狀態(tài),之后還能讓這個(gè)程序正常地繼續執行。
GDB提供了兩種方式來(lái)調試正在運行的進(jìn)程:一種是在GDB命令行上指定進(jìn)程的PID,另一種是在GDB中使用“attach”命令。例如,開(kāi)發(fā)人員可以先啟動(dòng)debugme程序,讓其開(kāi)始等待用戶(hù)的輸入。示例如下:
#./debugme
Enter a string to count words:
接下去在另一個(gè)虛擬控制臺中用下面的命令查出該進(jìn)程對應的進(jìn)程號:
# ps -ax | grep debugme
555 pts/1 S 0:00 ./debugme
得到進(jìn)程的PID后,就可以使用GDB對其進(jìn)行調試了:
# gdb debugme 555
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Attaching to program: /home/xiaowp/debugme, process 555
Reading symbols from /lib/libc.so.6...done.
……
在上面的輸出信息中,以Attaching to program開(kāi)始的行表明GDB已經(jīng)成功地附加在PID為555的進(jìn)程上了。另外一種連接到其它進(jìn)程的方法是先用file命令加載調試時(shí)所需的符號表,然后再通過(guò)“attaché”命令進(jìn)行連接:
(gdb) file /home/xiaowp/debugme
Reading symbols from /home/xiaowp/debugme...done.
(gdb) attach 555
……
如果想知道程序現在運行到了哪里,同樣可以使用“backtrace”命令。當然也可以使用“step”命令對程序進(jìn)行單步調試。
在完成調試之后,不要忘記用detach命令斷開(kāi)連接,讓被調試的進(jìn)程可以繼續正常運行:
GDB是Linux下一個(gè)最基本的調試器,其功能非常豐富。完整地介紹GDB的功能可能需要幾百頁(yè),本文只涵蓋了GDB的一些最常見(jiàn)的用法。作為一個(gè)合格的Linux程序員,花在GDB上的功夫和時(shí)間越多,從調試中獲得的益處就越多。