Cover image for Setting up a UEFI development environment based on edk2 on Linux

Setting up a UEFI development environment based on edk2 on Linux

Words 2.8k
Views
Visitors
Timeline

Timeline

2025-01-10

init

This article introduces how to set up a UEFI development environment based on edk2 on Linux, including downloading the edk2 source code, installing build tools, and demonstrating through a HelloWorld example the process of writing DSC, INF, and C code, compiling to generate target files for x64 and aarch64 platforms, running in Emulator and QEMU, and debugging with gdb. The article also shows the HelloStd example, explaining how to call the standard C library in UEFI.

Basic Environment Setup

This is my hardware environment and operating system

Hardware environment
Hardware environment

Download the edk2 source code

123456789101112131415161718
# Install required software packagessudo apt updatesudo apt install gitmkdir -p ~/UEFIcd UEFIgit clone "https://github.com/tianocore/edk2.git"cd edk2# Use this branchgit checkout origin/stable/202408git submodule update --init --recursivegit branch# Check whether all submodules have been correctly initialized. If the submodules are not fully downloaded, there will be some problems during compilation.git submodule statuscd -# Download the edk2-libc code, mainly to use the C standard library in UEFI development.git clone https://github.com/tianocore/edk2-libc.git# Create a code folder to store our own codemkdir -p code

Install build tools

12345678
# Download some basic software packagessudo apt-get install python3 python3-distutils uuid-dev build-essential bison flex nasm acpica-tools gcc# Install the ARM compiler, mainly for compiling for aarch64mkdir -p ~/UEFI/toolchaincd ~/UEFI/toolchainwget https://developer.arm.com/-/media/Files/downloads/gnu-a/8.2-2019.01/gcc-arm-8.2-2019.01-x86_64-aarch64-elf.tar.xztar -xf gcc-arm-8.2-2019.01-x86_64-aarch64-elf.tar.xzcd -

HelloWorld

Next, use a HelloWorld example to compile UEFI code for the x64 or aarch64 target platform, support running in Emulator and QEMU, and finally debug the program with gdb.

Code

12345
touch HelloWorld.dsctouch HelloWorld.inftouch HelloWorld.c# This command-line tool can generate UUIDs. The UUIDs in the DSC and INF files later are generated this way.uuidgen

HelloWorld.dsc

The DSC file is a package description file, in whichDefinesall fields in it are mandatory.

For paths in LibraryClasses, you can find them with the following command

12345
cd edk2# Take UefiApplicationEntryPoint as an examplegrep UefiApplicationEntryPoint -r ./ --include=*.inf | grep LIBRARY_CLASS# Find by GUIDgrep -i 752F3136 -r ./ --exclude-dir=Build

The format of LibraryClasses is

1
LibraryClassName|Path/To/LibInstanceName.inf

For a complete explanation of the DSC file, refer to the following link:

1234567891011121314151617181920212223242526272829303132333435363738
[Defines]  DSC_SPECIFICATION         = 0x0001001A  PLATFORM_GUID             = c08977d4-6e87-42f6-bf5c-4d41cfe7ba53  PLATFORM_VERSION          = 0.01  PLATFORM_NAME             = HelloWorld  SKUID_IDENTIFIER          = DEFAULT  SUPPORTED_ARCHITECTURES   = AARCH64|X64  BUILD_TARGETS             = DEBUG|RELEASE|NOOPT  OUTPUT_DIRECTORY          = $(PKG_OUTPUT_DIR)[LibraryClasses]  BaseLib|MdePkg/Library/BaseLib/BaseLib.inf  BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf  DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf  PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf  UefiHiiServicesLib|MdeModulePkg/Library/UefiHiiServicesLib/UefiHiiServicesLib.inf  ShellCEntryLib|ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.inf  HiiLib|MdeModulePkg/Library/UefiHiiLib/UefiHiiLib.inf  UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf  UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf  DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf  PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf[LibraryClasses.ARM,LibraryClasses.AARCH64]  NULL|ArmPkg/Library/CompilerIntrinsicsLib/CompilerIntrinsicsLib.inf  NULL|MdePkg/Library/BaseStackCheckLib/BaseStackCheckLib.inf[LibraryClasses.X64]  RegisterFilterLib|MdePkg/Library/RegisterFilterLibNull/RegisterFilterLibNull.inf[Components]  HelloWorld.inf

HelloWorld.inf

The INF file is the configuration file for an edk2 app, in which

  • [Defines] This section defines basic information about a module
    • BASE_NAME: the name of the app
    • FILE_GUID can be generated with the uuidgen command. UEFI uses GUIDs to distinguish different modules.
    • MODULE_TYPE: fill in UEFI here_APPLICATION
    • ENTRY_POINT: the name of the main function in the C code
  • [Sources] The source code of the module, usually .c and .h files
  • [Packages] The packages that need to be used
  • [LibraryClasses] The libraries that need to be used

For a complete explanation of the INF file, refer to the following link:

Below is a simple module definition

12345678910111213141516171819202122
# Variables defined to be used during the build process[Defines]  INF_VERSION       = 1.25  BASE_NAME         = HelloWorld  FILE_GUID         = 5455334b-dbd9-4f95-b6ed-5ae261a6a0c1  MODULE_TYPE       = UEFI_APPLICATION  VERSION_STRING    = 1.0  ENTRY_POINT       = UefiMain# Source code[Sources]  HelloWorld.c# Required packages[Packages]  MdePkg/MdePkg.dec            # Contains Uefi and UefiLib# Required Libraries[LibraryClasses]  UefiApplicationEntryPoint    # Uefi application entry point  UefiLib                      # UefiLib  UefiBootServicesTableLib

HelloWorld.c

12345678910
#include <Library/UefiLib.h>#include <Uefi.h>EFI_STATUSEFIAPIUefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {  Print(L"Hello World!!!\n");  SystemTable->BootServices->Stall(10000000);  return EFI_SUCCESS;}

Build script

First, we need to create a script to set environment variables

12
touch env.shchmod a+x env.sh

env.sh

123456789101112131415161718192021222324252627
#!/bin/bash# Project name, also the source directory of the source codeexport PROJ_NAME="HelloWorld"# DSC file nameexport DSC_NAME="HelloWorld"# INF file nameexport INF_NAME="HelloWorld"# It is also the name of the generated *.efi, defined in the BASE_NAME of the INFexport INF_BASE_NAME="HelloWorld"# UEFI working directoryexport UEFI_WORKSPACE="$HOME/UEFI"# EDK II pathexport EDK_PATH="$UEFI_WORKSPACE/edk2"# EDK II libc pathexport EDK_LIBC_PATH="$UEFI_WORKSPACE/edk2-libc"# Application code pathexport APP_PATH="$UEFI_WORKSPACE/code/$PROJ_NAME"# Build output directoryexport PKG_OUTPUT_DIR="$APP_PATH/Build"# Emulator pathexport EMULATOR_PATH="$EDK_PATH/Build/EmulatorX64/DEBUG_GCC5/X64"# Package path setting, supports multiple paths, separated by colonsexport PACKAGES_PATH="$EDK_PATH:$EDK_LIBC_PATH:$APP_PATH"# Specify the Python interpreterexport PYTHON_COMMAND="/usr/bin/python3"# Confirm the setup is completeecho "Environment variables for $PROJ_NAME project are configured."

Next, write a script to compile our code for the x64 target platform

12
touch build-x64.shchmod a+x build-x64.sh

build-x64.sh

123456789101112131415161718192021
#!/bin/bashset -etrap "Exiting" INT# environment variablessource env.shexport GCC5=/usr/bin/gcccd $EDK_PATHsource edksetup.shcd -# Building BaseToolsmake -C $EDK_PATH/BaseTools# Here set the -b parameter to DEBUG, use RELEASE when deploying# -p --platform=# -m --module=# -a --arch=# -b --buildtarget=# -t --tagname=build -p $APP_PATH/$DSC_NAME.dsc -m $APP_PATH/$INF_NAME.inf -a X64 -t GCC5 -b DEBUG -D PKG_OUTPUT_DIR=$PKG_OUTPUT_DIR

Compiling to the aarch64 platform is the same

12
touch build-aarch64.shchmod a+x build-aarch64.sh

build-aarch64.sh

123456789101112131415161718
#!/bin/bashset -etrap "Exiting" INT# environment variablessource env.shexport GCC5_AARCH64_PREFIX=$UEFI_WORKSPACE/toolchain/gcc-arm-8.2-2019.01-x86_64-aarch64-elf/bin/aarch64-elf-cd $EDK_PATHsource edksetup.shcd -# Building BaseToolsmake -C $EDK_PATH/BaseToolsbuild -p $APP_PATH/$DSC_NAME.dsc -m $APP_PATH/$INF_NAME.inf -a AARCH64 -t GCC5 -b DEBUG -D PKG_OUTPUT_DIR=$PKG_OUTPUT_DIR

Run

Running the Emulator

Finally, we write a script to run it on the emulator that comes with edk2. Note that**This requires you to have a GUI environment.**If you only have a command line, skip this step and see the next section for running with QEMU.

12
touch run.shchmod a+x run.sh

run.sh

12345678910111213141516171819
#!/bin/bashset -etrap "Exiting" INTsource env.shexport GCC5=/usr/bin/gcc# Emulator compilation: once compiled, you don't need to compile again.cd $EDK_PATHsource edksetup.shbuild -p $EDK_PATH/EmulatorPkg/EmulatorPkg.dsc -t GCC5 -a X64sudo mkdir -p $EMULATOR_PATH/UEFI_Disksudo cp $APP_PATH/Build/DEBUG_GCC5/X64/$INF_BASE_NAME.efi $EMULATOR_PATH/UEFI_Disk/cd $EMULATOR_PATH./Host

QEMU run

First, compile and install QEMU. Here I choose version 8.1.5. If you don’t get the expected results, consider using this version of QEMU.

123456789101112
git clone https://gitlab.com/qemu-project/qemu.gitcd qemugit checkout stable-8.1sudo apt install python3-venv python3-pip python3-setuptools python3-sphinx ninja-build pkg-config libglib2.0-dev libpixman-1-dev# x86_64./configure --target-list=x86_64-softmmumake -j$(nproc)sudo make install# aarch64./configure --target-list=aarch64-softmmumake -j$(nproc)sudo make install

Next, write a script to run it with QEMU. Some parameters here are for debugging with GDB in the next section, but it doesn’t matter if you just want to run it with QEMU.

12
touch debug.shchmod a+x debug.sh

debug-x64.sh

123456789101112131415161718192021222324252627282930313233343536373839
#!/bin/bashset -etrap "Exiting" INT# environment variablessource env.shexport GCC5=/usr/bin/gcc# Once compiled, there is no need to compile again.cd $EDK_PATHsource edksetup.shbuild -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5 -b DEBUG #-D SOURCE_DEBUG_ENABLEcd $APP_PATHmkdir -p _ovmf_dbgcd _ovmf_dbgrm -f debug.log# It is incompatible with the default QEMU in the Ubuntu 22.04 software repository; you need to upgrade QEMU to v8.1.5.cp $EDK_PATH/Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd ./mkdir -p UEFI_Diskcp $APP_PATH/Build/DEBUG_GCC5/X64/$INF_BASE_NAME.efi ./UEFI_Disk/cp $APP_PATH/Build/DEBUG_GCC5/X64/$INF_BASE_NAME.debug ./UEFI_Disk/# -s enables GDB debugging, listening on 127.0.0.1:1234 by default.# -bios OVMF.fd specifies the OVMF firmware file, which is a QEMU firmware that supports UEFI.# -debugcon file:debug.log redirects debug output to the debug.log file.# -global isa-debugcon.iobase=0x402 configures the I/O base address of the debug console.qemu-system-x86_64 \-s \-bios OVMF.fd \-drive format=raw,file=fat:rw:UEFI_Disk/ \-net none \-debugcon file:debug.log \-global isa-debugcon.iobase=0x402 \-nographic

This script first compiles OVMF (Open Virtual Machine Firmware). OVMF is a firmware based on EDKII that can run under the QEMU x86-64 virtual machine. This makes debugging and experimenting with UEFI firmware easier, whether for testing OS booting or using the (built-in) EFI shell.

The OVMF firmware (the UEFI implementation for QEMU) is split into two files:

  • OVMF_CODE.fd: contains the actual UEFI firmware.
  • OVMF_VARS.fd: acts as a ‘template’ for emulating persistent NVRAM storage.
    All virtual machine instances can share the system-wide read-only OVMF from the ovmf package_CODE.fd file, but each instance needs a private, writable OVMF_VARS.fd copy.
    In QEMU, you can specify OVMF_CODE.fd and OVMF_VARS.fd separately, or use a simplified form:
123456789
# Specify separatelyqemu-system-x86_64 -drive if=pflash,format=raw,readonly,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF_CODE.fd \                     -drive if=pflash,format=raw,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF_VARS.fd \                     -nographic \                     -net none# Simplified formqemu-system-x86_64 -drive if=pflash,format=raw,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd \                     -nographic \                     -net none

Run debug-x64.sh, and if all goes well, the following interface will appear, which is the UEFI Shell.

123456789101112
UEFI Interactive Shell v2.2EDK IIUEFI v2.70 (EDK II, 0x00010000)Mapping table      FS0: Alias(s):HD0a1:;BLK1:          PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)     BLK0: Alias(s):          PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)     BLK2: Alias(s):          PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)Press ESC in 2 seconds to skip startup.nsh or any other key to continue.Shell>

In this shell, type fs0: (note the English colon), then type HelloWorld.efi to run our program, expecting the output “Hello World!!!”

In the Shell, if pressing Backspace doesn’t respond, you can press Ctrl+H instead.

12345678910111213141516171819202122
UEFI Interactive Shell v2.2EDK IIUEFI v2.70 (EDK II, 0x00010000)Mapping table      FS0: Alias(s):HD0a1:;BLK1:          PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)     BLK0: Alias(s):          PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)     BLK2: Alias(s):          PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)Press ESC in 2 seconds to skip startup.nsh or any other key to continue.Shell> fs0:FS0:\> lsDirectory of: FS0:\01/08/2025  22:23                  82  gdb_commands.txt01/10/2025  20:22             184,544  HelloWorld.debug01/10/2025  20:22               5,760  HelloWorld.efi01/10/2025  12:22               1,391  NvVars          4 File(s)     191,777 bytes          0 Dir(s)FS0:\> HelloWorld.efiHello World!!!

To exit QEMU, press Ctrl+A, release it and then press X

Below is the aarch64 version.
debug-aarch64.sh

12345678910111213141516171819202122232425262728293031323334
#!/bin/bashset -etrap "Exiting" INT# environment variablessource env.shexport GCC5_AARCH64_PREFIX=$UEFI_WORKSPACE/toolchain/gcc-arm-8.2-2019.01-x86_64-aarch64-elf/bin/aarch64-elf-# Once compiled, there is no need to compile again.cd $EDK_PATHsource edksetup.shbuild -a AARCH64 -p ArmVirtPkg/ArmVirtQemu.dsc -t GCC5 -b RELEASEcd $APP_PATH/$INF_NAMEmkdir -p _armvirt_dbgcd _armvirt_dbgrm -f debug.log# It is incompatible with the default QEMU in the Ubuntu 22.04 software repository; you need to upgrade QEMU to v8.1.5.cp $EDK_PATH/Build/ArmVirtQemu-AARCH64/RELEASE_GCC5/FV/QEMU_EFI.fd ./mkdir -p UEFI_Diskcp $APP_PATH/$INF_NAME/Build/DEBUG_GCC5/AARCH64/$INF_BASE_NAME.efi ./UEFI_Disk/cp $APP_PATH/$INF_NAME/Build/DEBUG_GCC5/AARCH64/$INF_BASE_NAME.debug ./UEFI_Disk/#qemu commandqemu-system-aarch64 \-machine virt,kernel_irqchip=on,gic-version=3 \-cpu cortex-a57 -m 1G  \-drive format=raw,file=fat:rw:UEFI_Disk/ \-bios QEMU_EFI.fd \-net none \-nographic

Debugging

Debugging UEFI programs with gdb is a bit troublesome, but you can use scripts to automate some operations. The overall process is as follows:

  1. Run debug.sh, then enter the UEFI Shell and run the code (same operation as running with qemu in the previous section; the main purpose here is to, in _ovmf_dbg/debug.log, get the driver startup address)
  2. Open another terminal and run the script addr.sh below.
  3. In _ovmf_dbg/UEFI_Disk directory, run gdb -x gdb_commands.txt
  4. Set a breakpoint in gdb, for example break UefiMain
  5. Add gdb debug target remote localhost:1234
  6. Run, type c to jump to the first breakpoint
  7. Run your code in the UEFI Shell

addr-x64.sh

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
#!/bin/bashsource env.shcd _ovmf_dbglogfile="debug.log"line=$(grep -oP "Loading driver at 0x[0-9a-fA-F]+ EntryPoint=0x[0-9a-fA-F]+ $INF_BASE_NAME\.efi" "$logfile" | tail -n 1)# Use regular expressions to extract the two addressesif [[ $line =~ Loading\ driver\ at\ (0x[0-9a-fA-F]+)\ EntryPoint=(0x[0-9a-fA-F]+)\ $INF_BASE_NAME\.efi ]]; then    address0="${BASH_REMATCH[1]}"    address1="${BASH_REMATCH[2]}"    echo "Loading driver at $address0"    echo "EntryPoint=$address1"else    echo "Error: No matching line found, maybe you need to run $INF_BASE_NAME in qemu first"    exit 0ficd UEFI_Disk# Use objdump to get the file header information and extract the File off of .text and .datatext_offset=$(objdump -h "$INF_BASE_NAME.efi" | awk '  /\.text/ {print $6}  # Extract the File off of .text')data_offset=$(objdump -h "$INF_BASE_NAME.efi" | awk '  /\.data/ {print $6}  # Extract the File off of .data')# Output the extracted resultsecho ".text file off: $text_offset"echo ".data file off: $data_offset"# Calculatetext_addr=$((0x${address0#0x} + 0x${text_offset}))data_addr=$((0x${address0#0x} + 0x${data_offset}))# Use hexadecimal format when outputting resultsprintf "text_addr: 0x%X   data_addr: 0x%X\n" $text_addr $data_addrrm -rf gdb_commands.txt# Create the gdb_commands.txt file and write content to it.cat <<EOL > gdb_commands.txtfile ${INF_BASE_NAME}.efiadd-symbol-file ${INF_BASE_NAME}.debug 0x$(printf "%X" $text_addr) -s .data 0x$(printf "%X" $data_addr)EOL# Output the file content to confirm.echo "gdb_commands.txt has been created with the following content:"printf "\n"cat gdb_commands.txtprintf "\n"echo "run the following command to debug"echo "cd _ovmf_dbg/UEFI_Disk"echo "gdb -x gdb_commands.txt"echo "break UefiMain"echo "target remote localhost:1234"echo "c"

HelloStd

Another example: using edk-libc to call standard C library programs in UEFI.

You can copy HelloWorld.dsc, modify the GUID based on it, and then remember to modify [Components] to HelloStd.inf, and finally in the DSC’s [LibraryClassesAt the end of ], add a line of the following code.

HelloStd.dsc

1
!include StdLib/StdLib.inc

Next is HelloStd.inf. First, [DefinesIn ], change ENTRY_POINT to ShellCEntryLib, [PackagesIn ], add the two packages StdLib/StdLib.dec and ShellPkg/ShellPkg.dec, [LibraryClassesIn ], remove UefiApplicationEntryPoint, add the two libraries LibC and LibStdio. Below is the declaration of HelloStd.inf.

HelloStd.inf

12345678910111213141516171819202122232425
# Variables defined to be used during the build process[Defines]  INF_VERSION       = 1.25  BASE_NAME         = HelloStd  FILE_GUID         = d0956d2b-c033-45af-8ef2-76c9d30518ec  MODULE_TYPE       = UEFI_APPLICATION  VERSION_STRING    = 1.0  ENTRY_POINT       = ShellCEntryLib# Source code[Sources]  HelloStd.c# Required packages[Packages]  MdePkg/MdePkg.dec            # Contains Uefi and UefiLib  StdLib/StdLib.dec  ShellPkg/ShellPkg.dec# Required Libraries[LibraryClasses]  # UefiApplicationEntryPoint    # Uefi application entry point  UefiLib                      # UefiLib  LibC  LibStdio

Then we can call standard library programs in UEFI.

HelloStd.c

123456789101112131415161718
#include <Library/ShellCEntryLib.h>#include <Library/UefiBootServicesTableLib.h>#include <Library/UefiLib.h>#include <Library/UefiRuntimeServicesTableLib.h>#include <Uefi.h>#include <stdio.h>#include <stdlib.h>int main(IN int Argc, IN char **Argv) {  EFI_TIME curTime;  printf("HelloStd!!!\n");  gBS->Stall(2000);  gRT->GetTime(&curTime, NULL);  printf("Current Time: %d-%d-%d %02d:%02d:%02d\n", curTime.Year, curTime.Month,         curTime.Day, curTime.Hour, curTime.Minute, curTime.Second);  return 0;}

Next, change PROJ in env.sh._NAME、DSC_NAME、INF_NAME、INF_Just set BASE_NAME and you can compile. Running, debugging, and other operations are the same as described in HelloWorld.

References

Loading comments…