调试技巧:GDB基本使用,条件断点,watchpoint,backtrace

说实话,写C语言代码这么多年,我最怕的不是编译不过,而是程序跑起来之后莫名其妙地崩溃或者结果不对。你想想看,一个嵌入式程序,可能跑了几个小时才出问题,这时候靠printf打日志?效率太低了。我个人习惯是,遇到这种疑难杂症,直接上GDB。

GDB的全称是GNU Debugger,说白了就是Linux下最经典的调试器。它不需要图形界面,一个命令行就能搞定。今天我就把GDB最核心的几个功能——条件断点、watchpoint、backtrace——给你讲透。

核心观点:GDB不是万能的,但如果你不会用GDB,调试复杂bug的效率至少低三倍。

24.1 GDB基本使用:启动、运行、暂停

先说说怎么启动GDB。假设你有一个程序叫app,编译的时候记得加-g选项:

gcc -g -o app app.c
gdb ./app

进入GDB之后,常用的命令就这几个:

命令 作用 我常用的场景
runr 运行程序 第一次启动,看看能不能跑起来
breakb 设置断点 在可疑函数入口停住
nextn 单步执行(不进入函数) 跳过库函数调用
steps 单步执行(进入函数) 查看自定义函数内部逻辑
continuec 继续运行 跳过当前断点,跑到下一个
printp 打印变量值 检查变量是否异常
quitq 退出GDB 调试结束

举个例子,假设你有一个函数process_data,你想看看它执行时参数对不对:

(gdb) b process_data
(gdb) r
(gdb) p input_value
$1 = 42
(gdb) n
(gdb) p result
$2 = 0

嗯,这里要注意:p命令打印出来的$1$2是GDB自动给的值历史编号,你可以直接用$1引用之前的结果。

24.2 条件断点:只在特定条件下停住

我在项目中遇到过最头疼的情况:一个循环跑了10000次,第9999次才出错。你总不能按n按到手抽筋吧?这时候条件断点就派上用场了。

条件断点的语法很简单:

(gdb) b 行号 if 条件表达式

比如:

// 代码片段
for (int i = 0; i < 10000; i++) {
    process_item(i);
}

你可以在循环体里设置条件断点:

(gdb) b 10 if i == 9999
(gdb) r

这样程序只在i等于9999时停住。省时省力。

小技巧:条件断点也可以用在函数参数上。比如b process_item if value < 0,当传入负数时自动停住。我曾经用这个技巧抓到一个罕见的参数越界bug。

还有一个常用场景:调试内存泄漏时,你可以在malloc上设置条件断点,只关注特定大小的分配:

(gdb) b malloc if size == 1024

这样每次分配1024字节时都会停住,你可以查看调用栈,找到是谁在分配。

24.3 Watchpoint:监视变量的变化

断点是在特定位置停住,但有时候你根本不知道变量在哪里被修改了。比如一个全局变量g_counter,莫名其妙变成了负数。你翻遍所有代码也找不到赋值的地方。怎么办?

用watchpoint。它监视一个变量,只要值发生变化,GDB就会自动停住。

(gdb) watch g_counter

然后运行程序,当g_counter的值被修改时,GDB会停住并显示旧值和新值:

Hardware watchpoint 2: g_counter
Old value = 10
New value = -5
main () at app.c:42
42          g_counter = -5;  // 原来是你!

watchpoint有三种形式:

命令 作用
watch 变量 变量被写入时停住
rwatch 变量 变量被读取时停住
awatch 变量 变量被读取或写入时都停住

注意:watchpoint依赖硬件支持,嵌入式平台上可能有限制。我曾经在ARM Cortex-M3上调试,硬件watchpoint只有2个,用完了就得手动删除旧的。

删除watchpoint用info watchpoints查看编号,然后用delete 编号删除。

24.4 Backtrace:查看函数调用栈

程序崩溃时,最想知道的就是「谁调了谁,最后死在哪」。backtrace就是干这个的。

当程序停在断点处或者崩溃时,输入backtrace(简写bt):

(gdb) bt
#0  0x08048400 in divide (a=10, b=0) at app.c:15
#1  0x08048450 in calculate (x=10, y=0) at app.c:25
#2  0x080484a0 in main () at app.c:35

这个输出告诉你:

  • #0:当前停在divide函数,参数b=0,第15行
  • #1dividecalculate调用,参数y=0
  • #2calculatemain调用

一眼就能看出,是main传了y=0calculate,然后calculate传了b=0divide,导致除零错误。

你还可以用frame 编号切换到指定栈帧,查看局部变量:

(gdb) frame 1
(gdb) p x
$3 = 10
(gdb) p y
$4 = 0

这样就能一层一层往上查,找到问题的根源。

避坑指南:我曾经遇到一个bug,程序在某个回调函数里崩溃,backtrace显示调用栈只有一层。后来发现是栈被破坏了,GDB无法回溯。这种情况,先检查是不是有局部数组越界或者栈溢出。

24.5 综合实战:用GDB定位一个段错误

光说不练假把式。我们来看一个真实场景。

假设你有这样一段代码:

#include <stdio.h>
#include <stdlib.h>

void fill_data(int *arr, int len) {
    for (int i = 0; i <= len; i++) {  // 注意:这里是 <=,越界了
        arr[i] = i * 2;
    }
}

int main() {
    int *data = (int *)malloc(10 * sizeof(int));
    if (!data) return -1;
    fill_data(data, 10);
    printf("%d\n", data[0]);
    free(data);
    return 0;
}

运行后段错误。用GDB调试:

gdb ./app
(gdb) r
Starting program: ./app

Program received signal SIGSEGV, Segmentation fault.
0x08048410 in fill_data (arr=0x804a008, len=10) at app.c:6
6               arr[i] = i * 2;

先看backtrace:

(gdb) bt
#0  0x08048410 in fill_data (arr=0x804a008, len=10) at app.c:6
#1  0x08048460 in main () at app.c:14

嗯,死在fill_data的第6行。看看i的值:

(gdb) p i
$1 = 10

数组长度是10,下标最大是9,这里i=10,明显越界了。再看arr[10]写到了哪里:

(gdb) p &arr[10]
$2 = (int *) 0x804a030

这个地址可能属于堆管理结构或者另一个变量,写进去就崩了。

修复方法很简单:把<=改成<。但如果没有GDB,你可能要花半小时一行一行看代码。

24.6 本章知识体系

下面这张图总结了GDB调试的核心流程和命令关系:

GDB调试核心流程 gdb ./程序名 设置断点:b 行号 / b 函数名 运行:r / run 调试操作 n(单步) | s(进入) | c(继续) | p(打印) | bt(回溯) 条件断点:b if 条件 Watchpoint:watch 变量 Backtrace:bt / backtrace

这张图展示了GDB调试的完整链路:启动程序 → 设置断点 → 运行 → 执行调试操作。右侧的三个特殊功能——条件断点、watchpoint、backtrace——是解决复杂问题的利器。

24.7 避坑总结

最后,我把自己这些年用GDB踩过的坑总结一下:

  • 编译时一定要加-g:不加的话,GDB看不到源码行号和变量名,只能看到汇编。我曾经有一次忘了加,调试了一下午才发现。
  • 优化级别不要太高-O2-O3会改变代码执行顺序,导致单步执行跳来跳去。调试时用-O0-Og
  • watchpoint有数量限制:嵌入式平台通常只有2-4个硬件watchpoint。用info watchpoints查看当前有哪些。
  • 多线程调试要加set follow-fork-mode child:如果程序fork了子进程,默认GDB只跟踪父进程。我踩过这个坑,子进程崩溃了GDB毫无反应。
  • 段错误先看backtrace:不要一上来就到处设断点。先bt看调用栈,定位到具体函数,再设断点细查。

GDB这东西,说白了就是熟能生巧。刚开始可能觉得命令行麻烦,但用顺手之后,你会发现它比任何IDE调试器都灵活。下次遇到程序崩溃,别急着加printf,试试GDB,你会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321