Running the Lil UEFI example, which boots up a simple "operating system" that just fills the display with a changing color.
Simple, headers-only C library with definitions of UEFI types and protocols, intended for UEFI application (operating system) programming, for the 2022 Wheel Reinvention Jam.
What happens when your CPU first powers on?
As it turns out, a lot. And much of that runs even before an operating system has any of its code called. If you're used to programming user applications or games - which you'd generally intend to run in an operating system's userspace - there is generally no reason to know or care (other than the obvious frustration of long boot times even with blazingly-fast hardware); the operating system calls your code.
But who calls the operating system, and how? Well, let's start at the beginning...
After your machine is turned on, the first thing that runs is software that was "flashed" onto your hardware by the original device manufacturers. This will be very low-level code that is specific to the hardware in question, generally to initialize the hardware at the most minimal-possible level.
Following that stage, more hardware-vendor-provided code will run, which will be looking to boot something. That code has a set of rules for searching for code to call, or to "boot" - for instance, it might look for an operating system stored on a USB stick that you've connected to the motherboard.
When it finds that operating system, it will transfer execution to the operating system's code. That operating system will then gather as much useful information as possible about the hardware it's running on, and prepare its own data structures, memory, and set up the hardware for whatever it plans on doing.
As you might imagine, the set of hardware vendors and configurations is large, and operating systems don't build one version of their kernel for every possible element in that set. To try and avoid that situation, standards have been designed to allow hardware to communicate with other hardware, and to an operating system, and to allow an operating system to communicate back with hardware, in a more uniform way.
One such standard is known as "UEFI". UEFI (Unified Extensible Firmware Interface) defines common "protocols" (and type information) that allows various stages of the booting process to communicate with each other, eventually calling into, say, an operating system. This is a very popular standard; if you buy modern consumer operating system hardware, it's likely that all of its pre-flashed software will communicate through UEFI protocols.
And, furthermore, it's not actually very difficult to write a program that can be called by UEFI hardware - in fact, you can do it today! And with the Lil UEFI library, it's easier than ever.
UEFI firmware looks for a special executable files, called "EFI files". The EFI file format is simply another way of packaging executable code, just like PE (.exe, on Windows) or ELF (Linux executables). The firmware will locate an entry point in such a file by looking for the
EfiMain symbol, and it'll begin executing.
Aside: This partly works to standardize across hardware configurations, but does not entirely solve the problem, particularly with regards to "protocols on top of protocols". UEFI does not, for example, standardize an ISA for executing code on a GPU - but it does standardize one way for PCI bus firmware to interact with operating system code. Further driver code is often necessary for a "real" operating system.
Long story short, when you're writing your operating system, your
EfiMain entry point will be passed a couple of pointers. One of those pointers will contain a table of function pointers that you can then call, to use a number of "UEFI protocols". You need type information to properly interpret and use those pointers - this type information is all defined in the UEFI spec. Microsoft's linker already has the capability to produce EFI files, so it's actually quite easy to get started doing operating system development in this way, by just using normal C/C++ development tools, and an emulator like Qemu.
When we were looking around at writing some beginner UEFI code, we were a bit disappointed with the existing materials and libraries that defined all of the necessary types to start using UEFI from barebones C code - they were difficult to find, difficult to read/interpret, poorly-organized, and sometimes not intended for UEFI applications (and instead for, say, drivers or firmware implementations). So, we decided to repackage all of the standardized types and protocols in accordance with the spec in a way that made more sense to us.
And, thus, "Lil UEFI" was born! We've packaged up a number of UEFI types and definitions in a hopefully straightforward way, and commented where in the spec each piece of code was derived from. So, you can just
#include the library and get rolling with using UEFI protocols to communicate with hardware. NOTE THAT we have not yet covered the entire spec, and we did take some design liberties in organizing and naming the code.
We've included an example "operating system" that boots up and uses some of the most basic UEFI protocols to grab memory information about the system, and then draw a simple solid color to the display.