完全可以!而且用 QEMU 来实操是吃透 Linux 内核和设备树(Device Tree)解析流程的“天花板级”玩法。
在真实物理板卡(如树莓派、瑞芯微)上,内核早期的代码(比如 MMU 还没打开时的汇编阶段)你根本没办法挂载 GDB 调试器去单步跟踪,只能靠疯狂加printk,体验极其痛苦。
但在 QEMU 虚拟机里,QEMU 自带了一个“上帝级的硬件 stub”。它可以直接在虚拟机的 CPU 核心上挂一个 GDB 远程调试端口。这意味着:你可以从内核开机的第一行汇编代码开始,像调普通 C 语言单片机程序一样,一句话一句话地单步单断点跟进,亲眼看着设备树是怎么被解包、平台设备是怎么被创建的!
下面为你梳理一套目前工业界最标准、最轻量化的QEMU + GDB 内核全流程单步调试实操方案:
🛠️ 核心搭建三部曲
为了把实验环境压缩到最精简,不把时间浪费在编译巨大的文件系统上,我们采用ARM64 (QEMU virt 虚拟板) + 最小内核 + 内存文件系统 (initramfs)的硬核黄金组合。
1. 编译你自己的精简内核(开启 Debug 调试标志)
首先,我们需要一份带调试信息、没有被编译器过度优化的内核镜像:
Bash
# 1. 进入你的 Linux 内核源码目录(推荐使用稳定的 LTS 版本,如 6.1 或 6.6) export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- # 2. 生成标准的 generic/virt 板级默认配置 make defconfig # 3. 关键:进入菜单配置,必须开启以下几个硬核 Debug 开关 make menuconfig在图形菜单中,务必勾选以下选项:
Kernel hacking--->Compile-time checks and compiler options--->[*] Compile the kernel with debug info(这个是核心,否则 GDB 找不到 C 语言源码行)[*] Provide GDB scripts for kernel debugging(内核自带的 GDB 辅助脚本)
[*] Debug OS initialization code(允许调试系统早期初始化代码)
Bash
# 4. 编译内核(生成带调试符号的 vmlinux 以及压缩后的 Image) make -j$(nproc)编译完成后,你会在源码根目录下得到vmlinux(包含所有函数名和源码映射表的未压缩大文件,给 GDB 用),在arch/arm64/boot/Image得到实际运行的内核。
2. 用 QEMU 启动并挂起内核(等待 GDB 连接)
QEMU 虚拟出来的virt板子非常神奇,它不需要你人肉写一个巨大的.dtb文件。当你启动 QEMU 时,QEMU 会根据你传给它的参数(有多少 CPU、多少物理内存、挂了什么虚拟外设),自动在内存里动态生成一份绝对标准、绝无 bug 的二进制设备树(DTB),并自动把物理地址传给 ARM64 的x0寄存器!
运行以下命令拉起虚拟机:
Bash
qemu-system-aarch64 \ -M virt \ -cpu cortex-a57 \ -smp 2 \ -m 2048M \ -kernel arch/arm64/boot/Image \ -display none \ -serial stdio \ -s -S💡 关键参数解密:
-s:在后台偷偷开启一个 GDB 远程服务器,监听 TCP 端口1234。
-S:极其关键!告诉 QEMU 在 CPU 刚上电的第一行汇编指令处死死刹车,原地等待 GDB 过来接管。此时你会发现终端一片漆黑,没有任何输出。别慌,内核此时正处于“时间静止”状态。
🔬 华彩乐章:GDB 动态单步抓取设备树解包现场
现在,开启第二个终端窗口,我们要化身“上帝”,切入内核的生命周期:
Bash
# 1. 启动交叉编译版的 GDB,并直接加载内核的源码符号表文件 vmlinux aarch64-linux-gnu-gdb vmlinux进入 GDB 命令行界面后,执行以下神级操作:
🎯 抓取断点一:连接虚拟机并直击开机第一行
代码段
(gdb) target remote :1234此时 GDB 会瞬间连接上 QEMU,你会看到屏幕停在arch/arm64/kernel/head.S的某行汇编上。这就是整个 Linux 系统生命的起点!你可以输入si(单步汇编指令)走两步。
🎯 抓取断点二:设备树从二进制变成树状结构(DT 户口本建立)
我们要去抓取二进制 FDT 被解析成内存结构体device_node的历史瞬间:
代码段
(gdb) b unflatten_device_tree (gdb) c内核会飞速运行,然后“啪”地一下死死卡在unflatten_device_tree函数入口。
此时你可以输入n(单步走下两行 C 语言代码),去亲眼看一看of_fdt_unflatten_tree()是怎么在堆内存里分配节点空间的。
想要验证内存里是不是真的有设备树节点了?在函数执行完后,打印一个节点看看:
代码段
(gdb) p of_root->child->name你会惊奇地发现,内核在内存里已经解析出了第一个子节点的名字(比如chosen或cpus)!
🎯 抓取断点三:直击“节点升级为平台设备(platform_device)”的调度现场
接下来,我们要去围观内核是怎么在开机时大批量把设备树节点包装成具体驱动对象的:
代码段
(gdb) b of_platform_device_create_pdata (gdb) c这时候内核会开始不断在这个断点上停下来。每停一次,就代表一个设备树节点正在被“提拔”为平台设备。
我们可以用 GDB 打印当前正在被升级的节点名字:
代码段
(gdb) p np->full_name随着你不断输入c(继续运行),你会看到类似"/pl011@9000000"(QEMU 的虚拟串口)、"/virtio_mmio@a000000"等节点一个个被实例化为platform_device!
🏁 终极实操建议:怎么玩效率最高?
既然你想追求最高效率,那么在实操时:
不要一上来就去读几十万行的
drivers/of/源码。那会迷失在细节里。按照我们上面给出的三个断点(
unflatten_device_tree->of_platform_device_create_pdata-> 某个具体的probe),先在 QEMU 里顺利地让指针跑完一整圈。跑完一圈后,挑你最感兴趣的断点(比如设备创建那一瞬间),在 GDB 里输入
bt(backtrace,打印函数调用栈)。
通过bt命令,你会看到一幅波澜壮阔的、由内核各种子系统串联起来的调用瀑布图。逆着这个调用栈去翻看内核源码,你就会彻底明白是哪个子系统的哪一行代码在哪个时间点调度了它。
用这套 QEMU 环境做一两次真刀真枪的 debug 跟踪,比看任何技术博客和教程都管用。这层软硬件交互的无缝逻辑,就在你的单步调试中被彻底坐实了!赶紧去板子上建一个试试!