diff --git a/Makefile b/Makefile index 9f93c4e..cd94090 100644 --- a/Makefile +++ b/Makefile @@ -341,6 +341,63 @@ schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $ kernel: $(BUILD)/kernel/kernel +# ----------------------------------------------------------------------- +# UEFI loader +# +# A PE/COFF executable, not ELF, so it needs a different target and a +# different linker driver -- but the same clang install. That is the whole +# reason the toolchain is clang: one compiler reaches every target here. +# +# -mno-red-zone is not optional. UEFI firmware may take an interrupt at any +# point, and the red zone is memory below RSP that a handler is entitled to +# clobber; leaving it enabled corrupts locals at random. +# -fshort-wchar makes L"..." UTF-16, which is the only string type UEFI has. +# ----------------------------------------------------------------------- +UEFI_CFLAGS = --target=x86_64-unknown-windows -ffreestanding -fshort-wchar -mno-red-zone -fno-stack-protector -Wall -Wextra -O1 -I./include + +$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/schboot.h + @mkdir -p $(BUILD)/uefi + clang $(UEFI_CFLAGS) -c boot/uefi/main.c -o $(BUILD)/uefi/main.obj + lld-link -subsystem:efi_application -entry:EfiMain -nodefaultlib $(BUILD)/uefi/main.obj -out:$@ + +uefi: $(BUILD)/uefi/BOOTX64.EFI + +# The EFI System Partition, as a directory. QEMU serves a directory as FAT +# with `-drive file=fat:rw:`, which is enough to boot and test without any +# image-building tools -- mtools is not present on every supported host. +# A real ESP image for `make iso` is still to come. +$(BUILD)/esp/EFI/BOOT/BOOTX64.EFI: $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/kernel/kernel + @mkdir -p $(BUILD)/esp/EFI/BOOT $(BUILD)/esp/schos + @cp $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI + @cp $(BUILD)/kernel/kernel $(BUILD)/esp/schos/kernel + @cp boot/uefi/startup.nsh $(BUILD)/esp/startup.nsh + @echo "ESP staged in $(BUILD)/esp" + +esp: $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI + +# Boot the UEFI path under OVMF. The firmware ships with QEMU; the varstore +# is copied because it must be writable. +# +# i440fx, not q35: the kernel's IDE driver speaks to the legacy ports at +# 0x1f0, and q35 has no legacy IDE at all -- only AHCI. The root volume would +# simply not be there. +OVMF_CODE = $(BUILD)/OVMF_CODE.fd +OVMF_VARS = $(BUILD)/OVMF_VARS.fd +QEMU_SHARE = $(dir $(shell command -v $(QEMU)))share + +$(OVMF_CODE): + @mkdir -p $(BUILD) + cp "$(QEMU_SHARE)/edk2-x86_64-code.fd" $@ + chmod u+w $@ + +$(OVMF_VARS): + @mkdir -p $(BUILD) + cp "$(QEMU_SHARE)/edk2-i386-vars.fd" $@ + chmod u+w $@ + +qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) + $(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 -cpu qemu64,+rdtscp -serial mon:stdio $(QEMU_DISPLAY) + # Build the ramdisk-root variant. This is a target rather than just # "make RAMROOT=1" because toggling RAMROOT changes CFLAGS (-DRAMDISK_ROOT), # and make has no way to notice that: objects compiled for one setting would be diff --git a/boot/README.md b/boot/README.md index d590b2d..4e7b762 100644 --- a/boot/README.md +++ b/boot/README.md @@ -25,7 +25,7 @@ a second loader is correct exactly when it satisfies it. | | Where | Notes | |---|---|---| | Kernel image | loaded per its ELF program headers | entry at `entry64` | -| CPU state | long mode, paging on, interrupts off | BIOS path builds this itself | +| CPU state | see below -- the two paths differ here | | | Memory map | `E820_MAP_ADDR`, 24-byte entries, count at `E820_COUNT_ADDR` | `kernel/arch/x86_64/e820.c` reads it | | Framebuffer | `VBE_INFO_ADDR` | address, pitch, width, height, depth | | Verbose flag | `BOOT_VERBOSE_FLAG_ADDR` | set by the F8 menu | @@ -35,6 +35,35 @@ The addresses are defined once, in `include/memlayout.h` and kernel does not care which firmware produced them, and that is what keeps the split cheap. +## Two entry points, because the two firmwares hand over differently + +The BIOS path enters the kernel in **32-bit protected mode** at the multiboot +entry, with EAX holding the framebuffer address; `mboot_entry` then builds page +tables and climbs into long mode itself. + +UEFI hands a loader long mode with paging already on. Dropping back to +protected mode only to climb out again is a mode switch whose failure mode is a +triple fault with no console, so the kernel publishes a second entry instead. +`kernel/arch/x86_64/entry.S` carries a `sch64_header` — magic `SCH64BOO`, +8-byte aligned within the first 16 KB, followed by a 64-bit physical address — +found by scanning, exactly as the BIOS loader scans for the multiboot magic. +`include/schboot.h` holds the magic so loader and kernel cannot disagree. + +What that entry expects: + +- long mode, interrupts off +- CR3 pointing at tables that identity-map the low 4 GB **and** map the same + 4 GB at `0xffff800000000000`. `entry64low`'s first instruction is a `movabs` + to a high address, so identity alone triple faults immediately. +- RBX = the framebuffer physical address, and never 1 — that value marks an + application processor in `entry64high`. + +The kernel loads its own GDT at that entry (`entry64uefi`), rather than the +loader doing it. A loader arriving in long mode is still on the firmware's +descriptors, and the first thing the kernel does that depends on its own +selectors takes a `#GP` against a GDT the firmware no longer owns — it +surfaced as `unexpected trap 13 ... cs=38`, a long way from the cause. + `e820.c` reads a *BIOS* memory map. UEFI hands out a different structure (`EFI_MEMORY_DESCRIPTOR`, from `GetMemoryMap`), so the UEFI loader has to translate it into the same 24-byte entries rather than inventing a second diff --git a/boot/uefi/efi.h b/boot/uefi/efi.h new file mode 100644 index 0000000..93e8a62 --- /dev/null +++ b/boot/uefi/efi.h @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#ifndef SCHOS_EFI_H +#define SCHOS_EFI_H + +// The parts of UEFI this loader actually uses, written from the +// specification. +// +// Not gnu-efi and not EDK II. Both are large dependencies for something this +// small, and both raise a licensing question the project does not need to +// answer -- see docs/licensing.md. What is here is the handful of structures +// and function pointers needed to read a file, ask for the framebuffer, take +// the memory map and leave: perhaps two hundred lines against tens of +// thousands. +// +// Everything is called through function pointers in tables the firmware hands +// us, using the Microsoft x64 calling convention. clang's +// --target=x86_64-unknown-windows produces that natively, which is why no +// __attribute__((ms_abi)) appears anywhere below. +// +// Fields this loader never touches are left as `void *` padding rather than +// spelled out. The offsets are what matter and they are preserved exactly; +// naming a field we do not use would only invite it to drift. + +typedef unsigned char UINT8; +typedef unsigned short UINT16; +typedef unsigned int UINT32; +typedef unsigned long long UINT64; +typedef long long INTN; +typedef unsigned long long UINTN; +typedef UINT16 CHAR16; +typedef UINT8 BOOLEAN; +typedef void *EFI_HANDLE; +typedef UINTN EFI_STATUS; +typedef UINT64 EFI_PHYSICAL_ADDRESS; +typedef UINT64 EFI_VIRTUAL_ADDRESS; + +#define EFI_SUCCESS 0 +#define EFI_BUFFER_TOO_SMALL ((EFI_STATUS)(0x8000000000000000ULL | 5)) +#define EFI_ERROR(s) (((INTN)(s)) < 0) + +typedef struct { + UINT32 Data1; + UINT16 Data2; + UINT16 Data3; + UINT8 Data4[8]; +} EFI_GUID; + +// --- memory --------------------------------------------------------------- + +typedef enum { + AllocateAnyPages, + AllocateMaxAddress, + AllocateAddress +} EFI_ALLOCATE_TYPE; + +// Only the types this loader has to reason about are named. The rule it +// applies is simply which of them are free for the kernel to use. +#define EfiReservedMemoryType 0 +#define EfiLoaderCode 1 +#define EfiLoaderData 2 +#define EfiBootServicesCode 3 +#define EfiBootServicesData 4 +#define EfiRuntimeServicesCode 5 +#define EfiRuntimeServicesData 6 +#define EfiConventionalMemory 7 +#define EfiUnusableMemory 8 +#define EfiACPIReclaimMemory 9 +#define EfiACPIMemoryNVS 10 +#define EfiMemoryMappedIO 11 +#define EfiMemoryMappedIOPortSpace 12 +#define EfiPalCode 13 +#define EfiPersistentMemory 14 + +typedef struct { + UINT32 Type; + UINT32 Pad; + EFI_PHYSICAL_ADDRESS PhysicalStart; + EFI_VIRTUAL_ADDRESS VirtualStart; + UINT64 NumberOfPages; + UINT64 Attribute; +} EFI_MEMORY_DESCRIPTOR; + +// --- simple text output --------------------------------------------------- + +typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { + void *Reset; + EFI_STATUS (*OutputString)(struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + CHAR16 *String); + void *TestString; + void *QueryMode; + void *SetMode; + void *SetAttribute; + EFI_STATUS (*ClearScreen)(struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This); + void *SetCursorPosition; + void *EnableCursor; + void *Mode; +} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL; + +// --- boot services -------------------------------------------------------- + +typedef struct { + char Hdr[24]; + void *RaiseTPL; + void *RestoreTPL; + + EFI_STATUS (*AllocatePages)(EFI_ALLOCATE_TYPE Type, UINTN MemoryType, + UINTN Pages, EFI_PHYSICAL_ADDRESS *Memory); + EFI_STATUS (*FreePages)(EFI_PHYSICAL_ADDRESS Memory, UINTN Pages); + EFI_STATUS (*GetMemoryMap)(UINTN *MemoryMapSize, + EFI_MEMORY_DESCRIPTOR *MemoryMap, + UINTN *MapKey, UINTN *DescriptorSize, + UINT32 *DescriptorVersion); + EFI_STATUS (*AllocatePool)(UINTN PoolType, UINTN Size, void **Buffer); + EFI_STATUS (*FreePool)(void *Buffer); + + void *CreateEvent; + void *SetTimer; + void *WaitForEvent; + void *SignalEvent; + void *CloseEvent; + void *CheckEvent; + + void *InstallProtocolInterface; + void *ReinstallProtocolInterface; + void *UninstallProtocolInterface; + EFI_STATUS (*HandleProtocol)(EFI_HANDLE Handle, EFI_GUID *Protocol, + void **Interface); + void *Reserved; + void *RegisterProtocolNotify; + void *LocateHandle; + void *LocateDevicePath; + void *InstallConfigurationTable; + + void *LoadImage; + void *StartImage; + void *Exit; + void *UnloadImage; + EFI_STATUS (*ExitBootServices)(EFI_HANDLE ImageHandle, UINTN MapKey); + + void *GetNextMonotonicCount; + EFI_STATUS (*Stall)(UINTN Microseconds); + void *SetWatchdogTimer; + + void *ConnectController; + void *DisconnectController; + + void *OpenProtocol; + void *CloseProtocol; + void *OpenProtocolInformation; + + void *ProtocolsPerHandle; + void *LocateHandleBuffer; + EFI_STATUS (*LocateProtocol)(EFI_GUID *Protocol, void *Registration, + void **Interface); +} EFI_BOOT_SERVICES; + +typedef struct { + char Hdr[24]; + CHAR16 *FirmwareVendor; + UINT32 FirmwareRevision; + EFI_HANDLE ConsoleInHandle; + void *ConIn; + EFI_HANDLE ConsoleOutHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; + EFI_HANDLE StandardErrorHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; + void *RuntimeServices; + EFI_BOOT_SERVICES *BootServices; + UINTN NumberOfTableEntries; + void *ConfigurationTable; +} EFI_SYSTEM_TABLE; + +// --- loaded image --------------------------------------------------------- + +#define EFI_LOADED_IMAGE_PROTOCOL_GUID \ + { 0x5B1B31A1, 0x9562, 0x11D2, { 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } } + +typedef struct { + UINT32 Revision; + EFI_HANDLE ParentHandle; + void *SystemTable; + EFI_HANDLE DeviceHandle; // the volume this image was loaded from + void *FilePath; + void *Reserved; + UINT32 LoadOptionsSize; + void *LoadOptions; + void *ImageBase; + UINT64 ImageSize; + UINTN ImageCodeType; + UINTN ImageDataType; + void *Unload; +} EFI_LOADED_IMAGE_PROTOCOL; + +// --- file system ---------------------------------------------------------- + +#define EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID \ + { 0x0964E5B22, 0x6459, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } } + +#define EFI_FILE_MODE_READ 0x0000000000000001ULL + +typedef struct EFI_FILE_PROTOCOL { + UINT64 Revision; + EFI_STATUS (*Open)(struct EFI_FILE_PROTOCOL *This, + struct EFI_FILE_PROTOCOL **NewHandle, + CHAR16 *FileName, UINT64 OpenMode, UINT64 Attributes); + EFI_STATUS (*Close)(struct EFI_FILE_PROTOCOL *This); + void *Delete; + EFI_STATUS (*Read)(struct EFI_FILE_PROTOCOL *This, UINTN *BufferSize, + void *Buffer); + void *Write; + EFI_STATUS (*GetPosition)(struct EFI_FILE_PROTOCOL *This, UINT64 *Position); + EFI_STATUS (*SetPosition)(struct EFI_FILE_PROTOCOL *This, UINT64 Position); + void *GetInfo; + void *SetInfo; + void *Flush; +} EFI_FILE_PROTOCOL; + +typedef struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL { + UINT64 Revision; + EFI_STATUS (*OpenVolume)(struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This, + EFI_FILE_PROTOCOL **Root); +} EFI_SIMPLE_FILE_SYSTEM_PROTOCOL; + +// --- graphics output ------------------------------------------------------ + +#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID \ + { 0x9042A9DE, 0x23DC, 0x4A38, { 0x96, 0xFB, 0x7A, 0xDE, 0xD0, 0x80, 0x51, 0x6A } } + +#define PixelRedGreenBlueReserved8BitPerColor 0 +#define PixelBlueGreenRedReserved8BitPerColor 1 +#define PixelBitMask 2 +#define PixelBltOnly 3 + +typedef struct { + UINT32 Version; + UINT32 HorizontalResolution; + UINT32 VerticalResolution; + UINT32 PixelFormat; + UINT32 PixelInformation[4]; + UINT32 PixelsPerScanLine; +} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION; + +typedef struct { + UINT32 Mode; + UINT32 MaxMode; + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info; + UINTN SizeOfInfo; + EFI_PHYSICAL_ADDRESS FrameBufferBase; + UINTN FrameBufferSize; +} EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE; + +typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL { + void *QueryMode; + void *SetMode; + void *Blt; + EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode; +} EFI_GRAPHICS_OUTPUT_PROTOCOL; + +#endif /* SCHOS_EFI_H */ diff --git a/boot/uefi/main.c b/boot/uefi/main.c new file mode 100644 index 0000000..359d250 --- /dev/null +++ b/boot/uefi/main.c @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// SchOS UEFI loader. +// +// The BIOS path and this one meet at a contract, not at shared code: see +// boot/README.md. This loader's whole job is to arrive at that contract from +// the other direction. +// +// 1. find the kernel on the volume this image was loaded from +// 2. load its ELF PT_LOAD segments to their physical addresses +// 3. record the framebuffer where kernel/dev/vga.c looks for it +// 4. translate the UEFI memory map into the E820 entries +// kernel/arch/x86_64/e820.c already parses +// 5. ExitBootServices +// 6. build the page tables the kernel's 64-bit entry expects, and jump +// +// Step 6 is why kernel/arch/x86_64/entry.S grew a `sch64_header`. The +// multiboot entry is 32-bit code that climbs into long mode itself; the +// firmware has already put us there, and dropping back down only to climb out +// again is a mode switch whose failure mode is a triple fault with no console. +// The kernel publishes its 64-bit entry instead, and this jumps straight to it. +// +// Nothing here allocates after ExitBootServices, and nothing calls a firmware +// service after it either -- there are none. Everything the jump needs is +// computed before. + +#include "efi.h" +#include "../../include/schboot.h" + +// Where the kernel expects to find things. These are the BIOS loader's +// addresses and they are deliberately unchanged: the kernel should not be able +// to tell which firmware booted it. +#define VBE_INFO_ADDR 0x30000 +#define E820_COUNT_ADDR (VBE_INFO_ADDR + 0x200) +#define E820_MAP_ADDR (VBE_INFO_ADDR + 0x208) +#define E820_MAXENTRIES 32 +#define E820_USABLE 1 +#define E820_RESERVED 2 +#define E820_ACPI 3 +#define E820_NVS 4 + +#define KERNEL_PATH L"\\schos\\kernel" +#define KERNEL_PATH_ALT L"\\kernel" + +// Page tables are built here, exactly where mboot_entry builds them, so the +// kernel's entry sees the state it was written for. +#define PML4_ADDR 0x1000 +#define PDPT_ADDR 0x2000 + +#define PTE_P 0x001 +#define PTE_W 0x002 +#define PTE_PS 0x080 + +struct e820entry { + UINT64 base; + UINT64 length; + UINT32 type; + UINT32 acpi_ext; +} __attribute__((packed)); + +// ELF64, only what is needed to place segments. +struct elf64_ehdr { + UINT8 e_ident[16]; + UINT16 e_type, e_machine; + UINT32 e_version; + UINT64 e_entry, e_phoff, e_shoff; + UINT32 e_flags; + UINT16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx; +}; + +struct elf64_phdr { + UINT32 p_type, p_flags; + UINT64 p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align; +}; + +#define PT_LOAD 1 + +static EFI_SYSTEM_TABLE *ST; +static EFI_BOOT_SERVICES *BS; + +// --------------------------------------------------------------------------- +// Console. Enough to say what went wrong, and no more. +// --------------------------------------------------------------------------- + +static void +print(const CHAR16 *s) +{ + if (ST && ST->ConOut) + ST->ConOut->OutputString(ST->ConOut, (CHAR16 *)s); +} + +static void +print_hex(UINT64 v) +{ + CHAR16 buf[19]; + const CHAR16 *digits = L"0123456789abcdef"; + buf[0] = L'0'; buf[1] = L'x'; + for (int i = 0; i < 16; i++) + buf[2 + i] = digits[(v >> ((15 - i) * 4)) & 0xf]; + buf[18] = 0; + print(buf); +} + +// A failure here cannot be recovered from and must not be silent: without this +// the firmware simply returns to its boot menu and the user is told nothing. +static EFI_STATUS +die(const CHAR16 *msg, EFI_STATUS st) +{ + print(L"schos: "); print(msg); + print(L" (status "); print_hex(st); print(L")\r\n"); + print(L"schos: halted.\r\n"); + for (;;) + __asm__ volatile("hlt"); +} + +static void +zero(void *p, UINTN n) +{ + UINT8 *b = (UINT8 *)p; + while (n--) *b++ = 0; +} + +static void +copy(void *dst, const void *src, UINTN n) +{ + UINT8 *d = (UINT8 *)dst; + const UINT8 *s = (const UINT8 *)src; + while (n--) *d++ = *s++; +} + +// --------------------------------------------------------------------------- +// Reading the kernel off the ESP +// --------------------------------------------------------------------------- + +static EFI_FILE_PROTOCOL * +open_kernel(EFI_HANDLE image) +{ + EFI_GUID li_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID; + EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID; + EFI_LOADED_IMAGE_PROTOCOL *li = 0; + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0; + EFI_FILE_PROTOCOL *root = 0, *file = 0; + EFI_STATUS st; + + // The volume this loader was itself loaded from, rather than a search of + // every filesystem: booting one installation's kernel from another's ESP + // is not a thing to do by accident. + st = BS->HandleProtocol(image, &li_guid, (void **)&li); + if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st); + + st = BS->HandleProtocol(li->DeviceHandle, &fs_guid, (void **)&fs); + if (EFI_ERROR(st)) die(L"boot volume has no filesystem", st); + + st = fs->OpenVolume(fs, &root); + if (EFI_ERROR(st)) die(L"cannot open the boot volume", st); + + st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0); + if (EFI_ERROR(st)) + st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH_ALT, + EFI_FILE_MODE_READ, 0); + if (EFI_ERROR(st)) die(L"no kernel at \\schos\\kernel or \\kernel", st); + + return file; +} + +// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the +// kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and +// p_paddr is where the linker script says each segment belongs. +static UINT64 +load_elf(EFI_FILE_PROTOCOL *file) +{ + struct elf64_ehdr eh; + UINTN n = sizeof eh; + EFI_STATUS st; + + st = file->SetPosition(file, 0); + if (EFI_ERROR(st)) die(L"cannot seek the kernel", st); + st = file->Read(file, &n, &eh); + if (EFI_ERROR(st) || n != sizeof eh) die(L"cannot read the ELF header", st); + + if (eh.e_ident[0] != 0x7f || eh.e_ident[1] != 'E' || + eh.e_ident[2] != 'L' || eh.e_ident[3] != 'F') + die(L"kernel is not an ELF file", 0); + if (eh.e_ident[4] != 2) // ELFCLASS64 + die(L"kernel is not 64-bit", 0); + + UINT64 lowest = ~0ULL, highest = 0; + + for (UINT16 i = 0; i < eh.e_phnum; i++) { + struct elf64_phdr ph; + n = sizeof ph; + st = file->SetPosition(file, eh.e_phoff + (UINT64)i * eh.e_phentsize); + if (EFI_ERROR(st)) die(L"cannot seek a program header", st); + st = file->Read(file, &n, &ph); + if (EFI_ERROR(st) || n != sizeof ph) die(L"cannot read a program header", st); + + if (ph.p_type != PT_LOAD || ph.p_memsz == 0) + continue; + + // Claim the pages before writing them. The firmware is still running + // and this is its memory until it says otherwise; writing to + // conventional memory it has not handed over is how a loader corrupts + // the very services it is about to call. + EFI_PHYSICAL_ADDRESS at = ph.p_paddr & ~0xfffULL; + UINTN pages = (UINTN)((ph.p_paddr + ph.p_memsz - at + 0xfff) / 0x1000); + st = BS->AllocatePages(AllocateAddress, EfiLoaderData, pages, &at); + if (EFI_ERROR(st)) { + // Already allocated by the firmware for something of its own, or + // simply not conventional memory. Not fatal on its own -- the + // range may still be usable -- but worth saying, because a kernel + // that boots and then corrupts itself starts here. + print(L"schos: warning: could not reserve "); print_hex(at); + print(L"\r\n"); + } + + if (ph.p_filesz) { + n = (UINTN)ph.p_filesz; + st = file->SetPosition(file, ph.p_offset); + if (EFI_ERROR(st)) die(L"cannot seek a segment", st); + st = file->Read(file, &n, (void *)(UINTN)ph.p_paddr); + if (EFI_ERROR(st) || n != ph.p_filesz) + die(L"short read loading a segment", st); + } + // .bss: present in memsz, absent from filesz, and the kernel assumes + // it is zeroed. + if (ph.p_memsz > ph.p_filesz) + zero((void *)(UINTN)(ph.p_paddr + ph.p_filesz), + (UINTN)(ph.p_memsz - ph.p_filesz)); + + if (ph.p_paddr < lowest) lowest = ph.p_paddr; + if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz; + } + + if (highest == 0) die(L"kernel has no loadable segments", 0); + + // Find the 64-bit entry the kernel publishes, by scanning the first 16 KB + // of what was just loaded -- the same way the BIOS loader finds the + // multiboot header. A magic in the image beats a symbol table: no ELF + // symbol parsing, and no fixed offset to drift. + UINT8 *img = (UINT8 *)(UINTN)lowest; + for (UINTN off = 0; off + sizeof(struct sch64_header) <= 16384; off += 8) { + struct sch64_header *h = (struct sch64_header *)(img + off); + if (h->magic0 == SCH64_MAGIC0 && h->magic1 == SCH64_MAGIC1) + return h->entry; + } + + die(L"kernel has no SCH64 header -- too old for this loader", 0); + return 0; +} + +// --------------------------------------------------------------------------- +// Framebuffer +// --------------------------------------------------------------------------- + +static UINT32 +setup_framebuffer(void) +{ + EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + EFI_GRAPHICS_OUTPUT_PROTOCOL *gop = 0; + + if (EFI_ERROR(BS->LocateProtocol(&gop_guid, 0, (void **)&gop)) || !gop) { + print(L"schos: no graphics output; console only\r\n"); + return 0; + } + + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = gop->Mode->Info; + UINT32 fb = (UINT32)gop->Mode->FrameBufferBase; + + // The layout kernel/dev/vga.c reads, byte for byte. It is a VBE mode-info + // block as the BIOS path leaves it, and the kernel takes exactly four + // fields out of it -- so those four are what get written, at the offsets + // it uses. + UINT8 *vbe = (UINT8 *)VBE_INFO_ADDR; + zero(vbe, 512); + + UINT32 bpp = 32; // GOP's 8-bit formats are 32bpp + UINT16 stride = (UINT16)(info->PixelsPerScanLine * (bpp / 8)); + + *(UINT16 *)(vbe + 16) = stride; // bytes/line + *(UINT16 *)(vbe + 18) = (UINT16)info->HorizontalResolution; + *(UINT16 *)(vbe + 20) = (UINT16)info->VerticalResolution; + *(UINT8 *)(vbe + 25) = (UINT8)bpp; + *(UINT32 *)(vbe + 40) = fb; + + print(L"schos: framebuffer "); print_hex(fb); + print(L" "); print_hex(info->HorizontalResolution); + print(L"x"); print_hex(info->VerticalResolution); + print(L"\r\n"); + + return fb; +} + +// --------------------------------------------------------------------------- +// Memory map +// +// UEFI hands out EFI_MEMORY_DESCRIPTORs; the kernel parses E820 entries. The +// translation happens here rather than in the kernel, so that +// kernel/arch/x86_64/e820.c stays the one place that understands a memory map. +// --------------------------------------------------------------------------- + +static UINT32 +efi_type_to_e820(UINT32 t) +{ + switch (t) { + // Boot services memory is ours the moment ExitBootServices returns, and + // the loader's own pages likewise -- the kernel is what runs next. + case EfiConventionalMemory: + case EfiBootServicesCode: + case EfiBootServicesData: + case EfiLoaderCode: + case EfiLoaderData: + return E820_USABLE; + case EfiACPIReclaimMemory: + return E820_ACPI; + case EfiACPIMemoryNVS: + return E820_NVS; + default: + // Runtime services, MMIO, unusable, persistent, reserved: all of it is + // memory the kernel must not allocate from. + return E820_RESERVED; + } +} + +// --------------------------------------------------------------------------- + +EFI_STATUS +EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) +{ + ST = systab; + BS = systab->BootServices; + + if (ST->ConOut) + ST->ConOut->ClearScreen(ST->ConOut); + print(L"SchOS UEFI loader\r\n"); + + EFI_FILE_PROTOCOL *file = open_kernel(image); + UINT64 entry = load_elf(file); + file->Close(file); + + print(L"schos: kernel entry "); print_hex(entry); print(L"\r\n"); + + UINT32 fb = setup_framebuffer(); + + // The memory map, and then out. GetMemoryMap must be the last thing before + // ExitBootServices: the key it returns is invalidated by any allocation, + // and AllocatePool for the map buffer is itself an allocation -- hence the + // static buffer and the retry. + static UINT8 mapbuf[32768]; + UINTN mapsize = sizeof mapbuf, mapkey = 0, descsize = 0; + UINT32 descver = 0; + EFI_STATUS st; + + st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf, + &mapkey, &descsize, &descver); + if (EFI_ERROR(st)) die(L"GetMemoryMap failed", st); + + // Translate before leaving, while the map is still readable and while + // printing still works, but write it to low memory only after the firmware + // has let go -- 0x30000 may well be firmware data until then. + static struct e820entry e820[E820_MAXENTRIES]; + UINT32 nent = 0; + for (UINTN off = 0; off + descsize <= mapsize && nent < E820_MAXENTRIES; + off += descsize) { + EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off); + UINT32 type = efi_type_to_e820(d->Type); + + // Coalesce adjacent runs of the same type. A UEFI map is routinely + // over a hundred descriptors and the kernel accepts 32; without this + // it would see a truncated map and believe the machine has less + // memory than it does. + if (nent > 0 && e820[nent-1].type == type && + e820[nent-1].base + e820[nent-1].length == d->PhysicalStart) { + e820[nent-1].length += d->NumberOfPages * 0x1000; + continue; + } + e820[nent].base = d->PhysicalStart; + e820[nent].length = d->NumberOfPages * 0x1000; + e820[nent].type = type; + e820[nent].acpi_ext = 1; + nent++; + } + + print(L"schos: exiting boot services\r\n"); + + st = BS->ExitBootServices(image, mapkey); + if (EFI_ERROR(st)) { + // The map changed underneath us between GetMemoryMap and here. Take it + // again and retry once; this is normal and the specification expects + // a loader to handle it. + mapsize = sizeof mapbuf; + st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf, + &mapkey, &descsize, &descver); + if (EFI_ERROR(st)) die(L"GetMemoryMap failed on retry", st); + st = BS->ExitBootServices(image, mapkey); + if (EFI_ERROR(st)) die(L"ExitBootServices failed", st); + } + + // ---- no firmware services exist past this line ---- + + __asm__ volatile("cli"); + + copy((void *)E820_MAP_ADDR, e820, nent * sizeof e820[0]); + *(UINT32 *)E820_COUNT_ADDR = nent; + + // The page tables mboot_entry would have built. entry64low's first act is + // a movabs to 0xffff8000...., so the high half must be mapped before the + // jump, not after: identity alone triple faults on the first instruction. + UINT64 *pml4 = (UINT64 *)PML4_ADDR; + UINT64 *pdpt = (UINT64 *)PDPT_ADDR; + zero(pml4, 0x1000); + zero(pdpt, 0x1000); + pml4[0] = PDPT_ADDR | PTE_P | PTE_W; // identity + pml4[256] = PDPT_ADDR | PTE_P | PTE_W; // 0xffff800000000000 + for (int i = 0; i < 4; i++) // 4 GB in 1 GB pages + pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W; + + // RBX carries the framebuffer address into kmain, and must never be 1 -- + // that value marks an application processor in entry64high. A physical + // MMIO address or zero, so it cannot collide. + __asm__ volatile( + "mov %0, %%cr3\n\t" + "mov %1, %%rbx\n\t" + "jmp *%2\n\t" + : + : "r"((UINT64)PML4_ADDR), "r"((UINT64)fb), "r"(entry) + : "memory"); + + for (;;) + __asm__ volatile("hlt"); +} diff --git a/boot/uefi/startup.nsh b/boot/uefi/startup.nsh new file mode 100644 index 0000000..24738f8 --- /dev/null +++ b/boot/uefi/startup.nsh @@ -0,0 +1,2 @@ +FS0: +EFI\BOOT\BOOTX64.EFI diff --git a/include/schboot.h b/include/schboot.h new file mode 100644 index 0000000..0f5dcf3 --- /dev/null +++ b/include/schboot.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#ifndef SCHBOOT_H +#define SCHBOOT_H + +// The loader/kernel hand-off, in one place. +// +// Included by kernel/arch/x86_64/entry.S and by boot/uefi/main.c, so the two +// cannot disagree about the magic or the layout. Defining it twice is exactly +// how a loader ends up scanning for bytes the kernel never wrote — the first +// attempt here searched for "SCH6" while the assembler had stored 0x53434836, +// which lands in the file as "6HCS". +// +// Keep this header assembler-safe: no typedefs, no structs outside the C +// guard, nothing entry.S cannot preprocess. + +// Scanned for in the first 16 KB of the kernel image, 8-byte aligned, exactly +// as the BIOS loader scans for the multiboot magic. Followed by a little-endian +// 64-bit physical address: the kernel's 64-bit entry point. +// +// The values spell "SCH6" "4BOO" when stored little-endian, which is what +// makes them findable by eye in a hex dump of the image. +#define SCH64_MAGIC0 0x36484353 /* "SCH6" */ +#define SCH64_MAGIC1 0x4f4f4234 /* "4BOO" */ + +#ifndef __ASSEMBLER__ + +struct sch64_header { + unsigned int magic0; // SCH64_MAGIC0 + unsigned int magic1; // SCH64_MAGIC1 + unsigned long long entry; // physical address, entered in long mode +}; + +#endif /* !__ASSEMBLER__ */ + +#endif /* SCHBOOT_H */ diff --git a/kernel/arch/x86_64/entry.S b/kernel/arch/x86_64/entry.S index 5ead585..9c4757b 100644 --- a/kernel/arch/x86_64/entry.S +++ b/kernel/arch/x86_64/entry.S @@ -27,7 +27,31 @@ #define mboot_magic 0x1badb002 #define mboot_flags 0x00010000 #include "mmu.h" +#include "schboot.h" +/* A second entry point, for firmware that has already put us in long mode. + * + * The multiboot header below is entered in 32-bit protected mode, and + * mboot_entry builds the page tables and makes the long-mode transition + * itself. UEFI firmware hands a loader long mode with paging already on, so + * that path would mean dropping back to protected mode only to climb out + * again -- a mode switch whose failure mode is a triple fault with no console. + * + * Instead the 64-bit entry is published where a loader can find it, by the + * same trick the BIOS loader already uses to find the multiboot header: scan + * the first 16 KB for a magic. boot/uefi/ looks for SCH64_MAGIC and jumps to + * the physical address recorded here. + * + * The contract at that address, which boot/uefi/main.c must satisfy: + * - long mode, interrupts off + * - CR3 pointing at page tables that identity-map the low 4 GB *and* map + * the same 4 GB at 0xffff800000000000 -- entry64low's first act is a + * movabs to a high address, so a loader that only identity-maps triple + * faults on the very first instruction + * - RBX = 0, marking the boot processor rather than an AP + * Those are exactly the conditions mboot_entry has established by the time it + * reaches entry64low, which is why this points there and not somewhere new. + */ .code64 .global mboot_header .global mboot_entry @@ -42,6 +66,18 @@ mboot_header: .long mboot_bss_end .long mboot_entry_addr +/* After the multiboot header, never before it: the header's own header_addr + field says it lives at mboot_load_addr, and the BIOS loader computes the + kernel's file offset from (header_addr - load_addr). Sixteen bytes inserted + ahead of it makes that arithmetic wrong and loads the kernel shifted, which + fails as a triple fault rather than as anything that names the cause. */ +.align 8 +.global sch64_header +sch64_header: + .long SCH64_MAGIC0 + .long SCH64_MAGIC1 + .quad (entry64uefi - mboot_header + mboot_load_addr) + .code32 mboot_entry: mov %eax, %ebp @@ -110,6 +146,29 @@ gdt64_end: .align 16 .code64 + +/* Entered by boot/uefi/ with the firmware's GDT still loaded. + * + * mboot_entry reaches entry64low by way of entry32mp, which does the lgdt on + * the way past. A loader that arrives already in long mode skips all of that, + * so it arrives running on the firmware's descriptors -- OVMF's CS is 0x38 -- + * and the first thing the kernel does that depends on its own selectors takes + * a #GP against a GDT the firmware no longer owns. It surfaced as + * "unexpected trap 13 ... cs=38", a long way from the cause. + * + * So load the kernel's GDT here and reload CS through it before going on. + * Doing it in the kernel rather than in the loader is deliberate: the GDT is + * the kernel's, and a loader that had to know its layout would have to be + * changed every time it moved. + */ +.global entry64uefi +entry64uefi: + lgdt (gdtr64 - mboot_header + mboot_load_addr) + movq $(entry64low - mboot_header + mboot_load_addr), %rax + pushq $8 /* the 64-bit code selector in gdt64 */ + pushq %rax + lretq /* far return: reloads CS */ + entry64low: movabs $entry64high, %rax jmp *%rax