比 GDB 更方便的代碼調試工具:CGDB
作 者:道哥,10 + 年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux。
目錄
-
有 bug 的示例代碼
-
GDB 調試操作
-
CGDB 調試操作
別人的經驗,我們的階梯!
CGDB 是GDB
的前端,在終端窗口中意圖形化的形式來調試代碼 (基於ncurse
),非常方便。相對於GDB
來說,可以很大的提高效率。
這篇文章就來分享一下CGDB
的最基本使用方法,如果是第一次聽說,強烈建議您體驗一下,一定會愛上它的!
有 bug 的示例代碼
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
typedef struct USER_DATA{
char data[32];
unsigned short data_len;
unsigned int flag;
}__attribute((packed))__;
const unsigned char * g_data = "hello";
/*
功能:加載一段數據
參數1: data[OUT]: 數據被加載的緩衝區
參數2: len [OUT]:實際被加載的數據的長度
返回值: 0-成功,else-失敗
*/
static int get_data(unsigned char *data, unsigned int *len)
{
assert(data && len);
memcpy((void *)data, (void *)g_data, strlen(g_data));
*len = strlen(g_data);
return 0;
}
int main(int argc, char *argv[])
{
// 創建結構體變量
struct USER_DATA user_data;
user_data.flag = 0xA5;
// 往結構體變量中加載數據
if (0 == get_data(user_data.data, &user_data.data_len))
{
printf("get_data ok! \n");
printf("data_len = %d, data = %s \n", user_data.data_len, user_data.data);
printf("user_data.flag = 0x%x \n", user_data.flag); // 期望值:0xA5
}
else
{
printf("get_data failed! \n");
}
return 0;
}
在編譯之前,先看一下代碼,你能發現其中的bug
嗎?
當然了,在編譯的時候,編譯器以Warning
的方式給出了風險提示。因爲示例代碼很簡單,所以很容易發現。
但是在一個項目中,如果不喜歡消除編譯Warning
警告的話,這個bug
還是比較隱蔽的。
編譯測試代碼:gcc -g test.c -o test
因爲要使用GDB
調試,所以別忘了加上-g
選項。
GDB 調試操作
$ gdb ./test
(gdb) r // 直接全速執行一次
(gdb) r
Starting program: /home/captain/demos_2022/cgdb/test
test start...
get_data ok!
data_len = 5, data = hello
user_data.flag = 0x0
[Inferior 1 (process 9933) exited normally]
發現user_data.flag
的值不對,決定在調用get_data
之前的那行下一個斷點,然後從頭開始執行:
查看代碼行號:
(gdb) l main
18 *len = strlen(g_data);
19 return 0;
20 }
21
22 int main(int argc, char *argv[])
23 {
24 struct USER_DATA user_data;
25 user_data.flag = 0xA5;
26 if (0 == get_data(user_data.data, &user_data.data_len))
27 {
下斷點在25
行:
(gdb) b 25
Breakpoint 1 at 0x400771: file test.c, line 25.
開始運行:
(gdb) r
Starting program: /home/captain/demos_2022/cgdb/test
Breakpoint 1, main (argc=1, argv=0x7fffffffdc58) at test.c:25
25 user_data.flag = 0xA5;
在斷點處停了下來,此時該賦值語句還沒有執行,所以先單步執行一次:
(gdb) step
26 if (0 == get_data(user_data.data, &user_data.data_len))
此時,打印一下這個變量user_data.flag
的值和地址:
因爲待會進入被調用函數,這個變量就不可見了,所以需要通過地址來打印。
(gdb) print &user_data.flag
$1 = (unsigned int *) 0x7fffffffdb62
(gdb) print/x user_data.flag
$2 = 0xa5
此時賦值是正確的,再接着往下執行,進入被調用函數get_data()
了,
(gdb) step
get_data (data=0x7fffffffdb40 "n\333\377\377\377\177", len=0x7fffffffdb60) at test.c:16
16 assert(data && len);
這個函數一共就4
行代碼,我們每單步執行一句,就打印一下user_data.flag
變量的內容。
單步執行下一行memcpy
處,並且看一下user_data.flag
變量地址處的內容是否仍然爲:0xa5
:
(gdb) step
17 memcpy((void *)data, (void *)g_data, strlen(g_data));
(gdb) print/x *0x7fffffffdb62
$3 = 0xa5
繼續單步執行 (因爲不需要跟進memcpy、strlen
的內部,所以使用next
命令),並打印:
(gdb) next
18 *len = strlen(g_data); // 這一句即將被執行
(gdb) print/x *0x7fffffffdb62
$4 = 0xa5
(gdb) next
19 return 0;
(gdb) print/x *0x7fffffffdb62
$5 = 0x0
發現問題了:在執行*len = strlen(g_data)
語句之後,變量user_data.flag
地址中的內容就被改變了。
再仔細檢查一下代碼,就可以診斷出是數據類型使用錯了。
解決 bug: get_data()
函數的最後一個參數,應該是unsigned short
型指針才正確。
問題是解決了,但是回過頭來看一下gdb
的調試過程,還是比較繁瑣的:調試指令和代碼顯示夾雜在一起,需要敲很多指令。
CGDB 調試操作
啓動CGDB
之後,終端窗口被評分爲上下兩部分:上面是代碼窗口,下面是調試窗口。
按下ESC
鍵進入代碼窗口,此時可以上下瀏覽代碼,並且可以進行一系列的操作:
空格鍵:設置或者取消斷點;
o:查看代碼所在的文件;
/ 或者 ?:在代碼中搜索字符串;
。。。
還有很多方便的快捷鍵:
-:縮小代碼窗口;
+:擴大代碼窗口;
gg: 光標移動到文件頭部;
GG:光標移動到文件尾部;
ctrl + b:代碼向上翻一頁;
ctrl + u:代碼向上翻半頁;
ctrl + f:代碼向下翻一頁;
ctrl + d:代碼向下翻半頁;
按下i
鍵回到調試窗口,進入調試模式,使用的調試指令與GDB
幾乎一樣!
也就是說:可以在實時查看代碼的情況下進行調試操作,大大提高了效率。
我們按照上面GDB
的調試過程走一遍:
按下ESC
鍵進入代碼窗口,此時代碼前面的行號如果是白色的,表示所在的當前行。
按下j
鍵,向下移動高亮的當前行。當移動到25
行時,如下:
按下空格鍵,表示在此行設置一個斷點,此時行號變成紅色的:
並且在調試窗口打印一行信息:
(gdb)
Breakpoint 1 at 0x400771: file test.c, line 25.
按下i
鍵回到調試操作窗口,然後輸入運行指令r
,會在第25
行停下來的,如下綠色的箭頭所示:
當然了,調試窗口也會打印出相關信息:
(gdb) r
Starting program: /home/captain/demos_2022/cgdb/test
Breakpoint 1, main (argc=1, argv=0x7fffffffdc58) at test.c:25
單步step
執行這條賦值語句,然後打印一下user_data.flag
的值和地址:
(gdb) print/x user_data.flag
1: /x user_data.flag = 0xa5
(gdb) print &user_data.flag
2: &user_data.flag = (unsigned int *) 0x7fffffffdb62
此時,賦值語句正確執行,打印的值也是符合預期的。
再執行單步指令,進入函數get_data()
內部:
(gdb) step
get_data (data=0x7fffffffdb40 "n\333\377\377\377\177", len=0x7fffffffdb60) at test.c:16
此時,上面的代碼窗口自動進入get_data()
相關的代碼,如下所示:
繼續單步,在執行賦值語句*len = strlen(g_data);
之前打印一下變量user_data.flag
地址中的內容:
(gdb) print/x *0x7fffffffdb62
$2 = 0xa5
正確!然後執行賦值語句之後,再次打印:
(gdb) next
(gdb) print/x *0x7fffffffdb62
$3 = 0x0
發現問題:在執行*len = strlen(g_data)
語句之後,變量user_data.flag
地址中的內容就被改變了。
小結:
CGDB
的操作過程,雖然我寫的比較囉嗦,但是實際使用起來,真的是非常的絲滑,就像巧克力一樣!
如果轉載本文,文末務必註明:“轉自微信公衆號:IOT 物聯網小鎮”。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-Nokj7AsJBcWrJvm-0teOg