ernesto cruz
Back to writing
June 8, 2026·5 min read

The EDK II Wall

What it is actually like to start writing UEFI firmware with EDK II: the documentation, the missing C library, the build system, and the moment it finally clicks.

uefiedk2firmwarec

EDK II has a reputation. People who write firmware talk about it the way climbers talk about a particular pitch: not impossible, but steep, and the first attempt humbles you. When I started writing UEFI modules, I expected the hard part to be the low-level concepts. It was not. The hard part was simply finding out how anything was supposed to work.

The wall is the documentation

The concepts behind UEFI are reasonable once you see them. Getting there is the problem. The reference material is vast, scattered across the specification, the Tianocore wiki, header comments, and the source of other packages, and it is written in a dense, formal register that assumes you already know the shape of the thing it is describing.

I am fluent in English, but I am not a native speaker, and EDK II is where that gap was most expensive. It was not the vocabulary. It was the way ideas were phrased: long sentences defining terms with other undefined terms, documents that describe a mechanism without ever telling you why you would reach for it. I would read a page three times and still not know whether it applied to my case.

A "hello world" UEFI application took me about two hours. For a program that prints one line, that number says everything about the slope of the wall.

It is C, but not the C you know

The first concrete shock: there is no standard C library. EDK II is a freestanding environment. #include <string.h> does not exist. malloc does not exist. Instead there is a parallel universe of library classes, with functions like AllocatePool, FreePool, CopyMem, and AsciiStrLen.

This becomes very real the moment you try to reuse existing C code. I needed a QR code generator and reached for a well-known single-file library. It would not compile, because it leaned on stdlib.h and string.h. The fix was to write a small shim layer mapping the standard calls onto their EDK II equivalents, so the library believed it was in an ordinary C environment while actually running inside firmware. Useful to know in advance: most portable C is portable right up until it meets a world with no libc.

The build system is a religion

A module is described by three files before a single line of logic matters:

  • a .dec declaring the package and its GUIDs,
  • a .dsc describing how to build it,
  • an .inf declaring one module, its sources, and its library dependencies.

Then you source the environment and build against a toolchain tag:

. edksetup.sh
build -p MyPkg/MyPkg.dsc -a X64 -t GCC5

Getting a .efi out is only half of it. To actually run a driver you integrate it into OVMF for QEMU by editing the platform .dsc and .fdf, or, for an application, you register it as a boot option from the UEFI shell:

bcfg boot add 0 FS0:\EFI\Boot\MyApp.efi "My App"

None of this is hard once you know it. The cost is entirely in not knowing it, and in the documentation rarely showing you the whole path end to end.

Where does my code even run?

UEFI boots in phases: SEC, PEI, DXE, then BDS. This is not trivia; it decides how your code is even loaded. A DXE driver is dispatched automatically by the DXE core during initialization. A UEFI application runs later, launched as a boot option in the BDS phase. Putting code in the wrong category means it never runs, and nothing tells you why. Understanding the phase model was the difference between "my driver does nothing" and "my driver loads."

Debugging without a debugger

There is no comfortable source-level debugger in the normal loop. You instrument with debug macros that print over serial:

DEBUG ((DEBUG_INFO, "PicoWifiDxe: found device on handle %p\n", Handle));

Then you rebuild, boot QEMU with OVMF, read the serial log, and repeat. The iteration loop is slow enough that it changes how you write: you think harder before each run because each run is expensive.

Everything is a protocol on a handle

This was the idea that, once it clicked, made the rest fall into place. EDK II is built around handles and protocols. A protocol is just a GUID-identified struct of function pointers and data. You install a protocol on a handle, and other code finds it by locating that GUID. Drivers publish capabilities this way; applications consume them. Persistent state lives in NVRAM variables, addressed by a name and a vendor GUID through GetVariable and SetVariable.

Once you internalize "capabilities are protocols, you publish and locate them by GUID, and state is a variable under a GUID," the architecture stops feeling alien and starts feeling consistent.

It only goes up from the first step

What got me over the wall was not the official material. It was a random blog post, one I did not even think to bookmark, that explained handles and protocols with small concrete examples instead of formal definitions. That was the moment the model became legible. After that first step, it was only up: still work, but work with a map.

If you are starting EDK II, that is the thing to know. The wall is real, but it is almost entirely front-loaded. Find one explanation that uses examples, get a single module to build and run, and understand that everything is a protocol on a handle. The slope eases quickly after that.