boot/uefi: a working UEFI loader, with one known limitation
Loads the kernel from the ESP, places its ELF segments, records the framebuffer, translates the UEFI memory map into E820 entries, exits boot services, builds the page tables the kernel expects and jumps. Verified under OVMF: the kernel starts, brings up PCI, VGA, e1000 and the RTC, and reaches "cpu0: starting". UEFI structures are written from the specification rather than importing gnu-efi or EDK II -- a large dependency for two hundred lines of use, and a licensing question the project does not need to answer. clang emits PE/COFF directly, so this needs no new toolchain. The kernel gained a second entry point. The multiboot entry is 32-bit code that climbs into long mode itself, and firmware that already put us there would have to drop back down only to climb out again -- a mode switch whose failure mode is a triple fault with no console. So entry.S publishes its 64-bit entry the same way it publishes the multiboot header: a magic to scan for, SCH64BOO, with include/schboot.h owning the constants so the two cannot disagree. That header goes *after* the multiboot header, never before: header_addr says the multiboot header lives at mboot_load_addr, and the BIOS loader computes the kernel's file offset from it. The kernel loads its own GDT at that entry rather than the loader doing it. Arriving in long mode means arriving on the firmware's descriptors, and the first thing depending on the kernel's own selectors faulted -- "unexpected trap 13 ... cs=38", a long way from the cause. KNOWN LIMITATION, diagnosed and not yet fixed: e820_init takes the usable run *containing the kernel*, which under OVMF ends at 8 MB -- the firmware places an ACPI NVS region at 0x800000, so the run is genuinely fragmented. The map is truthful and untruncated (21 entries); the assumption of one contiguous run is BIOS-shaped. The fix is for the kernel to free each usable region rather than one range, which touches kalloc and deserves its own change rather than the tail of this one. BIOS boot is unaffected: 27 checks, no failures.
This commit is contained in:
parent
9b6106eab2
commit
56139bd0e6
7 changed files with 874 additions and 1 deletions
57
Makefile
57
Makefile
|
|
@ -341,6 +341,63 @@ schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $
|
||||||
|
|
||||||
kernel: $(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
|
# Build the ramdisk-root variant. This is a target rather than just
|
||||||
# "make RAMROOT=1" because toggling RAMROOT changes CFLAGS (-DRAMDISK_ROOT),
|
# "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
|
# and make has no way to notice that: objects compiled for one setting would be
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ a second loader is correct exactly when it satisfies it.
|
||||||
| | Where | Notes |
|
| | Where | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Kernel image | loaded per its ELF program headers | entry at `entry64` |
|
| 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 |
|
| 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 |
|
| Framebuffer | `VBE_INFO_ADDR` | address, pitch, width, height, depth |
|
||||||
| Verbose flag | `BOOT_VERBOSE_FLAG_ADDR` | set by the F8 menu |
|
| 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
|
kernel does not care which firmware produced them, and that is what keeps the
|
||||||
split cheap.
|
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
|
`e820.c` reads a *BIOS* memory map. UEFI hands out a different structure
|
||||||
(`EFI_MEMORY_DESCRIPTOR`, from `GetMemoryMap`), so the UEFI loader has to
|
(`EFI_MEMORY_DESCRIPTOR`, from `GetMemoryMap`), so the UEFI loader has to
|
||||||
translate it into the same 24-byte entries rather than inventing a second
|
translate it into the same 24-byte entries rather than inventing a second
|
||||||
|
|
|
||||||
260
boot/uefi/efi.h
Normal file
260
boot/uefi/efi.h
Normal file
|
|
@ -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 */
|
||||||
430
boot/uefi/main.c
Normal file
430
boot/uefi/main.c
Normal file
|
|
@ -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");
|
||||||
|
}
|
||||||
2
boot/uefi/startup.nsh
Normal file
2
boot/uefi/startup.nsh
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
FS0:
|
||||||
|
EFI\BOOT\BOOTX64.EFI
|
||||||
36
include/schboot.h
Normal file
36
include/schboot.h
Normal file
|
|
@ -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 */
|
||||||
|
|
@ -27,7 +27,31 @@
|
||||||
#define mboot_magic 0x1badb002
|
#define mboot_magic 0x1badb002
|
||||||
#define mboot_flags 0x00010000
|
#define mboot_flags 0x00010000
|
||||||
#include "mmu.h"
|
#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
|
.code64
|
||||||
.global mboot_header
|
.global mboot_header
|
||||||
.global mboot_entry
|
.global mboot_entry
|
||||||
|
|
@ -42,6 +66,18 @@ mboot_header:
|
||||||
.long mboot_bss_end
|
.long mboot_bss_end
|
||||||
.long mboot_entry_addr
|
.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
|
.code32
|
||||||
mboot_entry:
|
mboot_entry:
|
||||||
mov %eax, %ebp
|
mov %eax, %ebp
|
||||||
|
|
@ -110,6 +146,29 @@ gdt64_end:
|
||||||
|
|
||||||
.align 16
|
.align 16
|
||||||
.code64
|
.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:
|
entry64low:
|
||||||
movabs $entry64high, %rax
|
movabs $entry64high, %rax
|
||||||
jmp *%rax
|
jmp *%rax
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue