Dependency-free POSIX compatibility layer for UEFI
bzt ab6f1c382c Fixed issue #54 | 5 months ago | |
---|---|---|
examples | 1 year ago | |
uefi | 5 months ago | |
utils | 2 years ago | |
LICENSE | 3 years ago | |
Makefile | 2 years ago | |
OLVASSEL.md | 1 year ago | |
README.md | 1 year ago |
We hate that horrible and ugly UEFI API, we want the usual POSIX!
NOTE: a smartypants on reddit is worried that I'm supposedly "hiding something" because of the use of -Wno-builtin-declaration-mismatch
(gcc) and -Wno-incompatible-library-redeclaration
(Clang) flags. Here's my answer to you: I hide nothing, that flag is only
needed because you can disable transparent UTF-8 conversion. You see, under the hood UEFI uses 16 bit characters, and for example
strlen(wchar_t *str)
or main(int argc, wchar_t *argv)
isn't exactly POSIX-standard, that's why there's a need for that flag.
You should know that had you have spent more time learning or just reading this README instead of falsely accusing others on reddit.
This is a very small build environment that helps you to develop for UEFI under Linux (and other POSIX systems). It was greatly inspired by gnu-efi (big big kudos to those guys), but it is a lot smaller, easier to integrate (works with LLVM Clang and GNU gcc both) and easier to use because it provides a POSIX like API for your UEFI application.
An UEFI environment consist of two parts: a firmware with GUID protocol interfaces and a user library. We cannot change the former, but we can make the second friendlier. That's what POSIX-UEFI does for your application. It is a small API wrapper library around the GUID protocols, not a fully blown POSIX compatible libc implementation.
You have two options on how to integrate it into your project:
Same method as with gnu-efi, not really recommended. In the uefi
directory, run
$ USE_GCC=1 make
This will create build/uefi
with all the necessary files in it. These are:
You can use this and link your application with it, but you won't be able to recompile it, plus you're on your own with the linking and converting.
Strictly speaking you'll only need crt0.o and link.ld, that will get you started and will call your application's "main()", but to get libc functions like memcmp, strcpy, malloc or fopen, you'll have to link with libuefi.a too.
Clang is configured in a way to directly create PE files, so the resulting library's name is libuefi.dll.a.
This is the preferred way, as it also provides a Makefile to set up your toolchain properly.
uefi
directory into your source tree (or set up a git submodule and a symlink). A dozen files, about 132K in total.make
TARGET = helloworld.efi
include uefi/Makefile
An example helloworld.c goes like this:
#include <uefi.h>
int main(int argc, char **argv)
{
printf("Hello World!\n");
return 0;
}
By default it uses Clang + lld, and PE is generated directly without conversion. If USE_GCC
is set, then the host native's
GNU gcc + ld is used to create a shared object and get converted into an .efi file with objcopy, just like how gnu-efi does.
NOTE: if you don't want to clone this entire repo, just the uefi
directory,
git clone --no-checkout https://gitlab.com/bztsrc/posix-uefi.git .
git sparse-checkout set --no-cone '/uefi/*'
git checkout
Variable | Description |
---|---|
TARGET |
the target application (required) |
SRCS |
list of source files you want to compile (defaults to *.c *.S) |
CFLAGS |
compiler flags you want to use (empty by default, like "-Wall -pedantic -std=c99") |
LDFLAGS |
linker flags you want to use (I don't think you'll ever need this, just in case) |
LIBS |
additional libraries you want to link with (like "-lm", only static .a libraries allowed) |
EXTRA |
any additional object files you might want to link with, these are also called as makefile rules before compiling |
ALSO |
additional makefile rules to be called after compiling |
OUTDIR |
if given, then your project's object files are generated into this directory (by default not set) |
USE_GCC |
set this if you want native GNU gcc + ld + objcopy instead of LLVM Clang + Lld |
ARCH |
the target architecture |
Here's a more advanced Makefile example:
ARCH = x86_64
TARGET = helloworld.efi
SRCS = $(wildcard *.c)
CFLAGS = -pedantic -Wall -Wextra -Werror --std=c11 -O2
LDFLAGS =
LIBS = -lm
OUTDIR = build/loader
USE_GCC = 1
include uefi/Makefile
The build environment configurator was created in a way that it can handle any number of architectures, however only
x86_64
crt0 has been throughfully tested for now. There's an aarch64
and a riscv64
crt0 too, but since I don't
have neither an ARM UEFI, nor a RISC-V UEFI board, these haven't been tested at all. Should work though. If you want
to port it to another architecture, all you need is a setjmp struct in uefi.h, and an appropriate crt_X.c file. That's
it. Everything else was coded in an architecture independent way.
These can be set at the beginning of uefi.h
.
Define | Description |
---|---|
UEFI_NO_UTF8 |
Do not use transparent UTF-8 conversion between the application and the UEFI interface |
UEFI_NO_TRACK_ALLOC |
Do not keep track of allocated buffers (faster, but causes out of bound reads on realloc) |
This library is nowhere near as complete as glibc or musl for example. It only provides the very basic libc functions for you, because simplicity was one of its main goals. It is the best to say this is just wrapper around the UEFI API, rather than a POSIX compatible libc.
All strings in the UEFI environment are stored with 16 bits wide characters. The library provides wchar_t
type for that,
and the UEFI_NO_UTF8
define to convert between char
and wchar_t
transparently. If you have UEFI_NO_UTF8
, then for
example your main() will NOT be like main(int argc, char **argv)
, but main(int argc, wchar_t **argv)
instead. All
the other string related libc functions (like strlen() for example) will use this wide character type too. For this reason,
you must specify your string literals with L""
and characters with L''
. To handle both configurations, char_t
type is
defined, which is either char
or wchar_t
, and the CL()
macro which might add the L
prefix to constant literals.
Functions that are supposed to handle characters in int type (like getchar
, putchar
), do not truncate to unsigned char,
rather to wchar_t.
Sadly UEFI has no concept of reallocation. AllocatePool does not accept input, and there's no way to query the size of an
already allocated buffer. So we are left with two bad options with realloc
:
UEFI_NO_TRACK_ALLOC
define.File types in dirent are limited to directories and files only (DT_DIR, DT_REG), but for stat in addition to S_IFDIR and S_IFREG, S_IFIFO (for console streams: stdin, stdout, stderr), S_IFBLK (for Block IO) and S_IFCHR (for Serial IO) also returned.
Note that getenv
and setenv
aren't POSIX standard, because UEFI environment variables are binary blobs.
That's about it, everything else is the same.
Function | Description |
---|---|
opendir | as usual, but might accept wide char strings |
readdir | as usual |
rewinddir | as usual |
closedir | as usual |
Because UEFI has no concept of device files nor of symlinks, dirent fields are limited and only DT_DIR and DT_REG supported.
Function | Description |
---|---|
atoi | as usual, but might accept wide char strings and understands "0x" prefix |
atol | as usual, but might accept wide char strings and understands "0x" prefix |
strtol | as usual, but might accept wide char strings |
malloc | as usual |
calloc | as usual |
realloc | as usual |
free | as usual |
abort | as usual |
exit | as usual |
exit_bs | leave this entire UEFI bullshit behind (exit Boot Services) |
mbtowc | as usual (UTF-8 char to wchar_t) |
wctomb | as usual (wchar_t to UTF-8 char) |
mbstowcs | as usual (UTF-8 string to wchar_t string) |
wcstombs | as usual (wchar_t string to UTF-8 string) |
srand | as usual |
rand | as usual, but uses EFI_RNG_PROTOCOL if possible |
getenv | pretty UEFI specific |
setenv | pretty UEFI specific |
int exit_bs();
Exit Boot Services. Returns 0 on success. You won't be able to return from main() after calling this successfully, you must transfer control directly.
uint8_t *getenv(char_t *name, uintn_t *len);
Query the value of environment variable name
. On success, len
is set, and a malloc'd buffer returned. It is
the caller's responsibility to free the buffer later. On error returns NULL.
int setenv(char_t *name, uintn_t len, uint8_t *data);
Sets an environment variable by name
with data
of length len
. On success returns 1, otherwise 0 on error.
Function | Description |
---|---|
remove | as usual, but might accept wide char strings |
fopen | as usual, but might accept wide char strings, also for mode |
fclose | as usual |
fflush | as usual |
fread | as usual, only real files and blk io accepted (no stdin) |
fwrite | as usual, only real files and blk io accepted (no stdout nor stderr) |
fseek | as usual, only real files and blk io accepted (no stdin, stdout, stderr) |
ftell | as usual, only real files and blk io accepted (no stdin, stdout, stderr) |
feof | as usual, only real files and blk io accepted (no stdin, stdout, stderr) |
fprintf | as usual, might be wide char strings, BUFSIZ, files, ser, stdout, stderr |
printf | as usual, might be wide char strings, max BUFSIZ, stdout only |
sprintf | as usual, might be wide char strings, max BUFSIZ |
vfprintf | as usual, might be wide char strings, BUFSIZ, files, ser, stdout, stderr |
vprintf | as usual, might be wide char strings, max BUFSIZ, stdout only |
vsprintf | as usual, might be wide char strings, max BUFSIZ |
snprintf | as usual, might be wide char strings |
vsnprintf | as usual, might be wide char strings |
getchar | as usual, blocking, stdin only (no stream redirects), returns UNICODE |
getchar_ifany | non-blocking, returns 0 if there was no key press, UNICODE otherwise |
putchar | as usual, stdout only (no stream redirects) |
String formating is limited; only supports padding via positive number prefixes, %d
, %i
, %x
, %X
, %c
, %s
, %q
and
%p
(no %e
, %f
, %g
, no asterisk and dollar). When UEFI_NO_UTF8
is defined, then formating operates on wchar_t, so
it also supports the non-standard %S
(printing an UTF-8 string) and %Q
(printing an escaped UTF-8 string). These
functions don't allocate memory, but in return the total length of the output string cannot be longer than BUFSIZ
(8k if you haven't defined otherwise), except for the variants which have a maxlen argument. For convenience, %D
requires
efi_physical_address_t
as argument, and it dumps memory, 16 bytes or one line at once. With the padding modifier you can
dump more lines, for example %5D
gives you 5 lines (80 dumped bytes).
File open modes: "r"
read, "w"
write, "a"
append. Because of UEFI peculiarities, "wd"
creates directory.
Special "device files" you can open:
Name | Description |
---|---|
/dev/stdin |
returns ST->ConIn |
/dev/stdout |
returns ST->ConOut, fprintf |
/dev/stderr |
returns ST->StdErr, fprintf |
/dev/serial(baud) |
returns Serial IO protocol, fread, fwrite, fprintf |
/dev/disk(n) |
returns Block IO protocol, fseek, ftell, fread, fwrite, feof |
With Block IO, fseek and buffer size for fread and fwrite is always truncated to the media's block size. So fseek(513) for example will seek to 512 with standard block sizes, and 0 with large 4096 block sizes. To detect the media's block size, use fstat.
if(!fstat(f, &st))
block_size = st.st_size / st.st_blocks;
To interpret a GPT, there are typedefs like efi_partition_table_header_t
and efi_partition_entry_t
which you can point
to the read data.
Function | Description |
---|---|
memcpy | as usual, works on bytes |
memmove | as usual, works on bytes |
memset | as usual, works on bytes |
memcmp | as usual, works on bytes |
memchr | as usual, works on bytes |
memrchr | as usual, works on bytes |
memmem | as usual, works on bytes |
memrmem | as usual, works on bytes |
strcpy | might work on wide char strings |
strncpy | might work on wide char strings |
strcat | might work on wide char strings |
strncat | might work on wide char strings |
strcmp | might work on wide char strings |
strncmp | might work on wide char strings |
strdup | might work on wide char strings |
strchr | might work on wide char strings |
strrchr | might work on wide char strings |
strstr | might work on wide char strings |
strtok | might work on wide char strings |
strtok_r | might work on wide char strings |
strlen | might work on wide char strings |
Function | Description |
---|---|
stat | as usual, but might accept wide char strings |
fstat | UEFI doesn't have fd, so it uses FILE* |
mkdir | as usual, but might accept wide char strings, and mode unused |
Because UEFI has no concept of device major and minor number nor of inodes, struct stat's fields are limited.
The actual implementation of fstat
is in stdio.c, because it needs to access static variables defined there.
Function | Description |
---|---|
localtime | argument unused, always returns current time in struct tm |
mktime | as usual |
time | as usual |
Function | Description |
---|---|
usleep | as usual (uses BS->Stall) |
sleep | as usual |
unlink | as usual, but might accept wide char strings |
rmdir | as usual, but might accept wide char strings |
It is very likely that you want to call UEFI specific functions directly. For that, POSIX-UEFI specifies some globals
in uefi.h
:
Global Variable | Description |
---|---|
*BS , gBS |
efi_boot_services_t, pointer to the Boot Time Services |
*RT , gRT |
efi_runtime_t, pointer to the Runtime Services |
*ST , gST |
efi_system_table_t, pointer to the UEFI System Table |
IM |
efi_handle_t of your Loaded Image |
The EFI structures, enums, typedefs and defines are all converted to ANSI C standard POSIX style, for example BOOLEAN -> boolean_t, UINTN -> uintn_t, EFI_MEMORY_DESCRIPTOR -> efi_memory_descriptor_t, and of course EFI_BOOT_SERVICES -> efi_boot_services_t.
Calling UEFI functions is as simple as with EDK II, just do the call, no need for "uefi_call_wrapper":
ST->ConOut->OutputString(ST->ConOut, L"Hello World!\r\n");
(Note: unlike with printf, with OutputString you must use L""
and also print carrige return L"\r"
before L"\n"
. These
are the small things that POSIX-UEFI does for you to make your life comfortable.)
There are two additional, non-POSIX calls in the library. One is exit_bs()
to exit Boot Services, and the other is
a non-blocking version getchar_ifany()
.
Unlike gnu-efi, POSIX-UEFI does not pollute your application's namespace with unused GUID variables. It only provides header definitions, so you must create each GUID instance if and when you need them.
Example:
efi_guid_t gopGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
efi_gop_t *gop = NULL;
status = BS->LocateProtocol(&gopGuid, NULL, (void**)&gop);
Also unlike gnu-efi, POSIX-UEFI does not provide standard EFI headers. It expects that you have installed those under /usr/include/efi from EDK II or gnu-efi, and POSIX-UEFI makes it possible to use those system wide headers without naming conflicts. POSIX-UEFI itself ships the very minimum set of typedefs and structs (with POSIX-ized names).
#include <efi.h>
#include <uefi.h> /* this will work as expected! Both POSIX-UEFI and EDK II / gnu-efi typedefs available */
The advantage of this is that you can use the simplicity of the POSIX-UEFI library and build environment, while getting access to the most up-to-date protocol and interface definitions at the same time.
IMPORTANT NOTE
In some cases the combination of GNU-EFI headers and Clang might incorrectly define uint64_t
as 32 bits. If this happens,
then
#undef __STDC_VERSION__
#include <efi.h>
#include <uefi.h>
should workaround the problem by avoiding the inclusion of stdint.h and defining uint64_t by efibind.h as unsigned long long
.
POSIX_UEFI is licensed under the terms of the MIT license.
I'd like to say thanks to @vladimir132 for a through testing and very accurate and detailed feedbacks.
Cheers,
bzt