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调试的标准流程——下断点、启动、单步跟踪、观察变量、定位问题。你想想看,如果没有GDB,这种数组越界的bug得找多久?

知识体系总览

下面这张图总结了本章的核心命令及其关系:

GDB核心命令知识体系 控制流命令 断点命令 观察命令 run next / step continue quit break 条件断点 ignore delete print list display x(内存) 调试流程 1. 编译时加 -g 选项 2. gdb ./program 启动调试器 3. break 下断点 → run 启动 → next/step 单步跟踪 4. print 观察变量 → 定位问题 → quit 退出

这七个命令,说白了就是GDB调试的七种武器。你只要把它们练熟了,90%的调试场景都能应付。剩下的10%,等遇到具体问题时再查文档也来得及。

记住一点:调试不是碰运气,而是有方法论的。先想清楚问题可能出在哪,再选择合适的命令去验证。这样效率最高。

我的建议:初学者可以先在简单的程序上反复练习这七个命令,直到形成肌肉记忆。等你真正遇到棘手的bug时,这些基础命令就是你最可靠的武器。