The problem#
It's annoying to test boot sequences, either of flash drives or your computer. Rebooting every time you want to test a change is slow and loses your place. Which is especially frustrating if you're using a live USB to attempt to repair a broken bootloader and you have to try multiple times. For a bootable flash drive, at least you might have another computer to test it on, making the problem just mildly inconvenient. But what if you didn't have to boot your computer to test the boot sequence?
The solution#
Of course, you have to boot some computer. But it doesn't have to be a
physical one: it can be a virtual machine. While virtual machines
often use virtual hard disks backed by files, they can also use real
disks. And QEMU's -snapshot
flag lets you read
from a real disk without actually writing back to it. All changes are
stored in temporary storage unless you "commit" them, which you do not
want to do for this purpose.
IMPORTANT: Always make sure to use -snapshot
when running QEMU on a
real disk, especially if that disk is in use by the host system. The
following commands also have you set the file permissions so QEMU does
not have write access to the disks to be extra careful.
The following will run a virtual machine booting off a flash drive
(change /dev/sdc
to the appropriate device):
# Grant current user read-only access to flash drive
sudo chgrp "$(id -gn)" /dev/sdc
sudo chmod g=r /dev/sdc
# Boot VM off flash drive
qemu-system-x86_64 -snapshot \
-net none -machine q35 \
-bios /usr/share/ovmf/OVMF.fd \
-cpu host -m 8G -enable-kvm \
/dev/sdc
To instead boot off your machine's internal drive (assuming it is an
NVME SSD with device name /dev/nvme0n1
):
uefivars -i efivarfs -o edk2 \
-I /sys/firmware/efi/efivars -O OVMF_VARS.fd
# Grant current user read-only access to primary SSD
sudo chgrp "$(id -gn)" /dev/nvme0n1
sudo chmod g=r /dev/nvme0n1
# Boot VM off primary SSD
qemu-system-x86_64 -snapshot \
-net none \
-drive file=/dev/nvme0n1,if=none,id=nvm \
-device nvme,serial=deadbeef,drive=nvm \
-machine q35 \
-drive if=pflash,format=raw,unit=0,readonly=on,\
file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
-cpu host -m 8G -enable-kvm
Those commands require QEMU, KVM, and OVMF installed as well as the Python package uefivars. The paths for OVMF are where Debian installed the files on my system, but they may be in a different place or have slightly different filenames on your system.
Note booting the VM may not be instant: that second command takes almost a minute to reach the GRUB menu on my computer.
The details#
Basic QEMU invocation#
The most basic invocation of QEMU is to just give it a disk image:
qemu-system-x86_64 disk.img
As mentioned above, we want to make sure to use the
-snapshot
flag to not accidentally write to the disk.
Additionally, I included -net none
to also make sure there's no
side-effects on the network either as QEMU attaches VMs to the network
by default; that also prevents it from wasting its time trying
PXE network booting.
You may need to add the -display gtk
option or possibly install the
qemu-system-gui
package (or your distribution's equivalent) if QEMU
does not display the emulated computer's screen or tells you to connect
via VNC to see it.
Performance options#
That last line -cpu host -m 8G -enable-kvm
makes sure you aren't
emulating an underpowered computer. -m 8G
gives it 8 GiB of memory
since the default of 128 MiB may be insufficient. -cpu host
tells
it to "emulate" the same processor your computer actually has.
-enable-kvm
has it use KVM to use your CPU's virtualization
support instead of actually acting as an emulator which would be much
slower.
NVME emulation#
If you have an NVME SSD, in order to make the virtual machine look enough like your machine to be able to boot, it may be insufficient to just point QEMU at the block device as works for booting off a flash drive or a SATA disk. QEMU supports NVME emulation. The command I gave above uses the example from that documentation on the simplest way to use it.
EFI emulation#
If you try to use just what was described above, you may find that
you are able to virtualize your machine booting a flash drive but not
booting itself. The problem is that QEMU does not default to EFI
boot, while modern computers do. The fix is to use
OVMF which provides the firmware for an EFI machine along with
the -machine q35
option which tells QEMU to emulate a machine that
supports that firmware. On Debian, you can install the ovmf
package
and then provide QEMU with -bios /usr/share/ovmf/OVMF.fd
.
EFI booting flash drives#
Note that flash drives can boot in EFI mode, they just often support
legacy BIOS mode. One confusion I ran into using the multiboot USB
loader Ventoy is that it supports both, but when booting in
legacy BIOS mode, it, naturally, can't load .efi
images. Since it
dynamically generates the boot menu with the items it can find, that
means that those items are just silently omitted from the list when
booted in legacy BIOS mode but appear when booting via EFI.
EFI vars#
In order to accurately emulate your machine, you also need
your machine's EFI configuration, which can be found in
/sys/firmware/efi/efivars
. OVMF supports configurable EFI vars, and
there's a tool uefivars that will convert EFI vars between
different formats including the one OVMF uses.
The Python package is not in Debian, so I had to install it separately:
python -m venv ./venv
. ./venv/bin/activate
pip install uefivars
Then you can use it to create the OVMF_VARS.fd
file:
uefivars -i efivarfs -o edk2 \
-I /sys/firmware/efi/efivars -O OVMF_VARS.fd
Since the firmware is now in two separate files, we can't use the
-bios
shortcut to pass it into QEMU. Also, we need to use the
code-only OVMF image since we're providing our own vars:
-drive if=pflash,format=raw,unit=0,readonly=on,\
file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
I got those options from this blog post, which goes into great detail on exactly what is going on with QEMU and EFI.
Comments
Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.
There are no comments yet.