调试基础: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
有两种方式:
- 直接启动:
gdb ./my_program - 附加到进程:
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调试的标准流程。
实战小例子:用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设断点 - 用
next和step单步执行 - 用
print看变量
这三个命令,就能解决80%的调试问题。剩下的20%,比如多线程调试、远程调试、core dump分析,等你把这几个基础命令用熟了,自然就会去探索。
嗯,今天就先讲到这里。记住:调试不是浪费时间,而是节省时间。一个熟练的GDB使用者,定位问题的速度是printf党的五倍以上。