2. GDB深度实战:从安装到高级调试技巧
调试嵌入式软件,说白了就是跟bug斗智斗勇。而GDB,就是我手里最顺手的武器。这一章咱们不讲虚的,直接上手——从安装配置到常用命令,再到条件断点和观察点,每一步我都会结合自己踩过的坑来讲。
2.1 GDB的安装与配置
先说说安装。不同平台方法不一样,我列个表,你一看就明白。
| 平台 | 安装命令 | 备注 |
|---|---|---|
| Ubuntu/Debian | sudo apt install gdb |
默认版本够用 |
| CentOS/RHEL | sudo yum install gdb |
可能需要EPEL源 |
| macOS | brew install gdb |
需要签名,麻烦点 |
| 交叉编译环境 | arm-none-eabi-gdb |
嵌入式专用 |
我个人习惯用 ~/.gdbinit 文件做配置。这个文件会在GDB启动时自动加载,省得每次敲重复命令。
# 我的 .gdbinit 配置
set pagination off # 关闭分页,输出不卡顿
set confirm off # 关闭确认提示
set print pretty on # 结构化打印
set print elements 0 # 打印完整字符串
set history save on # 保存命令历史
set history size 1000 # 历史记录条数
arm-none-eabi-gdb,别用本地gdb去调试ARM程序——那会报错说“格式不对”。
2.2 常用命令:调试的七种武器
GDB命令很多,但真正高频使用的就这几个。我按调试流程给你捋一遍。
2.2.1 break —— 断点,调试的起点
断点就是让程序停在你关心的位置。用法很简单:
(gdb) break main # 在main函数入口
(gdb) break 42 # 在当前文件第42行
(gdb) break file.c:100 # 在file.c第100行
(gdb) break func if x == 5 # 条件断点,后面细讲
我曾经在一个多线程程序里,断点打在共享变量的访问处,结果每次停的位置都不对——后来才发现是线程切换导致的。嗯,这里要注意:多线程调试时,断点默认对所有线程生效。
2.2.2 run —— 启动程序
设置好断点后,用 run 启动。可以带参数:
(gdb) run arg1 arg2 # 带命令行参数
(gdb) run < input.txt # 重定向输入
run 通常不用。我们一般用 target remote 连接开发板,然后用 continue 或 reset 启动。
2.2.3 next 与 step —— 单步的艺术
这两个命令的区别,我刚开始学的时候也搞混过。说白了:
- next (n):逐过程执行,遇到函数调用直接跳过,不进入函数内部。
- step (s):逐语句执行,遇到函数调用就钻进去。
(gdb) next # 执行下一行,不进入函数
(gdb) step # 执行下一行,进入函数
(gdb) nexti # 汇编级别,跳过一条指令
(gdb) stepi # 汇编级别,进入一条指令
你想想看,调试一个三层嵌套的函数调用时,用 next 还是 step?我的经验是:先 next 确认外层逻辑没问题,再 step 进可疑函数深挖。
2.2.4 continue —— 继续跑起来
断点停下后,想继续执行到下一个断点,就用 continue (c)。
(gdb) continue # 继续执行
(gdb) c # 简写
有一次我调试一个死循环,断点打在循环体里,每次 continue 都停在同一行。我当时纳闷了半天,后来才发现——循环条件写错了,根本跳不出来。
2.2.5 print —— 看变量的值
调试的核心就是看数据。print 命令能帮你看到变量当前的值。
(gdb) print x # 打印变量x
(gdb) print &x # 打印x的地址
(gdb) print *ptr # 打印指针指向的值
(gdb) print array[5] # 打印数组元素
(gdb) print /x 100 # 以十六进制打印
(gdb) print /t 100 # 以二进制打印
print /x 看寄存器值。比如 print /x $r0 看ARM的R0寄存器。这个在分析中断现场时特别有用。
2.2.6 backtrace —— 看调用栈
程序崩溃了,停在某个函数里,你肯定想知道它是怎么走到这一步的。backtrace (bt) 就是干这个的。
(gdb) backtrace # 打印调用栈
(gdb) bt # 简写
(gdb) bt full # 打印每个栈帧的局部变量
(gdb) frame 2 # 切换到第2层栈帧
我记得有一次,程序在 free() 时崩溃,bt 一看,调用栈里有个函数传了个野指针进来。顺着栈帧往上查,很快就定位到问题——是某个结构体成员没初始化。
2.3 条件断点与观察点
这两个功能,是GDB真正体现“深度”的地方。普通断点只能停,条件断点和观察点能帮你精准定位。
2.3.1 条件断点
条件断点就是“满足某个条件时才停”。比如循环1000次,你只关心第500次的情况:
(gdb) break 42 if i == 500
(gdb) break func if x > 100 && y < 0
我曾在项目中遇到一个bug:一个缓冲区在特定输入长度下才会溢出。用条件断点 break memcpy if len > 128,一下子就抓住了问题现场。你想想看,要是没有条件断点,你得手动按多少次 continue?
ignore 命令先忽略前面N次命中:
(gdb) break 42
(gdb) ignore 1 499 # 忽略断点1的前499次命中
2.3.2 观察点
观察点监控的是“内存地址的值发生变化”。你不需要指定在哪一行停,只要某个变量的值变了,GDB就会自动停下来。
(gdb) watch x # 监控变量x
(gdb) watch *ptr # 监控指针指向的内存
(gdb) watch (int*)0x20001000 # 监控绝对地址
(gdb) rwatch x # 监控读操作
(gdb) awatch x # 监控读写操作
我曾经调试过一个“变量莫名其妙被改”的bug。代码里明明没有赋值语句,但运行到某个点,值就变了。用 watch 监控那个变量,程序一停,发现是某个DMA回调函数通过指针写入了不该写的地方。嗯,这种问题,不用观察点根本查不出来。
2.4 知识体系总览
这一章的内容比较多,我画了张图帮你理清脉络。
2.5 实战小贴士
最后,分享几个我常用的调试技巧:
- 调试段错误:先
bt看调用栈,再frame切到可疑函数,print看局部变量。八成是野指针或数组越界。 - 调试死循环:按
Ctrl+C中断程序,bt看停在哪,print看循环变量。十有八九是循环条件写错了。 - 调试内存泄漏:用
watch监控malloc返回的地址,看它什么时候被free。没被释放就是泄漏。 - 调试多线程:用
thread apply all bt看所有线程的调用栈,快速定位哪个线程卡住了。
我的习惯:每次调试前,先在 .gdbinit 里配好 set print pretty on 和 set pagination off。这两个配置能省下不少时间。另外,嵌入式调试时,记得用 target remote :3333 连接OpenOCD或JLink,别傻乎乎地敲 run。