summaryrefslogtreecommitdiff
path: root/boot/common/efi_loader.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'boot/common/efi_loader.cpp')
-rw-r--r--boot/common/efi_loader.cpp423
1 files changed, 423 insertions, 0 deletions
diff --git a/boot/common/efi_loader.cpp b/boot/common/efi_loader.cpp
new file mode 100644
index 0000000..da10b91
--- /dev/null
+++ b/boot/common/efi_loader.cpp
@@ -0,0 +1,423 @@
+// ============================================================================
+// efi_loader.cpp - Core UEFI loader logic (architecture-independent)
+//
+// Sequence:
+// 1. Load kernel ELF from ESP
+// 2. Parse ELF and load segments into memory
+// 3. Get framebuffer via GOP
+// 4. Find ACPI/FDT tables
+// 5. Get final memory map + ExitBootServices
+// 6. Populate BootInfo and return to arch entry for the final jump
+// ============================================================================
+
+#include <boot/efi.h>
+#include <elf/elf.h>
+#include <boot/boot_info.h>
+
+// TODO: move it to include
+// Stringify helper for kernel file path (wide-string literal from config macro).
+#define _WIDE(x) L##x
+#define WIDE(x) _WIDE(x)
+
+// Parse file and allocate memory for kernel
+extern ElfLoadResult elf_load(const uint8_t* data, uint64_t size, EFI_BOOT_SERVICES* bs);
+
+
+namespace efi {
+
+static EFI_SYSTEM_TABLE* gST = nullptr; /* global System Table, has pointers to console, boot services, runtime services and other*/
+static EFI_BOOT_SERVICES* gBS = nullptr; /* global Boot Services, function table, that has memory allocation, protocol discovery, GetMemoryMap, ExitBootServices and other */
+
+// Max memory-map entries
+//TODO: fix problem with that
+static constexpr uint64_t MAX_MEMORY_REGIONS = 256;
+
+// Low-level halt - safe to call even after ExitBootServices
+[[noreturn]] static void panic_halt() noexcept {
+ for (;;) {
+#if defined(EFI_ARCH_X86_64)
+ asm volatile("hlt");
+#elif defined(EFI_ARCH_AARCH64)
+ asm volatile("wfi");
+#endif
+ }
+}
+
+// Console helpers
+void print(const CHAR16* msg) {
+ gST->ConOut->OutputString(gST->ConOut, const_cast<CHAR16*>(msg));
+}
+
+void print_hex(uint64_t val) {
+ CHAR16 buf[19];
+ buf[0] = u'0';
+ buf[1] = u'x';
+ for (int i = 15; i >= 0; i--) {
+ uint8_t nibble = (val >> (i * 4)) & 0xFu;
+ buf[17 - i] = nibble < 10 ? (u'0' + nibble) : (u'A' + nibble - 10);
+ }
+ buf[18] = 0;
+ print(buf);
+}
+
+[[noreturn]] void panic(const CHAR16* msg) {
+ print(L"[PANIC] ");
+ print(msg);
+ print(L"\r\n");
+ panic_halt();
+}
+
+// Type-safe protocol lookup helpers
+//
+// UEFI's HandleProtocol/LocateProtocol take void** which requires an explicit
+// reinterpret_cast at every call site. These templates absorb the cast.
+// TODO: move it to efi.hpp
+template<typename T>
+[[nodiscard]] static EFI_STATUS handle_protocol(EFI_HANDLE h, EFI_GUID& guid, T** out) {
+ return gBS->HandleProtocol(h, &guid, reinterpret_cast<void**>(out));
+}
+
+template<typename T>
+[[nodiscard]] static EFI_STATUS locate_protocol(EFI_GUID& guid, T** out) {
+ return gBS->LocateProtocol(&guid, nullptr, reinterpret_cast<void**>(out));
+}
+
+// RAII wrapper for EFI_FILE_PROTOCOL
+//
+// Closes the underlying file handle when it goes out of scope.
+// Non-copyable; move is not needed in the loader's simple linear flow.
+// TODO: maybe move it efi.h
+class ScopedFile {
+public:
+ ScopedFile() = default;
+ ScopedFile(const ScopedFile&) = delete;
+ ScopedFile& operator=(const ScopedFile&) = delete;
+
+ ~ScopedFile() { if (h) h->Close(h); }
+
+ EFI_FILE_PROTOCOL* operator->() const noexcept { return h; }
+ explicit operator bool() const noexcept { return h != nullptr; }
+
+ EFI_FILE_PROTOCOL*& get(){
+ return h;
+ }
+private:
+ EFI_FILE_PROTOCOL* h = nullptr;
+};
+
+// Load kernel file from ESP
+//
+// Returns a pointer to the raw ELF bytes allocated with AllocatePages.
+// The caller (efi_loader_main) is responsible for keeping the pages alive
+// until ExitBootServices is called (they are freed automatically thereafter
+// because they are typed EfiLoaderData).
+// image_handle: opaque pointer, that UEFI firmware passes to efi_main - it identifies loaded EFI application (our loader)
+// out_size: size of the file
+// return: pointer to file in allocated memory
+
+uint8_t* load_kernel_file(EFI_HANDLE image_handle, uint64_t* out_size) {
+ EFI_STATUS status;
+
+ // Find the boot device via the loaded-image protocol.
+ EFI_GUID lip_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
+ EFI_LOADED_IMAGE_PROTOCOL* loaded_image = nullptr;
+ status = handle_protocol(image_handle, lip_guid, &loaded_image);
+ if (EFI_ERROR(status))
+ panic(L"Failed to get LoadedImageProtocol");
+
+ // Open the filesystem on that device.
+ EFI_GUID sfsp_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
+ EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fs = nullptr;
+ status = handle_protocol(loaded_image->DeviceHandle, sfsp_guid, &fs);
+ if (EFI_ERROR(status))
+ panic(L"Failed to get SimpleFileSystemProtocol");
+
+ // Open the ESP root directory.
+ ScopedFile root;
+ status = fs->OpenVolume(fs, &root.get());
+ if (EFI_ERROR(status))
+ panic(L"Failed to open ESP volume");
+
+ // Open kernel.elf.
+ ScopedFile kernel_file;
+ status = root->Open(root.get(), &kernel_file.get(),
+ const_cast<CHAR16*>(WIDE("\\" CONFIG_KERNEL_FILE_NAME)),
+ EFI_FILE_MODE_READ, 0);
+ if (EFI_ERROR(status))
+ panic(L"Failed to open " WIDE("\\" CONFIG_KERNEL_FILE_NAME));
+
+ // Query file size via GetInfo.
+ EFI_GUID fi_guid = EFI_FILE_INFO_ID;
+ UINTN info_size = sizeof(EFI_FILE_INFO) + 256; // 256 bytes extra for filename
+ uint8_t info_buf[sizeof(EFI_FILE_INFO) + 256];
+ status = kernel_file->GetInfo(kernel_file.get(), &fi_guid, &info_size, info_buf);
+ if (EFI_ERROR(status))
+ panic(L"Failed to get kernel file info");
+
+ const auto* file_info = reinterpret_cast<const EFI_FILE_INFO*>(info_buf);
+ const uint64_t file_size = file_info->FileSize;
+
+ // Allocate pages to hold the file.
+ // Round-up division to convert a byte size into a number of 4 KiB pages
+ const UINTN pages = (file_size + ELF_PAGE_MASK) / ELF_PAGE_SIZE;
+ EFI_PHYSICAL_ADDRESS addr = 0;
+ status = gBS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages, &addr);
+ if (EFI_ERROR(status))
+ panic(L"Failed to allocate memory for kernel");
+
+ // Read the file into the allocated buffer.
+ auto* file_data = reinterpret_cast<uint8_t*>(addr);
+ UINTN read_size = file_size;
+ status = kernel_file->Read(kernel_file.get(), &read_size, file_data);
+ if (EFI_ERROR(status))
+ panic(L"Failed to read kernel file");
+
+ *out_size = file_size;
+ return file_data;
+}
+
+// Locate GOP framebuffer
+//
+// Graphics Output Protocol: https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html
+// Returns false if GOP is unavailable (non-fatal — the kernel can run headless).
+
+bool get_framebuffer(FramebufferInfo* fb) {
+ EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
+ EFI_GRAPHICS_OUTPUT_PROTOCOL* gop = nullptr;
+ EFI_STATUS status = locate_protocol(gop_guid, &gop);
+ if (EFI_ERROR(status) || !gop || !gop->Mode || !gop->Mode->Info)
+ return false;
+
+ const auto* mode = gop->Mode;
+ const auto* info = mode->Info;
+
+ fb->base = mode->FrameBufferBase;
+ fb->width = info->HorizontalResolution;
+ fb->height = info->VerticalResolution;
+ fb->pitch = info->PixelsPerScanLine * 4;
+
+ switch (info->PixelFormat) {
+ case PixelRedGreenBlueReserved8BitPerColor: fb->format = PixelFormat::RGB; break;
+ case PixelBlueGreenRedReserved8BitPerColor: fb->format = PixelFormat::BGR; break;
+ default: fb->format = PixelFormat::Mask; break;
+ }
+ return true;
+}
+
+// Search the UEFI Configuration Table
+//
+// The System Table has an array of {GUID, pointer} pairs published by various
+// firmware subsystems. Returns the VendorTable pointer, or 0 if not found.
+
+uint64_t find_config_table(const EFI_GUID& target_guid) {
+ for (UINTN i = 0; i < gST->NumberOfTableEntries; i++) {
+ if (guid_equal(gST->ConfigurationTable[i].VendorGuid, target_guid))
+ return reinterpret_cast<uint64_t>(gST->ConfigurationTable[i].VendorTable);
+ }
+ return 0;
+}
+
+// Convert UEFI memory descriptor type to MemoryRegionType
+
+MemoryRegionType convert_memory_type(uint32_t efi_type) {
+ switch (efi_type) {
+ case EfiConventionalMemory: return MemoryRegionType::Usable; /* Free RAM - the kernel can allocate this */
+ case EfiACPIReclaimMemory: return MemoryRegionType::AcpiReclaimable; /* ACPI tables - usable after the kernel is done parsing them */
+ case EfiACPIMemoryNVS: return MemoryRegionType::AcpiNvs; /* ACPI Non_volatile Storage - must be preserved */
+ case EfiBootServicesCode:
+ case EfiBootServicesData:
+ case EfiLoaderCode:
+ case EfiLoaderData: return MemoryRegionType::BootloaderReclaimable; /* Memory used by our EFI loader - the kernel can reclaim it later */
+ default: return MemoryRegionType::Reserved; /* Hardware-reserved, MMIO registers, firmware and other */
+ }
+}
+
+// Pointer arithmetic helper for the packed memory descriptor array
+//
+// UEFI memory descriptors are desc_size bytes each (not sizeof(EFI_MEMORY_DESCRIPTOR)
+// firmware may use a larger struct with extra fields at the end).
+
+[[nodiscard]] static const EFI_MEMORY_DESCRIPTOR* mem_desc_at(const void* map, UINTN index, UINTN desc_size) noexcept {
+ return reinterpret_cast<const EFI_MEMORY_DESCRIPTOR*>(
+ reinterpret_cast<uintptr_t>(map) + index * desc_size
+ );
+}
+
+} // namespace efi
+
+// ============================================================================
+// Main loader — called by arch-specific entry.cpp
+// ============================================================================
+
+bool efi_loader_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table, BootInfo* boot_info) {
+ efi::gST = system_table;
+ efi::gBS = system_table->BootServices;
+
+ efi::gST->ConOut->ClearScreen(efi::gST->ConOut);
+ efi::print(L"Bastion UEFI Loader\r\n");
+ efi::print(L"=====================\r\n\r\n");
+
+ // 1. Load kernel ELF from ESP.
+ efi::print(L"Loading " WIDE("\\" CONFIG_KERNEL_FILE_NAME) " from ESP...\r\n");
+ uint64_t kernel_file_size = 0;
+ uint8_t* kernel_file = efi::load_kernel_file(image_handle, &kernel_file_size);
+ efi::print(L" Size: "); efi::print_hex(kernel_file_size); efi::print(L"\r\n");
+
+ // 2. Parse and load ELF segments into memory.
+ efi::print(L"Parsing ELF...\r\n");
+ ElfLoadResult elf = elf_load(kernel_file, kernel_file_size, efi::gBS);
+ if (!elf.success) {
+ switch (elf.error) {
+ case ELF_ERR_FILE_TOO_SMALL: efi::panic(L"ELF: file too small");
+ case ELF_ERR_INVALID_MAGIC: efi::panic(L"ELF: bad magic");
+ case ELF_ERR_INVALID_CLASS: efi::panic(L"ELF: bad class (need ELF64)");
+ case ELF_ERR_INVALID_IDENT: efi::panic(L"ELF: bad endianness (need LE)");
+ case ELF_ERR_INVALID_ARCH: efi::panic(L"ELF: wrong machine type");
+ case ELF_ERR_INVALID_TYPE: efi::panic(L"ELF: not an executable");
+ case ELF_ERR_NO_LOAD_SEGS: efi::panic(L"ELF: no loadable segments");
+ case ELF_ERR_ALLOC_FAILED:
+ efi::print(L"ELF: AllocatePages failed at "); efi::print_hex(elf.virt_base);
+ efi::print(L" EFI status: "); efi::print_hex(elf.efi_alloc_status);
+ efi::panic(L"\r\n");
+ default:
+ efi::panic(L"ELF: unknown error");
+ }
+ }
+ efi::print(L" Entry: "); efi::print_hex(elf.entry_point); efi::print(L"\r\n");
+ efi::print(L" Phys: "); efi::print_hex(elf.phys_base); efi::print(L"\r\n");
+
+ // 3. Acquire framebuffer (non-fatal if absent).
+ efi::print(L"Locating GOP...\r\n");
+ if (!efi::get_framebuffer(&boot_info->framebuffer)) {
+ efi::print(L" WARNING: No GOP framebuffer found\r\n");
+ #pragma message ("maybe panic too?")
+ } else {
+ efi::print(L" FB at: "); efi::print_hex(boot_info->framebuffer.base); efi::print(L"\r\n");
+ }
+
+ // 4. Find firmware tables (ACPI RSDP or FDT depending on arch).
+#ifdef CONFIG_ARCH_X86
+ EFI_GUID acpi_guid = EFI_ACPI_20_TABLE_GUID;
+ boot_info->rsdp_address = efi::find_config_table(acpi_guid);
+#endif
+#ifdef CONFIG_ARCH_AARCH64
+ EFI_GUID fdt_guid = EFI_DTB_TABLE_GUID;
+ boot_info->fdt_address = efi::find_config_table(fdt_guid);
+#endif
+
+ // 5. GetMemoryMap → ExitBootServices
+ /*
+ Memory map is array of entries
+ each entry is
+ - Base addr - where the region starts
+ - Length - size of region
+ - Type - look MemoryRegionType
+ For example, 256MiB Qemu may have around 120 entreis.
+ For now kernel prints all of them at boot, then sums up the Usable regions to report total free RAM
+
+ map_size - buffer size on input; We pass in how big our buffer is. UEFI fills in how many bytes it actuall used.
+ map_key - opaque snapshot token - must match when calling ExitBootServices.
+ (If an allocation happens between GetMemoryMap and ExitBootServices,
+ the key goes stale and ExitBootServices returns EFI_INVALID_PARAMETER)
+ desc_size - actual size of each EFI_MEMORY_DESCRIPTOR entry in the returned map
+ (may be larger than sizeof(EFI_MEMORY_DESCRIPTOR) in newer
+ firmware — never assume the struct size).
+ */
+ efi::print(L"Exiting boot services...\r\n");
+
+ UINTN map_size = 0, map_key = 0, desc_size = 0;
+ [[maybe_unused]] uint32_t desc_version = 0; //unused
+ EFI_MEMORY_DESCRIPTOR* efi_map = nullptr;
+
+ // Get required size
+ // First call: probe the required buffer size.
+ efi::gBS->GetMemoryMap(&map_size, nullptr, &map_key, &desc_size, &desc_version);
+ // We add desc_size*4 because next allocation will change memory map, and 4 descriptors mush be enough
+ map_size += desc_size * 4;
+ efi::gBS->AllocatePool(EfiLoaderData, map_size, reinterpret_cast<void**>(&efi_map));
+
+ // Second call: fill the buffer.
+ EFI_STATUS status = efi::gBS->GetMemoryMap(&map_size, efi_map, &map_key, &desc_size, &desc_version);
+ if (EFI_ERROR(status))
+ efi::panic(L"GetMemoryMap failed");
+
+ // Point of No Return
+ efi::print(L"EFI Point of No Return\r\n");
+ status = efi::gBS->ExitBootServices(image_handle, map_key);
+ if (EFI_ERROR(status)) {
+ // Map changed between GetMemoryMap and ExitBootServices — retry once.
+ // Do NOT allocate between this GetMemoryMap and ExitBootServices.
+ efi::gBS->GetMemoryMap(&map_size, efi_map, &map_key, &desc_size, &desc_version);
+ status = efi::gBS->ExitBootServices(image_handle, map_key);
+ if (EFI_ERROR(status)) {
+ // EFI services are in an unknown state — use the low-level halt;
+ // calling efi::panic() (which uses ConOut) is unsafe here.
+ efi::panic_halt();
+ }
+ }
+
+ // ═══ NO MORE UEFI CALLS FROM THIS POINT ═══
+
+ // 6. Convert the UEFI memory map to our compact MemoryRegion format.
+ //
+ // Convert big UEFI-specific struct (with fields like catching flags, vaddreses, attr and other)
+ // to our small map (maybe in future i will use more fierds from UEFI map)
+ // Note: MAX_MEMORY_REGIONS (256) covers typical hardware. Systems with
+ // many RAM sticks, large MMIO holes, or complex firmware may exceed this.
+ // TODO: compute the required count from map_size/desc_size before capping.
+ // TODO: read how Linux solves it
+
+ static MemoryRegion new_memory_map[efi::MAX_MEMORY_REGIONS];
+ uint64_t new_memory_map_count = 0;
+
+ const UINTN entry_count = map_size / desc_size;
+ for (UINTN i = 0; i < entry_count && new_memory_map_count < efi::MAX_MEMORY_REGIONS; i++, new_memory_map_count++) {
+ const auto* desc = efi::mem_desc_at(efi_map, i, desc_size);
+ new_memory_map[new_memory_map_count].base = desc->PhysicalStart;
+ new_memory_map[new_memory_map_count].length = desc->NumberOfPages * ELF_PAGE_SIZE;
+ new_memory_map[new_memory_map_count].type = efi::convert_memory_type(desc->Type);
+ }
+
+ // Mark the kernel's physical pages as KernelAndModules so the
+ // PMM (Phase 3) does not hand them out as free RAM.
+ // The kernel was allocated with EfiLoaderData, so convert_memory_type()
+ // classified it as BootLoaderReclaimable - the PMM would free it in Phase 3.
+ {
+ const uint64_t k_base = elf.phys_base;
+ const uint64_t k_end = k_base + elf.total_size;
+ for (uint64_t i = 0; i < new_memory_map_count; ++i) {
+ const uint64_t region_end = new_memory_map[i].base + new_memory_map[i].length;
+ if (new_memory_map[i].base < k_end && region_end > k_base)
+ new_memory_map[i].type = MemoryRegionType::KernelAndModules;
+ }
+ }
+
+ // Mark the framebuffer region so the PMM never hands it out as free RAM.
+ // GOP framebuffer memory isn't guaranteed a distinct EFI type (some firmware
+ // reports it as EfiConventionalMemory), so we fix it up manually here now
+ // that we know the framebuffer address and size.
+ if (boot_info->framebuffer.base != 0) {
+ const uint64_t fb_base = boot_info->framebuffer.base;
+ const uint64_t fb_size = static_cast<uint64_t>(boot_info->framebuffer.height)
+ * boot_info->framebuffer.pitch;
+ const uint64_t fb_end = fb_base + fb_size;
+
+ for (uint64_t i = 0; i < new_memory_map_count; i++) {
+ const uint64_t region_end = new_memory_map[i].base + new_memory_map[i].length;
+ if (new_memory_map[i].base < fb_end && region_end > fb_base)
+ new_memory_map[i].type = MemoryRegionType::Framebuffer;
+ }
+ }
+
+ // 7. Populate BootInfo for the kernel.
+ boot_info->magic = BOOT_INFO_MAGIC;
+ boot_info->memory_map = new_memory_map;
+ boot_info->memory_map_count = new_memory_map_count;
+ boot_info->kernel_phys_base = elf.phys_base;
+ boot_info->kernel_virt_base = elf.virt_base;
+ boot_info->kernel_size = elf.total_size;
+ boot_info->kernel_entry_point = elf.entry_point;
+ boot_info->hhdm_base = 0;
+
+ return true;
+}