GDB的深刻研究

GDB的深刻研究

1、GDB代碼調試

(一)GDB調試實例

  • 在終端中編譯一個示例C語言小程序,保存爲文件 gdblianxi.c 中,用GCC編譯。

  • 在上面的命令行中,使用-o參數指定了編譯生成的可執行文件名爲 gdblianxi,使用參數-g表示將源代碼信息編譯到可執行文件中。若是不使用參數-g,會給後面的GDB調試形成不便。
  • 下面輸入「gdb」命令啓動GDB,將首先顯示GDB說明:

  • 下面使用「file」命令載入被調試程序 gdblianxi(這裏的 gdblianxi 即前面 GCC 編譯輸出的可執行文件)

  • 上圖中最後一行「(gdb) 」爲GDB內部命令引導符,等待用戶輸入GDB命令。
  • 上圖倒數第二行提示已經加載成功。
  • 下面使用「r」命令執行(Run)被調試文件,由於還沒有設置任何斷點,將直接執行到程序結束
  • 使用「b」命令在 main 函數開頭設置一個斷點(Breakpoint)
  • 以後一行提示已經成功設置斷點,並給出了該斷點信息:在源文件 gdblianxi.c 第14行處設置斷點;這是本程序的第一個斷點(序號爲1);斷點處的代碼地址爲 0x40055d。向上看源代碼,第14行中的代碼爲「n = 1」,剛好是 main 函數中的第一個可執行語句(由於前面的「int n;」爲變量定義語句,並不是可執行語句)。
  • 再次使用「r」命令執行(Run)被調試程序:

  • 程序中斷在gdblianxi.c第14行處,即main函數是第一個可執行語句處。 上面最後一行信息爲:下一條將要執行的源代碼爲「n = 1;」,它是源代碼文件gdblianxi.c中的第14行。
  • 下面使用「s」命令(Step)執行下一行代碼(即第14行「n = 1;」):

  • 上面的信息表示已經執行完「n = 1;」,並顯示下一條要執行的代碼爲第15行的「n++;」。前端

  • 下面咱們分別在第21行打印處、tempFunction 函數開頭各設置一個斷點(分別使用命令「b 21」「b tempFunction」):linux

  • 使用「c」命令繼續(Continue)執行被調試程序,程序將中斷在第二個斷點(21行),此時全局變量 nGlobalVar 的值應該是 88;再一次執行「c」命令,程序將中斷於第三個斷點(7行,tempFunction 函數開頭處),此時tempFunction 函數的兩個參數 a、b 的值應分別是 1 和 2:

  • 再一次執行「c」命令(Continue),由於後面再也沒有其它斷點,程序將一直執行到結束:

(二)GDB經常使用命令

命令 解釋 示例
file <文件名> 加載被調試的可執行程序文件。
由於通常都在被調試程序所在目錄下執行GDB,於是文本名不須要帶路徑
(gdb) file gdblianxi
r Run的簡寫,運行被調試的程序。
若是此前沒有下過斷點,則執行完整個程序;若是有斷點,則程序暫停在第一個可用斷點處。
(gdb) r
c Continue的簡寫,繼續執行被調試程序,直至下一個斷點或程序結束。 (gdb) c
b <行號>
b <函數名稱>
b * <函數名稱>
b * <代碼地址>
d [編號]
b: Breakpoint的簡寫,設置斷點。兩可使用「行號」「函數名稱」「執行地址」等方式指定斷點位置。
其中在函數名稱前面加「*」符號表示將斷點設置在「由編譯器生成的prolog代碼處」。若是不瞭解彙編,能夠不予理會此用法。
d: Delete breakpoint的簡寫,刪除指定編號的某個斷點,或刪除全部斷點。斷點編號從1開始遞增。
(gdb) b 8
(gdb) b main
(gdb) b * main
(gdb) b * 0x804835c
(gdb) d
s, n s: 執行一行源程序代碼,若是此行代碼中有函數調用,則進入該函數;
n: 執行一行源程序代碼,此行代碼中的函數調用也一併執行。
s 至關於其它調試器中的「Step Into (單步跟蹤進入)」
n 至關於其它調試器中的「Step Over (單步跟蹤)」
這兩個命令必須在有源代碼調試信息的狀況下才可使用(GCC編譯時使用「-g」參數)。
(gdb) s
(gdb) n
si, ni si命令相似於s命令,ni命令相似於n命令。
所不一樣的是,這兩個命令(si/ni)所針對的是彙編指令,而s/n針對的是源代碼。
(gdb) si
(gdb) ni
p <變量名稱> Print的簡寫,顯示指定變量(臨時變量或全局變量)的值。 (gdb) p i
(gdb) p nGlobalVar
display ...
undisplay <編號>
display,設置程序中斷後欲顯示的數據及其格式。
例如,若是但願每次程序中斷後能夠看到即將被執行的下一條彙編指令,可使用命令「display /i $pc」
其中 $pc 表明當前彙編指令,/i 表示以十六進行顯示。當須要關心彙編代碼時,此命令至關有用。
undispaly,取消先前的display設置,編號從1開始遞增。
(gdb) display /i $pc
(gdb) undisplay 1
i Info的簡寫,用於顯示各種信息,詳情請查閱「help i」。 (gdb) i r
q Quit的簡寫,退出GDB調試環境。 (gdb) q
help [命令名稱] GDB幫助命令,提供對GDB名種命令的解釋說明。
若是指定了「命令名稱」參數,則顯示該命令的詳細說明;若是沒有指定參數,則分類顯示全部GDB命令,供用戶進一步瀏覽和查詢。
(gdb) help display

2、CGDB代碼調試

  • cgdb能夠看做gdb的界面加強版,cgdb主要功能是在調試時進行代碼的同步顯示,這增長了調試的方便性,提升了調試效率。其餘功能則與gdb同樣,可以使用其經常使用命令。因此這裏只作簡單介紹,經常使用命令等參見gdb。
主要功能介紹:

(1)相比GDB,增長了語法加亮的代碼窗口,顯示在GDB窗口的上部,隨GDB的調試位置代碼同步顯示。程序員

(2)斷點設置可視化 。正則表達式

(3)在代碼窗口中可以使用GDB經常使用命令 。編程

(4)在代碼窗口可進行代碼查找,支持正則表達式 。小程序

界面及使用說明

(1)代碼窗口windows

調試時同步顯示被調試程序源代碼,自動標記出程序運行到的位置。當焦點在代碼窗口時,能夠瀏覽代碼、查找代碼以及執行命令 ,操做方式同vi 。經常使用命令以下:數組

i : 焦點切換到GDB窗口 。
 o :打開文件選擇框,可選擇要顯示的代碼文件 。
 空格 :設置/取消斷點 。
 k:向上移動
 j:向下移動
 /:查找

(2)狀態條窗口數據結構

  • 同vi的狀態條,通常顯示當前打開的源文件名,當代碼窗口進入命令狀態時,顯示輸入的命令等信息

(3)GDB窗口多線程

  • CGDB的操做界面,同GDB ,按ESC鍵則焦點切換到代碼窗口 。

  • 啓動&退出——啓動:cgdb;退出:在代碼窗口或GDB窗口,執行quit命令 。

代碼實現:

「(gdb)」表示GDB已經啓動,等待咱們輸入命令。此時程序並未開始運行,輸入「run」開始運行程序。這種方式在GDB內部運行程序:

List n,m表示顯示n到m行的代碼

設置斷點,break n,用step單步執行(這裏break 21):

3、彙編代碼調試

彙編級的調試或跟蹤,須要用到display命令「display /i $pc」,如上表所示,

「display /i $pc」
其中 $pc 表明當前彙編指令,/i 表示以十六進行顯示。當須要關心彙編代碼時,此命令至關有用。
undispaly,取消先前的display設置,編號從1開始遞增。

而且之後程序每次中斷都將顯示下一條彙編指定(「si」命令用於執行一條彙編代碼——區別於「s」執行一行C代碼)

接下來咱們試一下命令「b * <函數名稱> 」。 爲了更簡明,有必要先刪除目前全部斷點(使用「d」命令——Delete breakpoint)

當被詢問是否刪除全部斷點時,輸入「y」並按回車鍵便可。
下面使用命令「b *main」在 main 函數的 prolog 代碼處設置斷點(prolog、epilog,分別表示編譯器在每一個函數的開頭和結尾自行插入的代碼):

此時可使用「i r」命令顯示寄存器中的當前值———「i r」即「Infomation Register」,
也能夠輸入「i r 寄存器名」顯示任意一個指定的寄存器值:

最後輸入命令「q」,退出(Quit)GDB調試環境

4、DDD代碼調試

(一)DDD簡介

  • DDD,全稱是Data Display Debugger,對於Linux系統中的編程人員來講,它就是windows系統下面的visual studio ,功能強大,是Linux世界中少數有圖形界面的程序調試工具。DDD是命令行調試器的圖形前端,除了通常的程序調試功能之外,還具備交互式圖形數據顯示的功能。它在嵌入式應用開發中也十分出色。DDD最初源於1990年Andreas Zeller編寫的VSL結構化語言,後來通過一些程序員的努力,演化成今天的模樣。DDD的功能很是強大,能夠調試用C\C++、Ada、 Fortran、Pascal、Modula-2和Modula-3編寫的程序;能夠超文本方式瀏覽源代碼;可以進行斷點設置、回溯調試和歷史紀錄編輯;具備程序在終端運行的仿真窗口,並在遠程主機上進行調試的能力;圖形數據顯示功能(Graphical Data Display)是建立該調試器的初衷之一,可以顯示各類數據結構之間的關係,並將數據結構以圖形化形式顯示;具備GDB/DBX/XDB的命令行界面,包括徹底的文本編輯、歷史紀錄、搜尋引擎。

(二)DDD調試過程

打開終端命令行窗口,輸入命令vi testddd.c,創建testddd.c文件做爲以後調試的文件:

在testddd.c文件中輸入一些C語言的程序數據,DDD工具能夠調試不少種程序設置基於的代碼,本次調試以C語言做爲說明對象。

把testddd.c文件編譯成能夠執行的文件testddd,命令:gcc -g -o testddd testddd.c,注意必定要帶-g參數,不然生成的可執行文件中沒有必要的調試信息,最終使用DDD工具不能調試。

運行DDD調試工具,直接輸入命令ddd就能夠打開DDD工具。

DDD工具打開後以下圖所示,上面較大空白部分爲代碼區,和工具區,分割線下面是調試生成信息區。

點擊菜單欄上的「文件」----->「打開程序」,準備打開咱們上面準備的testddd.c文件

在打開程序框中,定位到咱們要調試的程序的目錄下,在Files列表下選擇咱們要調試 信息,以後點擊左下方的打開按鈕。

調試程序打開後,在代碼區能夠看到咱們的代碼,右邊的一些按鈕是咱們調試要用的工具。

在代碼區點鼠標右鍵,會彈出如圖所示的菜單:

咱們能夠給程序設置斷點等,點擊工具區裏面的Run按鈕,能夠執行程序,在下面的調試信息區能夠看到程序的執行結果。

如上圖所示:在鼠標右鍵點擊的地方設置了斷點,在下方調試信息生成區顯示了程序運行的輸入信息。

PS:也能夠在Terminal中輸入ddd 文件名來直接打開ddd調試該文件的界面:

在懷疑程序哪一個變量爲可疑變量時,能夠在控制檯輸入以下命令

或者在主窗口原程序中點擊某個變量如sum選中該變量,右擊後選擇display sum 選項就會看到該變量的值在主窗口的上方。 接着往下單步運行,屢次點擊工具欄中的「Step」按鈕,觀察變量sum的結果。

若是問題出在count上。這時點擊命令工具欄上的「Kill」按鈕將程序斷掉,把初始化sum的那一句改正確。從新運行以後,發現結果正確,調試過程完畢。

(三)經常使用命令簡介

run       執行程序
step      單步調試
kill      殺死正在運行的程序
interrupt 退出這次調試回到原始狀態
  • DDD的數據顯示功能很是強大。
  • 對於固定大小的數組,用鼠標選中數組名,點擊plot按鈕便可畫出圖形。
  • 對於變長數組,可使用graph plot數組名[起始索引] @ 數組大小的命令來顯示。
  • 對於複雜的數據結構,DDD也能夠用圖形方式解析: DDD有一個detect aliases的選項,能夠智能的判別數據是否會被重複顯示。這種方式經過內存地址的檢測來實現的。

5、段錯誤

  • 定義:段錯誤是指訪問的內存超出了系統給這個程序所設定的內存空間,例如訪問了不存在的內存地址、訪問了系統保護的內存地址、訪問了只讀的內存地址等等狀況。

段錯誤產生的緣由:
(1) 訪問不存在的內存地址
(2) 訪問系統保護的內存地址
(3) 訪問只讀的內存地址
(4) 棧溢出

下面以緣由一訪問不存在的內存地址爲例,進行實踐。

(一)使用gcc和gdb(對於簡單代碼)

  • 首先,編寫一段代碼,訪問不存在內存地址。編譯後進入CGDB,運行程序:

  • 從輸出中能夠看出,程序收到SIGSEGV信號,觸發段錯誤,並提示0x00000000004004e六、調用main報的錯,在Derro.c中23行。而且在代碼窗口第23行被標記出來。

  • 適用場景

僅當能肯定程序必定會發生段錯誤的狀況下使用。
當程序的源碼能夠得到的狀況下,使用-g參數編譯程序。
通常用於測試階段,生產環境下gdb會有反作用:使程序運行減慢,運行不夠穩定,等等。
即便在測試階段,若是程序過於複雜,gdb也不能處理。

(二)使用core文件和gdb

  • 提到段錯誤會觸發SIGSEGV信號,經過man 7 signal,能夠看到SIGSEGV默認的handler會打印段錯誤出錯信息,併產生core文件,由此咱們能夠藉助於程序異常退出時生成的core文件中的調試信息,使用gdb工具來調試程序中的段錯誤。
  • 查看core文件發現不存在:

  • 查看系統core文件的大小限制,發現爲0,這樣不會自動生成core文件。把大小設置爲1000。運行程序後再次查看可看到存在core文件:

  • 加載core文件,使用gdb工具進行調試。從輸出中能夠看出一樣的段錯誤信息:

6、多進程與多線程

(一)多進程

一、進程的基本概念
  • 進程定義了一個計算的基本單元,能夠認爲是一個程序的一次運行。它是一個動態實體,是獨立的任務。它擁有獨立的地址空間、執行堆棧、文件描述符等。 每一個進程擁有獨立的地址空間,進程間正常狀況下,互不影響,一個進程的崩潰不會形成其餘進程的崩潰。 當進程間共享某一資源時,需注意兩個問題:同步問題和通訊問題。
二、建立進程
  • 父進程經過調用fork函數來建立一個新的運行子進程。fork函數定義以下:
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
  • fork函數只被調用一次,可是會返回兩次:父進程返回子進程的PID,子進程返回0.若是失敗返回-1。
  • fork後,子進程和父進程繼續執行fork()函數後的指令。子進程是父進程的副本。子進程擁有父進程的數據空間、堆棧的副本。但父、子進程並不共享這些存儲空間部分。若是代碼段是隻讀的,則父子進程共享代碼段。若是父子進程同時對同一文件描述字操做,而又沒有任何形式的同步,則會出現混亂的情況; 父進程中調用fork以前打開的全部描述字在函數fork返回以後子進程會獲得一個副本。fork後,父子進程均須要將本身不使用的描述字關閉,有兩方面的緣由:(1)以避免出現不一樣步的狀況;(2)最後能正常關閉描述字

  • 在BSD3.0中開始出現,主要爲了解決fork昂貴的開銷。它是徹底共享的建立,新老進程共享一樣的資源,徹底沒有拷貝。 二者的基本區別在於當使用vfork()建立新進程時,父進程將被暫時阻塞,而子進程則能夠借用父進程的地址空間。這個奇特狀態將持續直到子進程退出或調用execve()函數,至此父進程才繼續執行。

三、終止進程

進程的終止存在兩個可能:(1)父進程先於子進程終止(init進程領養) (2)子進程先於主進程終止。對於後者,系統內核爲子進程保留必定的狀態信息:進程ID、終止狀態、CPU時間等;當父進程調用wait或waitpid函數時,獲取這些信息; 當子進程正常或異常終止時,系統內核向其父進程發送SIGCHLD信號;缺省狀況下,父進程忽略該信號,或者提供一個該信號發生時即被調用的函數。

#include <stdlib.h>
void exit(int status);
  • 本函數終止調用進程。關閉全部子進程打開的描述符,向父進程發送SIGCHLD信號,並返回狀態。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);
  • 返回:終止子進程的ID-成功;-1-出錯;statloc存儲子進程的終止狀態(一個整數);

  • 若是沒有終止的子進程,可是有一個或多個正在執行的子進程,則該函數將堵塞,直到有一個子進程終止或者wait被信號中斷時,wait返回。 當調用該系統調用時,若是有一個子進程已經終止,則該系統調用當即返回,並釋放子進程全部資源。

pidt waitpid(pidt pid, int *statloc, int options);
  • 返回:終止子進程的ID-成功;-1-出錯;statloc存儲子進程的終止狀態;
  • 當pid=-1,option=0時,該函數等同於wait,不然由參數pid和option共同決定函數行爲,其中pid參數意義以下:
-1:要求知道任何一個子進程的返回狀態(等待第一個終止的子進程);
>0:要求知道進程號爲pid的子進程的狀態;
<-1: wait  for  any  child process whose process group ID is equal to the absolute value of pid.
  • Options最經常使用的選項是WNOHANG,它通知內核在沒有已終止進程時不要堵塞。

  • 調用wait或waitpid函數時,正常狀況下,可能會有如下幾種狀況:

阻塞(若是其全部子進程都還在運行);
得到子進程的終止狀態並當即返回(若是一個子進程已終止,正等待父進程存取其終止狀態); 
出錯當即返回(若是它沒有任何子進程)

四、調試進程
  • 通常狀況下,父進程fork一個子進程,gdb只會繼續調試父進程而不會管子進程的運行。若是想跟蹤子進程進行調試,可使用set follow-fork-mode mode來設置fork跟隨模式。
  • set follow-fork-mode 所帶的mode參數能夠是如下的一種:
parent        gdb只跟蹤父進程,不跟蹤子進程,這是默認的模式。
    child         gdb在子進程產生之後只跟蹤子進程,放棄對父進程的跟蹤。
  • 進入gdb之後,咱們可使用show follow-fork-mode來查看目前的跟蹤模式。

  • 能夠看到目前使用的模式是parent。

  • 有時,咱們想同時調試父進程和子進程,以上的方法就不能知足了。Linux提供了set detach-on-fork mode命令來供咱們使用。其使用的mode能夠是如下的一種:

on        只調試父進程或子進程的其中一個(根據follow-fork-mode來決定),這是默認的模式。
    off       父子進程都在gdb的控制之下,其中一個進程正常調試(根據follow-fork-mode來決定)
  • 另外一個進程會被設置爲暫停狀態。
  • 一樣,show detach-on-fork顯示了目前是的detach-on-fork模式,如圖所示。

  • 以上是調試fork產生子進程的狀況,可是若是子進程使用exec系統函數而裝載了新程序執行,咱們就使用set follow-exec-mode mode提供的模式來跟蹤這個exec裝載的程序。mode能夠是如下的一種:
new 當發生exec的時候,若是這個選項是new,則新建一個inferior給執行起來的子進程,而父進程的inferior仍然保留,當前保留的inferior的程序狀態是沒有執行。
 same 當發生exec的時候,若是這個選項是same(默認值),由於父進程已經退出,因此自動在執行exec的inferior上控制子進程。

(二)多線程

  • 線程:運行在單一進程上下文中的邏輯流,由內核進行調度,共享同一進程的虛擬地址空間。

    基於線程的併發編程
  • 線程由內核自動調度,每一個線程都有它本身的線程上下文(thread context),包括一個唯一的整數線程ID(Thread ID,TID),棧,棧指針,程序計數器,通用目的寄存器和條件碼。每一個線程和其餘線程一塊兒共享進程上下文的剩餘部分,包括整個用戶的虛擬地址空間,它是由只讀文本(代碼),讀/寫數據,堆以及全部的共享庫代碼和數據區域組成的,還有,線程也共享一樣的打開文件的集合。
  • 線程不像進程那樣,不是按照嚴格的父子層次來組織的。和一個進程相關的線程組成一個對等線程池,獨立於其餘線程建立的線程。進程中第一個運行的線程稱爲主線程。對等(線程)池概念的主要影響是,一個線程能夠殺死它的任何對等線程,或者等待它的任意對等線程終止;進一步來講,每一個對等線程都能讀寫相同的共享數據。
  • 線程是可執行代碼的可分派單元。這個名稱來源於「執行的線索」的概念。在基於線程的多任務的環境中,全部進程有至少一個線程,可是它們能夠具備多個任務。這意味着單個程序能夠併發執行兩個或者多個任務。
  • 簡而言之,線程就是把一個進程分爲不少片,每一片均可以是一個獨立的流程。這已經明顯不一樣於多進程了,進程是一個拷貝的流程,而線程只是把一條河流截成不少條小溪。它沒有拷貝這些額外的開銷,可是僅僅是現存的一條河流,就被多線程技術幾乎無開銷地轉成不少條小流程,它的偉大就在於它少之又少的系統開銷。

linux提供的多線程的系統調用:

(1)函數pthread_create用來建立一個線程,它的原型爲:

extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,void *(*__start_routine) (void *), void *__arg));

第一個參數爲指向線程標識符的指針,第二個參數用來設置線程屬性,第三個參數是線程運行函數的起始地址,最後一個參數是運行函數的參數。

(2)函數pthread_join用來等待一個線程的結束。函數原型爲:

2extern int pthread_join __P ((pthread_t __th, void **__thread_return));

第一個參數爲被等待的線程標識符,第二個參數爲一個用戶定義的指針,它能夠用來存儲被等待線程的返回值。這個函數是一個線程阻塞的函數,調用它的函數將一直等待到被等待的線程結束爲止,當函數返回時,被等待線程的資源被收回。

(3)一個線程的結束有兩種途徑,一種是象咱們上面的例子同樣,函數結束了,調用它的線程也就結束了;另外一種方式是經過函數pthread_exit來實現。它的函數原型爲:

extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));

惟一的參數是函數的返回代碼,只要pthread_ join中的第二個參數thread_ return不是NULL,這個值將被傳遞給 thread_return。

最後要說明的是,一個線程不能被多個線程等待,不然第一個接收到信號的線程成功返回,其他調用pthread_join的線程則返回錯誤代碼ESRCH。

  • Linux系統下的多線程遵循POSIX線程接口,稱爲pthread。編寫Linux下的多線程程序,須要使用頭文件pthread.h,鏈接時須要使用庫libpthread.a。Linux下pthread的實現是經過系統調用clone()來實現的。clone()是Linux所特有的系統調用,它的使用方式相似fork。

  • 下面代碼示例:

  • 代碼分析:主線程作本身的事情,生成2個子線程,task1爲分離,任其自生自滅,而task2仍是繼續送外賣,須要等待返回。

  • 編譯運行:

  • 屢次運行發現結果並不徹底相同,這是不一樣的線程搶佔CPU的結果。

7、心得體會

這篇GDB的深刻研究是我作的第一個加分項目,到此算是告一段落了。在作以前,一直感受GDB調試是很困難的一件事,可是本身真正去實踐才發現它並無我想象中的那麼難。此次我完成了GDB代碼調試、CGDB代碼調試、彙編代碼調試、DDD代碼調試以及多進程與多線程的學習,中途也遇到過不少問題,可是經過查閱資料,參考以前的學長學姐的經驗最終都解決了。不知不覺,寫博客已經有一年的時間了,這門課程也快結束了。從一開始的煩躁到後面的適應再到習慣,本身自主學習的能力提高了太多。之後繼續加油!

相關文章
相關標籤/搜索