The case for an EFI based bootloader on ARM
Jul 17, 2013
Let’s start off initially with what EFI or UEFI even is. EFI provides a firmware environment with core services and API programs can use for booting and additional services the operating system can use for maintaining platform data such as NVRAM variable storage. EFI’s boot services give the user ways to perform things that are normally done by hand in the bootloader stage, such as printing text to a console, or loading a file off of a volume. You can find the EFI Development Kit II source over here on Tianocore’s GitHub.
U-boot on ARM is very similar, but it doesn’t seem to provide a rich execution environment for programs. The data has to all be loaded in memory by the bootloader during core startup. This isn’t a very flexible design.
But what about XNU?
I made an EFI booter for ARM XNU because I want flexibility. Having a kernel simply loading off TFTP and using U-Boot’s/Linux’s ATAG functionality is nice for creating a core memory map, parsing command lines and also for sending ramdisks to the kernel, but it isn’t enough. The bootloader needs to be recompiled for each board to provide debug output over serial UART.
This gets annoying very quickly, as a new UART driver would need to be made every time a new board configuration is added.
Enter UEFI
UEFI provides core services such as Print
, ZeroMem
and so on. But, those are only the beginning. Enter the EFI boot services table.
typedef struct {
EFI_TABLE_HEADER Hdr;
EFI_RAISE_TPL RaiseTPL;
EFI_RESTORE_TPL RestoreTPL;
EFI_ALLOCATE_PAGES AllocatePages;
EFI_FREE_PAGES FreePages;
EFI_GET_MEMORY_MAP GetMemoryMap;
EFI_ALLOCATE_POOL AllocatePool;
EFI_FREE_POOL FreePool;
EFI_CREATE_EVENT CreateEvent;
EFI_SET_TIMER SetTimer;
EFI_WAIT_FOR_EVENT WaitForEvent;
EFI_SIGNAL_EVENT SignalEvent;
EFI_CLOSE_EVENT CloseEvent;
EFI_CHECK_EVENT CheckEvent;
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;
EFI_IMAGE_LOAD LoadImage;
EFI_IMAGE_START StartImage;
EFI_EXIT Exit;
EFI_IMAGE_UNLOAD UnloadImage;
EFI_EXIT_BOOT_SERVICES ExitBootServices;
EFI_GET_NEXT_MONOTONIC_COUNT GetNextMonotonicCount;
EFI_STALL Stall;
EFI_SET_WATCHDOG_TIMER SetWatchdogTimer;
EFI_CONNECT_CONTROLLER ConnectController;
EFI_DISCONNECT_CONTROLLER DisconnectController;
EFI_OPEN_PROTOCOL OpenProtocol;
EFI_CLOSE_PROTOCOL CloseProtocol;
EFI_OPEN_PROTOCOL_INFORMATION OpenProtocolInformation;
EFI_PROTOCOLS_PER_HANDLE ProtocolsPerHandle;
EFI_LOCATE_HANDLE_BUFFER LocateHandleBuffer;
EFI_LOCATE_PROTOCOL LocateProtocol;
EFI_INSTALL_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;
That’s a lot of boot services. In EFI, everything is handled by specific protocols, which are installed by various drivers. These provide rich functionality for the user programs. For example, the SimpleFileSystem
protocol allows the user to open volumes without having to rewrite platform code to open eMMC, then parse the partition tables, and finally then parsing the filesystem.
Loading the mach_kernel.
Although EFI provides a rich firmware environment, its programming paradigm can be awfully over the top at times.
/* Load the kernel. */
Print(L" * Loading \"%s\"...", kMachKernelName);
Status = gBS->LocateProtocol (&gEfiSimpleFileSystemProtocolGuid, NULL, (VOID **)&FsProtocol);
if(Status != EFI_SUCCESS)
return EFI_LOAD_ERROR;
/* Try to open the volume and get the root directory. */
Status = FsProtocol->OpenVolume (FsProtocol, &Fs);
if(Status != EFI_SUCCESS)
return EFI_LOAD_ERROR;
File = NULL;
Status = Fs->Open (Fs, &File, kMachKernelName, EFI_FILE_MODE_READ, 0);
if(Status != EFI_SUCCESS)
return EFI_LOAD_ERROR;
Size = 0;
File->GetInfo(File, &gEfiFileInfoGuid, &Size, NULL);
FileInfo = AllocatePool (Size);
Status = File->GetInfo(File, &gEfiFileInfoGuid, &Size, FileInfo);
if(Status != EFI_SUCCESS)
return EFI_LOAD_ERROR;
/* Get the file size. */
Size = FileInfo->FileSize;
FreePool(FileInfo);
Status = gBS->AllocatePages(Type, EfiBootServicesCode, EFI_SIZE_TO_PAGES(Size), Image);
if ((Status == EFI_OUT_OF_RESOURCES) && (Type != AllocateAnyPages)) {
Status = gBS->AllocatePages(AllocateAnyPages, EfiBootServicesCode, EFI_SIZE_TO_PAGES(Size), Image);
}
if (!EFI_ERROR (Status)) {
Status = File->Read (File, &Size, (VOID*)(UINTN)(*Image));
}
This is overly complex, but still, not as complex as having to do it all from a core loader. People may ask, “why not just integrate everything into u-boot”? The answer to this is to be board agnostic. If I can just place my boot.efi
file on a SD card for one board and I don’t have to recompile the firmware entirely, it makes my day go better.
Loading Other Files
Darwin on ARM, at least my version, is intended to act a lot like desktop Mac OS X, that is, it will have a proper HFS root file system with a /System/Library/Extensions
folder. The kernel will not be prelinked as per iOS, but will instead be a standalone binary in the root of the filesystem. On u-boot, this isn’t really possible without extending the functionality built in to the bootloader, and I don’t really want to recompile u-boot for every single platform either.
EFI, as I stated before, provides a way to load files off block devices with recognized file systems, albeit a very cumbersome one. Loading kexts and parsing them is a simple task to do in this environment.
Enter the Matrix
Now, passing control from EFI to the actual OS? That’s simple. By calling GetMemoryMap
and then ExitBootServices
, the core of EFI is effectively jettisoned out, and now the OS is free to control mainly all of memory.
I pass control to Darwin by copying the kernel to where it should be, then doing core ARM initialization, and then finally using a bx lr
into the kernel entrypoint.
After that, the operating system has to do everything.
Share