Setting up a UEFI Development Environment based on edk2 in Linux
Basic Environment Setup
This is my hardware environment and operating system.
Download edk2 Source Code
1 | # Install required packages |
Install Compilation Tools
1 | # Install basic software packages |
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 | touch HelloWorld.dsc |
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 | cd edk2 |
The format for LibraryClasses is:1
LibraryClassName|Path/To/LibInstanceName.inf
For a full explanation of DSC files, refer to the following link:
1 | [Defines] |
HelloWorld.inf
The INF file is a configuration file for an EDK2 application. It contains several sections:
- [Defines]: This section defines basic information for the module.
- BASE_NAME: The name of the application.
- FILE_GUID: This can be generated using the uuidgen command. UEFI uses the GUID to distinguish between different modules.
- MODULE_TYPE: This should be set to UEFI_APPLICATION.
- ENTRY_POINT: The name of the main function in the C code.
- [Sources]: The source code for the module, typically .c and .h files.
- [Packages]: The packages required by the module.
- [LibraryClasses]: The libraries that the module depends on.
For a complete explanation of the INF file, refer to the following link:
Below is an example of a simple module definition:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 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
1 |
|
Build Script
First, we need to create a script to set up the environment variables:
1 | touch env.sh |
env.sh
1 |
|
Next, we will write a script to compile the code for the x64 target platform.
1 | touch build-x64.sh |
build-x64.sh
1 |
|
The process for compiling to the AArch64 platform is similar.
1 | touch build-aarch64.sh |
build-aarch64.sh
1 |
|
Running
Run on Emulator
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 | touch run.sh |
run.sh
1 |
|
Run with QEMU
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
12git 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.
1 | touch debug.sh |
debug-x64.sh
1 |
|
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:1
2
3
4
5
6
7
8
9# Specify separately
qemu-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 approach
qemu-system-x86_64 -drive if=pflash,format=raw,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd \
-nographic \
-net none
Run debug-x64.sh, and you should see the following interface, which is the UEFI Shell:
1 | UEFI Interactive Shell v2.2 |
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
1 | UEFI Interactive Shell v2.2 |
Exit QEMU by pressing CTRL+A - X.
Now, here is the AArch64 version:
debug-aarch64.sh1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
set -e
trap "Exiting" INT
# Environment variables
source env.sh
export GCC5_AARCH64_PREFIX=$UEFI_WORKSPACE/toolchain/gcc-arm-8.2-2019.01-x86_64-aarch64-elf/bin/aarch64-elf-
# 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
cp $EDK_PATH/Build/ArmVirtQemu-AARCH64/RELEASE_GCC5/FV/QEMU_EFI.fd ./
mkdir -p UEFI_Disk
cp $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 command
qemu-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 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.
- Run your code in UEFI Shell.
addr-x64.sh
1 |
|
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:
HelloStd.inf
1 | # Variables defined to be used during the build process |
Now, we can call standard library programs in UEFI.
HelloStd.c
1 |
|
Finally, modify PROJ_NAME, DSC_NAME, INF_NAME, and INF_BASE_NAME in env.sh, and you can compile and debug as described in the HelloWorld section.