diff --git a/Cargo.toml b/Cargo.toml index a462ec9c..c2a0f8f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "userspace/pjdfstest_runner", "userspace/repro_fork", "userspace/shell_pipeline", + "userspace/vibix_abi", ] # tests/pjdfstest is a vendored copy of saidsay-so/pjdfstest (2-clause BSD). It # is intentionally kept outside the workspace until #581 wires it into xtask — diff --git a/userspace/vibix_abi/Cargo.toml b/userspace/vibix_abi/Cargo.toml new file mode 100644 index 00000000..cf9b59a0 --- /dev/null +++ b/userspace/vibix_abi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "vibix_abi" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[lib] +name = "vibix_abi" +path = "src/lib.rs" diff --git a/userspace/vibix_abi/src/alloc.rs b/userspace/vibix_abi/src/alloc.rs new file mode 100644 index 00000000..d49f8fcf --- /dev/null +++ b/userspace/vibix_abi/src/alloc.rs @@ -0,0 +1,114 @@ +//! Global allocator for vibix userspace. +//! +//! Uses `brk` for small allocations (< 128 KiB) and `mmap` for large ones. +//! This is the allocator that std's `System` allocator delegates to on vibix. + +use core::alloc::{GlobalAlloc, Layout}; +use core::ptr; +use core::sync::atomic::{AtomicUsize, Ordering}; + +use crate::syscall; + +/// Syscall numbers (Linux x86_64 convention). +const SYS_BRK: u64 = 12; +const SYS_MMAP: u64 = 9; +const SYS_MUNMAP: u64 = 11; + +/// Allocations at or above this size use mmap instead of brk. +const MMAP_THRESHOLD: usize = 128 * 1024; + +/// mmap protection and flag constants. +const PROT_READ: u64 = 0x1; +const PROT_WRITE: u64 = 0x2; +const MAP_PRIVATE: u64 = 0x02; +const MAP_ANONYMOUS: u64 = 0x20; + +/// A simple bump allocator backed by `brk` for small allocations. +/// +/// Large allocations (>= MMAP_THRESHOLD) go directly to `mmap` so they can be +/// individually `munmap`'d without fragmenting the brk region. +pub struct VibixAllocator; + +/// Current brk pointer. Initialized lazily on first allocation. +static BRK_CURRENT: AtomicUsize = AtomicUsize::new(0); + +/// Initialize the brk region by querying the current program break. +fn brk_init() -> usize { + let current = unsafe { syscall::syscall1(SYS_BRK, 0) } as usize; + BRK_CURRENT.store(current, Ordering::Relaxed); + current +} + +unsafe impl GlobalAlloc for VibixAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let size = layout.size(); + let align = layout.align(); + + if size >= MMAP_THRESHOLD { + return mmap_alloc(size); + } + + // Bump-allocate from the brk region. + loop { + let mut current = BRK_CURRENT.load(Ordering::Relaxed); + if current == 0 { + current = brk_init(); + } + + // Align up. + let aligned = (current + align - 1) & !(align - 1); + let new_brk = aligned + size; + + // Extend the program break. + let result = unsafe { syscall::syscall1(SYS_BRK, new_brk as u64) } as usize; + if result < new_brk { + // brk failed -- cannot grow the heap. Return null (OOM). + // We intentionally do NOT fall back to mmap here because + // dealloc() uses the size threshold to decide whether to + // munmap(); a small mmap'd block would never be freed. + return ptr::null_mut(); + } + + // Try to commit our bump. If another thread raced us, retry. + match BRK_CURRENT.compare_exchange( + current, + new_brk, + Ordering::AcqRel, + Ordering::Relaxed, + ) { + Ok(_) => return aligned as *mut u8, + Err(_) => continue, + } + } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + let size = layout.size(); + if size >= MMAP_THRESHOLD { + unsafe { + syscall::syscall2(SYS_MUNMAP, ptr as u64, size as u64); + } + } + // Small allocations from brk are not individually freed (bump allocator). + } +} + +/// Allocate via anonymous mmap. +fn mmap_alloc(size: usize) -> *mut u8 { + let ret = unsafe { + syscall::syscall6( + SYS_MMAP, + 0, // addr (kernel chooses) + size as u64, // length + PROT_READ | PROT_WRITE, // prot + MAP_PRIVATE | MAP_ANONYMOUS, // flags + u64::MAX, // fd (-1) + 0, // offset + ) + }; + // mmap returns MAP_FAILED (typically -1..-4095) on error. + if ret < 0 && ret > -4096 { + return ptr::null_mut(); + } + ret as *mut u8 +} diff --git a/userspace/vibix_abi/src/errno.rs b/userspace/vibix_abi/src/errno.rs new file mode 100644 index 00000000..09f69e5c --- /dev/null +++ b/userspace/vibix_abi/src/errno.rs @@ -0,0 +1,19 @@ +//! Thread-local errno storage. +//! +//! Each task gets its own `ERRNO` cell via TLS (epic #827). The kernel +//! allocates a fresh TLS block per task and sets `MSR_FS_BASE` to the TCB +//! pointer, so the compiler's `%fs:`-relative accesses work out of the box. + +use core::cell::Cell; + +/// Per-thread errno value. Syscall wrappers store the positive error code +/// here when a raw syscall returns a negative value. +#[thread_local] +pub static ERRNO: Cell = Cell::new(0); + +/// C-ABI-compatible accessor for errno's address. This is what the libc +/// crate's `__errno_location` resolves to. +#[no_mangle] +pub extern "C" fn __errno_location() -> *mut i32 { + ERRNO.as_ptr() +} diff --git a/userspace/vibix_abi/src/lib.rs b/userspace/vibix_abi/src/lib.rs new file mode 100644 index 00000000..d9358c4c --- /dev/null +++ b/userspace/vibix_abi/src/lib.rs @@ -0,0 +1,13 @@ +//! `vibix_abi` -- the Rust ABI bridge between std's platform abstraction layer +//! and vibix syscalls. +//! +//! This crate provides the syscall macro, memory allocator, errno TLS, and +//! stdio wrappers that the std PAL calls into. + +#![no_std] +#![feature(thread_local)] + +pub mod alloc; +pub mod errno; +pub mod stdio; +pub mod syscall; diff --git a/userspace/vibix_abi/src/stdio.rs b/userspace/vibix_abi/src/stdio.rs new file mode 100644 index 00000000..9e4d0ece --- /dev/null +++ b/userspace/vibix_abi/src/stdio.rs @@ -0,0 +1,42 @@ +//! Standard I/O helpers for vibix userspace. +//! +//! Provides `write_stdout` and `write_stderr` using the `writev` syscall +//! (nr 20), which is the vectored-write primitive that std's `Stdout`/`Stderr` +//! implementations call through. + +use crate::syscall; + +/// `writev` syscall number (Linux x86_64). +const SYS_WRITEV: u64 = 20; + +/// Standard file descriptors. +const STDOUT_FD: u64 = 1; +const STDERR_FD: u64 = 2; + +/// An iovec for vectored I/O, matching the Linux `struct iovec` layout. +#[repr(C)] +struct IoVec { + iov_base: *const u8, + iov_len: usize, +} + +/// Write `buf` to stdout. Returns the number of bytes written, or a negative +/// errno on failure. +pub fn write_stdout(buf: &[u8]) -> i64 { + writev(STDOUT_FD, buf) +} + +/// Write `buf` to stderr. Returns the number of bytes written, or a negative +/// errno on failure. +pub fn write_stderr(buf: &[u8]) -> i64 { + writev(STDERR_FD, buf) +} + +/// Issue a `writev` syscall with a single iovec entry. +fn writev(fd: u64, buf: &[u8]) -> i64 { + let iov = IoVec { + iov_base: buf.as_ptr(), + iov_len: buf.len(), + }; + unsafe { syscall::syscall3(SYS_WRITEV, fd, &iov as *const IoVec as u64, 1) } +} diff --git a/userspace/vibix_abi/src/syscall.rs b/userspace/vibix_abi/src/syscall.rs new file mode 100644 index 00000000..2b2c9d43 --- /dev/null +++ b/userspace/vibix_abi/src/syscall.rs @@ -0,0 +1,175 @@ +//! Raw syscall interface for vibix. +//! +//! The `syscall!()` macro wraps inline x86_64 `syscall` assembly with the +//! correct clobber registers per the Linux x86_64 syscall convention (and +//! issue #531). The kernel does not preserve rdi/rsi/rdx/r8/r9/r10 across +//! a syscall, so every argument register is declared `inlateout` and rcx/r11 +//! are clobbered by the CPU itself. + +/// Issue a raw syscall. Returns the value left in `rax` (negative values +/// encode `-errno`). +/// +/// # Safety +/// +/// The caller must ensure the syscall number and arguments are valid. +#[macro_export] +macro_rules! syscall { + ($nr:expr) => { + $crate::syscall::syscall0($nr as u64) + }; + ($nr:expr, $a0:expr) => { + $crate::syscall::syscall1($nr as u64, $a0 as u64) + }; + ($nr:expr, $a0:expr, $a1:expr) => { + $crate::syscall::syscall2($nr as u64, $a0 as u64, $a1 as u64) + }; + ($nr:expr, $a0:expr, $a1:expr, $a2:expr) => { + $crate::syscall::syscall3($nr as u64, $a0 as u64, $a1 as u64, $a2 as u64) + }; + ($nr:expr, $a0:expr, $a1:expr, $a2:expr, $a3:expr) => { + $crate::syscall::syscall4($nr as u64, $a0 as u64, $a1 as u64, $a2 as u64, $a3 as u64) + }; + ($nr:expr, $a0:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr) => { + $crate::syscall::syscall5( + $nr as u64, $a0 as u64, $a1 as u64, $a2 as u64, $a3 as u64, $a4 as u64, + ) + }; + ($nr:expr, $a0:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr, $a5:expr) => { + $crate::syscall::syscall6( + $nr as u64, $a0 as u64, $a1 as u64, $a2 as u64, $a3 as u64, $a4 as u64, $a5 as u64, + ) + }; +} + +#[inline(always)] +pub unsafe fn syscall0(nr: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + lateout("rcx") _, + lateout("r11") _, + lateout("rdx") _, + lateout("rsi") _, + lateout("rdi") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + ret +} + +#[inline(always)] +pub unsafe fn syscall1(nr: u64, a0: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + inlateout("rdi") a0 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("rdx") _, + lateout("rsi") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + ret +} + +#[inline(always)] +pub unsafe fn syscall2(nr: u64, a0: u64, a1: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("rdx") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + ret +} + +#[inline(always)] +pub unsafe fn syscall3(nr: u64, a0: u64, a1: u64, a2: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + inlateout("rdx") a2 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + ret +} + +#[inline(always)] +pub unsafe fn syscall4(nr: u64, a0: u64, a1: u64, a2: u64, a3: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + inlateout("rdx") a2 => _, + inlateout("r10") a3 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("r8") _, + lateout("r9") _, + options(nostack, preserves_flags), + ); + ret +} + +#[inline(always)] +pub unsafe fn syscall5(nr: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + inlateout("rdx") a2 => _, + inlateout("r10") a3 => _, + inlateout("r8") a4 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("r9") _, + options(nostack, preserves_flags), + ); + ret +} + +#[inline(always)] +pub unsafe fn syscall6(nr: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 { + let ret: i64; + core::arch::asm!( + "syscall", + inlateout("rax") nr as i64 => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + inlateout("rdx") a2 => _, + inlateout("r10") a3 => _, + inlateout("r8") a4 => _, + inlateout("r9") a5 => _, + lateout("rcx") _, + lateout("r11") _, + options(nostack, preserves_flags), + ); + ret +}