青島網(wǎng)站設計公司聯(lián)系方式企業(yè)網(wǎng)絡營銷推廣案例
最近和許多同事交流時,發(fā)現(xiàn)好多人只是在IDE上debug,但是gdb卻一點都不了解;校招新來的同事更是都沒聽過gdb這個工具,所以在培訓時給他們培訓了一下;另外好久也沒寫blog了,剛好把這篇筆記簡單分享一下。
0 簡介
GDB 全稱“GNU symbolic debugger”,GNU家族成員之一。
所謂調(diào)試(Debug),就是讓代碼一步一步慢慢執(zhí)行,跟蹤程序的運行過程。比如,可以讓程序停在某個地方,查看當前所有變量的值,或者內(nèi)存中的數(shù)據(jù);也可以讓程序一次只執(zhí)行一條或者幾條語句,看看程序到底執(zhí)行了哪些代碼。
也就是說,通過調(diào)試程序,我們可以監(jiān)控程序執(zhí)行的每一個細節(jié),包括變量的值、函數(shù)的調(diào)用過程、內(nèi)存中數(shù)據(jù)、線程的調(diào)度等,從而發(fā)現(xiàn)隱藏的錯誤或者低效的代碼;在我們?nèi)粘oding debug時,有時很難肉眼發(fā)現(xiàn)自己寫的代碼的問題之處,這時GDB就排上用場了。
下載和安裝這里不做說明,放一個源碼鏈接:http://ftp.gnu.org/gnu/gdb/,感興趣的小伙伴,可以下載看下。
1 常用debug命令
這里僅介紹些基礎命令,覆蓋日常50-60%的使用,簡單調(diào)試夠用了,實際工程使用中肯定是不夠的,先掌握基本的命令,遇到問題再具體學習(有時間精力的小伙伴也可以找大全命令學習下)。
1.1 匯總
GDB 的主要功能就是監(jiān)控程序的執(zhí)行流程。這也就意味著,只有當源程序文件編譯為可執(zhí)行文件并執(zhí)行時,并且該文件中必須包含必要的調(diào)試信息(比如各行代碼所在的行號、包含程序中所有變量名稱的列表(又稱為符號表)等),GDB才會派上用場。
所以在編譯時需要使用 gcc/g++ -g 選項編譯源文件,才可生成滿足 GDB 要求的可執(zhí)行文件
1.2啟動程序
根據(jù)不同場景的需要,GDB 調(diào)試器提供了多種方式來啟動目標程序,其中最常用的就是run 指令,其次為 start 指令。也就是說,run和 start 指令都可以用來在 GDB 調(diào)試器中啟動程序,它們之間的區(qū)別是:
默認情況下:
run 指令會一直執(zhí)行程序,直到執(zhí)行結束。如果程序中手動設置有斷點,則 run指令會執(zhí)行程序至第一個斷點處;
start 指令會執(zhí)行程序至main()主函數(shù)的起始位置,即在main()函數(shù)的第一行語句處停止執(zhí)行(該行代碼尚未執(zhí)行)。
1.3 break命令
break 命令(可以用b 代替)常用的語法格式有以下 2 種。
1、(gdb) break location // b location
2、(gdb) break ... if cond // b .. if cond
其中,第一種格式中,location 用于指定打斷點的具體位置,其表示方式有多種,如表 1 所示。
第二種格式中,… 可以是表 1 中所有參數(shù)的值,用于指定打斷點的具體位置;cond
為某個表達式。整體的含義為:每次程序執(zhí)行到 … 位置時都計算 cond
的值,如果為 True
,則程序在該位置暫停;反之,程序繼續(xù)執(zhí)行。另外也可以用condition
為斷點設置命中條件。
1.4 刪除或禁用斷點
1.4.1刪除斷點
如果之前建立的斷點不再需要或者暫時不需要,該如何刪除或者禁用呢?常用的方式有 2 種:
使用 quit 命令退出調(diào)試,然后重新對目標程序啟動調(diào)試,此方法會將消除上一次調(diào)試操作中建立的所有斷點;
使用專門刪除或禁用斷點的命令,既可以刪除某一個斷點,也可以刪除全部斷點。
無論是普通斷點、觀察斷點還是捕捉斷點,都可以使用 clear 或者 delete 命令進行刪除。
clear 命令可以刪除指定位置處的所有斷點,常用的語法格式如下所示:
(gdb) clear location
參數(shù)location 通常為某一行代碼的行號或者某個具體的函數(shù)名。當 location 參數(shù)為某個函數(shù)的函數(shù)名時,表示刪除位于該函數(shù)入口處的所有斷點。
delete 命令(可以縮寫為 d)通常用來刪除所有斷點,也可以刪除指定編號的各類型斷點,語法格式如下:
delete [breakpoints] [num]
其中,breakpoints 參數(shù)可有可無,num 參數(shù)為指定斷點的編號,其可以是delete 刪除某一個斷點,而非全部。
如果不指定 num參數(shù),則 delete 命令會刪除當前程序中存在的所有斷點。
1.4.2禁用端點
禁用斷點可以使用 disable 命令,語法格式如下:
disable [breakpoints] [num...]
breakpoints 參數(shù)可有可無;num…表示可以有多個參數(shù),每個參數(shù)都為要禁用斷點的編號。如果指定 num…,disable 命令會禁用指定編號的斷點;反之若不設定 num…,則 disable 會禁用當前程序中所有的斷點。
對于禁用的斷點,可以使用enable 命令激活,該命令的語法格式有多種,分別對應有不同的功能:
- 1 enable [breakpoints] [num…] 激活用 num… 參數(shù)指定的多個斷點,如果不設定 num…,表示激活所有禁用的斷點
- 2 enable [breakpoints] once num… 臨時激活以 num… 為編號的多個斷點,但斷點只能使用 1 次,之后會自動回到禁用狀態(tài)
- 3 enable [breakpoints] count num… 臨時激活以 num… 為編號的多個斷點,斷點可以使用 count 次,之后進入禁用狀態(tài)
- 4 enable [breakpoints] delete num… 激活 num… 為編號的多個斷點,但斷點只能使用 1 次,之后會被永久刪除。
1.5查看變量或表達式的值
對于在調(diào)試期間查看某個變量或表達式的值,GDB 調(diào)試器提供有 2 種方法,即使用 print 命令或者 display命令。
1.5.1print
它的功能就是在 GDB 調(diào)試程序的過程中,輸出或者修改指定變量或者表達式的值。
print 命令可以縮寫為 p,最常用的語法格式如下所示:
(gdb) print num
(gdb) p num
其中,參數(shù) num 用來代指要查看或者修改的目標變量或者表達式。
當程序中包含多個作用域不同但名稱相同的變量或表達式時,可以借助::運算符明確指定要查看的目標變量或表達式。::運算符的語法格式如下:
1(gdb) print file::variable
2(gdb) print function::variable
其中 file用于指定具體的文件名,funciton 用于指定具體所在函數(shù)的函數(shù)名,variable表示要查看的目標變量或表達式。
另外,print也可以打印出類或者結構體變量的值。
1.5.2 display
和 print 命令一樣,display 命令也用于調(diào)試階段查看某個變量或表達式的值,它們的區(qū)別是,使用 display 命令查看變量或表達式的值,每當程序暫停執(zhí)行(例如單步執(zhí)行)時,GDB 調(diào)試器都會自動幫我們打印出來,而 print 命令則不會。
也就是說,使用 1 次 print 命令只能查看 1 次某個變量或表達式的值,而同樣使用 1 次 display 命令,每次程序暫停執(zhí)行時都會自動打印出目標變量或表達式的值。因此,當我們想頻繁查看某個變量或表達式的值從而觀察它的變化情況時,使用 display 命令可以一勞永逸。
display 命令沒有縮寫形式,常用的語法格式如下 2 種:
(gdb) display expr
(gdb) display/fmt expr
1.6 GDB單步調(diào)試
根據(jù)實際場景的需要,GDB 調(diào)試器共提供了 3 種可實現(xiàn)單步調(diào)試程序的方法,即使用 next、step 和 until 命令。換句話說,這 3 個命令都可以控制 GDB調(diào)試器每次僅執(zhí)行 1 行代碼,但除此之外,它們各自還有不同的功能。
1.6.1 next命令
next 是最常用來進行單步調(diào)試的命令,其最大的特點是當遇到包含調(diào)用函數(shù)的語句時,無論函數(shù)內(nèi)部包含多少行代碼,next 指令都會一步執(zhí)行完。也就是說,對于調(diào)用的函數(shù)來說,next 命令只會將其視作一行代碼。
next 命令可以縮寫為n 命令,使用方法也很簡單,語法格式如下:
(gdb) next count
1.6.2 step命令
通常情況下,step 命令和next命令的功能相同,都是單步執(zhí)行程序。不同之處在于,當step 命令所執(zhí)行的代碼行中包含函數(shù)時,會進入該函數(shù)內(nèi)部,并在函數(shù)第一行代碼處停止執(zhí)行。
step 命令可以縮寫為 s命令,用法和 next 命令相同,語法格式如下:
(gdb) step count
1.6.3 until命令
until 命令可以簡寫為 u 命令,有 2 種語法格式,如下所示:
1、(gdb) until
2、(gdb) until location
其中,參數(shù) location為某一行代碼的行號。
不帶參數(shù)的 until命令,可以使 GDB調(diào)試器快速運行完當前的循環(huán)體,并運行至循環(huán)體外停止。注意,until 命令并非任何情況下都會發(fā)揮這個作用,只有當執(zhí)行至循環(huán)體尾部(最后一行代碼)時,until命令才會發(fā)生此作用;反之,until命令和 next 命令的功能一樣,只是單步執(zhí)行程序。
1.6.4 return命令
實際調(diào)試時,在某個函數(shù)中調(diào)試一段時間后,可能不需要再一步步執(zhí)行到函數(shù)返回處,希望直接執(zhí)行完當前函數(shù),這時可以使用 finish命令。與finish 命令類似的還有 return 命令,它們都可以結束當前執(zhí)行的函數(shù)。
1.6.5 finish命令
finish 命令和 return命令的區(qū)別是,finish命令會執(zhí)行函數(shù)到正常退出;而 return 命令是立即結束執(zhí)行當前函數(shù)并返回,也就是說,如果當前函數(shù)還有剩余的代碼未執(zhí)行完畢,也不會執(zhí)行了。除此之外,return命令還有一個功能,即可以指定該函數(shù)的返回值。
1.6.6 jump命令
jump 命令的功能是直接跳到指定行繼續(xù)執(zhí)行程序,其語法格式為:
(gdb) jump location
其中,location 通常為某一行代碼的行號。
也就是說,jump 命令可以略過某些代碼,直接跳到 location處的代碼繼續(xù)執(zhí)行程序。這意味著,如果你跳過了某個變量(對象)的初始化代碼,直接執(zhí)行操作該變量(對象)的代碼,很可能會導致程序崩潰或出現(xiàn)其它 Bug。另外,如果 jump跳轉到的位置后續(xù)沒有斷點,那么 GDB會直接執(zhí)行自跳轉處開始的后續(xù)代碼。
1.7 GDB search 命令
調(diào)試文件時,某些時候可能會去找尋找某一行或者是某一部分的代碼??梢允褂?list 顯示全部的源碼,然后進行查看。當源文件的代碼量較少時,我們可以使用這種方式搜索。如果源文件的代碼量很大,使用這種方式尋找效率會很低。所以 GDB中提供了相關的源代碼搜索的的search命令。
search 命令的語法格式為:
search <regexp>
reverse-search <regexp>
第一項命令格式表示從當前行的開始向前搜索,后一項表示從當前行開始向后搜索。其中regexp 就是正則表達式,正則表達式描述了一種字符串匹配的模式,可以用來檢查一個串中是否含有某種子串、將匹配的子串替換或者從某個串中取出符合某個條件的子串。很多的編程語言都支持使用正則表達式。
1.8 查看堆棧信息
1.8.1 backtrace 命令 (bt)
backtrace 命令用于打印當前調(diào)試環(huán)境中所有棧幀的信息,常用的語法格式如下:
(gdb) backtrace [-full] [n]
其中,用 [ ] 括起來的參數(shù)為可選項,它們的含義分別為:
-
n:一個整數(shù)值,當為正整數(shù)時,表示打印最里層的 n 個棧幀的信息;n為負整數(shù)時,那么表示打印最外層n個棧幀的信息;
-
-full:打印棧幀信息的同時,打印出局部變量的值。
注意,當調(diào)試多線程程序時,該命令僅用于打印當前線程中所有棧幀的信息。如果想要打印所有線程的棧幀信息,應執(zhí)行thread apply all backtrace命令。
1.8.2 frame 命令
frame命令的常用形式有 2 個:
根據(jù)棧幀編號或者棧幀地址,選定要查看的棧幀,語法格式如下:
(gdb) frame spec
該命令可以將 spec 參數(shù)指定的棧幀選定為當前棧幀。spec 參數(shù)的值,常用的指定方法有 3 種:
通過棧幀的編號指定。0 為當前被調(diào)用函數(shù)對應的棧幀號,最大編號的棧幀對應的函數(shù)通常就是 main() 主函數(shù);
借助棧幀的地址指定。棧幀地址可以通過 info frame 命令(后續(xù)會講)打印出的信息中看到;
通過函數(shù)的函數(shù)名指定。注意,如果是類似遞歸函數(shù),其對應多個棧幀的話,通過此方法指定的是編號最小的那個棧幀。
除此之外,對于選定一個棧幀作為當前棧幀,GDB 調(diào)試器還提供有up 和down兩個命令。其中,up命令的語法格式為:
(gdb) up n
其中 n為整數(shù),默認值為 1。該命令表示在當前棧幀編號(假設為 m)的基礎上,選定 m+n為編號的棧幀作為新的當前棧幀。
相對地,down 命令的語法格式為:
(gdb) down n
其中n為整數(shù),默認值為 1。該命令表示在當前棧幀編號(假設為 m)的基礎上,選定m-n 為編號的棧幀作為新的當前棧幀。
借助如下命令,我們可以查看當前棧幀中存儲的信息:
(gdb) info frame
該命令會依次打印出當前棧幀的如下信息:
? 當前棧幀的編號,以及棧幀的地址;
? 當前棧幀對應函數(shù)的存儲地址,以及該函數(shù)被調(diào)用時的代碼存儲的地址
? 當前函數(shù)的調(diào)用者,對應的棧幀的地址;
? 編寫此棧幀所用的編程語言;
? 函數(shù)參數(shù)的存儲地址以及值;
? 函數(shù)中局部變量的存儲地址;
? 棧幀中存儲的寄存器變量,例如指令寄存器(64位環(huán)境中用 rip 表示,32為環(huán)境中用eip 表示)、堆?;羔樇拇嫫?#xff08;64位環(huán)境用 rbp表示,32位環(huán)境用 ebp表示)等。
除此之外,還可以使用info args命令查看當前函數(shù)各個參數(shù)的值;使用info locals命令查看當前函數(shù)中各局部變量的值。
2 讀寫內(nèi)存寄存器
調(diào)試過程中,要經(jīng)常查看或者改寫內(nèi)存、寄存器的值,操作如下:
2.1 讀取
- 讀取某個變量的值:
p <var>
- 讀取某個內(nèi)存地址里的內(nèi)容:
x <memaddr>
- 讀取某個寄存器的值:
info register
后面操作都以下面程序為例:
int main(void)
{unsigned int *src base addr = (unsigned int *)0x1c000292;srand(__get_rv_cycle() | get_rv_instret() | __RV_CSR_READ(CSR MCYCLE));//*src base addr = 0x7788;int a = 5;uint32 t rval = rand();uint32 t hartid = __RV_CSR_READ(CSR_MHARTID);rv_csr_t misa = __RV_CSR_READ(CSR_MISA);printf("Hart %d, MISA: 0x%lx\r\n", hartid, misa);print_misa();for (int i = e; i < RUN_LOOPS; i ++) {printf("%d: Hello World From Nuclei RISC-V Processor! rin",i)}simulation pass();
}
2.1.1 讀取變量值
- p/x:其中p為print,x代表16進制
2.1.2 讀取內(nèi)存
- 其中x代表examine,檢查可以直接查看內(nèi)存地址
- 也可以通過print打印該地址的解引用值
另外,使用x命令打印多條內(nèi)存數(shù)據(jù)的格式為x/nfu addr
。其中:
- n表示輸出單元的個數(shù);
- f表示輸出格式,比如x是以16進制形式輸出,o是以8進制形式輸出;
- u表示一個單元的長度,b是一個byte,h是兩個byte(halfword),w是四個byte(word),g是八個byte(giant word)。
2.1.3 查看寄存器信息
這里說的寄存器是通用寄存器,不是某個ip的寄存器(和內(nèi)存一樣操作)
2.2 寫操作
寫操作一般用的不多,但最好還是了解。
2.2.1 修改變量的值
set var <name> = <value>
2.2.2修改寄存器的值
set $<register> = <value>
修改通用寄存器
2.2.3修改pc值
2.2.4修改內(nèi)存值
3 watchpoint使用
很多情況下,程序的bug是由于某個變量或地址被莫名修改而導致的,但是具體什么時候修改了該值,我們很難定位到。
和breakpoint類似,watchpoint用來觀察數(shù)據(jù)或者地址變化,breakpoint是指令斷點;觀察點watchpoint功能,可以監(jiān)控程序中變量或表達式的值,只要在運行過程中發(fā)生改變,程序就會停止執(zhí)行??梢哉f學會watchpoint,能夠實現(xiàn)讓bug自動現(xiàn)身的效果。
3.1適用場景
- 數(shù)據(jù)污染,變量異常變化導致bug
- 內(nèi)存泄漏,踩了地址
- 確定了某個異常變量,但是該變量被多次使用、還會在各種循環(huán)內(nèi)被操作。
- 多線程場景,線程切來切去,不知道變量具體被哪個線程修改了。
3.2 watchpoint命令
3.3 使用演示
錯誤dump如下:
- 1)打印出現(xiàn)指令異常。當前PC值為0x1c000294
- 2)異常錯誤MCAUSE 是 2,非法指令
- 3)猜測應該是程序哪里修改了指令(可能是內(nèi)存踩踏、內(nèi)存泄漏),導致0x1c000294處的指令被修改為非法指令0x00
int main(void)
{unsigned int *src base addr = (unsigned int *)0x1c000292;srand(__get_rv_cycle() | get_rv_instret() | __RV_CSR_READ(CSR MCYCLE));//*src base addr = 0x7788;int a = 5;uint32 t rval = rand();uint32 t hartid = __RV_CSR_READ(CSR_MHARTID);rv_csr_t misa = __RV_CSR_READ(CSR_MISA);printf("Hart %d, MISA: 0x%lx\r\n", hartid, misa);print_misa();for (int i = e; i < RUN_LOOPS; i ++) {printf("%d: Hello World From Nuclei RISC-V Processor! rin",i)}simulation pass();
}
- 4)實際是刻意為之,修改了指令內(nèi)存地址
- 5)實際情況下是,我們不知道哪里出了問題,這時就已使用watchpoint來找出問題
- 6)當觀察點的內(nèi)存內(nèi)容被修改時,cpu將會被hang住,通過查看上下文鎖定位置這里在99行(還未執(zhí)行)上一句被修改
簡單介紹了下gdb的基本使用,內(nèi)存讀寫命令和watchpoint,拋磚引玉吧,如有錯誤之處請在評論區(qū)指出。