笨辦法學C 練習31:代碼調試

練習31:代碼調試

原文:Exercise 31: Debugging Code

譯者:飛龍

我已經教給你一些關于我的強大的調試宏的技巧,并且你已經開始用它們了。當我調試代碼時,我使用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.”的錯誤。這種情況下,你需要停止gdbex31程序,并重新啟動程序使它工作,只要你成功輸入了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打印出我需要了解的變量(argcargv),以及停下來的位置,之后打印出斷點的行號。

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使它在幾個迭代之后崩潰,之后嘗試得到它的核心轉儲并分析。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容

  • 程序調試的基本思想是“分析現象->假設錯誤原因->產生新的現象去驗證假設”這樣一個循環過程,根據現象如何假設錯誤原...
    Manfred_Zone閱讀 16,559評論 0 26
  • iOS包含許多“秘密”調試工具,包括環境變量、偏好、GCB的常規調用,等等。本技術說明描述了這些工具。如果你開發i...
    栗子烤肉閱讀 3,186評論 1 7
  • 你是否曾經苦惱于理解你的代碼,而去嘗試打印一個變量的值? NSLog(@"%@", whatIsInsideThi...
    paraneaeee閱讀 1,205評論 0 7
  • 【晨起感恩】 感恩這每一個醒來的早晨,我還可以呼吸,可以生而為人。 感恩父母的生養,感恩生命經由你們流淌向我。 感...
    黛兒微笑閱讀 300評論 3 4
  • 共一輪明月中 同一片星空下 未來的啊 我在望著你 在心中已經畫了一百幅畫 你到底像哪一個呢 比這些都美吧 當未來的...
    用不完的橡皮擦閱讀 215評論 0 0