SchOS/kernel/arch/x86_64/trap.c
Nexoniarz 54b25708c3 Demand paging, the RTL8168H, links that come up late, and Ctrl+Alt+F1..F12
- Anonymous memory can be reserved without being backed: large mappings,
  MAP_NORESERVE and PROT_NONE are recorded and each page appears, zeroed,
  on first touch -- from trap() for the program, from uva2ka() for the
  kernel on its behalf. WebKit reserves 4 GB at start and touches a sliver;
  backing it all failed with ENOMEM and Epiphany aborted.
- r8169: the 8168G generation and later (the laptop's 8168H, XID 541) are
  taken from the firmware's out-of-band mode, get their PLL powered, the
  receive gate opened, and their PHY reached through the OCP window. The
  PHY's power-down bit is cleared; the old code kept it while restarting
  autonegotiation. Untested on the hardware so far.
- The network thread follows each device's link into lwIP. The link was
  read once at attach, 0.02 s into boot; real autonegotiation takes
  seconds, so on hardware the interface stayed down for good.
- Twelve terminals, switched with Ctrl+Alt+F1..F12 by default: the keys X
  uses, so the console and the desktop agree. AltGr+F-key is gone from
  the defaults (chvt -c altgr+F brings it back).
- SO_PASSCRED on Unix sockets, with SCM_CREDENTIALS delivered by recvmsg.
  WebKit sets it on its IPC sockets and aborted when refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 11:27:48 +01:00

262 lines
10 KiB
C

#include "core/types.h"
#include "core/defs.h"
#include "core/irq.h"
#include "core/param.h"
#include "arch/memlayout.h"
#include "arch/mmu.h"
#include "core/proc.h"
#include "arch/x86.h"
#include "arch/traps.h"
#include "core/spinlock.h"
#include "uapi/posix.h"
uint *idt;
extern addr_t vectors[];
struct spinlock tickslock;
uint ticks;
static void
mkgate(uint *idt, uint n, addr_t kva, uint pl)
{
uint64 addr = (uint64) kva;
n *= 4;
idt[n+0] = (addr & 0xFFFF) | (KERNEL_CS << 16);
idt[n+1] = (addr & 0xFFFF0000) | 0x8E00 | ((pl & 3) << 13);
idt[n+2] = addr >> 32;
idt[n+3] = 0;
}
void idtinit(void)
{
struct {
ushort limit;
uint64 base;
} __attribute__((packed)) idtdesc;
idtdesc.limit = PGSIZE - 1;
idtdesc.base = (uint64)idt;
asm volatile("lidt %0" : : "m" (idtdesc));
}
void tvinit(void)
{
int n;
idt = (uint*) kalloc();
memset(idt, 0, PGSIZE);
for (n = 0; n < 256; n++)
mkgate(idt, n, vectors[n], 0);
mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3);
// NMI on its own stack, IST slot 1; see nmistack in ioperm.c.
idt[T_NMI * 4 + 1] |= 1;
}
uint
uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
// Which signal a CPU fault corresponds to, so a process killed by a fault is
// reported to wait() as died-by-signal with a sensible cause rather than a
// blanket SIGKILL.
static int
trapsig(uint64 trapno)
{
switch(trapno){
case T_DIVIDE: return SIGFPE;
case T_ILLOP: return SIGILL;
case T_GPFLT: return SIGSEGV;
case T_PGFLT: return SIGSEGV;
case T_BRKPT: return SIGTRAP;
case T_DEBUG: return SIGTRAP;
default: return SIGSEGV;
}
}
void
trap(struct trapframe *tf)
{
// Another processor has panicked and wants this one stopped: a processor
// left running carries on with a kernel that is already dead, and on a
// laptop that showed as the report flashing up and being drawn over, and
// as fans that stayed loud until somebody held the power button. First, and
// before the canary check, because this stack may be what is broken.
if(tf->trapno == T_NMI && panic_stop)
for(;;){
cli();
hlt();
}
kstack_check_canary("trap entry");
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
if(cpunum() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
// Charge the tick to whoever it interrupted. Every CPU does this, not just
// CPU 0 -- the global clock advances once, but the *work* happened on
// whichever processor was running that process.
//
// The split is by the interrupted privilege level, because that is the
// only thing the tick knows about what it interrupted. It is sampling, not
// measurement, which is what Linux does too and why these are ticks rather
// than microseconds.
//
// **stime is therefore almost always zero here, and that is structural.**
// MSR_SFMASK includes FL_IF (see syscallinit), so SYSCALL clears interrupts
// and nothing re-enables them for the duration -- a timer tick cannot land
// while a process is executing a syscall. The only ticks that ever reach
// the `else` are ones that arrive in a nested trap. Charging real system
// time needs TSC deltas closed and reopened around every sched(), which is
// scheduler accounting rather than sampling, and is not written.
if(myproc()){
if((tf->cs & 3) == DPL_USER) myproc()->utime++;
else myproc()->stime++;
}
// Debug registers are per-CPU, and there is no general IPI here, so this
// is how a watchpoint armed on one processor reaches the others: each
// notices the table's generation has moved and reloads. One comparison
// per tick when nothing has changed. See kernel/arch/x86_64/watch.c.
watch_sync();
lapiceoi();
break;
case T_DEBUG:
// A hardware watchpoint fired. It is a *trap*, not a fault -- the store
// has already happened -- so reporting and returning carries on with the
// program none the wiser, which is what lets this be used on a machine
// that is still running.
if(watch_trap(tf))
break;
// Not ours: a single-step, or a #DB from somewhere else. Falls through to
// the unexpected-trap path rather than being swallowed.
goto unexpected;
case T_IRQ0 + IRQ_SPURIOUS:
cprintf("cpu%d: spurious interrupt at %p:%p\n",
cpunum(), tf->cs, tf->rip);
lapiceoi();
break;
default:
// Every other IOAPIC pin goes through the registration table. The device
// interrupts used to be listed here one case at a time, which cannot work
// for anything found on the PCI bus: a driver does not know its line
// until it reads the device's config space at probe time, so there is no
// constant to write a case label for. See include/core/irq.h.
if(tf->trapno >= T_IRQ0 && tf->trapno < T_IRQ0 + NIRQ){
irq_dispatch(tf->trapno - T_IRQ0);
lapiceoi();
break;
}
unexpected:
// A user page fault on anonymous memory that is reserved but not backed
// yet: the page is made and the instruction runs again. See vm_fault().
if(tf->trapno == T_PGFLT && myproc() && (tf->cs&3) == DPL_USER &&
!(tf->err & 1) && vm_fault(rcr2()) == 0)
break;
if(myproc() == 0 || (tf->cs&3) == 0){
// A kernel-mode trap ends in panic() a dozen lines down, so the console
// can be put into its panic mode *now* -- and it has to be, because the
// fault may have happened inside the console itself.
//
// cprintf() takes cons.lock. If the faulting code was already holding
// it -- anything under console output, which on a verbose boot is most
// of the kernel -- then this report deadlocks on a lock its own call
// chain owns, acquire() panics with "acquire", and the trap that
// actually happened is never printed. That is a bad trade at the best
// of times and a worse one here: the whole point of the report is to
// name the fault.
cons_panic_mode();
// The rip is named where it can be. This is a kernel address, so
// ksym_lookup() has something to say about it -- and the whole reason
// docs/architecture/debugging.md puts D0 first is that "rip ffff8000001294c2"
// costs an objdump session and "rip in dma_alloc+0x8c" does not.
char where[72];
ksym_fmt(where, sizeof(where), tf->rip);
cprintf("unexpected trap %d from cpu %d rip %p <%s> (cr2=0x%p) err=0x%p\n",
tf->trapno, cpunum(), tf->rip, where, rcr2(), tf->err);
if (myproc())
cprintf("proc id: %d\n", myproc()->pid);
if (tf->trapno == T_GPFLT) {
uint64 *iretframe = (uint64*)tf->rsp;
cprintf("iretq frame @ %p: rip=%p cs=%p rflags=%p rsp=%p ss=%p\n",
iretframe, iretframe[0], iretframe[1], iretframe[2],
iretframe[3], iretframe[4]);
if (myproc())
cprintf("this tf=%p myproc()->tf=%p myproc()->kstack=%p (tf-kstack=%p)\n",
tf, myproc()->tf, myproc()->kstack, (uint64)tf - (uint64)myproc()->kstack);
}
panic("trap");
}
cprintf("pid %d %s: trap %d err %d on cpu %d "
"rip 0x%p addr 0x%p--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno, tf->err, cpunum(), tf->rip,
rcr2());
// General-purpose register dump. This used to be followed by ~25 lines
// that walked musl's mallocng group/meta structures out of rax/rdi and
// dumped the user stack -- scaffolding for the boot-crash investigation
// recorded in docs/history/kernel.md section 1. That
// investigation is closed and the underlying kernel bugs are fixed, so
// the allocator-specific part is gone; the registers stay, since they are
// useful for any fault.
// The full picture, for the two faults that mean something went wrong
// *elsewhere* rather than here: a general protection fault, and a jump to
// an address no code is ever linked at.
//
// The second case earns its own dump because of what it catches. A process
// executing at rip 0 did not branch there; it was handed a zero and
// followed it, and the registers say which one. The same dump on a
// *faulting address* below a page caught `addr 0xffffffffffffffea` in
// schinitd -- which is -22, which is -EINVAL: a call returned an error and
// the caller used it as a pointer. Neither of those is visible from the
// one-line report.
if (tf->trapno == T_GPFLT || tf->rip < PGSIZE || rcr2() + PGSIZE < PGSIZE * 2) {
cprintf(" rax=%p rbx=%p rcx=%p rdx=%p rsi=%p rdi=%p rbp=%p rsp=%p\n",
tf->rax, tf->rbx, tf->rcx, tf->rdx, tf->rsi, tf->rdi, tf->rbp, tf->rsp);
cprintf(" cs=%p ss=%p rflags=%p r11=%p tf=%p kstack=%p sz=%p\n",
tf->cs, tf->ss, tf->rflags, tf->r11, tf,
myproc()->kstack, myproc()->sz);
// The top of the user stack -- whatever a `ret` was about to use.
cprintf(" ustack:");
for (int i = -2; i < 4; i++) {
addr_t v;
if (fetchaddr(tf->rsp + i * 8, &v) == 0) cprintf(" %p", v);
else cprintf(" ????");
}
cprintf("\n");
}
myproc()->killed = 1;
if(myproc()->killsig == 0)
myproc()->killsig = trapsig(tf->trapno);
}
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
// The timer tick is when preemption is *considered*, not when it happens.
//
// This used to yield unconditionally, so a process alone on a CPU paid a
// full context switch -- switchuvm, a %cr3 reload, a flushed TLB -- a
// hundred times a second only to be handed straight back to itself.
// sched_slice_expired() asks whether it has actually had its share; see the
// scheduler in kernel/proc/proc.c.
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER
&& sched_slice_expired())
yield();
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
kstack_check_canary("trap exit");
}