This is an RFC for adding the UEFI (Unified Extensible Firmware Interface) platform into LLVM libc.
Context
Objective
Implement a C standard compatible libc environment targeting UEFI with relevant POSIX extensions.
Background
UEFI functions like an operating system, it supplies various interfaces called protocols (see UEFI specification for specifics). UEFI protocols can implement things such as networking, filesystems, graphics output, and many other things. Supporting these on baremetal is complex and difficult due to the wide range , of possible devices. UEFI provides a general interface to the hardware that simplifies things for operating systems & bootloaders.
Many developers of UEFI applications use Tianocore’s EDKII’s SDK. This SDK provides a simplified libc-like setup. However, this requires compiling EDKII and isn’t easy to adapt outside of the EDKII repository.
Design
Overview
When calling an LLVM libc function such as fopen
, puts
, or anything which traditionally calls a syscall, this can call a UEFI protocol function.
LLVM libc can provide access to the EFI system table and image handle for applications. This is necessary for applications to access protocols which are not used by LLVM libc due to it being out of the scope of a libc to provide functions for. This covers protocols like the graphics, networking, and various device driver interfaces.
Detailed Overview
Options for providing EFI system table & image handle
The EFI system table is the entry point table into every protocol and system API UEFI has to offer. The image handle is an opaque type which represents the UEFI application itself. These are both necessary for applications to perform functions which are not implemented in the libc. Without these, it is impossible to do things like load drivers, interface with user accounts, or perform graphics operations.
- Global variables
- Simplest
- Smallest code-wise and binary size
- Wrapper functions
- Requires wrapping many functions
- Needs to still provide direct access to
EFI_HANDLE
- Allows for a more POSIX like experience
- Adds
errno
handling
- Adds
- Limits what is possible due to needing to wrap more things
- Possible to only provide enough to get access to other protocols and wrap
EFI_SYSTEM_TABLE
only
- Possible to only provide enough to get access to other protocols and wrap
- Holding functions
- Similar to global variables
- Only two functions
- Uses the call stack
Start Files (crt0)
The start file needs to provide an EfiMain
instead of the traditional _start
function. This function needs to keep the EFI_SYSTEM_TABLE
and EFI_HANDLE
it receives from the arguments and store it in a way the LLVM libc can utilize it. It would be beneficial to expose these two to applications. This is because they allow functionality beyond the libc. This functionality includes things such as the graphics output protocol, compression, security, user account mangement, networking, and multiprocessor management. It also allows for direct access to the drivers for various kinds of devices like USB and PCI.
File I/O
UEFI has a few different ways of providing file I/O. Two typical ways are EFI_SIMPLE_FILE_SYSTEM_PROTOCOL
and SHELL_FILE_HANDLE
(from UEFI shell protocol). The primary method works outside a UEFI shell but requires mapping the devices to find the correct filesystem. On the otherhand, the SHELL_FILE_HANDLE
can be accessed simply via a UTF-16 string as the path. Fortunately, as both types buffer the output, that makes it possible to provide UEFI_FILE
as a union to mimick the behavior of a file descriptor. By including the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
and EFI_SIMPLE_TEXT_INPUT_PROTOCOL
as stdout
/stderr
and stdin
, we can provide something akin to this:
typedef struct {
union {
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* out;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL* in;
SHELL_FILE_HANDLE* shell;
EFI_FILE_PROTOCOL* file;
} data;
enum {
UEFI_FILE_IS_EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL,
UEFI_FILE_IS_EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL,
UEFI_FILE_IS_SHELL_FILE_HANDLE,
UEFI_SHELL_IS_EFI_FILE_PROTOCOL,
} tag;
} UEFI_FILE;
Networking
It is possible to provide network access via EFI_SIMPLE_NETWORK_PROTOCOL
, EFI_TCP4_PROTOCOL
, EFI_TCP6_PROTOCOL
, EFI_UDP4_PROTOCOL
, and EFI_UDP6_PROTOCOL
. Fortunately, the networking API’s are asynchronous and can be wired up in a way that is non-blocking.
EFI_SIMPLE_NETWORK_PROTOCOL
Simplest, just sends/receives data. However it requires implementing a full network stack.
EFI_TCP4_PROTOCOL
& EFI_TCP6_PROTOCOL
IPv4 & IPv6 TCP protocols
EFI_UDP4_PROTOCOL
& EFI_UDP6_PROTOCOL
IPv4 & IPv6 UDP protocols
Pthreads
It is possible to provide a pthreads compatible API via using EFI_BOOT_SERVICES
’s events and timers. Using the EFI_MP_SERVICES_PROTOCOL
from the UEFI Platform Initialization specification, it is possible to do multi-processor workloads. However, this would require implementing a scheduler. It is possible to implement one or require the application to provide one. Although it is possible to implement pthreads
, this likely will be a feature to be implemented much later on.
UEFI Protocol & libc mapping
A non-exhaustive list of UEFI protocols which can map to libc functions. Some UEFI protocol functions cannot be mapped or do not make sense to be mapped. Some functions not worth mapping are the string functions, memcpy
, and memset
as LLVM libc already provides those functions.
UEFI Protocol | libc |
---|---|
EFI_BOOT_SERVICES.Stall |
sleep |
EFI_RNG_PROTOCOL.GetRNG |
getrandom |
EFI_SHELL_PROTOCOL.GetEnv *1 |
getenv |
EFI_SHELL_PROTOCOL.SetEnv *1 |
setenv |
EFI_SHELL_PROTOCOL.Execute *1 |
exec |
*1 Defined by the UEFI shell protocol specification