2. 断点基础

调试嵌入式程序,说白了就是跟代码的「运行时行为」打交道。你写了一个逻辑,但跑起来对不对?数据流没断?中断触发时机准不准?这些问题,光靠看源码是看不出来的。这时候,断点就是你手里最趁手的工具。

我个人习惯,拿到一个新固件,第一件事不是单步跑,而是先下几个关键断点。为什么?因为断点能让你「冻结」现场,看清楚每一刻的状态。今天我们就从最基础的东西聊起。

什么是断点?

断点,就是你在程序里埋的一个「暂停标记」。当CPU执行到这一行时,GDB会把它停下来。你可以趁机查看变量、寄存器、堆栈——说白了,就是让程序「定格」在你关心的那一帧。

我在项目中遇到过一种情况:一个电机控制算法,跑起来总在特定角度下抖动。看代码逻辑没问题,但就是找不到原因。后来我在PWM更新函数入口下了个断点,每次停下来看角度值,才发现是浮点精度截断导致的累积误差。没有断点,这种问题你盯一天也盯不出来。

核心理解: 断点不是「改代码」,而是「看代码」。它不改变程序逻辑,只让你在特定位置停下来观察。

设置第一个断点:break 命令

设置断点,用 break 命令。简写是 b。用法很灵活,我列几种最常见的:

(gdb) break main              # 在 main 函数入口停
(gdb) break 42                 # 在当前文件的第42行停
(gdb) break file.c:100         # 在 file.c 的第100行停
(gdb) break func_name          # 在某个函数入口停
(gdb) break *0x08001234        # 在绝对地址停(裸机调试常用)

嗯,这里要注意:如果你在中断服务函数里下断点,要小心。中断频率高的话,程序可能一直停在断点里,根本跑不出去。我曾经在UART接收中断里下了个断点,结果串口直接卡死,还以为硬件坏了——其实就是断点触发的次数太多,CPU根本没机会处理后续数据。

我的习惯: 在中断函数里下断点,我会先确认中断触发频率。如果频率太高,我会加一个条件断点(后面会讲),或者干脆用观察点代替。

查看断点列表:info breakpoints

断点下多了,你总得知道当前有哪些断点、都在哪、状态如何。用 info breakpoints,简写 i b。

(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08000100 main
2       breakpoint     keep y   0x08000234 timer_isr
3       breakpoint     keep n   0x08000348 uart_send

这张表里,每一列都有用:

列名 含义 我常用的场景
Num 断点编号 删除或禁用时用这个编号
Type 断点类型 breakpoint 是普通断点,watchpoint 是观察点
Disp 触发后的行为 keep 表示保留,del 表示触发一次后自动删除
Enb 启用状态 y 启用,n 禁用
Address 断点地址 裸机调试时,我经常对照反汇编确认地址对不对
What 断点位置描述 函数名、文件名+行号

你想想看,如果调试一个多任务系统,你下了七八个断点,不查列表根本记不住哪个是哪个。我每次开始调试前,都会先跑一遍 i b,确认当前断点状态。这就像上战场前检查弹药——省得打到一半才发现不对劲。

删除与禁用断点

断点不是越多越好。下多了,程序跑几步就停,调试效率反而低。所以你得学会「管理」断点。

删除断点:clear 和 delete

两个命令都能删断点,但用法不同:

(gdb) clear main              # 删除 main 函数处的断点
(gdb) clear file.c:100        # 删除 file.c 第100行的断点
(gdb) delete 2                # 删除编号为2的断点
(gdb) delete                  # 删除所有断点(慎用!)

clear 是按位置删,delete 是按编号删。我个人更习惯用 delete,因为编号更精确,不会误删。有一次我手快,在调试一个I2C驱动时用了 clear,结果把另一个文件里同名的函数断点也删了——嗯,从那以后我就只用编号删了。

注意: 删除断点不可撤销。如果你不确定某个断点是否还有用,建议先禁用,而不是直接删。

禁用与启用:disable 和 enable

禁用断点,就是让它暂时失效,但保留在列表里。以后想用,再启用就行。

(gdb) disable 3               # 禁用编号为3的断点
(gdb) disable 1 2 4           # 禁用多个断点
(gdb) enable 3                # 启用编号为3的断点
(gdb) enable 1 2 4            # 启用多个断点

禁用断点的好处是什么?举个例子:你在调试一个状态机,有5个状态,每个状态入口都下了断点。现在你只想关注状态3和状态5,那就把其他3个禁用掉。等状态3和5调完了,再启用剩下的。这样不用重新下断点,省事。

我曾经调试一个Bootloader升级流程,下了十几个断点。每次只启用当前关注的2-3个,调完一段就切换。整个流程调下来,断点列表一直保留着,只是不断切换启用状态。效率很高。

知识体系图

下面这张图,把断点的基础操作串起来了。你可以把它当成一个「调试地图」:

断点基础操作 设置断点 (break) • 函数名:break main • 行号:break 42 • 文件+行:break file.c:100 • 绝对地址:break *0x08001234 查看断点 (info break) • 编号、类型、状态 • 地址、位置描述 • 触发后行为 (keep/del) 删除断点 (clear/delete) • clear main (按位置) • delete 2 (按编号) • delete (全部删除) 禁用/启用 (disable/enable) • disable 3 (禁用编号3) • enable 3 (启用编号3) • 保留断点,临时关闭 断点管理四步法:设置 → 查看 → 删除/禁用 → 启用

这张图把断点的四个核心操作串在了一起。你从「设置断点」开始,然后通过「查看断点」确认状态,最后根据需求选择「删除」或「禁用/启用」。说白了,这就是一个闭环管理流程。

避坑指南: 我曾经在调试一个RTOS任务切换时,下了断点在任务调度函数里。结果每次切换都触发,根本没法正常调试。后来我改用条件断点,只在特定任务ID下触发——这个我们后面会详细讲。但基础思路就是:断点不是越多越好,而是越精准越好。

好了,断点的基础操作就这些。你可以在自己的开发板上试试:先下一个断点,跑一下,看看程序停在哪。然后用 i b 查看列表,再用 disable 禁用一个,看看效果。最后用 delete 清理掉。多练几次,这些命令就会变成你的肌肉记忆。

可可精品资料库