GDB調(diào)試-從入門實踐到原理
你好,我是雨樂!
在上篇文章中,我們分析了線上coredump產(chǎn)生的原因,其中用到了coredump分析工具gdb,這幾天一直有讀者在問,能不能寫一篇關(guān)于gdb調(diào)試方面的文章,今天借助此文,分享一些工作中的調(diào)試經(jīng)驗,希望能夠幫到大家。
寫在前面
在我的工作經(jīng)歷中,前幾年在Windows上進行開發(fā),使用Visual Studio進行調(diào)試,簡直是利器,各種斷點等用鼠標點點點就能設(shè)置;大概從12年開始轉(zhuǎn)Linux開發(fā)了,所以調(diào)試都是基于GDB的。本來這篇文章也想寫寫Windows下調(diào)試相關(guān),奈何好多年沒用了,再加上工作太忙,所以本文就只寫了Linux下GDB調(diào)試相關(guān),對于Windows開發(fā)人員,實在對不住了??。
這篇文章,涉及的比較全面,總結(jié)了這些年的gdb調(diào)試經(jīng)驗(都是小兒科??),經(jīng)常用到的一些調(diào)試技巧,希望能夠?qū)氖翷inux開發(fā)的相關(guān)人員有所幫助

背景
作為C/C++開發(fā)人員,保證程序正常運行是最基本也是最主要的目的。而為了保證程序正常運行,調(diào)試則是最基本的手段,熟悉這些調(diào)試方式,可以方便我們更快的定位程序問題所在,提高開發(fā)效率。
在開發(fā)過程,如果程序的運行結(jié)果不符合預(yù)期,第一時間就是打開GDB進行調(diào)試,在對應(yīng)的地方設(shè)置斷點,然后分析原因;當線上服務(wù)出了問題,第一時間查看進程在不在,如果不在的話,是否生成了coredump文件,如果有,則使用gdb調(diào)試coredump文件,否則通過dmesg來分析內(nèi)核日志來查找原因。
概念
GDB是一個由GNU開源組織發(fā)布的、UNIX/LINUX操作系統(tǒng)下的、「基于命令行的、功能強大的程序調(diào)試工具」。
GDB支持斷點、單步執(zhí)行、打印變量、觀察變量、查看寄存器、查看堆棧等調(diào)試手段。在Linux環(huán)境軟件開發(fā)中,GDB是主要的調(diào)試工具,用來調(diào)試C和 C++程序(也支持go等其他語言)。
常用命令
斷點
斷點是我們在調(diào)試中經(jīng)常用的一個功能,我們在指定位置設(shè)置斷點之后,程序運行到該位置將會暫停,這個時候我們就可以對程序進行更多的操作,比如查看變量內(nèi)容,堆棧情況等等,以幫助我們調(diào)試程序。
以設(shè)置斷點的命令分為以下幾類:
breakpoint watchpoint catchpoint
breakpoint
可以根據(jù)行號、函數(shù)、條件生成斷點,下面是相關(guān)命令以及對應(yīng)的作用說明:
| 命令 | 作用 |
|---|---|
| break [file]:function | 在文件file的function函數(shù)入口設(shè)置斷點 |
| break [file]:line | 在文件file的第line行設(shè)置斷點 |
| info breakpoints | 查看斷點列表 |
| break [+-]offset | 在當前位置偏移量為[+-]offset處設(shè)置斷點 |
| break *addr | 在地址addr處設(shè)置斷點 |
| break ... if expr | 設(shè)置條件斷點,僅僅在條件滿足時 |
| ignore n count | 接下來對于編號為n的斷點忽略count次 |
| clear | 刪除所有斷點 |
| clear function | 刪除所有位于function內(nèi)的斷點 |
| delete n | 刪除指定編號的斷點 |
| enable n | 啟用指定編號的斷點 |
| disable n | 禁用指定編號的斷點 |
| save breakpoints file | 保存斷點信息到指定文件 |
| source file | 導(dǎo)入文件中保存的斷點信息 |
| break | 在下一個指令處設(shè)置斷點 |
| clear [file:]line | 刪除第line行的斷點 |
watchpoint
watchpoint是一種特殊類型的斷點,類似于正常斷點,是要求GDB暫停程序執(zhí)行的命令。區(qū)別在于watchpoint沒有駐留某一行源代碼中,而是指示GDB每當某個表達式改變了值就暫停執(zhí)行的命令。
watchpoint分為硬件實現(xiàn)和軟件實現(xiàn)兩種。前者需要硬件系統(tǒng)的支持;后者的原理就是每步執(zhí)行后都檢查變量的值是否改變。GDB在新建數(shù)據(jù)斷點時會優(yōu)先嘗試硬件方式,如果失敗再嘗試軟件實現(xiàn)。
| 命令 | 作用 |
|---|---|
| watch variable | 設(shè)置變量數(shù)據(jù)斷點 |
| watch var1 + var2 | 設(shè)置表達式數(shù)據(jù)斷點 |
| rwatch variable | 設(shè)置讀斷點,僅支持硬件實現(xiàn) |
| awatch variable | 設(shè)置讀寫斷點,僅支持硬件實現(xiàn) |
| info watchpoints | 查看數(shù)據(jù)斷點列表 |
| set can-use-hw-watchpoints 0 | 強制基于軟件方式實現(xiàn) |
使用數(shù)據(jù)斷點時,需要注意:
當監(jiān)控變量為局部變量時,一旦局部變量失效,數(shù)據(jù)斷點也會失效 如果監(jiān)控的是指針變量 p,則watch *p監(jiān)控的是p所指內(nèi)存數(shù)據(jù)的變化情況,而watch p監(jiān)控的是p指針本身有沒有改變指向
最常見的數(shù)據(jù)斷點應(yīng)用場景:「定位堆上的結(jié)構(gòu)體內(nèi)部成員何時被修改」。由于指針一般為局部變量,為了解決斷點失效,一般有兩種方法。
| 命令 | 作用 |
|---|---|
| print &variable | 查看變量的內(nèi)存地址 |
| watch *(type *)address | 通過內(nèi)存地址間接設(shè)置斷點 |
| watch -l variable | 指定location參數(shù) |
| watch variable thread 1 | 僅編號為1的線程修改變量var值時會中斷 |
catchpoint
從字面意思理解,是捕獲斷點,其主要監(jiān)測信號的產(chǎn)生。例如c++的throw,或者加載庫的時候,產(chǎn)生斷點行為。
| 命令 | 含義 |
|---|---|
| catch fork | 程序調(diào)用fork時中斷 |
| tcatch fork | 設(shè)置的斷點只觸發(fā)一次,之后被自動刪除 |
| catch syscall ptrace | 為ptrace系統(tǒng)調(diào)用設(shè)置斷點 |
?在
?command命令后加斷點編號,可以定義斷點觸發(fā)后想要執(zhí)行的操作。在一些高級的自動化調(diào)試場景中可能會用到。
命令行
| 命令 | 作用 |
|---|---|
| run arglist | 以arglist為參數(shù)列表運行程序 |
| set args arglist | 指定啟動命令行參數(shù) |
| set args | 指定空的參數(shù)列表 |
| show args | 打印命令行列表 |
程序棧
| 命令 | 作用 |
|---|---|
| backtrace [n] | 打印棧幀 |
| frame [n] | 選擇第n個棧幀,如果不存在,則打印當前棧幀 |
| up n | 選擇當前棧幀編號+n的棧幀 |
| down n | 選擇當前棧幀編號-n的棧幀 |
| info frame [addr] | 描述當前選擇的棧幀 |
| info args | 當前棧幀的參數(shù)列表 |
| info locals | 當前棧幀的局部變量 |
多進程、多線程
多進程
GDB在調(diào)試多進程程序(程序含fork調(diào)用)時,默認只追蹤父進程。可以通過命令設(shè)置,實現(xiàn)只追蹤父進程或子進程,或者同時調(diào)試父進程和子進程。
| 命令 | 作用 |
|---|---|
| info inferiors | 查看進程列表 |
| attach pid | 綁定進程id |
| inferior num | 切換到指定進程上進行調(diào)試 |
| print $_exitcode | 顯示程序退出時的返回值 |
| set follow-fork-mode child | 追蹤子進程 |
| set follow-fork-mode parent | 追蹤父進程 |
| set detach-on-fork on | fork調(diào)用時只追蹤其中一個進程 |
| set detach-on-fork off | fork調(diào)用時會同時追蹤父子進程 |
在調(diào)試多進程程序時候,默認情況下,除了當前調(diào)試的進程,其他進程都處于掛起狀態(tài),所以,如果需要在調(diào)試當前進程的時候,其他進程也能正常執(zhí)行,那么通過設(shè)置set schedule-multiple on即可。
多線程
多線程開發(fā)在日常開發(fā)工作中很常見,所以多線程的調(diào)試技巧非常有必要掌握。
默認調(diào)試多線程時,一旦程序中斷,所有線程都將暫停。如果此時再繼續(xù)執(zhí)行當前線程,其他線程也會同時執(zhí)行。
| 命令 | 作用 |
|---|---|
| info threads | 查看線程列表 |
| print $_thread | 顯示當前正在調(diào)試的線程編號 |
| set scheduler-locking on | 調(diào)試一個線程時,其他線程暫停執(zhí)行 |
| set scheduler-locking off | 調(diào)試一個線程時,其他線程同步執(zhí)行 |
| set scheduler-locking step | 僅用step調(diào)試線程時其他線程不執(zhí)行,用其他命令如next調(diào)試時仍執(zhí)行 |
如果只關(guān)心當前線程,建議臨時設(shè)置 scheduler-locking 為 on,避免其他線程同時運行,導(dǎo)致命中其他斷點分散注意力。
打印輸出
通常情況下,在調(diào)試的過程中,我們需要查看某個變量的值,以分析其是否符合預(yù)期,這個時候就需要打印輸出變量值。
| 命令 | 作用 |
|---|---|
| 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 | 打印不限制字符串長度/或不限制數(shù)組長度 |
| call printf("%s\n",xxx) | 這時打印出的字符串不會含有多余的轉(zhuǎn)義符 |
| printf "%s\n",xxx | 同上 |
打印數(shù)組
| 命令 | 作用 |
|---|---|
| print *array@10 | 打印從數(shù)組開頭連續(xù)10個元素的值 |
| print array[60]@10 | 打印array數(shù)組下標從60開始的10個元素,即第60~69個元素 |
| set print array-indexes on | 打印數(shù)組元素時,同時打印數(shù)組的下標 |
打印指針
| 命令 | 作用 |
|---|---|
| print ptr | 查看該指針指向的類型及指針地址 |
| print *(struct xxx *)ptr | 查看指向的結(jié)構(gòu)體的內(nèi)容 |
打印指定內(nèi)存地址的值
使用x命令來打印內(nèi)存的值,格式為x/nfu addr,以f格式打印從addr開始的n個長度單元為u的內(nèi)存值。
n:輸出單元的個數(shù)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進制打印數(shù)組array的前8個byte的值 |
| x/8xw array | 以16進制打印數(shù)組array的前16個word的值 |
打印局部變量
| 命令 | 作用 |
|---|---|
| info locals | 打印當前函數(shù)局部變量的值 |
| backtrace full | 打印當前棧幀各個函數(shù)的局部變量值,命令可縮寫為bt |
| bt full n | 從內(nèi)到外顯示n個棧幀及其局部變量 |
| bt full -n | 從外向內(nèi)顯示n個棧幀及其局部變量 |
打印結(jié)構(gòu)體
| 命令 | 作用 |
|---|---|
| set print pretty on | 每行只顯示結(jié)構(gòu)體的一名成員 |
| set print null-stop | 不顯示'\000' |
函數(shù)跳轉(zhuǎn)
| 命令 | 作用 |
|---|---|
| set step-mode on | 不跳過不含調(diào)試信息的函數(shù),可以顯示和調(diào)試匯編代碼 |
| finish | 執(zhí)行完當前函數(shù)并打印返回值,然后觸發(fā)中斷 |
| return 0 | 不再執(zhí)行后面的指令,直接返回,可以指定返回值 |
| call printf("%s\n", str) | 調(diào)用printf函數(shù),打印字符串(可以使用call或者print調(diào)用函數(shù)) |
| print func() | 調(diào)用func函數(shù)(可以使用call或者print調(diào)用函數(shù)) |
| set var variable=xxx | 設(shè)置變量variable的值為xxx |
| set {type}address = xxx | 給存儲地址為address,類型為type的變量賦值 |
| info frame | 顯示函數(shù)堆棧的信息(堆棧幀地址、指令寄存器的值等) |
其它
圖形化
tui為terminal user interface的縮寫,在啟動時候指定-tui參數(shù),或者調(diào)試時使用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 | 查看函數(shù)的匯編代碼 |
| disassemble /mr function | 同時比較函數(shù)源代碼和匯編代碼 |
調(diào)試和保存core文件
| 命令 | 含義 |
|---|---|
| file exec_file ?*# * | 加載可執(zhí)行文件的符號表信息 |
| core core_file | 加載core-dump文件 |
| gcore core_file | 生成core-dump文件,記錄當前進程的狀態(tài) |
啟動方式
使用gdb調(diào)試,一般有以下幾種啟動方式:
gdb filename: 調(diào)試可執(zhí)行程序 gdb attach pid: 通過”綁定“進程ID來調(diào)試正在運行的進程 gdb filename -c coredump_file: 調(diào)試可執(zhí)行文件
在下面的幾節(jié)中,將分別對上述幾種調(diào)試方式進行講解,從例子的角度出發(fā),使得大家能夠更好的掌握調(diào)試技巧。
調(diào)試
可執(zhí)行文件
單線程
首先,我們先看一段代碼:
#include
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;
}
這個代碼比較簡單,下面我們開始進入調(diào)試:
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?
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:
...
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命令啟動調(diào)試,然后通過執(zhí)行r(run命令的縮寫)執(zhí)行程序,直至退出,換句話說,上述命令是一個完整的使用gdb運行可執(zhí)行程序的完整過程(只使用了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函數(shù)。
(gdb)?step
print?(xx=10,?xxptr=0x7fffffffe424)?at?test_main.cc:4
4???printf("In?print():\n");
(gdb)
接著,我們執(zhí)行了step命令,即進入函數(shù)內(nèi)。下面我們繼續(xù)通過backtrace命令來查看棧幀信息。
(gdb)?backtrace
#0??print?(xx=10,?xxptr=0x7fffffffe424)?at?test_main.cc:4
#1??0x0000000000400612?in?main?()?at?test_main.cc:15
(gdb)
從上面輸出結(jié)果,我們能夠看出,有兩個棧幀,第1幀屬于main函數(shù),第0幀屬于print函數(shù)。
每個棧幀都列出了該函數(shù)的參數(shù)列表。從上面我們可以看出,main函數(shù)沒有參數(shù),而print函數(shù)有參數(shù),并且顯示了其參數(shù)的值。
有一點我們可能比較迷惑,在第一次執(zhí)行backtrace的時候,main函數(shù)所在的棧幀編號為0,而第二次執(zhí)行的時候,main函數(shù)的棧幀為1,而print函數(shù)的棧幀為0,這是因為_與棧的向下增長_規(guī)律一致,我們只需要記住_編號最小幀號就是最近一次調(diào)用的函數(shù)_。
frame
棧幀用來存儲函數(shù)的變量值等信息,默認情況下,GDB總是位于當前正在執(zhí)行函數(shù)對應(yīng)棧幀的上下文中。
在前面的例子中,由于當前正在print()函數(shù)中執(zhí)行,GDB位于第0幀的上下文中。可以通過frame命令來獲取當前正在執(zhí)行的上下文所在的幀。
(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)
如果我們想看其他棧幀的內(nèi)容呢?比如main函數(shù)中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)
多線程
為了方便進行演示,我們創(chuàng)建一個簡單的例子,代碼如下:
#include?
#include?
#include?
#include?
#include?
int?fun_int(int?n)?{
??std::this_thread::sleep_for(std::chrono::seconds(10));
??std::cout?<"in?fun_int?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?=?"?<std::endl;
??
??return?0;
}
int?main()?{
??std::vector<int>?v;
??v.emplace_back(1);
??v.emplace_back(2);
??v.emplace_back(3);
??std::cout?<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;
}
上述代碼比較簡單:
函數(shù)fun_int的功能是休眠10s,然后打印其參數(shù) 函數(shù)fun_string功能是休眠10s,然后打印其參數(shù) main函數(shù)中,創(chuàng)建兩個線程,分別執(zhí)行上述兩個函數(shù)
下面是一個完整的調(diào)試過程:
(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
在上述調(diào)試過程中:
b 27 在第27行加上斷點
b test.cc:32 在第32行加上斷點(效果與b 32一致)
info b 輸出所有的斷點信息
r 程序開始運行,并在第一個斷點處暫停
c 執(zhí)行c命令,在第二個斷點處暫停,在第一個斷點和第二個斷點之間,創(chuàng)建了兩個線程t1和t2
info threads 輸出所有的線程信息,從輸出上可以看出,總共有3個線程,分別為main線程、t1和t2
thread 2 切換至線程2
bt 輸出線程2的堆棧信息
c 直至程序結(jié)束
q 退出gdb
多進程
同上面一樣,我們?nèi)匀灰砸粋€例子進行模擬多進程調(diào)試,代碼如下:
#include?
#include?
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()函數(shù)創(chuàng)建的子進程。
在默認情況下,在多進程程序中,GDB只調(diào)試main進程,也就是說無論程序調(diào)用了多少次fork()函數(shù)創(chuàng)建了多少個子進程,GDB在默認情況下,只調(diào)試父進程。為了支持多進程調(diào)試,從GDB版本7.0開始支持單獨調(diào)試(調(diào)試父進程或者子進程)和同時調(diào)試多個進程。
那么,我們該如何調(diào)試子進程呢?我們可以使用如下幾種方式進行子進程調(diào)試。
attach
首先,無論是父進程還是子進程,都可以通過attach命令啟動gdb進行調(diào)試。我們都知道,對于每個正在運行的程序,操作系統(tǒng)都會為其分配一個唯一ID號,也就是進程ID。如果我們知道了進程ID,就可以使用attach命令對其進行調(diào)試了。
在上面代碼中,fork()函數(shù)創(chuàng)建的子進程內(nèi)部,首先會進入while循環(huán)sleep,然后在while循環(huán)之后調(diào)用printf函數(shù)。這樣做的目的有如下:
幫助attach捕獲要調(diào)試的進程id 在使用gdb進行調(diào)試的時候,真正的代碼(即print函數(shù))沒有被執(zhí)行,這樣就可以從頭開始對子進程進行調(diào)試
?可能會有疑惑,上面代碼以及進入while循環(huán),無論如何是不會執(zhí)行到下面printf函數(shù)。其實,這就是gdb的厲害之處,可以通過gdb命令修改num的值,以便其跳出while循環(huán)
?
使用如下命令編譯生成可執(zhí)行文件test_process
g++?-g?test_process.cc?-o?test_process
現(xiàn)在,我們開始嘗試啟動調(diào)試。
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)
在上述命令中,我們執(zhí)行了n(next的縮寫),使其重新對while循環(huán)的判斷體進行判斷。
(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循環(huán),我們使用set命令設(shè)置了num的值為1,這樣條件就會失效退出while循環(huán),進而執(zhí)行下面的printf()函數(shù);在最后我們執(zhí)行了c(continue的縮寫)命令,支持程序退出。
?如果程序正在正常運行,出現(xiàn)了死鎖等現(xiàn)象,則可以通過ps獲取進程ID,然后根據(jù)gdb attach pid進行綁定,進而查看堆棧信息
?
指定進程
默認情況下,GDB調(diào)試多進程程序時候,只調(diào)試父進程。GDB提供了兩個命令,可以通過follow-fork-mode和detach-on-fork來指定調(diào)試父進程還是子進程。
follow-fork-mode
該命令的使用方式為:
(gdb)?set?follow-fork-mode?mode
其中,mode有以下兩個選項:
parent:父進程,mode的默認選項 child:子進程,其目的是告訴 gdb 在目標應(yīng)用調(diào)用fork之后接著調(diào)試子進程而不是父進程,因為在Linux系統(tǒng)中fork()系統(tǒng)調(diào)用成功會返回兩次,一次在父進程,一次在子進程
(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:指定調(diào)試子進程模式 r:運行程序,直接運行程序,此時會進入子進程,然后執(zhí)行while循環(huán) ctrl + c:通過該命令,可以使得GDB收到SIGINT命令,從而暫停執(zhí)行while循環(huán) n(next):繼續(xù)執(zhí)行,進而進入到while循環(huán)的條件判斷處 show follow-fork-mode:再次執(zhí)行該命令,通過輸出可以看出,當前處于child模式下
detach-on-fork
如果一開始指定要調(diào)試子進程還是父進程,那么使用follow-fork-mode命令完全可以滿足需求;但是如果想在調(diào)試過程中,想根據(jù)實際情況在父進程和子進程之間來回切換調(diào)試呢?
GDB提供了另外一個命令:
(gdb)?set?detach-on-fork?mode
其中mode有如下兩個值:
on:默認值,即表明只調(diào)試一個進程,可以是子進程,也可以是父進程
off:程序中的每個進程都會被記錄,進而我們可以對所有的進程進行調(diào)試
如果選擇關(guān)閉detach-on-fork模式(mode為off),那么GDB將保留對所有被fork出來的進程控制,即可用調(diào)試所有被fork出來的進程。可用 使用info forks命令列出所有的可被GDB調(diào)試的fork進程,并可用使用fork命令從一個fork進程切換到另一個fork進程。
info forks: 打印DGB控制下的所有被fork出來的進程列表。該列表包括fork id、進程id和當前進程的位置 fork fork-id: 參數(shù)fork-id是GDB分配的內(nèi)部fork編號,該編號可用通過上面的命令 info forks獲取
coredump
當我們開發(fā)或者使用一個程序時候,最怕的莫過于程序莫名其妙崩潰。為了分析崩潰產(chǎn)生的原因,操作系統(tǒng)的內(nèi)存內(nèi)容(包括程序崩潰時候的堆棧等信息)會在程序崩潰的時候dump出來(默認情況下,這個文件名為core.pid,其中pid為進程id),這個dump操作叫做coredump(核心轉(zhuǎn)儲),然后我們可以用調(diào)試器調(diào)試此文件,以還原程序崩潰時候的場景。
在我們分析如果用gdb調(diào)試coredump文件之前,先需要生成一個coredump,為了簡單起見,我們就用如下例子來生成:
#include?
void?print(int?*v,?int?size)?{
??for?(int?i?=?0;?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
段錯誤(吐核)
如我們預(yù)期,程序產(chǎn)生了異常,但是卻沒有生成coredump文件,這是因為在系統(tǒng)默認情況下,coredump生成是關(guān)閉的,所以需要設(shè)置對應(yīng)的選項以打開coredump生成。
針對多線程程序產(chǎn)生的coredump,有時候其堆棧信息并不能完整的去分析原因,這就使得我們得有其他方式。
18年有一次線上故障,在測試環(huán)境一切正常,但是在線上的時候,就會coredump,根據(jù)gdb調(diào)試coredump,只能定位到了libcurl里面,但卻定位不出原因,用了大概兩天的時間,發(fā)現(xiàn)只有在超時的時候,才會coredump,而測試環(huán)境因為配置比較差超時設(shè)置的是20ms,而線上是5ms,知道coredump原因后,采用逐步定位縮小范圍法,逐步縮小代碼范圍,最終定位到是libcurl一個bug導(dǎo)致。所以,很多時候,定位線上問題需要結(jié)合實際情況,采取合適的方法來定位問題。
配置
配置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后面的數(shù)為0,即不生成coredump文件,我們可以通過如下命令進行設(shè)置
ulimit?-c?size
其中size為允許生成的coredump大小,這個一般盡量設(shè)置大點,以防止生成的coredump信息不全,筆者一般設(shè)置為不限。
ulimit?-c?unlimited
需要說明的是,臨時配置的coredump選項,其默認生成路徑為執(zhí)行該命令時候的路徑,可以通過修改配置來進行路徑修改。
永久
上面的設(shè)置只是使能了core dump功能,缺省情況下,內(nèi)核在coredump時所產(chǎn)生的core文件放在與該程序相同的目錄中,并且文件名固定為core。很顯然,如果有多個程序產(chǎn)生core文件,或者同一個程序多次崩潰,就會重復(fù)覆蓋同一個core文件。
過修改kernel的參數(shù),可以指定內(nèi)核所生成的coredump文件的文件名。使用下面命令,可以實現(xiàn)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
調(diào)試
現(xiàn)在,我們重新執(zhí)行如下命令,按照預(yù)期產(chǎn)生coredump文件:
./test_coredump
elem[955]?=?1702113070
elem[956]?=?1667200115
elem[957]?=?6648431
elem[958]?=?0
elem[959]?=?0
段錯誤(吐核)
然后使用下面的命令進行coredump調(diào)試:
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命令,可以獲取堆棧調(diào)用信息。當進行coredump調(diào)試時候,這個是最基本且最有用處的命令。where命令輸出的結(jié)果包含程序中 的函數(shù)名稱和相關(guān)參數(shù)值。
?
通過where命令,我們能夠發(fā)現(xiàn)程序core在了第5行,那么根據(jù)分析源碼基本就能定位原因。
?需要注意的是,在多線程運行的時候,core不一定在當前線程,這就需要我們對代碼有一定的了解,能夠保證哪塊代碼是安全的,然后通過thread num切換線程,然后再通過bt或者where命令查看堆棧信息,進而定位coredump原因。
?
原理
在前面幾節(jié),我們講了gdb的命令,以及這些命令在調(diào)試時候的作用,并以例子進行了演示。作為C/C++ coder,要知其然,更要知其所以然。所以,借助本節(jié),我們大概講下GDB調(diào)試的原理。
gdb 通過系統(tǒng)調(diào)用 ptrace 來接管一個進程的執(zhí)行。ptrace 系統(tǒng)調(diào)用提供了一種方法使得父進程可以觀察和控制其它進程的執(zhí)行,檢查和改變其核心映像以及寄存器。它主要用來實現(xiàn)斷點調(diào)試和系統(tǒng)調(diào)用跟蹤。

ptrace系統(tǒng)調(diào)用定義如下:
#include?
long?ptrace(enum?__ptrace_request?request,?pid_t?pid,?void?*addr,?void?*data)
pid_t pid:指示 ptrace 要跟蹤的進程 void *addr:指示要監(jiān)控的內(nèi)存地址 enum __ptrace_request request:決定了系統(tǒng)調(diào)用的功能,幾個主要的選項: PTRACE_TRACEME:表示此進程將被父進程跟蹤,任何信號(除了 SIGKILL)都會暫停子進程,接著阻塞于wait()等待的父進程被喚醒。子進程內(nèi)部對exec()的調(diào)用將發(fā)出SIGTRAP信號,這可以讓父進程在子進程新程序開始運行之前就完全控制它PTRACE_ATTACH:attach 到一個指定的進程,使其成為當前進程跟蹤的子進程,而子進程的行為等同于它進行了一次 PTRACE_TRACEME 操作。但需要注意的是,雖然當前進程成為被跟蹤進程的父進程,但是子進程使用 getppid()的到的仍將是其原始父進程的pidPTRACE_CONT:繼續(xù)運行之前停止的子進程。可同時向子進程交付指定的信號
調(diào)試原理
運行并調(diào)試新進程
運行并調(diào)試新進程,步驟如下:
運行g(shù)db exe 輸入run命令,gdb執(zhí)行以下操作: 通過fork()系統(tǒng)調(diào)用創(chuàng)建一個新進程 在新創(chuàng)建的子進程中執(zhí)行ptrace(PTRACE_TRACEME, 0, 0, 0)操作 在子進程中通過execv()系統(tǒng)調(diào)用加載指定的可執(zhí)行文件
attach運行的進程
可以通過gdb attach pid來調(diào)試一個運行的進程,gdb將對指定進程執(zhí)行ptrace(PTRACE_ATTACH, pid, 0, 0)操作。
需要注意的是,當我們attach一個進程id時候,可能會報如下錯誤:
Attaching?to?process?28849
ptrace:?Operation?not?permitted.
這是因為沒有權(quán)限進行操作,可以根據(jù)啟動該進程用戶下或者root下進行操作。
斷點原理
實現(xiàn)原理
當我們通過b或者break設(shè)置斷點時候,就是在指定位置插入斷點指令,當被調(diào)試的程序運行到斷點的時候,產(chǎn)生SIGTRAP信號。該信號被gdb捕獲并 進行斷點命中判斷。
設(shè)置原理
在程序中設(shè)置斷點,就是先在該位置保存原指令,然后在該位置寫入int 3。當執(zhí)行到int 3時,發(fā)生軟中斷,內(nèi)核會向子進程發(fā)送SIGTRAP信號。當然,這個信號會轉(zhuǎn)發(fā)給父進程。然后用保存的指令替換int 3并等待操作恢復(fù)。
命中判斷
gdb將所有斷點位置存儲在一個鏈表中。命中判定將被調(diào)試程序的當前停止位置與鏈表中的斷點位置進行比較,以查看斷點產(chǎn)生的信號。
條件判斷
在斷點處恢復(fù)指令后,增加了一個條件判斷。如果表達式為真,則觸發(fā)斷點。由于需要判斷一次,添加條件斷點后,是否觸發(fā)條件斷點,都會影響性能。在 x86 平臺上,部分硬件支持硬件斷點。不是在條件斷點處插入 int 3,而是插入另一條指令。當程序到達這個地址時,不是發(fā)出int 3信號,而是進行比較。特定寄存器的內(nèi)容和某個地址,然后決定是否發(fā)送int 3。因此,當你的斷點位置被程序頻繁“通過”時,盡量使用硬件斷點,這將有助于提高性能。
單步原理
這個ptrace函數(shù)本身就支持,可以通過ptrace(PTRACE_SINGLESTEP, pid,...)調(diào)用來實現(xiàn)單步。
?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,最后調(diào)用ptrace進行單步調(diào)試。
其它
借助本文,簡單介紹下筆者工作過程中使用的一些其他命令或者工具。
pstack
此命令可顯示每個進程的棧跟蹤。pstack 命令必須由相應(yīng)進程的屬主或 root 運行。可以使用 pstack 來確定進程掛起的位置。此命令允許使用的唯一選項是要檢查的進程的 PID。
這個命令在排查進程問題時非常有用,比如我們發(fā)現(xiàn)一個服務(wù)一直處于work狀態(tài)(如假死狀態(tài),好似死循環(huán)),使用這個命令就能輕松定位問題所在;可以在一段時間內(nèi),多執(zhí)行幾次pstack,若發(fā)現(xiàn)代碼棧總是停在同一個位置,那個位置就需要重點關(guān)注,很可能就是出問題的地方;
以前面的多線程代碼為例,其進程ID是4507(在筆者本地),那么通過
pstack 4507輸出結(jié)果如下:
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?()
在上述輸出結(jié)果中,將進程內(nèi)部的詳細信息都輸出在終端,以方便分析問題。
ldd
在我們編譯過程中通常會提示編譯失敗,通過輸出錯誤信息發(fā)現(xiàn)是找不到函數(shù)定義,再或者編譯成功了,但是運行時候失敗(往往是因為依賴了非正常版本的lib庫導(dǎo)致),這個時候,我們就可以通過ldd來分析該可執(zhí)行文件依賴了哪些庫以及這些庫所在的路徑。
用來查看程式運行所需的共享庫,常用來解決程式因缺少某個庫文件而不能運行的一些問題。
仍然查看可執(zhí)行程序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)
在上述輸出中:
第一列:程序需要依賴什么庫 第二列:系統(tǒng)提供的與程序需要的庫所對應(yīng)的庫 第三列:庫加載的開始地址
在有時候,我們通過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查看,就能找到對應(yīng)的lib庫,但是這個缺點是臨時的,即退出終端后,再執(zhí)行l(wèi)dd,仍然會提示找不到該庫,所以就有了另外一種方式,即通過修改/etc/ld.so.conf,在該文件的后面加上需要的路徑,即
include?ld.so.conf.d/*.conf
/path/to/
然后通過如下命令,即可永久生效
?/sbin/ldconfig
c++filt
因為c++支持重載,也就引出了編譯器的name mangling機制,對函數(shù)進行重命名。
我們通過strings命令查看test_thread中的函數(shù)信息(僅輸出fun等相關(guān))
strings?test_thread?|?grep?fun_
in?fun_int?n?=
in?fun_string?s?=
_GLOBAL__sub_I__Z7fun_inti
_Z10fun_stringRKSs
可以看到_Z10fun_stringRKSs這個函數(shù),如果想知道這個函數(shù)定義的話,可以使用c++filt命令,如下:
?c++filt?_Z10fun_stringRKSs
fun_string(std::basic_string<char,?std::char_traits<char>,?std::allocator<char>?>?const&)
通過上述輸出,我們可以將編譯器生成的函數(shù)名還原到我們代碼中的函數(shù)名即fun_string。
結(jié)語
GDB是一個在Linux上進行開發(fā)的一個必不可少的調(diào)試工具,使用場景依賴于具體的需求或者遇到的具體問題。在我們的日常開發(fā)工作中,熟練使用GDB加以輔助,能夠使得開發(fā)過程事半功倍。
本文從一些簡單的命令出發(fā),通過舉例調(diào)試可執(zhí)行程序(單線程、多線程以及多進程場景)、coredump文件等各個場景,使得大家能夠更加直觀的了解GDB的使用。GDB功能非常強大,筆者工作中使用的都是非常基本的一些功能,如果想深入理解GDB,則需要去官網(wǎng)進行閱讀了解。
好了,本期的文章就到這,我們下期見。
?本文從構(gòu)思到完成,大概用了三周時間,寫作過程是痛苦的(需要整理資料以及構(gòu)建各種場景,以及將各種現(xiàn)場還原),同時又是收獲滿滿的。通過本文,進一步加深了對GDB的底層原理理解。
?
參考
https://www.codetd.com/en/article/13107993
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
如果對本文有疑問可以加筆者微信直接交流,筆者也建了C/C++相關(guān)的技術(shù)群,有興趣的可以聯(lián)系筆者加群。
聊聊服務(wù)注冊與發(fā)現(xiàn)
點個關(guān)注吧!
