在 Arch Linux QEMU 上安装 Windows 7 UEFI 虚拟机

近日需上网课,但网课软件需要 Windows 操作系统才能使用。且本着安全和隐私的顾虑,单独安装了一个 Windows 7 虚拟机。该虚拟机原本在 VirtualBox 中,使用 SP1 MSDN 镜像,通过 BIOS 引导,因为我记得 Win7 ISO 的 UEFI 引导有些问题,在 VBox 中无法使用。

安装过程没有特殊只处,安装好后也是标准的 MBR 分区表。后来,由于 USB 兼容问题更换了 libvirt,虚拟机则是 QEMU + KVM。

然而,在使用了一会之后,博主心血来潮,想将它转换为 GPT + UEFI,于是开始了一番折腾。为何需要 GPT 呢?因为 Windows 7 并不支持 MBR + UEFI。

由 MBR 无损转换到 GPT 看上去是一个繁琐耗时的工作,其实不然。Microsoft 在 Windows 10 中新增了一个 mbr2gpt 工具,可离线将硬盘无损转换为 GPT 分区表,并自动设置上面的 Windows 引导。该工具不支持 Windows 7,但可以从 Windows 10 PE 中离线操作 Windows 7 磁盘,毕竟该工具是为将系统由 Windows 7 升级到 Windows 10 而设计的。转换步骤非常简单,打开 PE 的 Shell,运行 mbr2gpt /validate 后执行 mbr2gpt 即可完成,随后会提示需要将固件的 BIOS 引导模式转换为 UEFI 引导模式。然而,这正是麻烦的开始。

注意!

这不是任何产品的官方文档、帮助或使用说明,仅代表博主个人的经验总结,难免会有疏漏和错误之处。请务必结合官方文档进行阅读,以作补充。

Windows 7 UEFI 需求

这里再次重温 Windows 7 UEFI 引导的需求:

  • 必须有 GPT 分区表
  • BIOS 必须支持 Class 2 UEFI,即支持 CSM;Class 3 是无法启动的,即不支持 CSM 的 “纯” UEFI 固件。

博主正是在第二个需求上翻了车。

翻车回顾

博主在转换完分区表后,怀着极其忐忑的心情重启了虚拟机。机器在重启后,显示 Starting Windows 两秒后黑屏,没有任何反馈。其实这种结果是预料之中的,做这种从未做过的操作,不翻车反而显得过于幸运了。然而,接下来便面临着一个棘手的问题:调试。博主对 Windows 的知识可谓是一点都没有,尤其是内核和驱动的调试。Windows 不像 Linux 或 BSD,可以在串行接口上输出详细的启动和调试日志,故更加无所适从。

博主也尝试了使用安全模式引导,但屏幕在滚动到 Classpnp.sys 后便再次陷入了黑屏。

好在,Windows 内核提供了输出调试日志的功能。在开机时按 F8,选择 Enable Boot Logging,如:

Enable Boot Logging

即可在启动时输出一些调试日志到 C:\Windows\ntbtlog.txt 中。

回车选择该选项,引导内核,在黑屏后断电,离线挂载虚拟磁盘,果真看到了该文件,且其中包含大量错误信息:

   Service Pack 1 8 23 2021 12:48:45.484
Loaded driver \SystemRoot\system32\ntoskrnl.exe
Loaded driver \SystemRoot\system32\hal.dll
...(省略成功加载的驱动)...
Loaded driver \SystemRoot\system32\drivers\disk.sys
Loaded driver \SystemRoot\system32\drivers\CLASSPNP.SYS
Did not load driver @nettun.inf,%isatap.displayname%;Microsoft ISATAP Adapter
Did not load driver @hal.inf,%acpi_amd64.devicedesc%;ACPI x64-based PC
Did not load driver @battery.inf,%*compbatt.devicedesc%;Microsoft Composite Battery
Did not load driver @netavpna.inf,%mp-agilevpn-dispname%;WAN Miniport (IKEv2)
Did not load driver @netrasa.inf,%mp-l2tp-dispname%;WAN Miniport (L2TP)
Did not load driver @netrasa.inf,%mp-bh-dispname%;WAN Miniport (Network Monitor)
Did not load driver @netrasa.inf,%mp-ip-dispname%;WAN Miniport (IP)
Did not load driver @netrasa.inf,%mp-ipv6-dispname%;WAN Miniport (IPv6)
Did not load driver @netrasa.inf,%mp-pppoe-dispname%;WAN Miniport (PPPOE)
Did not load driver @netrasa.inf,%mp-pptp-dispname%;WAN Miniport (PPTP)
Did not load driver @netsstpa.inf,%mp-sstp-dispname%;WAN Miniport (SSTP)
(上面这段和类似上面这段的日志重复了五十多次,若不关机或许还会重复更多)
Loaded driver \SystemRoot\system32\DRIVERS\cdfs.sys
Did not load driver AFD.SYS
Did not load driver AFD.SYS
Loaded driver \SystemRoot\system32\DRIVERS\hidusb.sys
Loaded driver \SystemRoot\system32\DRIVERS\mouhid.sys
Loaded driver \SystemRoot\system32\DRIVERS\usbccgp.sys
Did not load driver AFD.SYS
Did not load driver @usbvideo.inf,%usbvideo.devicedesc%;USB Video Device
Did not load driver @wdma_usb.inf,%usb\class_01.devicedesc%;USB Audio Device
Did not load driver @usbvideo.inf,%usbvideo.devicedesc%;USB Video Device
Did not load driver @wdma_usb.inf,%usb\class_01.devicedesc%;USB Audio Device
Did not load driver @usbvideo.inf,%usbvideo.devicedesc%;USB Video Device
Did not load driver @wdma_usb.inf,%usb\class_01.devicedesc%;USB Audio Device
Did not load driver AFD.SYS
(AFD.SYS 日志共重复了二十七次)

我查看了 C:\Windows\System32\drivers 目录,确认了这些文件存在;Windows 文件名是不分大小写的,故也不会是大小写问题。此时便不知所措了,这些日志也没有提供具体的加载错误的原因。

于是我问了一下群里大佬:

imbushuo [Sev1], [23.08.21 18:20]
你看一下 BCD?
听起来是掉盘,因为 reference 变了
device 和 osdevice 不一致?

大意约为,类似于在 Linux 上更改了 Root 分区的 UUID 或 PARTUUID 而没有变更内核选项,故无法读取 Root 分区的情况。

然而,在我检查 BCD 后,结果却无法解释这一问题:

bcdedit 结果

图为在 Windows 10 PE 下离线通过 bcdedit 读取 Windows 7 的 BCD。可见 device 和 osdevice 都是 C:,没有任何问题。

随后我看到了他人的谈话:

jimchen5209, [23.08.21 18:23]
說起來 win7 支援 uefi 嗎

imbushuo [Sev1], [23.08.21 18:24]
[In reply to jimchen5209]
y

imbushuo [Sev1], [23.08.21 18:24]
But Class 2 systems only

imbushuo [Sev1], [23.08.21 18:25]
就是你得开 CSM

那是不是我的 OVMF 有什么问题呢?Arch 的 edk2-ovmf 包开没开 CSM 我并不太清楚,问题便搁置了。

解决问题:给 Arch Linux 的 OVMF 打上 CSM

于是终于进入了正题:给 Arch Linux 的 OVMF 打上 CSM。

读到这里的读者肯定知道了答案,Arch Linux 预编译的 edk2-ovmf 包并不包含 CSM,故它其实是一个 Class 3 UEFI 固件。这很可能就是无法启动 Windows 7 的元凶。

我在上网了解相关信息时,找到了 SeaBIOS 编译为 OVMF CSM 的文档,其中提到了 CSM_ENABLE 这个编译选项。在 Arch Linux edk2-ovmf PKGBUILD 中查找后,果然找不到该选项,说明 Arch 编译的 OVMF 不带有 CSM。

那既然如此,博主决定自己编译 OVMF 试一下。EDK2 是一个庞大的工程,它带有自己的编译系统,且由于它的组件化设计导致博主这个不理解 UEFI 或 EDK2 开发流程的人难以学习如何编译。因此博主想通过修改 Arch Linux 的 PKGBUILD 来达到为其增加 CSM 支持的目的。

给 OVMF 增加 CSM 并非难事,总归分为以下几步:

  1. 配置 SeaBIOS,设置编译目标为 CSM
  2. 编译 SeaBIOS,并将输出的 Csm16.bin 复制到 OvmfPkg/Csm/Csm16/Csm16.bin
  3. 编译 OVMF,带上 -D CSM_ENABLE 这个选项

Arch edk2-ovmf 包的 PKGBUILD 是拆包构建,在编译 OVMF 的同时还会编译 EDK2 Shell。同时它还会编译 AArch64(即 Arm64)的 OVMF。这显然不是我们需要的,在经过一番精简后,博主得出了能用的 PKGBUILD。该包使用 202105 版本的 EDK2,即写作本文时的最新版,并使用 Git 版的 SeaBIOS,即主分支直接编译。具体修改过程无需赘述,完整 PKGBUILD 在 GitHub Gist 上,博主应该不会持续更新。

最后,只需要使用 makepkg -C 编译,安装(会替换 edk2-ovmf,但应该不是必须的,读者可自行研究)后即可。

随后在虚拟机管理器中将原先的 fd 换位新的 fd,启动,按 Esc 进入 TianoCore Setup,可以看到 CSM 相关引导项:

CSM 引导项

不要选择它们。直接选择 UEFI 引导项,在这里是 UEFI QEMU HARDDISK 即可。随后可见 Windows 7 成功引导:

Windows 7 UEFI

参考文献