This article introduces the complete process of building a UEFI development environment based on edk2 in a Linux environment, including basic environment configuration, source code download, and compilation tool installation. Through 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 specific methods for running programs using Emulator and QEMU and debugging 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 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 some issues during compilation git submodule status cd - # Download the edk2-libc code, which is 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 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
Below, through a HelloWorld example, we will implement compiling UEFI code for target platforms x64 or aarch64, 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 DSC and INF files below are all generated this way uuidgen
HelloWorld.dsc
The DSC file is a package description file, in whichDefinesall 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 workspace 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 to 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, let’s write a script to run it on the emulator that comes with edk2, note thatThis requires you to have a GUI environment, if you only have the command line, skip this step and look at 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 you don't need to compile it 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 effect you can 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 using gdb to debug the program in the next section, but if you just want to run it with qemu it doesn’t matter
export GCC5=/usr/bin/gcc # Once compiled, there is no need to compile it 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 # It is incompatible with the default qemu in the Ubuntu 22.04 software source; you need to upgrade the qemu version to v8.1.5 cp$EDK_PATH/Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd ./
# -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.
This script will first compile OVMF (Open Virtual Machine Firmware), OVMF is a firmware based on EDKII that can run under qemu x86-64 virtual machines. This makes debugging and experimenting with UEFI firmware much easier; whether for testing OS booting, or using the (built-in) EFI shell.
OVMF firmware (UEFI implementation for QEMU) is divided 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 needs a private, writable OVMF_VARS.fd copy. In qemu, you can specify OVMF_CODE.fd and OVMF_VARS.fd separately, or you can 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!!!”
If pressing BackSpace in the Shell doesn’t work, you can press Ctrl+H instead
# Once compiled, there is no need to compile it 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 # It is incompatible with the default qemu in the Ubuntu 22.04 software source; you need to upgrade the qemu version to v8.1.5
Debugging UEFI programs with gdb is slightly troublesome, but you can use scripts to automate some operations. The overall process is as follows:
Run debug.sh, then enter the UEFI Shell and run the following code (the same operation as running with qemu in the previous section, mainly to get the driver startup address in _ovmf_dbg/debug.log to get the driver startup address)
Open another terminal and run the following script addr.sh
In _ovmf_dbg/UEFI_Run gdb -x gdb in the Disk directory_commands.txt
# 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 the 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 ')
# Use hexadecimal format when outputting the results printf"text_addr: 0x%X data_addr: 0x%X\n"$text_addr$data_addr
rm -rf gdb_commands.txt
# Create the gdb_commands.txt file and write content to it cat <<EOL > gdb_commands.txt file ${INF_BASE_NAME}.efi add-symbol-file ${INF_BASE_NAME}.debug 0x$(printf "%X" $text_addr) -s .data 0x$(printf "%X" $data_addr) EOL
# Output file content confirmation 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 implement calling standard C library programs in UEFI
You can copy HelloWorld.dsc, modify the guid based on it, then remember to modify [Components] to HelloWorld.inf, and finally in the dsc’s [LibraryClasses] add a line of the following code at the end
HelloStd.dsc
1
!include StdLib/StdLib.inc
Next is HelloStd.inf, first [Defines]'s ENTRY_POINT needs to be changed to ShellCEntryLib, [Packages] add these two packages: StdLib/StdLib.dec and ShellPkg/ShellPkg.dec, [LibraryClasses] remove UefiApplicationEntryPoint, add these 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
Next, change the PROJ in env.sh_NAME, DSC_NAME, INF_NAME, INF_BASE_NAME to compile, and the operations like running and debugging are described in HelloWorld