SchOS/kernel/arch/x86_64/e820.c
Nexoniarz eab13241a0 hwreport: describe a machine onto a USB stick, from a Linux-style /sys
hwreport writes one folder to the first read-write FAT mount, or mounts
the first USB stick that takes vfat itself and unmounts it afterwards:
summary.txt (machine, firmware, CPU, memory, every PCI and USB device
with its driver or "-- no driver"), dmesg.txt, lspci.txt in the shape of
lspci -nnk -xxx, lsusb.txt, dmi.txt, memmap.txt, the ACPI tables as raw
.dat files iasl -d reads, and copies of /proc. Serials, the UUID and the
MSDM/SLIC tables (a Windows licence key) are left out.

It reads /sys, which is new: sysfs at Linux's paths and in Linux's
formats -- /sys/bus/pci/devices with uevent, config and resource,
/sys/bus/usb/devices, /sys/firmware/acpi/tables, dmi/tables, memmap
and efi, /sys/class/dmi/id. toybox's lspci and lsusb work unmodified
against it. The USB core now keeps a table of attached devices for it.

The machine's name comes from SMBIOS (kernel/dev/dmi.c). The UEFI loader
hands over the entry point from the configuration table, the kernel
falls back to the 0xF0000 scan, and the log gets Linux's "DMI:" line.

Two bugs found on the way:

- The loader handed over at most 40 memory-map entries, in firmware
  order. A laptop's UEFI map is 60-150 descriptors with RAM above 4 GB
  listed last, so that is what was dropped. The entries now have their
  own area at 0x31000 with room for 256, are sorted before merging, and
  the kernel's 32 usable regions keep the largest when there are more.
- MemTotal and sysinfo's totalram were PHYSTOP, an address, which counts
  the PCI hole: a 6 GB machine reported 8. Now the usable RAM, summed.

usb.ids moves to /usr/share/hwdata, where lsusb looks for it, and
pci.ids joins it (`make pci-ids`, BSD-3-Clause like usb.ids).

Checked under QEMU q35: the report written to a FAT32 stick, fsck.fat
clean, DSDT.dat disassembled by iasl on the host, lspci -k and lsusb
showing names, 6 GB of RAM seen as 6137 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 12:54:11 +02:00

226 lines
8.2 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
#include "hwinfo.h"
// 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));
// The map exactly as handed over, for /sys/firmware/memmap. The low page it
// came in is not the kernel's to keep.
static struct e820_rec map_copy[E820_MAXENTRIES];
static int nmap_copy;
int
e820_nth(int i, struct e820_rec *out)
{
if (i < 0 || i >= nmap_copy)
return -1;
*out = map_copy[i];
return 0;
}
uint64
ram_total_bytes(void)
{
uint64 t = 0;
for (int i = 0; i < nmem_usable; i++)
t += mem_usable[i].end - mem_usable[i].base;
return t;
}
void
e820_init(void)
{
uint32 count = *(uint32 *)P2V(SCHBOOT_E820_COUNT);
struct e820entry *e = (struct e820entry *)P2V(SCHBOOT_E820_MAP);
if (count <= E820_MAXENTRIES)
for (uint32 i = 0; i < count; i++) {
map_copy[i].base = e[i].base;
map_copy[i].len = e[i].length;
map_copy[i].type = e[i].type;
nmap_copy = (int)i + 1;
}
// 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;
// Full: the smallest region gives way to a larger one. A map that
// fragments RAM into more than MEMREGION_MAX pieces loses its crumbs
// rather than whatever happens to be listed last, which is the RAM above
// 4 GB.
if (nmem_usable == MEMREGION_MAX) {
int s = 0;
for (int k = 1; k < nmem_usable; k++)
if (mem_usable[k].end - mem_usable[k].base <
mem_usable[s].end - mem_usable[s].base)
s = k;
if (mem_usable[s].end - mem_usable[s].base >= end - base)
continue;
mem_usable[s] = mem_usable[--nmem_usable];
}
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);
}