GDB的全稱是GNU project debugger,是類Unix系統上一個十分強大的調試器。這裡通過一個簡單的例子(插入算法)來介紹如何使用gdb進行調試,特別是如何通過中斷來高效地找出死循環;我們還可以看到,在修正了程序錯誤並重新編譯後,我們仍然可以通過原先的GDB session進行調試(而不需要重開一個GDB),這避免了一些重復的設置工作;同時,在某些受限環境中(比如某些實時或嵌入式系統),往往只有一個Linux字符界面可供調試。這種情況下,可以使用job在代碼編輯器、編譯器(編譯環境)、調試器之間做到無縫切換。這也是高效調試的一個方法。
先來看看這段插入排序算法(a.cpp),裡面有一些錯誤。
// a.cpp #include <stdio.h> #include <stdlib.h> int x[10]; int y[10]; int num_inputs; int num_y = 0; void get_args(int ac, char **av) { num_inputs = ac - 1; for (int i = 0; i < num_inputs; i++) x[i] = atoi(av[i+1]); } void scoot_over(int jj) { for (int k = num_y-1; k > jj; k++) y[k] = y[k-1]; } void insert(int new_y) { if (num_y = 0) { y[0] = new_y; return; } for (int j = 0; j < num_y; j++) { if (new_y < y[j]) { scoot_over(j); y[j] = new_y; return; } } } void process_data() { for (num_y = 0; num_y < num_inputs; num_y++) insert(x[num_y]); } void print_results() { for (int i = 0; i < num_inputs; i++) printf("%d\n",y[i]); } int main(int argc, char ** argv) { get_args(argc,argv); process_data(); print_results(); return 0; }
代碼就不分析了,稍微花點時間應該就能明白。你能發現幾個錯誤?
使用gcc編譯:
gcc -g -Wall -o insert_sort a.cpp
"-g"告訴gcc在二進制文件中加入調試信息,如符號表信息,這樣gdb在調試時就可以把地址和函數、變量名對應起來。在調試的時候你就可以根據變量名查看它的值、在源代碼的某一行加一個斷點等,這是調試的先決條件。“-Wall”是把所有的警告開關打開,這樣編譯時如果遇到warning就會打印出來。一般情況下建議打開所有的警告開關。
運行編譯後的程序(./insert_sort),才發現程序根本停不下來。上調試器!(有些bug可能一眼就能看出來,這裡使用GDB只是為了介紹相關的基本功能)
TUI模式
現在版本的GDB都支持所謂的終端用戶接口模式(Terminal User Interface),就是在顯示GDB命令行的同時可以顯示源代碼。好處是你可以隨時看到當前執行到哪條語句。之所以叫TUI,應該是從GUI抄過來的。注意,可以通過ctrl + x + a來打開或關閉TUI模式。
gdb -tui ./insert_sort
死循環
進入GDB後運行run命令,傳入命令行參數,也就是要排序的數組。當然,程序也是停不下來:
為了讓程序停下來,我們可以發送一個中斷信號(ctrl + c),GDB捕捉到該信號後會掛起被調試進程。注意,什麼時候發送這個中斷有點技巧,完全取決於我們的經驗和程序的特點。像這個簡單的程序,正常情況下幾乎立刻就會執行完畢。如果感覺到延遲就說明已經發生了死循環(或其他什麼),這時候發出中斷肯定落在死循環的循環體中。這樣我們才能通過檢查上下文來找到有用信息。大型程序如果正常情況下就需要跑個幾秒鐘甚至幾分鐘,那麼你至少需要等到它超時後再去中斷。
此時,程序暫停在第44行(第44行還未執行),TUI模式下第44行會被高亮顯示。我們知道,這一行是某個死循環體中的一部分。
因為暫停的代碼有一定的隨機性,可以多運行幾次,看看每次停留的語句有什麼不同。後面執行run命令的時候可以不用再輸入命令行參數(“12 5”),GDB會記住。還有,再執行run的時候GDB會問是否重頭開始執行程序,當然我們要從頭開始執行。
基本確定位置後(如上面的44行),因為這個程序很小,可以單步(step)一條條語句查看。不難發現問題出在第24行,具體的步驟就省略了。
無縫切換
在編碼、調試的時候,除非你有集成開發環境,一般你會需要打開三個窗口:代碼編輯器(比如很多人用的VIM)、編譯器(新開的窗口運行gcc或者make命令、執行程序等)、調試器。集成開發環境當然好,但某些倒閉的場合下你無法使用任何GUI工具,比如一些僅提供字符界面的嵌入式設備——你只有一個Linux命令行可以使用。顯然,如果在VIM中修改好代碼後需要先關閉VIM才能敲入gcc的編譯命令,或者調試過程中發現問題需要先關閉調試器才能重新打開VIM修改代碼、編譯、再重新打開調試器,那麼不言而喻,這個過程太痛苦了!
好在可以通過Linux的作業管理機制,通過ctrl + z把當前任務掛起,返回終端做其他事情。通過jobs命令可以查看當前shell有哪些任務。比如,當我暫停GDB時,jobs顯示我的VIM編輯器進程與GDB目前都處於掛起狀態。
以下是些相關的命令,比較常用
fg %1 // 打開VIM,1是VIM對應的作業號
fg %2 // 打開GDB
bg %1 // 讓VIM到後台運行
kill %1 && fg // 徹底殺死VIM進程
GDB的“在線刷新”
好了,剛才介紹了無縫切換,那我們可以在不關閉GDB的情況下(注意,ctrl + z不是關閉GDB這個進程,只是掛起)切換到VIM中去修改代碼來消除死循環(把第24行的“if (num_y = 0)" 改成"if (num_y == 0)")。動作序列可以是:
ctrl + z // 掛起GDB
jobs // 查看VIM對應的作業號,假設為1
fg %1 // 進入VIM,修改代碼..
ctrl + z // 修改完後掛起VIM
gcc -g -Wall -o insert_sort a.cpp // 重新編譯程序
fg %2 // 進入GDB,假設GDB的作業號為2
現在,我們又返回GDB調試界面了!但在調試前還有一步,如何讓GDB識別新的程序(因為程序已經重新編譯)?只要再次運行run就可以了。因為GDB沒有關閉,所以之前設置的斷點、運行run時傳入的命令行參數等還保留著,不需要重新輸入。很好用吧!
GDB自動檢測到程序發生改變,重新加載符號。
其他bug
關於本例中的其他bug,這裡就不多說了。有興趣的同學,可以和我討論。
GDB調試程序用法 http://www.linuxidc.com/Linux/2013-06/86044.htm
GDB+GDBserver無源碼調試Android 動態鏈接庫的技巧 http://www.linuxidc.com/Linux/2013-06/85936.htm
使用hello-gl2建立ndk-GDB環境(有源碼和無源碼調試環境) http://www.linuxidc.com/Linux/2013-06/85935.htm
在Ubuntu上用GDB調試printf源碼 http://www.linuxidc.com/Linux/2013-03/80346.htm
Linux下用GDB調試可加載模塊 http://www.linuxidc.com/Linux/2013-01/77969.htm
強大的C/C++ 程序調試工具GDB http://www.linuxidc.com/Linux/2016-09/135171.htm
使用GDB命令行調試器調試C/C++程序 http://www.linuxidc.com/Linux/2014-11/109845.htm
GDB調試命令總結 http://www.linuxidc.com/Linux/2016-08/133988.htm
GDB調試工具入門 http://www.linuxidc.com/Linux/2016-09/135168.htm
GDB 的詳細介紹:請點這裡
GDB 的下載地址:請點這裡