原文地址:https://www.cnblogs.com/jkin/p/13877679.html
GDB查看棧信息
當程序因某種異常停止運行時,我們要做的就是找到程序停止的具體位置,分析導致程序停止的原因。
對於 C、C++ 程序而言,異常往往出現在某個函數體內,例如 main() 主函數、調用的系統庫函數或者自定義的函數等。要知道,程序中每個被調用的函數在執行時,都會生成一些必要的信息,包括:
- 函數調用發生在程序中的具體位置;
- 調用函數時的參數;
- 函數體內部各局部變量的值等等。
這些信息會集中存儲在一塊稱為“棧幀”的內存空間中。也就是說,程序執行時調用了多少個函數,就會相應產生多少個棧幀,其中每個棧幀自函數調用時生成,函數調用結束后自動銷毀。
注意,這些棧幀所在的位置也不是隨意的,它們集中位於一個大的內存區域里,我們通常將其稱為棧區或者棧。
當程序因某種異常暫停執行時,如果其發生在某個函數內部,我們可以嘗試借助該函數對應棧幀中記錄的信息,找到程序發生異常的原因。
慶幸的是,GDB 調試器為了方便用戶在調試程序時查看某個棧幀中記錄的信息,提供了 frame 和 backtrace 命令。接下來,我將給讀者詳細講解一下這 2 個命令的功能和用法。
GDB frame命令
任何一個被調用的函數,執行時都會生成一個存儲必要信息的棧幀。對於 C、C++ 程序而言,其至少也要包含一個函數,即 main() 主函數,這意味着程序執行時至少會生成一個棧幀。main() 主函數對應的棧幀,又稱為初始幀或者最外層的幀。除此之外,每當程序中多調用一個函數,執行過程中就會生成一個新的棧幀。更甚者,如果該函數是一個遞歸函數,則會生成多個棧幀。
在程序內部,各個棧幀用地址作為它們的標識符,注意這里的地址並不一定為棧幀的起始地址。我們知道,每個棧幀往往是由連續的多個字節構成,每個字節都有自己的地址,不同操作系統為棧幀選定地址標識符的規則不同,它們會選擇其中一個字節的地址作為棧幀的標識符。
然而,GDB 調試器並沒有套用地址標識符的方式來管理棧幀。對於當前調試環境中存在的棧幀,GDB 調試器會按照既定規則對它們進行編號:當前正被調用函數對應的棧幀的編號為 0,調用它的函數對應棧幀的編號為 1,以此類推。
frame 命令的常用形式有 2 個:
1) 根據棧幀編號或者棧幀地址,選定要查看的棧幀,語法格式如下:
(gdb) frame spec
該命令可以將 spec 參數指定的棧幀選定為當前棧幀。spec 參數的值,常用的指定方法有 3 種:
- 通過棧幀的編號指定。0 為當前被調用函數對應的棧幀號,最大編號的棧幀對應的函數通常就是 main() 主函數;
- 借助棧幀的地址指定。棧幀地址可以通過 info frame 命令(后續會講)打印出的信息中看到;
- 通過函數的函數名指定。注意,如果是類似遞歸函數,其對應多個棧幀的話,通過此方法指定的是編號最小的那個棧幀。
除此之外,對於選定一個棧幀作為當前棧幀,GDB 調試器還提供有 up 和 down 兩個命令。其中,up 命令的語法格式為:
(gdb) up n
其中 n 為整數,默認值為 1。該命令表示在當前棧幀編號(假設為 m)的基礎上,選定 m+n 為編號的棧幀作為新的當前棧幀。
相對地,down 命令的語法格式為:
(gdb) down n
其中 n 為整數,默認值為 1。該命令表示在當前棧幀編號(假設為 m)的基礎上,選定 m-n 為編號的棧幀作為新的當前棧幀。
2) 借助如下命令,我們可以查看當前棧幀中存儲的信息:
(gdb) info frame
該命令會依次打印出當前棧幀的如下信息:
- 當前棧幀的編號,以及棧幀的地址;
- 當前棧幀對應函數的存儲地址,以及該函數被調用時的代碼存儲的地址
- 當前函數的調用者,對應的棧幀的地址;
- 編寫此棧幀所用的編程語言;
- 函數參數的存儲地址以及值;
- 函數中局部變量的存儲地址;
- 棧幀中存儲的寄存器變量,例如指令寄存器(64位環境中用 rip 表示,32為環境中用 eip 表示)、堆棧基指針寄存器(64位環境用 rbp 表示,32位環境用 ebp 表示)等。
除此之外,還可以使用info args命令查看當前函數各個參數的值;使用info locals命令查看當前函數中各局部變量的值。
GDB backtrace命令
backtrace 命令用於打印當前調試環境中所有棧幀的信息,常用的語法格式如下:
(gdb) backtrace [-full] [n]
其中,用 [ ] 括起來的參數為可選項,它們的含義分別為:
- n:一個整數值,當為正整數時,表示打印最里層的 n 個棧幀的信息;n 為負整數時,那么表示打印最外層 n 個棧幀的信息;
- -full:打印棧幀信息的同時,打印出局部變量的值。
注意,當調試多線程程序時,該命令僅用於打印當前線程中所有棧幀的信息。如果想要打印所有線程的棧幀信息,應執行thread apply all backtrace命令。
基於以上對 frame 和 backtrace 命令的介紹,這里以調試如下 C 語言程序為例,給大家演示這 2 個命令的作用。
1 #include <stdio.h> 2 3 int funC(int* num){ 4 if(*num > 0){ 5 (*num)--; 6 }else{ 7 (*num)++; 8 } 9 10 printf("In funC num: %d\n", *num); 11 12 return *num; 13 } 14 15 16 void funCN(int num){ 17 if(num > 0){ 18 (num)--; 19 }else{ 20 (num)++; 21 } 22 23 printf("In funCN num: %d\n", num); 24 25 return; 26 } 27 28 int funD(int num){ 29 if(num == 1){ 30 return 1; 31 }else{ 32 return num * funD(num - 1); 33 } 34 } 35 36 int main(int argc, char **argv) 37 { 38 printf("Hello world!\n"); 39 40 int num = 0; 41 printf("please input num: "); 42 scanf("%d", &num); 43 44 printf("num mul: %d\n", funD(num)); 45 46 printf("in main num: %d\n", funC(&num)); 47 48 int tmp = funD(num); 49 printf("num mul: %d\n", tmp); 50 51 funCN(num); 52 printf("in main num: %d\n", (num)); 53 54 return 0; 55 }
不難發現,funC()是一個能夠修改傳入參數的函數,funCN()是一個不能修改傳入參數的函數;funD() 是一個遞歸函數,已編譯為可供 GDB 調試的 main 可執行文件。在此基礎上,進行如下調試:
(gdb) b 3
Breakpoint 1 at 0x4004cf: file main.c, line 3.
(gdb) r
Starting program: ~/demo/main.exe
Breakpoint 1, func (num=5) at main.c:3
3 if(num==1){
(gdb) c
Continuing.
Breakpoint 1, func (num=4) at main.c:3
3 if(num==1){
(gdb) p num
$1 = 4
(gdb) backtrace <-- 打印所有的棧幀信息
#0 func (num=4) at main.c:3
#1 0x00000000004004e9 in func (num=5) at main.c:6
#2 0x0000000000400508 in main () at main.c:12
(gdb) info frame <-- 打印當前棧幀的詳細信息
Stack level 0, frame at 0x7fffffffe240: <-- 棧幀編號 0,地址 0x7fffffffe240
rip = 0x4004cf in func (main.c:3); saved rip 0x4004e9 <-- 函數的存儲地址 0x4004cf,調用它的函數地址為 0x4004e9
called by frame at 0x7fffffffe260 <-- 當前棧幀的上一級棧幀(編號 1 的棧幀)的地址為 0x7fffffffe260
source language c.
Arglist at 0x7fffffffe230, args: num=4 <-- 函數參數的地址和值
Locals at 0x7fffffffe230, Previous frame's sp is 0x7fffffffe240 <--函數內部局部變量的存儲地址
Saved registers: <-- 棧幀內部存儲的寄存器
rbp at 0x7fffffffe230, rip at 0x7fffffffe238
(gdb) info args <-- 打印當前函數參數的值
num = 4
(gdb) info locals <-- 打印當前函數內部局部變量的信息(這里沒有)
No locals.
(gdb) up <-- 查看編號為 1 的棧幀
#1 0x00000000004004e9 in func (num=5) at main.c:6
6 return num*func(num-1);
(gdb) frame 1 <-- 當編號為 1 的棧幀作為當前棧幀
#1 0x00000000004004e9 in func (num=5) at main.c:6
6 return num*func(num-1);
(gdb) info frame <-- 打印 1 號棧幀的詳細信息
Stack level 1, frame at 0x7fffffffe260:
rip = 0x4004e9 in func (main.c:6); saved rip 0x400508
called by frame at 0x7fffffffe280, caller of frame at 0x7fffffffe240 <--上一級棧幀地址為 0x7fffffffe280,下一級棧幀地址為 0x7fffffffe240
source language c.
Arglist at 0x7fffffffe250, args: num=5
Locals at 0x7fffffffe250, Previous frame's sp is 0x7fffffffe260
Saved registers:
rbp at 0x7fffffffe250, rip at 0x7fffffffe258
(gdb)

