UEFI Hello World with qemu and EDK2

Posted on Apr 2, 2023

This is a quick guide on how to write a UEFI Hello, world application and run it using qemu.

First, create a working directory and git clone the EDK 2 repository inside it:

$ mkdir ~/uefi-hello && cd ~/uefi-hello
$ git clone https://github.com/tianocore/edk2
$ cd edk2

Then follow the setup instructions in the EDK 2 repository to set up prerequisites for the build tools:

$ sudo apt install gcc build-essential uuid-dev iasl nasm python-is-python3

Afterwards follow the common instructions to set up a build environment:

$ git checkout edk2-stable202302
$ git submodule update --init
$ make -C BaseTools
$ source ./edksetup.sh

Now you are ready to build a UEFI Hello, world application. In order to test that the application does not brick the device it’s running on, it’s good to try it out in a virtual machine first. For that, install qemu and ovmf (a port of Intel’s tianocore firmware to the qemu virtual machine):

$ sudo apt install qemu-system ovmf

Next, create a bootable EFI image for qemu:

$ mkdir ~/uefi-hello/qemu/ && cd ~/uefi-hello/qemu
$ dd if=/dev/zero of=boot.img bs=1M count=512
$ sudo mkfs.vfat boot.img
$ sudo mount boot.img /mnt/
$ sudo mkdir -p /mnt/efi/boot/
$ sudo cp /usr/lib/grub/x86_64-efi/monolithic/grubx64.efi /mnt/efi/boot/bootx64.efi
$ sudo umount /mnt/

Then copy over the firmware from ovmf and start qemu:

$ cp /usr/share/OVMF/OVMF_CODE.fd ./ovmf.fd
$ qemu-system-x86_64 -drive file=./ovmf.fd,format=raw,if=pflash -cdrom boot.img

Which if done correctly should look like this: Image of grub running inside qemu

Off to the UEFI Hello, world application – the EDK 2 repository contains such an application, and it looks like this:

/** @file
  This sample application bases on HelloWorld PCD setting
  to print "UEFI Hello World!" to the UEFI Console.

  Copyright (c) 2006 - 2018, Intel Corporation. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent

**/
#include <Uefi.h>
#include <Library/PcdLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>

GLOBAL_REMOVE_IF_UNREFERENCED EFI_STRING_ID  mStringHelpTokenId = STRING_TOKEN (STR_HELLO_WORLD_HELP_INFORMATION);
EFI_STATUS EFIAPI UefiMain (IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
  UINT32  Index;
  Index = 0;
  if (FeaturePcdGet (PcdHelloWorldPrintEnable)) {
    for (Index = 0; Index < PcdGet32 (PcdHelloWorldPrintTimes); Index++) {
      Print ((CHAR16 *)PcdGetPtr (PcdHelloWorldPrintString));
    }
  }
  return EFI_SUCCESS;
}

There is one caveat to this app. It finishes execution too quickly to be seen with the naked eye, so it would be best to add a call to make the application sleep for 30s after printing its message:

$ cd ~/uefi-hello/edk2
$ git diff
diff --git a/MdeModulePkg/Application/HelloWorld/HelloWorld.c b/MdeModulePkg/Application/HelloWorld/HelloWorld.c
index ab581c040c..a704325f56 100644
--- a/MdeModulePkg/Application/HelloWorld/HelloWorld.c
+++ b/MdeModulePkg/Application/HelloWorld/HelloWorld.c
@@ -11,6 +11,7 @@
 #include <Library/PcdLib.h>
 #include <Library/UefiLib.h>
 #include <Library/UefiApplicationEntryPoint.h>
+#include <Library/UefiBootServicesTableLib.h>

 //
 // String token ID of help message text.
@@ -53,6 +54,7 @@ UefiMain (
       // Use UefiLib Print API to print string to UEFI console
       //
       Print ((CHAR16 *)PcdGetPtr (PcdHelloWorldPrintString));
+      gBS->Stall (30000000);
     }
   }

Then modify Conf/target.txt to configure a release build for x64:

$ echo 'ACTIVE_PLATFORM       = MdeModulePkg/MdeModulePkg.dsc
TARGET                = RELEASE
TARGET_ARCH           = X64
TOOL_CHAIN_CONF       = Conf/tools_def.txt
TOOL_CHAIN_TAG        = GCC5
BUILD_RULE_CONF = Conf/build_rule.txt' > Conf/target.txt

Trigger the build:

$ build
//...output omitted
$  file Build/MdeModule/RELEASE_GCC5/X64/HelloWorld.efi
Build/MdeModule/RELEASE_GCC5/X64/HelloWorld.efi: PE32+ executable (EFI application) x86-64, for MS Windows, 3 sections

Remount boot.img and copy over the HelloWorld.efi application:

$ cd ~/uefi-hello/qemu
$ sudo mount boot.img /mnt/
$ sudo cp ~/uefi-hello/edk2/Build/MdeModule/RELEASE_GCC5/X64/HelloWorld.efi /mnt/efi/boot/bootx64.efi
$ sudo umount /mnt

Finally, launch qemu again and you will see the UEFI program run:

$ qemu-system-x86_64 -drive file=./ovmf.fd,format=raw,if=pflash -cdrom boot.img

Image of the UEFI HelloWorld application

With that you successfully ran a UEFI Hello, World application inside qemu.

References

https://www.linux.it/~ema/posts/efi-sb-notes/

https://wiki.ubuntu.com/UEFI/OVMF

https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/5_uefi_services/51_services_that_uefi_drivers_commonly_use/517_stall