rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 11:00:20 -0700 |
|---|---|---|
| commit | 7ad1388c3b1af493be4824bf58f1cf29159bd94e | |
| parent | a64c7daa9bbdf2bc171da89655822defeb6176b8 | |
| tree | browse files at this commit | |
WIP 15.1: per-thread FPU/SSE/AVX state (XSAVE/FXSAVE), signal fpstate, #MF/#XM, fputest
| M | Cargo.toml | +5 -0 |
| M | build.rs | +1 -0 |
| A | src/fpu.rs | +208 -0 |
| M | src/interrupts.rs | +19 -0 |
| M | src/lib.rs | +2 -0 |
| M | src/process.rs | +16 -6 |
| M | src/signal.rs | +104 -10 |
| M | src/smp.rs | +1 -0 |
| M | src/thread.rs | +8 -0 |
| M | src/trap.rs | +4 -0 |
| A | tests/musl_prereq.rs | +74 -0 |
| A | user/fputest.rs | +457 -0 |
12 files changed, 899 insertions, 16 deletions
@@ -86,6 +86,10 @@ harness = false run-args = [ # Four CPU cores (smp.rs starts the other three). "-smp", "4",+ # The most capable CPU QEMU can emulate: it has XSAVE and AVX (the+ # default "qemu64" model has neither, only FXSAVE + SSE2). fpu.rs+ # handles both.+ "-cpu", "max", # Second disk: the FAT32 image build.rs makes (target/disk.img), as a # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay, # so the image is the same at every boot (and tests can't corrupt it).@@ -123,6 +127,7 @@ run-args = [ # Used by `cargo test`: headless, results on serial, exit code 33 = pass. test-args = [ "-smp", "4", # four CPU cores (tests that call init_smp use them all)+ "-cpu", "max", # XSAVE + AVX (fpu.rs; the default qemu64 has only FXSAVE) # Second disk: the FAT32 image build.rs makes (target/disk.img), as a # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay, # so the image is the same at every boot (and tests can't corrupt it).
@@ -77,6 +77,7 @@ const PROGRAMS: &[&str] = &[ "random", "getty", "tile", // stage 13: virtual drivers "forktest", "cowtest", "mmaptest", "sigtest", // stage 14: processes "threads", "echoserver", "ptytest", "nbtest", // stage 15: libc plumbing+ "fputest", // stage 15.1: musl prerequisites ]; const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -0,0 +1,208 @@+//! The FPU, SSE and AVX registers of user programs (stage 15.1).+//!+//! Besides the 16 general-purpose registers, an x86_64 CPU has a second set+//! for floating point and vector maths: the old x87 FPU stack, the 16 SSE+//! registers xmm0-xmm15 (128 bits each) plus their control register MXCSR,+//! and, on CPUs with AVX, the upper halves that make them 256-bit ymm0-ymm15.+//! Every C compiler uses xmm registers for `double` arithmetic and for fast+//! `memcpy`, so to the kernel they are as much "the program's registers" as+//! rax is: if two threads share a CPU, each must find its own values there+//! when it runs again.+//!+//! The kernel itself is built *soft-float* (see x86_64-rust_kernel.json): it+//! never touches these registers. That makes the job simple: whatever is in+//! them while the kernel runs belongs to the user thread the CPU last ran.+//! So we only+//!+//! * save them into the thread's `FpuState` when the scheduler switches+//! away from a user thread, and load the next user thread's (thread.rs,+//! "eager" switching -- simple, and cheap enough at ~1 KiB);+//! * copy them into a fork's child / a new thread (they start with the+//! parent's values, like Linux);+//! * reset them on execve (a new program starts clean);+//! * put them in signal frames, so a handler may use floats and the+//! interrupted code gets its own values back (signal.rs).+//!+//! The CPU has instructions that save/load all of it at once:+//!+//! * FXSAVE / FXRSTOR (every x86_64 CPU): x87 + SSE, a 512-byte area.+//! * XSAVE / XRSTOR (newer CPUs): a 512-byte legacy part in the same+//! format, a 64-byte header (which parts are saved), then one part per+//! extra feature -- AVX's upper ymm halves are 256 bytes more. Which+//! features are switched on is the OS's choice, in register XCR0.+//!+//! Before user code may use any of this, the OS must say it will save the+//! state (otherwise the CPU raises "invalid opcode"): control register bits+//! CR4.OSFXSR (SSE allowed), CR4.OSXMMEXCPT (SSE maths errors raise #XM, not+//! #UD) and CR4.OSXSAVE (XSAVE + XCR0 allowed), on every CPU.++use core::arch::asm;+use core::arch::x86_64::{__cpuid, __cpuid_count};+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};++/// Room for the biggest state we enable: x87 + SSE + AVX = 832 bytes with+/// XSAVE (we don't switch on AVX-512, which would need ~2.7 KiB).+pub const AREA_SIZE: usize = 1024;++/// One thread's saved FPU/SSE/AVX registers, in the FXSAVE/XSAVE memory+/// format. XSAVE needs 64-byte alignment.+#[repr(C, align(64))]+#[derive(Clone)]+pub struct FpuState(pub [u8; AREA_SIZE]);++/// XSAVE is used (else FXSAVE).+static USE_XSAVE: AtomicBool = AtomicBool::new(false);+/// The features in XCR0 (bit 0 x87, bit 1 SSE, bit 2 AVX).+static XCR0: AtomicU64 = AtomicU64::new(0);+/// Bytes of state: 512 (FXSAVE) or what CPUID says for our XCR0.+static SIZE: AtomicU32 = AtomicU32::new(512);+/// MXCSR bits the CPU supports; loading any other bit faults (#GP).+static MXCSR_MASK: AtomicU32 = AtomicU32::new(0xffbf);++/// MXCSR at reset: all six SSE exceptions masked, round to nearest.+pub const MXCSR_DEFAULT: u32 = 0x1f80;+/// x87 control word at reset (FNINIT): all exceptions masked, 64-bit precision.+const FCW_DEFAULT: u16 = 0x037f;++pub const XFEATURE_X87: u64 = 1;+pub const XFEATURE_SSE: u64 = 2;+pub const XFEATURE_AVX: u64 = 4;++/// Switch the FPU/SSE (and AVX) on for this CPU. Every CPU calls it once at+/// boot: CR0/CR4/XCR0 are per CPU.+pub fn init_cpu() {+ // CPUID leaf 1: ECX bit 26 = XSAVE, bit 28 = AVX.+ let leaf1 = __cpuid(1);+ let has_xsave = leaf1.ecx & (1 << 26) != 0;+ let has_avx = leaf1.ecx & (1 << 28) != 0;+ unsafe {+ // CR0: EM (bit 2, "no FPU: trap every FPU instruction") and TS (bit+ // 3, "trap the next one", for lazy switching) off; MP (bit 1) and NE+ // (bit 5, report x87 errors as exception #MF, not via the old PC IRQ 13) on.+ let mut cr0: u64;+ asm!("mov {}, cr0", out(reg) cr0);+ cr0 = (cr0 & !(1 << 2 | 1 << 3)) | 1 << 1 | 1 << 5;+ asm!("mov cr0, {}", in(reg) cr0);+ // CR4: OSFXSR (bit 9) + OSXMMEXCPT (bit 10), and OSXSAVE (bit 18).+ let mut cr4: u64;+ asm!("mov {}, cr4", out(reg) cr4);+ cr4 |= 1 << 9 | 1 << 10;+ if has_xsave {+ cr4 |= 1 << 18;+ }+ asm!("mov cr4, {}", in(reg) cr4);+ }+ let xcr0 = XFEATURE_X87 | XFEATURE_SSE | if has_avx && has_xsave { XFEATURE_AVX } else { 0 };+ if has_xsave {+ // XSETBV ecx=0: write XCR0 (edx:eax).+ unsafe { asm!("xsetbv", in("ecx") 0, in("eax") xcr0 as u32, in("edx") (xcr0 >> 32) as u32) };+ // CPUID leaf 0xD, sub-leaf 0: EBX = bytes XSAVE needs for the+ // features currently on in XCR0.+ let size = __cpuid_count(0xd, 0).ebx;+ assert!(size as usize <= AREA_SIZE, "XSAVE area too big: {}", size);+ SIZE.store(size, Ordering::Relaxed);+ XCR0.store(xcr0, Ordering::Relaxed);+ USE_XSAVE.store(true, Ordering::Relaxed);+ } else {+ XCR0.store(XFEATURE_X87 | XFEATURE_SSE, Ordering::Relaxed);+ }+ // Clean registers, and find out which MXCSR bits are allowed: FXSAVE+ // stores that mask at byte 28 (0 means the old default, 0xffbf).+ unsafe { asm!("fninit") };+ let mut probe = FpuState([0; AREA_SIZE]);+ unsafe { asm!("fxsave64 [{}]", in(reg) probe.0.as_mut_ptr()) };+ let mask = u32::from_le_bytes(probe.0[28..32].try_into().unwrap());+ MXCSR_MASK.store(if mask == 0 { 0xffbf } else { mask }, Ordering::Relaxed);+ restore(&initial());+}++pub fn uses_xsave() -> bool {+ USE_XSAVE.load(Ordering::Relaxed)+}+pub fn xfeatures() -> u64 {+ XCR0.load(Ordering::Relaxed)+}+/// Bytes of saved state (512 with FXSAVE).+pub fn size() -> usize {+ SIZE.load(Ordering::Relaxed) as usize+}++/// The state a new program starts with: everything zero, the x87 control+/// word and MXCSR at their reset values. The XSAVE header (bytes 512..576)+/// is all zero, which tells XRSTOR "every extra feature is in its initial+/// state" (zero).+pub fn initial() -> FpuState {+ let mut s = FpuState([0; AREA_SIZE]);+ s.0[0..2].copy_from_slice(&FCW_DEFAULT.to_le_bytes());+ s.0[24..28].copy_from_slice(&MXCSR_DEFAULT.to_le_bytes());+ if uses_xsave() {+ // ... except x87 + SSE, which we say are valid (the values above).+ s.0[512..520].copy_from_slice(&(XFEATURE_X87 | XFEATURE_SSE).to_le_bytes());+ }+ s+}++/// Save this CPU's registers into `s`.+pub fn save(s: &mut FpuState) {+ let p = s.0.as_mut_ptr();+ unsafe {+ if uses_xsave() {+ let m = xfeatures();+ asm!("xsave64 [{}]", in(reg) p, in("eax") m as u32, in("edx") (m >> 32) as u32);+ } else {+ asm!("fxsave64 [{}]", in(reg) p);+ }+ }+}++/// The registers right now (for fork, clone and signal frames).+pub fn save_live() -> FpuState {+ let mut s = FpuState([0; AREA_SIZE]);+ save(&mut s);+ s+}++/// Load `s` into this CPU's registers. `s` must be valid (see `sanitize`):+/// a bad MXCSR or XSAVE header would fault in the kernel.+pub fn restore(s: &FpuState) {+ let p = s.0.as_ptr();+ unsafe {+ if uses_xsave() {+ let m = xfeatures();+ asm!("xrstor64 [{}]", in(reg) p, in("eax") m as u32, in("edx") (m >> 32) as u32);+ } else {+ asm!("fxrstor64 [{}]", in(reg) p);+ }+ }+}++/// execve and signal handlers start from clean registers.+pub fn reset_live() {+ restore(&initial());+}++/// Make state that came from user memory (a signal frame the program may+/// have edited) safe to load: false if MXCSR has bits the CPU doesn't+/// support (XRSTOR would fault). Fixes the XSAVE header: only features we+/// enabled, standard (not compacted) format, reserved bytes zero.+pub fn sanitize(s: &mut FpuState) -> bool {+ let mxcsr = u32::from_le_bytes(s.0[24..28].try_into().unwrap());+ if mxcsr & !MXCSR_MASK.load(Ordering::Relaxed) != 0 {+ return false;+ }+ if uses_xsave() {+ let bv = u64::from_le_bytes(s.0[512..520].try_into().unwrap()) & xfeatures();+ s.0[512..520].copy_from_slice(&bv.to_le_bytes());+ s.0[520..576].fill(0); // XCOMP_BV + reserved+ }+ true+}++/// The SSE status flags of the last SIMD exception (#XM), for SIGFPE's+/// si_code: MXCSR bits 0-5 = invalid, denormal, divide by zero, overflow,+/// underflow, precision.+pub fn mxcsr() -> u32 {+ let mut v: u32 = 0;+ unsafe { asm!("stmxcsr [{}]", in(reg) &mut v as *mut u32) };+ v+}
@@ -73,6 +73,8 @@ lazy_static! { idt.invalid_opcode.set_handler_addr(addr(trap::trap_invalid_opcode)); idt.general_protection_fault.set_handler_addr(addr(trap::trap_general_protection)); idt.page_fault.set_handler_addr(addr(trap::trap_page_fault));+ idt.x87_floating_point.set_handler_addr(addr(trap::trap_x87_error));+ idt.simd_floating_point.set_handler_addr(addr(trap::trap_simd_error)); idt[InterruptIndex::ApicTimer.as_u8()].set_handler_addr(addr(trap::trap_apic_timer)); // Run the double-fault handler on its own known-good stack (IST).@@ -127,11 +129,28 @@ pub extern "C" fn trap_handler(frame: &mut TrapFrame) { 0 => exception(frame, signal::SIGFPE, 1 /* FPE_INTDIV */, frame.rip, "divide error"), 6 => exception(frame, signal::SIGILL, 1 /* ILL_ILLOPC */, frame.rip, "invalid opcode"), 13 => exception(frame, signal::SIGSEGV, signal::SI_KERNEL, 0, "general protection fault"),+ 16 => exception(frame, signal::SIGFPE, 0, frame.rip, "x87 floating-point error"),+ 19 => exception(frame, signal::SIGFPE, simd_fpe_code(), frame.rip, "SIMD floating-point error"), v => panic!("unexpected trap {}", v), } signal::deliver(frame); } +/// SIGFPE's si_code for an SSE exception (#XM, only raised if the program+/// unmasked it in MXCSR): which one, from MXCSR's status flags.+fn simd_fpe_code() -> i32 {+ let m = crate::fpu::mxcsr();+ let unmasked = m & !(m >> 7) & 0x3f; // flags whose mask bit (7-12) is clear+ match unmasked {+ f if f & 1 != 0 => 7, // FPE_FLTINV: invalid operation+ f if f & 4 != 0 => 3, // FPE_FLTDIV: divide by zero+ f if f & 8 != 0 => 4, // FPE_FLTOVF: overflow+ f if f & 16 != 0 => 5, // FPE_FLTUND: underflow+ f if f & 32 != 0 => 6, // FPE_FLTRES: inexact result+ _ => 0,+ }+}+ /// An exception in user mode is the program's fault, not the kernel's: it /// gets a signal (which kills it, unless it has a handler). In the kernel /// it's a bug: stop.
@@ -29,6 +29,7 @@ pub mod dev; // /dev: null, random, hvc0, fb0, tty, ptmx, pts/N pub mod elf; // ELF64 loader for user programs pub mod errno; // Linux-compatible error numbers pub mod fbcon; // text console drawn on the framebuffer+pub mod fpu; // FPU/SSE/AVX registers: per-thread save/restore (XSAVE or FXSAVE) pub mod font; // 8x16 bitmap font (Spleen) for fbcon pub mod futex; // futexes: user-space locks' sleep/wake (threads) pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs@@ -58,6 +59,7 @@ pub mod virtio; // modern virtio transport + virtio-rng, -console, -gpu /// PICs, and only then enable interrupts (`sti`). pub fn init() { gdt::init();+ fpu::init_cpu(); // before any CPU starts: smp.rs copies our CR0/CR4 syscall::init(); interrupts::init_idt(); unsafe { interrupts::PICS.lock().initialize() };
@@ -53,6 +53,7 @@ use crate::elf::{self, ElfError}; use crate::errno::Errno;+use crate::fpu::FpuState; use crate::fs::{self, file::File}; use crate::mm::Mm; use crate::signal::{self, SigInfo, SignalState, ThreadSig};@@ -184,6 +185,10 @@ pub struct Task { /// Its own signal mask and pending signals. Locked with interrupts off, /// after the process's `signals` if both are needed. pub sig: Mutex<ThreadSig>,+ /// Its FPU/SSE/AVX registers while it isn't running (fpu.rs). Only the+ /// scheduler touches it, with interrupts off: saved when the thread is+ /// switched out, loaded when it's switched in.+ pub fpu: Mutex<crate::fpu::FpuState>, } impl Task {@@ -347,19 +352,20 @@ pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc vfork_done: AtomicBool::new(false), }; serial_println!("[process] started pid {} ({}), entry {:#x}", pid, name, start.rip);- Ok(start_process(process, start, 0, 0))+ Ok(start_process(process, start, 0, 0, crate::fpu::initial())) } /// Put the process in the table and start its first thread (tid = pid).-fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64) -> Arc<Process> {+fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState) -> Arc<Process> { let process = Arc::new(process); without_interrupts(|| PROCESSES.lock().insert(process.pid, process.clone()));- start_task(&process, process.pid, start, fs_base, blocked);+ start_task(&process, process.pid, start, fs_base, blocked, fpu); process } /// Start a thread `tid` of `process` that enters user mode with `start`.-fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64) -> Arc<Task> {+/// `fpu`: its FPU/SSE registers (a copy of the creator's, or clean).+fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState) -> Arc<Task> { let task = Arc::new(Task { tid: AtomicU64::new(tid), process: process.clone(),@@ -369,6 +375,7 @@ fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, start: Mutex::new(Some(start)), killed: AtomicBool::new(false), sig: Mutex::new(ThreadSig::new(blocked)),+ fpu: Mutex::new(fpu), }); without_interrupts(|| process.tasks.lock().push(task.clone())); let l4 = process.mm.lock().l4;@@ -894,7 +901,9 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) - vfork_done: AtomicBool::new(false), }; serial_println!("[process] pid {} forked pid {}", parent.pid, pid);- let child = start_process(child, start, fs_base, blocked);+ // The child's FPU/SSE registers start as a copy of ours (we're running,+ // so ours are in the CPU right now).+ let child = start_process(child, start, fs_base, blocked, crate::fpu::save_live()); if vfork { // The parent sleeps until the child has exec'd or exited (real // vfork shares the memory meanwhile; we just use copy-on-write).@@ -929,7 +938,7 @@ pub fn clone_thread(p: &Arc<Process>, frame: &TrapFrame, stack: u64, tls: Option let blocked = without_interrupts(|| me.sig.lock().blocked); // Forget scheduler threads of threads that are completely gone. without_interrupts(|| p.kthreads.lock().retain(|&k| thread::exists(k)));- let task = start_task(p, tid, start, fs_base, blocked);+ let task = start_task(p, tid, start, fs_base, blocked, crate::fpu::save_live()); task.clear_child_tid.store(clear_tid, Ordering::Relaxed); serial_println!("[process] pid {} new thread {}", p.pid, tid); Ok(tid)@@ -980,6 +989,7 @@ pub fn execve(p: &Arc<Process>, frame: &mut TrapFrame, path: &str, argv: &[Vec<u *p.exe.lock() = String::from(path); if let Some(me) = thread::current_task() { me.fs_base.store(0, Ordering::Relaxed); // no TLS until the new libc sets one+ crate::fpu::reset_live(); // clean FPU/SSE/AVX registers for the new program me.clear_child_tid.store(0, Ordering::Relaxed); } *p.env.lock() = envp.iter().map(|e| String::from_utf8_lossy(e).into_owned()).collect();
@@ -627,7 +627,7 @@ struct SigContext { trapno: u64, oldmask: u64, cr2: u64,- fpstate: u64, // pointer to saved FPU/SSE state; 0 = none (see below)+ fpstate: u64, // pointer to the saved FPU/SSE/AVX state (below the frame); 0 = none reserved: [u64; 8], } @@ -655,20 +655,47 @@ struct RtSigFrame { /// enable, I/O privilege level...) only the kernel decides. const USER_RFLAGS: u64 = 0x0004_0DD5 | (1 << 16); +// The FPU/SSE/AVX state in a signal frame (Linux's layout, stage 15.1).+// It sits above the rt_sigframe, 64-byte aligned, and+// `uc.mcontext.fpstate` points at it. Its first 512 bytes are the FXSAVE+// format; bytes 464..512 (which FXSAVE leaves unused) hold+// `struct _fpx_sw_bytes`, saying whether an XSAVE area follows:+//+// u32 magic1 = FP_XSTATE_MAGIC1 u32 extended_size (state + 4)+// u64 xfeatures (= XCR0) u32 xstate_size + 7 u32 padding+//+// and FP_XSTATE_MAGIC2 is stored right after the XSAVE area. With only+// FXSAVE, magic1 is 0. A handler may read and even change the saved+// values: sigreturn loads whatever is there (after checking it).+const FP_XSTATE_MAGIC1: u32 = 0x4650_5853;+const FP_XSTATE_MAGIC2: u32 = 0x4650_5845;+const SW_BYTES: usize = 464;+/// uc_flags: an XSAVE area is there (UC_FP_XSTATE), and the two flags+/// Linux always sets on x86_64 (UC_SIGCONTEXT_SS | UC_STRICT_RESTORE_SS).+const UC_FP_XSTATE: u64 = 1;+const UC_SS_FLAGS: u64 = 2 | 4;++/// Bytes the FPU state takes in a signal frame.+fn fpstate_size() -> u64 {+ if crate::fpu::uses_xsave() { crate::fpu::size() as u64 + 4 } else { 512 }+}+ /// Push a signal frame on the user stack and point `frame` at the handler.+/// The stack, from higher to lower addresses: ///-/// Note: we don't save the FPU/SSE registers (`fpstate` = 0). Our kernel-/// doesn't use them and our programs' handlers don't touch floats, so they-/// survive anyway; a full libc needs XSAVE state here (roadmap).+/// [interrupted code's stack] [128-byte red zone] [FPU state, 64-aligned]+/// [rt_sigframe: pretcode, ucontext, siginfo] <- rsp of the handler fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action: &SigAction) -> Result<(), ()> { if action.flags & SA_RESTORER == 0 || action.restorer == 0 { return Err(()); // nowhere to return to (x86_64 requires a restorer) } let size = core::mem::size_of::<RtSigFrame>() as u64; // Below the 128-byte "red zone" (leaf functions may use it without- // moving rsp), aligned so the handler starts with rsp % 16 == 8, as- // if it had been called.- let sp = ((frame.rsp.wrapping_sub(128).wrapping_sub(size)) & !0xf).wrapping_sub(8);+ // moving rsp) goes the FPU state, then the frame, aligned so the+ // handler starts with rsp % 16 == 8, as if it had been called.+ let fp_size = fpstate_size();+ let fp = (frame.rsp.wrapping_sub(128).wrapping_sub(fp_size)) & !63;+ let sp = ((fp.wrapping_sub(size)) & !0xf).wrapping_sub(8); // The mask sigreturn restores: the real one, if a ppoll & co. had // swapped in a temporary mask. let blocked = without_interrupts(|| {@@ -700,17 +727,44 @@ fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action trapno: frame.vector, oldmask: blocked, cr2: if sig == SIGSEGV || sig == SIGBUS { info.fields[0] } else { 0 },+ fpstate: fp, ..Default::default() };+ let xsave = crate::fpu::uses_xsave(); let rt = RtSigFrame { pretcode: action.restorer,- uc: UContext { mcontext, sigmask: blocked, ..Default::default() },+ uc: UContext {+ flags: UC_SS_FLAGS | if xsave { UC_FP_XSTATE } else { 0 },+ mcontext,+ sigmask: blocked,+ ..Default::default()+ }, info: *info, };- if !crate::mm::user_access_ok(sp, size, true) {+ if !crate::mm::user_access_ok(sp, fp + fp_size - sp, true) { return Err(()); }- unsafe { core::ptr::write_unaligned(sp as *mut RtSigFrame, rt) };+ // The FPU registers right now are still the interrupted code's (the+ // kernel never touches them): save them into the frame.+ let mut state = crate::fpu::save_live();+ let n = crate::fpu::size();+ let sw = &mut state.0[SW_BYTES..512];+ sw.fill(0);+ if xsave {+ sw[0..4].copy_from_slice(&FP_XSTATE_MAGIC1.to_le_bytes());+ sw[4..8].copy_from_slice(&(n as u32 + 4).to_le_bytes());+ sw[8..16].copy_from_slice(&crate::fpu::xfeatures().to_le_bytes());+ sw[16..20].copy_from_slice(&(n as u32).to_le_bytes());+ state.0[n..n + 4].copy_from_slice(&FP_XSTATE_MAGIC2.to_le_bytes());+ }+ unsafe {+ core::ptr::copy_nonoverlapping(state.0.as_ptr(), fp as *mut u8, fp_size as usize);+ core::ptr::write_unaligned(sp as *mut RtSigFrame, rt);+ }+ // Like Linux, the handler starts with clean FPU/SSE registers (MXCSR at+ // its default, so its float maths can't trap because the interrupted+ // code had unmasked an exception).+ crate::fpu::reset_live(); frame.rsp = sp; frame.rip = action.handler; frame.rdi = sig as u64;@@ -766,6 +820,46 @@ pub fn sys_rt_sigreturn(frame: &mut TrapFrame) { if let Some(t) = thread::current_task() { without_interrupts(|| t.sig.lock().blocked = rt.uc.sigmask & !UNBLOCKABLE); }+ if restore_fpstate(m.fpstate).is_err() {+ process::exit_group(ExitStatus::Killed(SIGSEGV, "bad sigreturn FPU state"));+ }+}++/// sigreturn's FPU part: load the state `fpstate` points to (0 = clean+/// registers, like Linux). An XSAVE area is used only if both magic numbers+/// are there; otherwise just the 512-byte FXSAVE part (the AVX halves are+/// then cleared). Err if it's unreadable or has an impossible MXCSR.+fn restore_fpstate(fpstate: u64) -> Result<(), ()> {+ use crate::fpu;+ if fpstate == 0 {+ fpu::reset_live();+ return Ok(());+ }+ let mut state = fpu::FpuState([0; fpu::AREA_SIZE]);+ let legacy = crate::syscall::user_slice(fpstate, 512).map_err(|_| ())?;+ state.0[..512].copy_from_slice(legacy);+ let word = |b: &[u8], at: usize| u32::from_le_bytes(b[at..at + 4].try_into().unwrap());+ let n = fpu::size();+ let mut extended = false;+ if fpu::uses_xsave() && word(&state.0, SW_BYTES) == FP_XSTATE_MAGIC1 && word(&state.0, SW_BYTES + 16) as usize == n {+ if let Ok(all) = crate::syscall::user_slice(fpstate, n as u64 + 4) {+ if word(all, n) == FP_XSTATE_MAGIC2 {+ state.0[..n].copy_from_slice(&all[..n]);+ extended = true;+ }+ }+ }+ if fpu::uses_xsave() && !extended {+ // An FXSAVE-only frame: x87 + SSE from it, every other feature+ // back to its initial state (XSAVE header: just bits 0 and 1).+ state.0[512..fpu::AREA_SIZE].fill(0);+ state.0[512..520].copy_from_slice(&(fpu::XFEATURE_X87 | fpu::XFEATURE_SSE).to_le_bytes());+ }+ if !fpu::sanitize(&mut state) {+ return Err(());+ }+ fpu::restore(&state);+ Ok(()) } // ---------------------------------------------------------------------------
@@ -336,6 +336,7 @@ extern "C" fn ap_entry(cpu: u64) -> ! { let cpu = cpu as usize; AP_ALIVE.store(true, Ordering::SeqCst); gdt::init_ap(cpu);+ crate::fpu::init_cpu(); // XCR0 and the FPU are per CPU interrupts::init_idt(); crate::syscall::init(); apic::init_ap();
@@ -366,6 +366,12 @@ pub fn schedule() { } } + // The FPU/SSE/AVX registers (fpu.rs): the kernel never uses them, so+ // they still hold the outgoing user thread's values. Save those...+ if let Some(task) = &s.thread(cur).task {+ crate::fpu::save(&mut task.fpu.lock());+ }+ let bit = 1u32 << cpu; let t = s.thread(next); t.state = State::Running;@@ -379,6 +385,8 @@ pub fn schedule() { x86_64::registers::model_specific::FsBase::write(x86_64::VirtAddr::new_truncate( task.fs_base.load(core::sync::atomic::Ordering::Relaxed), ));+ // ...and load the incoming one's.+ crate::fpu::restore(&task.fpu.lock()); } if let Some(stack) = &t.stack { if t.tlb_clean_on & bit == 0 {
@@ -178,6 +178,8 @@ trap_stub!("trap_divide_error", "0", "push 0"); // SIGFPE trap_stub!("trap_invalid_opcode", "6", "push 0"); // SIGILL trap_stub!("trap_general_protection", "13", ""); // SIGSEGV (CPU pushes an error code) trap_stub!("trap_page_fault", "14", ""); // SIGSEGV or demand paging+trap_stub!("trap_x87_error", "16", "push 0"); // SIGFPE (an unmasked x87 exception)+trap_stub!("trap_simd_error", "19", "push 0"); // SIGFPE (an unmasked SSE exception) trap_stub!("trap_apic_timer", "0x40", "push 0"); unsafe extern "C" {@@ -185,5 +187,7 @@ unsafe extern "C" { pub fn trap_invalid_opcode(); pub fn trap_general_protection(); pub fn trap_page_fault();+ pub fn trap_x87_error();+ pub fn trap_simd_error(); pub fn trap_apic_timer(); }
@@ -0,0 +1,74 @@+//! Stage 15.1, what a static musl binary needs from the kernel before it+//! can even start: its own FPU/SSE/AVX registers per thread (saved on+//! every context switch, in signal frames, cleared by exec), Linux's+//! C-string path syscalls and `struct stat`, and Linux's syscall numbers.+//! As before the detailed checks live in user programs (fputest,+//! pathtest, rawabi: exit code = failed checks); this file runs them and+//! checks nothing is left behind.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::string::String;+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, memory, thread};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rustbelt::init();+ rustbelt::init_memory(boot_info);+ rustbelt::init_smp(); // all 4 CPUs run threads+ fs::init();+ rustbelt::init_storage();+ test_main();+ rustbelt::hlt_loop();+}++fn run(name: &str, args: &[&str]) -> ExitStatus {+ let mut spawn = SpawnArgs::kernel(name);+ spawn.args.extend(args.iter().map(|a| String::from(*a)));+ let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");+ process::wait(&p)+}++fn counts() -> (usize, usize, usize) {+ thread::sleep_ticks(3); // let exited threads' stacks be freed+ (thread::thread_count(), process::count(), memory::frames_in_use())+}++/// Run a program twice (the first warms caches), check it passes and+/// threads, processes and memory frames come back to where they were.+fn run_without_leaks(name: &str, args: &[&str]) {+ assert_eq!(run(name, args), ExitStatus::Exited(0), "{} failed (warm-up)", name);+ let before = counts();+ assert_eq!(run(name, args), ExitStatus::Exited(0), "{} failed", name);+ assert_eq!(counts(), before, "{} leaked something", name);+}++/// The kernel set up the FPU on every CPU: XSAVE under QEMU's `-cpu max`.+#[test_case]+fn fpu_enabled() {+ assert!(rustbelt::fpu::size() >= 512);+ if rustbelt::fpu::uses_xsave() {+ assert!(rustbelt::fpu::xfeatures() & 3 == 3, "x87 and SSE state in XCR0");+ }+}++/// 12 threads in 3 processes with distinct vector registers on 4 CPUs;+/// signal handlers that clobber them; fork copies, exec clears; SIGFPE.+#[test_case]+fn fpu_state_is_per_thread() {+ run_without_leaks("fputest", &[]);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rustbelt::test_panic_handler(info)+}
@@ -0,0 +1,457 @@+//! fputest -- every thread keeps its own FPU/SSE/AVX registers (stage 15.1).+//!+//! The kernel never uses the FPU registers itself, but user code does (any+//! C program's `double` maths, and libc's memcpy/strlen use SSE). So on a+//! context switch the kernel saves one thread's registers and loads the+//! next one's (XSAVE, or FXSAVE on older CPUs), a signal handler gets+//! clean registers and sigreturn puts the old ones back, fork copies them+//! and exec clears them. This program checks all of that:+//!+//! - threads + processes (spread over all CPUs) each load *distinct*+//! values into xmm0-15 (all 256 bits of ymm0-15 if the CPU has AVX) and+//! MXCSR, spin and yield for a while, then check nothing changed;+//! - a signal handler that scribbles over every register, synchronously+//! (kill to ourselves) and asynchronously (another thread's tgkill while+//! we spin): the interrupted code must see its own values afterwards;+//! - the handler starts with clean registers, finds the saved ones via+//! uc_mcontext.fpstate (Linux's layout), and a value it changes there is+//! what sigreturn loads;+//! - an unmasked SSE divide-by-zero raises SIGFPE (FPE_FLTDIV);+//! - fork copies the registers; exec starts with zeros and MXCSR 0x1f80.+//!+//! Our programs are built soft-float, so Rust code here never touches the+//! vector registers: only the `asm!` blocks below do. (That's also why the+//! asm can't name xmm registers as operands: it uses memory instead.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::sync::Arc;+use alloc::vec::Vec;+use core::arch::asm;+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};+use ulib::*;++/// The 16 vector registers, 32 bytes each (only the low 16 used without AVX).+#[repr(C, align(32))]+#[derive(Clone, Copy, PartialEq)]+struct Regs([u8; 512]);++static AVX: AtomicBool = AtomicBool::new(false);++/// Can we use AVX? The CPU must have it (CPUID.1:ECX bit 28) *and* the+/// kernel must have turned on XSAVE (OSXSAVE, bit 27) with the AVX state+/// in XCR0 (bits 1 and 2): otherwise AVX instructions fault (#UD).+fn detect_avx() -> bool {+ let ecx = core::arch::x86_64::__cpuid(1).ecx;+ if ecx & (1 << 27) == 0 || ecx & (1 << 28) == 0 {+ return false;+ }+ let (lo, _hi): (u32, u32);+ unsafe { asm!("xgetbv", in("ecx") 0, out("eax") lo, out("edx") _hi, options(nomem, nostack)) };+ lo & 6 == 6+}+fn avx() -> bool {+ AVX.load(Ordering::Relaxed)+}++/// Load all vector registers from `r`.+fn load(r: &Regs) {+ unsafe {+ if avx() {+ asm!(+ "vmovdqu ymm0, [{p} + 0]",+ "vmovdqu ymm1, [{p} + 32]",+ "vmovdqu ymm2, [{p} + 64]",+ "vmovdqu ymm3, [{p} + 96]",+ "vmovdqu ymm4, [{p} + 128]",+ "vmovdqu ymm5, [{p} + 160]",+ "vmovdqu ymm6, [{p} + 192]",+ "vmovdqu ymm7, [{p} + 224]",+ "vmovdqu ymm8, [{p} + 256]",+ "vmovdqu ymm9, [{p} + 288]",+ "vmovdqu ymm10, [{p} + 320]",+ "vmovdqu ymm11, [{p} + 352]",+ "vmovdqu ymm12, [{p} + 384]",+ "vmovdqu ymm13, [{p} + 416]",+ "vmovdqu ymm14, [{p} + 448]",+ "vmovdqu ymm15, [{p} + 480]",+ p = in(reg) r.0.as_ptr(), options(nostack, readonly, preserves_flags));+ } else {+ asm!(+ "movdqu xmm0, [{p} + 0]",+ "movdqu xmm1, [{p} + 32]",+ "movdqu xmm2, [{p} + 64]",+ "movdqu xmm3, [{p} + 96]",+ "movdqu xmm4, [{p} + 128]",+ "movdqu xmm5, [{p} + 160]",+ "movdqu xmm6, [{p} + 192]",+ "movdqu xmm7, [{p} + 224]",+ "movdqu xmm8, [{p} + 256]",+ "movdqu xmm9, [{p} + 288]",+ "movdqu xmm10, [{p} + 320]",+ "movdqu xmm11, [{p} + 352]",+ "movdqu xmm12, [{p} + 384]",+ "movdqu xmm13, [{p} + 416]",+ "movdqu xmm14, [{p} + 448]",+ "movdqu xmm15, [{p} + 480]",+ p = in(reg) r.0.as_ptr(), options(nostack, readonly, preserves_flags));+ }+ }+}++/// Store all vector registers (the bytes AVX doesn't have stay 0).+fn store() -> Regs {+ let mut r = Regs([0; 512]);+ unsafe {+ if avx() {+ asm!(+ "vmovdqu [{p} + 0], ymm0",+ "vmovdqu [{p} + 32], ymm1",+ "vmovdqu [{p} + 64], ymm2",+ "vmovdqu [{p} + 96], ymm3",+ "vmovdqu [{p} + 128], ymm4",+ "vmovdqu [{p} + 160], ymm5",+ "vmovdqu [{p} + 192], ymm6",+ "vmovdqu [{p} + 224], ymm7",+ "vmovdqu [{p} + 256], ymm8",+ "vmovdqu [{p} + 288], ymm9",+ "vmovdqu [{p} + 320], ymm10",+ "vmovdqu [{p} + 352], ymm11",+ "vmovdqu [{p} + 384], ymm12",+ "vmovdqu [{p} + 416], ymm13",+ "vmovdqu [{p} + 448], ymm14",+ "vmovdqu [{p} + 480], ymm15",+ p = in(reg) r.0.as_mut_ptr(), options(nostack, preserves_flags));+ } else {+ asm!(+ "movdqu [{p} + 0], xmm0",+ "movdqu [{p} + 32], xmm1",+ "movdqu [{p} + 64], xmm2",+ "movdqu [{p} + 96], xmm3",+ "movdqu [{p} + 128], xmm4",+ "movdqu [{p} + 160], xmm5",+ "movdqu [{p} + 192], xmm6",+ "movdqu [{p} + 224], xmm7",+ "movdqu [{p} + 256], xmm8",+ "movdqu [{p} + 288], xmm9",+ "movdqu [{p} + 320], xmm10",+ "movdqu [{p} + 352], xmm11",+ "movdqu [{p} + 384], xmm12",+ "movdqu [{p} + 416], xmm13",+ "movdqu [{p} + 448], xmm14",+ "movdqu [{p} + 480], xmm15",+ p = in(reg) r.0.as_mut_ptr(), options(nostack, preserves_flags));+ }+ }+ r+}++fn get_mxcsr() -> u32 {+ let mut m = 0u32;+ unsafe { asm!("stmxcsr [{}]", in(reg) &mut m, options(nostack, preserves_flags)) };+ m+}+fn set_mxcsr(m: u32) {+ unsafe { asm!("ldmxcsr [{}]", in(reg) &m, options(nostack, readonly, preserves_flags)) };+}++/// A register image no other `seed` produces (only the parts this CPU has).+fn pattern(seed: u64) -> Regs {+ let mut r = Regs([0; 512]);+ let mut x = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1;+ for (i, chunk) in r.0.chunks_mut(8).enumerate() {+ x ^= x << 13;+ x ^= x >> 7;+ x ^= x << 17;+ if avx() || i % 4 < 2 {+ chunk.copy_from_slice(&x.to_le_bytes());+ }+ }+ r+}+/// A distinct MXCSR per seed: all exceptions masked, rounding mode (bits+/// 13-14) and flush-to-zero / denormals-are-zero (15, 6) from the seed.+fn mxcsr_for(seed: u64) -> u32 {+ 0x1f80 | ((seed as u32 & 3) << 13) | ((seed as u32 >> 2 & 1) << 15) | ((seed as u32 >> 3 & 1) << 6)+}++/// Load a pattern, spin and yield across many timer ticks (so the thread+/// is switched out and probably migrated), checking it now and then.+/// Returns how many checks found a wrong register.+fn hammer(seed: u64, rounds: u32) -> u32 {+ let want = pattern(seed);+ let mx = mxcsr_for(seed);+ load(&want);+ set_mxcsr(mx);+ let mut bad = 0;+ for round in 0..rounds {+ let mut x = round as u64;+ for _ in 0..20_000 {+ x = core::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1));+ }+ if round % 3 == 0 {+ sched_yield();+ }+ if store() != want || get_mxcsr() != mx {+ bad += 1;+ load(&want); // keep going with good values+ set_mxcsr(mx);+ }+ }+ set_mxcsr(0x1f80);+ bad+}++// --- signal handlers -----------------------------------------------------------++/// Offsets in the ucontext the kernel passes (same as Linux's): the+/// `fpstate` pointer in uc_mcontext, and inside the FXSAVE area: MXCSR,+/// xmm0, the software-reserved bytes (sw_bytes) and, in the XSAVE area,+/// the upper halves of ymm0-15.+const UC_FLAGS: usize = 0;+const UC_FPSTATE: usize = 40 + 23 * 8;+const FX_MXCSR: usize = 24;+const FX_XMM: usize = 160;+const FX_SW: usize = 464;+const XS_YMM_HI: usize = 576;+const MAGIC1: u32 = 0x4650_5853;+const MAGIC2: u32 = 0x4650_5845;++static SYNC_SEED: AtomicU64 = AtomicU64::new(0);+/// Bit flags the handler sets for main to check.+static H_CLEAN: AtomicU32 = AtomicU32::new(0); // registers zero + MXCSR default on entry+static H_SAVED: AtomicU32 = AtomicU32::new(0); // the frame held the interrupted values+static H_LAYOUT: AtomicU32 = AtomicU32::new(0); // magic numbers / flags right+static ASYNC_HITS: AtomicU32 = AtomicU32::new(0);+const NEW_XMM15: u64 = 0x1515_1515_cafe_f00d;++fn rd32(p: *const u8) -> u32 {+ unsafe { core::ptr::read_unaligned(p as *const u32) }+}+fn rd64(p: *const u8) -> u64 {+ unsafe { core::ptr::read_unaligned(p as *const u64) }+}++/// Scribble over every vector register and MXCSR (a handler that does+/// float maths, or calls memcpy, would).+fn clobber() {+ load(&pattern(0xdead));+ set_mxcsr(0x1f80 | (3 << 13));+}++extern "C" fn on_usr1(_sig: i32, _info: *const SigInfo, uc: *mut u8) {+ let clean = store() == Regs([0; 512]) && get_mxcsr() == 0x1f80;+ H_CLEAN.store(clean as u32, Ordering::SeqCst);+ let want = pattern(SYNC_SEED.load(Ordering::SeqCst));+ let fp = rd64(unsafe { uc.add(UC_FPSTATE) }) as *mut u8;+ if fp.is_null() || fp as u64 % 64 != 0 {+ H_SAVED.store(0, Ordering::SeqCst);+ } else {+ // xmm i is at FX_XMM + 16*i; our image has register i at 32*i.+ let mut ok = rd32(unsafe { fp.add(FX_MXCSR) }) == mxcsr_for(SYNC_SEED.load(Ordering::SeqCst));+ for i in 0..16 {+ for b in 0..16 {+ ok &= unsafe { *fp.add(FX_XMM + 16 * i + b) } == want.0[32 * i + b];+ }+ }+ let flags = rd64(unsafe { uc.add(UC_FLAGS) });+ let magic1 = rd32(unsafe { fp.add(FX_SW) });+ let layout = if magic1 == MAGIC1 {+ // XSAVE frame: sw_bytes say how big it is; magic2 at the end.+ let size = rd32(unsafe { fp.add(FX_SW + 16) }) as usize;+ let xfeatures = rd64(unsafe { fp.add(FX_SW + 8) });+ if avx() {+ for i in 0..16 {+ for b in 0..16 {+ ok &= unsafe { *fp.add(XS_YMM_HI + 16 * i + b) } == want.0[32 * i + 16 + b];+ }+ }+ }+ flags & 1 != 0 && rd32(unsafe { fp.add(size) }) == MAGIC2 && xfeatures & 3 == 3 && (!avx() || xfeatures & 4 != 0)+ } else {+ // FXSAVE-only frame (no XSAVE): fine as long as we're not using AVX.+ magic1 == 0 && !avx()+ };+ H_SAVED.store(ok as u32, Ordering::SeqCst);+ H_LAYOUT.store(layout as u32, Ordering::SeqCst);+ // Change the saved xmm15: sigreturn must load what's in the frame.+ unsafe { core::ptr::write_unaligned(fp.add(FX_XMM + 16 * 15) as *mut u64, NEW_XMM15) };+ }+ clobber();+}++extern "C" fn on_usr2(_sig: i32) {+ clobber();+ ASYNC_HITS.fetch_add(1, Ordering::SeqCst);+}++static FPE_CODE: AtomicU32 = AtomicU32::new(0);+/// SIGFPE from an unmasked SSE exception. Returning would just run the+/// faulting instruction again, so mask all exceptions (and clear the+/// flags) in the *saved* MXCSR first: the retry then gives inf quietly.+extern "C" fn on_fpe(_sig: i32, info: *const SigInfo, uc: *mut u8) {+ FPE_CODE.store(unsafe { (*info).code } as u32, Ordering::SeqCst);+ let fp = rd64(unsafe { uc.add(UC_FPSTATE) }) as *mut u8;+ if !fp.is_null() {+ unsafe { core::ptr::write_unaligned(fp.add(FX_MXCSR) as *mut u32, 0x1f80) };+ }+}++// --- the tests ---------------------------------------------------------------------++/// N threads in this process, each with its own seed; returns bad checks.+fn thread_round(base: u64, n: u64, rounds: u32) -> u32 {+ let bad = Arc::new(AtomicU32::new(0));+ let mut ts = Vec::new();+ for i in 0..n {+ let bad = bad.clone();+ ts.push(spawn_thread(0, move || {+ bad.fetch_add(hammer(base + i, rounds), Ordering::SeqCst);+ }));+ }+ let main_bad = hammer(base + 99, rounds);+ for t in ts {+ match t {+ Ok(t) => t.join(),+ Err(_) => return 1000,+ }+ }+ bad.load(Ordering::SeqCst) + main_bad+}++fn exec_check() -> i64 {+ // Run straight after exec: nothing has touched the vector registers.+ let clean = store() == Regs([0; 512]) && get_mxcsr() == 0x1f80;+ if clean { 0 } else { 1 }+}++fn main() -> i64 {+ AVX.store(detect_avx(), Ordering::Relaxed);+ if arg(1) == Some("exec-check") {+ return exec_check();+ }+ let mut c = Checks::new("fputest");+ println!(+ "fputest: {} registers, {} CPU(s)",+ if avx() { "ymm0-15 (AVX)" } else { "xmm0-15 (SSE)" },+ sysinfo().cpus+ );++ // 1. The registers start clean (main ran just after exec).+ c.check(exec_check() == 0, "a new program starts with zeroed registers and MXCSR 0x1f80");++ // 2. Threads and processes, all with distinct values, on all CPUs.+ let mut children = Vec::new();+ for p in 1..=2u64 {+ let pid = fork();+ if pid == 0 {+ exit(thread_round(p * 1000, 3, 60).min(100) as i64);+ }+ children.push(pid);+ }+ let bad = thread_round(0, 3, 60);+ let mut child_bad = 0;+ for pid in children {+ child_bad += wait(pid);+ }+ c.check(bad == 0, "4 threads kept their own vector registers + MXCSR");+ c.check(child_bad == 0, "so did 4 threads in each of 2 forked processes (12 at once)");++ // 3. fork copies the registers; exec clears them.+ let want = pattern(7);+ load(&want);+ set_mxcsr(mxcsr_for(7));+ let pid = fork();+ if pid == 0 {+ let same = store() == want && get_mxcsr() == mxcsr_for(7);+ if !same {+ exit(2);+ }+ execve("/mnt/disk0/bin/fputest", &["fputest", "exec-check"], &[]);+ exit(3);+ }+ let parent_same = store() == want;+ set_mxcsr(0x1f80);+ let code = wait(pid);+ c.check(parent_same && code != 2, "fork: the child starts with the parent's registers");+ c.check(code == 0, "exec: the new program starts with clean registers");++ // 4. A synchronous signal whose handler clobbers everything.+ sigaction_info(SIGUSR1, on_usr1, 0);+ SYNC_SEED.store(42, Ordering::SeqCst);+ let mut want = pattern(42);+ load(&want);+ set_mxcsr(mxcsr_for(42));+ kill(getpid(), SIGUSR1); // the handler runs on the way back from this+ let got = store();+ let mx = get_mxcsr();+ set_mxcsr(0x1f80);+ c.check(H_CLEAN.load(Ordering::SeqCst) == 1, "the handler starts with clean registers");+ c.check(H_SAVED.load(Ordering::SeqCst) == 1, "uc_mcontext.fpstate holds the interrupted registers");+ c.check(H_LAYOUT.load(Ordering::SeqCst) == 1, "the fpstate has Linux's layout (sw_bytes, magics, uc_flags)");+ want.0[32 * 15..32 * 15 + 8].copy_from_slice(&NEW_XMM15.to_le_bytes());+ c.check(got == want && mx == mxcsr_for(42), "after sigreturn: our registers back, with the handler's xmm15 edit");++ // 5. Asynchronous signals: another thread interrupts us while we spin.+ signal(SIGUSR2, on_usr2);+ let me = gettid();+ let sender = spawn_thread(0, move || {+ for _ in 0..40 {+ tgkill(getpid(), me, SIGUSR2);+ sleep_ms(2);+ }+ });+ let want = pattern(4242);+ load(&want);+ set_mxcsr(mxcsr_for(4242));+ let t0 = now_us();+ let mut bad = 0;+ while ASYNC_HITS.load(Ordering::SeqCst) < 40 && now_us() - t0 < 5_000_000 {+ if store() != want || get_mxcsr() != mxcsr_for(4242) {+ bad += 1;+ load(&want);+ set_mxcsr(mxcsr_for(4242));+ }+ }+ set_mxcsr(0x1f80);+ if let Ok(t) = sender {+ t.join();+ }+ let hits = ASYNC_HITS.load(Ordering::SeqCst);+ println!("fputest: {} async signals, each handler clobbered every register", hits);+ c.check(hits == 40 && bad == 0, "40 async signals with clobbering handlers changed nothing we saw");++ // 6. An unmasked SSE exception (divide by zero) -> SIGFPE, FPE_FLTDIV.+ sigaction_info(SIGFPE, on_fpe, 0);+ let one = 1.0f64.to_bits();+ let zero = 0u64;+ let mut out = 0u64;+ set_mxcsr(0x1f80 & !(1 << 9)); // unmask ZE (bit 9)+ unsafe {+ asm!(+ "movq xmm0, [{a}]",+ "divsd xmm0, [{b}]", // 1.0 / 0.0+ "movq [{o}], xmm0",+ a = in(reg) &one, b = in(reg) &zero, o = in(reg) &mut out,+ options(nostack)+ );+ }+ let mx = get_mxcsr();+ set_mxcsr(0x1f80);+ let code = FPE_CODE.load(Ordering::SeqCst);+ if code == 0 && out == f64::INFINITY.to_bits() && mx & 4 != 0 {+ // QEMU's software CPU (TCG) sets the flag but never raises #XM.+ println!("fputest: skipped SIGFPE check: this CPU doesn't raise SIMD exceptions (QEMU TCG)");+ } else {+ c.check(code == 3, "unmasked 1.0/0.0 -> SIGFPE with si_code FPE_FLTDIV");+ c.check(out == f64::INFINITY.to_bits() && mx & (1 << 9) != 0, "the handler's MXCSR edit took effect (retry gave inf)");+ }++ c.done()+}