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
install them first (package names are for centos/fedora).
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:
Next clone the tiaocore/edk2 repository and also fetch the git submodules.
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).
Next step is building the BaseTools (also needed only once):
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:
The firmware volumes built can be found
Building the aarch64 firmware instead:
The build results land
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:
There are a bunch of compile time options, typically enabled
-D NAME or
-D NAME=TRUE. Options
which are enabled by default can be turned off using
NAME=FALSE. Available options are defined in
*.dsc files referenced by the
command. So a feature-complete build looks more like this:
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:
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:
The build results will be in
Booting fresh firmware builds
The x86 firmware builds create three different images:
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
- 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.
The all-in-one image with both
VARS. This can be loaded as ROM using
-bios, with two drawbacks: (a) UEFI variables are not persistent, and (b) it does not work for
qemu handles pflash storage as block devices, so we have to create block devices for the firmware images:
Here is the arm version of that (using the padded files created
dd, see above):
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:
- This holds both the x64-specific code (i.e. OVMF itself) and virtualization-specific code shared by all architectures (virtio drivers).
- Arm specific virtual machine support code.
- MdePkg, MdeModulePkg
- Most core code is here (PCI support, USB support, generic services and drivers, ...).
- 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
SomeThingDxe for example.
- 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.