即使經(jīng)驗非常豐富的程序員,在編寫程序的時候也避免不了出錯粥谬。
程序中的語法錯誤通掣馗可以在翻譯階段就能被診斷出來,
但邏輯錯誤卻很難被發(fā)現(xiàn)和糾正漏策,
比如在解決問題時使用了錯誤或者不完備的思路派哲。
在這種情況下調(diào)試可能是唯一辦法。
通過設(shè)置適當(dāng)?shù)臄帱c哟玷,你可以觀察結(jié)果并和預(yù)期的結(jié)果進(jìn)行比較以縮小問題代碼的范圍狮辽,并最終發(fā)現(xiàn)問題所在一也。
什么是調(diào)試?
調(diào)試(Debug)喉脖,就是讓代碼一步一步執(zhí)行椰苟,跟蹤程序的運行過程。
比如树叽,可讓程序停在某個地方舆蝴,查看當(dāng)前所有變量的值,或者內(nèi)存中的數(shù)據(jù)题诵;
也可以讓程序一次只執(zhí)行一條或者幾條語句洁仗,查看到底執(zhí)行了哪些分支。
在調(diào)試過程中性锭,可以監(jiān)控程序的每一個細(xì)節(jié)赠潦,包括變量的值、函數(shù)的調(diào)用過程草冈、內(nèi)存中數(shù)據(jù)她奥、線程的調(diào)度等,發(fā)現(xiàn)隱藏的錯誤或者低效的代碼怎棱。
編譯器可以發(fā)現(xiàn)程序的語法錯誤哩俭,
調(diào)試可以發(fā)現(xiàn)程序的邏輯錯誤。
所謂邏輯錯誤是指代碼思路或者設(shè)計上的缺陷拳恋。
學(xué)習(xí)調(diào)試也可以增加編程的功力凡资,能更加了解自己的程序,變量是什么時候賦值的谬运、內(nèi)存是什么時候分配的隙赁。
調(diào)試是每個程序員必須掌握的技能,沒有選擇的余地吩谦!
調(diào)試需要借助專業(yè)的輔助軟件——調(diào)試器(Debugger)鸳谜。
現(xiàn)在主流C/C++調(diào)試器有下面幾種:
1) Remote Debugger
Remote Debugger 是 VC/VS 自帶的調(diào)試器,與整個IDE無縫銜接式廷,使用非常方便咐扭,初學(xué)者建議使用該調(diào)試器,本教程也以 VS2010 為例講解調(diào)試技巧滑废。
2) WinDbg
Windows 下的調(diào)試器蝗肪,功能甚至超越了 Remote Debugger,它還有一個命令行版本(cdb.exe)蠕趁,但是這個命令行版本的調(diào)試器指令比較復(fù)雜薛闪,不建議初學(xué)者使用。
3) LLDB
XCode 自帶的調(diào)試器俺陋,Mac OS X 下開發(fā)必備調(diào)試器豁延。
4) GDB
Linux 下使用最多的一款調(diào)試器昙篙,也有 Windows 的移植版。
關(guān)于 GDB
GDB 是 Linux 下的 C/C++ 程序調(diào)試工具诱咏,開源免費苔可,功能強(qiáng)大,命令眾多.
GDB 調(diào)試器圖標(biāo)長成這樣
Windows C/C++ 開發(fā)的一定要熟悉 Visual Studio袋狞、
Java 開發(fā)的要熟悉 Eclipse 或 IntelliJ IDEA焚辅、
Android 開發(fā)的要熟悉 Android Studio、
iOS 開發(fā)的要熟悉 XCode 一樣苟鸯,
Linux C/C++ 開發(fā)要熟悉 GDB同蜻。
雖然 Linux 系統(tǒng)下編寫 C/C++ 代碼的 IDE 可以自由選擇,但是調(diào)試生成的 C/C++ 程序一定是直接或者間接使用 GDB早处。
調(diào)試是開發(fā)流程中一重要環(huán)節(jié)
從事 Linux C/C++ 開發(fā)人員熟練使用 GDB 調(diào)試是一項基本要求湾蔓。
GDB 入門
以下面的代碼為例在 Linux 系統(tǒng)下來講解 GBD 的調(diào)試流程:
int main (void) {
unsigned long long int n, sum;
n = 1;
sum = 0;
while (n <= 100) {
sum = sum + n;
n = n + 1;
}
return 0;
}
將代碼保存到 /demo/main.c,表示當(dāng)前用戶的主目錄陕赃,也即 home 目錄卵蛉。
首先使用 GCC 來編譯 main.c,編譯的時候使用-g選項么库,目的是向可執(zhí)行程序中加入調(diào)試信息,包括源代碼甘有、符號表等诉儒,GDB 需要這些額外的信息來完成調(diào)試工作。
此外還建議關(guān)閉編譯器的程序優(yōu)化選項亏掀。編譯器的程序優(yōu)化選項一般有五個級別忱反,從 O0 ~ O4, O0 表示不優(yōu)化滤愕,從 O1 ~ O4 優(yōu)化級別越來越高温算,O4 最高。這樣做的目的是為了調(diào)試的時候间影,符號文件顯示的調(diào)試變量等能與源代碼完全對應(yīng)起來注竿。
使用 GCC 編譯源文件:
$ cd demo
$ gcc main.c -o main.out -g
打開 demo 文件夾,發(fā)現(xiàn)多了一個 mian.out魂贬,說明編譯成功巩割。
1) 啟動 GDB 調(diào)試器
接下來啟動 GDB 并調(diào)試 main.out:
$ gdb main.out -silent
Reading symbols from /home/mozhiyan/demo/main.out...done.
選項-silent用于屏蔽 GDB 的前導(dǎo)信息,否則它會在屏幕上打印一堆免責(zé)條款付燥。
啟動 GDB 后宣谈,輸出的信息表明已經(jīng)讀入了 mian.out 的符號表。
接下來键科,GDB 會顯示自己的提示符(gbd)闻丑,提示并等待你輸入調(diào)試命令漩怎。
2) gdb -b 選項:設(shè)置斷點
調(diào)試一個程序的時候,應(yīng)在我們關(guān)注的地方嗦嗡,或在故障點的前邊設(shè)置一個斷點(Breakpoint)勋锤,讓程序執(zhí)行到這里停下來,這樣我們就可以慢慢地用別的調(diào)試命令進(jìn)行觀察酸钦。
在 GDB 中怪得,設(shè)置斷點的方法很多,包括在指定的內(nèi)存地址處設(shè)置斷點卑硫、在源代碼的某一行設(shè)置斷點徒恋,或者在某個函數(shù)的入口處設(shè)置斷點等等。
設(shè)置斷點的命令是b或者break欢伏,在這里我 們是將 main 函數(shù)的入口處作為斷點:
(gdb) b main
Breakpoint 1 at 0x4004f4: file main.c, line 5.
b 命令在執(zhí)行后返回了斷點的具體信息入挣,也就是說,斷點(main 函數(shù)的入口位置)的內(nèi)存地址為 0x4004f4硝拧,對應(yīng)于源文件的第 5 行(也就是說,main 函數(shù)位于源文件的第 5 行)滋恬。
如果我們用內(nèi)存地址的方式來設(shè)置這個斷點抱究,則可以是:
b * 0x4004f4
星號*意味著是以內(nèi)存地址作為斷點的鼓寺。
如果用源代碼行的形式設(shè)置這個斷點,則可以是
b 5
3) gdb -r 選項:執(zhí)行程序
一旦設(shè)置了斷點敢靡,下一步就是用r或者run命令執(zhí)行被調(diào)試的程序啸胧,執(zhí)行后會自動在第一個斷點處停下來:
Starting program: /home/mozhiyan/demo/main.out
[New Thread 1500.0x1e34]
[New Thread 1500.0x2fb8]
Thread 1 hit Breakpoint 1, main () at main.c:5
5 n = 1;
在運行了被調(diào)試的程序后吓揪,GDB 的輸出信息顯示程序己經(jīng)啟動柠辞,下一個將要執(zhí)行的語句是第 5 行的n = 1;主胧。
注意,這條語句并沒有執(zhí)行焙格,而僅僅是告訴你,再繼續(xù)執(zhí)行程序的話予颤,執(zhí)行的語句會是它冬阳。
4) gdb -p 選項:打印變量的值
在當(dāng)前位置 變量 n 和 sum 己經(jīng)分配,但并沒有開始賦值驳庭。此時氯窍,這兩個變量的值會是多少呢?我們可以使用p或者print命令來分別顯示:
(gdb) p n
$1 = 24
(gdb) p sum
$2 = 140737488347344
GDB 的 p 命令用于打印一個表達(dá)式的值,在這里是表達(dá)式 n 和 sum政供。
GDB 先計算表達(dá)式的值鲫骗,并把它保存在一個存儲區(qū)中执泰,存儲區(qū)的名字用1 = 16毯侦,意思是表達(dá)式 n 的值保存在 $1 中具垫,其內(nèi)容為 16侈离。
注意,在你的計算機(jī)上卦碾,變量 n 和 sum 的當(dāng)前值可能和這里顯示的不同。這很好理解起宽,內(nèi)存是反復(fù)使用的,當(dāng)一個程序終止后燎含,它占用的內(nèi)存會分配給其他程序使用宾濒;當(dāng)一個變量不再使用后屏箍,它占用的內(nèi)存也會重新分配绘梦,并成為另一個變量。因為變量 n 和 sum 剛剛分配赴魁,還沒有往里面保存任何數(shù)值卸奉,故它們的內(nèi)容是隨機(jī)的颖御,是其他程序或者變量用過的垃圾值榄棵。
順便說一下疹鳄,既然 $1 是 GDB 用于保存計算結(jié)果的內(nèi)部存儲區(qū)的名字,那么我們也可以用 p 命令來打印它:
(gdb) p $1
$3 = 24
5) gdb -n 選項:單步調(diào)試
下面腺怯,我們將通過單步執(zhí)行程序,來看一看變量 n 和 sum 賦值后的值呛占。調(diào)試命令n或者next用于繼續(xù)執(zhí)行源文件中的下一行晾虑。
(gdb) n
6 sum = 0;
執(zhí)行 n 命令后,實際執(zhí)行的是第 5 行n = 1;,GDB 顯示下一個即將執(zhí)行的源代碼行走贪,也就是第 6 行的sum = 0;佛猛。
因為此時己經(jīng)往變量 n 寫入了 1,所以我們可繼續(xù)用 p 命令來觀察它現(xiàn)在的存儲值:
(gdb) p n
$4 = 1
顯然經(jīng)賦值后坠狡,變量 n 的值己經(jīng)變成 1继找。
繼續(xù)執(zhí)行下一條指令,實際執(zhí)行的是第 6 行sum = 0逃沿。執(zhí)行后婴渡,GDB 停下并顯示下一條即將執(zhí)行的源代碼行,也即第 8 行的while (n <= 100)凯亮,第 7 行為空行边臼,所以直接跳過了:
(gdb) n
8 while (n <= 100)
剛才執(zhí)行的語句是往變量 sum 保存數(shù)值 0,故我們可以再次用 p 命令來觀察變量 sum 現(xiàn)在的存儲值假消,可發(fā)現(xiàn)它己經(jīng)變成 0:
(gdb) p sum
$5 = 0
繼續(xù)用 n 命令執(zhí)行下一個源代碼行柠并,則將計算 while 語句的控制表達(dá)式,并根據(jù)該表達(dá)式的值決定是否進(jìn)入循環(huán)體富拗,執(zhí)行后 GDB 顯示下一條即將執(zhí)行的源代碼行是第 10 行:
(gdb) n
10 sum = sum + n;
進(jìn)入循環(huán)體之后臼予,我們想再看看變量 n 和 sum 的當(dāng)前值。但這次使用 p 命令的方法不一樣啃沪,這次是用花括號將表達(dá)式 n 和 sum 圍住以形成一個集合粘拾。GDB 允許用這種方式來一次性地打印多個表達(dá)式的值:
(gdb) p {n, sum}
$6 = {1, 0}
顯然,變量 n 和 sum 此時的值依然分別為 1 和 0创千。
繼續(xù)用 n 命令執(zhí)行第 10 行缰雇,執(zhí)行后 GDB 停留在即將執(zhí)行的第 11 行:
(gdb) n
11 n = n + 1;
注意,第 10 行己經(jīng)執(zhí)行完畢追驴,但第 11 行還沒有執(zhí)行械哟。猜猜看,變量 n 和 sum 此時的值是多少殿雪?猜測之后戒良,用 p 命令看看結(jié)果是否如你所想:
(gdb) p {n, sum}
$7 = {1, 1}
繼續(xù)用 n 命令執(zhí)行下一個源代碼行,這將執(zhí)行第 11 行的n = n + 1;冠摄,執(zhí)行后控制又回到了循環(huán)的起始處,也即第 8 行:
(gdb) n
8 while (n <= 100)
此時几缭,變量 n 和 sum 的值各自會是多少河泳?使用 p 命令打印一下就知道了:
(gdb) p {n, sum}
$8 = {2, 1}
因為現(xiàn)在處于一個循環(huán)體內(nèi),如果繼續(xù)用 n 命令往下執(zhí)行年栓,則其過程與前面相比大同小異拆挥。前面己經(jīng)循環(huán)過一次,本次循環(huán)完整的調(diào)試過程如下:
(gdb) n
10 sum = sum + n;
(gdb) n
11 n = n + 1;
(gdb) n
8 while (n <= 100)
(gdb) p {n, sum}
$9 = {3, 3}
顯然,第二次循環(huán)過后纸兔,變量 n 的值為 3惰瓜,變量 sum 的值也是 3装盯。
你可能己經(jīng)發(fā)現(xiàn)了曲稼,我們現(xiàn)在進(jìn)退維谷:如果繼續(xù)用 n 命令執(zhí)行下面,則將陷入循環(huán)碟案,直到變量 n 的值等于 101氏捞。好在這也算不上什么大的問題棉饶,我們可以在循環(huán)語句的后面設(shè)置斷點垦页,然后命令程序一直執(zhí)行紊册,直至到達(dá)這個斷點赋续。
6) gdb -l 選項:列出源文件內(nèi)容
為了搞清楚 while 語句的下一條語句的行號男翰,我們需要列出源文件的內(nèi)容,這需要使用l或者list命令:
(gdb) l
3 unsigned long long int n, sum;
4
5 n = 1;
6 sum = 0;
7
8 while (n <= 100)
9 {
10 sum = sum + n;
11 n = n + 1;
12 }
l 命令默認(rèn)每次顯示 10 行源代碼纽乱,但我們關(guān)心的那一行顯然還沒有出來蛾绎。為此,可繼續(xù)使用 l 命令來顯示后面的行:
(gdb) l
13
14 return 0;
15 }
好了鸦列,我們己經(jīng)知道 while 語句之后是 return 語句租冠,它的行號是 14,現(xiàn)在就可以用 b 命令設(shè)置一個新的斷點:
(gdb) b 14
Breakpoint 2 at 0x40051a: file main.c, line 14.
7) gdb -c 選項:繼續(xù)執(zhí)行程序
現(xiàn)在敛熬,可以用一個新的命令c或者continue來持續(xù)執(zhí)行程序肺稀,直至遇到斷點或者程序結(jié)束。因為己經(jīng)設(shè)置斷點应民,故程序?qū)⒊掷m(xù)執(zhí)行话原,在第 14 行處停下:
(gdb) c
Continuing.
Breakpoint 2, main () at main.c:14
14 return 0;
非常好,既然己經(jīng)退出了 while 循環(huán)诲锹,說明累加過程己經(jīng)成功結(jié)束繁仁,變量 sum 的值就是累加結(jié)果。我們來看看它到底是多少:
(gdb) p {n, sum}
$10 = {101, 5050}
顯然归园,變量 n 的當(dāng)前值是 101黄虱,變量 sum 的當(dāng)前值是 5050,和數(shù)學(xué)計算的結(jié)果一模 一樣庸诱。
- gdb -q 選項:退出調(diào)試
本次調(diào)試即將結(jié)束捻浦,我們可以先用 c 命令讓程序“跑完全程”,然后再用q或者quit結(jié)束本次調(diào)試工作桥爽,這將使得調(diào)試器 GDB 結(jié)束運行并返回到操作系統(tǒng):
(gdb) c
Continuing.
[Inferior 1 (process 2814) exited normally]
(gdb) q
[c.biancheng.net demo]$