This is my hardware environment and operating system.
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 # Switch to this branch git checkout origin/stable/202408 git submodule update --init --recursive git branch # Check if all submodules are initialized correctly. If not, compilation may have issues. git submodule status cd - # Download 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
# Install basic software packages sudo apt-get install python3 python3-distutils uuid-dev build-essential bison flex nasm acpica-tools gcc # Install 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 Example
Let’s implement a UEFI code compilation for the target platform x64 or aarch64, which supports running on Emulator and qemu, and debugging with gdb.
Code
1 2 3 4 5
touch HelloWorld.dsc touch HelloWorld.inf touch HelloWorld.c # Use this command to generate UUID, used in dsc and inf files uuidgen
HelloWorld.dsc
The DSC file is the package description file, where all fields in Defines are mandatory. To find paths for LibraryClasses, use the following command:
1 2 3 4 5
cd edk2 # Take UefiApplicationEntryPoint as an example grep UefiApplicationEntryPoint -r ./ --include=*.inf | grep LIBRARY_CLASS # Find by GUID grep -i 752F3136 -r ./ --exclude-dir=Build
The format for LibraryClasses is:
1
LibraryClassName|Path/To/LibInstanceName.inf
For a full explanation of DSC files, 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 directory of the source files export PROJ_NAME="HelloWorld" # DSC file name export DSC_NAME="HelloWorld" # INF file name export INF_NAME="HelloWorld" # Also the name of the generated *.efi, defined in INF's BASE_NAME 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 settings, supports multiple paths separated by colon export PACKAGES_PATH="$EDK_PATH:$EDK_LIBC_PATH:$APP_PATH" # Specify Python interpreter export PYTHON_COMMAND="/usr/bin/python3" # Confirm that environment variables have been set echo"Environment variables for $PROJ_NAME project are configured."
Next, we will write a script to compile the 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 # In this script, the -b flag is set to DEBUG. When deploying, you should use RELEASE instead. # -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
The process for compiling to the AArch64 platform is similar.
Finally, we write a script to run on the emulator provided by edk2. Note that you need a GUI environment here. If you’re only using the command line, skip this step and refer to the next section to run 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 your version is not working as expected, consider using QEMU version 8.1.5.
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 the program with GDB in the next section, but if you just want to run with QEMU, it won’t affect the operation.
export GCC5=/usr/bin/gcc # Compiled once, 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 # The default QEMU from Ubuntu 22.04 repo is not compatible; we need to upgrade to QEMU v8.1.5 cp$EDK_PATH/Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd ./
This script first compiles the OVMF (Open Virtual Machine Firmware), which is a firmware based on EDKII that runs on QEMU x86-64 virtual machines. This makes debugging and experimenting with UEFI firmware easier, whether for testing OS booting or using the (built-in) EFI shell.
The OVMF firmware (for QEMU’s UEFI implementation) is divided into two files:
OVMF_CODE.fd: contains the actual UEFI firmware.
OVMF_VARS.fd: acts as a “template” for simulating persistent NVRAM storage. All virtual machine instances can share the read-only OVMF_CODE.fd file from the OVMF package, but each instance needs a private, writable copy of OVMF_VARS.fd. In QEMU, you can specify OVMF_CODE.fd and OVMF_VARS.fd separately, or use a simplified approach:
Run debug-x64.sh, and you should see the following interface, 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 your program, and the expected output should be “Hello World!!!”
If the Backspace key doesn’t respond in the shell, try using Ctrl+H instead
# No need to compile again after one successful compilation 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 # Ubuntu 22.04 default QEMU is incompatible, need to upgrade QEMU to version 8.1.5
Debugging UEFI programs using gdb can be a bit tricky, but it can be automated with scripts. The general process is as follows:
Run debug.sh, then enter UEFI Shell and run the following code (same as using QEMU in the previous section; this is mainly to capture the driver startup address in _ovmf_dbg/debug.log).
Open another terminal and run the script addr.sh below.
Run gdb -x gdb_commands.txt in the _ovmf_dbg/UEFI_Disk directory.
Set a breakpoint in GDB, e.g., break UefiMain.
Add the GDB debug target target remote localhost:1234.
Run and type c to continue to the first breakpoint.
# Use regular expression to extract the 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 info and extract .text and .data File offsets text_offset=$(objdump -h "$INF_BASE_NAME.efi" | awk ' /\.text/ {print $6} # Extract .text File offset ')
# Output the file content for verification 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_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 implement a program that calls the standard C library in UEFI.
You can copy HelloWorld.dsc, modify the GUID, and make sure to change the [Components] section to HelloWorld.inf. Finally, add the following line to the [LibraryClasses] section at the end: HelloStd.dsc
1
!include StdLib/StdLib.inc
Next, for HelloStd.inf, modify the ENTRY_POINT in the [Defines] section to ShellCEntryLib, and in the [Packages] section, add the StdLib/StdLib.dec and ShellPkg/ShellPkg.dec packages. In the [LibraryClasses], remove UefiApplicationEntryPoint, and add the LibC and LibStdio libraries. 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