歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

從一個實例來認識GDB與高效調試

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 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved