第1章:GDB基本命令——启动与退出、设置断点、单步执行、查看变量、backtrace调用栈

各位同学好,我是你们的嵌入式调试讲师。今天咱们聊聊GDB最基础、也是最核心的几个命令。说实话,我见过太多工程师拿着GDB只会设个断点、打个print,遇到复杂bug就抓瞎。其实啊,把基础命令用熟了,80%的问题都能搞定。

我个人习惯把GDB调试比作「手术刀」——你得知道怎么下刀、怎么观察病灶、怎么追溯病因。这一章,咱们就把这把刀磨锋利。

1.1 启动与退出:别小看这一步

启动GDB的方式有好几种,我工作中最常用的是这两种:

# 方式一:直接调试可执行文件
gdb ./my_program

# 方式二:先启动GDB,再加载文件
gdb
(gdb) file ./my_program

嗯,这里有个坑。我曾经在调试一个交叉编译的ARM程序时,直接敲了gdb ./arm_binary,结果GDB报错说格式不对。你想想看,本地GDB怎么能调试ARM指令呢?后来我换成了arm-linux-gnueabihf-gdb才搞定。

注意:跨平台调试时,一定要用对应的交叉编译工具链中的GDB版本。别拿本地GDB去调试ARM/MIPS程序,会出大问题。

退出GDB很简单:

(gdb) quit
# 或者简写
(gdb) q

如果程序还在运行,GDB会问你是否要终止。我一般直接y确认。

1.2 设置断点:调试的起点

断点就是让程序停在你关心的位置。说白了,没有断点,调试就无从谈起。

常用的断点设置方式:

命令 说明 示例
break 行号 在指定行设置断点 break 42
break 函数名 在函数入口设置断点 break main
break 文件:行号 在指定文件的某行设断点 break src/main.c:88
break 文件:函数名 在指定文件的函数设断点 break src/utils.c:parse_data

我在项目中遇到过一种情况:一个bug只在循环第100次时才出现。你总不能手动按100次continue吧?这时候条件断点就派上用场了:

(gdb) break 42 if i == 100

这样程序只会在i等于100时停下来,省时省力。

小技巧:用info break查看所有断点,用delete 断点编号删除指定断点。我习惯给断点起名字,比如break main if argc > 1,方便记忆。

1.3 单步执行:一步步看程序怎么跑

设好断点后,程序停下来了。接下来怎么走?GDB提供了几个「步进」命令:

  • next(或n):单步执行,遇到函数调用不进入,直接跳过。
  • step(或s):单步执行,遇到函数调用会进入函数内部。
  • finish:一直执行到当前函数返回。
  • continue(或c):继续执行,直到下一个断点。

举个例子,假设你有这么一段代码:

int main() {
    int a = 10;
    int b = add_one(a);  // 第3行
    printf("%d\n", b);   // 第4行
    return 0;
}

在第3行停下时:

  • 敲next,直接执行完add_one,停在printf那行。
  • 敲step,会跳进add_one函数内部,一行行看它是怎么算的。

我个人习惯是:确认函数没问题就用next跳过;怀疑函数有bug就用step进去看。你想想看,如果每次都step进标准库函数,那得累死。

1.4 查看变量:调试的核心目的

断点停了,步进走了,最终目的是什么?看变量值对不对啊!

最常用的命令:

(gdb) print 变量名
# 例如
(gdb) print i
$1 = 42

(gdb) print &i
$2 = (int *) 0x7fffffffe4c

(gdb) print array[0]
$3 = 100

还可以用display命令让变量自动显示:

(gdb) display i
1: i = 42
(gdb) display array[0]
2: array[0] = 100

这样每次单步执行时,GDB会自动打印这些变量的值。我曾经调试一个内存泄漏问题,就是靠display盯着指针地址的变化,才发现有个地方忘了释放。

重点:对于结构体变量,用print *ptr可以打印指针指向的内容。用print struct_var.field可以访问结构体成员。

1.5 backtrace调用栈:追根溯源的法宝

程序崩溃了,停在某个函数里。你怎么知道它是怎么走到这一步的?这时候backtrace(简写bt)就派上用场了。

(gdb) bt
#0  0x00005555555546a7 in divide (a=10, b=0) at calc.c:12
#1  0x00005555555546d3 in main () at main.c:8

这个输出告诉我们:程序当前在divide函数里(第0帧),是main函数调用了它(第1帧)。每一帧都显示了函数名、参数、文件和行号。

如果想看更详细的信息,可以用:

(gdb) bt full
#0  0x00005555555546a7 in divide (a=10, b=0) at calc.c:12
        result = 0
#1  0x00005555555546d3 in main () at main.c:8
        x = 10
        y = 0

这样连局部变量值都一起打印出来了。

我曾经遇到一个递归调用栈溢出的bug。程序跑着跑着就崩了,用bt一看,好家伙,调用栈有3000多层!原来递归函数忘了写终止条件。没有backtrace,这种问题排查起来就像大海捞针。

避坑指南:如果程序没有编译时加-g选项,bt只能看到地址,看不到函数名和行号。我曾经因为这个浪费了半天时间——编译时忘了加-g,调试信息全丢了。

本章小结

这一章咱们把GDB最基础的几个命令过了一遍:

  • 启动退出:gdb ./program 和 quit
  • 设置断点:break、条件断点、info break
  • 单步执行:next、step、finish、continue
  • 查看变量:print、display
  • 调用栈:backtrace、bt full

这些命令就像工具箱里的螺丝刀、扳手——单个用可能不起眼,组合起来威力巨大。下一章咱们会深入讲讲「条件断点」和「观察点」的高级用法,到时候你会发现,原来调试可以这么爽。

记住:调试不是碰运气,而是有方法论的。把基础打牢,遇到再诡异的bug也不慌。

GDB调试核心命令知识体系 GDB调试命令 启动与退出 设置断点 单步执行 查看变量 backtrace gdb ./prog quit / q break 行号/函数 条件断点 next / step finish / continue print / display print *ptr bt / bt full 帧信息查看 核心思路:停 → 看 → 走 → 查 → 追

可可精品资料库