跳到主要内容

11.2 概念上理解 KGDB 的工作原理

上一节我们搭建好了 SEALS 项目,拿到了「硬件」(虽然是虚拟的)和根文件系统。现在,我们要把真正的武器——调试器——搬到内核战场。

KGDB 允许你进行源码级的内核调试。这是什么概念?意味着你不再是面对一堆汇编指令或者模糊的 Oops 报告,而是可以直接停在 C 语言代码的某一行,查看变量,单步执行。

但你可能马上会意识到一个问题,这个问题也是 KGDB 设计的核心难点。

停顿与悖论:谁来调试调试器?

我们用 GDB 调试普通应用程序时,GDB 是运行在操作系统之上的,它随时可以暂停那个倒霉的应用进程。但如果我们要调试的是内核本身呢?

当 GDB 暂停内核去执行断点指令时,整个操作系统都停了。CPU 在那一刻被冻结在内核态。这时候,谁来运行 GDB?谁来处理网络包?谁来响应键盘输入?

这是一个典型的「自己给自己做手术」的问题。如果你一边给自己做阑尾炎手术,一边还要自己拿着手术刀,这显然是不可能的。

为了解决这个悖论,GDB 采用了客户端-服务端 架构,而这需要两台机器(或者两个隔离的运行环境)的配合。

舞台双主演:Host 与 Target

KGDB 的解决方案是把 GDB 拆成两半,分别放在两个不同的世界里:

  1. Host(宿主机):这是你平时用的那台电脑(或者是我们的 x86_64 虚拟机)。上面运行着GDB 客户端。这是一个庞然大物,包含了所有漂亮的符号解析、源代码显示、TUI 图形界面等功能。它是「指挥官」。
  2. Target(目标机):这是我们要调试的那台机器(或者是 QEMU 虚拟出来的 ARM32 板子)。上面运行着GDB 服务端,也就是 KGDB。这是一个轻量级的组件,它作为内核的一部分常驻在内核空间。它是「前线侦察兵」。

类比(第一次)

你可以把这个过程想象成拆弹专家和现场助手的配合。

  • Host 是坐在安全卡车里的拆弹专家,他有图纸、有咖啡、有大屏幕(GDB 客户端)。
  • Target 是那个穿着防爆服站在炸弹旁边的现场助手(KGDB 服务端)。
  • 炸弹就是即将崩溃的内核。
  • 专家不能上去剪线,因为他不在现场;助手必须听专家的指挥,剪哪根线、读哪个电压表,然后把结果汇报给专家。

但这里有一个关键的区别:真正的拆弹助手有自主意识,而 KGDB 服务端只是一个嵌入在内核里的线程。当 KGDB 活跃时,目标机的整个内核其实是暂停的——只有当 KGDB 收到 Host 的命令(比如「读取变量 x 的值」),它会短暂苏醒,执行一下内存读取,然后把结果发回给 Host,然后再次让内核休眠。

通信线路:穿过隔离带

既然分了家,两人就得通话。

GDB 客户端和服务端之间通常使用 TCP/IP 网络通信(默认端口是 1234),当然也可以使用串口。

这个过程是这样的:

  1. 你在 Host 的 GDB 里敲下 step
  2. GDB 客户端把这个命令打包,通过网络发给 Target 上的 KGDB。
  3. Target 上的 KGDB 收到命令,接管 CPU 控制权,单步执行一条指令。
  4. KGDB 把执行后的寄存器状态抓取下来,打包发回给 Host。
  5. 你的 GDB 屏幕更新,显示下一行源代码。

类比(第二次:揭示距离)

回到刚才的拆弹比喻。

这个比喻有一点是「过度拟人化」的:助手(KGDB)在内核被暂停时其实并没有在「思考」。它更像是木偶的线

当内核暂停时,整个世界(Target)按了暂停键。KGDB 唯一能做的,就是响应 Host 的请求去读写内存或寄存器。这不像助手在用对讲机喊「长官电压是多少」,而更像是你通过远程桌面软件控制了一台死机的电脑——你的每一次鼠标点击(命令)都是一股远程的电信号,强制 CPU 瞬间动一下,然后又瘫回去。

这就解释了为什么 Target 的屏幕在调试期间通常会「死屏」——因为负责画图的那个内核线程根本没机会跑。

为什么不用 JTAG?(旁白一句)

提到嵌入式调试,老鸟可能会问:为什么不用 JTAG(比如 BDI2000 这种硬件调试器)?

JTAG 确实更硬核,它是直接在芯片层面干活,甚至在内核还没起来时就能用。而且 JTAG 自带的 gdbserver 通常比 KGDB 更稳定,因为它不依赖内核本身的健壮性。

但 JTAG 贵,而且得接线。KGDB 的优势在于它是纯软件的。只要你的 Linux 内核还能喘口气,能跑网络协议栈,你就能用 KGDB。对于我们在 QEMU 这种虚拟环境里折腾,或者是做早期的内核驱动开发,KGDB 是那个「唾手可得」的瑞士军刀。

好了,原理清楚了:Host 发号施令,Target 乖乖照做。现在我们需要把这个机制搬到具体的硬件平台上。

准备舞台:vmlinux 与 bzImage

在进入实战之前,还有最后一点概念要理清。

当我们在 Linux 下编译内核时,会生成两个关键文件。你一定要分清它们,因为后面调试时加载错了就会跪。

  1. vmlinux:这是未压缩的内核镜像。它是一个巨大的 ELF 格式文件,包含了所有的符号表。这是给人类和 GDB 看的。
  2. bzImage / zImage:这是压缩后的内核镜像(位于 arch/<arch>/boot/ 目录下)。这是给Bootloader(U-Boot、GRUB)看 的。系统启动时,实际加载并运行的是这个。

类比(第三次:回收验证)

回到那个说明书的类比。

  • bzImage 是那台被压缩打包、准备运送到战场上的机器。
  • vmlinux 是那张详细到每个螺丝钉位置的工程蓝图

当我们用 KGDB 调试时,GDB 需要的是蓝图。它得知道 sys_open 这个函数在蓝图上的第几页第几行(内存地址)。至于机器是怎么被压缩打包进 BOOT 分区的,GDB 不关心。

如果你告诉 GDB 加载 bzImage,就像给工程师一包压缩饼干让他修机器——他看不懂里面的结构。所以,一定要把未压缩的 vmlinux 拿在手里。

光有 vmlinux 还不够。这蓝图如果是「简版」的,关键部位被涂黑了(没有调试符号),那也没法修。我们需要在编译内核时,特意把符号信息烙印到 vmlinux 里去。

这就是下一节要干的脏活:修改内核配置,开启 CONFIG_DEBUG_INFO