Kevin Boone

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.