gdb

第十章 gdbshell

程序中除了一目瞭然的Bug以外都須要必定的調試手段來分析到底錯在哪。到目前爲止咱們的調試手段只有一種:根據程序執行時的出錯現象假設錯誤緣由,而後在代碼中適當的爲止插入printf,執行程序並分析打印結果,若是結果和預期的同樣,就基本上證實了本身假設的錯誤緣由,就能夠動手修正Bug了,若是結果和預期的不同,就根據結果作進一步的假設和分析。調試工具gdb的基本思想仍然是「分析現象->假設錯誤緣由->產生新的現象去驗證假設」。數組

1.單步執行和跟蹤函數調用函數

在編譯時要加上-g選項,生成的可執行文件才能用gdb進行調試:工具

 

-g選項的做用是在可執行 文件中加入源代碼的信息,好比可執行文件中第幾條機器指令對應源代碼的第幾行,但並非把整個源文件嵌入到可執行文件中,因此在調試時必須保證gdb能找到源文件。gdb提供一個相似shell的命令行環境,上面的(gdb)就是提示符,在這個提示符下輸入help能夠查看命令的類別,能夠進一步查看某一類別中有哪些命令,例如查看files類別下有哪些命令能夠用:學習

 

如今試試用list命令從第一行開始列出源代碼:操作系統

 

一次只列10行,若是要從11行開始繼續列源代碼能夠輸入命令行

 

也能夠什麼都不輸直接敲回車,gdb提供了一個很方便的功能,在提示符下直接敲回車表示用適當的參數重複上一條命令。gdb的不少經常使用命令有簡寫形式,例如list命令能夠寫成l,要列一個函數的源代碼也能夠用函數名作參數:3d

 

如今退出gdb的環境:調試

 

如今把源代碼更名或移到別處再用gdb調試,就列不出源代碼了:blog

可見gcc的-g選項並非把源代碼 嵌入到可執行文件中的,在調試時也須要源文件。如今把源代碼恢復原樣,咱們繼續調試。首先用start命令開始執行程序:

 

這表示停在main函數中變量定義以後的第一條語句處等待咱們發命令,gdb列出這條語句表示它還沒執行,而且立刻要執行。咱們能夠用next命令(簡寫爲n)控制這些語句一條一條地執行:

 

用n命令依次執行兩行賦值語句和一行打印語句,在執行打印語句時結果馬上打出來了,而後停留在return語句以前等待咱們發命令。雖然咱們徹底控制了程序的執行,但仍然看不出哪裏錯了,由於錯誤不在main函數而在add_range函數,如今用start命令從新來過,此次用step命令(簡寫爲s)進入函數中去執行:

 

此次停在了函數中變量定義以後的第一條語句處。在函數中有幾種查看狀態的辦法,backtrace命令(簡寫爲bt)能夠查看函數調用的棧幀:

 

可見當前的add_range函數是被main函數調用的,main傳進來的參數是low=1,high=10。main函數的棧幀編號爲1,add_range的棧幀編號爲0。如今能夠用info命令(簡寫爲i)查看add_range局部變量的值:

 

若是想查看main函數當前局部變量的值也能夠作到,先用frame命令(簡寫爲f)選擇1號棧幀後再查看局部變量:

 

注意到result數組中有不少元素具備雜亂無章的值,咱們知道,未經初始化的局部變量具備不肯定的值。到目前爲止一切正常。用s或n往下走幾步,而後用print命令(簡寫爲p)打出變量sum的值:

 

第一次循環i是1,第二次循環i是2,加起來是3,沒錯。這裏的$1表示gdb保存着這些中間結果,$後面的編號會自動增加,在命令中能夠用$一、$二、$3等編號代替相應的值。因爲咱們原本就知道第一次調用的結果是正確的,再往下跟也沒意義了,能夠用finish命令讓程序一直運行到從當前函數返回爲止:

 

返回值是55,當前正準備執行賦值操做,用s命令賦值,而後查看result數組:

 

第一個值55確實賦給了result數組的第0個元素。下面用s命令進入第二次add_range掉用,進入以後首先查看參數和局部變量:

 

因爲局部變量i和sum沒初始化,因此具備不肯定的值,又因爲兩次調用是挨着的,i和sum正好取了上次調用時的值。i的初值不是0倒不要緊,在for循環中會賦值爲0的,但sum若是初值不是0,累加獲得的結果就錯了。好了,咱們已經找到錯誤緣由,能夠退出gdb修改源代碼了。若是咱們不想浪費這一次調試機會,能夠在gdb中立刻把sum的初值改成0繼續運行,看看這一處改了以後還有沒有別的Bug:

 

這樣結果就對了。修改變量的值除了用set命令以外也能夠用print命令,由於print命令後面跟的是表達式,而咱們知道賦值和函數調用也都是表達式,因此還能夠用print來修改變量的值,或者調用函數:

 

printf的返回值表示實際打印的字符數,因此$6的結果是13。

前面用到的gdb的基本命令:

 

2.斷點

斷點調試實例:

 

這個程序的做用是:首先從鍵盤讀入一串數字存到字符數組input中,而後轉換成整型存到sum中,而後打印出來,一直這樣循環下去。scanf(「%s」, input);這個調用功能是等待用戶輸入一個字符串並回車,scanf把其中第一段非空白(非空格、Tab、換行)的字符串放到input數組中,並自動在末尾添加‘\0’。接下來的循環從左到右掃描字符串並把每一個數字累加到結果中,例如輸入是「2345」,則循環累加的過程是(((0*10+2)*10+3)*10+4)*10+5=2345。注意字符型的‘2’要減去‘0’的ASCII碼才能轉換成整數值的2,‘0’的ASCII碼是48,而‘\0’的ASCII碼是0,兩者是不相同的。下面運行程序看問題:

 

又是這種現象,第一次是對的,第二次不對。而這個程序咱們賦了初值,下面調試:

 

可見,若是變量要賦初值,start不會跳過變量定義語句。

用display命令使得每次停下來的時候都顯示當前sum值,而後繼續往下走:

 

用undisplay能夠取消對先前設置的那些變量的跟蹤。這個循環應該是沒有問題的,由於第一次的結果正確。若是不想一步一步走這個循環,能夠用break命令(簡寫爲b)在第9行設一個斷點(Breakpoint):

 

break命令的參數也能夠是函數名,表示在某一個函數開頭設斷點。如今用continue命令(簡寫爲c)連續運行而非單步運行,程序到達斷點會自動停下來,這樣就能夠停在下一次循環的開頭:

 

而後輸入新的字符串準備轉換:

 

問題暴露出來了,新的轉換應該再次從0開始累加,而sum如今已是123了,緣由在於新的循環沒有把sum歸零。可見斷點有助於快速跳過與問題無關的代碼,而後在有問題的代碼上慢慢走慢慢分析,「斷點加單步」是使用調試器的基本方法。至於應該在哪裏設置斷點,怎麼知道哪些代碼能夠跳過而哪些代碼要慢慢走,也要經過對錯誤現象的分析和假設來肯定,就像之前分析肯定在哪裏插入printf語句同樣。一次調試能夠設置多個斷點,用info命令能夠查看已經設置的斷點:

 

每一個斷點都有一個編號,能夠用編號指定刪除某個斷點:

 

有時候一個斷點暫時不想用能夠禁用掉而沒必要刪除,這樣之後想用的時候能夠直接啓用,而沒必要從新從代碼裏找應該在哪一行設斷點:

 

gdb的斷點功能很是靈活,還能夠設置斷點在知足某個條件時才激活,例如咱們仍然在循環開頭設置斷點,可是僅當sum不等於0時才中斷,而後用run命令(簡寫爲r)從新從程序開頭連續執行:

 

結果是第一次執行scanf以前沒有中斷,第二次卻中斷了。

gdb基本命令:

 

3.觀察點

斷點是當程序執行到某一代碼行時中斷,而觀察點(Watchpoint)是當程序訪問某一存儲單元時中斷,若是咱們不知道某一存儲單元時在哪裏被改動的,這時候觀察點尤爲有用。

觀察點調試實例:

 

下面善春原來設的斷點,從頭執行程序,重複上次的輸入,用watch命令設置觀察點,跟蹤input[4]後面那個字節(input[5]):

 

gdb基本命令:

 

4.段錯誤

在gdb中遇到段錯誤就會停下來。如scanf輸入整型變量就必需要加&,不然就會出段錯誤,而輸出字符串就不要加&。

學習C語言不可能不去了解底層計算機體系結構和操做系統的原理,不瞭解底層原理一個scanf都用很差,更沒辦法寫出正確的程序。

相關文章
相關標籤/搜索