3. GDB 基础调试:安装、命令与断点类型

GDB,全称 GNU Debugger,是我们做 ARM64 底层调试的瑞士军刀。说实话,我见过不少工程师用 IDE 点几下就完事,但真到了现场分析 coredump、定位内核崩溃的时候,还是得靠 GDB 这把老骨头。这一章,咱们就把 GDB 的基础操作捋一遍。

3.1 安装 GDB

在 Ubuntu 或 Debian 系统上,安装很简单:

sudo apt-get update
sudo apt-get install gdb gdb-multiarch

如果你要调试 ARM64 的交叉编译目标,gdb-multiarch 是必须的。我个人习惯在宿主机上装两个版本:一个本地 GDB,一个 multiarch。为什么?因为有时候本地 GDB 版本太新,跟目标板上的 gdbserver 不兼容,这时候 multiarch 反而更稳。

小提示: 装完后用 gdb --version 确认版本。我建议至少 8.0 以上,新版本对 Python 脚本的支持更好,后面咱们做自动化分析时会用到。

3.2 基本命令速览

GDB 的命令很多,但常用的就这几个。我把它们分成三组,方便你记忆。

命令 缩写 作用
break b 设置断点
run r 启动程序
next n 单步执行,不进入函数
step s 单步执行,进入函数
continue c 继续运行到下一个断点
print p 打印变量或寄存器值
info registers i r 查看所有寄存器

举个例子,假设我们有一个简单的 C 程序 test.c:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 10;
    int y = 20;
    int z = add(x, y);
    printf("Result: %d\n", z);
    return 0;
}

编译时记得加 -g 选项:

gcc -g -o test test.c

然后启动 GDB:

gdb ./test

在 GDB 里,你可以这样操作:

(gdb) break main          # 在 main 函数入口设断点
(gdb) run                  # 运行程序,停在 main
(gdb) next                 # 执行下一行(不进入 add)
(gdb) print x              # 打印 x 的值
(gdb) step                 # 进入 add 函数
(gdb) info registers       # 查看当前寄存器状态
(gdb) continue             # 继续运行到结束
关键区别: next 和 step 的区别在于是否进入函数。我在调试第三方库时,经常先用 next 跳过不关心的函数,等到了关键调用再用 step 钻进去。

3.3 断点类型详解

GDB 支持三种断点:软件断点、硬件断点和观察点。很多人以为断点都一样,其实不然。选错了类型,轻则调试效率低,重则根本断不下来。

3.3.1 软件断点

软件断点是最常用的。它的原理是在目标地址插入一条 BRK 或 INT3 指令,程序执行到这里就会触发异常,GDB 捕获后暂停。

设置方式:

(gdb) break *0x4004c0     # 在指定地址设断点
(gdb) break add           # 在函数名设断点
(gdb) break main if x > 5 # 条件断点

软件断点的优点是数量不限,缺点是在某些只读内存区域(比如 Flash)上无法使用。我记得有一次调试 Bootloader,代码在 Flash 里跑,软件断点死活打不上去,折腾了半天才意识到问题。

3.3.2 硬件断点

硬件断点依赖 CPU 的调试寄存器。ARM64 架构提供了最多 6 个硬件断点寄存器(DBGBCR0-5),每个可以监控一个地址。

设置方式:

(gdb) hbreak main         # 硬件断点
(gdb) hbreak *0x4004c0    # 在指定地址设硬件断点

什么时候用硬件断点?

  • 调试只读内存中的代码(如 Flash、ROM)
  • 调试实时性要求高的场景,硬件断点不影响执行速度
  • 软件断点失效时,作为备选方案
注意: 硬件断点数量有限。ARM64 通常只有 4-6 个,用完了 GDB 会报错。我曾经在调试一个多线程死锁问题时,一口气设了 8 个硬件断点,结果 GDB 直接罢工。后来学乖了,先规划好再动手。

3.3.3 观察点

观察点用来监控内存地址的读写。当指定地址的值发生变化时,程序自动暂停。这在追踪变量被谁修改时特别好用。

设置方式:

(gdb) watch x             # 监控变量 x 的写入
(gdb) rwatch x            # 监控变量 x 的读取
(gdb) awatch x            # 监控变量 x 的读写

举个例子,假设你发现全局变量 counter 莫名其妙变成了负数:

(gdb) watch counter
(gdb) continue

程序会在 counter 被修改的瞬间停下,你就能看到是哪一行代码干的。

观察点也分硬件和软件两种。GDB 会优先尝试硬件观察点,如果硬件不支持或资源不足,就回退到软件观察点。软件观察点效率很低,每执行一条指令都要检查一次,所以尽量别在循环里用。

3.4 三种断点对比

类型 命令 原理 数量限制 适用场景
软件断点 break 插入 BRK 指令 无限制 普通代码调试
硬件断点 hbreak 使用调试寄存器 4-6 个 只读内存、实时调试
观察点 watch 监控内存访问 有限 追踪变量修改

3.5 知识结构图

下面这张图帮你理清 GDB 调试的核心脉络:

GDB 调试核心知识结构 GDB 调试核心知识结构 GDB 调试 安装与配置 基本命令 断点类型 gdb gdb-multiarch break run next/step 软件断点 硬件断点 观察点 核心要点 软件断点最常用,硬件断点用于只读内存,观察点用于追踪变量修改

3.6 实战避坑指南

最后分享几个我踩过的坑:

  • 断点打不上? 先确认代码有没有被优化掉。用 -O0 编译最保险。
  • 观察点不触发? 检查变量是不是被编译器优化成了寄存器变量。可以加 volatile 关键字强制内存访问。
  • 硬件断点不够用? 优先用软件断点,只在必要时用硬件断点。我一般留 1-2 个硬件断点给紧急情况。
  • 多线程调试? 默认断点会停住所有线程。用 set scheduler-locking on 可以只停当前线程。
我的习惯: 每次开始调试前,先用 info breakpoints 查看当前所有断点,避免重复设置。调试结束后用 delete 清空断点,防止下次调试时被旧断点干扰。