This is a neat trick I learnt recently while experimenting with building a small EFI bootloader shim. Most guides will recommend testing using qemu and EDK2, but there actually is an even simpler way to test (some) EFI applications entirely without qemu, dedicated test hardware or constant rebooting.
U-Boot can be compiled as a simple user-space Linux executable using the "sandbox" pseudo-architecture. This is mostly intended for the U-Boot test suite but we can also use it as a simple user-space UEFI test environment.
Building the sandbox binary is as easy as fetching the source code from https://github.com/u-boot/u-boot , selecting the Sandbox configuration and building the binary via:
$ make sandbox_defconfig all
The resulting u-boot file is a regular ELF Linux binary. Running it with -h gives us a list of supported command line options.
$ ./u-boot -h U-Boot 2024.07-rc2 (May 19 2024 - 17:27:46 +0200) u-boot, a command line test interface to U-Boot Usage: u-boot [options] Options: --autoboot_keyed Allow keyed autoboot -b, --boot Run distro boot commands -c, --command <arg> Execute U-Boot command -d, --fdt <arg> Specify U-Boot's control FDT -D, --default_fdt Use the default u-boot.dtb control FDT in U-Boot directory -h, --help Display help -i, --interactive Enter interactive mode -j, --jump <arg> Jumped from previous U-Boot -k, --select_unittests <arg> Select unit tests to run -K, --double_lcd Double the LCD display size in each direction -l, --show_lcd Show the sandbox LCD display -L, --log_level <arg> Set log level (0=panic, 7=debug) -m, --memory <arg> Read/write ram_buf memory contents from file -n, --ignore_missing Ignore missing state on read -p, --program <arg> U-Boot program name -r, --read Read the state FDT on startup --rm_memory Remove memory file after reading -s, --state <arg> Specify the sandbox state FDT -S, --signals Handle signals (such as SIGSEGV) in sandbox -t, --terminal <arg> Set terminal to raw/cooked mode -T, --test_fdt Use the test.dtb control FDT in U-Boot directory -u, --unittests Run unit tests -v, --verbose Show test output -w, --write Write state FDT on exit
More info on sandbox mode and other interesting configuration options can be found in the official U-Boot documentation at [1].
Running the binary without arguments allows us to access a regular U-Boot shell, load files and execute them via UEFI. The last missing puzzle piece is getting the binary from the host file system into U-Boot. For this we can use the host interface documented at [2].
host load allows as to load a file from the host file system directly into U-Boot's address space. The easiest way to load and execute a binary is loading it to the pre-defined kernel_addr_r address and then executing it via bootefi.
For simplicity we use the helloworld.efi application that ships with U-Boot:
=> host load hostfs - $kernel_addr_r lib/efi_loader/helloworld.efi 5632 bytes read in 0 ms => bootefi $kernel_addr_r No EFI system partition No EFI system partition Failed to persist EFI variables Missing TPMv2 device for EFI_TCG_PROTOCOL Missing RNG device for EFI_RNG_PROTOCOL Booting /lib\efi_loader\helloworld.efi Hello, world! Running on UEFI 2.10 Have ACPI 2.0 table Load options:File path: /lib\efi_loader\helloworld.efi Missing device handle =>
And there we have our "Hello, world!". Of course we can also run more complex applications as long as they work in U-Boot. Even graphical EFI apps should not be a problem thanks to built-in SDL support.
Finally, to make things even easier we can use the -c flag to pass those commands directly to u-boot and skip the interactive session entirely:
$ ./u-boot -c "host load hostfs - \$kernel_addr_r lib/efi_loader/helloworld.efi; bootefi \$kernel_addr_r"