2. QEMU 启动流程分析:BIOS/UEFI 阶段、内核加载、根文件系统挂载
理解 QEMU 的启动流程是定位模拟启动故障、复现硬件异常场景的基础。本章将拆解从虚拟上电到用户态 Shell 就绪的完整链路,重点分析 BIOS/UEFI 固件阶段、内核加载与解压、根文件系统挂载 三个核心环节,并给出每个环节的调试切入点。
调试前置知识: 启动流程中的每一个阶段都可能因配置错误、设备模型不匹配或固件缺失而崩溃。掌握各阶段的标志性日志与断点位置,能将故障定位时间缩短 70% 以上。
2.1 启动流程全景:从虚拟上电到 Shell
QEMU 模拟的启动过程与物理机高度相似,但增加了虚拟化层特有的初始化步骤。下图展示了完整的启动阶段划分与关键事件。
2.2 BIOS/UEFI 阶段:固件引导与设备枚举
QEMU 默认使用 SeaBIOS(传统 BIOS)或 OVMF(UEFI 固件)作为虚拟固件。该阶段的核心任务是初始化基本硬件、枚举引导设备并加载引导程序。
2.2.1 固件选择与启动参数
| 固件类型 | QEMU 参数 | 典型文件 | 调试特点 |
|---|---|---|---|
| SeaBIOS (传统 BIOS) | -bios seabios.bin |
/usr/share/qemu/bios.bin |
启动快,日志输出少,适合快速验证 |
| OVMF (UEFI) | -bios OVMF.fd |
/usr/share/edk2/ovmf/OVMF.fd |
支持 UEFI Shell,可调试 UEFI 应用 |
| 无固件 (直接内核) | -kernel vmlinuz -append "..." |
直接加载内核 | 跳过固件阶段,适合内核调试 |
调试技巧: 使用
-d in_asm 可以观察固件执行的第一条指令。对于 SeaBIOS,第一条指令位于 0xFFFFFFF0(复位向量);对于 OVMF,入口在 0xFFFFFFF0 但立即跳转到 SEC 阶段代码。
2.2.2 固件阶段调试断点
在 GDB 中连接 QEMU 后,可以针对固件阶段设置以下关键断点:
# 连接 QEMU (需使用 -s -S 参数启动)
(gdb) target remote localhost:1234
# SeaBIOS 入口断点
(gdb) break *0xFFFFFFF0
# OVMF SEC 阶段入口 (x86_64)
(gdb) break *0xFFFFFFF0
# 引导设备选择函数 (SeaBIOS)
(gdb) break seabios:func_boot
# 打印固件版本信息
(gdb) monitor info roms
2.3 内核加载阶段:解压、页表与初始化
当 BootLoader(如 GRUB)或 QEMU 的 -kernel 直接加载将内核镜像放入内存后,内核进入 解压与初始化 阶段。这是调试内核启动故障的核心区域。
2.3.1 内核加载流程分解
- 解压阶段: 对于压缩内核(zImage/bzImage),解压代码将内核解压到高地址内存。
- 页表初始化: 内核建立初始页表,从实模式切换到保护模式(x86)或设置页表(ARM64)。
- start_kernel 入口: 架构无关的 C 代码入口,初始化调度器、内存管理、中断等核心子系统。
- 设备驱动探测: 根据设备树 (DTB) 或 ACPI 表枚举并初始化设备。
2.3.2 内核启动参数调试
通过 -append 传递内核参数可以控制启动行为,以下参数对调试尤其重要:
# 启用内核早期打印 (需内核配置 CONFIG_EARLY_PRINTK)
-append "earlyprintk=serial,ttyS0,115200 console=ttyS0"
# 禁用 ACPI,强制使用设备树
-append "acpi=off"
# 指定根设备 (避免挂载阶段失败)
-append "root=/dev/vda1"
# 启用内核调试输出
-append "debug ignore_loglevel"
# 在 init 之前暂停,等待调试器
-append "initcall_debug"
2.4 根文件系统挂载阶段:根设备识别与 init 启动
根文件系统挂载是启动流程的最后关键步骤。内核必须正确识别根设备、挂载文件系统并执行 /sbin/init。此阶段失败通常表现为 Kernel panic - not syncing: VFS: Unable to mount root fs。
2.4.1 根设备识别流程
| 步骤 | 内核函数 | 说明 | 调试方法 |
|---|---|---|---|
| 1. 解析 root= 参数 | root_dev_setup() |
从 cmdline 提取根设备路径 | 检查 /proc/cmdline |
| 2. 设备驱动匹配 | do_mounts.c |
等待根设备对应的驱动就绪 | 添加 rootdelay=10 |
| 3. 文件系统探测 | mount_root() |
尝试 ext4、btrfs 等文件系统 | 使用 rootfstype=ext4 指定 |
| 4. 执行 init | run_init_process() |
启动 /sbin/init 或指定程序 |
使用 init=/bin/bash 调试 |
常见陷阱: 使用
-kernel 直接启动时,如果未指定 -initrd 或 -drive 提供根文件系统,内核将无法找到根设备并 panic。务必检查 QEMU 的存储设备配置与内核参数中的 root= 是否匹配。
2.4.2 根文件系统挂载故障模拟与调试
以下命令模拟一个典型的根文件系统挂载失败场景,并展示如何通过 QEMU 调试定位:
# 1. 模拟根设备不存在 (故意指定错误的 root 参数)
qemu-system-x86_64 \
-kernel bzImage \
-append "root=/dev/sda99 console=ttyS0" \
-nographic
# 2. 观察内核 panic 输出
# 输出示例:
# [ 1.234567] VFS: Cannot open root device "sda99" or unknown-block(0,0)
# [ 1.234567] Please append a correct "root=" boot option; here are the available partitions:
# [ 1.234567] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
# 3. 使用 GDB 在 mount_root 设置断点
(gdb) break mount_root
(gdb) continue
# 观察 root_dev 变量的值,确认设备号是否正确
2.4.3 init 进程启动失败调试
即使根文件系统挂载成功,init 进程也可能因缺失或崩溃导致启动失败。调试方法如下:
# 使用 init=/bin/bash 绕过 init,直接进入 shell
qemu-system-x86_64 \
-kernel bzImage \
-append "root=/dev/vda1 console=ttyS0 init=/bin/bash" \
-drive file=rootfs.qcow2,format=qcow2 \
-nographic
# 进入 shell 后检查 init 文件
# ls -l /sbin/init
# file /sbin/init
# 如果 init 缺失,可以手动挂载并修复
# mount -o remount,rw /
# cp /bin/bash /sbin/init
2.5 启动流程故障速查表
下表总结了各阶段典型故障现象、根因与调试命令,可作为快速参考:
| 阶段 | 故障现象 | 常见根因 | 调试命令/参数 |
|---|---|---|---|
| BIOS/UEFI | 黑屏无输出 | 固件文件缺失或路径错误 | -bios /path/to/bios.bin |
| BIOS/UEFI | 循环重启 | 引导设备未找到 | -boot order=c 指定引导顺序 |
| 内核加载 | Kernel panic - not syncing | 内核镜像不兼容或损坏 | file bzImage 检查格式 |
| 内核加载 | Early #PF 异常 | 页表初始化失败 | -d cpu_reset,int 跟踪异常 |
| 根文件系统 | VFS: Unable to mount root fs | root= 参数错误或驱动缺失 | root=/dev/vda1 rootfstype=ext4 |
| 根文件系统 | init 崩溃或未找到 | init 文件缺失或权限错误 | init=/bin/bash 绕过 |
核心要点: QEMU 启动调试的本质是 观察 + 断点 + 日志。通过
-d 系列日志观察事件流,通过 GDB 断点深入关键函数,通过串口输出捕获内核消息。掌握这三个工具,即可系统性地定位任意启动阶段故障。