Three things, and the first one is the freeze.
**The loader jumped out from under itself.** A machine with a lot of RAM
stopped at "Starting the kernel" with the splash still up: no panic, no reset,
nothing. The firmware's own exception handler had the answer the whole time,
once a machine was found that printed it:
X64 Exception Type - 0E(#PF - Page-Fault)
ExceptionData - 0x10 I:1 P:0
RIP - 0000000140004DEE <- the loader's own code, at 5.4 GB
RAX - 0000000000001000 <- the page tables it had just installed
RCX - 00000000001000F0 <- the kernel it was about to jump to
I:1 P:0 is an instruction fetch from a page that is not present. The loader
built page tables covering four gigabytes, loaded cr3 -- and the firmware had
placed the loader image *above* four gigabytes, so the next instruction fetch
was unmapped. It reproduces exactly on `-m 8G` and never on `-m 1G`, which is
why every test here passed.
The PDPT is one page whether four entries are filled or all 512, so all 512
are: 512 GB of identity map, replaced by kvmalloc() a few hundred instructions
later.
**And 3 GB was never the limit, it was a circle.** PHYSTOP_MAX was 0xC0000000
because kvmalloc() mapped exactly four 1 GB pages, and kvmalloc mapped four
because that was the cap -- each holding the other up, throwing away thirteen
gigabytes of a sixteen gigabyte machine. kvmalloc now maps as many gigabytes as
the machine has RAM and tells ioremap where the direct map stopped, instead of
ioremap assuming it stopped at four.
Freeing memory becomes two passes, and the ordering is forced rather than
chosen: kinit1() runs *before* kvmalloc because kvmalloc calls kalloc for its
own page tables, and freeing a page means writing into it -- so kinit1 can only
touch what the loader mapped. kinit_high() adds the rest afterwards.
`phystop` and the new `lowtop` are now two different numbers: the DMA arena
stays below 4 GB because a device is not obliged to reach the top of memory,
and an RTL8139's buffer address register is thirty-two bits wide.
**And the 0 MB.** The System Information screen reported "0 MB usable" on a
machine with sixteen gigabytes in it, because memory_mb() had its own 16 KB
guess at how big a memory map gets, and returned zero when GetMemoryMap said
EFI_BUFFER_TOO_SMALL. That is the same bug as the previous commit's, in a
second copy, and this one failed silently. There is one buffer and one
map_read() now.
Verified at 1 GB, 8 GB and 16 GB: all three boot, no traps, no panics, socktest
and memfdtest green in each. 8 GB and 16 GB did not boot at all before -- and
16 GB now reports 17408 MB with "mem: 13312 MB above 4 GB added to the
allocator", where the old kernel would have used three. Both display
configurations still clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
7 KiB
C
180 lines
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;
|
|
|
|
// The top of the low region. See memlayout.h: the DMA arena and everything
|
|
// kinit1() frees live below this, because that is all the loader's page tables
|
|
// map and all a 32-bit device can reach.
|
|
uint64 lowtop = 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;
|
|
phystop = PHYSTOP_FALLBACK;
|
|
lowtop = PHYSTOP_FALLBACK;
|
|
}
|
|
|
|
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 phys_top = 0, low_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);
|
|
|
|
// More than the direct map will ever cover. Clipped rather than refused:
|
|
// a machine with more RAM than this should use what it can.
|
|
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++;
|
|
|
|
// Two different questions, which used to be one and should not have been.
|
|
//
|
|
// `phystop` is how far RAM goes, and it is simply the highest end: kfree()
|
|
// uses it as a bound and kvmalloc() maps up to it.
|
|
if (end > phys_top)
|
|
phys_top = end;
|
|
|
|
// `lowtop` is where the DMA arena goes, and it has to be the largest
|
|
// region *below the boot direct map* -- contiguous, reachable by a 32-bit
|
|
// device, and mapped before kvmalloc has run. The largest such region is
|
|
// the only one certain to have room.
|
|
{
|
|
uint64 lo_end = end > DIRECTMAP_BOOT ? DIRECTMAP_BOOT : end;
|
|
if (base < lo_end && lo_end - base > largest) {
|
|
largest = lo_end - base;
|
|
low_top = lo_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;
|
|
}
|
|
|
|
// 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 (low_top < PHYSTOP_MIN) {
|
|
cprintf("e820: only %d MB usable below 4 GB; SchOS needs at least %d MB "
|
|
"there\n",
|
|
(int)(low_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.
|
|
//
|
|
// The kernel log is *not* up here -- it sits just above the kernel image,
|
|
// where the firmware does not allocate. See klog_init()'s caller.
|
|
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 = phys_top;
|
|
lowtop = low_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;
|
|
|
|
kinfo("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);
|
|
}
|