First steps in bare-metal programming using UEFI – part 4

This series of articles describes how to start writing bare-metal programs for modern PCs, using Linux as the development platform. By “bare metal” I mean without any operating system: the application will be run directly by the computer’s EFI firmware, at boot time.
In earlier articles I described how to compile, link, and transform a
C program into a PE32+ executable named test.efi that EFI
can understand, and how to deploy it in such a way that EFI can load and
run it at boot time. In these earlier articles I used the
gnu-efi library to wrap the raw EFI calls in a way that is
palatable to a C programmer, and I shall continue to do so, even though
more sophisticated frameworks are available.
Later articles delve into EFI and gnu-efi in more depth.
First, though, I want to offer some general, pragmatic suggestions to
folks with a Linux background, who find themselves starting out in EFI
programming.
Make a Linux test harness
Compiling and linking an EFI program on Linux isn’t hugely different
from building a regular Linux application. You’ll need the usual
compiler and build tools – GCC, Rust, whatever – a Makefile
or CMakeLists, and so on. The details are different, but
the basic principles should be familiar.
Similarly, in a substantial program, it’s probably the case that most of the code will be ordinary C (or Rust, or whatever) – not EFI-specific at all. Implementing a linked list, or a string tokenizer, or an associative array isn’t specific to the platform.
It isn’t easy to debug and profile code on an EFI platform. So far as
I know, none of the tools we all know and love, like gdb or
valgrind, will work in such an environment. The more of the
code we can test using an ordinary Linux platform, the easier our lives
will be.
My approach – which I don’t claim is optimal – is to maintain
separate Makefiles for EFI and Linux. They’re similar, but
not so similar that a single file will do for both. Perhaps it might be
easier to use the same build configuration with more sophisticated
tools.
It’s easy enough to use #ifdef or similar, to make use
of the same C source files for both Linux and EFI targets. I tend find
in practice that there are whole source units that I need only for EFI,
and I have to decide whether to reflect that fact in the
Makefile, or just put #ifdef ... #endif around
entire source blocks. I guess this is just a matter of taste.
Be aware of the Windows-ness of EFI
The EFI API is strongly influenced by Microsoft Windows. Programming
for EFI is a little like low-level programming for the Windows API:
everything is identified by an integer handle, pathnames use the stupid
backslash (\) separator, characters are two bytes wide, and
so on.
In my view it’s worth deciding early on in an EFI project whether you’re just going to live with this Windows-y weirdness (and I appreciate, of course, that it probably doesn’t seem weird to a long-term Windows programmer), or try to tame it in some way.
If you’re working entirely in the EFI domain, then the fact that EFI has its own programming idiom isn’t really a problem – even if you’re not familiar with it at the start. It is, at least, consistent. Problems start when you want to use existing Linux code with EFI. There’s plenty of Linux C code – you almost certainly have some of your own – that assumes that directory names are separated by a forward-slash. This code won’t work unmodificatied with EFI, nor will any code that tries to iterate a string until it reaches a zero byte at the end. Since EFI strings are made of two-byte characters, most likely there will be plenty of zero bytes to confuse matters.
The more existing Linux code or library code you want to bring into an EFI project, the more EFI’s Windows flavour will be a nuisance. As the developer it’s up to you to decide how much it will benefit you, in the long term, to write a heap of wrapper and ‘glue’ code to allow you to work in a more Linux-like way.
Get used to the lack of a C standard library
The EFI library has a certain amount of general-purpose
functionality, and the gnu-efi library adds some of its
own. But you won’t find equivalents of, say, printf() or
fopen(). EFI just doesn’t work in a way that is a good fit
to the conventional C standard library.
Some gnu-efi functions do have POSIX equivalents, and
they work in a familiar way. For example, AllocatePool() is
functionally identical to malloc(). As a Linux programmer,
it makes sense for me to define my own malloc():
void *malloc (size_t size)
{
return AllocatePool (size);
}
Then library code that uses malloc() will still
work.
But wait… what is size_t? It’s defined in a header that
forms part of the standard C library… which we don’t have. In fact, we
can’t really use the standard header files at all, because they will
fool the compiler into thinking that functions are available that EFI
doesn’t have.
We could define malloc() differently, perhaps:
void *malloc (int size)
but this just seems… wrong. It’s going to be confusing and
error-prone to use function declarations that are just slightly
different from what is familiar. Safer just to use
AllocatePool() instead. But then how can we write code that
may also be tested with Linux?
My experience is that, once I start down the path of wrapping EFI in a POSIX-like coating, I’ve entered a rabbit-hole. A rabbit-hole with no obvious bottom. The more code I add, the more I need to add, just to make everything work.
But, as a long-time Linux programmer it’s worth it. At least, in the long run it is. For “Hello, World” it definitely isn’t: the more substantial the EFI application, there more there is to gain by creating a familiar framework to work with. The more Linux-like my code is, the better I understand it, and the easier it is to test and maintain with Linux tools.
Consider POSIX-like frameworks
Rather than doing all the Linux compatibility work yourself, you could base your EFI application on an existing POSIX compatibility library for EFI. There are several of these in different stages of development. I’m reluctant to name any, because this might be seen as an endorsement, and I don’t have enough experience with these tools to recommend anything in particular.
Still, it’s worth thinking about, if you’re embarking on a new project.
Be careful with type sizes
It’s been a bugbear of C programmers for forty-odd years that
standard data types like int have no well-defined size or
range. The EFI interface, however, expects definite type sizes.
The gnu-efi library provides definitions for functions
and data structures in terms of its own types. For example
typedef struct {
UINT16 Year;
UINT8 Month;
...
} EFI_TIME;
UINT16 is a 16-bit unsigned integer; UINT8
is… well, you get the idea. It’s crucial that you program in a way that
respects these type sizes and, where possible, that you use the
API-specific types throughout.
I once wasted many hours tracking down a bug I introduced by falsely
assuming that the EFI status code EFI_STATUS could be
stored in an int. After all, Linux errno
values are all small integers. In fact, EFI_STATUS is a
64-bit value, and trying to store one in a int is highly
problematic. Not to mention frustrating.
Using appropriate types is even more important if you want to support different architectures, or both 32-bit and 64-bit platforms.
Be aware of EFI’s limitations
Even if you develop an application with a POSIX-compatibility layer –
whether your own or from a library – you’ll soon run up against EFI’s
inherent limitations. I mentioned earlier, for example, that the
malloc() function could be substituted for
AllocatePool() in gnu-efi. But what about
realloc() ? It turns out that EFI has no way to
change the size of an allocated region of memory. If you want to provide
an equivalent of realloc(), you’ll need to take over memory
management completely.
Similarly, it’s comparatively easy to manage files using EFI’s Simple File Protocol, but this method has limitations, when compared to using a C standard library. It doesn’t provide any way, for example, to open an existing for appending new data. If your application needs to do this, you’ll have to implement it yourself.
Some of the limitations of the file protocol follow from the fact that it’s only designed to work with VFAT disk partitions. If you want to use any other filesystem, you’ll have to implement it yourself, at the block device level. I find it particularly irksome that EFI will boot my program from a flash drive containing an iso9660 filesystem, but the program then can’t read any files on the flash drive, because Simple File Protocol supports only VFAT.
There are many similar limitations. The EFI console, for example, is not a terminal – you can’t send ANSI escape codes to it to change the text colour or position the cursor. There’s no unified filesystem – you have to handle each storage device as an independent volume – and no concept of a current directory. And so on, and on. You need to be prepared to implement an awful lot of functionality yourself, that would ordinarily be provided by an operating system.
Of course, that’s exactly what you should expect, working in a bare-metal environment.
Closing remarks
That’s enough pontificating. In the next article I’ll move on to file handling.
Have you posted something in response to this page?
Feel free to send a webmention
to notify me, giving the URL of the blog or page that refers to
this one.


