I’ve always wanted to make a kernel, or something that looks like a kernel. It’s a really nice way to approach a lot of common problems you can face in system programming, and plenty of tutorials exists around the web for this kind of task.

What about making it more convoluted for no reasons ? A while ago I made a UEFI EDK2 Platform support package for QEMU as part of the Google Summer Of Code, and I thought why not playing around with EFI ?

Efistub

Linux has a neat feature called called “Efistub”, it allows the kernel to masquarade as a PE/COFF UEFI compliant file that can be booted directly by the firmware with no need for a bootloader.

The stub code starts, prepares the environnement for the kernel, decompresses it (the kernel is compressed and embedded inside the PE application) and control is passed to the kernel, kinda cool.

So, for the kernel we’re going to write, I wanted to have this mechanism. Starting from this point we have two options:

Compliant-enough.h

What do we need to achieve proper efi stubbing + being able to call the runtime services from the kernel?

We need the BootServices table, which allows us to call UEFI functions and services from the context of the UEFI app, and the RuntimeServices table, for calls from a kernel context.

If you never worked with UEFI before, a protocol is a collection of functions offered by an application, for example the EFI_FILE_PROTOCOL offers us filesystems abstractions

typedef struct EFI_FILE_PROTOCOL {
  UINT64 Revision;
  EFI_FILE_OPEN Open;
  EFI_FILE_CLOSE Close;
  EFI_FILE_DELETE Delete;
  EFI_FILE_READ Read;
  EFI_FILE_WRITE Write;
  EFI_FILE_GET_POSITION GetPosition;
  EFI_FILE_SET_POSITION SetPosition;
  EFI_FILE_GET_INFO GetInfo;
  EFI_FILE_SET_INFO SetInfo;
  EFI_FILE_FLUSH Flush;
  EFI_FILE_OPEN_EX OpenEx;   // Added for revision 2
  EFI_FILE_READ_EX ReadEx;   // Added for revision 2
  EFI_FILE_WRITE_EX WriteEx; // Added for revision 2
  EFI_FILE_FLUSH_EX FlushEx; // Added for revision 2
} EFI_FILE_PROTOCOL;

Those protocols are regrouped in handles, a handle is the fundamental object in UEFI, it represents an application, a device … For example a Disk handle exposes a DevicePath protocol, a Block I/O protocol to interact with it. The protocols are provided by the relevant drivers tasked to make the device work.

We can locate protocols on a Handle using the ProtocolHandle() function provided by the BootServices table.

UEFI handles and protocols

Those are two big structures defining functions we can call, and it forces us to define a big chunk of the UEFI specification in our header.

typedef struct {
  EFI_TABLE_HEADER Hdr;

  ...

  // Memory services
  EFI_ALLOCATE_PAGES AllocatePages;
  EFI_FREE_PAGES FreePages;
  EFI_GET_MEMORY_MAP GetMemoryMap;
  EFI_ALLOCATE_POOL AllocatePool;
  EFI_FREE_POOL FreePool;

  ...

  // Protocol Handler services
  EFI_INSTALL_PROTOCOL_INTERFACE InstallProtocolInterface;
  EFI_REINSTALL_PROTOCOL_INTERFACE ReinstallProtocolInterface;
  EFI_UNINSTALL_PROTOCOL_INTERFACE UninstallProtocolInterface;
  EFI_HANDLE_PROTOCOL HandleProtocol;
  VOID *Reserved;
  EFI_REGISTER_PROTOCOL_NOTIFY RegisterProtocolNotify;
  EFI_LOCATE_HANDLE LocateHandle;
  EFI_LOCATE_DEVICE_PATH LocateDevicePath;
  EFI_INSTALL_CONFIGURATION_TABLE InstallConfigurationTable;

  // Image services
  EFI_IMAGE_UNLOAD LoadImage;
  EFI_IMAGE_START StartImage;
  EFI_EXIT Exit;
  EFI_IMAGE_UNLOAD UnloadImage;
  EFI_EXIT_BOOT_SERVICES ExitBootServices;

  ...

  // Open and Close Protocol Services
  EFI_OPEN_PROTOCOL OpenProtocol;
  EFI_CLOSE_PROTOCOL CloseProtocol;
  EFI_OPEN_PROTOCOL_INFORMATION OpenProtocolInformation;

  // Library Services
  EFI_PROTOCOLS_PER_HANDLE ProtocolsPerHandle;
  EFI_LOCATE_HANDLE_BUFFER LocateHandleBuffer;
  EFI_LOCATE_PROTOCOL LocateProtocol;
  EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES InstallMultipleProtocolInterfaces;
  EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES
  UninstallMultipleProtocolInterfaces;

  EFI_CALCULATE_CRC32 CalculateCrc32;

  EFI_COPY_MEM CopyMem;
  EFI_SET_MEM SetMem;
  EFI_CREATE_EVENT_EX CreateEventEx;

} EFI_BOOT_SERVICES;

PE/COFF because why not

Now that we have a UEFI compliant-ish header file, that means we can access the minimal functions that we need to operate as a bootloader, we can even add the GUIDs and definitions of the protocols we wanna use, for example for disk access. But now we have to compile, and UEFI does not use ELF but instead PE/COFF (with the DOS dependency hell included) so we have to compile as PE/COFF with subsystem type 10 (EFI)

We write a little hello world with our newly made lib

#include "Efi.h"

VOID efi_main(IN EFI_HANDLE *ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_STATUS Status;
    Status = SystemTable->ConsOut->ClearScreen(SystemTable->ConsOut);
    Status = SystemTable->ConsOut->OutputString(SystemTable->ConsOut, L"Hi");

    return Status;
}

With this command

clang --target=x86_64-windows -ffreestanding -nostdlib -mno-stack-arg-probe -mgeneral-regs-only -fno-asynchronous-unwind-tables -fuse-ld=lld-link -Wl,-entry:efi_main -Wl,-subsystem:efi_application -Wl,-largeaddressaware -march=x86-64 -mtune=generic -Wall -Os -flto -fno-ident -o  app.efi main.c

And we get an output on the QEMU screen ! All of that without cloning a gigantic project and use its build “tool”. Pretty neat uh? We can interact with the boot services, query handles etc…

#include "Efi.h"

EFI_STATUS efi_main(IN EFI_HANDLE *ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_STATUS Status;
    EFI_LOADED_IMAGE_PROTOCOL *LoadedImage;
    EFI_GUID LoadedImageGUID = EFI_LOADED_IMAGE_PROTOCOL_GUID;
    EFI_GUID SimpleFSGUID = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;

    Status = SystemTable->BootServices->HandleProtocol(ImageHandle, &LoadedImageGUID, (VOID**) &LoadedImage);
    Status = SystemTable->BootServices->HandleProtocol(LoadedImage->DeviceHandle, &SimpleFSGUID, (VOID**) &FileSystem);

    if(Status != 0)
    {
        Status = SystemTable->ConsOut->OutputString(SystemTable->ConsOut, L"An error happened");
    }
    else{
        Status = SystemTable->ConsOut->OutputString(SystemTable->ConsOut, L"it worked lol");
    }


    while(1);
    return Status;
}

Loading a kernel