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)
- 调试实时性要求高的场景,硬件断点不影响执行速度
- 软件断点失效时,作为备选方案
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 调试的核心脉络:
3.6 实战避坑指南
最后分享几个我踩过的坑:
- 断点打不上? 先确认代码有没有被优化掉。用
-O0编译最保险。 - 观察点不触发? 检查变量是不是被编译器优化成了寄存器变量。可以加
volatile关键字强制内存访问。 - 硬件断点不够用? 优先用软件断点,只在必要时用硬件断点。我一般留 1-2 个硬件断点给紧急情况。
- 多线程调试? 默认断点会停住所有线程。用
set scheduler-locking on可以只停当前线程。
info breakpoints 查看当前所有断点,避免重复设置。调试结束后用 delete 清空断点,防止下次调试时被旧断点干扰。