Groundwork for the unified loader: before the two paths can share code, they have to agree on what they are producing, and until now they only agreed by coincidence. Every field of the loader/kernel hand-off was #defined in whichever file happened to need it -- kernel/core/main.c, kernel/dev/vga.c, kernel/arch/x86_64/e820.c, boot/bios/cdboot.S, boot/bios/stage2main.c and boot/uefi/main.c, six copies of the same addresses. schboot.h existed for exactly this and held only the entry magic and the RSDP slot, with a comment describing the rest of the block it did not define. The whole block lives there now: the VBE info fields, the verbose and media flags (with the real-mode segment:offset spelling cdboot.S needs), the e820 count, map and cap, and the RSDP. Every one of those files includes it instead of restating it. This is not a tidy-up. It is the same hazard as defining the entry magic twice, only quieter -- nothing breaks until one copy moves, and the two loaders diverging is precisely what produced the EFER.SCE and RSDP bugs, where a fix on one path silently did not exist on the other. Verified on both firmwares: BIOS boots to schinit with the same e820, ACPI and VGA lines as before; UEFI reaches cpu0 with 1018 MB in 7 regions and its own 1280x800 mode. `make iso` still builds. bootcheck 108 [ok]. Next: the loaders become starters that gather firmware-specific facts and hand to one common stage. That needs the BIOS side to climb to long mode first, since common code can only be compiled for one CPU mode -- which is the risky part, and is why the contract went first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
6.7 KiB
C
167 lines
6.7 KiB
C
// Find out how much memory this machine actually has.
|
|
//
|
|
// PHYSTOP used to be a constant: 0xE000000, 224 MB, inherited from xv6 and
|
|
// never revisited. That is not a limit on how much memory the kernel *uses* --
|
|
// it is an assertion that exactly that much exists. Boot a machine with less
|
|
// and kinit1() walks off the end of RAM into nothing; boot one with more and
|
|
// every byte above 224 MB is invisible. Measured before this file existed:
|
|
// SchOS booted at 224 MB and triple faulted at 192 MB, for no reason at all.
|
|
//
|
|
// The firmware has known the answer the whole time. boot/bios/stage2.S asks it with
|
|
// INT 0x15 / E820 in real mode -- the same interface Linux's boot stub and GRUB
|
|
// use -- and leaves the result in low memory. stage2main.c already parses it to
|
|
// print detected RAM on the splash. The kernel simply never read it.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "dma.h" // DMA_ARENA_BYTES: the top of PHYSTOP must be able to hold it
|
|
|
|
// Set before kinit1() and never again. Starts at the old constant so that a
|
|
// crash between entry and e820_init() behaves the way it always did rather
|
|
// than in some new way.
|
|
uint64 phystop = PHYSTOP_FALLBACK;
|
|
|
|
#include "schboot.h" /* the hand-off block */
|
|
|
|
#define E820_USABLE 1
|
|
#define E820_MAXENTRIES SCHBOOT_E820_MAX
|
|
|
|
// Every usable region, page aligned inwards and clipped to PHYSTOP_MAX.
|
|
// kinit1() frees these; see include/memlayout.h for why a list and not just a
|
|
// top address.
|
|
struct memregion mem_usable[MEMREGION_MAX];
|
|
int nmem_usable;
|
|
|
|
// The fallback path must still hand kalloc something, or the kernel comes up
|
|
// with an empty free list and dies somewhere far less obvious than here. One
|
|
// region spanning what the old constant asserted reproduces the previous
|
|
// behaviour exactly.
|
|
static void
|
|
assume_fallback(void)
|
|
{
|
|
mem_usable[0].base = EXTMEM;
|
|
mem_usable[0].end = PHYSTOP_FALLBACK;
|
|
nmem_usable = 1;
|
|
}
|
|
|
|
struct e820entry {
|
|
uint64 base;
|
|
uint64 length;
|
|
uint32 type;
|
|
uint32 acpi_ext;
|
|
} __attribute__((packed));
|
|
|
|
void
|
|
e820_init(void)
|
|
{
|
|
uint32 count = *(uint32 *)P2V(SCHBOOT_E820_COUNT);
|
|
struct e820entry *e = (struct e820entry *)P2V(SCHBOOT_E820_MAP);
|
|
|
|
// A count of zero means the firmware declined; anything absurd means we are
|
|
// reading something that is not a memory map. Either way the constant is the
|
|
// only honest answer left.
|
|
if (count == 0 || count > E820_MAXENTRIES) {
|
|
cprintf("e820: no usable map (count %d), assuming %d MB\n",
|
|
count, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
// Collect every usable region rather than just the one containing the
|
|
// kernel.
|
|
//
|
|
// A memory map is not one run of RAM: there are reserved holes in it -- ACPI
|
|
// tables, firmware scratch, the legacy hole below 1 MB. The old code took the
|
|
// run containing the kernel and stopped, because kalloc walked a single range
|
|
// and taking the global maximum would have stepped over a reserved region and
|
|
// put firmware tables on the free list. That was the right answer for the
|
|
// wrong shape: under OVMF the kernel's run ends at the ACPI NVS region at
|
|
// 8 MB, and every byte above it was lost. kalloc now frees region by region,
|
|
// so the holes are skipped instead of being either freed or fatal.
|
|
uint64 largest = 0;
|
|
uint64 top = 0;
|
|
nmem_usable = 0;
|
|
for (uint32 i = 0; i < count; i++) {
|
|
if (e[i].type != E820_USABLE)
|
|
continue;
|
|
|
|
// Align inwards: a region is only usable where a whole page fits.
|
|
uint64 base = (e[i].base + PGSIZE - 1) & ~(uint64)(PGSIZE - 1);
|
|
uint64 end = (e[i].base + e[i].length) & ~(uint64)(PGSIZE - 1);
|
|
|
|
// Above PHYSTOP_MAX the direct map is device memory, not RAM (see below).
|
|
if (end > PHYSTOP_MAX)
|
|
end = PHYSTOP_MAX;
|
|
if (base >= end)
|
|
continue;
|
|
|
|
mem_usable[nmem_usable].base = base;
|
|
mem_usable[nmem_usable].end = end;
|
|
nmem_usable++;
|
|
|
|
// phystop comes from the largest region, not the highest. It has to be a
|
|
// region that can hold the DMA arena, which is carved off the top of
|
|
// PHYSTOP and must be physically contiguous; the largest is the only one
|
|
// that is certain to have room. A small usable region above it is left
|
|
// unfreed rather than raising the bound past a hole -- kfree() rejects
|
|
// anything at or above PHYSTOP, and that check is worth more than the
|
|
// stray page it costs.
|
|
if (end - base > largest) {
|
|
largest = end - base;
|
|
top = end;
|
|
}
|
|
}
|
|
|
|
if (nmem_usable == 0) {
|
|
cprintf("e820: no usable regions in %d entries, assuming %d MB\n",
|
|
count, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
// The PHYSTOP_MAX ceiling is applied per region above, when each one is
|
|
// clipped. kvmalloc() maps physical 0-4 GB with four 1 GB pages and marks the
|
|
// top one uncached because that is where PCI BARs and the framebuffer live,
|
|
// so 3 GB is the ceiling for anything the kernel will treat as RAM: above it
|
|
// the direct map either does not exist or is device memory that would be
|
|
// catastrophically slow to allocate from and is not ours to hand out.
|
|
|
|
// Below this the kernel image, its page tables and the DMA arena do not fit,
|
|
// and continuing produces a failure much harder to read than this line.
|
|
if (top < PHYSTOP_MIN) {
|
|
cprintf("e820: only %d MB usable; SchOS needs at least %d MB\n",
|
|
(int)(top / (1024 * 1024)), (int)(PHYSTOP_MIN / (1024 * 1024)));
|
|
// Left at the fallback deliberately rather than lowered: a machine this
|
|
// small is going to fail, and it should fail having said why.
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
// dma_init() carves [PHYSTOP - DMA_ARENA_BYTES, PHYSTOP) and needs it
|
|
// physically contiguous, so the region that set `top` must be able to hold
|
|
// it. Picking the largest region makes this true on any machine worth
|
|
// booting; checking says so out loud if it ever is not.
|
|
if (largest < DMA_ARENA_BYTES) {
|
|
cprintf("e820: largest usable region is %d MB, too small for the %d MB "
|
|
"DMA arena\n", (int)(largest / (1024 * 1024)),
|
|
(int)(DMA_ARENA_BYTES / (1024 * 1024)));
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
phystop = top;
|
|
|
|
// Report what is actually usable, not phystop: with a fragmented map those
|
|
// are different numbers, and printing the top address as though it were the
|
|
// amount of RAM is how the 8 MB under OVMF went unnoticed for so long.
|
|
uint64 usable = 0;
|
|
for (int i = 0; i < nmem_usable; i++)
|
|
usable += mem_usable[i].end - mem_usable[i].base;
|
|
|
|
cprintf("e820: %d MB usable in %d region%s, top %d MB (%d entries)\n",
|
|
(int)(usable / (1024 * 1024)), nmem_usable,
|
|
nmem_usable == 1 ? "" : "s",
|
|
(int)(phystop / (1024 * 1024)), count);
|
|
}
|