2. GDB基础命令
调试嵌入式程序,说白了就是跟代码的「灵魂」对话。GDB就是那个翻译官。我刚开始用GDB时,其实就靠七个命令活着——run、break、next、step、continue、print、quit。别小看这七个,它们能解决你90%的调试问题。
今天我就把这七个命令掰开揉碎了讲。每个命令我都会结合项目中的真实场景,告诉你什么时候用、怎么用、有什么坑。
2.1 启动与退出:run 和 quit
run 是启动程序的命令。用法很简单:在GDB里直接敲 run,程序就跑起来了。如果你想传参数,比如 ./app --port 8080,那就敲 run --port 8080。
重要提示: run 每次都会从头启动程序。如果你已经设了断点,程序会在第一个断点处停下。
quit 就是退出GDB。敲 quit 或者 q 都行。如果程序还在跑,GDB会问你是否确认退出。嗯,这里要注意——如果你在调试嵌入式设备上的远程程序,直接 quit 可能会让目标设备上的进程变成孤儿进程。我建议先 kill 掉再退出。
小技巧: 在GDB里敲 run < input.txt 可以重定向输入。我在调试串口协议解析时经常这么干,把抓到的数据包喂给程序。
2.2 断点管理:break
break 是GDB的灵魂。没有断点,调试就像闭着眼睛找东西。
基本用法:
(gdb) break main // 在main函数入口设断点
(gdb) break 42 // 在当前文件的第42行设断点
(gdb) break file.c:100 // 在file.c的第100行设断点
(gdb) break func_name // 在函数func_name入口设断点
我曾经在调试一个多线程的嵌入式系统时,遇到一个诡异的死锁问题。我在每个锁的获取和释放处都设了断点,然后一步步跟踪。最后发现是一个线程忘记释放锁了。如果没有断点,这种问题根本没法查。
避坑指南: 我曾经在调试优化后的代码时,发现断点位置跟源码对不上。这是因为编译器优化了代码顺序。解决办法:编译时加 -O0 关闭优化,或者用 -g 保留调试信息。
断点还可以加条件:
(gdb) break 42 if count == 5 // 当count等于5时才触发
(gdb) break func if errno != 0 // 只在出错时停住
条件断点特别适合排查循环中的问题。比如你有一个1000次的循环,只在第500次出问题,直接设条件断点,省得你手动按500次 continue。
2.3 单步执行:next 和 step
这两个命令经常让人混淆。我简单说一下区别:
| 命令 | 作用 | 会不会进入函数内部 |
|---|---|---|
| next (n) | 执行下一行代码 | 不会,把函数调用当一步 |
| step (s) | 执行下一行代码 | 会,进入函数内部 |
举个例子:
int main() {
int a = 10; // 断点停在这里
int b = add(a, 5); // 按next:直接执行完add,停在下一行
// 按step:进入add函数内部
printf("%d\n", b);
return 0;
}
我个人习惯是:调试自己的代码时多用 next,因为我知道函数内部没问题。调试别人的库或者系统调用时,如果怀疑参数传错了,就用 step 进去看看。
小技巧: 在嵌入式调试中,step 进入中断服务函数(ISR)特别有用。我曾经用 step 跟踪一个GPIO中断,发现中断标志位没清干净,导致反复进入中断。这种问题光看代码很难发现。
2.4 继续执行:continue
continue(简写 c)让程序继续跑,直到遇到下一个断点或者程序结束。
用法:
(gdb) continue // 继续执行
(gdb) continue 5 // 忽略当前断点5次,第6次停住
continue 5 这个用法知道的人不多。它相当于「跳过这个断点5次」。我在调试一个循环中的内存泄漏时,发现第10次循环才出问题。直接 continue 9,然后停在第十次,省时省力。
重要提示: continue 和 run 的区别:run 是重新启动程序,continue 是从当前暂停位置继续。如果你改了代码,必须重新 run 才能生效。
2.5 查看变量:print
print(简写 p)是调试时最常用的命令。它能打印变量、表达式、结构体、指针内容等。
基本用法:
(gdb) print a // 打印变量a的值
(gdb) print &a // 打印变量a的地址
(gdb) print *ptr // 打印指针指向的内容
(gdb) print array[5] // 打印数组第6个元素
(gdb) print sizeof(node) // 打印结构体大小
print 还能做类型转换和格式化输出:
(gdb) print /x 255 // 以十六进制打印:0xff
(gdb) print /d 0xff // 以十进制打印:255
(gdb) print /s str // 以字符串形式打印
(gdb) print (struct node*)0x1000 // 把地址强制转换成结构体指针
我在调试网络协议栈时,经常用 print /x 查看内存中的字节序。有一次发现一个粘包问题,就是用 print 逐字节打印缓冲区内容,发现两个数据包的头尾重叠了。
避坑指南: 我曾经在调试时用 print 打印一个很大的结构体,结果GDB卡住了。原因是结构体里有指针指向了非法地址。解决办法:用 print *ptr@10 只打印前10个元素,或者用 set print elements 0 限制打印长度。
2.6 命令组合实战:粘包调试场景
说了这么多,咱们来一个实战场景。假设你在调试一个TCP粘包问题,接收缓冲区里可能包含多个数据包。你想看看当前缓冲区里到底有几个包。
调试步骤:
- 在接收函数入口设断点:
break recv_data - 运行程序:
run - 程序停在断点处,查看缓冲区内容:
print /x buffer@64 - 单步执行到解析逻辑:
next - 查看解析后的包长度:
print pkt_len - 如果发现长度不对,step 进入解析函数:
step - 继续执行到下一个包:
continue - 重复步骤3-7,直到找到问题
- 退出GDB:
quit
你看,七个命令来回组合,就能解决一个复杂的粘包问题。你想想看,如果没有这些基础命令,你只能靠加printf来调试,改一次代码编译一次,效率低得可怕。
2.7 命令速查表
| 命令 | 简写 | 作用 | 常用场景 |
|---|---|---|---|
| run | r | 启动程序 | 开始调试、重新运行 |
| break | b | 设置断点 | 在关键位置暂停 |
| next | n | 单步执行(不进入函数) | 跳过函数调用 |
| step | s | 单步执行(进入函数) | 跟踪函数内部逻辑 |
| continue | c | 继续执行 | 跳到下一个断点 |
| p | 打印变量或表达式 | 查看变量值、内存内容 | |
| quit | q | 退出GDB | 结束调试会话 |
我的建议: 刚开始用GDB时,把这七个命令记熟。别急着学那些花哨的高级命令。我见过太多人一上来就学什么逆向执行、线程调试,结果连next和step都分不清。基础打牢了,后面学什么都快。
2.8 本章知识体系
下面这张图展示了七个基础命令之间的关系和调试流程:
这张图展示了调试的基本流程:启动GDB → run运行 → break设断点 → 程序暂停 → 选择调试方式(next/step/continue)→ 配合print查看变量 → 最终quit退出。记住这个流程,调试就不再是盲人摸象了。
好了,七个基础命令就讲完了。说实话,我用了十年GDB,最常用的还是这七个。后面的章节我们会在这个基础上,加入更多高级技巧,比如条件断点、观察点、回溯调用栈等等。但今天先把这七个练熟,你就能应付大部分调试场景了。
核心要点回顾:
- run 启动程序,quit 退出GDB
- break 设断点,支持行号、函数名、条件
- next 跳过函数,step 进入函数
- continue 继续执行到下一个断点
- print 查看变量,支持格式化输出
- 七个命令组合使用,解决实际调试问题
可可精品资料库