edk2 quickstart for virtualization
Here is a quickstart for everyone who wants (or needs to) deal with edk2 firmware, with a focus on virtual machine firmware. The article assumes you are using a linux machine with gcc.
Building firmware for VMs
To build edk2 you need to have a bunch of tools installed. An
compiler and the make
are required of course, but also
iasl
, nasm
and libuuid
. So
install them first (package names are for centos/fedora).
dnf install -y make gcc binutils iasl nasm libuuid-devel
If you want cross-build arm firmware on a x86 machine you also need cross compilers. While being at also set the environment variables needed to make the build system use the cross compilers:
dnf install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnu
export GCC5_AARCH64_PREFIX="aarch64-linux-gnu-"
export GCC5_ARM_PREFIX="arm-linux-gnu-"
Next clone the tiaocore/edk2 repository and also fetch the git submodules.
git clone https://github.com/tianocore/edk2.git
cd edk2
git submodule update --init
The edksetup
script will prepare the build environment
for you. The script must be sourced because it sets some
environment variables (WORKSPACE
being the most
important one). This must be done only once (as long as you keep
the shell with the configured environment variables open).
source edksetup.sh
Next step is building the BaseTools (also needed only once):
make -C BaseTools
Note: Currently (April 2022) BaseTools are being rewritten in Python, so most likely this step will not be needed any more at some point in the future.
Finally the build (for x64 qemu) can be kicked off:
build -t GCC5 -a X64 -p OvmfPkg/OvmfPkgX64.dsc
The firmware volumes built can be found
in Build/OvmfX64/DEBUG_GCC5/FV
.
Building the aarch64 firmware instead:
build -t GCC5 -a AARCH64 -p ArmVirtPkg/ArmVirtQemu.dsc
The build results land
in Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV
.
Qemu expects the aarch64 firmware images being 64M im size. The firmware images can't be used as-is because of that, some padding is needed to create an image which can be used for pflash:
dd of="QEMU_EFI-pflash.raw" if="/dev/zero" bs=1M count=64
dd of="QEMU_EFI-pflash.raw" if="QEMU_EFI.fd" conv=notrunc
dd of="QEMU_VARS-pflash.raw" if="/dev/zero" bs=1M count=64
dd of="QEMU_VARS-pflash.raw" if="QEMU_VARS.fd" conv=notrunc
There are a bunch of compile time options, typically enabled
using -D NAME
or -D NAME=TRUE
. Options
which are enabled by default can be turned off using -D
NAME=FALSE
. Available options are defined in
the *.dsc
files referenced by the build
command. So a feature-complete build looks more like this:
build -t GCC5 -a X64 -p OvmfPkg/OvmfPkgX64.dsc \
-D FD_SIZE_4MB \
-D NETWORK_IP6_ENABLE \
-D NETWORK_HTTP_BOOT_ENABLE \
-D NETWORK_TLS_ENABLE \
-D TPM2_ENABLE
Secure boot support (on x64) requires SMM mode. Well, it builds and works without SMM, but it's not secure then. Without SMM nothing prevents the guest OS writing directly to flash, bypassing the firmware, so protected UEFI variables are not actually protected.
Also suspend (S3) support works with enabled SMM only in case parts of the firmware (PEI specifically, see below for details) run in 32bit mode. So the secure boot variant must be compiled this way:
build -t GCC5 -a IA32 -a X64 -p OvmfPkg/OvmfPkgIa32X64.dsc \
-D FD_SIZE_4MB \
-D SECURE_BOOT_ENABLE \
-D SMM_REQUIRE \
[ ... add network + tpm + other options as needed ... ]
The FD_SIZE_4MB
option creates a larger firmware image,
being 4MB instead of 2MB (default) in size, offering more space for
both code and vars. The RHEL/CentOS builds use that. The Fedora
builds are 2MB in size, for historical reasons.
If you need 32-bit firmware builds for some reason, here is how to
do it:
build -t GCC5 -a ARM -p ArmVirtPkg/ArmVirtQemu.dsc
build -t GCC5 -a IA32 -p OvmfPkg/OvmfPkgIa32.dsc
The build results will be in
in Build/ArmVirtQemu-ARM/DEBUG_GCC5/FV
and
Build/OvmfIa32/DEBUG_GCC5/FV
Booting fresh firmware builds
The x86 firmware builds create three different images:
OVMF_VARS.fd
-
This is the firmware volume for persistent UEFI variables,
i.e. where the firmware stores all configuration (boot entries and
boot order, secure boot keys, ...). Typically this is used as
template for an empty variable store and each VM gets its own
private copy, libvirt for example stores them
in
/var/lib/libvirt/qemu/nvram
. OVMF_CODE.fd
- This is the firmware volume with the code. Separating this from VARS does (a) allow for easy firmware updates, and (b) allows to map the code read-only into the guest.
OVMF.fd
-
The all-in-one image with both
CODE
andVARS
. This can be loaded as ROM using-bios
, with two drawbacks: (a) UEFI variables are not persistent, and (b) it does not work forSMM_REQUIRE=TRUE
builds.
qemu handles pflash storage as block devices, so we have to create block devices for the firmware images:
CODE=${WORKSPACE}/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd
VARS=${WORKSPACE}/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_VARS.fd
qemu-system-x86_64 \
-drive if=none,id=code,format=raw,file=${CODE},readonly=on \
-drive if=none,id=vars,format=raw,file=${VARS},snapshot=on \
-machine q35,pflash0=code,pflash1=vars \
[ ... ]
Here is the arm version of that (using the padded files created
using dd
, see above):
CODE=${WORKSPACE}/Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV/QEMU_EFI-pflash.raw
VARS=${WORKSPACE}/Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV/QEMU_VARS-pflash.raw
qemu-system-aarch64 \
-drive if=none,id=code,format=raw,file=${CODE},readonly=on \
-drive if=none,id=vars,format=raw,file=${VARS},snapshot=on \
-machine virt,pflash0=code,pflash1=vars \
[ ... ]
Source code structure
The core edk2 repo holds a number of packages, each package has its own toplevel directory. Here are the most interesting ones:
- OvmfPkg
- This holds both the x64-specific code (i.e. OVMF itself) and virtualization-specific code shared by all architectures (virtio drivers).
- ArmVirtPkg
- Arm specific virtual machine support code.
- MdePkg, MdeModulePkg
- Most core code is here (PCI support, USB support, generic services and drivers, ...).
- PcAtChipsetPkg
- Some Intel architecture drivers and libs.
- ArmPkg, ArmPlatformPkg
- Common Arm architecture support code.
- CryptoPkg, NetworkPkg, FatPkg, CpuPkg, ...
- As the names of the packages already suggest: Crypto support (using openssl), Network support (including network boot), FAT Filesystem driver, ...
Firmware boot phases
The firmware modules in the edk2 repo often named after the boot
phase they are running in. Most drivers are
named SomeThingDxe
for example.
- ResetVector
- This is where code execution starts after a machine reset. The code will do the bare minimum needed to enter SEC. On x64 the most important step is the transition from 16-bit real mode to 32-bit mode or 64bit long mode.
- SEC (Security)
- This code typically loads and uncompresses the code for PEI and SEC. On physical hardware SEC often lives in ROM memory and can not be updated. The PEI and DXE firmware volumes are loaded from (updateable) flash.
- With OVMF both SEC firmware volume and the compressed volume holding PXE and DXE code are part of the OVMF_CODE image and will simply be mapped into guest memory.
- PEI (Pre-EFI Initialization)
- Platform Initialization is done here. Initialize the chipset. Not much to do here in virtual machines, other than loading the x64 e820 memory map (via fw_cfg) from qemu, or get the memory map from the device tree (on aarch64). The virtual hardware is ready-to-go without much extra preaparation.
- PEIMs (PEI Modules) can implement functionality which must be executed before entering the DXE phase. This includes security-sensitive things like initializing SMM mode and locking down flash memory.
- DXE (Driver Execution Environment)
- When PEI is done it hands over control to the full EFI environment contained in the DXE firmware volume. Most code is here. All kinds of drivers. the firmware setup efi app, ...
- Strictly speaking this isn't only one phase. The code for all phases after PEI is part of the DXE firmware volume though.