调试基础:GDB入门、断点设置、单步执行

说实话,很多C语言开发者写代码时最怕的就是程序崩溃。段错误、死循环、逻辑错乱……这些问题光靠printf大法,效率实在太低了。我刚开始做嵌入式那会儿,也喜欢到处加打印语句,结果代码改得乱七八糟,问题还没定位到。

后来我花了三天时间,认认真真把GDB啃了下来。嗯,从那以后,我的调试效率至少翻了三倍。今天我就把GDB最核心的用法——断点设置和单步执行,给你讲透。

核心观点:GDB不是万能的,但不会GDB,调试效率至少低一半。

GDB是什么?为什么嵌入式开发离不开它?

GDB,全称GNU Debugger,是Linux下最经典的调试器。它可以在程序运行时暂停、查看变量、修改内存、跟踪函数调用链。说白了,它让你能“钻进”程序内部,看每一行代码到底干了什么。

我在项目中遇到过最典型的一个场景:一个嵌入式设备跑着跑着就死机了,没有任何错误信息。用printf?根本来不及打印。用GDB挂上去,设置断点,单步跟踪,发现是一个全局变量被中断服务程序意外修改了。这种问题,没有GDB,你查三天都未必能找到。

我的习惯:写代码时就把调试符号带上(-g选项),哪怕发布版本也要保留一份带符号的副本。这样线上出问题,还能用core dump分析。

GDB入门:三步走

别被GDB的命令行界面吓到。你只需要记住三个步骤:编译、启动、调试。

第一步:编译时加-g选项

这是最容易被忽略的一步。不加-g,GDB就看不到源码和符号信息,调试体验大打折扣。

gcc -g -o my_program my_program.c

如果你用Makefile,记得在CFLAGS里加上-g。我个人习惯在调试阶段用-O0 -g,避免优化导致代码行号错乱。

第二步:启动GDB

有两种方式:

  1. 直接启动:gdb ./my_program
  2. 附加到进程:gdb -p <PID> —— 这个在嵌入式调试中非常常用,比如程序已经跑起来了,你发现它卡住了,直接attach上去看。

注意:附加到进程时,程序会暂停。调试完记得用detach命令释放,否则程序会一直挂起。

第三步:运行程序

在GDB里输入run(或简写r)启动程序。如果需要传参数,直接跟在后面:run arg1 arg2

断点设置:让程序停在你想停的地方

断点是GDB最强大的功能之一。你想想看,程序有几十万行代码,你不可能一行一行看。你只需要在怀疑有问题的位置设个断点,程序跑到那里就会自动停下来。

常用断点命令

命令 说明 示例
break 行号 在指定行设断点 break 42
break 函数名 在函数入口设断点 break main
break 文件名:行号 在多文件项目中指定文件 break uart.c:128
break 行号 if 条件 条件断点,满足条件才停下 break 88 if count > 5
info breakpoints 查看所有断点 info b
delete 断点编号 删除指定断点 delete 2

重点:条件断点非常实用。比如你有一个循环执行1000次,只在第500次时出错。你不需要按500次next,直接设break 行号 if i == 500,程序会自动停在那一轮。

我曾经踩过的坑

我曾经在调试一个多线程程序时,在共享变量的写操作上设了断点。结果每次断点触发,线程上下文就变了,导致问题无法复现。后来我改用watch命令监视变量变化,才真正定位到问题。所以记住:断点会改变程序执行时序,多线程调试时要格外小心。

单步执行:一步一步看代码怎么跑

断点让程序停下来,单步执行则让你看清每一步发生了什么。GDB提供了几个单步命令,各有用途。

核心单步命令

命令 简写 作用
next n 执行下一行,不进入函数内部
step s 执行下一行,会进入函数内部
finish fin 执行到当前函数返回
until u 跳出循环或执行到指定行
continue c 继续执行,直到下一个断点

举个例子。假设你有这样一段代码:

int calculate(int a, int b) {
    int tmp = a + b;   // 第5行
    tmp = tmp * 2;     // 第6行
    return tmp;        // 第7行
}

int main() {
    int x = 10;        // 第10行
    int y = 20;        // 第11行
    int z = calculate(x, y);  // 第12行
    printf("%d\n", z); // 第13行
    return 0;          // 第14行
}

你在第12行设了断点。程序停在第12行时:

  • 输入n,会直接执行完calculate函数,跳到第13行。你看不到函数内部发生了什么。
  • 输入s,会进入calculate函数,停在函数内部第5行。你可以继续单步看tmp的变化。

我个人习惯是:先n跳过不关心的函数,遇到可疑的函数再用s进去看。这样效率最高。

小技巧:在单步过程中,随时可以用print 变量名(简写p)查看变量当前值。比如p tmp。也可以用display 变量名让GDB每次停下时自动打印该变量。

知识体系:GDB调试核心逻辑

下面这张图,是我自己总结的GDB调试流程。你照着这个思路走,基本不会乱。

GDB调试核心流程 编译带-g选项 gcc -g -o prog prog.c 启动GDB gdb ./prog 或 gdb -p PID 设置断点 break / watch 运行程序 run / continue 单步执行 & 查看变量 next / step / print 定位问题,修复代码 重新编译调试

这张图的核心逻辑是:编译 → 启动 → 设断点 → 运行 → 单步查看 → 修复。如果修复后问题还在,就重新编译再走一遍。这个闭环,就是GDB调试的标准流程。

实战小例子:用GDB定位段错误

段错误(Segmentation Fault)是C语言最常见的崩溃方式。我教你一个快速定位的方法。

假设你有这样一个程序:

#include <stdio.h>

void crash() {
    int *p = NULL;
    *p = 42;  // 这里会段错误
}

int main() {
    crash();
    return 0;
}

编译:gcc -g -o crash crash.c

启动GDB:gdb ./crash

输入run,程序会崩溃,GDB会自动停在崩溃的那一行,并显示:

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555146 in crash () at crash.c:5
5           *p = 42;

看到没?GDB直接告诉你:第5行,*p = 42,p是空指针。你连断点都不用设。这就是GDB的威力。

注意:如果程序没有-g选项,GDB只会显示地址,不会显示源码行号。所以再次强调:调试版本一定要加-g。

总结一下

GDB入门其实不难。你只要记住:

  • 编译加-g
  • break设断点
  • nextstep单步执行
  • print看变量

这三个命令,就能解决80%的调试问题。剩下的20%,比如多线程调试、远程调试、core dump分析,等你把这几个基础命令用熟了,自然就会去探索。

嗯,今天就先讲到这里。记住:调试不是浪费时间,而是节省时间。一个熟练的GDB使用者,定位问题的速度是printf党的五倍以上。

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