QEMU内存后端文件与FUSE兼容性问题及其定位
最近想要将文件系统和 QEMU 模拟器结合起来做一些实验性的工作,需要使用 QEMU 内存后端文件的特性。前面的进展都很顺利,但是待准备工作都完成后,却遇到了模拟器无法启动的问题,好在最终还是解决了,接下来便一一细说。
环境准备
我的基本开发环境为 QEMU 6.2 版本,Linux 5.10 版本内核,面向架构为 x86_64。这部分环境的准备没有遇到什么大问题,只是需要注意我选用的版本相对较老,在使用较新的编译器编译时可能会报出警告,因此需要在编译前的 configure 阶段将 Werror
的 flag 禁用,以免在编译时将警告当作错误来对待。
例如对于 QEMU 来说,configure 的命令为:
$ ./configure --target-list=x86_64-softmmu --disable-docs --disable-werror
这里最好也加上
--disable-docs
,因为编译文档的工具链同样容易由于版本问题导致编译失败。
文件系统方面,我当前的需求是需要实现一个最简的堆叠式加密文件系统,无需关注具体的加密算法是什么。一般来说,有两种方案:内核文件系统和 FUSE。
想要实现一个堆叠式的内核文件系统,可以参考 Wrapfs,这是一个经典的堆叠式文件系统模板,可以对其进行扩展,引入需要的特性(如加密)。目前最新已经适配到了 5.xx 的内核版本。
但是由于内核 API 接口变动快且复杂,难以学习和移植,而我的工作对性能又没有什么要求,因此便想要采用第二种方案——也就是 FUSE。有关如何基于 FUSE 实现一个堆叠式文件系统,不是本文的重点,后面有时间可以整理一下分享出来。
模拟器启动
在将需要的环境准备好后,便编写 QEMU 的启动命令:
#!/bin/bash
QEMU_DIR=/home/lordaeronesz/LWS/620-qemu/build/
MEM_BKEND="-object memory-backend-file,id=pc.ram,size=512M,mem-path=mnt/mem,prealloc=on,share=on "
MACHINE="-machine q35,memory-backend=pc.ram "
${QEMU_DIR}qemu-system-x86_64 \
-cpu qemu64 \
${MEM_BKEND} ${MACHINE} -m 512M \
-smp 4 \
-kernel bzImage \
-append "root=/dev/vda ro console=ttyS0" \
-drive file=rootfs.ext4,format=raw,id=hd0,if=none \
-device virtio-blk-pci,drive=hd0 \
-nographic
可以看到,我将 QEMU 模拟器的内存后端指定为 mnt
目录下的文件 mem
,其中 mnt
目录即为我基于 FUSE 实现的堆叠式文件系统的挂载点。
但是执行上述脚本,尝试运行时,却一直卡住,没有任何提示信息。而不使用内存后端文件,则能够正常启动。
尝试更换其他的堆叠式文件系统,如 eCryptfs,发现能够正常启动,因此推测是 FUSE 的问题——可能是某些接口没有实现导致的。但是在 LLM 的帮助下,我将可能出现问题的 FUSE 接口均实现了一遍,发现问题仍旧存在。
最终不得已改用其他版本的 QEMU 进行尝试,先是尝试了安装在我本机的 QEMU 8.2.2,发现能够正常启动。随后又陆续尝试了其他版本:7.0.0、8.0.0 等等。最终简单定位到 QEMU 版本在 7.1.0 时无法启动,在 7.2.0 时能够启动,应该是两个版本之间的某次 commit 修复了这个与 FUSE 不兼容的问题。
commit 定位
但是 7.1.0 到 7.2.0 之间也有上千次的 commit,应该如何定位到那个关键提交呢?
最开始我采用的是关键词搜索的方式,先将 7.1.0 到 7.2.0 之间的所有 commit 信息保存到日志文件中:
$ git log v7.1.0..v7.2.0 > output.log
然后再 grep
该日志文件,搜索 fuse
, mmap
, memory-backend
等关键字。
这种方法效率很低,而且最终也没能帮助我找到合适的提交。在 LLM 的提示下,我尝试采用一种新的方法,即二分法验证。
二分法验证(git bisect)
在进行搜索前,需要确定两个关键提交,这两个关键提交需要在一条时间线上,根据从左到右的顺序分别称之为“左端点”和“右端点”,左端点为 good 状态,右断点为 bad 状态。
这里的所谓 “good” 和 “bad” 需要依场景而定,由于 git bisect 原本设计是用来查找“最早引入 bug 的提交”的,因此左端点认为是好的状态 good(如能够正常编译),右端点认为是不好的状态 bad(如无法正常编译)。
但是我们这里是想要找出“最早修复 bug 的提交”,因此对于 good 和 bad 的定义需要转变一下:无法正常启动模拟器为 good 状态,能够正常启动模拟器为 bad 状态。
使用方法很简单,首先切换到包含这两次关键提交的分支(如主分支),然后重置 bisect
状态,并开始:
# 确保在主分支或包含这两个版本的分支上
$ git checkout master
# 重置 bisect 状态
$ git bisect reset
# 开始 bisect
$ git bisect start
然后标记左右两次关键提交:
# 标记“好”的版本
$ git bisect good refs/tags/v7.1.0
# 标记“坏”的版本
$ git bisect bad refs/tags/v7.2.0
此时本地代码仓库便会自动切换到左右两端点中间的提交,将代码重新进行 configure 并编译,尝试能否成功启动模拟器:如果能,标记为 bad;如果不能,标记为 good。以此循环往复:
# 配置 & 编译
$ ./configure --target-list=x86_64-softmmu ...
$ make -j$(nproc)
# 运行 QEMU 启动测试
$ /path/to/qemu-system-x86_64 ...
# 根据结果标记
$ git bisect bad # 如果能启动
$ git bisect good # 如果卡住
最终,在经过了约 10 次(搜索约 $2^{10} = 1024$ 个 commit)的尝试后,最终定位到的 commit 为:
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[ab1b2ba9c9e9871bb622b0f14a1b2e3f4adaa68f] update seabios source from 1.16.0 to 1.16.1
发现该提交并没有 QEMU 代码的更新,只是将 seabios 的版本从 1.16.0 更新到了 1.16.1。
最终尝试用新版本的 BIOS(目录为 pc-bios/bios-256k.bin
)替换 QEMU 6.2 中旧版本的 BIOS 后,模拟器能够成功启动,问题解决。