Reputation: 109
I have ELF kernel. So I need a bootloader that will load my 64 bit ELF file. I don't need obsolete Legacy BIOS bootloaders, I need UEFI bootloader with/without GUI.
Upvotes: 0
Views: 2156
Reputation: 2884
I asked a question on StackOverflow not long ago where I give my x64 UEFI bootloader code. Here's the code for reference:
#include <Uefi.h>
#include <Protocol/SimpleFileSystem.h>
#include <Protocol/GraphicsOutput.h>
#include <Library/BaseLib.h>
#define EFI_ACPI_TABLE_GUID { 0xeb9d2d30, 0x2d88, 0x11d3, {0x9a, 0x16, 0x0, 0x90, 0x27, 0x3f, 0xc1, 0x4d }}
#define EFI_ACPI_20_TABLE_GUID { 0x8868e871, 0xe4f1, 0x11d3, {0xbc, 0x22, 0x0, 0x80, 0xc7, 0x3c, 0x88, 0x81 }}
typedef struct {
CHAR8 Signature[8];
UINT8 Checksum;
UINT8 OemId[6];
UINT8 Revision;
UINT32 RsdtAddress;
UINT32 Length;
UINT64 XsdtAddress;
UINT8 ExtendedChecksum;
UINT8 Reserved[3];
} RSDP;
typedef struct {
UINT64 Address;
UINT64 Size;
UINT64 HorizontalResolution;
UINT64 VerticalResolution;
UINT64 PixelsPerScanLine;
} FrameBuffer;
BOOLEAN CompareGUID(EFI_GUID rguid1, EFI_GUID rguid2){
return
rguid1.Data1 == rguid2.Data1 &&
rguid1.Data2 == rguid2.Data2 &&
rguid1.Data3 == rguid2.Data3 &&
rguid1.Data4[0] == rguid2.Data4[0] &&
rguid1.Data4[1] == rguid2.Data4[1] &&
rguid1.Data4[2] == rguid2.Data4[2] &&
rguid1.Data4[3] == rguid2.Data4[3] &&
rguid1.Data4[4] == rguid2.Data4[4] &&
rguid1.Data4[5] == rguid2.Data4[5] &&
rguid1.Data4[6] == rguid2.Data4[6] &&
rguid1.Data4[7] == rguid2.Data4[7];
}
void MemCpy(void *dest, void *src, UINTN n){
CHAR8* cdest = (CHAR8*) dest;
CHAR8* csrc = (CHAR8*) src;
for (UINTN i = 0; i < n; i++)
cdest[i] = csrc[i];
}
EFI_STATUS EFIAPI UefiMain (IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable){
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World!\n");
/*Locate RSDP table*/
RSDP* rsdpPointer = NULL;
RSDP rsdp;
EFI_GUID AcpiGuid = EFI_ACPI_20_TABLE_GUID;
for (UINTN i = 0; i < SystemTable->NumberOfTableEntries; i++){
if (CompareGUID(SystemTable->ConfigurationTable[i].VendorGuid, AcpiGuid)){
CHAR8* TablePointer = (CHAR8*) SystemTable->ConfigurationTable[i].VendorTable;
if (TablePointer[0] == 'R' && TablePointer[1] == 'S' && TablePointer[2] == 'D' && TablePointer[3] == ' ' &&
TablePointer[4] == 'P' && TablePointer[5] == 'T' && TablePointer[6] == 'R' && TablePointer[7] == ' '){
rsdpPointer = (RSDP*)SystemTable->ConfigurationTable[i].VendorTable;
rsdp = *rsdpPointer;
}
}
}
if (rsdpPointer == NULL){
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Kernel Panic!!: Could not locate the RSDP.\n");
goto DONE;
}else{
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Found the RSDP.\n");
}
RSDP* RSDPTable = (RSDP*)0x350000;
*RSDPTable = rsdp;
/*Get the kernel's file*/
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Booting the kernel.\n");
EFI_STATUS Status;
EFI_BOOT_SERVICES* BS = SystemTable->BootServices;
EFI_GUID FSPGuid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
EFI_HANDLE* Handles = NULL;
UINTN HandleCount = 0;
Status = BS->LocateHandleBuffer(ByProtocol, &FSPGuid, NULL, &HandleCount, &Handles);
if (EFI_ERROR(Status)){
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Kernel Panic!!: Could not locate the handle buffer.\n");
goto DONE;
}else{
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Located the handle buffer for file system protocol.\n");
}
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* FS = NULL;
EFI_FILE_PROTOCOL* Root = NULL;
EFI_FILE_PROTOCOL* Token = NULL;
for (UINTN index = 0; index < (UINTN)HandleCount; index++)
{
Status = BS->HandleProtocol(Handles[index], &FSPGuid, (void**)&FS);
Status = FS->OpenVolume(FS, &Root);
Status = Root->Open(Root, &Token, L"kernel.elf", EFI_FILE_MODE_READ, EFI_FILE_MODE_WRITE);
if(!EFI_ERROR(Status))
break;
}
UINTN BufferSize = 100000;
CHAR8 KernelBuffer[100000];
Status = Token->Read(Token, &BufferSize, KernelBuffer);
if(EFI_ERROR(Status)){
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Kernel Panic!!: Located the kernel, but could not read from it.\n");
goto DONE;
}else
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Could read the Kernel properly now jumping to it's entry point.\n");
/*Get the frame buffer info*/
EFI_GUID GOPGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL* GOP;
Status = BS->LocateProtocol(&GOPGuid, NULL, (void**)&GOP);
//GOP->SetMode(GOP, GOP->Mode->MaxMode - 1);
FrameBuffer Video = {GOP->Mode->FrameBufferBase, GOP->Mode->FrameBufferSize, GOP->Mode->Info->HorizontalResolution,
GOP->Mode->Info->VerticalResolution, GOP->Mode->Info->PixelsPerScanLine};
FrameBuffer* VideoPointer = (FrameBuffer*)0x351000;
*VideoPointer = Video;
/*
Got the kernel's file in a buffer, now map it
in memory and jump to its entry point
*/
UINT64 EntryPoint;
MemCpy(&EntryPoint, KernelBuffer + 24, 8);
UINT64 ProgramHeaderPosition;
MemCpy(&ProgramHeaderPosition, KernelBuffer + 32, 8);
UINT16 NumberOfEntriesInProgramHeader;
MemCpy(&NumberOfEntriesInProgramHeader, KernelBuffer + 56, 2);
for (UINTN i = 0; i < NumberOfEntriesInProgramHeader; i++){
UINT32 SegmentType;
UINT64 SegmentDataPosition;
UINT64 SegmentVirtualAddress;
UINT64 SegmentSizeInFile;
UINT64 SegmentSizeInMemory;
MemCpy(&SegmentType, KernelBuffer + ProgramHeaderPosition, 4);
if (SegmentType == 1){
MemCpy(&SegmentDataPosition, KernelBuffer + ProgramHeaderPosition + 8, 8);
MemCpy(&SegmentVirtualAddress, KernelBuffer + ProgramHeaderPosition + 16, 8);
MemCpy(&SegmentSizeInFile, KernelBuffer + ProgramHeaderPosition + 32, 8);
MemCpy(&SegmentSizeInMemory, KernelBuffer + ProgramHeaderPosition + 40, 8);
CHAR8* VirtualAddress = (CHAR8*)SegmentVirtualAddress;
for (UINT64 i = 0; i < SegmentSizeInMemory; i++){
if (i < SegmentSizeInFile){
*(VirtualAddress + i) = *(KernelBuffer + SegmentDataPosition + i);
}else{
*(VirtualAddress + i) = 0;
}
}
}
ProgramHeaderPosition += 56;
}
/*Final jump to the entry point*/
SystemTable->BootServices->ExitBootServices(ImageHandle, 0);
BASE_LIBRARY_JUMP_BUFFER JumpBuffer;
SetJump(&JumpBuffer);
JumpBuffer.Rip = EntryPoint;
LongJump(&JumpBuffer, 1);
DONE:
return EFI_SUCCESS;
}
The code will place the RSDP at 0x350000 and info on the framebuffer at 0x351000. This is simply a design choice I made to simplify passing the data to my kernel at static positions in RAM.
The code allocates a static 100k bytes of data for the kernel which might not be enough for yours. It will boot any ELF file that is static and freestanding and called kernel.elf at the root directory of the ESP partition of your hard-disk.
You may need to allocate the kernel's buffer with AllocatePages instead using the UEFI allocator. I found out that, after the kernel gets to a certain size, the static allocation will make the UEFI app crash.
If you want info on how to compile the code look at my answer here: Build edk2 in linux
Also, if you do decide to compile the bootloader and run into any problems feel free to ask me directly. I may help fix any issue.
Upvotes: 1
Reputation: 37214
I have ELF kernel. So I need a bootloader that will load my 64 bit ELF file.
You have an ELF kernel; so you probably need a boot loader that will:
load the kernel's ELF file
tell the kernel about the memory map
tell the kernel various things about the hardware ("flattened device tree" or ACPI tables or ...), likely including frame buffer details.
also load other files (e.g. initial RAM disk); because (even for a "modular monolithic" kernel) the kernel can't load a disk driver from disk when it hasn't loaded a disk driver from disk yet.
tell the kernel some kind of kernel configuration (could be another file, could be "kernel command line args").
who knows what else (e.g. I expect my boot loaders to set up paging and map kernel at its final virtual address; decompression is worth considering to improve boot times when disk IO is slow; doing sanity checks to see if kernel's file has been tampered with before trusting it makes sense, ...).
In other words; you need a full detailed specification about how any/all of these things happen (e.g. how kernel retrieves memory map from boot loader, which format it's in, if there's guarantees like "2 or more entries in the memory map will not describe the same/overlapping area of memory", etc); where both the boot loader (or all boot loaders) and the kernel (or all kernels) comply with the detailed specification.
"The kernel is ELF" is just a small piece of that detailed specification.
This leaves you with 2 choices:
find a detailed specification designed by someone else that happens to include "the kernel is ELF" (or at least "the boot loader must support ELF"), and adopt their specification, and then put up with all of their design decisions whether they make sense for your OS or not. The only choice here (that I know of) is the multi-boot specification.
create your own detailed specification designed for your OS; then either write your own boot loader/s or let other people write them from your specification. This is what almost every well known kernel (Windows, Linux, FreeBSD, ...) does.
Note 1: typically it's not "one boot loader", but more like a set of them (one for booting from "GPT partitioned hard disk", one for booting from network, another for booting from removable media, ...). It's possible to work around this by splitting "boot loader" into halves (many different "1st stages" that handle the differences, plus a common "2nd stage" that handles the similarities).
Note 2: For UEFI, you could just use UEFI as the "detailed specification designed by someone else" and not bother having a boot loader. In that case you'd have to either convert your ELF into PE format; or insert your ELF file "as is" inside a PE file as data (where the PE file has some code to unpack the ELF contained inside it).
Note 3: In theory it's really about environments; where "boot loader" changes from one environment (e.g. from UEFI) to another environment (e.g. to "the environment your kernel expects"); and these "pieces of code that change environments" can be layered (e.g. maybe "BIOS -> multi-boot -> UEFI emulator -> something else -> what your kernel expects").
Note 4: For the final "environment your kernel expects"; it may be worthwhile considering things like "kexec()" where a previous instance of your kernel is used to start the next instance of your kernel. This has practical benefits of its own (e.g. faster kernel updates), but the main reason to think about it is to refine the design of the "environment your kernel expects" (to help determine what the kernel actually wants and avoid introducing baggage from what other environments provide that aren't quite what your kernel really wanted).
TLDR: You'll probably end up using Multiboot2 (and GRUB). You can find the specification for Multiboot2 here: https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html
Upvotes: 3