深入淺出GDB調(diào)試器

轉(zhuǎn)載自:[深入淺出GDB調(diào)試器] (深入淺出GDB調(diào)試器 (qq.com))

前言

GDB全稱GNU symbolic debugger,它是誕生于GNU開(kāi)源組織的(同時(shí)誕生的還有 GCC、Emacs 等)UNIX及UNIX-like下的調(diào)試工具,是Linux下最常用的程序調(diào)試器,GDB 支持調(diào)試多種編程語(yǔ)言編寫(xiě)的程序,包括C、C++、Go、Objective-C、OpenCL、Ada 等。但是在實(shí)際應(yīng)用中,GDB 更常用來(lái)調(diào)試C和C++程序。雖然說(shuō)在Linux系統(tǒng)下我們可以借助諸多集成開(kāi)發(fā)工具來(lái)完成程序的編寫(xiě)和調(diào)試,但實(shí)際上,調(diào)試C/C++程序一定是直接或者間接使用GDB完成的。所以說(shuō)GDB調(diào)試幾乎可以說(shuō)是Linux程序員必備的基本技能。本文將手把手教你使用GDB調(diào)試程序,并帶你深入了解什么是GDB調(diào)試器。


image.png

文章目錄
前言

一、什么是GDB
(1)查看GDB版本
(2)安裝GDB調(diào)試器
(3)卸載GDB

  1. 為什么要有GDB
  2. 下載安裝GDB
    二、GDB的啟動(dòng)與調(diào)試程序的上下文設(shè)置
    (1)gdb工作目錄
    (2)程序運(yùn)行參數(shù)
    (3)查看及修改運(yùn)行環(huán)境
    (4)輸入輸出重定向
    (1)測(cè)試程序中的main函數(shù)參數(shù)解析argc與argv[]
    (2)gcc編譯時(shí) ==-g== 選項(xiàng)幫我們做了什么?
    (3)啟動(dòng)GDB與指定目標(biāo)調(diào)試程序的方式
  3. 準(zhǔn)備知識(shí)
  4. 程序上下文
    三、GDB實(shí)戰(zhàn)講解
    (1)創(chuàng)建一個(gè)多線程測(cè)試文件
    (2)undefined reference to `pthread_create' 錯(cuò)誤
    (3)多線程調(diào)試
    (1)什么是 core dump 核心轉(zhuǎn)儲(chǔ)
    (2)產(chǎn)生 core dump 的原因
    (3)core 文件的相關(guān)配置與 shell 資源限制
    (4)通過(guò)core文件調(diào)試當(dāng)?shù)舻某绦?br> (1)調(diào)試非運(yùn)行狀態(tài)的可執(zhí)行程序
    (2)調(diào)試一個(gè)正在運(yùn)行的程序
    (1)r(run)運(yùn)行與start運(yùn)行程序
    (2)q(quit)退出調(diào)試
    (3)help
    (4)l(lsit)查看代碼
    (5)set 傳入?yún)?shù)
    (6)n(next)執(zhí)行下一條語(yǔ)句,不進(jìn)入函數(shù)內(nèi)部
    (7)s(step)執(zhí)行下一條語(yǔ)句,且進(jìn)入函數(shù)內(nèi)部
    (8)u(until)
    (9)b(break)設(shè)置斷點(diǎn)以及打斷點(diǎn)的六種方式
    (10)tbreak
    (11)rbreak
    (12)disable 與 enable
    (13)watch
    (14)rwatch
    (15)awatch
    (16)catch
    (17)c(continue)執(zhí)行到下一個(gè)斷點(diǎn)處
    (18)info 查看
    (19)del(delete)刪除
    (20)clear
    (21)ignore
    (22)p (print)
    (23)ptype 查看類型
    (24)display 跟蹤變化
    (25)undisplay 取消跟蹤
    (26)bt (backtrace)查看棧信息
    (27)x 查看內(nèi)存
    (28)disas 反匯編
    (29)finish
    (30)return
    (31)call
    (32)edit
    (33)search
  5. GDB命令詳解
  6. GDB跟蹤可以正常編譯運(yùn)行的源文件
  7. GDB跟蹤core(調(diào)試掛掉的程序)
  8. GDB調(diào)試多線程
    總結(jié)

一、什么是GDB

  1. 為什么要有GDB
    我們?cè)陂_(kāi)發(fā)程序的過(guò)程中,應(yīng)該很少會(huì)有一次就編譯通過(guò)的吧,有時(shí)候即便是寫(xiě)了短短幾十行的代碼,都難免會(huì)有一些小的疏忽,更何況是幾千上萬(wàn)甚至更大的代碼,反正我在開(kāi)發(fā)中幾乎每次寫(xiě)完程序都會(huì)經(jīng)過(guò)反復(fù)的調(diào)試,鍵盤(pán)的F11鍵經(jīng)常會(huì)壞掉。在程序中,出現(xiàn)的錯(cuò)誤主要分為 2大 類,即語(yǔ)法錯(cuò)誤和邏輯錯(cuò)誤:
    語(yǔ)法錯(cuò)誤,顧名思義就是不符合編程語(yǔ)言語(yǔ)法的錯(cuò)誤,這類錯(cuò)誤一般都可以由編譯器診斷出來(lái),GCC編譯器的編譯階段會(huì)進(jìn)行語(yǔ)法檢查(這方面內(nèi)容我在GCC編譯器那篇文章中已經(jīng)詳細(xì)介紹過(guò)了);
    邏輯錯(cuò)誤,這部分錯(cuò)誤是指我們?cè)诔绦蛟O(shè)計(jì)的邏輯上的錯(cuò)誤,程序編譯通過(guò),但是執(zhí)行結(jié)果并不符合我們的預(yù)期,這類錯(cuò)誤就沒(méi)有辦法依靠GCC編譯器去檢查了,需要我們自己調(diào)試分析;
    程序出現(xiàn)語(yǔ)法錯(cuò)誤,可以依靠GCC檢查出來(lái),而邏輯錯(cuò)誤就要我們今天的主角GDB登場(chǎng)解決了。所謂調(diào)試(Debug),就是單步執(zhí)行代碼,或通過(guò)斷點(diǎn)讓程序執(zhí)行到某個(gè)位置,以此來(lái)逐步鎖定程序出現(xiàn)問(wèn)題的范圍。在單步調(diào)試的過(guò)程中,我們可以監(jiān)控程序執(zhí)行的每一個(gè)行為,包括變量值的變化、函數(shù)的調(diào)用、內(nèi)存中數(shù)據(jù)的變化、線程的調(diào)度等等,以此來(lái)修復(fù)BUG或者優(yōu)化代碼。
    我們?cè)赪indows下開(kāi)發(fā)最常用的Visual Studio,它自帶的調(diào)試器是Remote Debugger,調(diào)試器與整個(gè)IDE無(wú)縫銜接,使用非常方便。在Linux下C/C++必備的調(diào)試器就是GDB了,下面講解如何查看GDB版本及安裝GDB。
  2. 下載安裝GDB
    (1)查看GDB版本
gdb -v
gdb --version

如果你的執(zhí)行結(jié)果如下,說(shuō)明已經(jīng)安裝好了gdb,版本號(hào)如下,一般我們裝好Linux后可以通過(guò)這個(gè)命令來(lái)測(cè)試是否已經(jīng)安裝gdb調(diào)試器。


image.png

如果你的運(yùn)行結(jié)果顯示 not found ,說(shuō)明未安裝gdb調(diào)試器,安裝gdb的方法主要有兩個(gè),下面一節(jié)介紹安裝方法。

bash: gdb: command not found

(2)安裝GDB調(diào)試器
安裝gdb主要有兩種方法:
① 直接安裝。通常我們安裝好Linux之后,操作系統(tǒng)內(nèi)會(huì)附帶有g(shù)db的安裝包,我們可以直接使用操作系統(tǒng)內(nèi)已有的gdb安裝包,使用包管理器進(jìn)行安裝。這種方法簡(jiǎn)單有效,只需要一條命令就可以安裝成功(以CentOS為例)

yum -y install gdb

安裝好后,可以通過(guò) gdb -v 查看版本,一般來(lái)說(shuō)通過(guò)這種方式安裝的gdb都不是最新版本,并且無(wú)法自己選擇版本。
② 通過(guò)源碼安裝。源碼安裝是指首先去網(wǎng)上下載源碼壓縮包,然后在本地解壓安裝,我們可以選擇自己需要的版本進(jìn)行安裝,可以直接點(diǎn)擊源碼包的鏈接gdb源碼去下載。


image.png

里面有很多版本和格式,我們可以選擇一個(gè)自己需要的版本 .tar.gz 格式下載,下載后進(jìn)行下面的操作.
解壓文件
找到下載好的壓縮包并解壓

tar -zxvf gdb-11.2.tar.gz

如果你是在Windows下下載好的壓縮包,要傳到Linux下,可以借助SecureCRT的rz命令.


image.png

后面就只說(shuō)命令了。

解壓后進(jìn)入解壓出來(lái)的目錄

運(yùn)行 configure 文件配置環(huán)境,這時(shí)候會(huì)創(chuàng)建一個(gè)Makefile文件

make 編譯源碼文件

安裝 make install

gdb -v 查看

tar -zxvf gdb-11.2.tar.gz
cd gdb-11.2
./configure
make
make install
gdb -v

(3)卸載GDB
gdb調(diào)試器的卸載命令

yum remove gdb

二、GDB的啟動(dòng)與調(diào)試程序的上下文設(shè)置

  1. 準(zhǔn)備知識(shí)
    (1)測(cè)試程序中的main函數(shù)參數(shù)解析argc與argv[]
    首先我們創(chuàng)建一個(gè)C文件gdb_test.c,以用于后面舉例使用,程序如下
#include <stdio.h> 
#include <stdlib.h> 
  
struct st 
{ 
    int a; 
    int b; 
}; 
 
void print_array(char* array, int len) 
{ 
    int i = 0; 
    for(i = 0; i < len; i++) 
    { 
        printf("array[%d]: %c\n", i, array[i]); 
    } 
} 
 
int main(int argc, char* argv[])
{
    struct st st_temp;
    int i = 0;
    char array[5];

    st_temp.a = 10;
    st_temp.b = 11;

    for(i = 0; i < 5; i++)
    {
        array[i] = i + '0';
    }   
    print_array(array, 5);

    for(i = 0; i < argc; i++)
    {
        printf("hello...argv[%d]: %s\n", i, argv[i]);   
    }
    
    return 0;
}

在這個(gè)測(cè)試程序中,main函數(shù)貌似有點(diǎn)不同尋常啊

int main(int argc, char* argv[])

多了兩個(gè)東西,argc和argv,其實(shí)在main函數(shù)中本就應(yīng)該有這兩個(gè)參數(shù),只不過(guò)在我們平常的大部分學(xué)習(xí)中,都弱化了這兩個(gè)參數(shù)的作用,估計(jì)大部分人在學(xué)習(xí)編程時(shí)都從來(lái)沒(méi)有寫(xiě)過(guò)這兩個(gè)參數(shù)。第一個(gè)參數(shù)argc用來(lái)統(tǒng)計(jì)程序運(yùn)行時(shí)傳遞給main函數(shù)的命令行參數(shù)的個(gè)數(shù),這個(gè)不需要我們?cè)O(shè)置;argv是一個(gè)字符串?dāng)?shù)組,用來(lái)存放我們傳入的參數(shù),其中argv[0]默認(rèn)就是程序運(yùn)行的路徑名。說(shuō)起來(lái)不好理解,我們舉個(gè)例子,就用上面給出的gdb_test.c文件,我們編譯好運(yùn)行一下,并傳遞參數(shù)

gcc gdb_test.c -o g3
./g3 111111
image.png

首先可以看到argc的值是2,argv的第一個(gè)參數(shù)是 ./g3 表示當(dāng)前目錄,第二個(gè)參數(shù)是我們傳入的111111。如果我們不傳任何參數(shù),argc就是1,argv只有一個(gè)字符串就是當(dāng)前路徑。


image.png

(2)gcc編譯時(shí) -g 選項(xiàng)幫我們做了什么?
gdb主要的作用是跟蹤程序的執(zhí)行過(guò)程,所以要想用gdb調(diào)試程序,首先要把源程序編譯為可執(zhí)行文件。但是,我們正常使用gcc命令編譯出來(lái)的可執(zhí)行文件是無(wú)法通過(guò)gdb調(diào)試的,因?yàn)檫@樣編譯出來(lái)的可執(zhí)行文件缺少gdb調(diào)試所需要的調(diào)試信息(比如每一行代碼的行號(hào)、包含程序中所有符號(hào)的符號(hào)表等信息)。要想生成帶有g(shù)db調(diào)試信息的可執(zhí)行文件,就要在gcc編譯的時(shí)候添加==-g== 選項(xiàng)。
你可能通過(guò)嘗試后會(huì)說(shuō),不加gcc的 -g 選項(xiàng)也能進(jìn)入gdb調(diào)試,確實(shí)是這樣,但是進(jìn)入gdb并不代表就可以調(diào)試,比如下面


image.png

我們不加 -g 編譯一個(gè)源文件,并啟動(dòng)gdb
image.png

進(jìn)入gdb后我們發(fā)現(xiàn),使用 r 命令執(zhí)行可以,但是通過(guò) list 查看源代碼卻不行。這是因?yàn)?,我們不?-g 編譯出來(lái)的可執(zhí)行文件不包含行號(hào)和符號(hào)表等調(diào)試所需要的信息,所以你想查看源碼、添加斷點(diǎn)都是無(wú)法實(shí)現(xiàn)的。而這就是 -g 選項(xiàng)的作用,我們可以對(duì)比一下加與不加 -g 選項(xiàng)生成的可執(zhí)行文件大小

能夠看得出,加了 -g 選項(xiàng)后編譯出來(lái)的可執(zhí)行文件占據(jù)了更多個(gè)空間,這是因?yàn)?,它包含了調(diào)試信息。
有時(shí)候,我們?cè)诰幾g時(shí)會(huì)組合 -g 和 -O 來(lái)使用,通常用 -Og 來(lái)實(shí)現(xiàn)在保證快速編譯和更好的調(diào)試前提下,進(jìn)行一定的優(yōu)化。
(3)啟動(dòng)GDB與指定目標(biāo)調(diào)試程序的方式
啟動(dòng)gdb調(diào)試器分為四種情況:
① 調(diào)試非運(yùn)行狀態(tài)且編譯通過(guò)可運(yùn)行的可執(zhí)行文件

gdb exe(可執(zhí)行文件名)
gdb ./exe(可執(zhí)行文件名)

② 調(diào)試正在運(yùn)行的可執(zhí)行文件

gdb -p pid(進(jìn)程號(hào))

③ 調(diào)試core

gdb exe(可執(zhí)行文件名) core.19761(core文件名)
gdb ./exe(可執(zhí)行文件名) core.19761(core文件名)

上面這三種情況會(huì)在后面對(duì)應(yīng)的章節(jié)詳細(xì)介紹。
④ 假如直接使用 gdb 命令進(jìn)入gdb調(diào)試器,gdb自己是無(wú)法確定要調(diào)試哪個(gè)可執(zhí)行文件的,即使當(dāng)前目錄只有一個(gè)可執(zhí)行文件也無(wú)法自動(dòng)識(shí)別,這時(shí)我們可以手動(dòng)指定目標(biāo)調(diào)試文件。


image.png

提示信息中已經(jīng)告訴我們使用哪個(gè)命令來(lái)指定待調(diào)試程序了,那就是 file 命令,使用方法是 file 直接加可執(zhí)行文件所在目錄以及可執(zhí)行文件名,如果可執(zhí)行文件就在gdb當(dāng)前工作目錄下,可以不加目錄,這樣我們就可以使用gdb調(diào)試 file 命令指定的可執(zhí)行文件了


image.png

不管哪種情況,我們進(jìn)入gdb時(shí),總會(huì)打印一堆聲明
image.png

要想去掉這些聲明,可以在gdb后面加 –silent 或 -q 或 –quiet 選項(xiàng)。
image.png

只要最下面有一個(gè) (gdb) 就說(shuō)明進(jìn)入成功。

  1. 程序上下文
    (1)gdb工作目錄
    默認(rèn)情況下,GDB調(diào)試器會(huì)把啟動(dòng)時(shí)所在的目錄作為工作目錄,但有時(shí)候我們可能需要根據(jù)情況去改變gdb的工作目錄,查看gdb當(dāng)前工作目錄和改變工作目錄的命令和 shell 下一樣。
    ① 查看當(dāng)前gdb工作目錄
    pwd 命令可以查看當(dāng)前gdb工作目錄


    image.png

    ② 改變gdb工作目錄
    使用shell下的 cd 命令,可以改變gdb工作目錄,用法與shell下一樣


    image.png

    另外提示一下,gdb調(diào)試時(shí),也可以使用 tab 鍵命令補(bǔ)全、上下鍵查看歷史命令等。
    (2)程序運(yùn)行參數(shù)
    傳遞運(yùn)行參數(shù)的方式有三種:
    ① 啟動(dòng)gdb時(shí)指定(exe表示可執(zhí)行文件名,paras表示參數(shù))
gdb --args exe paras

我們用前面的gdb_test.c編譯為g3,并傳入?yún)?shù)111111111


image.png

② set命令
gdb調(diào)試器啟動(dòng)后,在運(yùn)行過(guò)程中,可以借助 set 命令指定目標(biāo)調(diào)試程序啟動(dòng)所需要的運(yùn)行參數(shù)

set args paras

我們?cè)诤瘮?shù)print_array()處設(shè)置一個(gè)斷點(diǎn),并執(zhí)行到斷點(diǎn)處,然后把函數(shù)參數(shù)len設(shè)置為2,也就是只打印兩個(gè)數(shù)據(jù)(array總共5個(gè)數(shù)據(jù),可以看前面的圖中打印結(jié)果)


image.png

可以看到 set 在運(yùn)行的過(guò)程中改變了參數(shù)len的值。
③ 運(yùn)行時(shí)指定
gdb調(diào)試器啟動(dòng)后,在運(yùn)行時(shí)可以通過(guò)run 和 start 來(lái)指定參數(shù)

run paras
start paras
image.png

(3)查看及修改運(yùn)行環(huán)境
① 查看程序的運(yùn)行路徑

show paths

② 設(shè)置程序的運(yùn)行路徑

path /xxx/xxx/

③ 查看環(huán)境變量

show environment

④ 設(shè)置環(huán)境變量

set environment PARA=para

(4)輸入輸出重定向
① 輸入輸出重定向
默認(rèn)情況下,程序中的輸出都是打印在終端上的,通過(guò)重定向可以把結(jié)果打印到指定位置。比如,我們可以把程序中的打印結(jié)果都打印到某個(gè)文件中


image.png

可以看到,運(yùn)行程序后,屏幕上沒(méi)有任何輸出,我們退出gdb查看1.txt文件


image.png

程序運(yùn)行結(jié)果都被打印到了該文件中。
② 選擇終端
使用終端tty1,命令如下
tty /dev/tty1

三、GDB實(shí)戰(zhàn)講解

  1. GDB命令詳解
    在下面所有的命令標(biāo)題中,括號(hào)內(nèi)為命令全寫(xiě),括號(hào)外為命令縮寫(xiě),使用效果一樣,例如運(yùn)行命令 r(run),下面兩種用法效果一致
(gdb)r
(gdb)run

下面的例子都是用前面編譯好的文件 gdb_test.c 及可執(zhí)行文件 g3。
(1)r(run)運(yùn)行與start運(yùn)行程序
run 運(yùn)行程序,如果有斷點(diǎn)則停在斷點(diǎn)處,如果沒(méi)有斷點(diǎn)會(huì)一直執(zhí)行到程序結(jié)束。start 會(huì)執(zhí)行到main函數(shù)的起始位置,相當(dāng)于在main()加一個(gè)斷點(diǎn),然后使用 run 執(zhí)行。如果在程序調(diào)試或者執(zhí)行中使用 run 或 start 都代表從頭開(kāi)始重新執(zhí)行程序。
在 r 或 start 命令后面加參數(shù)可以把參數(shù)傳入并執(zhí)行(前面已經(jīng)介紹過(guò)了)

(gdb)r para

傳入?yún)?shù)para并執(zhí)行。


image.png

start 會(huì)執(zhí)行到 mian 處。

(2)q(quit)退出調(diào)試
退出 gdb 調(diào)試,回到 shell。

(3)help
查看幫助手冊(cè),按 q 退出幫助手冊(cè)。


image.png

(4)l(lsit)查看代碼
① 一次顯示10行


image.png

image.png

② 指定一個(gè)行號(hào)n,查看 n-5 到 n+4 行(共10行)


image.png

③ 查看第 n1 到 n2 行代碼 list n1,n2
image.png

④ 查看其他文件代碼,用于包含多個(gè)源文件的情況,比如可執(zhí)行文件 test 由 test1.c 和 test2.c 編譯而成,可以通過(guò)指定文件名來(lái)查看 test1.c 或 test2.c 的源代碼。
查看 test1.c 的代碼1到10行
(gdb)list test1.c:1,10

(5)set 傳入?yún)?shù)
① set 可以傳入?yún)?shù)或者修改變量的值


image.png

② 變量名與gdb命令名沖突
比如你在源代碼中有一個(gè)變量名叫 width ,如果你要用 set 設(shè)置這個(gè)變量的值會(huì)產(chǎn)生沖突,因?yàn)?set width 是gdb的命令,這時(shí)可以通過(guò) set var 告訴gdb該變量是用戶變量。建議自己寫(xiě)代碼時(shí)要避免和系統(tǒng)函數(shù)、編譯調(diào)試等命令重名的函數(shù)或變量,以避免不必要的麻煩。

(gdb)set var width=10

③ 設(shè)置命令
比如說(shuō)我們?cè)诖蛴〗Y(jié)構(gòu)體的時(shí)候,使用 p 命令默認(rèn)就是普通的打印,可能不是很美觀,我們可以通過(guò)命令使打印出來(lái)的結(jié)構(gòu)體更符合我們觀看的習(xí)慣

(gdb)set print pretty
image.png

(6)n(next)執(zhí)行下一條語(yǔ)句,不進(jìn)入函數(shù)內(nèi)部
單步執(zhí)行代碼,一條語(yǔ)句一條語(yǔ)句的執(zhí)行,如果遇到函數(shù)不會(huì)進(jìn)入函數(shù)內(nèi)部,可以理解為VS的 F10 調(diào)試鍵。也可以在后面加數(shù)字表示執(zhí)行多少行

(gdb)n num
image.png

(7)s(step)執(zhí)行下一條語(yǔ)句,且進(jìn)入函數(shù)內(nèi)部
用法基本與 next 相同,區(qū)別在于 step 在遇到函數(shù)的時(shí)候會(huì)進(jìn)入函數(shù)內(nèi)部(像 printf 等這種庫(kù)函數(shù)不會(huì)進(jìn)入),可以理解為VS的 F11 調(diào)試鍵。


image.png

可以看到,當(dāng)執(zhí)行到我們自己的函數(shù) print_array() 的時(shí)候,按 step 會(huì)進(jìn)入這個(gè)函數(shù)的內(nèi)部,停在這個(gè)函數(shù)內(nèi)部語(yǔ)句的第一行。同樣,step 也可以在后面加數(shù)字表示一次執(zhí)行多少行。
(8)u(until)
① 跳出循環(huán)體
在遇到循環(huán)體時(shí),如果在循環(huán)體尾部(最后一行代碼)按 until 調(diào)試鍵,會(huì)直接執(zhí)行完整個(gè)循環(huán)體,并停在循環(huán)體外。


image.png

② 跳轉(zhuǎn)至某一行
(gdb)until num

直接跳至第 num 行執(zhí)行并停在這一行。
③ 在其他時(shí)候,功能和 next 一樣,都是單步執(zhí)行
(9)b(break)設(shè)置斷點(diǎn)以及打斷點(diǎn)的六種方式
斷點(diǎn)(BreakPoint),可以讓程序執(zhí)行到斷點(diǎn)處并停在這里,加斷點(diǎn)應(yīng)該是調(diào)試的時(shí)候最常用的一種方法,就像VS中的 F9 鍵。加斷點(diǎn)的方式有很多種,下面將逐一介紹:
① b function (直接加函數(shù)名)在某個(gè)函數(shù) function 處添加斷點(diǎn)
在函數(shù) print_array() 處加斷點(diǎn)并執(zhí)行,會(huì)停在該函數(shù)內(nèi)部的第一行


image.png

② b num (直接加行號(hào))在第num行添加斷點(diǎn)


image.png

這里有一點(diǎn)要注意,因?yàn)槌绦蛞呀?jīng)啟動(dòng)了,如果我們要想執(zhí)行到斷點(diǎn)處,應(yīng)該使用命令 c ,如果使用 run 或 start 會(huì)重新運(yùn)行程序。
③ b file.c:num 在 file.c 文件的第 num 行加斷點(diǎn),如果不加文件名 file.c 則默認(rèn)是在含main函數(shù)的那個(gè)文件第 num 行加斷點(diǎn)。
④ b file.c:function 在 file.c 文件中名為 function 的函數(shù)處加斷點(diǎn)。
⑤ b ±num 通過(guò)偏移地址設(shè)置斷點(diǎn),+ 表示從當(dāng)前程序運(yùn)行行開(kāi)始,往下數(shù) num 行并設(shè)置斷點(diǎn);- 表示當(dāng)前程序運(yùn)行行開(kāi)始,往上數(shù) num 行并設(shè)置斷點(diǎn)。
舉例,當(dāng)前程序在第34行,通過(guò) b +12可以把程序打在 34+12=16 行處。
image.png

⑥ b (上面五種方式指定斷點(diǎn)位置) if expression 當(dāng)滿足表達(dá)式 expression 的時(shí)候打斷點(diǎn),也就是說(shuō)只有當(dāng)這個(gè)表達(dá)式為真的時(shí)候,這個(gè)斷點(diǎn)才會(huì)生效。
使用舉例:

(gdb)b 12 if i==2   當(dāng)i==2的時(shí)候在第12行加斷點(diǎn)
(gdb)b func if i>3  當(dāng)i>3的時(shí)候在函數(shù)func處加斷點(diǎn)

(10)tbreak
命令的格式與用法與 break 相同,但是設(shè)置的斷點(diǎn)只生效一次,該斷點(diǎn)使用一次后自動(dòng)去除。

(11)rbreak
該命令用于給函數(shù)加斷點(diǎn), rbreak regex 給所有滿足表達(dá)式 REGEX 的函數(shù)加斷點(diǎn),設(shè)置的斷點(diǎn)和 break 設(shè)置的斷點(diǎn)一樣。這個(gè)命令在C++調(diào)試的時(shí)候,用于給所有重載函數(shù)加斷點(diǎn)非常方便。也可以加文件名來(lái)限制為哪個(gè)文件中的所有滿足表達(dá)式的函數(shù)加斷點(diǎn) rbreak file.c:regex 。
(12)disable 與 enable
用于禁用和激活斷點(diǎn)(普通斷點(diǎn)、捕捉點(diǎn)、觀察點(diǎn)、display的變量),通過(guò)斷點(diǎn)號(hào)來(lái)指定要禁用或激活的斷點(diǎn)(通過(guò) info 查看斷點(diǎn)號(hào)),可以通過(guò) help 手冊(cè)查看用法,被 disable 禁用的斷點(diǎn)將會(huì)暫時(shí)失效,使用 enable 激活后會(huì)再度恢復(fù)正常使用。


image.png

enable 可以激活多個(gè)斷點(diǎn),并且可以指點(diǎn)被激活的斷點(diǎn)起作用的次數(shù)。


image.png

舉個(gè)小例子
image.png

可以看到,Enb 那一欄從 yes 變成了 no。

(13)watch
設(shè)置觀察點(diǎn),如果在執(zhí)行過(guò)程中變量發(fā)生變化,就把他打印出來(lái),并停止運(yùn)行。


image.png

這里要注意,如果你用指針(或地址)來(lái)設(shè)置觀察點(diǎn),一定要解引用,* 指針才是對(duì)指針?biāo)赶虻淖兞窟M(jìn)行觀察如果不解引用,那就是對(duì)指針變量本身(地址)進(jìn)行觀察。另外,如果你觀察一個(gè)臨時(shí)變量或表達(dá)式,當(dāng)它的生命周期結(jié)束的時(shí)候,對(duì)應(yīng)的觀察點(diǎn)也就失效了。
觀察點(diǎn)有軟件觀察點(diǎn)和硬件觀察點(diǎn),這里不再詳細(xì)介紹。
(14)rwatch
只要程序中出現(xiàn)讀取目標(biāo)變量或表達(dá)式的值的操作,程序就會(huì)停止運(yùn)行。(讀)
(15)awatch
只要程序中出現(xiàn)讀取目標(biāo)變量或表達(dá)式的值或者改變值的操作,程序就會(huì)停止運(yùn)行。(讀寫(xiě))
(16)catch

(gdb)catch event       監(jiān)控某一事件 event 的發(fā)生,當(dāng)事件發(fā)生時(shí),程序停止

這個(gè) event 可以是下面的情況:
① C++中 throw 拋出的異?;?catch 捕捉到的異常;
② load 命令或 unload 命令,在動(dòng)態(tài)庫(kù)加載或卸載時(shí)程序停止執(zhí)行;
③ fork、vfork、exec 系統(tǒng)調(diào)用時(shí),程序停止運(yùn)行;
舉個(gè)例子測(cè)試一下,先準(zhǔn)備一個(gè)C++源文件,并編譯生成帶調(diào)試信息的可執(zhí)行文件 test。


image.png

進(jìn)入調(diào)試,設(shè)置捕捉點(diǎn),捕捉 string 類型的異常


image.png

(17)c(continue)執(zhí)行到下一個(gè)斷點(diǎn)處
繼續(xù)執(zhí)行程序,一直執(zhí)行到下一個(gè)斷點(diǎn)處。
(18)info 查看
① info breakpoints 查看所有斷點(diǎn)的信息
image.png

Num:斷點(diǎn)編號(hào)

Type:斷點(diǎn)類型
Enb:激活狀態(tài),y表示已激活,n表示未激活
② info breakpoints num 查詢 num 號(hào)斷點(diǎn)的信息


image.png

③ info variables 查詢當(dāng)前全局變量信息
image.png

④ info watchpoints 查看觀察點(diǎn)信息
⑤ 查看寄存器
image.png

⑥ 查看當(dāng)前函數(shù)內(nèi)部臨時(shí)變量的值
image.png

⑦ 查看當(dāng)前函數(shù)參數(shù)的值
image.png

⑧ 更多用法,請(qǐng)查看幫助手冊(cè)
image.png

(19)del(delete)刪除
如果我們使用 quit 退出調(diào)試,然后再次啟動(dòng) gdb 的話,之前設(shè)置的所有類型的斷點(diǎn)(包括觀察點(diǎn)、捕捉點(diǎn))都會(huì)消失。通過(guò) delete 可以在當(dāng)前調(diào)試中刪除斷點(diǎn)。在使用 delete 刪除斷點(diǎn)的時(shí)候,要先用 info 命令查看斷點(diǎn)信息,在顯示信息的第一列會(huì)有斷點(diǎn)的編號(hào),然后再根據(jù)編號(hào)刪除斷點(diǎn)即可。(刪除觀察點(diǎn)、捕捉點(diǎn)方法與刪除斷點(diǎn)一致)
image.png

如果直接使用 delete 命令,不加斷點(diǎn)號(hào)的話,會(huì)刪除當(dāng)前所有斷點(diǎn)。

(20)clear
刪除斷點(diǎn),后面加行號(hào)或函數(shù)名,(delete是按照斷點(diǎn)號(hào)刪除)

(gdb)clear func     刪除函數(shù) func 處的斷點(diǎn)
(gdb)clear num  刪除第 num 行的斷點(diǎn)

(21)ignore
忽視斷點(diǎn)

(gdb)ignore num count   忽視編號(hào)為 num 的斷點(diǎn) count 次

(22)p (print)
① 打印變量的值

(gdb)p val  打印變量 val 的值
(gdb)p &val     打印變量 val 的地址
image.png

array 類型為 char ,地址每次+1增長(zhǎng)1個(gè)字節(jié)。
② 指定打印變量值的進(jìn)制,比如 /x 表示按16進(jìn)制打印


image.png

進(jìn)制表如下:
命令 進(jìn)制
/t 二進(jìn)制
/d 十進(jìn)制有符號(hào)
/u 十進(jìn)制無(wú)符號(hào)數(shù)
/x 十六進(jìn)制
/o 八進(jìn)制
/f 浮點(diǎn)型
/c 字符型
其實(shí)和我們?cè)贑語(yǔ)言中的語(yǔ)法是一樣的。
③ 打印表達(dá)式結(jié)果


image.png

④ 修改變量的值
image.png

(23)ptype 查看類型
查看一個(gè)變量的數(shù)據(jù)類型
image.png

(24)display 跟蹤變化
查看某個(gè)變量或表達(dá)式的值,和 p 命令類似,但是 display 會(huì)一直跟蹤這個(gè)變量或表達(dá)式值得變化,每執(zhí)行一條語(yǔ)句都會(huì)打印一次變量或表達(dá)式的值。


image.png

display 也可以按格式打印,語(yǔ)法和 print 一樣,請(qǐng)參照上表(print)。
display 跟蹤得變量或表達(dá)式也會(huì)放入一張表中,使用 info 命令可以查看信息
image.png

同樣,Num表示編號(hào),Enb表示是否激活,Expression表示被跟蹤的表達(dá)式。
(25)undisplay 取消跟蹤
后面加 Num 編號(hào),刪除取消跟蹤。其實(shí)也可以使用 del 刪除。
image.png

(26)bt (backtrace)查看棧信息
在一個(gè)程序的執(zhí)行過(guò)程中,如果遇到函數(shù)調(diào)用,會(huì)產(chǎn)生一系列一些與函數(shù)上下文相關(guān)的信息:比如函數(shù)調(diào)用的位置、函數(shù)參數(shù)、函數(shù)內(nèi)部的臨時(shí)變量等。這些信息會(huì)被存放在一塊稱為棧幀的內(nèi)存空間中,并且每一個(gè)函數(shù)調(diào)用都對(duì)應(yīng)一個(gè)棧幀(main 函數(shù)也有自己的棧幀,稱為初始幀)。這些所有的棧幀都存放在內(nèi)存中的棧區(qū)。通過(guò)命令 info frame 可以查看當(dāng)前使用的棧幀所存儲(chǔ)的信息,這里面包含了棧幀編號(hào)、棧幀地址、調(diào)用者、源碼編程語(yǔ)言等信息。通過(guò)命令 frame num 、up 、down 可以選的改變棧幀。
image.png

查看當(dāng)前所有棧幀 bt
image.png

(27)x 查看內(nèi)存
image.png

同樣可以指定按什么格式查看。
(28)disas 反匯編
查看函數(shù) print_array() 的反匯編代碼,使用命令 q 退出。
image.png

(29)finish
跳出當(dāng)前所在的函數(shù)。
(30)return
忽略后面的語(yǔ)句,立即返回,可以指定返回值 return -1 。

(31)call
調(diào)用某個(gè)函數(shù),call func() 調(diào)用 func() 函數(shù)。
(32)edit
進(jìn)入編輯模式


image.png

(33)search
search 搜索,reverse-search 反向搜索。
image.png
  1. GDB跟蹤可以正常編譯運(yùn)行的源文件
    (1)調(diào)試非運(yùn)行狀態(tài)的可執(zhí)行程序
    這個(gè)很簡(jiǎn)單,我們前面介紹命令時(shí),所舉的例子,都是在這種情況下進(jìn)行的。也就是對(duì)編譯好的可執(zhí)行文件進(jìn)行調(diào)試。


    image.png

    進(jìn)入gdb調(diào)試,然后用上面介紹的命令進(jìn)行調(diào)試即可。
    (2)調(diào)試一個(gè)正在運(yùn)行的程序
    有時(shí)候我們運(yùn)行一個(gè)一直執(zhí)行的程序時(shí),希望能夠調(diào)試這個(gè)程序。比如某個(gè)帶有無(wú)限循環(huán)打印某些信息的程序。


    image.png

    我們可以這么做,首先編譯生成可執(zhí)行文件,然后在運(yùn)行時(shí)加 & 讓進(jìn)程轉(zhuǎn)為后臺(tái)執(zhí)行,或者通過(guò) SecureCRT 克隆會(huì)話來(lái)新打開(kāi)一個(gè)會(huì)話進(jìn)行調(diào)試。
    ① 首先通過(guò) ps 命令查看進(jìn)程號(hào),找到 loop 進(jìn)程的進(jìn)程信息
    image.png

    ② 通過(guò)gdb的 -p 參數(shù),指定進(jìn)程進(jìn)入調(diào)試
    image.png

    ③ 正在運(yùn)行的程序會(huì)暫停,可以正常調(diào)試了

  2. GDB跟蹤core(調(diào)試掛掉的程序)
    (1)什么是 core dump 核心轉(zhuǎn)儲(chǔ)
    core是指core memory,dump即堆放。core dump就是核心轉(zhuǎn)儲(chǔ)的意思。在Unix系統(tǒng)中,經(jīng)常會(huì)將主內(nèi)存 main memory 稱為核心 core,而核心映像 core image 是指進(jìn)程執(zhí)行時(shí)的內(nèi)存狀態(tài)。當(dāng)程序發(fā)生錯(cuò)誤或者異?;蛘呤盏侥承┬盘?hào)而終止執(zhí)行的時(shí)候,操作系統(tǒng)會(huì)把核心映像寫(xiě)入一個(gè)文件(core 文件)來(lái)作為調(diào)試依據(jù),這就是核心轉(zhuǎn)儲(chǔ) core dump。
    換句話說(shuō),當(dāng)我們寫(xiě)的程序在運(yùn)行時(shí)發(fā)生異常而退出的時(shí)候,由操作系統(tǒng)把程序當(dāng)前的內(nèi)存狀況存儲(chǔ)在一個(gè)core文件中,這就叫core dump。也就是說(shuō),所謂core dump核心轉(zhuǎn)儲(chǔ),就是當(dāng)我們寫(xiě)的程序當(dāng)?shù)簦ó惓M顺觯r(shí),把程序當(dāng)前的內(nèi)存狀況存儲(chǔ)起來(lái),以作為調(diào)試的參考的這么一種技術(shù)。
    (2)產(chǎn)生 core dump 的原因
    主要原因可以分為三大類:
    ① 訪問(wèn)越界
    包括數(shù)組下標(biāo)越界,C語(yǔ)言字符串無(wú)結(jié)束符引起的越界,使用非法指針(空指針NULL、野指針、未初始化的指針、越界指針)等。
    ② 多線程
    多線程訪問(wèn)全局變量未加同步機(jī)制(鎖機(jī)制等),或使用了線程不安全的函數(shù)。
    ③ 堆棧溢出
    使用了太大的局部變量或無(wú)限嵌套、遞歸調(diào)用函數(shù),可能會(huì)造成棧溢出。
    (3)core 文件的相關(guān)配置與 shell 資源限制
    我們先準(zhǔn)備一個(gè)有問(wèn)題的程序


    image.png

    編譯并運(yùn)行這個(gè)程序,程序發(fā)生 core dump,但是我們并沒(méi)有找到 core 文件


    image.png

    這是因?yàn)椋J(rèn)情況下 core 文件被 shell 限制大小為0了,所以我們看不到 core 文件,可以通過(guò) ulimit 命令查看限制
    image.png

    實(shí)際上,ulimit 是 shell 的一個(gè)命令,通過(guò)這個(gè)命令可以查看 shell 對(duì)各種資源的限制,比如 -a 選項(xiàng)可以查看所有限制
    image.png

    第一條就是 core 文件的限制,大小被限制為0。我們可以去改變它的大小限制,最簡(jiǎn)單的方法就是改為無(wú)限制,無(wú)限制就相當(dāng)于可以是任意大小。
ulimit -c unlimited

再次查看 shell 的限制就能看到,現(xiàn)在 core 的限制變?yōu)?unlimited 了


image.png

我們現(xiàn)在再一次運(yùn)行剛才的 err 可執(zhí)行文件,就可以看到生成了一個(gè) core 文件


image.png

作為一個(gè)優(yōu)秀的程序員,我們可能決定還不夠好,這名字是啥呀 core.9546,怪怪的,我們希望他有一個(gè)符合我們心意的名字,這也可以實(shí)現(xiàn),我們可以修改 core 的配置文件 /proc/sys/kernel/core_pattern ,那你改吧,你發(fā)現(xiàn)改完保存不了。
image.png

因?yàn)檫@個(gè)文件是不能寫(xiě)入的,我們可以借助重定向來(lái)修改這個(gè)文件

echo "core-%e-%t" > /proc/sys/kernel/core_pattern
image.png

關(guān)于里面的參數(shù),列表如下
參數(shù) 含義
%p 添加 pid
%u 添加 uid
%g 添加 gid
%s 添加導(dǎo)致 core dump 的信號(hào)
%t 添加 core 生成的時(shí)間
%h 添加主機(jī)名
%e 添加命令名
注意,core 文件是執(zhí)行可執(zhí)行文件時(shí),產(chǎn)生 core dump 后才會(huì)產(chǎn)生的一種文件,所以要先執(zhí)行可執(zhí)行文件,產(chǎn)生 core dump,這樣才能得到 core 文件。

(4)通過(guò)core文件調(diào)試當(dāng)?shù)舻某绦?br> 使用 gdb 可執(zhí)行文件名 core文件名 進(jìn)入gdb調(diào)試


image.png

where 命令查看出錯(cuò)的位置


image.png
  1. GDB調(diào)試多線程
    (1)創(chuàng)建一個(gè)多線程測(cè)試文件
    創(chuàng)建一個(gè)測(cè)試文件,代碼如下,本人 Linux 專題系列有線程專題與進(jìn)程專題,本文只做一個(gè)簡(jiǎn)單的線程創(chuàng)建。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

void* thread1()
{
    printf("this is thread1...\n");
    for(;;)
    {
        sleep(1);
    }
}

void* thread2()
{
    printf("this is thread2...\n");
    for(;;)
    {
        sleep(1);
    }
}

int main(int argc, char* argv[])
{
    pthread_t tid1;
    pthread_t tid2;
    
    printf("this is main...");
    
    pthread_create(&tid1, NULL, thread1, NULL); /*創(chuàng)建線程1*/
    pthread_create(&tid2, NULL, thread2, NULL); /*創(chuàng)建線程2*/
    
    /*等待線程結(jié)束*/
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    return 0;
}

(2)undefined reference to pthread_create’ 錯(cuò)誤 上面的文件創(chuàng)建好之后,如果直接編譯,會(huì)報(bào)錯(cuò)undefined reference topthread_create’

image.png

這是因?yàn)椋?lt;pthread.h> 并非 Linux 系統(tǒng)的默認(rèn)庫(kù),而是POSIX線程庫(kù)。在Linux中將 <pthread.h> 作為一個(gè)庫(kù)來(lái)使用的話,要加上 -l pthread 來(lái)顯式鏈接該庫(kù)。
image.png

這樣編譯就通過(guò)了。
(3)多線程調(diào)試
① 首先,運(yùn)行 ttt 可執(zhí)行文件,這里也會(huì)顯示主進(jìn)程 ID
image.png

② 然后用 SecureCRT 克隆會(huì)話或在 Linux 下直接打開(kāi)一個(gè)新的終端,在另一個(gè)會(huì)話中查看進(jìn)程 ID
image.png

查看主線程的線程樹(shù) pstree ,可以看到兩個(gè)子線程的線程 ID
image.png

③ 查看線程棧信息,pstack
image.png

④ 進(jìn)入 gdb 調(diào)試
image.png

查看線程
image.png

切換線程,根據(jù) info 查看到的編號(hào)來(lái)切換,我們可以通過(guò)線程 ID 來(lái)判斷是否切換
image.png

⑤ 打斷點(diǎn)等等指令與之前講的無(wú)異,這里講一些用于線程的命令

(gdb)thread apply num n     讓線程 num 繼續(xù)執(zhí)行,num 是線程的編號(hào),用info查看
(gdb)set scheduler-locking on   只執(zhí)行當(dāng)前線程,輸入 n 繼續(xù)執(zhí)行
(gdb)set scheduler-locking off  所有線程并發(fā)執(zhí)行

thread apply 線程id gdb_cmd : 指定某線程執(zhí)行某gdb命令
thread apply all gdb_cmd :全部的線程執(zhí)行某gdb命令
break xxx thread ID :為某個(gè)線程設(shè)置斷點(diǎn)
set scheduler-locking step: 當(dāng)單步執(zhí)行某一線程時(shí),其它線程不會(huì)執(zhí)行,同時(shí)保證在調(diào)試過(guò)程中當(dāng)前線程不會(huì)發(fā)生改變。
但如果該模式下執(zhí)行 continue、until、finish 命令,則其它線程也會(huì)執(zhí)行,并且如果某一線程執(zhí)行過(guò)程遇到斷點(diǎn),則 GDB 調(diào)試器會(huì)將該線程作為當(dāng)前線程。
NOTE:只有線程創(chuàng)建之后這個(gè)命令的暫停效果才有效,比如set scheduler-locking on開(kāi)啟之后,但pthread_create語(yǔ)句還沒(méi)執(zhí)行,這時(shí)執(zhí)行continue后邊創(chuàng)造的線程不會(huì)被暫停,必須等線程創(chuàng)造后再執(zhí)行coneinue。

總結(jié)
熟練掌握 gdb 調(diào)試是一個(gè)高水平程序員的基本技能,其實(shí)我們用習(xí)慣了 IDE 中的調(diào)試器之后,反而越來(lái)越忽視 gdb 這種命令行的調(diào)試。但是實(shí)際上,熟練掌握 gdb 會(huì)對(duì)調(diào)試程序本身產(chǎn)生更深刻的理解,可以大大提高程序調(diào)試水平。如果這篇文章大家覺(jué)得有幫助,可以關(guān)注我的 Linux 專欄,里面有更多 Linux 相關(guān)的優(yōu)質(zhì)文章。“紙上得來(lái)終覺(jué)淺,絕知此事要躬行”,學(xué)習(xí) Linux 知識(shí)的同時(shí),一定要?jiǎng)邮志毩?xí),親自去調(diào)試一些程序,只能理解這只指令是怎么執(zhí)行的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容