3、GDB基础调试:GDB安装与配置、断点设置、单步执行、查看寄存器与内存、Core Dump分析
调试,说白了就是给程序看病。GDB 就是那个听诊器。
我刚开始做嵌入式那会儿,最怕的就是程序跑飞了。没有 GDB,全靠 printf 打日志,打完还得重新编译烧录,效率低得让人抓狂。后来我师父扔给我一句话:「学会 GDB,你才算真正入了内核调试的门。」
嗯,今天我们就来聊聊 GDB 的基础调试。别小看这些基础操作,我见过不少老手,断点设得飞起,但一遇到 Core Dump 就懵了。咱们一步步来。
3.1 GDB 的安装与配置
GDB 的安装其实很简单,但不同平台略有差异。我个人习惯在 Linux 环境下工作,所以以 Ubuntu 为例:
sudo apt-get update
sudo apt-get install gdb gdb-multiarch
为什么要装 gdb-multiarch?因为微内核调试经常要交叉编译,目标架构可能是 ARM、RISC-V 或者 MIPS。这个版本支持多架构,省心。
装完之后,建议做一点基础配置。我习惯在 ~/.gdbinit 里写几行:
set pagination off
set print pretty on
set confirm off
- set pagination off:关掉分页,免得每次输出一屏就停下来问你「按回车继续」。
- set print pretty on:结构体打印时自动换行缩进,看着舒服。
- set confirm off:关掉确认提示,操作起来更流畅。
set architecture i386:x86-64:intel 之类的架构声明,避免 GDB 猜错指令集。
3.2 断点设置——让程序在你想停的地方停下来
断点,是调试的起点。没有断点,程序就像脱缰的野马,你根本抓不住它。
GDB 里设置断点的方式有好几种,我挑最常用的说:
| 命令 | 说明 | 示例 |
|---|---|---|
break <行号> |
在指定行设置断点 | break 42 |
break <函数名> |
在函数入口设置断点 | break task_switch |
break <文件名>:<行号> |
在指定文件的某行设断点 | break scheduler.c:128 |
break <地址> |
在内存地址设断点 | break *0x40001000 |
我曾经调试一个微内核的调度器,发现任务切换时偶尔会触发一个空指针异常。我在 task_switch 函数入口设了个断点,然后一步步跟进去,才发现是某个任务控制块的指针被意外清零了。如果没有断点,这种问题靠猜是猜不出来的。
3.3 单步执行——像看电影慢放一样看代码
断点停住之后,接下来就是单步执行。GDB 提供了几个命令:
- step (s):步入,进入函数内部。
- next (n):步过,不进入函数,直接执行完当前函数调用。
- finish:执行到当前函数返回。
- continue (c):继续执行,直到下一个断点。
我个人的习惯是:遇到自己写的函数用 step,遇到库函数用 next。你想想看,如果每次 printf 都 step 进去,那得看到猴年马月去?
举个例子,假设我们有一段代码:
void init_task(struct task *t) {
t->id = 1;
t->state = TASK_READY;
t->priority = 10;
set_priority(t, t->priority); // 这里我想进去看看
}
在 GDB 里:
(gdb) break init_task
(gdb) run
(gdb) step // 进入 init_task
(gdb) next // 执行 t->id = 1
(gdb) next // 执行 t->state = TASK_READY
(gdb) step // 进入 set_priority 函数内部
这样一步步走下来,变量的变化一目了然。我曾经用这个方法抓到一个优先级反转的 bug——某个任务的优先级被错误地设置成了负数,导致调度器直接崩溃。
3.4 查看寄存器与内存——深入底层
单步执行只能看到 C 语言层面的东西。但微内核调试,很多时候需要看底层。寄存器、内存,这些才是硬道理。
查看寄存器:
(gdb) info registers
(gdb) info registers rsp rbp rip // 只看这几个
查看内存:
(gdb) x/10x $rsp // 以十六进制显示栈顶的 10 个单元
(gdb) x/20i $rip // 以指令形式显示当前指令附近的 20 条指令
(gdb) x/s 0x40001000 // 以字符串形式显示某个地址的内容
这里 x 是 examine 的缩写,格式是 x/<数量><格式> <地址>。格式可以是:
x:十六进制d:十进制i:指令s:字符串c:字符
我记得有一次调试一个内存越界的问题,程序跑着跑着就挂了。我怀疑是栈被破坏了,于是在函数入口和出口分别打印了 $rsp 的值。结果发现栈指针在函数内部竟然减少了 0x200 字节,而局部变量只需要 0x40 字节——明显是某个数组越界写穿了栈。
3.5 Core Dump 分析——事后诸葛亮也是诸葛亮
有些 bug 不是每次都能复现的。跑了一整天,突然崩了,你又没开着 GDB 盯着。怎么办?
这时候 Core Dump 就派上用场了。Core Dump 是程序崩溃时操作系统把内存映像保存下来的文件。你可以事后用 GDB 加载它,像调试活着的程序一样分析死因。
首先,确保系统允许生成 Core Dump:
ulimit -c unlimited // 临时设置
echo "core" > /proc/sys/kernel/core_pattern // 设置生成路径
程序崩溃后,会生成一个 core 文件。然后用 GDB 加载:
gdb ./your_program core
进去之后,第一件事就是看崩溃位置:
(gdb) bt // backtrace,查看调用栈
(gdb) info registers // 看看寄存器状态
(gdb) frame 0 // 切换到最内层的栈帧
(gdb) list // 看看崩溃点附近的源代码
我曾经分析过一个 Core Dump,程序在 memcpy 里崩溃了。调用栈显示是从一个网络驱动函数调过来的。我看了下寄存器,rdi 指向的地址是 0x0——典型的空指针传参。再往上查,发现是某个消息队列在初始化时没有分配缓冲区,导致后续的 memcpy 直接写到了空地址上。
Core Dump 分析的关键在于:不要只看崩溃的那一行,要看调用链,看数据流。崩溃点往往只是结果,原因可能在几层调用之前。
ulimit -c 1048576(1GB),够用就行,别太大。
小结
GDB 基础调试,说白了就是三板斧:断点停住、单步跟踪、事后分析。这三招用熟了,大部分 bug 都逃不出你的手掌心。
我个人觉得,调试能力不是看书看出来的,是踩坑踩出来的。你想想看,每次遇到一个 bug,你花半小时定位,下次再遇到类似的,可能五分钟就搞定了。这就是经验。
下一章我们会聊更高级的调试技巧——条件断点、观察点、以及如何调试多线程程序。到时候你会发现,GDB 远比你想象的强大。