深入淺出 GDB 調試器

前言

GDB 全稱 GNU symbolic debugger,它是誕生於 GNU 開源組織的(同時誕生的還有 GCC、Emacs 等)UNIX 及 UNIX-like 下的調試工具,是 Linux 下最常用的程序調試器,GDB 支持調試多種編程語言編寫的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada 等。但是在實際應用中,GDB 更常用來調試 C 和 C++ 程序。雖然說在 Linux 系統下我們可以藉助諸多集成開發工具來完成程序的編寫和調試,但實際上,調試 C/C++ 程序一定是直接或者間接使用 GDB 完成的。所以說 GDB 調試幾乎可以說是 Linux 程序員必備的基本技能。本文將手把手教你使用 GDB 調試程序,並帶你深入瞭解什麼是 GDB 調試器。

文章目錄

  • 前言

  • 一、什麼是 GDB

  • (1)查看 GDB 版本

  • (2)安裝 GDB 調試器

  • (3)卸載 GDB

    1. 爲什麼要有 GDB
    1. 下載安裝 GDB
  • 二、GDB 的啓動與調試程序的上下文設置

  • (1)gdb 工作目錄

  • (2)程序運行參數

  • (3)查看及修改運行環境

  • (4)輸入輸出重定向

  • (1)測試程序中的 main 函數參數解析 argc 與 argv[]

  • (2)gcc 編譯時 ==-g== 選項幫我們做了什麼?

  • (3)啓動 GDB 與指定目標調試程序的方式

    1. 準備知識
    1. 程序上下文
  • 三、GDB 實戰講解

  • (1)創建一個多線程測試文件

  • (2)undefined reference to `pthread_create' 錯誤

  • (3)多線程調試

  • (1)什麼是 core dump 核心轉儲

  • (2)產生 core dump 的原因

  • (3)core 文件的相關配置與 shell 資源限制

  • (4)通過 core 文件調試當掉的程序

  • (1)調試非運行狀態的可執行程序

  • (2)調試一個正在運行的程序

  • (1)r(run)運行與 start 運行程序

  • (2)q(quit)退出調試

  • (3)help

  • (4)l(lsit)查看代碼

  • (5)set 傳入參數

  • (6)n(next)執行下一條語句,不進入函數內部

  • (7)s(step)執行下一條語句,且進入函數內部

  • (8)u(until)

  • (9)b(break)設置斷點以及打斷點的六種方式

  • (10)tbreak

  • (11)rbreak

  • (12)disable 與 enable

  • (13)watch

  • (14)rwatch

  • (15)awatch

  • (16)catch

  • (17)c(continue)執行到下一個斷點處

  • (18)info 查看

  • (19)del(delete)刪除

  • (20)clear

  • (21)ignore

  • (22)p (print)

  • (23)ptype 查看類型

  • (24)display 跟蹤變化

  • (25)undisplay 取消跟蹤

  • (26)bt (backtrace)查看棧信息

  • (27)x 查看內存

  • (28)disas 反彙編

  • (29)finish

  • (30)return

  • (31)call

  • (32)edit

  • (33)search

    1. GDB 命令詳解
    1. GDB 跟蹤可以正常編譯運行的源文件
    1. GDB 跟蹤 core(調試掛掉的程序)
    1. GDB 調試多線程
  • 總結

一、什麼是 GDB

1. 爲什麼要有 GDB

我們在開發程序的過程中,應該很少會有一次就編譯通過的吧,有時候即便是寫了短短几十行的代碼,都難免會有一些小的疏忽,更何況是幾千上萬甚至更大的代碼,反正我在開發中幾乎每次寫完程序都會經過反覆的調試,鍵盤的 F11 鍵經常會壞掉。在程序中,出現的錯誤主要分爲 2 大 類,即語法錯誤和邏輯錯誤:

程序出現語法錯誤,可以依靠 GCC 檢查出來,而邏輯錯誤就要我們今天的主角 GDB 登場解決了。所謂調試(Debug),就是單步執行代碼,或通過斷點讓程序執行到某個位置,以此來逐步鎖定程序出現問題的範圍。在單步調試的過程中,我們可以監控程序執行的每一個行爲,包括變量值的變化、函數的調用、內存中數據的變化、線程的調度等等,以此來修復 BUG 或者優化代碼。
我們在 Windows 下開發最常用的 Visual Studio,它自帶的調試器是 Remote Debugger,調試器與整個 IDE 無縫銜接,使用非常方便。在 Linux 下 C/C++ 必備的調試器就是 GDB 了,下面講解如何查看 GDB 版本及安裝 GDB。

2. 下載安裝 GDB

(1)查看 GDB 版本

gdb -v
gdb --version

如果你的執行結果如下,說明已經安裝好了 gdb,版本號如下,一般我們裝好 Linux 後可以通過這個命令來測試是否已經安裝 gdb 調試器。

如果你的運行結果顯示 not found ,說明未安裝 gdb 調試器,安裝 gdb 的方法主要有兩個,下面一節介紹安裝方法。

bash: gdb: command not found

(2)安裝 GDB 調試器

安裝 gdb 主要有兩種方法:
① 直接安裝。通常我們安裝好 Linux 之後,操作系統內會附帶有 gdb 的安裝包,我們可以直接使用操作系統內已有的 gdb 安裝包,使用包管理器進行安裝。這種方法簡單有效,只需要一條命令就可以安裝成功(以 CentOS 爲例)

yum -y install gdb

安裝好後,可以通過 gdb -v 查看版本,一般來說通過這種方式安裝的 gdb 都不是最新版本,並且無法自己選擇版本。
② 通過源碼安裝。源碼安裝是指首先去網上下載源碼壓縮包,然後在本地解壓安裝,我們可以選擇自己需要的版本進行安裝,可以直接點擊源碼包的鏈接 gdb 源碼去下載。

裏面有很多版本和格式,我們可以選擇一個自己需要的版本 .tar.gz 格式下載,下載後進行下面的操作,比如說我下載最新的版本 gdb-11.2.tar.gz。(通過源碼安裝文件以及 tar 壓縮包管理命令可以查看我的文章《【Linux 王者之路基礎篇:基本命令與基礎知識】Linux 常用 shell 命令(及相關知識)詳解與用法演示》第六章節《六、壓縮文件管理相關命令》有詳細介紹)

找到下載好的壓縮包並解壓

tar -zxvf gdb-11.2.tar.gz

如果你是在 Windows 下下載好的壓縮包,要傳到 Linux 下,可以藉助 SecureCRT 的 rz 命令,教程請見《【Linux 開發環境搭建:工具篇】SecureCRT 工具連接虛擬機、rz/sz 傳輸、中文亂碼問題解決》。

我下載的太慢了,半小時才下載三分之一,所以後面就只說命令了。

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

(3)卸載 GDB

gdb 調試器的卸載命令

yum remove gdb

二、GDB 的啓動與調試程序的上下文設置

1. 準備知識

(1)測試程序中的 main 函數參數解析 argc 與 argv[]

首先我們創建一個 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;
}

在這個測試程序中,main 函數貌似有點不同尋常啊

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

多了兩個東西,argc 和 argv,其實在 main 函數中本就應該有這兩個參數,只不過在我們平常的大部分學習中,都弱化了這兩個參數的作用,估計大部分人在學習編程時都從來沒有寫過這兩個參數。第一個參數 argc 用來統計程序運行時傳遞給 main 函數的命令行參數的個數,這個不需要我們設置;argv 是一個字符串數組,用來存放我們傳入的參數,其中 argv[0] 默認就是程序運行的路徑名。說起來不好理解,我們舉個例子,就用上面給出的 gdb_test.c 文件,我們編譯好運行一下,並傳遞參數

gcc gdb_test.c -o g3
./g3 111111


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

(2)gcc 編譯時 -g 選項幫我們做了什麼?

gdb 主要的作用是跟蹤程序的執行過程,所以要想用 gdb 調試程序,首先要把源程序編譯爲可執行文件。但是,我們正常使用 gcc 命令編譯出來的可執行文件是無法通過 gdb 調試的,因爲這樣編譯出來的可執行文件缺少 gdb 調試所需要的調試信息(比如每一行代碼的行號、包含程序中所有符號的符號表等信息)。要想生成帶有 gdb 調試信息的可執行文件,就要在 gcc 編譯的時候添加 ==-g== 選項。
你可能通過嘗試後會說,不加 gcc 的 -g 選項也能進入 gdb 調試,確實是這樣,但是進入 gdb 並不代表就可以調試,比如下面

我們不加 -g 編譯一個源文件,並啓動 gdb

進入 gdb 後我們發現,使用 r 命令執行可以,但是通過 list 查看源代碼卻不行。這是因爲,我們不加 -g 編譯出來的可執行文件不包含行號和符號表等調試所需要的信息,所以你想查看源碼、添加斷點都是無法實現的。而這就是 -g 選項的作用,我們可以對比一下加與不加 -g 選項生成的可執行文件大小

能夠看得出,加了 -g 選項後編譯出來的可執行文件佔據了更多個空間,這是因爲,它包含了調試信息。
有時候,我們在編譯時會組合 -g 和 -O 來使用,通常用 -Og 來實現在保證快速編譯和更好的調試前提下,進行一定的優化。

(3)啓動 GDB 與指定目標調試程序的方式

啓動 gdb 調試器分爲四種情況:
① 調試非運行狀態且編譯通過可運行的可執行文件

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

② 調試正在運行的可執行文件

gdb -p pid(進程號)

③ 調試 core

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

上面這三種情況會在後面對應的章節詳細介紹。
④ 假如直接使用 gdb 命令進入 gdb 調試器,gdb 自己是無法確定要調試哪個可執行文件的,即使當前目錄只有一個可執行文件也無法自動識別,這時我們可以手動指定目標調試文件。

提示信息中已經告訴我們使用哪個命令來指定待調試程序了,那就是 file 命令,使用方法是 file 直接加可執行文件所在目錄以及可執行文件名,如果可執行文件就在 gdb 當前工作目錄下,可以不加目錄,這樣我們就可以使用 gdb 調試 file 命令指定的可執行文件了

不管哪種情況,我們進入 gdb 時,總會打印一堆聲明

要想去掉這些聲明,可以在 gdb 後面加 –silent 或 -q 或 –quiet 選項。

只要最下面有一個 (gdb) 就說明進入成功。

2. 程序上下文

(1)gdb 工作目錄

默認情況下,GDB 調試器會把啓動時所在的目錄作爲工作目錄,但有時候我們可能需要根據情況去改變 gdb 的工作目錄,查看 gdb 當前工作目錄和改變工作目錄的命令和 shell 下一樣。
① 查看當前 gdb 工作目錄
pwd 命令可以查看當前 gdb 工作目錄

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

另外提示一下,gdb 調試時,也可以使用 tab 鍵命令補全、上下鍵查看歷史命令等。

(2)程序運行參數

傳遞運行參數的方式有三種:
① 啓動 gdb 時指定(exe 表示可執行文件名,paras 表示參數)

gdb --args exe paras

我們用前面的 gdb_test.c 編譯爲 g3,並傳入參數 111111111

② set 命令
gdb 調試器啓動後,在運行過程中,可以藉助 set 命令指定目標調試程序啓動所需要的運行參數

set paras

我們在函數 print_array() 處設置一個斷點,並執行到斷點處,然後把函數參數 len 設置爲 2,也就是隻打印兩個數據(array 總共 5 個數據,可以看前面的圖中打印結果)

可以看到 set 在運行的過程中改變了參數 len 的值。
③ 運行時指定
gdb 調試器啓動後,在運行時可以通過 run 和 start 來指定參數

run paras
start paras

(3)查看及修改運行環境

① 查看程序的運行路徑

show paths

② 設置程序的運行路徑

path /xxx/xxx/

③ 查看環境變量

show environment

④ 設置環境變量

set environment PARA=para

(4)輸入輸出重定向

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

可以看到,運行程序後,屏幕上沒有任何輸出,我們退出 gdb 查看 1.txt 文件

程序運行結果都被打印到了該文件中。
② 選擇終端
使用終端 tty1,命令如下

tty /dev/tty1

三、GDB 實戰講解

1. GDB 命令詳解

在下面所有的命令標題中,括號內爲命令全寫,括號外爲命令縮寫,使用效果一樣,例如運行命令 r(run),下面兩種用法效果一致

(gdb)r
(gdb)run

下面的例子都是用前面編譯好的文件 gdb_test.c 及可執行文件 g3。

(1)r(run)運行與 start 運行程序

run 運行程序,如果有斷點則停在斷點處,如果沒有斷點會一直執行到程序結束。start 會執行到 main 函數的起始位置,相當於在 main() 加一個斷點,然後使用 run 執行。如果在程序調試或者執行中使用 run 或 start 都代表從頭開始重新執行程序。
在 r 或 start 命令後面加參數可以把參數傳入並執行(前面已經介紹過了)

(gdb)r para

傳入參數 para 並執行。

start 會執行到 mian 處。

(2)q(quit)退出調試

退出 gdb 調試,回到 shell。

(3)help

查看幫助手冊,按 q 退出幫助手冊。

(4)l(lsit)查看代碼

① 一次顯示 10 行

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

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

④ 查看其他文件代碼,用於包含多個源文件的情況,比如可執行文件 test 由 test1.c 和 test2.c 編譯而成,可以通過指定文件名來查看 test1.c 或 test2.c 的源代碼。
查看 test1.c 的代碼 1 到 10 行

(gdb)list test1.c:1,10

(5)set 傳入參數

① set 可以傳入參數或者修改變量的值

② 變量名與 gdb 命令名衝突
比如你在源代碼中有一個變量名叫 width ,如果你要用 set 設置這個變量的值會產生衝突,因爲 set width 是 gdb 的命令,這時可以通過 set var 告訴 gdb 該變量是用戶變量。建議自己寫代碼時要避免和系統函數、編譯調試等命令重名的函數或變量,以避免不必要的麻煩。

(gdb)set var width=10

③ 設置命令
比如說我們在打印結構體的時候,使用 p 命令默認就是普通的打印,可能不是很美觀,我們可以通過命令使打印出來的結構體更符合我們觀看的習慣

(gdb)set print pretty

(6)n(next)執行下一條語句,不進入函數內部

單步執行代碼,一條語句一條語句的執行,如果遇到函數不會進入函數內部,可以理解爲 VS 的 F10 調試鍵。也可以在後面加數字表示執行多少行

(gdb)n num

(7)s(step)執行下一條語句,且進入函數內部

用法基本與 next 相同,區別在於 step 在遇到函數的時候會進入函數內部(像 printf 等這種庫函數不會進入),可以理解爲 VS 的 F11 調試鍵。

可以看到,當執行到我們自己的函數 print_array() 的時候,按 step 會進入這個函數的內部,停在這個函數內部語句的第一行。同樣,step 也可以在後面加數字表示一次執行多少行。

(8)u(until)

① 跳出循環體
在遇到循環體時,如果在循環體尾部(最後一行代碼)按 until 調試鍵,會直接執行完整個循環體,並停在循環體外。

② 跳轉至某一行

(gdb)until num

直接跳至第 num 行執行並停在這一行。
③ 在其他時候,功能和 next 一樣,都是單步執行。

(9)b(break)設置斷點以及打斷點的六種方式

斷點(BreakPoint),可以讓程序執行到斷點處並停在這裏,加斷點應該是調試的時候最常用的一種方法,就像 VS 中的 F9 鍵。加斷點的方式有很多種,下面將逐一介紹:
① b function (直接加函數名)在某個函數 function 處添加斷點
在函數 print_array() 處加斷點並執行,會停在該函數內部的第一行

② b num (直接加行號)在第 num 行添加斷點

這裏有一點要注意,因爲程序已經啓動了,如果我們要想執行到斷點處,應該使用命令 c ,如果使用 run 或 start 會重新運行程序。
③ b file.c:num 在 file.c 文件的第 num 行加斷點,如果不加文件名 file.c 則默認是在含 main 函數的那個文件第 num 行加斷點。
④ b file.c:function 在 file.c 文件中名爲 function 的函數處加斷點。
⑤ b ±num 通過偏移地址設置斷點,+ 表示從當前程序運行行開始,往下數 num 行並設置斷點;- 表示當前程序運行行開始,往上數 num 行並設置斷點。
舉例,當前程序在第 34 行,通過 b +12 可以把程序打在 34+12=16 行處。

⑥ b (上面五種方式指定斷點位置) if expression 當滿足表達式 expression 的時候打斷點,也就是說只有當這個表達式爲真的時候,這個斷點纔會生效。
使用舉例:

(gdb)b 12 if i==2 	當i==2的時候在第12行加斷點
(gdb)b func if i>3	當i>3的時候在函數func處加斷點

(10)tbreak

命令的格式與用法與 break 相同,但是設置的斷點只生效一次,該斷點使用一次後自動去除。

(11)rbreak

該命令用於給函數加斷點, rbreak regex 給所有滿足表達式 REGEX 的函數加斷點,設置的斷點和 break 設置的斷點一樣。這個命令在 C++ 調試的時候,用於給所有重載函數加斷點非常方便。也可以加文件名來限制爲哪個文件中的所有滿足表達式的函數加斷點 rbreak file.c:regex 。

(12)disable 與 enable

用於禁用和激活斷點(普通斷點、捕捉點、觀察點、display 的變量),通過斷點號來指定要禁用或激活的斷點(通過 info 查看斷點號),可以通過 help 手冊查看用法,被 disable 禁用的斷點將會暫時失效,使用 enable 激活後會再度恢復正常使用。

enable 可以激活多個斷點,並且可以指點被激活的斷點起作用的次數。

舉個小例子

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

(13)watch

設置觀察點,如果在執行過程中變量發生變化,就把他打印出來,並停止運行。
這裏要注意,如果你用指針(或地址)來設置觀察點,一定要解引用,* 指針纔是對指針所指向的變量進行觀察如果不解引用,那就是對指針變量本身(地址)進行觀察。另外,如果你觀察一個臨時變量或表達式,當它的生命週期結束的時候,對應的觀察點也就失效了。
觀察點有軟件觀察點和硬件觀察點,這裏不再詳細介紹。

(14)rwatch

只要程序中出現讀取目標變量或表達式的值的操作,程序就會停止運行。(讀)

(15)awatch

只要程序中出現讀取目標變量或表達式的值或者改變值的操作,程序就會停止運行。(讀寫)

(16)catch

(gdb)catch enevt 	監控某一事件 event 的發生,當事件發生時,程序停止

這個 event 可以是下面的情況:
① C++ 中 throw 拋出的異常或 catch 捕捉到的異常;
② load 命令或 unload 命令,在動態庫加載或卸載時程序停止執行;
③ fork、vfork、exec 系統調用時,程序停止運行;
舉個例子測試一下,先準備一個 C++ 源文件,並編譯生成帶調試信息的可執行文件 test。

進入調試,設置捕捉點,捕捉 string 類型的異常

(17)c(continue)執行到下一個斷點處

繼續執行程序,一直執行到下一個斷點處。

(18)info 查看

① info breakpoints 查看所有斷點的信息

② info breakpoints num 查詢 num 號斷點的信息

③ info variables 查詢當前全局變量信息

④ info watchpoints 查看觀察點信息
⑤ 查看寄存器
⑥ 查看當前函數內部臨時變量的值

⑦ 查看當前函數參數的值

⑧ 更多用法,請查看幫助手冊

(19)del(delete)刪除

如果我們使用 quit 退出調試,然後再次啓動 gdb 的話,之前設置的所有類型的斷點(包括觀察點、捕捉點)都會消失。通過 delete 可以在當前調試中刪除斷點。在使用 delete 刪除斷點的時候,要先用 info 命令查看斷點信息,在顯示信息的第一列會有斷點的編號,然後再根據編號刪除斷點即可。(刪除觀察點、捕捉點方法與刪除斷點一致)

如果直接使用 delete 命令,不加斷點號的話,會刪除當前所有斷點。

(20)clear

刪除斷點,後面加行號或函數名,(delete 是按照斷點號刪除)

(gdb)clear func 	刪除函數 func 處的斷點
(gdb)clear num	刪除第 num 行的斷點

(21)ignore

忽視斷點

(gdb)ignore num count 	忽視編號爲 num 的斷點 count 次

(22)p (print)

① 打印變量的值

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


array 類型爲 char ,地址每次 + 1 增長 1 個字節。
② 指定打印變量值的進制,比如 /x 表示按 16 進制打印

進製表如下:

vEDgLD

其實和我們在 C 語言中的語法是一樣的。
③ 打印表達式結果

④ 修改變量的值

(23)ptype 查看類型

查看一個變量的數據類型

(24)display 跟蹤變化

查看某個變量或表達式的值,和 p 命令類似,但是 display 會一直跟蹤這個變量或表達式值得變化,每執行一條語句都會打印一次變量或表達式的值。

display 也可以按格式打印,語法和 print 一樣,請參照上表(print)。
display 跟蹤得變量或表達式也會放入一張表中,使用 info 命令可以查看信息

同樣,Num 表示編號,Enb 表示是否激活,Expression 表示被跟蹤的表達式。

(25)undisplay 取消跟蹤

後面加 Num 編號,刪除取消跟蹤。其實也可以使用 del 刪除。

(26)bt (backtrace)查看棧信息

在一個程序的執行過程中,如果遇到函數調用,會產生一系列一些與函數上下文相關的信息:比如函數調用的位置、函數參數、函數內部的臨時變量等。這些信息會被存放在一塊稱爲棧幀的內存空間中,並且每一個函數調用都對應一個棧幀(main 函數也有自己的棧幀,稱爲初始幀)。這些所有的棧幀都存放在內存中的棧區。通過命令 info frame 可以查看當前使用的棧幀所存儲的信息,這裏麪包含了棧幀編號、棧幀地址、調用者、源碼編程語言等信息。通過命令 frame num 、up 、down 可以選的改變棧幀。

查看當前所有棧幀 bt

(27)x 查看內存


同樣可以指定按什麼格式查看。

(28)disas 反彙編

查看函數 print_array() 的反彙編代碼,使用命令 q 退出。

(29)finish

跳出當前所在的函數。

(30)return

忽略後面的語句,立即返回,可以指定返回值 return -1 。

(31)call

調用某個函數,call func() 調用 func() 函數。

(32)edit

進入編輯模式

(33)search

search 搜索,reverse-search 反向搜索。

2. GDB 跟蹤可以正常編譯運行的源文件

(1)調試非運行狀態的可執行程序

這個很簡單,我們前面介紹命令時,所舉的例子,都是在這種情況下進行的。也就是對編譯好的可執行文件進行調試。

進入 gdb 調試,然後用上面介紹的命令進行調試即可。

(2)調試一個正在運行的程序

有時候我們運行一個一直執行的程序時,希望能夠調試這個程序。比如某個帶有無限循環打印某些信息的程序。

我們可以這麼做,首先編譯生成可執行文件,然後在運行時加 & 讓進程轉爲後臺執行,或者通過 SecureCRT 克隆會話來新打開一個會話進行調試。
① 首先通過 ps 命令查看進程號,找到 loop 進程的進程信息

② 通過 gdb 的 -p 參數,指定進程進入調試

③ 正在運行的程序會暫停,可以正常調試了

3. GDB 跟蹤 core(調試掛掉的程序)

(1)什麼是 core dump 核心轉儲

core 是指 core memory,dump 即堆放。core dump 就是核心轉儲的意思。在 Unix 系統中,經常會將主內存 main memory 稱爲核心 core,而核心映像 core image 是指進程執行時的內存狀態。當程序發生錯誤或者異常或者收到某些信號而終止執行的時候,操作系統會把核心映像寫入一個文件(core 文件)來作爲調試依據,這就是核心轉儲 core dump。
換句話說,當我們寫的程序在運行時發生異常而退出的時候,由操作系統把程序當前的內存狀況存儲在一個 core 文件中,這就叫 core dump。也就是說,所謂 core dump 核心轉儲,就是當我們寫的程序當掉(異常退出)時,把程序當前的內存狀況存儲起來,以作爲調試的參考的這麼一種技術。

(2)產生 core dump 的原因

主要原因可以分爲三大類:
① 訪問越界
包括數組下標越界,C 語言字符串無結束符引起的越界,使用非法指針(空指針 NULL、野指針、未初始化的指針、越界指針)等。
② 多線程
多線程訪問全局變量未加同步機制(鎖機制等),或使用了線程不安全的函數。
③ 堆棧溢出
使用了太大的局部變量或無限嵌套、遞歸調用函數,可能會造成棧溢出。

(3)core 文件的相關配置與 shell 資源限制

我們先準備一個有問題的程序

編譯並運行這個程序,程序發生 core dump,但是我們並沒有找到 core 文件

這是因爲,默認情況下 core 文件被 shell 限制大小爲 0 了,所以我們看不到 core 文件,可以通過 ulimit 命令查看限制

實際上,ulimit 是 shell 的一個命令,通過這個命令可以查看 shell 對各種資源的限制,比如 -a 選項可以查看所有限制

第一條就是 core 文件的限制,大小被限制爲 0。我們可以去改變它的大小限制,最簡單的方法就是改爲無限制,無限制就相當於可以是任意大小。

ulimit -c unlimited

再次查看 shell 的限制就能看到,現在 core 的限制變爲 unlimited 了

我們現在再一次運行剛纔的 err 可執行文件,就可以看到生成了一個 core 文件

作爲一個優秀的程序員,我們可能決定還不夠好,這名字是啥呀 core.9546,怪怪的,我們希望他有一個符合我們心意的名字,這也可以實現,我們可以修改 core 的配置文件 /proc/sys/kernel/core_pattern ,那你改吧,你發現改完保存不了。

因爲這個文件是不能寫入的,我們可以藉助重定向來修改這個文件

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


關於裏面的參數,列表如下

qMJeKQ

注意,core 文件是執行可執行文件時,產生 core dump 後纔會產生的一種文件,所以要先執行可執行文件,產生 core dump,這樣才能得到 core 文件。

(4)通過 core 文件調試當掉的程序

使用 gdb 可執行文件名 core 文件名 進入 gdb 調試

where 命令查看出錯的位置

4. GDB 調試多線程

(1)創建一個多線程測試文件

創建一個測試文件,代碼如下,本人 Linux 專題系列有線程專題與進程專題,本文只做一個簡單的線程創建。

#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); /*創建線程1*/
	pthread_create(&tid2, NULL, thread2, NULL); /*創建線程2*/
	
	/*等待線程結束*/
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	
	return 0;
}

(2)undefined reference to `pthread_create’ 錯誤

上面的文件創建好之後,如果直接編譯,會報錯 undefined reference to `pthread_create’

這是因爲,<pthread.h> 並非 Linux 系統的默認庫,而是 POSIX 線程庫。在 Linux 中將 <pthread.h> 作爲一個庫來使用的話,要加上 -l pthread 來顯式鏈接該庫。

這樣編譯就通過了。

(3)多線程調試

① 首先,運行 ttt 可執行文件,這裏也會顯示主進程 ID

② 然後用 SecureCRT 克隆會話或在 Linux 下直接打開一個新的終端,在另一個會話中查看進程 ID

查看主線程的線程樹 pstree ,可以看到兩個子線程的線程 ID

③ 查看線程棧信息,pstack

④ 進入 gdb 調試

查看線程

切換線程,根據 info 查看到的編號來切換,我們可以通過線程 ID 來判斷是否切換

⑤ 打斷點等等指令與之前講的無異,這裏講一些用於線程的命令

(gdb)thread apply num n 	讓線程 num 繼續執行,num 是線程的編號,用info查看
(gdb)set scheduler-locking on 	只執行當前線程,輸入 n 繼續執行
(gdb)set scheduler-locking off 	所有線程併發執行

總結

熟練掌握 gdb 調試是一個高水平程序員的基本技能,其實我們用習慣了 IDE 中的調試器之後,反而越來越忽視 gdb 這種命令行的調試。但是實際上,熟練掌握 gdb 會對調試程序本身產生更深刻的理解,可以大大提高程序調試水平。如果這篇文章大家覺得有幫助,可以關注我的 Linux 專欄,裏面有更多 Linux 相關的優質文章。“紙上得來終覺淺,絕知此事要躬行”,學習 Linux 知識的同時,一定要動手練習,親自去調試一些程序,只能理解這隻指令是怎麼執行的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OQbao-OtZwlNXuisJzqEGw