设备树学习4(TODO)
完全可以而且用 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 ARCHarm64 export CROSS_COMPILEaarch64-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寄存器运行以下命令拉起虚拟机Bashqemu-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继续运行你会看到类似/pl0119000000QEMU 的虚拟串口、/virtio_mmioa000000等节点一个个被实例化为platform_device 终极实操建议怎么玩效率最高既然你想追求最高效率那么在实操时不要一上来就去读几十万行的drivers/of/源码。那会迷失在细节里。按照我们上面给出的三个断点unflatten_device_tree-of_platform_device_create_pdata- 某个具体的probe先在 QEMU 里顺利地让指针跑完一整圈。跑完一圈后挑你最感兴趣的断点比如设备创建那一瞬间在 GDB 里输入bt(backtrace打印函数调用栈)。通过bt命令你会看到一幅波澜壮阔的、由内核各种子系统串联起来的调用瀑布图。逆着这个调用栈去翻看内核源码你就会彻底明白是哪个子系统的哪一行代码在哪个时间点调度了它。用这套 QEMU 环境做一两次真刀真枪的 debug 跟踪比看任何技术博客和教程都管用。这层软硬件交互的无缝逻辑就在你的单步调试中被彻底坐实了赶紧去板子上建一个试试