docs/plan/ldls.md L3a -- the class bridge, and the first of the three pieces
L3 turned out to be.
/dev/netdev is /dev/pci with the arrows reversed. That hands a device to a
process; this lets a process be one. The kernel side is netdev_ops and nothing
else -- transmit, receive, mac, link_up, the same four entries e1000
publishes -- so a process claims the device, supplies a MAC, and from then on
read() gives it frames the stack wants sent and write() hands it frames that
arrived.
cmd/netecho proves it and is deliberately not a driver: no hardware, none
claimed, every frame answered by the program itself. DHCP so the interface
gets an address at all, ARP so the stack can find it, ICMP so it can be
pinged. All built by hand from RFCs 826, 791, 792 and 2131.
# netecho &
netuser: ldls0 is a process now, 2:4c:44:4c:53:1
netecho: I am 10.9.0.1 and will offer 10.9.0.2
netecho: answered DHCP, offering an address
# cat /proc/netif
interface ldls0 driver netuser address 10.9.0.2 gateway 10.9.0.1
# ping 10.9.0.1 3
netecho: answered an ARP
netecho: answered a ping
3 packets transmitted, 3 received, 0% packet loss
The interface got its address by DHCP from a process and then answered ping
from one. lwIP never learns that the thing behind netdev_ops is not a driver,
which is the whole property the bridge exists to have.
It dies the way it should, too. kill the process and link_up() answers false
immediately, so the stack refuses to send -- "ping: sendto: -5" -- rather than
swallowing packets into a driver that is not there. Start another and the
*same* interface comes back, keeping its address. That came free:
netuser_release_all() sits beside pciuser_release_all() in exit() for exactly
the same reason, and the reason is that a process holding a device is one that
may crash.
One thing the kernel needed: netinit() starts DHCP on the devices that exist
when it runs, so an interface created later -- which is how an LDLS-hosted
driver arrives -- would have come up with no address and no way to get one.
net_attach_late() fixes that, and lives in kernel/net/net.c because that is
the only file allowed to include lwIP's headers.
Recorded as not done, because it is not: this proves the bridge, not a driver.
Nothing in that test loads a module. That is why it was done first -- if ping
had not worked here the bridge would be wrong, and now a failure with a real
driver can only be the driver's side. L3b is the SchOS-side host, L3c is the
fifteen symbols for real, and L3 is the two joined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
316 lines
14 KiB
C
316 lines
14 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.
|
|
netuser_init();
|
|
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++;
|
|
}
|
|
}
|