diff --git a/boot/uefi/main.c b/boot/uefi/main.c index b247709..acc5804 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -570,6 +570,9 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) } } +static UINT32 build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize, + struct e820entry *out); + /* Both defined below, next to the hand-off code they belong to. The system * information panel reads the same two facts the kernel will be handed, on * purpose: a panel with its own idea of what counts as usable memory, or its @@ -957,6 +960,41 @@ guid_eq(const EFI_GUID *a, const EFI_GUID *b) return 1; } +// Translate a UEFI memory map into E820 entries. +// +// Adjacent runs of the same type are coalesced: a real UEFI map is routinely +// over a hundred descriptors and the kernel accepts E820_MAXENTRIES, so +// without this it would see a truncated map and believe the machine has less +// memory than it does. Boot services memory becomes usable here, which is what +// makes the coalescing effective -- it is most of the fragmentation. +// +// A function rather than inline code because it has to run twice: once before +// ExitBootServices, and again if that fails and the map is re-read. +static UINT32 +build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize, + struct e820entry *out) +{ + 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); + + if (nent > 0 && out[nent-1].type == type && + out[nent-1].base + out[nent-1].length == d->PhysicalStart) { + out[nent-1].length += d->NumberOfPages * 0x1000; + continue; + } + out[nent].base = d->PhysicalStart; + out[nent].length = d->NumberOfPages * 0x1000; + out[nent].type = type; + out[nent].acpi_ext = 1; + nent++; + } + return nent; +} + // The ACPI RSDP, from the UEFI configuration table. // // The kernel's own search scans the EBDA and 0xE0000-0x100000. That is a BIOS @@ -1033,27 +1071,15 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) // 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); + UINT32 nent = build_e820(mapbuf, mapsize, descsize, e820); - // 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++; - } + // Truncation is silent data loss: phystop comes from the largest usable + // region, so dropping the tail of the map can leave the kernel believing a + // 16 GB machine has a fraction of that. Say so while there is still a + // console to say it on -- after ExitBootServices there is nowhere to + // report anything at all. + if (nent >= E820_MAXENTRIES) + print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n"); print(L"schos: exiting boot services\r\n"); @@ -1066,6 +1092,11 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf, &mapkey, &descsize, &descver); if (EFI_ERROR(st)) die(L"GetMemoryMap failed on retry", st); + // Translate again. The retry exists because the map *changed*, so + // handing the kernel the table built from the previous one describes + // memory the firmware has already rearranged. This path is normal on + // real firmware and never taken under OVMF, which is why it survived. + nent = build_e820(mapbuf, mapsize, descsize, e820); st = BS->ExitBootServices(image, mapkey); if (EFI_ERROR(st)) die(L"ExitBootServices failed", st); } diff --git a/include/schboot.h b/include/schboot.h index 744f166..ba0aa9b 100644 --- a/include/schboot.h +++ b/include/schboot.h @@ -71,7 +71,11 @@ // 0x508, which is what leaves 0x600 free below. #define SCHBOOT_E820_COUNT (SCHBOOT_BASE + 0x200) #define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x208) -#define SCHBOOT_E820_MAX 32 +/* 40, not 32. The map lives at SCHBOOT_E820_MAP (BASE+0x208) and the next + * field is SCHBOOT_RSDP_ADDR at BASE+0x600, so there are 1016 bytes and each + * entry is 24 -- 42 fit. Real UEFI firmware produces far more descriptors than + * QEMU's, and the headroom costs nothing that was being used. */ +#define SCHBOOT_E820_MAX 40 // Where the loader leaves the ACPI RSDP, as a little-endian 64-bit physical // address. Zero means "not provided, go and look for it yourself".