3. GDB 进阶:条件断点、观察点、回溯跟踪、Core Dump 分析

调试这件事,说白了就是跟代码的「灵魂」对话。基础断点只能让你停在某个位置,但真正的问题往往藏在特定条件下。这一章,我们来聊聊 GDB 那些真正能救命的高级功能。

本章核心能力:掌握条件断点、观察点、回溯跟踪和 Core Dump 分析,能让你在复杂嵌入式项目中定位问题的效率提升 5 倍以上。

3.1 条件断点:只在需要的时候停下来

普通断点就像在门口装了个铃铛,谁路过都响。条件断点呢?它只在你指定的「嫌疑人」经过时才触发。

我早期做电机控制项目时,有个 bug 只在电流超过 3A 时出现。普通断点根本没法用——每次循环都停,手都按麻了。条件断点一次搞定。

3.1.1 基本用法

(gdb) break motor_control.c:128 if current > 3.0
(gdb) break 128 if current > 3.0 && speed < 100
(gdb) break func_name if flag == 1

条件表达式支持 C 语言的所有比较和逻辑运算。你甚至可以调用函数:

(gdb) break packet_handler if check_crc(packet) == 0
我的习惯:条件断点里尽量用简单表达式。复杂函数调用会拖慢执行速度,在实时性要求高的嵌入式系统里尤其明显。

3.1.2 条件断点的坑

我曾经犯过一个错:在条件里写了个 if ptr != NULL,结果程序跑飞了。为什么?因为 ptr 是个全局指针,被中断服务程序改了。条件断点每次命中都会重新求值,如果表达式本身有副作用,后果很严重。

避坑指南:条件表达式里不要使用会修改程序状态的函数或操作。比如 break x if count++ > 10 这种写法,调试完记得删掉。

3.2 观察点:盯住变量的每一次变化

断点停的是「位置」,观察点盯的是「数据」。当某个变量被意外修改时,观察点能直接告诉你:谁干的?

3.2.1 硬件观察点 vs 软件观察点

类型 命令 原理 适用场景
硬件观察点 watch var 利用 CPU 调试寄存器 变量被写、读、执行
软件观察点 awatch var GDB 单步模拟 硬件寄存器不够时
读写观察点 rwatch var 读操作也触发 排查数据竞争

硬件观察点数量有限(通常 4-8 个),但效率极高。软件观察点没有数量限制,但会让程序慢得像蜗牛。

3.2.2 实战案例

我记得有个项目,串口接收缓冲区的数据总被莫名其妙地覆盖。用 watch rx_buffer[0] 一盯,发现是 DMA 传输完成中断里有个指针偏移算错了。观察点直接定位到那行代码,省了我两天时间。

(gdb) watch rx_buffer[0]
Hardware watchpoint 1: rx_buffer[0]
(gdb) continue
Continuing.
Hardware watchpoint 1: rx_buffer[0]

Old value = 0xAA
New value = 0x55
dma_handler.c:47  rx_buffer[offset] = dma_data;
小技巧:观察点配合条件使用效果更好。watch var if var > 100 可以避免大量无意义的触发。

3.3 回溯跟踪:事故现场的「黑匣子」

程序崩溃时,回溯跟踪就是你的黑匣子。它能告诉你:程序是怎么走到这一步的?

3.3.1 backtrace 命令

(gdb) backtrace
#0  fault_handler () at exception.c:15
#1  0x08001234 in task_scheduler () at scheduler.c:88
#2  0x08004567 in main_loop () at main.c:42
#3  0x08000100 in Reset_Handler () at startup.s:78

每一帧都对应一个函数调用。从 #0 到 #3,就是程序从启动到崩溃的完整路径。

3.3.2 帧信息查看

(gdb) frame 1          # 切换到第 1 帧
(gdb) info args        # 查看函数参数
(gdb) info locals      # 查看局部变量
(gdb) list             # 查看当前帧的源代码

我曾经遇到一个栈溢出问题,回溯跟踪显示递归深度达到了 2000 多层。一看代码,有个递归函数忘了写终止条件。这种问题,没有回溯跟踪根本无从下手。

注意:如果栈被严重破坏,回溯跟踪可能不完整。这时候可以试试 backtrace full 强制显示更多信息。

3.4 Core Dump:让程序「死前」留下遗言

Core Dump 是程序崩溃时内存的快照。有了它,你可以在事后慢慢分析,不用守着现场。

3.4.1 开启 Core Dump

# 设置 Core Dump 文件大小(不限)
$ ulimit -c unlimited

# 查看当前设置
$ ulimit -c
unlimited

嵌入式 Linux 系统里,还要确保 Core Dump 的路径可写:

$ echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern

3.4.2 分析 Core Dump

$ arm-none-eabi-gdb my_program.elf core.12345
(gdb) backtrace
(gdb) info registers
(gdb) x/20x $sp    # 查看栈内容

Core Dump 里包含了程序崩溃瞬间的所有信息:寄存器值、栈内容、堆数据。你甚至可以查看全局变量的值。

关键点:分析 Core Dump 时,必须使用与生成 Core Dump 时完全相同的可执行文件。版本对不上,符号表就乱了。

3.4.3 嵌入式环境下的特殊处理

裸机或 RTOS 环境下,没有操作系统帮你生成 Core Dump。我的做法是:在 HardFault_Handler 里手动保存现场。

void HardFault_Handler(void) {
    // 保存寄存器到预定义缓冲区
    save_context_to_ram();
    // 触发软件复位
    NVIC_SystemReset();
}

复位后,通过调试器读取 RAM 中的保存数据,就能还原崩溃现场。这招我在好几个项目里用过,屡试不爽。

3.5 知识体系总览

下面这张图,把本章的核心知识点串起来了:

GDB 进阶调试知识体系 GDB 进阶调试 条件断点 观察点 回溯跟踪 Core Dump 表达式条件 硬件/软件观察点 帧信息查看 事后分析 调试效率提升路径 条件断点 → 精准定位触发条件 观察点 → 追踪数据异常修改 回溯跟踪 + Core Dump → 完整还原崩溃现场

3.6 实战技巧总结

最后,分享几个我多年积累的实用技巧:

  • 条件断点优先用简单比较:复杂表达式会拖慢执行,实时系统里尤其明显
  • 观察点用完就删:info watchpoints 查看,delete num 删除,别留着占硬件资源
  • 回溯跟踪配合日志:在关键函数入口加 printf,崩溃时回溯 + 日志双管齐下
  • Core Dump 要保留原始 ELF:strip 过的文件没法做符号解析,等于白费
我的经验:调试不是碰运气,而是有方法论的。条件断点、观察点、回溯跟踪、Core Dump 这四个工具,就像医生的听诊器、CT、MRI 和病理切片。用对了,问题无所遁形。