3. 主流调试器与工具链概览
调试器这东西,说白了就是给嵌入式程序装了个「慢放键」。GDB 是我用得最多的调试工具,没有之一。刚开始接触时我也觉得命令太多记不住,但后来发现,真正核心的就那么几个——断点、单步、看变量、查内存。把这四样玩熟了,90% 的 bug 都能手到擒来。
3.1 断点管理:让程序在指定位置停下来
断点是调试的起点。没有断点,程序一跑到底,你根本不知道中间发生了什么。
3.1.1 设置断点的几种方式
| 命令 | 说明 | 示例 |
|---|---|---|
break <行号> |
在源文件指定行设断点 | break 42 |
break <函数名> |
在函数入口设断点 | break main |
break <文件名:行号> |
在多文件项目中指定文件 | break uart.c:87 |
break <行号> if <条件> |
条件断点,满足条件才停下 | break 55 if count == 10 |
我个人习惯在函数入口先设一个断点,确认程序确实执行到了这里。有一次调试一个串口驱动,怎么调都不进中断,后来发现是中断向量表配错了——断点都没触发,问题一目了然。
if i == 500 就行,不用一次次手动跳过。
3.1.2 管理断点
(gdb) info breakpoints # 查看所有断点
(gdb) disable 2 # 禁用编号为2的断点
(gdb) enable 2 # 重新启用
(gdb) delete 2 # 删除断点
(gdb) clear main # 清除main函数处的断点
嗯,这里要注意:delete 是按编号删,clear 是按位置删。我刚开始时经常搞混,结果想删一个断点,把所有的都删了……
3.2 单步执行:一步步看清程序的轨迹
断点让程序停下来,单步执行则让你看清每一步发生了什么。
3.2.1 单步命令对比
| 命令 | 缩写 | 行为 | 适用场景 |
|---|---|---|---|
step |
s |
进入函数内部 | 想查看被调函数的执行细节 |
next |
n |
不进入函数,直接执行完 | 确认函数返回值,不关心内部 |
finish |
fin |
执行到当前函数返回 | 不小心 step 进去了,想快速退出 |
continue |
c |
继续运行到下一个断点 | 跳过已确认无误的代码段 |
你想想看,如果每次调用 printf 都用 step 进去,那得等到猴年马月。我一般用 next 跳过库函数,只在自定义函数上用 step。
step 进入了调度器内部,结果 GDB 直接卡死。后来才知道,调度器代码涉及栈切换,单步执行很容易跑飞。遇到这种情况,建议在关键位置设断点,用 continue 跳转。
3.3 变量查看:实时监控数据变化
程序停下来了,总得看看变量值对不对吧?
3.3.1 基本变量查看命令
(gdb) print count # 打印变量 count 的值
(gdb) print &count # 打印 count 的地址
(gdb) print *ptr # 打印指针指向的内容
(gdb) display count # 每次停下时自动显示 count
(gdb) undisplay 1 # 取消编号为1的自动显示
display 是我最常用的命令之一。调试循环时,把循环变量设成自动显示,每次单步都能看到它的变化,省得一遍遍敲 print。
3.3.2 查看复杂数据结构
(gdb) print my_struct # 打印整个结构体
(gdb) print my_struct->member # 打印结构体成员
(gdb) print array[10] # 打印数组第11个元素
(gdb) print *array@5 # 打印数组前5个元素
我记得有一次调试一个链表操作,程序总是崩溃。用 print *node 一看,发现某个节点的 next 指针指向了 0xDEADBEEF——典型的野指针问题。这种问题光看代码很难发现,但一查变量就原形毕露了。
3.4 内存操作:直接与硬件对话
嵌入式调试和普通软件调试最大的区别,就是我们需要直接操作内存。寄存器、外设地址、DMA 缓冲区……这些都在内存里。
3.4.1 查看内存
(gdb) x/10x 0x40020000 # 以十六进制查看10个单元
(gdb) x/10d &buffer # 以十进制查看buffer的10个元素
(gdb) x/10c &str # 以字符形式查看字符串
(gdb) x/10i $pc # 查看当前指令附近的10条汇编
x 命令的格式是 x/<数量><格式> <地址>。格式包括:x(十六进制)、d(十进制)、c(字符)、i(指令)、s(字符串)。
3.4.2 修改内存
(gdb) set var count = 100 # 修改变量
(gdb) set {int}0x40020000 = 0 # 修改指定内存地址的值
(gdb) set $r0 = 0x1234 # 修改寄存器值
这个功能在测试边界条件时特别有用。比如你想测试一个缓冲区满的情况,直接 set var index = BUFFER_SIZE - 1,不用等程序慢慢跑满。
print 确认当前值,改完后立即用 continue 让程序继续运行,不要长时间停在调试状态。
3.5 知识结构总览
下面这张图把 GDB 核心命令的关系梳理清楚了。调试时先设断点,然后单步执行,同时查看变量和内存,发现问题后可以直接修改——这是一个完整的闭环。
3.6 实战演练:一个完整的调试流程
光说不练假把式。我们用一个实际例子串一遍:
// bug.c - 一个故意写错的程序
#include <stdio.h>
int sum_array(int *arr, int len) {
int sum = 0;
for (int i = 0; i <= len; i++) { // 注意:这里应该是 <,不是 <=
sum += arr[i];
}
return sum;
}
int main() {
int data[] = {1, 2, 3, 4, 5};
int result = sum_array(data, 5);
printf("Sum = %d\n", result);
return 0;
}
调试步骤:
- 编译带调试信息:
gcc -g -o bug bug.c - 启动 GDB:
gdb ./bug - 设断点:
break sum_array - 运行:
run - 单步:用
next一步步走,同时用display i和display sum - 发现问题:当
i变成 5 时,循环还在继续,说明条件写成了<= - 查看内存:
x/6d data确认数组越界后读取了垃圾值 - 修复:把
<=改成<,重新编译运行