2. 嵌入式系统基础

调试嵌入式软件,说白了就是跟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     # 历史记录条数
小技巧:嵌入式调试时,记得在交叉编译工具链里找对应的gdb版本。比如用ARM GCC,就用 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?

效率提示:条件断点比普通断点慢,因为GDB每次都要评估条件。如果循环次数很大(比如百万级),建议用 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回调函数通过指针写入了不该写的地方。嗯,这种问题,不用观察点根本查不出来。

硬件限制:嵌入式平台上的观察点数量有限(通常4个)。而且观察点依赖硬件调试模块,如果MCU不支持,GDB会退化为软件模拟,那速度就慢得离谱了。

2.4 知识体系总览

这一章的内容比较多,我画了张图帮你理清脉络。

GDB深度实战知识体系 GDB深度实战知识体系 GDB调试工具链 安装与配置 七种常用命令 条件断点与观察点 apt/yum/brew安装 交叉编译gdb配置 .gdbinit个性化配置 break — 设置断点 run — 启动程序 next/step — 单步执行 continue — 继续运行 print — 查看变量 backtrace — 调用栈 条件断点 (if 条件) ignore 忽略次数 watch 写观察点 rwatch/awatch 读写观察 硬件观察点限制 核心思路:先装好环境 → 掌握七种命令 → 用条件断点和观察点精准定位 调试不是碰运气,而是有方法论的

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。