diff --git a/Makefile b/Makefile index 47b878a..e6455d7 100644 --- a/Makefile +++ b/Makefile @@ -425,6 +425,14 @@ CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -MP -ggdb CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT) CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA) +# The kernel keeps its hands off the FPU and SSE registers: they belong to +# whichever process is running, and are saved only when it is switched out +# (see "The FPU" in kernel/proc/proc.c). Without this GCC vectorises struct +# copies and memset into %xmm, and an interrupt then scribbles over the +# registers of the program it interrupted. Kernel objects only -- cmd/ is +# built with these CFLAGS too and is welcome to use SSE. +$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o: CFLAGS += -mgeneral-regs-only + # lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip). # Only the network files are compiled with these: putting them in the global # CFLAGS would let any kernel file include lwIP by accident, and the stack is diff --git a/include/proc.h b/include/proc.h index d929672..d92f57d 100644 --- a/include/proc.h +++ b/include/proc.h @@ -308,9 +308,18 @@ struct proc { uint64 slice_used; // how much of it has been spent, in nanoseconds uint64 nivcsw, nvcsw; // preempted / voluntary context switches + // The process's x87/SSE registers while it is off the CPU, as fxsave + // writes them; and a copy taken when a signal handler starts, put back by + // rt_sigreturn. See "The FPU" above scheduler() in kernel/proc/proc.c. + uint8 fpu[512] __attribute__((aligned(16))); + uint8 sigfpu[512] __attribute__((aligned(16))); }; #define FD_CLOEXEC 1 +// What a process's FPU starts as: x87 control word 0x37F, every exception +// masked, MXCSR 0x1F80 -- the state FNINIT and a reset leave, and what the +// System V ABI promises a new program. In proc.c. +extern const uint8 fpu_default[512]; // wait(2) status encoding, as understood by musl's WIFEXITED/WEXITSTATUS/ // WIFSIGNALED/WTERMSIG macros: diff --git a/include/x86.h b/include/x86.h index 1a9668f..bab66c1 100644 --- a/include/x86.h +++ b/include/x86.h @@ -25,6 +25,19 @@ rdtsc(void) return ((uint64)hi << 32) | lo; } +// The x87/MMX/SSE register file, to and from a 512-byte, 16-byte-aligned area. +static inline void +fxsave(void *area) +{ + asm volatile("fxsave64 (%0)" : : "r"(area) : "memory"); +} + +static inline void +fxrstor(const void *area) +{ + asm volatile("fxrstor64 (%0)" : : "r"(area) : "memory"); +} + static inline void insl(int port, void *addr, int cnt) { diff --git a/kernel/fs/exec.c b/kernel/fs/exec.c index a545692..5f7a0ab 100644 --- a/kernel/fs/exec.c +++ b/kernel/fs/exec.c @@ -389,6 +389,10 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) console_takeover(); switchuvm(myproc()); + // A new program starts with the FPU as the ABI promises it, not with the + // old one's registers. + memmove(myproc()->fpu, fpu_default, sizeof(myproc()->fpu)); + fxrstor(myproc()->fpu); freevm(oldpgdir); kfree((char*)scratch); kfree(bounce); diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index 16476a6..adc0471 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -319,6 +319,7 @@ found: // fork() then copies the parent's vruntime over this, which is what stops a // process from getting a fresh share of the CPU simply by forking. sched_init_proc(p, 0); + memmove(p->fpu, fpu_default, sizeof(p->fpu)); p->signal_mask = 0; p->in_signal = 0; @@ -434,6 +435,10 @@ fork(void) if((np = allocproc()) == 0) return -1; + // The parent's registers are live in the CPU -- the kernel never touches + // them -- so the child's copy is taken straight from there. + fxsave(np->fpu); + if((np->pgdir = copyuvm(myproc()->pgdir, myproc()->sz)) == 0){ np->kstack = 0; // the slot keeps its stack; see kstacks[] np->state = UNUSED; @@ -815,6 +820,29 @@ proc_wedge(const char *name) return found; } +// --- The FPU ------------------------------------------------------------ +// +// Every process has its own x87 and SSE registers, and they were never saved. +// musl's memcpy, the compiler's struct copies and anything doing arithmetic on +// doubles keep live values in %xmm across a context switch, so two processes +// sharing a CPU wrote over each other's: two shells counting in a loop on one +// processor took a NULL+8 fault within seconds, at a different rip each time. +// It had always been there, and every crash with no cause anyone could find +// was a candidate. +// +// The kernel is built with -mgeneral-regs-only, so it never touches these +// registers itself and they stay the user's from trap entry to return. That +// makes saving them a switch-time job and nothing more: sched() saves the +// process leaving the CPU, scheduler() restores the one arriving. No lazy +// switching (CR0.TS and a #NM handler), which would save the copy for a +// process that never uses SSE -- there is no such process; musl's string +// functions use it. +__attribute__((aligned(16))) +const uint8 fpu_default[512] = { + [0] = 0x7F, [1] = 0x03, // FCW 0x037F + [24] = 0x80, [25] = 0x1F, // MXCSR 0x1F80 +}; + // --- The scheduler ------------------------------------------------------ // // It was round-robin: walk ptable from end to end, run everything RUNNABLE in @@ -1179,6 +1207,7 @@ scheduler(void) proc_set(p); switchuvm(p); + fxrstor(p->fpu); p->state = RUNNING; swtch(&mycpu()->scheduler, p->context); switchkvm(); @@ -1211,6 +1240,7 @@ sched(void) // way out means the vruntime the scheduler then compares is already // up to date rather than a tick stale. sched_charge(myproc()); + fxsave(myproc()->fpu); swtch(&myproc()->context, mycpu()->scheduler); mycpu()->intena = intena; diff --git a/kernel/sys/sys_sig.c b/kernel/sys/sys_sig.c index 9745225..2687fe7 100644 --- a/kernel/sys/sys_sig.c +++ b/kernel/sys/sys_sig.c @@ -2,6 +2,7 @@ #include "defs.h" #include "param.h" #include "proc.h" +#include "x86.h" #include "syscall.h" #include "uaccess.h" #include "errno.h" @@ -131,6 +132,7 @@ sys_rt_sigreturn(void) { addr_t saved_rax = myproc()->saved_sigtf.rax; *myproc()->tf = myproc()->saved_sigtf; + fxrstor(myproc()->sigfpu); // what the interrupted code had myproc()->in_signal = 0; // If this handler interrupted a sigsuspend(), put its pre-suspend // signal mask back now that the handler has finished. @@ -226,6 +228,9 @@ deliver_signals(struct trapframe *tf) myproc()->pending_signals &= ~(1ULL << sig); myproc()->saved_sigtf = *tf; + // The handler is free to use %xmm; the code it interrupted had values + // there. Put back by rt_sigreturn. + fxsave(myproc()->sigfpu); myproc()->in_signal = 1; tf->rsp = sp;