學習使用 GDB 調試代碼
GNU 調試器常以它的命令 gdb
稱呼它,它是一個互動式的控制台,可以幫助你瀏覽源代碼、分析執行的內容,其本質上是對錯誤的應用程序中出現的問題進行逆向工程。
故障排除的麻煩在於它很複雜。GNU 調試器 並不是一個特別複雜的應用程序,但如果你不知道從哪裡開始,甚至不知道何時和為何你可能需要求助於 GDB 來進行故障排除,那麼它可能會讓人不知所措。如果你一直使用 print
、echo
或 printf 語句來調試你的代碼,當你開始思考是不是還有更強大的東西時,那麼本教程就是為你準備的。
有錯誤的代碼
要開始使用 GDB,你需要一些代碼。這裡有一個用 C++ 寫的示例應用程序(如果你一般不使用 C++ 編寫程序也沒關係,在所有語言中原理都是一樣的),其來源於 猜謎遊戲系列 中的一個例子。
#include <iostream>
#include <stdlib.h> //srand
#include <stdio.h> //printf
using namespace std;
int main () {
srand (time(NULL));
int alpha = rand() % 8;
cout << "Hello world." << endl;
int beta = 2;
printf("alpha is set to is %sn", alpha);
printf("kiwi is set to is %sn", beta);
return 0;
} // main
這個代碼示例中有一個 bug,但它確實可以編譯(至少在 GCC 5 的時候)。如果你熟悉 C++,你可能已經看到了,但這是一個簡單的問題,可以幫助新的 GDB 用戶了解調試過程。編譯並運行它就可以看到錯誤:
$ g++ -o buggy example.cpp
$ ./buggy
Hello world.
Segmentation fault
排除段故障
從這個輸出中,你可以推測變數 alpha
的設置是正確的,因為否則的話,你就不會看到它後面的那行代碼執行。當然,這並不總是正確的,但這是一個很好的工作理論,如果你使用 printf
作為日誌和調試器,基本上也會得出同樣的結論。從這裡,你可以假設 bug 在於成功列印的那一行之後的某行。然而,不清楚錯誤是在下一行還是在幾行之後。
GNU 調試器是一個互動式的故障排除工具,所以你可以使用 gdb
命令來運行錯誤的代碼。為了得到更好的結果,你應該從包含有調試符號的源代碼中重新編譯你的錯誤應用程序。首先,看看 GDB 在不重新編譯的情況下能提供哪些信息:
$ gdb ./buggy
Reading symbols from ./buggy...done.
(gdb) start
Temporary breakpoint 1 at 0x400a44
Starting program: /home/seth/demo/buggy
Temporary breakpoint 1, 0x0000000000400a44 in main ()
(gdb)
當你以一個二進位可執行文件作為參數啟動 GDB 時,GDB 會載入該應用程序,然後等待你的指令。因為這是你第一次在這個可執行文件上運行 GDB,所以嘗試重複這個錯誤是有意義的,希望 GDB 能夠提供進一步的見解。很直觀,GDB 用來啟動它所載入的應用程序的命令就是 start
。默認情況下,GDB 內置了一個斷點,所以當它遇到你的應用程序的 main
函數時,它會暫停執行。要讓 GDB 繼續執行,使用命令 continue
:
(gdb) continue
Continuing.
Hello world.
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6
(gdb)
毫不意外:應用程序在列印 「Hello world」 後不久就崩潰了,但 GDB 可以提供崩潰發生時正在發生的函數調用。這有可能就足夠你找到導致崩潰的 bug,但為了更好地了解 GDB 的功能和一般的調試過程,想像一下,如果問題還沒有變得清晰,你想更深入地挖掘這段代碼發生了什麼。
用調試符號編譯代碼
要充分利用 GDB,你需要將調試符號編譯到你的可執行文件中。你可以用 GCC 中的 -g
選項來生成這個符號:
$ g++ -g -o debuggy example.cpp
$ ./debuggy
Hello world.
Segmentation fault
將調試符號編譯到可執行文件中的結果是得到一個大得多的文件,所以通常不會分發它們,以增加便利性。然而,如果你正在調試開源代碼,那麼用調試符號重新編譯測試是有意義的:
$ ls -l *buggy* *cpp
-rw-r--r-- 310 Feb 19 08:30 debug.cpp
-rwxr-xr-x 11624 Feb 19 10:27 buggy*
-rwxr-xr-x 22952 Feb 19 10:53 debuggy*
用 GDB 調試
載入新的可執行文件(本例中為 debuggy
)以啟動 GDB:
$ gdb ./debuggy
Reading symbols from ./debuggy...done.
(gdb) start
Temporary breakpoint 1 at 0x400a44
Starting program: /home/seth/demo/debuggy
Temporary breakpoint 1, 0x0000000000400a44 in main ()
(gdb)
如前所述,使用 start
命令進行:
(gdb) start
Temporary breakpoint 1 at 0x400a48: file debug.cpp, line 9.
Starting program: /home/sek/demo/debuggy
Temporary breakpoint 1, main () at debug.cpp:9
9 srand (time(NULL));
(gdb)
這一次,自動的 main
斷點可以指明 GDB 暫停的行號和該行包含的代碼。你可以用 continue
恢復正常操作,但你已經知道應用程序在完成之前就會崩潰,因此,你可以使用 next
關鍵字逐行步進檢查你的代碼:
(gdb) next
10 int alpha = rand() % 8;
(gdb) next
11 cout << "Hello world." << endl;
(gdb) next
Hello world.
12 int beta = 2;
(gdb) next
14 printf("alpha is set to is %sn", alpha);
(gdb) next
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6
(gdb)
從這個過程可以確認,崩潰不是發生在設置 beta
變數的時候,而是執行 printf
行的時候。這個 bug 在本文中已經暴露了好幾次(破壞者:向 printf
提供了錯誤的數據類型),但暫時假設解決方案仍然不明確,需要進一步調查。
設置斷點
一旦你的代碼被載入到 GDB 中,你就可以向 GDB 詢問到目前為止代碼所產生的數據。要嘗試數據自省,通過再次發出 start
命令來重新啟動你的應用程序,然後進行到第 11 行。一個快速到達 11 行的簡單方法是設置一個尋找特定行號的斷點:
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x400a48: file debug.cpp, line 9.
Starting program: /home/sek/demo/debuggy
Temporary breakpoint 2, main () at debug.cpp:9
9 srand (time(NULL));
(gdb) break 11
Breakpoint 3 at 0x400a74: file debug.cpp, line 11.
建立斷點後,用 continue
繼續執行:
(gdb) continue
Continuing.
Breakpoint 3, main () at debug.cpp:11
11 cout << "Hello world." << endl;
(gdb)
現在暫停在第 11 行,就在 alpha
變數被設置之後,以及 beta
被設置之前。
用 GDB 進行變數自省
要查看一個變數的值,使用 print
命令。在這個示例代碼中,alpha
的值是隨機的,所以你的實際結果可能與我的不同:
(gdb) print alpha
$1 = 3
(gdb)
當然,你無法看到一個尚未建立的變數的值:
(gdb) print beta
$2 = 0
使用流程式控制制
要繼續進行,你可以步進代碼行來到達將 beta
設置為一個值的位置:
(gdb) next
Hello world.
12 int beta = 2;
(gdb) next
14 printf("alpha is set to is %sn", alpha);
(gdb) print beta
$3 = 2
另外,你也可以設置一個觀察點,它就像斷點一樣,是一種控制 GDB 執行代碼流程的方法。在這種情況下,你知道 beta
變數應該設置為 2
,所以你可以設置一個觀察點,當 beta
的值發生變化時提醒你:
(gdb) watch beta > 0
Hardware watchpoint 5: beta > 0
(gdb) continue
Continuing.
Breakpoint 3, main () at debug.cpp:11
11 cout << "Hello world." << endl;
(gdb) continue
Continuing.
Hello world.
Hardware watchpoint 5: beta > 0
Old value = false
New value = true
main () at debug.cpp:14
14 printf("alpha is set to is %sn", alpha);
(gdb)
你可以用 next
手動步進完成代碼的執行,或者你可以用斷點、觀察點和捕捉點來控制代碼的執行。
用 GDB 分析數據
你可以以不同格式查看數據。例如,以八進位值查看 beta
的值:
(gdb) print /o beta
$4 = 02
要查看其在內存中的地址:
(gdb) print /o &beta
$5 = 0x2
你也可以看到一個變數的數據類型:
(gdb) whatis beta
type = int
用 GDB 解決錯誤
這種自省不僅能讓你更好地了解什麼代碼正在執行,還能讓你了解它是如何執行的。在這個例子中,對變數運行的 whatis
命令給了你一個線索,即你的 alpha
和 beta
變數是整數,這可能會喚起你對 printf
語法的記憶,使你意識到在你的 printf
語句中,你必須使用 %d
來代替 %s
。做了這個改變,就可以讓應用程序按預期運行,沒有更明顯的錯誤存在。
當代碼編譯後發現有 bug 存在時,特別令人沮喪,但最棘手的 bug 就是這樣,如果它們很容易被發現,那它們就不是 bug 了。使用 GDB 是獵取並消除它們的一種方法。
下載我們的速查表
生活的真相就是這樣,即使是最基本的編程,代碼也會有 bug。並不是所有的錯誤都會導致應用程序無法運行(甚至無法編譯),也不是所有的錯誤都是由錯誤的代碼引起的。有時,bug 是基於一個特別有創意的用戶所做的意外的選擇組合而間歇性發生的。有時,程序員從他們自己的代碼中使用的庫中繼承了 bug。無論原因是什麼,bug 基本上無處不在,程序員的工作就是發現並消除它們。
GNU 調試器是一個尋找 bug 的有用工具。你可以用它做的事情比我在本文中演示的要多得多。你可以通過 GNU Info 閱讀器來了解它的許多功能:
$ info gdb
無論你是剛開始學習 GDB 還是專業人員的,提醒一下你有哪些命令是可用的,以及這些命令的語法是什麼,都是很有幫助的。
via: https://opensource.com/article/21/3/debug-code-gdb
作者:Seth Kenlon 選題:lujun9972 譯者:wxy 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive