This article introduces the complete process of setting up a UEFI development environment based on edk2 on Linux, including basic environment configuration, source code download, and compilation tool installation. Through the HelloWorld and HelloStd examples, it explains in detail the writing of DSC and INF configuration files, the implementation of compilation scripts for x64 and aarch64 platforms, and demonstrates how to run programs using Emulator and QEMU and debug with GDB.
Basic Environment Setup
This is my hardware environment and operating system
Hardware Environment
Download edk2 Source Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# Install Required Software Packages sudo apt update sudo apt install git mkdir -p ~/UEFI cd UEFI git clone"https://github.com/tianocore/edk2.git" cd edk2 # Use this branch git checkout origin/stable/202408 git submodule update --init --recursive git branch # Check if all submodules are properly initialized; if submodules are not fully downloaded, there will be issues during compilation git submodule status cd - # Download the edk2-libc code, mainly for using the C standard library in UEFI development git clone https://github.com/tianocore/edk2-libc.git # Create a code folder to store our own code mkdir -p code
Install Compilation Tools
1 2 3 4 5 6 7 8
# Download some basic software packages sudo apt-get install python3 python3-distutils uuid-dev build-essential bison flex nasm acpica-tools gcc # Install the ARM compiler, mainly for compiling aarch64. mkdir -p ~/UEFI/toolchain cd ~/UEFI/toolchain wget https://developer.arm.com/-/media/Files/downloads/gnu-a/8.2-2019.01/gcc-arm-8.2-2019.01-x86_64-aarch64-elf.tar.xz tar -xf gcc-arm-8.2-2019.01-x86_64-aarch64-elf.tar.xz cd -
HelloWorld
Next, we will use a HelloWorld example to compile UEFI code targeting x64 or aarch64 platforms, support running in Emulator and qemu, and finally debug the program with gdb.
Code
1 2 3 4 5
touch HelloWorld.dsc touch HelloWorld.inf touch HelloWorld.c # This command-line tool can generate UUIDs; the UUIDs in the subsequent DSC and INF files are generated this way. uuidgen
HelloWorld.dsc
The DSC file is a package description file, whereDefinesall fields are mandatory.
The paths in LibraryClasses can be found using the following command:
1 2 3 4 5
cd edk2 # Take UefiApplicationEntryPoint as an example: grep UefiApplicationEntryPoint -r ./ --include=*.inf | grep LIBRARY_CLASS # Search by GUID grep -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:
# 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
#!/bin/bash # Project name, also the source file directory of the source code export PROJ_NAME="HelloWorld" # DSC file name export DSC_NAME="HelloWorld" # INF file name export INF_NAME="HelloWorld" # Also the name of the compiled *.efi, defined in the BASE_NAME of the INF export INF_BASE_NAME="HelloWorld" # UEFI working directory export UEFI_WORKSPACE="$HOME/UEFI" # EDK II path export EDK_PATH="$UEFI_WORKSPACE/edk2" # EDK II libc path export EDK_LIBC_PATH="$UEFI_WORKSPACE/edk2-libc" # Application code path export APP_PATH="$UEFI_WORKSPACE/code/$PROJ_NAME" # Build output directory export PKG_OUTPUT_DIR="$APP_PATH/Build" # Emulator path export EMULATOR_PATH="$EDK_PATH/Build/EmulatorX64/DEBUG_GCC5/X64" # Package path setting, supports multiple paths separated by colons export PACKAGES_PATH="$EDK_PATH:$EDK_LIBC_PATH:$APP_PATH" # Specify Python interpreter export PYTHON_COMMAND="/usr/bin/python3" # Confirm settings are complete echo"Environment variables for $PROJ_NAME project are configured."
Next, write a script to compile our code for the x64 target platform
export GCC5=/usr/bin/gcc cd$EDK_PATH source edksetup.sh cd -
# Building BaseTools make -C $EDK_PATH/BaseTools # Here set the -b parameter to DEBUG, use RELEASE when deploying # -p --platform= # -m --module= # -a --arch= # -b --buildtarget= # -t --taggname= 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
Finally, we write a script to run on the emulator that comes with edk2, noteHere you need a GUI environment, if you only have a command line, skip this step and see the next section on running with qemu
1 2
touch run.sh chmod a+x run.sh
run.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#!/bin/bash set -e trap"Exiting" INT
source env.sh
export GCC5=/usr/bin/gcc # Emulator compilation: once compiled, no need to compile again. cd$EDK_PATH source edksetup.sh build -p $EDK_PATH/EmulatorPkg/EmulatorPkg.dsc -t GCC5 -a X64
First, compile and install qemu. Here I choose version 8.1.5. If yours does not achieve the expected results, consider using this version of qemu.
1 2 3 4 5 6 7 8 9 10 11 12
git clone https://gitlab.com/qemu-project/qemu.git cd qemu git checkout stable-8.1 sudo 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-softmmu make -j$(nproc) sudo make install # aarch64 ./configure --target-list=aarch64-softmmu make -j$(nproc) sudo make install
Next, write a script to run 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.
export GCC5=/usr/bin/gcc # Once compiled, no need to compile again. cd$EDK_PATH source edksetup.sh build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5 -b DEBUG #-D SOURCE_DEBUG_ENABLE
cd$APP_PATH mkdir -p _ovmf_dbg cd _ovmf_dbg rm -f debug.log # Incompatible with the default qemu from Ubuntu 22.04 software sources; need to upgrade qemu version to v8.1.5. cp$EDK_PATH/Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd ./
# -s enables GDB debugging, default listening on 127.0.0.1:1234. # -bios OVMF.fd specifies the OVMF firmware file, which is a QEMU firmware supporting 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.
This script first compiles OVMF (Open Virtual Machine Firmware), which is an EDKII-based firmware 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 (UEFI implementation for QEMU) is split into two files:
OVMF_CODE.fd: contains the actual UEFI firmware.
OVMF_VARS.fd: serves 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 requires a private, writable OVMF_VARS.fd copy. In qemu, you can specify OVMF separately_CODE.fd and OVMF_VARS.fd, or use a simplified notation:
Run debug-x64.sh, and if all goes well, the following interface will appear, which is the UEFI Shell
1 2 3 4 5 6 7 8 9 10 11 12
UEFI Interactive Shell v2.2 EDK II UEFI 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 BackSpace does not respond, you can press Ctrl+H instead
# Once compiled, there is no need to compile again cd$EDK_PATH source edksetup.sh build -a AARCH64 -p ArmVirtPkg/ArmVirtQemu.dsc -t GCC5 -b RELEASE
cd$APP_PATH/$INF_NAME mkdir -p _armvirt_dbg cd _armvirt_dbg rm -f debug.log # Incompatible with the default qemu from Ubuntu 22.04 software sources, need to upgrade qemu version to v8.1.5
Debugging UEFI programs with gdb is a bit troublesome, but you can automate some operations with scripts. The overall process is as follows:
Run debug.sh, then enter the UEFI Shell and run the following code (same operation as running with qemu in the previous section, mainly to obtain the driver’s startup address in the _ovmf_dbg/debug.log)
Open another terminal and run the following script addr.sh
In the _ovmf_dbg/UEFI_Disk directory, run gdb -x gdb_commands.txt
Set breakpoints in gdb, for example break UefiMain
# Use regular expressions to extract two addresses if [[ $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 0 fi
cd UEFI_Disk
# Use objdump to get file header information and extract the File off of .text and .data text_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 result echo".text file off: $text_offset" echo".data file off: $data_offset"
# Confirm the output file content echo"gdb_commands.txt has been created with the following content:" printf"\n" cat gdb_commands.txt printf"\n" echo"run the following command to debug" echo"cd _ovmf_dgb/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 on this basis, and then remember to modify [Components] to HelloWorld.inf, and finally in the DSC’s [LibraryClasses] add the following line of code at the end
HelloStd.dsc
1
!include StdLib/StdLib.inc
Next is HelloStd.inf, first [Defines] the ENTRY_POINT should be changed to ShellCEntryLib, [Packages] add the two packages StdLib/StdLib.dec and ShellPkg/ShellPkg.dec, [LibraryClasses] remove UefiApplicationEntryPoint, add the two libraries LibC and LibStdio, below is the declaration of HelloStd.inf
# 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
Then change the PROJ in env.sh_NAME, DSC_NAME, INF_NAME, INF_BASE_NAME can be compiled, and operations such as running and debugging are described in HelloWorld.