關注公眾號【高性能架構探索】,回復【pdf】,免費獲取計算機經典資料
在上篇文章中,我們分析了線上coredump產生的原因,其中用到了coredump分析工具gdb,這幾天一直有讀者在問,能不能寫一篇關于gdb除錯方面的文章,今天借助此文,分享一些作業中的除錯經驗,希望能夠幫到大家,
寫在前面
在我的作業經歷中,前幾年在Windows上進行開發,使用Visual Studio進行除錯,簡直是利器,各種斷點等用滑鼠點點點就能設定;大概從12年開始轉Linux開發了,所以除錯都是基于GDB的,本來這篇文章也想寫寫Windows下除錯相關,奈何好多年沒用了,再加上作業太忙,所以本文就只寫了Linux下GDB除錯相關,對于Windows開發人員,實在對不住了😃,
這篇文章,涉及的比較全面,總結了這些年的gdb除錯經驗(都是小兒科😁),經常用到的一些除錯技巧,希望能夠對從事Linux開發的相關人員有所幫助

背景
作為C/C++開發人員,保證程式正常運行是最基本也是最主要的目的,而為了保證程式正常運行,除錯則是最基本的手段,熟悉這些除錯方式,可以方便我們更快的定位程式問題所在,提高開發效率,
在開發程序,如果程式的運行結果不符合預期,第一時間就是打開GDB進行除錯,在對應的地方設定斷點,然后分析原因;當線上服務出了問題,第一時間查看行程在不在,如果不在的話,是否生成了coredump檔案,如果有,則使用gdb除錯coredump檔案,否則通過dmesg來分析內核日志來查找原因,
概念
GDB是一個由GNU開源組織發布的、UNIX/LINUX作業系統下的、基于命令列的、功能強大的程式除錯工具,
GDB支持斷點、單步執行、列印變數、觀察變數、查看暫存器、查看堆疊等除錯手段,在Linux環境軟體開發中,GDB是主要的除錯工具,用來除錯C和 C++程式(也支持go等其他語言),
常用命令
斷點
斷點是我們在除錯中經常用的一個功能,我們在指定位置設定斷點之后,程式運行到該位置將會暫停,這個時候我們就可以對程式進行更多的操作,比如查看變數內容,堆疊情況等等,以幫助我們除錯程式,
以設定斷點的命令分為以下幾類:
- breakpoint
- watchpoint
- catchpoint
breakpoint
可以根據行號、函式、條件生成斷點,下面是相關命令以及對應的作用說明:
| 命令 | 作用 |
|---|---|
| break [file]:function | 在檔案file的function函式入口設定斷點 |
| break [file]:line | 在檔案file的第line行設定斷點 |
| info breakpoints | 查看斷點串列 |
| break [±]offset | 在當前位置偏移量為[±]offset處設定斷點 |
| break *addr | 在地址addr處設定斷點 |
| break … if expr | 設定條件斷點,僅僅在條件滿足時 |
| ignore n count | 接下來對于編號為n的斷點忽略count次 |
| clear | 洗掉所有斷點 |
| clear function | 洗掉所有位于function內的斷點 |
| delete n | 洗掉指定編號的斷點 |
| enable n | 啟用指定編號的斷點 |
| disable n | 禁用指定編號的斷點 |
| save breakpoints file | 保存斷點資訊到指定檔案 |
| source file | 匯入檔案中保存的斷點資訊 |
| break | 在下一個指令處設定斷點 |
| clear [file:]line | 洗掉第line行的斷點 |
watchpoint
watchpoint是一種特殊型別的斷點,類似于正常斷點,是要求GDB暫停程式執行的命令,區別在于watchpoint沒有駐留某一行源代碼中,而是指示GDB每當某個運算式改變了值就暫停執行的命令,
watchpoint分為硬體實作和軟體實作兩種,前者需要硬體系統的支持;后者的原理就是每步執行后都檢查變數的值是否改變,GDB在新建資料斷點時會優先嘗試硬體方式,如果失敗再嘗試軟體實作,
| 命令 | 作用 |
|---|---|
| watch variable | 設定變數資料斷點 |
| watch var1 + var2 | 設定運算式資料斷點 |
| rwatch variable | 設定讀斷點,僅支持硬體實作 |
| awatch variable | 設定讀寫斷點,僅支持硬體實作 |
| info watchpoints | 查看資料斷點串列 |
| set can-use-hw-watchpoints 0 | 強制基于軟體方式實作 |
使用資料斷點時,需要注意:
- 當監控變數為區域變數時,一旦區域變數失效,資料斷點也會失效
- 如果監控的是指標變數
p,則watch *p監控的是p所指記憶體資料的變化情況,而watch p監控的是p指標本身有沒有改變指向
最常見的資料斷點應用場景:定位堆上的結構體內部成員何時被修改,由于指標一般為區域變數,為了解決斷點失效,一般有兩種方法,
| 命令 | 作用 |
|---|---|
| print &variable | 查看變數的記憶體地址 |
| watch *(type *)address | 通過記憶體地址間接設定斷點 |
| watch -l variable | 指定location引數 |
| watch variable thread 1 | 僅編號為1的執行緒修改變數var值時會中斷 |
catchpoint
從字面意思理解,是捕獲斷點,其主要監測信號的產生,例如c++的throw,或者加載庫的時候,產生斷點行為,
| 命令 | 含義 |
|---|---|
| catch fork | 程式呼叫fork時中斷 |
| tcatch fork | 設定的斷點只觸發一次,之后被自動洗掉 |
| catch syscall ptrace | 為ptrace系統呼叫設定斷點 |
在
command命令后加斷點編號,可以定義斷點觸發后想要執行的操作,在一些高級的自動化除錯場景中可能會用到,
命令列
| 命令 | 作用 |
|---|---|
| run arglist | 以arglist為引數串列運行程式 |
| set args arglist | 指定啟動命令列引數 |
| set args | 指定空的引數串列 |
| show args | 列印命令列串列 |
程式堆疊
| 命令 | 作用 |
|---|---|
| backtrace [n] | 列印堆疊幀 |
| frame [n] | 選擇第n個堆疊幀,如果不存在,則列印當前堆疊幀 |
| up n | 選擇當前堆疊幀編號+n的堆疊幀 |
| down n | 選擇當前堆疊幀編號-n的堆疊幀 |
| info frame [addr] | 描述當前選擇的堆疊幀 |
| info args | 當前堆疊幀的引數串列 |
| info locals | 當前堆疊幀的區域變數 |
多行程、多執行緒
多行程
GDB在除錯多行程程式(程式含fork呼叫)時,默認只追蹤父行程,可以通過命令設定,實作只追蹤父行程或子行程,或者同時除錯父行程和子行程,
| 命令 | 作用 |
|---|---|
| info inferiors | 查看行程串列 |
| attach pid | 系結行程id |
| inferior num | 切換到指定行程上進行除錯 |
| print $_exitcode | 顯示程式退出時的回傳值 |
| set follow-fork-mode child | 追蹤子行程 |
| set follow-fork-mode parent | 追蹤父行程 |
| set detach-on-fork on | fork呼叫時只追蹤其中一個行程 |
| set detach-on-fork off | fork呼叫時會同時追蹤父子行程 |
在除錯多行程程式時候,默認情況下,除了當前除錯的行程,其他行程都處于掛起狀態,所以,如果需要在除錯當前行程的時候,其他行程也能正常執行,那么通過設定set schedule-multiple on即可,
多執行緒
多執行緒開發在日常開發作業中很常見,所以多執行緒的除錯技巧非常有必要掌握,
默認除錯多執行緒時,一旦程式中斷,所有執行緒都將暫停,如果此時再繼續執行當前執行緒,其他執行緒也會同時執行,
| 命令 | 作用 |
|---|---|
| info threads | 查看執行緒串列 |
| print $_thread | 顯示當前正在除錯的執行緒編號 |
| set scheduler-locking on | 除錯一個執行緒時,其他執行緒暫停執行 |
| set scheduler-locking off | 除錯一個執行緒時,其他執行緒同步執行 |
| set scheduler-locking step | 僅用step除錯執行緒時其他執行緒不執行,用其他命令如next除錯時仍執行 |
如果只關心當前執行緒,建議臨時設定 scheduler-locking 為 on,避免其他執行緒同時運行,導致命中其他斷點分散注意力,
列印輸出
通常情況下,在除錯的程序中,我們需要查看某個變數的值,以分析其是否符合預期,這個時候就需要列印輸出變數值,
| 命令 | 作用 |
|---|---|
| whatis variable | 查看變數的型別 |
| ptype variable | 查看變數詳細的型別資訊 |
| info variables var | 查看定義該變數的檔案,不支持區域變數 |
列印字串
使用x/s命令列印ASCII字串,如果是寬字符字串,需要先看寬字符的長度 print sizeof(str),
如果長度為2,則使用x/hs列印;如果長度為4,則使用x/ws列印,
| 命令 | 作用 |
|---|---|
| x/s str | 列印字串 |
| set print elements 0 | 列印不限制字串長度/或不限制陣列長度 |
| call printf("%s\n",xxx) | 這時列印出的字串不會含有多余的轉義符 |
| printf “%s\n”,xxx | 同上 |
列印陣列
| 命令 | 作用 |
|---|---|
| print *array@10 | 列印從陣列開頭連續10個元素的值 |
| print array[60]@10 | 列印array陣列下標從60開始的10個元素,即第60~69個元素 |
| set print array-indexes on | 列印陣列元素時,同時列印陣列的下標 |
列印指標
| 命令 | 作用 |
|---|---|
| print ptr | 查看該指標指向的型別及指標地址 |
| print *(struct xxx *)ptr | 查看指向的結構體的內容 |
列印指定記憶體地址的值
使用x命令來列印記憶體的值,格式為x/nfu addr,以f格式列印從addr開始的n個長度單元為u的記憶體值,
n:輸出單元的個數f:輸出格式,如x表示以16進制輸出,o表示以8進制輸出,默認為xu:一個單元的長度,b表示1個byte,h表示2個byte(half word),w表示4個byte,g表示8個byte(giant word)
| 命令 | 作用 |
|---|---|
| x/8xb array | 以16進制列印陣列array的前8個byte的值 |
| x/8xw array | 以16進制列印陣列array的前16個word的值 |
列印區域變數
| 命令 | 作用 |
|---|---|
| info locals | 列印當前函式區域變數的值 |
| backtrace full | 列印當前堆疊幀各個函式的區域變數值,命令可縮寫為bt |
| bt full n | 從內到外顯示n個堆疊幀及其區域變數 |
| bt full -n | 從外向內顯示n個堆疊幀及其區域變數 |
列印結構體
| 命令 | 作用 |
|---|---|
| set print pretty on | 每行只顯示結構體的一名成員 |
| set print null-stop | 不顯示’\000’這種 |
函式跳轉
| 命令 | 作用 |
|---|---|
| set step-mode on | 不跳過不含除錯資訊的函式,可以顯示和除錯匯編代碼 |
| finish | 執行完當前函式并列印回傳值,然后觸發中斷 |
| return 0 | 不再執行后面的指令,直接回傳,可以指定回傳值 |
| call printf("%s\n", str) | 呼叫printf函式,列印字串(可以使用call或者print呼叫函式) |
| print func() | 呼叫func函式(可以使用call或者print呼叫函式) |
| set var variable=xxx | 設定變數variable的值為xxx |
| set {type}address = xxx | 給存盤地址為address,型別為type的變數賦值 |
| info frame | 顯示函式堆疊的資訊(堆疊幀地址、指令暫存器的值等) |
其它
圖形化
tui為terminal user interface的縮寫,在啟動時候指定-tui引數,或者除錯時使用ctrl+x+a組合鍵,可進入或退出圖形化界面,
| 命令 | 含義 |
|---|---|
| layout src | 顯示原始碼視窗 |
| layout asm | 顯示匯編視窗 |
| layout split | 顯示原始碼 + 匯編視窗 |
| layout regs | 顯示暫存器 + 原始碼或匯編視窗 |
| winheight src +5 | 原始碼視窗高度增加5行 |
| winheight asm -5 | 匯編視窗高度減小5行 |
| winheight cmd +5 | 控制臺視窗高度增加5行 |
| winheight regs -5 | 暫存器視窗高度減小5行 |
匯編
| 命令 | 含義 |
|---|---|
| disassemble function | 查看函式的匯編代碼 |
| disassemble /mr function | 同時比較函式源代碼和匯編代碼 |
除錯和保存core檔案
| 命令 | 含義 |
|---|---|
| file exec_file *# * | 加載可執行檔案的符號表資訊 |
| core core_file | 加載core-dump檔案 |
| gcore core_file | 生成core-dump檔案,記錄當前行程的狀態 |
啟動方式
使用gdb除錯,一般有以下幾種啟動方式:
- gdb filename: 除錯可執行程式
- gdb attach pid: 通過”系結“行程ID來除錯正在運行的行程
- gdb filename -c coredump_file: 除錯可執行檔案
在下面的幾節中,將分別對上述幾種除錯方式進行講解,從例子的角度出發,使得大家能夠更好的掌握除錯技巧,
除錯
可執行檔案
單執行緒
首先,我們先看一段代碼:
#include<stdio.h>
void print(int xx, int *xxptr) {
printf("In print():\n");
printf(" xx is %d and is stored at %p.\n", xx, &xx);
printf(" ptr points to %p which holds %d.\n", xxptr, *xxptr);
}
int main(void) {
int x = 10;
int *ptr = &x;
printf("In main():\n");
printf(" x is %d and is stored at %p.\n", x, &x);
printf(" ptr points to %p which holds %d.\n", ptr, *ptr);
print(x, ptr);
return 0;
}
這個代碼比較簡單,下面我們開始進入除錯:
gdb ./test_main
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-114.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/test_main...done.
(gdb) r
Starting program: /root/./test_main
In main():
x is 10 and is stored at 0x7fffffffe424.
ptr points to 0x7fffffffe424 which holds 10.
In print():
xx is 10 and is stored at 0x7fffffffe40c.
xxptr points to 0x7fffffffe424 which holds 10.
[Inferior 1 (process 31518) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64
在上述命令中,我們通過gdb test命令啟動除錯,然后通過執行r(run命令的縮寫)執行程式,直至退出,換句話說,上述命令是一個完整的使用gdb運行可執行程式的完整程序(只使用了r命令),接下來,我們將以此為例子,介紹幾種比較常見的命令,
斷點
(gdb) b 15
Breakpoint 1 at 0x400601: file test_main.cc, line 15.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400601 in main() at test_main.cc:15
(gdb) r
Starting program: /root/./test_main
In main():
x is 10 and is stored at 0x7fffffffe424.
ptr points to 0x7fffffffe424 which holds 10.
Breakpoint 1, main () at test_main.cc:15
15 print(xx, xxptr);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64
(gdb)
backtrace
(gdb) backtrace
#0 main () at test_main.cc:15
(gdb)
backtrace命令是列出當前堆疊中的所有幀,在上面的例子中,堆疊上只有一幀,編號為0,屬于main函式,
(gdb) step
print (xx=10, xxptr=0x7fffffffe424) at test_main.cc:4
4 printf("In print():\n");
(gdb)
接著,我們執行了step命令,即進入函式內,下面我們繼續通過backtrace命令來查看堆疊幀資訊,
(gdb) backtrace
#0 print (xx=10, xxptr=0x7fffffffe424) at test_main.cc:4
#1 0x0000000000400612 in main () at test_main.cc:15
(gdb)
從上面輸出結果,我們能夠看出,有兩個堆疊幀,第1幀屬于main函式,第0幀屬于print函式,
每個堆疊幀都列出了該函式的引數串列,從上面我們可以看出,main函式沒有引數,而print函式有引數,并且顯示了其引數的值,
有一點我們可能比較迷惑,在第一次執行backtrace的時候,main函式所在的堆疊幀編號為0,而第二次執行的時候,main函式的堆疊幀為1,而print函式的堆疊幀為0,這是因為_與堆疊的向下增長_規律一致,我們只需要記住_編號最小幀號就是最近一次呼叫的函式_,
frame
堆疊幀用來存盤函式的變數值等資訊,默認情況下,GDB總是位于當前正在執行函式對應堆疊幀的背景關系中,
在前面的例子中,由于當前正在print()函式中執行,GDB位于第0幀的背景關系中,可以通過frame命令來獲取當前正在執行的背景關系所在的幀,
(gdb) frame
#0 print (xx=10, xxptr=0x7fffffffe424) at test_main.cc:4
4 printf("In print():\n");
(gdb)
下面,我們嘗試使用print命令列印下當前堆疊幀的值,如下:
(gdb) print xx
$1 = 10
(gdb) print xxptr
$2 = (int *) 0x7fffffffe424
(gdb)
如果我們想看其他堆疊幀的內容呢?比如main函式中x和ptr的資訊呢?假如直接列印這倆值的話,那么就會得到如下:
(gdb) print x
No symbol "x" in current context.
(gdb) print xxptr
No symbol "ptr" in current context.
(gdb)
在此,我們可以通過_frame num_來切換堆疊幀,如下:
(gdb) frame 1
#1 0x0000000000400612 in main () at test_main.cc:15
15 print(x, ptr);
(gdb) print x
$3 = 10
(gdb) print ptr
$4 = (int *) 0x7fffffffe424
(gdb)
多執行緒
為了方便進行演示,我們創建一個簡單的例子,代碼如下:
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
int fun_int(int n) {
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "in fun_int n = " << n << std::endl;
return 0;
}
int fun_string(const std::string &s) {
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "in fun_string s = " << s << std::endl;
return 0;
}
int main() {
std::vector<int> v;
v.emplace_back(1);
v.emplace_back(2);
v.emplace_back(3);
std::cout << v.size() << std::endl;
std::thread t1(fun_int, 1);
std::thread t2(fun_string, "test");
std::cout << "after thread create" << std::endl;
t1.join();
t2.join();
return 0;
}
上述代碼比較簡單:
- 函式fun_int的功能是休眠10s,然后列印其引數
- 函式fun_string功能是休眠10s,然后列印其引數
- main函式中,創建兩個執行緒,分別執行上述兩個函式
下面是一個完整的除錯程序:
(gdb) b 27
Breakpoint 1 at 0x4013d5: file test.cc, line 27.
(gdb) b test.cc:32
Breakpoint 2 at 0x40142d: file test.cc, line 32.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004013d5 in main() at test.cc:27
2 breakpoint keep y 0x000000000040142d in main() at test.cc:32
(gdb) r
Starting program: /root/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Breakpoint 1, main () at test.cc:27
(gdb) c
Continuing.
3
[New Thread 0x7ffff6fd2700 (LWP 44996)]
in fun_int n = 1
[New Thread 0x7ffff67d1700 (LWP 44997)]
Breakpoint 2, main () at test.cc:32
32 std::cout << "after thread create" << std::endl;
(gdb) info threads
Id Target Id Frame
3 Thread 0x7ffff67d1700 (LWP 44997) "test" 0x00007ffff7051fc3 in new_heap () from /lib64/libc.so.6
2 Thread 0x7ffff6fd2700 (LWP 44996) "test" 0x00007ffff7097e2d in nanosleep () from /lib64/libc.so.6
* 1 Thread 0x7ffff7fe7740 (LWP 44987) "test" main () at test.cc:32
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff6fd2700 (LWP 44996))]
#0 0x00007ffff7097e2d in nanosleep () from /lib64/libc.so.6
(gdb) bt
#0 0x00007ffff7097e2d in nanosleep () from /lib64/libc.so.6
#1 0x00007ffff7097cc4 in sleep () from /lib64/libc.so.6
#2 0x00007ffff796ceb9 in std::this_thread::__sleep_for(std::chrono::duration<long, std::ratio<1l, 1l> >, std::chrono::duration<long, std::ratio<1l, 1000000000l> >) () from /lib64/libstdc++.so.6
#3 0x00000000004018cc in std::this_thread::sleep_for<long, std::ratio<1l, 1l> > (__rtime=...) at /usr/include/c++/4.8.2/thread:281
#4 0x0000000000401307 in fun_int (n=1) at test.cc:9
#5 0x0000000000404696 in std::_Bind_simple<int (*(int))(int)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (this=0x609080)
at /usr/include/c++/4.8.2/functional:1732
#6 0x000000000040443d in std::_Bind_simple<int (*(int))(int)>::operator()() (this=0x609080) at /usr/include/c++/4.8.2/functional:1720
#7 0x000000000040436e in std::thread::_Impl<std::_Bind_simple<int (*(int))(int)> >::_M_run() (this=0x609068) at /usr/include/c++/4.8.2/thread:115
#8 0x00007ffff796d070 in ?? () from /lib64/libstdc++.so.6
#9 0x00007ffff7bc6dd5 in start_thread () from /lib64/libpthread.so.0
#10 0x00007ffff70d0ead in clone () from /lib64/libc.so.6
(gdb) c
Continuing.
after thread create
in fun_int n = 1
[Thread 0x7ffff6fd2700 (LWP 45234) exited]
in fun_string s = test
[Thread 0x7ffff67d1700 (LWP 45235) exited]
[Inferior 1 (process 45230) exited normally]
(gdb) q
在上述除錯程序中:
-
b 27 在第27行加上斷點
-
b test.cc:32 在第32行加上斷點(效果與b 32一致)
-
info b 輸出所有的斷點資訊
-
r 程式開始運行,并在第一個斷點處暫停
-
c 執行c命令,在第二個斷點處暫停,在第一個斷點和第二個斷點之間,創建了兩個執行緒t1和t2
-
info threads 輸出所有的執行緒資訊,從輸出上可以看出,總共有3個執行緒,分別為main執行緒、t1和t2
-
thread 2 切換至執行緒2
-
bt 輸出執行緒2的堆疊資訊
-
c 直至程式結束
-
q 退出gdb
多行程
同上面一樣,我們仍然以一個例子進行模擬多行程除錯,代碼如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == -1) {
perror("fork error\n");
return -1;
}
if(pid == 0) { // 子行程
int num = 1;
while(num == 1){
sleep(10);
}
printf("this is child,pid = %d\n", getpid());
} else { // 父行程
printf("this is parent,pid = %d\n", getpid());
wait(NULL); // 等待子行程退出
}
return 0;
}
在上面代碼中,包含兩個行程,一個是父行程(也就是main行程),另外一個是由fork()函式創建的子行程,
在默認情況下,在多行程程式中,GDB只除錯main行程,也就是說無論程式呼叫了多少次fork()函式創建了多少個子行程,GDB在默認情況下,只除錯父行程,為了支持多行程除錯,從GDB版本7.0開始支持單獨除錯(除錯父行程或者子行程)和同時除錯多個行程,
那么,我們該如何除錯子行程呢?我們可以使用如下幾種方式進行子行程除錯,
attach
首先,無論是父行程還是子行程,都可以通過attach命令啟動gdb進行除錯,我們都知道,對于每個正在運行的程式,作業系統都會為其分配一個唯一ID號,也就是行程ID,如果我們知道了行程ID,就可以使用attach命令對其進行除錯了,
在上面代碼中,fork()函式創建的子行程內部,首先會進入while回圈sleep,然后在while回圈之后呼叫printf函式,這樣做的目的有如下:
- 幫助attach捕獲要除錯的行程id
- 在使用gdb進行除錯的時候,真正的代碼(即print函式)沒有被執行,這樣就可以從頭開始對子行程進行除錯
可能會有疑惑,上面代碼以及進入while回圈,無論如何是不會執行到下面printf函式,其實,這就是gdb的厲害之處,可以通過gdb命令修改num的值,以便其跳出while回圈
使用如下命令編譯生成可執行檔案test_process
g++ -g test_process.cc -o test_process
現在,我們開始嘗試啟動除錯,
gdb -q ./test_process
Reading symbols from /root/test_process...done.
(gdb)
這里需要說明下,之所以加-q選項,是想去掉其他不必要的輸出,q為quite的縮寫,
(gdb) r
Starting program: /root/./test_process
Detaching after fork from child process 37482.
this is parent,pid = 37478
[Inferior 1 (process 37478) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libstdc++-4.8.5-36.el7.x86_64
(gdb) attach 37482
//符號類輸出,此處略去
(gdb) n
Single stepping until exit from function __nanosleep_nocancel,
which has no line number information.
0x00007ffff72b3cc4 in sleep () from /lib64/libc.so.6
(gdb)
Single stepping until exit from function sleep,
which has no line number information.
main () at test_process.cc:8
8 while(num==10){
(gdb)
在上述命令中,我們執行了n(next的縮寫),使其重新對while回圈的判斷體進行判斷,
(gdb) set num = 1
(gdb) n
12 printf("this is child,pid = %d\n",getpid());
(gdb) c
Continuing.
this is child,pid = 37482
[Inferior 1 (process 37482) exited normally]
(gdb)
為了退出while回圈,我們使用set命令設定了num的值為1,這樣條件就會失效退出while回圈,進而執行下面的printf()函式;在最后我們執行了c(continue的縮寫)命令,支持程式退出,
如果程式正在正常運行,出現了死鎖等現象,則可以通過ps獲取行程ID,然后根據gdb attach pid進行系結,進而查看堆疊資訊
指定行程
默認情況下,GDB除錯多行程程式時候,只除錯父行程,GDB提供了兩個命令,可以通過follow-fork-mode和detach-on-fork來指定除錯父行程還是子行程,
follow-fork-mode
該命令的使用方式為:
(gdb) set follow-fork-mode mode
其中,mode有以下兩個選項:
- parent:父行程,mode的默認選項
- child:子行程,其目的是告訴 gdb 在目標應用呼叫fork之后接著除錯子行程而不是父行程,因為在Linux系統中fork()系統呼叫成功會回傳兩次,一次在父行程,一次在子行程
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "parent".
(gdb) set follow-fork-mode child
(gdb) r
Starting program: /root/./test_process
[New process 37830]
this is parent,pid = 37826
^C
Program received signal SIGINT, Interrupt.
[Switching to process 37830]
0x00007ffff72b3e10 in __nanosleep_nocancel () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libstdc++-4.8.5-36.el7.x86_64
(gdb) n
Single stepping until exit from function __nanosleep_nocancel,
which has no line number information.
0x00007ffff72b3cc4 in sleep () from /lib64/libc.so.6
(gdb) n
Single stepping until exit from function sleep,
which has no line number information.
main () at test_process.cc:8
8 while(num==10){
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "child".
(gdb)
在上述命令中,我們做了如下操作:
- show follow-fork-mode:通過該命令來查看當前處于什么模式下,通過輸出可以看出,處于parent即父行程模式
- set follow-fork-mode child:指定除錯子行程模式
- r:運行程式,直接運行程式,此時會進入子行程,然后執行while回圈
- ctrl + c:通過該命令,可以使得GDB收到SIGINT命令,從而暫停執行while回圈
- n(next):繼續執行,進而進入到while回圈的條件判斷處
- show follow-fork-mode:再次執行該命令,通過輸出可以看出,當前處于child模式下
detach-on-fork
如果一開始指定要除錯子行程還是父行程,那么使用follow-fork-mode命令完全可以滿足需求;但是如果想在除錯程序中,想根據實際情況在父行程和子行程之間來回切換除錯呢?
GDB提供了另外一個命令:
(gdb) set detach-on-fork mode
其中mode有如下兩個值:
on:默認值,即表明只除錯一個行程,可以是子行程,也可以是父行程
off:程式中的每個行程都會被記錄,進而我們可以對所有的行程進行除錯
如果選擇關閉detach-on-fork模式(mode為off),那么GDB將保留對所有被fork出來的行程控制,即可用除錯所有被fork出來的行程,可用 使用info forks命令列出所有的可被GDB除錯的fork行程,并可用使用fork命令從一個fork行程切換到另一個fork行程,
- info forks: 列印DGB控制下的所有被fork出來的行程串列,該串列包括fork id、行程id和當前行程的位置
- fork fork-id: 引數fork-id是GDB分配的內部fork編號,該編號可用通過上面的命令
info forks獲取
coredump
當我們開發或者使用一個程式時候,最怕的莫過于程式莫名其妙崩潰,為了分析崩潰產生的原因,作業系統的記憶體內容(包括程式崩潰時候的堆疊等資訊)會在程式崩潰的時候dump出來(默認情況下,這個檔案名為core.pid,其中pid為行程id),這個dump操作叫做coredump(核心轉儲),然后我們可以用除錯器除錯此檔案,以還原程式崩潰時候的場景,
在我們分析如果用gdb除錯coredump檔案之前,先需要生成一個coredump,為了簡單起見,我們就用如下例子來生成:
#include <stdio.h>
void print(int *v, int size) {
for (int i = 0; i < size; ++i) {
printf("elem[%d] = %d\n", i, v[i]);
}
}
int main() {
int v[] = {0, 1, 2, 3, 4};
print(v, 1000);
return 0;
}
編譯并運行該程式:
g++ -g test_core.cc -o test_core
./test_core
輸出如下:
elem[775] = 1702113070
elem[776] = 1667200115
elem[777] = 6648431
elem[778] = 0
elem[779] = 0
段錯誤(吐核)
如我們預期,程式產生了例外,但是卻沒有生成coredump檔案,這是因為在系統默認情況下,coredump生成是關閉的,所以需要設定對應的選項以打開coredump生成,
針對多執行緒程式產生的coredump,有時候其堆疊資訊并不能完整的去分析原因,這就使得我們得有其他方式,
18年有一次線上故障,在測驗環境一切正常,但是在線上的時候,就會coredump,根據gdb除錯coredump,只能定位到了libcurl里面,但卻定位不出原因,用了大概兩天的時間,發現只有在超時的時候,才會coredump,而測驗環境因為配置比較差超時設定的是20ms,而線上是5ms,知道coredump原因后,采用逐步定位縮小范圍法,逐步縮小代碼范圍,最終定位到是libcurl一個bug導致,所以,很多時候,定位線上問題需要結合實際情況,采取合適的方法來定位問題,
配置
配置coredump生成,有臨時配置(退出終端后,配置失效)和永久配置兩種,
臨時
通過ulimit -a可以判斷當前有沒有配置coredump生成:
ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
從上面輸出可以看出core file size后面的數為0,即不生成coredump檔案,我們可以通過如下命令進行設定
ulimit -c size
其中size為允許生成的coredump大小,這個一般盡量設定大點,以防止生成的coredump資訊不全,筆者一般設定為不限,
ulimit -c unlimited
需要說明的是,臨時配置的coredump選項,其默認生成路徑為執行該命令時候的路徑,可以通過修改配置來進行路徑修改,
永久
上面的設定只是使能了core dump功能,預設情況下,內核在coredump時所產生的core檔案放在與該程式相同的目錄中,并且檔案名固定為core,很顯然,如果有多個程式產生core檔案,或者同一個程式多次崩潰,就會重復覆寫同一個core檔案,
過修改kernel的引數,可以指定內核所生成的coredump檔案的檔案名,使用下面命令,可以實作coredump永久配置、存放路徑以及生成coredump名稱等,
mkdir -p /www/coredump/
chmod 777 /www/coredump/
/etc/profile
ulimit -c unlimited
/etc/security/limits.conf
* soft core unlimited
echo "/www/coredump/core-%e-%p-%h-%t" > /proc/sys/kernel/core_pattern
除錯
現在,我們重新執行如下命令,按照預期產生coredump檔案:
./test_coredump
elem[955] = 1702113070
elem[956] = 1667200115
elem[957] = 6648431
elem[958] = 0
elem[959] = 0
段錯誤(吐核)
然后使用下面的命令進行coredump除錯:
gdb ./test_core -c /www/coredump/core_test_core_1640765384_38924 -q
輸出如下:
#0 0x0000000000400569 in print (v=0x7fff3293c100, size=1000) at test_core.cc:5
5 printf("elem[%d] = %d\n", i, v[i]);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libstdc++-4.8.5-36.el7.x86_64
(gdb)
可以看出,程式core在了第5行,此時,我們可以通過where命令來查看堆疊回溯資訊,
在gdb中輸入where命令,可以獲取堆疊呼叫資訊,當進行coredump除錯時候,這個是最基本且最有用處的命令,where命令輸出的結果包含程式中 的函式名稱和相關引數值,
通過where命令,我們能夠發現程式core在了第5行,那么根據分析原始碼基本就能定位原因,
需要注意的是,在多執行緒運行的時候,core不一定在當前執行緒,這就需要我們對代碼有一定的了解,能夠保證哪塊代碼是安全的,然后通過thread num切換執行緒,然后再通過bt或者where命令查看堆疊資訊,進而定位coredump原因,
原理
在前面幾節,我們講了gdb的命令,以及這些命令在除錯時候的作用,并以例子進行了演示,作為C/C++ coder,要知其然,更要知其所以然,所以,借助本節,我們大概講下GDB除錯的原理,
gdb 通過系統呼叫 ptrace 來接管一個行程的執行,ptrace 系統呼叫提供了一種方法使得父行程可以觀察和控制其它行程的執行,檢查和改變其核心映像以及暫存器,它主要用來實作斷點除錯和系統呼叫跟蹤,

ptrace系統呼叫定義如下:
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)
- pid_t pid:指示 ptrace 要跟蹤的行程
- void *addr:指示要監控的記憶體地址
- enum __ptrace_request request:決定了系統呼叫的功能,幾個主要的選項:
- PTRACE_TRACEME:表示此行程將被父行程跟蹤,任何信號(除了
SIGKILL)都會暫停子行程,接著阻塞于wait()等待的父行程被喚醒,子行程內部對exec()的呼叫將發出SIGTRAP信號,這可以讓父行程在子行程新程式開始運行之前就完全控制它 - PTRACE_ATTACH:attach 到一個指定的行程,使其成為當前行程跟蹤的子行程,而子行程的行為等同于它進行了一次 PTRACE_TRACEME 操作,但需要注意的是,雖然當前行程成為被跟蹤行程的父行程,但是子行程使用
getppid()的到的仍將是其原始父行程的pid - PTRACE_CONT:繼續運行之前停止的子行程,可同時向子行程交付指定的信號
- PTRACE_TRACEME:表示此行程將被父行程跟蹤,任何信號(除了
除錯原理
運行并除錯新行程
運行并除錯新行程,步驟如下:
- 運行gdb exe
- 輸入run命令,gdb執行以下操作:
- 通過fork()系統呼叫創建一個新行程
- 在新創建的子行程中執行ptrace(PTRACE_TRACEME, 0, 0, 0)操作
- 在子行程中通過execv()系統呼叫加載指定的可執行檔案
attach運行的行程
可以通過gdb attach pid來除錯一個運行的行程,gdb將對指定行程執行ptrace(PTRACE_ATTACH, pid, 0, 0)操作,
需要注意的是,當我們attach一個行程id時候,可能會報如下錯誤:
Attaching to process 28849
ptrace: Operation not permitted.
這是因為沒有權限進行操作,可以根據啟動該行程用戶下或者root下進行操作,
斷點原理
實作原理
當我們通過b或者break設定斷點時候,就是在指定位置插入斷點指令,當被除錯的程式運行到斷點的時候,產生SIGTRAP信號,該信號被gdb捕獲并 進行斷點命中判斷,
設定原理
在程式中設定斷點,就是先在該位置保存原指令,然后在該位置寫入int 3,當執行到int 3時,發生軟中斷,內核會向子行程發送SIGTRAP信號,當然,這個信號會轉發給父行程,然后用保存的指令替換int 3并等待操作恢復,
命中判斷
gdb將所有斷點位置存盤在一個鏈表中,命中判定將被除錯程式的當前停止位置與鏈表中的斷點位置進行比較,以查看斷點產生的信號,
條件判斷
在斷點處恢復指令后,增加了一個條件判斷,如果運算式為真,則觸發斷點,由于需要判斷一次,添加條件斷點后,是否觸發條件斷點,都會影響性能,在 x86 平臺上,部分硬體支持硬體斷點,不是在條件斷點處插入 int 3,而是插入另一條指令,當程式到達這個地址時,不是發出int 3信號,而是進行比較,特定暫存器的內容和某個地址,然后決定是否發送int 3,因此,當你的斷點位置被程式頻繁“通過”時,盡量使用硬體斷點,這將有助于提高性能,
單步原理
這個ptrace函式本身就支持,可以通過ptrace(PTRACE_SINGLESTEP, pid,…)呼叫來實作單步,
printf("attaching to PID %d\n", pid);
if (ptrace(PTRACE_ATTACH, pid, 0, 0) != 0)
{
perror("attach failed");
}
int waitStat = 0;
int waitRes = waitpid(pid, &waitStat, WUNTRACED);
if (waitRes != pid || !WIFSTOPPED(waitStat))
{
printf("unexpected waitpid result!\n");
exit(1);
}
int64_t numSteps = 0;
while (true) {
auto res = ptrace(PTRACE_SINGLESTEP, pid, 0, 0);
}
上述代碼,首先接收一個pid,然后對其進行attach,最后呼叫ptrace進行單步除錯,
其它
借助本文,簡單介紹下筆者作業程序中使用的一些其他命令或者工具,
pstack
此命令可顯示每個行程的堆疊跟蹤,pstack 命令必須由相應行程的屬主或 root 運行,可以使用 pstack 來確定行程掛起的位置,此命令允許使用的唯一選項是要檢查的行程的 PID,
這個命令在排查行程問題時非常有用,比如我們發現一個服務一直處于work狀態(如假死狀態,好似死回圈),使用這個命令就能輕松定位問題所在;可以在一段時間內,多執行幾次pstack,若發現代碼堆疊總是停在同一個位置,那個位置就需要重點關注,很可能就是出問題的地方;
以前面的多執行緒代碼為例,其行程ID是4507(在筆者本地),那么通過
pstack 4507輸出結果如下:
Thread 3 (Thread 0x7f07aaa69700 (LWP 45708)):
#0 0x00007f07aab2ee2d in nanosleep () from /lib64/libc.so.6
#1 0x00007f07aab2ecc4 in sleep () from /lib64/libc.so.6
#2 0x00007f07ab403eb9 in std::this_thread::__sleep_for(std::chrono::duration<long, std::ratio<1l, 1l> >, std::chrono::duration<long, std::ratio<1l, 1000000000l> >) () from /lib64/libstdc++.so.6
#3 0x00000000004018cc in void std::this_thread::sleep_for<long, std::ratio<1l, 1l> >(std::chrono::duration<long, std::ratio<1l, 1l> > const&) ()
#4 0x00000000004012de in fun_int(int) ()
#5 0x0000000000404696 in int std::_Bind_simple<int (*(int))(int)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) ()
#6 0x000000000040443d in std::_Bind_simple<int (*(int))(int)>::operator()() ()
#7 0x000000000040436e in std::thread::_Impl<std::_Bind_simple<int (*(int))(int)> >::_M_run() ()
#8 0x00007f07ab404070 in ?? () from /lib64/libstdc++.so.6
#9 0x00007f07ab65ddd5 in start_thread () from /lib64/libpthread.so.0
#10 0x00007f07aab67ead in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f07aa268700 (LWP 45709)):
#0 0x00007f07aab2ee2d in nanosleep () from /lib64/libc.so.6
#1 0x00007f07aab2ecc4 in sleep () from /lib64/libc.so.6
#2 0x00007f07ab403eb9 in std::this_thread::__sleep_for(std::chrono::duration<long, std::ratio<1l, 1l> >, std::chrono::duration<long, std::ratio<1l, 1000000000l> >) () from /lib64/libstdc++.so.6
#3 0x00000000004018cc in void std::this_thread::sleep_for<long, std::ratio<1l, 1l> >(std::chrono::duration<long, std::ratio<1l, 1l> > const&) ()
#4 0x0000000000401340 in fun_string(std::string const&) ()
#5 0x000000000040459f in int std::_Bind_simple<int (*(char const*))(std::string const&)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) ()
#6 0x000000000040441f in std::_Bind_simple<int (*(char const*))(std::string const&)>::operator()() ()
#7 0x0000000000404350 in std::thread::_Impl<std::_Bind_simple<int (*(char const*))(std::string const&)> >::_M_run() ()
#8 0x00007f07ab404070 in ?? () from /lib64/libstdc++.so.6
#9 0x00007f07ab65ddd5 in start_thread () from /lib64/libpthread.so.0
#10 0x00007f07aab67ead in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f07aba80740 (LWP 45707)):
#0 0x00007f07ab65ef47 in pthread_join () from /lib64/libpthread.so.0
#1 0x00007f07ab403e37 in std::thread::join() () from /lib64/libstdc++.so.6
#2 0x0000000000401455 in main ()
在上述輸出結果中,將行程內部的詳細資訊都輸出在終端,以方便分析問題,
ldd
在我們編譯程序中通常會提示編譯失敗,通過輸出錯誤資訊發現是找不到函式定義,再或者編譯成功了,但是運行時候失敗(往往是因為依賴了非正常版本的lib庫導致),這個時候,我們就可以通過ldd來分析該可執行檔案依賴了哪些庫以及這些庫所在的路徑,
用來查看程式運行所需的共享庫,常用來解決程式因缺少某個庫檔案而不能運行的一些問題,
仍然查看可執行程式test_thread的依賴庫,輸出如下:
ldd -r ./test_thread
linux-vdso.so.1 => (0x00007ffde43bc000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f8c5e310000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f8c5e009000)
libm.so.6 => /lib64/libm.so.6 (0x00007f8c5dd07000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f8c5daf1000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8c5d724000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8c5e52c000)
在上述輸出中:
- 第一列:程式需要依賴什么庫
- 第二列:系統提供的與程式需要的庫所對應的庫
- 第三列:庫加載的開始地址
在有時候,我們通過ldd查看依賴庫的時候,會提示找不到庫,如下:
ldd -r test_process
linux-vdso.so.1 => (0x00007ffc71b80000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fe4badd5000)
libm.so.6 => /lib64/libm.so.6 (0x00007fe4baad3000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fe4ba8bd000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe4ba4f0000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe4bb0dc000)
liba.so => not found
比如上面最后一句提示,liba.so找不到,這個時候,需要我們知道liba.so的路徑,比如在/path/to/liba.so,那么可以有下面兩種方式:
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/
這樣在通過ldd查看,就能找到對應的lib庫,但是這個缺點是臨時的,即退出終端后,再執行ldd,仍然會提示找不到該庫,所以就有了另外一種方式,即通過修改/etc/ld.so.conf,在該檔案的后面加上需要的路徑,即
include ld.so.conf.d/*.conf
/path/to/
然后通過如下命令,即可永久生效
/sbin/ldconfig
c++filter
因為c++支持多載,也就引出了編譯器的name mangling機制,對函式進行重命名,
我們通過strings命令查看test_thread中的函式資訊(僅輸出fun等相關)
strings test_thread | grep fun_
in fun_int n =
in fun_string s =
_GLOBAL__sub_I__Z7fun_inti
_Z10fun_stringRKSs
可以看到_Z10fun_stringRKSs這個函式,如果想知道這個函式定義的話,可以使用c++filt命令,如下:
c++filt _Z10fun_stringRKSs
fun_string(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
通過上述輸出,我們可以將編譯器生成的函式名還原到我們代碼中的函式名即fun_string,
結語
GDB是一個在Linux上進行開發的一個必不可少的除錯工具,使用場景依賴于具體的需求或者遇到的具體問題,在我們的日常開發作業中,熟練使用GDB加以輔助,能夠使得開發程序事半功倍,
本文從一些簡單的命令出發,通過舉例除錯可執行程式(單執行緒、多執行緒以及多行程場景)、coredump檔案等各個場景,使得大家能夠更加直觀的了解GDB的使用,GDB功能非常強大,筆者作業中使用的都是非常基本的一些功能,如果想深入理解GDB,則需要去官網進行閱讀了解,
本文從構思到完成,大概用了三周時間,寫作程序是痛苦的(需要整理資料以及構建各種場景,以及將各種現場還原),同時又是識訓滿滿的,通過本文,進一步加深了對GDB的底層原理理解,
參考
(https://www.codetd.com/en/article/13107993)
https://users.ece.utexas.edu/~adnan/gdb-refcard.pdf
https://www.cloudsavvyit.com/10921/debugging-with-gdb-getting-started/
https://blog.birost.com/a?ID=00650-b03e2257-94bf-41f3-b0fc-d352d5b02431
https://www.cnblogs.com/xsln/p/ptrace.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/404133.html
標籤:其他
