Two things, both about the debugger being available at the moment you
actually need it rather than once a shell exists.
**Ctrl-G on the console runs every view.** No filesystem, no /dev, no
shell -- it is the last thing main() sets up before entering the scheduler,
so it answers from early in boot, which is exactly when boot problems
happen. It sits beside the older Ctrl-P, and the difference between them is
the interesting part: procdump() takes no lock at all, deliberately,
because a lock is a way to hang while reporting a hang. The views cannot
follow that rule -- a free list read torn is worse than not read -- so they
take locks, and a lock must never be taken from an interrupt handler that
may have interrupted its holder. The key therefore *requests* and a kernel
thread *performs*. A scheduler too wedged to run a thread will not answer
Ctrl-G; Ctrl-P still will, and that division is the honest one.
**PSOGM says where the report went.** Its closing line used to be an
addr2line command, which the call chain no longer needs -- it prints names
now. What replaced it is the thing nobody would guess:
This report is in /proc/kmsg after a restart.
schdbg, or Ctrl-G on the console, asks more.
PSOGM -- Purple Screen of Guru Meditation
"Restart and read it" beats "photograph this screen" by a wide margin, and
until the log outlived the kernel it was not true. Both panic.c and
graphics.md had claimed in writing that the screen was the only artefact of
a crash; neither says so now. The QR code stays, because it is still the
only channel that works when the machine will not come back at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
315 lines
13 KiB
C
315 lines
13 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "vt.h"
|
|
#include "gfx.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "proc.h"
|
|
#include "x86.h"
|
|
#include "irq.h"
|
|
#include "dma.h"
|
|
#include "klog.h"
|
|
|
|
static void *klog_base; // the persistent log, just above the kernel image
|
|
|
|
static void startothers(void);
|
|
static void mpmain(void) __attribute__((noreturn));
|
|
extern char end[]; // first address after kernel loaded from ELF file
|
|
|
|
// Set by stage2main.c (boot/bios/stage2main.c, SCHBOOT_VERBOSE_FLAG) when the
|
|
// user picks "Boot SchOS (verbose)" from the F8 boot menu; read once here,
|
|
// before anything else, so subsystem init can log its own steps.
|
|
int boot_verbose;
|
|
|
|
// Must match SCHBOOT_VERBOSE_FLAG in boot/bios/stage2main.c exactly: just past
|
|
// the VBE info block, not 0x7000 (startothers() below uses that address for
|
|
// the AP boot trampoline).
|
|
#include "schboot.h"
|
|
|
|
// Bootstrap processor starts running C code here.
|
|
// Allocate a real stack and switch to it, first
|
|
// doing some setup required for memory allocator to work.
|
|
int
|
|
kmain(uint32 fb_addr)
|
|
{
|
|
boot_verbose = *(uchar*)P2V(SCHBOOT_VERBOSE_FLAG);
|
|
|
|
// On a verbose boot, put the kernel log on the screen from the first line.
|
|
//
|
|
// Everything below talks to the serial port and nothing else until the first
|
|
// exec calls console_takeover(). That is fine on a machine with a UART and
|
|
// useless on one without -- and a laptop, a Chromebox, anything built this
|
|
// century has no 16550 at 0x3f8. The screen showed the loader's progress bar
|
|
// and then nothing at all, for the whole of boot, on exactly the machines
|
|
// where something was going wrong.
|
|
//
|
|
// The loader has already put a framebuffer on the screen and described it in
|
|
// the boot block, so there is a perfectly good console sitting there unused.
|
|
// Attaching it here costs one struct and makes the difference between "it
|
|
// froze" and knowing which line it froze after.
|
|
//
|
|
// Unconditional, not just on a verbose boot. F8 is no use to someone whose
|
|
// machine resets before they can read anything, and "it froze after the
|
|
// loading bar" is a bug report nobody can act on. This is what Linux does
|
|
// too: the log is the default and `quiet splash` is the thing you ask for.
|
|
//
|
|
// The splash stays on screen until the kernel's first message, and a boot
|
|
// that succeeds clears the whole lot when the first program starts -- so
|
|
// the only time anyone sees this is the only time anyone needs it.
|
|
{
|
|
struct fb_info bi;
|
|
bi.ptr = (volatile uchar *)P2V((uint64)*(uint32*)P2V(SCHBOOT_VBE_LFB));
|
|
bi.width = *(uint16*)P2V(SCHBOOT_VBE_WIDTH);
|
|
bi.height = *(uint16*)P2V(SCHBOOT_VBE_HEIGHT);
|
|
bi.stride = *(uint16*)P2V(SCHBOOT_VBE_STRIDE);
|
|
bi.bpp = *(uchar *)P2V(SCHBOOT_VBE_BPP);
|
|
// Only if the loader actually described something. A zero here means the
|
|
// BIOS path or a firmware that gave no GOP, and drawing into address zero
|
|
// is a worse failure than staying quiet.
|
|
if (bi.ptr && bi.width && bi.height && bi.stride && bi.bpp >= 16) {
|
|
gfx_attach_scanout(&bi);
|
|
vt_init();
|
|
gfx_clear(0x0C0E13); /* SCH_BG; theme.h is not included here */
|
|
cons_serial_only = 0;
|
|
}
|
|
}
|
|
|
|
uartearlyinit();
|
|
if (boot_verbose) cprintf("verbose boot: uartearlyinit done\n");
|
|
// The DMA arena is carved off the top of physical memory *before* kalloc is
|
|
// given the rest, because a device needs physically contiguous memory and
|
|
// kalloc's free list makes no such promise -- two pages from it are adjacent
|
|
// only by chance. Taking it first is the only way to be sure the region
|
|
// exists; asking for it later would mean asking kalloc for something it
|
|
// fundamentally cannot provide. See include/dma.h.
|
|
// Before anything touches PHYSTOP, which is now a variable read from the
|
|
// firmware's memory map rather than a constant asserting 224 MB exists.
|
|
e820_init();
|
|
|
|
// The log region: the page after the kernel image, and the page allocator is
|
|
// started above it rather than at `end`.
|
|
//
|
|
// Low memory, deliberately, and it took a measurement to learn why. The
|
|
// obvious place is the top of RAM beside the DMA arena -- and on a reboot
|
|
// the magic came back as ASCII, because that is exactly where the firmware
|
|
// puts its own allocations before it hands the machine over. The bytes below
|
|
// the kernel image are the ones the loader has already committed to leaving
|
|
// alone, and `end` is the same address on every boot of the same build,
|
|
// which is the property a region has to have to be found again.
|
|
//
|
|
// No hole has to be punched in the allocator for this: kinit1's lower bound
|
|
// is a parameter, so moving it up by KLOG_BYTES is the whole of it.
|
|
klog_base = (void *)PGROUNDUP((addr_t)end);
|
|
klog_init(klog_base, KLOG_BYTES);
|
|
kinit1((char *)klog_base + KLOG_BYTES, P2V(PHYSTOP - DMA_ARENA_BYTES));
|
|
if (boot_verbose) cprintf("verbose boot: kinit1 done\n");
|
|
dma_init(P2V(PHYSTOP - DMA_ARENA_BYTES), P2V(PHYSTOP));
|
|
if (boot_verbose) cprintf("verbose boot: dma_init done\n");
|
|
kvmalloc();
|
|
if (boot_verbose) cprintf("verbose boot: kvmalloc done\n");
|
|
// ACPI first, the legacy MP table only if it finds nothing. Modern firmware
|
|
// publishes a complete MADT and an MP table with one processor in it, so
|
|
// asking the older interface first means never seeing more than one CPU.
|
|
// See kernel/arch/x86_64/acpi.c.
|
|
if (!acpi_init())
|
|
mpinit();
|
|
if (boot_verbose) cprintf("verbose boot: cpu enumeration done\n");
|
|
lapicinit();
|
|
if (boot_verbose) cprintf("verbose boot: lapicinit done\n");
|
|
tvinit();
|
|
// Once, on the boot processor. mpmain() runs on every CPU, and registering
|
|
// from there published the command as many times as there are processors.
|
|
watch_init();
|
|
if (boot_verbose) cprintf("verbose boot: tvinit done\n");
|
|
seginit();
|
|
if (boot_verbose) cprintf("verbose boot: seginit done\n");
|
|
ioapicinit();
|
|
if (boot_verbose) cprintf("verbose boot: ioapicinit done\n");
|
|
irq_init(); // must precede any driver init that registers a handler
|
|
if (boot_verbose) cprintf("verbose boot: irq_init done\n");
|
|
// Before the keyboard and mouse, which start producing events the moment
|
|
// their handlers are registered.
|
|
inputinit();
|
|
if (boot_verbose) cprintf("verbose boot: inputinit done\n");
|
|
// Before consoleinit, because the console writes to a terminal and there
|
|
// has to be one. vt_init reads the framebuffer geometry, so it also has to
|
|
// come after whatever attached a scanout.
|
|
vt_init();
|
|
if (boot_verbose) cprintf("verbose boot: vt_init done\n");
|
|
consoleinit();
|
|
if (boot_verbose) cprintf("verbose boot: consoleinit done\n");
|
|
uartinit();
|
|
if (boot_verbose) cprintf("verbose boot: uartinit done\n");
|
|
mouseinit();
|
|
if (boot_verbose) cprintf("verbose boot: mouseinit done\n");
|
|
// The devices that are simply known to be on a PC rather than enumerated by
|
|
// any bus, and the framebuffer the loader left running. Registered before
|
|
// pci_init() so that /proc/devices lists them first, which puts the machine's
|
|
// fixed furniture above whatever happened to be plugged into it -- and
|
|
// because the display has to work long before any bus is walked.
|
|
//
|
|
// main() names no display driver. Whichever one owns the boot framebuffer
|
|
// publishes it there as this kernel's scanout, and the console and crash
|
|
// screen draw on that; see include/gfx.h.
|
|
//
|
|
// No splash drawing here on purpose. The loader owns the splash, logo and
|
|
// all, and bringing the framebuffer up deliberately does not clear it -- so
|
|
// it stays on the display right through to console_takeover(). Painting it
|
|
// again from here would land in the last few hundred milliseconds of boot
|
|
// and change nothing.
|
|
platform_register_devices(fb_addr);
|
|
pciuser_init();
|
|
pci_init();
|
|
if (boot_verbose) cprintf("verbose boot: pci_init done\n");
|
|
pinit();
|
|
if (boot_verbose) cprintf("verbose boot: pinit done\n");
|
|
binit();
|
|
if (boot_verbose) cprintf("verbose boot: binit done\n");
|
|
fileinit();
|
|
if (boot_verbose) cprintf("verbose boot: fileinit done\n");
|
|
sockinit();
|
|
if (boot_verbose) cprintf("verbose boot: sockinit done\n");
|
|
// After the timer is running, so boot_epoch can subtract the uptime so far,
|
|
// and before userspace, so the very first timestamp any program takes is
|
|
// already real. Reading the RTC is slow, which is exactly why it happens once
|
|
// here rather than on every gettimeofday().
|
|
time_init();
|
|
ideinit();
|
|
if (boot_verbose) cprintf("verbose boot: ideinit done\n");
|
|
ramdiskinit();
|
|
if (boot_verbose) cprintf("verbose boot: ramdiskinit done\n");
|
|
// Start the application processors.
|
|
//
|
|
// This was broken from the beginning of the project and is fixed as of
|
|
// 2026-08-08. Five distinct bugs, each hidden behind the one before it, and
|
|
// all of them invisible under QEMU because it publishes only one processor in
|
|
// the legacy MP table -- see kernel/arch/x86_64/acpi.c. VMware published the
|
|
// real count, which is why the triple fault only ever appeared there.
|
|
//
|
|
// 1. entry64high had no AP branch at all; the check was a comment saying it
|
|
// had never been written. Every AP fell into the boot processor's path,
|
|
// took the BSP's stack, and ran kmain() a second time.
|
|
// 2. seginit() called kalloc() for its per-CPU page. kalloc locks, locking
|
|
// reads `cpu`, and `cpu` is what seginit() was about to set up.
|
|
// 3. kernel_fs_base was one global for all CPUs, and trapasm.S read it on
|
|
// every entry from user mode, so both CPUs shared one per-CPU block.
|
|
// 4. `cpu` and `proc` were __thread. That means per *thread*, so the compiler
|
|
// cached the thread pointer across calls -- but %fs here is per *CPU*, and
|
|
// a kernel thread changes CPU at every sched(). See include/proc.h.
|
|
// 5. The new per-CPU slots collided with syscall_entry's scratch word at
|
|
// %fs:(8), which is the same memory as %gs:8.
|
|
//
|
|
int smp_start_aps = 1;
|
|
if (smp_start_aps) {
|
|
startothers();
|
|
if (boot_verbose) cprintf("verbose boot: startothers done\n");
|
|
} else if (ncpu > 1) {
|
|
cprintf("smp: %d cpus found, running on 1 (AP startup is broken)\n", ncpu);
|
|
}
|
|
kinit2();
|
|
if (boot_verbose) cprintf("verbose boot: kinit2 done\n");
|
|
sinit();
|
|
if (boot_verbose) cprintf("verbose boot: sinit done\n");
|
|
// The TCP/IP stack, after sinit because it starts a kernel thread and
|
|
// kthread_create needs the process table to be usable. It returns
|
|
// immediately: everything it does happens in that thread, which cannot run
|
|
// until mpmain() below enters the scheduler.
|
|
netinit();
|
|
if (boot_verbose) cprintf("verbose boot: netinit done\n");
|
|
// The debugger's own thread, for the same reason netinit's comes after
|
|
// sinit: kthread_create needs a usable process table. From here on Ctrl-G
|
|
// on the console answers, with no filesystem mounted and nothing in
|
|
// userspace started -- which is the whole point of putting it this early.
|
|
dbg_thread_start();
|
|
if (boot_verbose) cprintf("verbose boot: dbg thread started\n");
|
|
mpmain();
|
|
}
|
|
|
|
// Other CPUs jump here from entryother.S.
|
|
void
|
|
mpenter(void)
|
|
{
|
|
switchkvm();
|
|
seginit();
|
|
lapicinit();
|
|
mpmain();
|
|
}
|
|
|
|
// Enable SSE/FXSAVE so user binaries can use XMM registers.
|
|
static void
|
|
enable_sse(void)
|
|
{
|
|
uint64 cr0, cr4;
|
|
asm volatile("mov %%cr0, %0" : "=r"(cr0));
|
|
cr0 &= ~(1UL << 2); // clear CR0.EM (no FP emulation)
|
|
cr0 |= (1UL << 1); // set CR0.MP (monitor coprocessor)
|
|
asm volatile("mov %0, %%cr0" :: "r"(cr0));
|
|
asm volatile("mov %%cr4, %0" : "=r"(cr4));
|
|
cr4 |= (1UL << 9); // CR4.OSFXSR — OS handles FXSAVE/FXRSTOR
|
|
cr4 |= (1UL << 10); // CR4.OSXMMEXCPT — OS handles SSE exceptions
|
|
asm volatile("mov %0, %%cr4" :: "r"(cr4));
|
|
}
|
|
|
|
// Common CPU setup code.
|
|
static void
|
|
mpmain(void)
|
|
{
|
|
cprintf("cpu%d: starting\n", cpunum());
|
|
enable_sse(); // allow user XMM/SSE instructions
|
|
idtinit(); // load idt register
|
|
syscallinit(); // syscall set up
|
|
xchg(&mycpu()->started, 1); // tell startothers() we're up
|
|
scheduler(); // start running processes
|
|
}
|
|
|
|
void entry32mp(void);
|
|
|
|
// Start the non-boot (AP) processors.
|
|
static void
|
|
startothers(void)
|
|
{
|
|
extern uchar _binary_entryother_start[], _binary_entryother_size[];
|
|
uchar *code;
|
|
struct cpu *c;
|
|
char *stack;
|
|
|
|
// Write entry code to unused memory at 0x7000.
|
|
// The linker has placed the image of entryother.S in
|
|
// _binary_entryother_start.
|
|
code = P2V(0x7000);
|
|
memmove(code, _binary_entryother_start,
|
|
(addr_t)_binary_entryother_size);
|
|
|
|
for(c = cpus; c < cpus+ncpu; c++){
|
|
if(c == cpus+cpunum()) // We've started already.
|
|
continue;
|
|
|
|
// Tell entryother.S what stack to use, where to enter, and what
|
|
// pgdir to use. We cannot use kpgdir yet, because the AP processor
|
|
// is running in low memory, so we use entrypgdir for the APs too.
|
|
// The AP's per-CPU page, allocated here rather than by the AP itself.
|
|
// seginit() explains why: an AP cannot call kalloc() before %fs is set up,
|
|
// because kalloc locks and locking reads the per-CPU pointer.
|
|
if (c->local == 0) {
|
|
c->local = kalloc();
|
|
memset(c->local, 0, PGSIZE);
|
|
}
|
|
|
|
stack = kalloc();
|
|
*(uint32*)(code-4) = 0x8000; // enough stack to get us to entry64mp
|
|
*(uint32*)(code-8) = v2p(entry32mp);
|
|
*(uint64*)(code-16) = (uint64) (stack + KSTACKSIZE);
|
|
|
|
lapicstartap(c->apicid, V2P(code));
|
|
|
|
// wait for cpu to finish mpmain()
|
|
while(c->started == 0)
|
|
;
|
|
|
|
// Counted here, on the boot processor, after the AP has actually reported
|
|
// in -- not by the AP itself. This is serialised by the loop, so it needs
|
|
// no atomic, and it cannot overcount a processor that started and died.
|
|
ncpu_online++;
|
|
}
|
|
}
|