2. GDB基础命令
说实话,很多嵌入式工程师对GDB的认知停留在「会启动、会看堆栈」的层面。但GDB真正强大的地方,在于它那套精炼的命令体系。今天我就把这几个最常用的命令掰开揉碎了讲清楚。
我个人习惯把GDB命令分成三类:控制流命令、断点命令、观察命令。咱们一个一个来。
1. run —— 启动程序的正确姿势
run命令,说白了就是让程序跑起来。但这里有个坑——很多人不知道run可以带参数。
(gdb) run arg1 arg2 # 带命令行参数启动
(gdb) run < input.txt # 重定向输入
(gdb) run 2> error.log # 重定向错误输出
set args设置参数,然后直接run。这样不用每次都敲一遍参数。
(gdb) set args -c config.ini --verbose
(gdb) run
2. break —— 断点,调试的灵魂
断点就是告诉GDB:「嘿,跑到这里停一下」。我见过有人只会break main,其实断点的花样多着呢。
| 命令形式 | 含义 |
|---|---|
break main |
在main函数入口处下断点 |
break 42 |
在当前文件的第42行下断点 |
break file.c:100 |
在file.c的第100行下断点 |
break func if x == 5 |
条件断点,x等于5时才触发 |
break *0x4004c0 |
在内存地址0x4004c0处下断点 |
ignore命令跳过前N次。
(gdb) break loop_func if count == 1000
(gdb) ignore 1 999 # 忽略断点1的前999次触发
3. next 与 step —— 单步调试的两兄弟
这两个命令初学者最容易搞混。我简单说一下:
- next (n):单步执行,遇到函数调用直接跳过,不进入函数内部
- step (s):单步执行,遇到函数调用就钻进去
举个例子你就明白了:
// 源代码
int result = compute(a, b); // 当前停在这里
printf("result = %d\n", result);
// 输入 next → 直接执行完 compute,停在 printf 这一行
// 输入 step → 跳进 compute 函数内部
我在项目中遇到过一个问题:调试一个多层嵌套的函数调用时,不小心用了step,结果一头扎进了标准库的源码里。那叫一个晕啊。后来我养成了习惯——不确定函数内部有没有问题时,先用next跳过,等确认有问题了再用step进去看。
4. continue —— 继续跑,别停
continue (c)就是让程序从当前断点继续执行,直到遇到下一个断点或程序结束。
(gdb) c # 继续执行
(gdb) c 5 # 忽略当前断点5次,第6次才停下
continue 5比手动按5次c要高效得多。我调试一个1000次的循环时,经常用continue 999直接跳到最后一次。
5. print —— 看变量的值
print (p)是调试时用得最频繁的命令之一。它能打印变量、表达式、甚至调用函数。
(gdb) p x # 打印变量x的值
(gdb) p/x 0x100 # 以十六进制打印
(gdb) p &x # 打印x的地址
(gdb) p *ptr # 打印指针指向的值
(gdb) p array[5] # 打印数组元素
(gdb) p func() # 调用函数并打印返回值
格式控制也很重要:
| 格式 | 说明 |
|---|---|
p/x |
十六进制 |
p/d |
十进制 |
p/o |
八进制 |
p/t |
二进制 |
p/c |
字符形式 |
p/f |
浮点数 |
p打印了一个大结构体,结果GDB卡住了。为什么?因为那个结构体有几百个成员,而且有些成员是函数指针,GDB试图解析它们。后来我学会了用p/x *ptr@10只打印前10个字节,或者用set print elements 0限制打印长度。
6. list —— 看源码,找上下文
list (l)用来显示源代码。调试时经常需要看看当前执行位置附近的代码。
(gdb) l # 显示当前行附近的10行
(gdb) l 42 # 显示第42行附近的代码
(gdb) l main # 显示main函数附近的代码
(gdb) l file.c:100 # 显示file.c第100行附近的代码
(gdb) l - # 显示当前行的前10行
我个人习惯把list和break配合使用:先用list找到想下断点的位置,然后break。这样不用记行号。
7. quit —— 优雅地退出
quit (q)就是退出GDB。但要注意,如果程序还在运行,GDB会问你是否确认退出。
(gdb) q # 退出GDB
Are you sure you want to quit? (y or n) y
如果你不想被询问,可以用quit y直接退出。
实战演练:一个完整的调试流程
说了这么多,咱们来走一遍完整的调试流程。假设我有这样一个有bug的程序:
// bug.c
#include <stdio.h>
int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int sum = 0;
for (int i = 0; i <= 5; i++) { // 注意这里:i <= 5,数组越界了
sum += arr[i];
}
printf("sum = %d\n", sum);
printf("5! = %d\n", factorial(5));
return 0;
}
调试过程:
$ gcc -g -o bug bug.c
$ gdb ./bug
(gdb) break main # 在main入口下断点
(gdb) run # 启动程序,停在main入口
(gdb) list # 看看源码
(gdb) break 14 # 在循环处下断点
(gdb) continue # 继续执行到循环
(gdb) print i # 打印循环变量
$1 = 0
(gdb) print arr[i] # 打印数组元素
$2 = 1
(gdb) next # 单步执行一次
(gdb) print i
$3 = 1
(gdb) print arr[i]
$4 = 2
# 快速跳到循环结束
(gdb) continue 4 # 忽略当前断点4次
(gdb) print i
$5 = 5 # 问题来了!i=5时还在循环里
(gdb) print arr[5] # 数组越界访问
$6 = 0 # 垃圾值
(gdb) quit # 退出GDB,修复bug
知识体系总览
下面这张图总结了本章的核心命令及其关系:
这七个命令,说白了就是GDB调试的七种武器。你只要把它们练熟了,90%的调试场景都能应付。剩下的10%,等遇到具体问题时再查文档也来得及。
记住一点:调试不是碰运气,而是有方法论的。先想清楚问题可能出在哪,再选择合适的命令去验证。这样效率最高。