跳到主要内容

11.3 搭建 ARM 目标系统与内核

先别急着烧录。

上一节我们搞清楚了 vmlinux(给工程师的蓝图)和 bzImage(给机器的压缩包)的区别。现在,我们要把这个理论落地,搭建一个实体的、能跑 KGDB 的调试环境。

这事儿比想象中麻烦——你不能随便抓一个生产环境跑着的内核就开始调试。我们需要一个定制构建的目标系统。任何能干活的 Linux 系统,哪怕再简陋,也得凑齐这几块拼图:

  • Bootloader:负责把内核唤醒。在 QEMU 虚拟环境里,QEMU 自己就充当了 Bootloader,省去了这一步;但如果你在真板子上(比如 ARM 嵌入式设备),通常会用 Das U-Boot,x86 上则是熟悉的 GRUB
  • DTB(设备树二进制):ARM/ARM64/PPC 这类架构必需的「硬件说明书」,告诉内核板子上有哪些设备、接在哪条总线上。
  • 内核镜像:这就是我们上一节说的压缩包,系统启动时真正加载的东西。
  • 根文件系统:内核启动后挂载的文件系统,里面有 init 进程和必要的工具。

凑齐这四个,你才算有了一个能跑的系统。

11.3.1 自动化构建:SEALS 项目

搭建根文件系统这事儿,说多了都是泪—— BusyBox 配置、库文件依赖、init 脚本……这都不是写代码能解决的,是纯工程体力活。

为了不让我们在「配置环境」这这一章耗尽半生,我们选择作弊:用 SEALS (Simple Embedded ARM Linux System) 项目。

这是一个开源的自动化构建工具,专门用来生成「简陋但能用」的 ARM Linux 系统。它会帮你搞定:

  1. 内核配置与编译。
  2. DTB 生成。
  3. 基于 BusyBox 的最小根文件系统。

SEALS 的默认配置

我们默认使用 SEALS 的 ARM Versatile Express (VExpress) 平台配置。

  • CPU:ARMv7 Cortex A9 多核。
  • 内存:512 MB RAM。
  • 运行环境:QEMU 虚拟机。

这种方式最妙的地方在于嵌套虚拟化:你可以在 x86_64 机器上跑一个 ARM 虚拟机,在这个 ARM 虚拟机里调试内核。这意味着你不需要买任何 ARM 开发板,就能完成本书的所有实验。

当然,Yocto 和 Buildroot 是更强大的工业级方案,甚至你手头可能有现成的树莓派或 BeagleBone。但对于「学 KGDB」这个单一目标,SEALS 足够简单、足够快,而且不需要任何特定硬件。


前置准备

用 SEALS 之前,你的宿主机得准备几样东西:

  • QEMU ARM 模拟器qemu-system-arm)。
  • 交叉编译工具链(x86_64 到 ARM32 的 gcc)。
  • 一些杂项依赖库。

因为 SEALS 不是本书的重点,我们就把构建细节留给它的 Wiki。你需要把 SEALS 项目克隆下来,按照 Wiki 配置好环境。这里只给个路标:

配置完环境跑起来,你应该能看到类似图 11.4 和 11.5 的界面。如果看到 QEMU 窗口里滚动的启动日志,说明地基打好了。

11.3.2 配置内核:开启 KGDB

环境有了,现在轮到内核了。

当你进入 make menuconfig 时,你要把自己当成一个侦探。你需要打开那些平时绝对不会碰的调试开关。我们已经在第 1 章讲过内核调试的基础,这里我们要针对 KGDB 做一些特定的手术。

必须开启的选项

不管你用 ARM 还是 x86,要想让 KGDB 跑起来,这几个内核配置项(CONFIG_ 前缀省略)是强制的:

  1. DEBUG_KERNEL=y

    • 位置:Kernel hacking -> Kernel debugging
    • 这其实是总开关,选了后面很多调试选项才会出现。
  2. DEBUG_INFO=y —— 最重要

    • 位置:Kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info
    • 作用:这就给编译器加 -g 参数,把调试符号烙印进 vmlinux
    • 为什么强制?没有它,GDB 就是一堆乱码地址,你根本看不懂在干嘛。技术上它不是「必须」的(内核也能跑),但实战中没它就别玩调试。
  3. KGDB=y

    • 位置:Kernel hacking -> Generic Kernel Debugging Instruments -> KGDB: kernel debugger
    • 作用:正式把 GDB 服务端代码塞进内核。有了它,内核才能听懂 GDB 发过来的命令。
    • 原理:内核里现在运行着一个 gdbserver,它坐在那边等着(通常通过串口或网络),一旦连上,你发给它的 GDB 命令它就在本地执行,把结果吐回来。
  4. KGDB_SERIAL_CONSOLE=y

    • 作用:允许复用串口控制台来做 KGDB 通信。这是最简单、最稳定的连接方式。
    • 细节:这里涉及到一个叫 kgdboc(KGDB over console)的内核参数,我们后面会细讲怎么用它来绑定具体的串口(如 ttyS0)。
  5. KGDB_HONOUR_BLOCKLIST=y

    • 建议开启。它防止你在某些不能打断的函数(比如 kprobe 黑名单上的函数)里设断点,避免递归陷阱导致系统直接死锁。
  6. MAGIC_SYSRQ=y

    • 位置:Kernel hacking -> Generic Kernel Debugging Instruments -> Magic SysRq key
    • 作用:允许你在系统运行时,通过 /proc/sysrq-trigger 介入内核。往里面写个 g,就能强制内核停下来进入 KGDB 调试模式。
    • 配合:你还需要在运行时把 /proc/sys/kernel/sysrq 设为 1,确保所有 SysRq 功能开启。

那个叫 Kdb 的东西是什么?

在这个菜单里,你可能还会看到 Kdb

Kdb 是一个只有命令行的调试器。它不需要两台机器,直接在串口上就能用。你可以看内存、看寄存器、看日志,但它不支持源码级调试

这就好比:KGDB 是带图形界面的专业 IDE,Kdb 是只有 hex 编辑器的控制台。我们这一章专注于 KGDB,Kdb 作为一个备选手段了解一下就行。

需要关闭的选项(避坑)

有些安全特性会跟调试软件断点打架。如果以下选项出现在你的菜单里,把它们关掉

  • CONFIG_STRICT_KERNEL_RWX
  • CONFIG_STRICT_MODULE_RWX

这俩选项强制内核代码段只读、数据段不可执行。这本意是安全防护,但它会阻止 GDB 写入软件断点指令(本质上是把某条指令替换成 INT 3)。

解决办法:要么关掉它们,要么使用硬件断点(Hardware Breakpoints)。硬件断点不修改内存,用的是 CPU 的调试寄存器,所以不怕这种保护。本书推荐优先使用硬件断点。

推荐的额外选项(可选但很有用)

如果不嫌麻烦,以下选项能提升体验:

  • FRAME_POINTER=yKernel hacking -> Compile the kernel with frame pointers
    • 虽然标了 Optional,但在栈回溯时非常有用。如果你的平台默认开了 CONFIG_UNWINDER_ARM,那这个选项可能冲突,看情况选择。
  • DEBUG_INFO_SPLIT=y:把调试信息拆分成 .dwo 文件,减少 vmlinux 体积。
  • GDB_SCRIPTS=y:这个很赞。它会在你加载 vmlinux 时,自动链接一些 Python 写的 GDB 辅助脚本(比如 lx-symbols, lx-lsmod)。后面有一节专门讲这个。
  • DEBUG_FS=y:挂载 debugfs 文件系统,很多内核调试信息都在这。

⚠️ 最后一个警告

在调试内核时,一定要关掉看门狗

软件或硬件看门狗如果开着,你一旦在断点处停久一点,喂狗超时,系统就会直接重启。你刚找到的 bug 就这样消失了,连个报错都看不到。


在 SEALS 里开启 KGDB

如果你用 SEALS,事情会简单点。SEALS 的构建脚本里有预留的 KGDB 开关。

打开你板子的 build.config 文件,找到这一行:

$ grep KGDB build.config
KGDB_MODE=0 # make '1' to have qemu run with the '-s -S'
# switch (waits for client GDB to 'connect')

KGDB_MODE 改成 1。这样 SEALS 生成的 QEMU 启动脚本就会自动加上 -s -S 参数,让 QEMU 在启动时就冻结 CPU,老实等着 GDB 连上来。

Tip:GCC Plugins

编译内核时如果问你 Enable GCC plugins? 建议选 No。 插件有时会干扰调试信息的生成,或者引入一些奇怪的优化,反而增加调试难度。保持简单。

11.3.3 试运行目标系统

别信我的,信事实。

配置完、编译完,把 SEALS 生成的所有组件凑齐。在 SEALS 的 staging 目录里,你应该能找到这些东西:

  • 内核镜像
    • .../linux-5.10.109/arch/arm/boot/zImage(给 Bootloader 的压缩包,启动用)
    • .../linux-5.10.109/vmlinux蓝图,带调试符号,给 GDB 用)
  • 设备树.../linux-5.10.109/arch/arm/boot/dts/vexpress-v2p-ca9.dtb
  • 根文件系统.../seals_staging_vexpress/images/rfs.img

现在,用 QEMU 把它们串起来。

如果你是手动跑 QEMU,命令大概长这样(不要被吓跑,我们拆开看):

qemu-system-arm \
-M vexpress-a9 \
-m 512 \
-smp 4,sockets=2 \
-kernel <...>/seals_staging_vexpress/images/zImage \
-drive file=/<...>/seals_staging_vexpress/images/rfs.img,if=sd,format=raw \
-append "console=ttyAMA0 rootfstype=ext4 root=/dev/mmcblk0 init=/sbin/init" \
-dtb /<...>/seals_staging_vexpress/images/vexpress-v2p-ca9.dtb \
-nographic

参数解读(别跳过)

  • -M vexpress-a9:指定模拟的板子型号。运行 qemu-system-arm -M help 可以看 QEMU 支持的所有板子。
  • -m 512:给虚拟机分配 512MB 内存。
  • -kernel ...:指定内核镜像。注意这里给的是 zImage,因为是要启动它。
  • -drive file=...,if=sd:指定根文件系统镜像,并告诉 QEMU 把它模拟成 SD 卡。
  • -append "...":这是传给内核的启动参数。
    • console=ttyAMA0:把控制台输出重定向到这个虚拟串口(ARM 的标准串口)。
    • root=/dev/mmcblk0:告诉内核根文件系统在模拟的 SD 卡上。
  • -dtb ...:必须指定 DTB 文件,否则内核不知道自己长啥样。

如果一切正常,你会在终端看到滚动的启动 log,最后停在 BusyBox 的 shell 提示符上:

[ 2.345678] Freeing unused kernel memory: 204K
/ #

看到那个 / # 了吗?

恭喜,你的 ARM 虚拟机活过来了。

图 11.4 和 11.5 展示了这一步的截图(QEMU 启动日志和 shell 登录)。如果你能复现这个界面,说明「目标系统」这块就已经搞定了。

现在我们手里有了一只听话的虚拟羊,下一章,我们要拿刀(GDB)切开它看看里面是怎么跑的。