EFI Shell mit QEMU

Wenn man EFI Apps entwickelt, möchte man sie natürlich auch testen. Natürlich kann man jedes neue .efi Binary auf einen USB-Stick kopieren und auf einem realen PC über die EFI-Shell ausführen, aber “effizient” ist das nicht.

Der Emulator QEMU bietet mit dem TianoCore OVMF (Open Virtual Machine Firmware) Paket die Möglichkeit, eine EFI Shell Sitzung oder eine EFI-Boot auf dem lokalen System schnell nachzustellen.


Um eine fertige .efi x64-App-Datei schnell testen zu können, braucht man folgende Zutaten:

  • Eine QEMU Installation mit dem qemu-system-x86_64 Emulator
  • Eine Firmware-Datei mit EFI-Shell Support wie z.B. in OVMF.fd
  • Die Linux mtools um per Script ein virtuelles USB-Stick-Image zu basteln
  • Und ein Script, welches alle Aufrufe zusammenfasst.

QEMU

Den System Emulator QEMU findet man auf qemu.org zum Download, aber als OpenSource-Projekt kann man ihn auch direkt aus den Quellcodes bauen lassen.

Am einfachsten läuft die Installation jedoch über das Paketmanagement des Betriebssystems:

  • Windows: winget install SoftwareFreedomConservancy.QEMU
  • Debian / Ubuntu: sudo apt install qemu

TianoCore Open Virtual Machine Firmware (OVMF)

Eigentlich brauchen wir nur eine Datei namens OVMF.fd, die aus dem TianoCore Projekt heraus gebildet wurde, damit wir eine virtuelle Maschine starten können, die von einem virtuellen USB Stick aus unsere App als BOOTX64.EFI startet.

Entweder sucht man sich dafür in den Weiten des Internet eine OVMF.fd, oder man klont sich das TianoCore GIT Projekt und startet dessen Build-Prozess.

Eine hervorragende Anleitung dazu, die auch bei mir unter Windows in WSL (Ubuntu 18.04) funktioniert hat, findet man unter
fabianlee.org/2018/09/12/kvm-building-the-latest-ovmf-firmware-for-virtual-machines/

Hier die Kommandos, die ich erfolgreich ausführen konnte:

 1## script from fabianlee.org:
 2sudo apt-get install build-essential git uuid-dev iasl nasm python -y
 3sudo apt-get install iasl -y
 4mkdir -p uuefi; cd uuefi
 5git clone git://github.com/tianocore/edk2.git
 6cd edk2
 7git submodule update --init
 8source ./edksetup.sh
 9make -C BaseTools/
10build -a X64 -t GCC5 -b RELEASE -p OvmfPkg/OvmfPkgX64.dsc

Nach dem Durchlaufen der Build-Prozesse findet man unter Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd die Firmware-Datei, mit der man QEMU zur Ausführung von EFI-Apps bringen kann.

USB-Stack FAT Image mit mtools

EFI braucht am Ende einen Datenträger, von dem gebootet werden kann. Natürlich könnte man jetzt einen richtigen USB-Stick formatieren und die Dateien einspielen und dann ein binäres Image davon ziehen, aber das wäre extrem mühsam.

Linux liefert aber mit den mtools einige sehr nützlich Programme mit, die Dateisysteme “offline” bearbeiten können, und wie üblich ist es dabei egal, ob es um reale Datenträger, oder nur Image-Dateien geht.

EFI erwartet eine FAT Partition, in der im Unterverzeichnis /EFI/BOOT die Datei BOOTX64.EFI liegt.
Ein schönes Beispiel findet man auf
wiki.osdev.org/UEFI_App_Bare_Bones

Nachdem ich mit Windows und WSL arbeite, müssen die mtools mit sudo apt install mtools zuvor installiert sein. Danach kann man folgendes BATCH Script nutzen:

1COPY path\to\built\efi-app.efi BOOTX64.EFI
2wsl dd if=/dev/zero of=./efi-boot.img bs=1k count=2880
3wsl mformat -i ./efi-boot.img -f 2880 ::
4wsl mmd -i ./efi-boot.img ::/EFI
5wsl mmd -i ./efi-boot.img ::/EFI/BOOT
6wsl mcopy -i ./efi-boot.img ./BOOTX64.EFI ::/EFI/BOOT

In efi-boot.img liegt dann ein Disketten-FAT-Image (hier mit 2.88 MB), in welches die EFI-App als BOOTX64.EFI integriert ist. EFI-Apps sind ja normalerweise nicht mehrere Megabytes groß, womit das kleine Laufwerk reicht. Wer mehr will, kann natürlich auch ganze virtuelle Festplatten mit GUID-Partitionstabelle und einer eigenen EFI-Partition anlegen.

Da das wsl Kommando die Linux-Prozesse so startet, dass das Arbeitsverzeichnis in WSL auf das Windows-Dateisystem zeigt, schreiben die mtools die Dateien in das Verzeichnis, in dem das BATCH Script ausgeführt wurde.

QEMU mit EFI-Firmware und Image starten

Das finale magische Kommando lautet:

qemu-system-x86_64 -L OVMF_dir/ -pflash OVMF.fd -drive if=none,id=stick,format=raw,file=efi-boot.img -device nec-usb-xhci,id=xhci -device usb-storage,bus=xhci.0,drive=stick

Wichtig ist, dass die QEMU Programme im PATH liegen und dass sowohl OVMF.fd und efi-boot.img (oder wie auch immer man die Dateien nennen möchte) über die qemu-system-x86_64 Parameter erreichbar sind.

Dann öffnet sich nämlich das Emulator-Fenster und wenn die eigene EFI-App richtig erstellt wurde, kann man ihren Output am Bildschirm sehen und daran Freude haben.

Ich habe mir daher meine gebaute OVMF.fd fix zum Projekt hinzugefügt und lasse eine von MSVC fertig gebaute EFI APP mit folgendem Script bei mir ausführen:

 1@ECHO OFF
 2REM Starting in "\scripts"
 3cd ..\bin
 4REM Cleanup output directory
 5rmdir /s /q efi_x64_env
 6mkdir efi_x64_env
 7REM Create EFI boot image
 8copy ..\build\msbuild_efi64\deploy\bin\Release\gatecli.efi ^
 9  .\efi_x64_env\BOOTX64.EFI
10wsl dd if=/dev/zero of=./efi_x64_env/efi-boot.img bs=1k count=2880
11wsl mformat -i ./efi_x64_env/efi-boot.img -f 2880 ::
12wsl mmd -i ./efi_x64_env/efi-boot.img ::/EFI
13wsl mmd -i ./efi_x64_env/efi-boot.img ::/EFI/BOOT
14wsl mcopy -i ./efi_x64_env/efi-boot.img ^
15  ./efi_x64_env/BOOTX64.EFI ::/EFI/BOOT
16REM Start QEMU session
17SET PATH=C:\Program Files\qemu;%PATH%
18qemu-system-x86_64 ^
19  -L OVMF_dir/ -pflash OVMF.fd -drive ^
20  if=none,id=stick,format=raw,file=.\efi_x64_env\efi-boot.img ^
21  -device nec-usb-xhci,id=xhci ^
22  -device usb-storage,bus=xhci.0,drive=stick

Fazit

Meine ersten Experimente waren echt mühsam. Jedes Kompilat wurde manuell auf einen USB-Stick kopiert und dann wurde ein Test-Mainboard damit gestartet.

Mit QEMU und mtools lässt sich eine EFI-App binnen Sekunden in einer virtuellen Umgebung testen und das ist einfach nur geil!

Mit diesem Werkzeug werde ich also noch einige Zeit verbringen.