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:关掉确认提示,操作起来更流畅。
小提示:如果你调试的是内核模块,建议在 .gdbinit 里加上 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 函数入口设了个断点,然后一步步跟进去,才发现是某个任务控制块的指针被意外清零了。如果没有断点,这种问题靠猜是猜不出来的。

注意:在嵌入式调试中,如果目标板是 ROM 运行,硬件断点数量有限(通常只有 2-4 个)。别一股脑设十几个断点,到时候全失效了你还不知道。

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 分析的关键在于:不要只看崩溃的那一行,要看调用链,看数据流。崩溃点往往只是结果,原因可能在几层调用之前。

避坑指南:我曾经遇到一个情况,Core Dump 文件有 2GB 大,加载进去后 GDB 卡了半天。后来发现是 ulimit 没设好,系统把整个内存都 dump 出来了。建议设置 ulimit -c 1048576(1GB),够用就行,别太大。

小结

GDB 基础调试,说白了就是三板斧:断点停住、单步跟踪、事后分析。这三招用熟了,大部分 bug 都逃不出你的手掌心。

我个人觉得,调试能力不是看书看出来的,是踩坑踩出来的。你想想看,每次遇到一个 bug,你花半小时定位,下次再遇到类似的,可能五分钟就搞定了。这就是经验。

下一章我们会聊更高级的调试技巧——条件断点、观察点、以及如何调试多线程程序。到时候你会发现,GDB 远比你想象的强大。