GDB的深入研究


GDB的深入研究

一、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”):

  • 使用“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

二、CGDB代碼調試

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

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

(2)斷點設置可視化 。

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

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

界面及使用說明

(1)代碼窗口

調試時同步顯示被調試程序源代碼,自動標記出程序運行到的位置。當焦點在代碼窗口時,可以瀏覽代碼、查找代碼以及執行命令 ,操作方式同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):

三、匯編代碼調試

匯編級的調試或跟蹤,需要用到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調試環境

四、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的選項,可以智能的判別數據是否會被重復顯示。這種方式通過內存地址的檢測來實現的。

五、段錯誤

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

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

下面以原因一訪問不存在的內存地址為例,進行實踐。

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

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

  • 從輸出中可以看出,程序收到SIGSEGV信號,觸發段錯誤,並提示0x00000000004004e6、調用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工具進行調試。從輸出中可以看出同樣的段錯誤信息:

六、多進程與多線程

(一)多進程

1、進程的基本概念
  • 進程定義了一個計算的基本單元,可以認為是一個程序的一次運行。它是一個動態實體,是獨立的任務。它擁有獨立的地址空間、執行堆棧、文件描述符等。 每個進程擁有獨立的地址空間,進程間正常情況下,互不影響,一個進程的崩潰不會造成其他進程的崩潰。 當進程間共享某一資源時,需注意兩個問題:同步問題和通信問題。
2、創建進程
  • 父進程通過調用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()函數,至此父進程才繼續執行。

3、終止進程

進程的終止存在兩個可能:(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函數時,正常情況下,可能會有以下幾種情況:

阻塞(如果其所有子進程都還在運行);
獲得子進程的終止狀態並立即返回(如果一個子進程已終止,正等待父進程存取其終止狀態); 
出錯立即返回(如果它沒有任何子進程)

4、調試進程
  • 一般情況下,父進程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的結果。

七、心得體會

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM