SchOS/kernel/arch/x86_64/e820.c
Nexoniarz 0500e4382d boot: one hand-off contract, defined once
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>
2026-08-10 21:37:36 +02:00

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);
}