foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 11:00:20 -0700 |
|---|---|---|
| commit | 618ed3f823d916066aae2a189d31e57ee4314adb | |
| parent | 0094a45ff3029bf74d17d16bdf577bb9e6b9a344 | |
| tree | browse files at this commit | |
WIP 15.1: per-thread FPU/SSE/AVX state (XSAVE/FXSAVE), signal fpstate, #MF/#XM, fputest
| A | fputest.rs | +457 -0 |
1 file changed, 457 insertions, 0 deletions
@@ -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()+}