[KongchangAI]
Tech Frontiers· 2 min read· 1,012 words

Getting Started with Bare-Metal UEFI Programming: Writing Your First Program from Scratch

Getting Started with Bare-Metal UEFI Programming: Writing Your First Program from Scratch

A beginner's guide to bare-metal UEFI programming: from environment setup to Hello World and debugging tips.

Based on Kevin Boone's tutorial, this article introduces the basics of bare-metal programming with UEFI. UEFI, the standard firmware on modern computers, offers a friendlier development interface than traditional BIOS. The article covers toolchain preparation (GNU-EFI, QEMU/OVMF), the steps to write a minimal UEFI program (defining efi_main, initializing the library, calling Print), and explains the System Table, Boot Services, and Runtime Services in the UEFI boot process. It also shares common pitfalls and debugging tips such as testing with QEMU, serial output, and error handling, and concludes with a learning path and next steps.

ListenDialogue
0:0011:35

Why Choose UEFI for Bare-Metal Programming?

Traditional bare-metal programming often starts with BIOS or direct hardware manipulation, but virtually all modern computers use UEFI firmware. UEFI provides a standardized interface that lets developers run their own code before the operating system loads. Kevin Boone's article "First steps in bare-metal programming using UEFI" documents in detail how to take that first step.

UEFI's advantage is that it abstracts away hardware differences and provides a Protocol mechanism for accessing devices. For developers who want to explore low-level programming, UEFI is an ideal starting point—you don't need to handle complex hardware initialization, yet you can interact directly with the firmware.

The Difference Between UEFI Firmware and BIOS: UEFI (Unified Extensible Firmware Interface) is the modern replacement for the traditional BIOS. BIOS uses 16-bit real mode, is limited to 1MB of memory addressing, and relies on interrupt calls to access hardware; UEFI, by contrast, runs in a 32-bit or 64-bit flat memory model, provides rich boot services and runtime services, and supports loading applications from large-capacity storage devices. UEFI firmware is typically stored in a flash chip and includes a Compatibility Support Module (CSM) to emulate the traditional BIOS. This architectural innovation allows developers to write low-level code in a way that is much closer to how an operating system works, without dealing with cumbersome hardware initialization, thereby lowering the barrier to entry for bare-metal programming.

Preparation: Toolchain and Environment

Before you begin, you need to prepare the following tools:

  • Compiler: GNU-EFI or the EDK2 development package. GNU-EFI provides header files and libraries that simplify UEFI application development.
  • Build system: Makefile or CMake.
  • Test environment: A QEMU virtual machine or real hardware (booting from a USB drive).

The article recommends using GNU-EFI because it is lightweight and easy to get started with. You need to install the gnu-efi package and ensure your compiler supports options such as -fPIC.

Writing Your First UEFI Program

A minimal UEFI program needs to define an entry point efi_main and call system services. Here are the core steps:

  1. Include header files: #include <efi.h> and #include <efilib.h>.
  2. Define the entry point: EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable).
  3. Initialize the library: InitializeLib(ImageHandle, SystemTable);.
  4. Output information: Use Print(L"Hello, UEFI!\n");.
  5. Return status: return EFI_SUCCESS;.

When compiling, link against the GNU-EFI library to produce an .efi file. Place the file at /EFI/BOOT/BOOTX64.EFI on a FAT32-formatted USB drive and you can boot it from UEFI.

The Composition and Role of the GNU-EFI Library: GNU-EFI is a lightweight development package that contains header files (such as efi.h and efilib.h) and static libraries (such as libgnuefi.a and libefi.a). The header files define the data types, protocol structures, and service function prototypes from the UEFI specification; the libraries provide wrappers for common functionality—for example, InitializeLib initializes the library and obtains the system table pointer, and the Print function wraps console output and simplifies string formatting. Using GNU-EFI lets you avoid directly manipulating the complex UEFI service pointers and focus on your business logic. When linking, you must specify the entry point efi_main and generate a PE32+ executable, which is the format UEFI firmware can recognize.

Understanding the UEFI Boot Process in Depth

UEFI firmware loads EFI applications during boot. Programs run in a 64-bit flat memory model and can call Boot Services and Runtime Services. Boot Services are available only during the boot phase and include memory allocation, protocol handling, and more.

The article emphasizes that understanding the System Table is key. It contains pointers to various services, such as ConOut for console output. You can output a string directly via SystemTable->ConOut->OutputString.

The Lifecycle of Boot Services and Runtime Services: Boot Services are available only during the firmware boot phase and include memory allocation (AllocatePool), protocol management (HandleProtocol), event and timer services, and more. Once ExitBootServices() is called, these services can no longer be used and control is handed over to the OS loader. Runtime Services remain available while the operating system is running—for example, variable read/write (GetVariable/SetVariable), time services, and system reset. Understanding the boundary between the two is crucial, because incorrectly calling Boot Services after ExitBootServices will crash the system. In bare-metal programming, you typically use only Boot Services to complete initialization and then pass information to subsequent stages via Runtime Services.

Common Pitfalls and Debugging Tips

Debugging bare-metal code is difficult because there is no operating system support. The author shares a few lessons:

  • Use QEMU: Load UEFI firmware via qemu-system-x86_64 -bios OVMF.fd for convenient testing.
  • Serial output: If the hardware supports it, configure serial output for debug information.
  • Error handling: Check the return status of every UEFI call to avoid silent failures.

In addition, make sure your compiler options are correct—for example, disable stack protection (-fno-stack-protector), because the UEFI environment does not have a full runtime.

Using OVMF with QEMU: OVMF (Open Virtual Machine Firmware) is an open-source UEFI firmware implementation designed specifically for virtual machines. Loading OVMF firmware in QEMU via the -bios OVMF.fd parameter simulates a real UEFI boot environment. QEMU also supports mounting a FAT32 disk image containing an EFI application as a virtual hard disk, so you can test the boot process. The advantage of debugging with QEMU is fast iteration, and you can redirect serial output to the terminal via -serial stdio to conveniently view debug information. In addition, QEMU supports a GDB stub, allowing you to single-step debug UEFI programs with GDB, greatly improving troubleshooting efficiency.

Summary and Next Steps

Bare-metal UEFI programming opens a door to the low-level world for developers. Starting from a simple Hello World, you can progressively explore file systems, network protocols, and even graphics output. Kevin Boone's tutorial provides a solid starting point. As a next step, try loading ELF files or implementing a simple bootloader.

According to discussions on Hacker News, many developers find UEFI friendlier than the traditional BIOS, but a learning curve still exists. Mastering UEFI programming not only deepens your understanding of the computer boot process but also lays the foundation for developing firmware tools.

#UEFI bare-metal programming#GNU-EFI tutorial#UEFI Hello World#QEMU OVMF#UEFI boot process
Share:

Related articles