feat: add Rust userspace support; fix TLS layout for p_align < 16; kill whole process on sibling-thread faults
This commit is contained in:
1 parent
a81298fb24
commit
f8b8f9610f
31 files changed
+1076
-38
No files matched your search
@@ -0,0 +1,15 @@
|
||||
# Phase 1 Rust support: no_std + alloc on a stock nightly, built with
|
||||
# -Zbuild-std against the custom target spec next to this directory.
|
||||
# Paths below are relative to programs/rust/ (the parent of .cargo/).
|
||||
|
||||
[build]
|
||||
target = "x86_64-unknown-montauk.json"
|
||||
|
||||
[unstable]
|
||||
build-std = ["core", "alloc"]
|
||||
json-target-spec = true
|
||||
|
||||
[target.x86_64-unknown-montauk]
|
||||
# The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and
|
||||
# links the static non-PIE ET_EXEC layout the kernel ELF loader expects.
|
||||
linker = "../../toolchain/local/bin/x86_64-montauk-gcc"
|
||||
@@ -0,0 +1 @@
|
||||
target/
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "montauk-rt"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "rusthello"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"montauk-rt",
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["montauk-rt", "rusthello"]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
debug = false
|
||||
@@ -0,0 +1,70 @@
|
||||
# Rust on MontaukOS
|
||||
|
||||
Phase 1: `no_std` + `alloc` programs, cross-compiled from Linux with a
|
||||
stock nightly toolchain and a custom target spec. No compiler fork.
|
||||
|
||||
## Setup (once)
|
||||
|
||||
```bash
|
||||
curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
|
||||
~/.cargo/bin/rustup toolchain install nightly --component rust-src
|
||||
```
|
||||
|
||||
`rust-toolchain.toml` selects nightly + `rust-src` for this directory.
|
||||
`make programs` builds the Rust programs when that toolchain is present
|
||||
and prints a notice and skips them otherwise. It never downloads a toolchain
|
||||
(`RUSTUP_AUTO_INSTALL=0`).
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make -C programs rust # cargo build --release, copies to bin/os/*.elf
|
||||
cd programs/rust && cargo build --release
|
||||
```
|
||||
|
||||
Output: `target/x86_64-unknown-montauk/release/<name>`, installed as
|
||||
`programs/bin/os/<name>.elf`. Add new binaries to `RUST_PROGRAMS` in
|
||||
`programs/GNUmakefile` and to the workspace `members`.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `x86_64-unknown-montauk.json` | Target spec: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort |
|
||||
| `.cargo/config.toml` | `-Zbuild-std=core,alloc`, linker = `x86_64-montauk-gcc` |
|
||||
| `montauk-rt/` | Runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
|
||||
| `rusthello/` | Smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`) |
|
||||
|
||||
The GCC driver links crt1/crti/crtbegin, the libc and libgcc from
|
||||
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
|
||||
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
|
||||
|
||||
Syscall numbers and status codes are generated at build time by
|
||||
`montauk-rt/build.rs` from `programs/include/Api/Syscall.hpp` and
|
||||
`kernel/src/Common/StatusCodes.hpp`, so there is nothing to keep in sync by hand.
|
||||
|
||||
## Writing a program
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
extern crate alloc;
|
||||
|
||||
montauk_rt::entry!(main);
|
||||
|
||||
fn main() -> i32 {
|
||||
montauk_rt::println!("hello from Rust");
|
||||
0
|
||||
}
|
||||
```
|
||||
|
||||
## Limits (Phase 1)
|
||||
|
||||
- No `std`. No `std::fs`, `std::net`, `std::process` or `std::thread`; use
|
||||
`montauk_rt` equivalents where they exist.
|
||||
- `panic = "abort"`. A panic on any thread prints the message and kills the
|
||||
whole process.
|
||||
- Thread stacks are owned by the `JoinHandle`. An unjoined thread leaks its stack.
|
||||
|
||||
Phase 2 (a forked rustc at `~/dev/mtk/rust` with a native `target_os =
|
||||
"montauk"` std) replaces the JSON spec with a built-in target.
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "montauk-rt"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Minimal no_std runtime for Rust programs on MontaukOS"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
@@ -0,0 +1,65 @@
|
||||
// Generate syscall numbers and status codes from the C++ headers so Rust
|
||||
// never becomes another hand-maintained copy of the syscall table.
|
||||
//
|
||||
// programs/include/Api/Syscall.hpp -> pub const SYS_*: u64
|
||||
// kernel/src/Common/StatusCodes.hpp -> pub enum Status (positive values,
|
||||
// matching montauk/statuscodes.h)
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let repo = manifest.join("../../..");
|
||||
let syscall_hpp = repo.join("programs/include/Api/Syscall.hpp");
|
||||
let status_hpp = repo.join("kernel/src/Common/StatusCodes.hpp");
|
||||
println!("cargo:rerun-if-changed={}", syscall_hpp.display());
|
||||
println!("cargo:rerun-if-changed={}", status_hpp.display());
|
||||
|
||||
let mut out = String::from("// @generated by montauk-rt/build.rs - do not edit.\n\n");
|
||||
|
||||
// static constexpr uint64_t SYS_NAME = 123; (decimal literals only)
|
||||
let syscalls = fs::read_to_string(&syscall_hpp).expect("read Syscall.hpp");
|
||||
let mut count = 0;
|
||||
for line in syscalls.lines() {
|
||||
let line = line.trim();
|
||||
let Some(rest) = line.strip_prefix("static constexpr uint64_t SYS_") else { continue };
|
||||
let Some((name, value)) = rest.split_once('=') else { continue };
|
||||
let value = value.split(';').next().unwrap_or("").trim();
|
||||
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
out.push_str(&format!("pub const SYS_{}: u64 = {};\n", name.trim(), value));
|
||||
count += 1;
|
||||
}
|
||||
assert!(count > 100, "parsed only {count} syscalls from {}", syscall_hpp.display());
|
||||
|
||||
// enum StatusCodes { Success = 0, GenericFailure = -1, ... };
|
||||
let status = fs::read_to_string(&status_hpp).expect("read StatusCodes.hpp");
|
||||
let body = status
|
||||
.split_once("enum StatusCodes")
|
||||
.and_then(|(_, rest)| rest.split_once('{'))
|
||||
.and_then(|(_, rest)| rest.split_once('}'))
|
||||
.map(|(body, _)| body)
|
||||
.expect("enum StatusCodes not found");
|
||||
out.push_str("\n/// Kernel status codes, stored positive like montauk/statuscodes.h.\n");
|
||||
out.push_str("/// The kernel returns them negated; see [`Status::from_result`].\n");
|
||||
out.push_str("#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Status {\n");
|
||||
let mut variants = Vec::new();
|
||||
for entry in body.split(',') {
|
||||
let entry = entry.split("//").next().unwrap_or("").trim();
|
||||
let Some((name, value)) = entry.split_once('=') else { continue };
|
||||
let value: i32 = value.trim().parse().expect("status value");
|
||||
out.push_str(&format!(" {} = {},\n", name.trim(), -value));
|
||||
variants.push((name.trim().to_string(), -value));
|
||||
}
|
||||
out.push_str("}\n\nimpl Status {\n pub const fn from_code(code: i32) -> Status {\n match code {\n");
|
||||
for (name, value) in &variants {
|
||||
out.push_str(&format!(" {} => Status::{},\n", value, name));
|
||||
}
|
||||
out.push_str(" _ => Status::GenericFailure,\n }\n }\n}\n");
|
||||
|
||||
let dest = PathBuf::from(env::var("OUT_DIR").unwrap()).join("abi.rs");
|
||||
fs::write(dest, out).expect("write abi.rs");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Command-line arguments, captured from crt1.o's argv by [`crate::entry!`].
|
||||
|
||||
use core::ffi::CStr;
|
||||
use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
|
||||
|
||||
static ARGC: AtomicUsize = AtomicUsize::new(0);
|
||||
static ARGV: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut());
|
||||
|
||||
/// # Safety
|
||||
/// `argv` must point to `argc` NUL-terminated strings that live forever.
|
||||
#[doc(hidden)]
|
||||
pub unsafe fn init(argc: i32, argv: *const *const u8) {
|
||||
ARGV.store(argv as *mut *const u8, Ordering::Relaxed);
|
||||
ARGC.store(argc.max(0) as usize, Ordering::Release);
|
||||
}
|
||||
|
||||
/// Iterator over the program arguments; argv[0] is the executable path.
|
||||
/// Arguments that are not valid UTF-8 are yielded lossily as `"?"`.
|
||||
pub fn args() -> Args {
|
||||
Args { next: 0 }
|
||||
}
|
||||
|
||||
pub struct Args {
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl Iterator for Args {
|
||||
type Item = &'static str;
|
||||
|
||||
fn next(&mut self) -> Option<&'static str> {
|
||||
let argc = ARGC.load(Ordering::Acquire);
|
||||
if self.next >= argc {
|
||||
return None;
|
||||
}
|
||||
let argv = ARGV.load(Ordering::Relaxed);
|
||||
// SAFETY: init() guarantees argc valid, immortal C strings.
|
||||
let s = unsafe { CStr::from_ptr(*argv.add(self.next) as *const core::ffi::c_char) };
|
||||
self.next += 1;
|
||||
Some(s.to_str().unwrap_or("?"))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let left = ARGC.load(Ordering::Acquire).saturating_sub(self.next);
|
||||
(left, Some(left))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for Args {}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Global allocator over the Montauk libc heap (process-wide locked, so
|
||||
//! safe across threads). libc malloc returns 16-byte aligned blocks; larger
|
||||
//! alignments over-allocate and stash the original pointer just below the
|
||||
//! aligned block.
|
||||
|
||||
use core::alloc::{GlobalAlloc, Layout};
|
||||
use core::ptr;
|
||||
|
||||
unsafe extern "C" {
|
||||
fn malloc(size: usize) -> *mut u8;
|
||||
fn free(ptr: *mut u8);
|
||||
fn realloc(ptr: *mut u8, size: usize) -> *mut u8;
|
||||
}
|
||||
|
||||
const MALLOC_ALIGN: usize = 16;
|
||||
|
||||
pub struct LibcAllocator;
|
||||
|
||||
unsafe impl GlobalAlloc for LibcAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
unsafe {
|
||||
if layout.align() <= MALLOC_ALIGN {
|
||||
return malloc(layout.size().max(1));
|
||||
}
|
||||
let Some(total) = layout.size().checked_add(layout.align() + size_of::<usize>()) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let raw = malloc(total);
|
||||
if raw.is_null() {
|
||||
return raw;
|
||||
}
|
||||
let start = raw as usize + size_of::<usize>();
|
||||
let aligned = (start + layout.align() - 1) & !(layout.align() - 1);
|
||||
*((aligned - size_of::<usize>()) as *mut usize) = raw as usize;
|
||||
aligned as *mut u8
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
|
||||
unsafe {
|
||||
if layout.align() <= MALLOC_ALIGN {
|
||||
free(p);
|
||||
} else {
|
||||
free(*((p as usize - size_of::<usize>()) as *const usize) as *mut u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn realloc(&self, p: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
if layout.align() <= MALLOC_ALIGN {
|
||||
return realloc(p, new_size.max(1));
|
||||
}
|
||||
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
|
||||
let q = self.alloc(new_layout);
|
||||
if !q.is_null() {
|
||||
ptr::copy_nonoverlapping(p, q, layout.size().min(new_size));
|
||||
self.dealloc(p, layout);
|
||||
}
|
||||
q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: LibcAllocator = LibcAllocator;
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Terminal output. `print!`/`println!` format into a stack buffer and hand
|
||||
//! it to SYS_PRINT in NUL-terminated chunks, so one call is one (or a few)
|
||||
//! syscalls rather than libc write()'s one SYS_PUTCHAR per byte.
|
||||
|
||||
use core::fmt::{self, Write};
|
||||
|
||||
use crate::sys;
|
||||
|
||||
const CHUNK: usize = 256;
|
||||
|
||||
/// Buffered writer to the process's terminal (stdout).
|
||||
pub struct Stdout {
|
||||
buf: [u8; CHUNK],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Stdout {
|
||||
pub const fn new() -> Self {
|
||||
Stdout { buf: [0; CHUNK], len: 0 }
|
||||
}
|
||||
|
||||
pub fn write_bytes(&mut self, bytes: &[u8]) {
|
||||
for &b in bytes {
|
||||
if b == 0 {
|
||||
// SYS_PRINT stops at NUL; send it on its own.
|
||||
self.flush();
|
||||
unsafe { sys::syscall1(sys::SYS_PUTCHAR, 0) };
|
||||
continue;
|
||||
}
|
||||
if self.len == CHUNK - 1 {
|
||||
self.flush();
|
||||
}
|
||||
self.buf[self.len] = b;
|
||||
self.len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) {
|
||||
if self.len == 0 {
|
||||
return;
|
||||
}
|
||||
self.buf[self.len] = 0;
|
||||
unsafe { sys::syscall1(sys::SYS_PRINT, self.buf.as_ptr() as u64) };
|
||||
self.len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Stdout {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Stdout {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write_bytes(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Stdout {
|
||||
fn drop(&mut self) {
|
||||
self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
let mut out = Stdout::new();
|
||||
let _ = out.write_fmt(args);
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => { $crate::io::_print(format_args!($($arg)*)) };
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => { $crate::print!("\n") };
|
||||
($($arg:tt)*) => { $crate::io::_print(format_args!("{}\n", format_args!($($arg)*))) };
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
|
||||
//!
|
||||
//! Phase 1 of Rust support: `core` + `alloc` on a stock nightly with a
|
||||
//! custom target spec. Programs link against the Montauk libc through the
|
||||
//! x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, environment,
|
||||
//! .init_array) and calls the `main` that [`entry!`] defines.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #![no_std]
|
||||
//! #![no_main]
|
||||
//! montauk_rt::entry!(main);
|
||||
//! fn main() -> i32 { montauk_rt::println!("hi"); 0 }
|
||||
//! ```
|
||||
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub mod sys;
|
||||
pub mod io;
|
||||
pub mod heap;
|
||||
pub mod env;
|
||||
pub mod process;
|
||||
pub mod sync;
|
||||
pub mod thread;
|
||||
pub mod time;
|
||||
|
||||
mod panic;
|
||||
|
||||
pub use sys::Status;
|
||||
|
||||
/// Define the C `main` that crt1.o calls, forwarding to `fn() -> i32`.
|
||||
#[macro_export]
|
||||
macro_rules! entry {
|
||||
($main:path) => {
|
||||
const _: () = {
|
||||
// Exported as the C symbol `main`; the Rust name stays free so
|
||||
// the program can call its own entry function `main`.
|
||||
#[unsafe(export_name = "main")]
|
||||
extern "C" fn __montauk_rt_main(argc: i32, argv: *const *const u8) -> i32 {
|
||||
// SAFETY: crt1.o passes its static argv table, which lives
|
||||
// for the whole program and is never modified after startup.
|
||||
unsafe { $crate::env::init(argc, argv) };
|
||||
let f: fn() -> i32 = $main;
|
||||
f()
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use core::fmt::Write;
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
let mut out = crate::io::Stdout::new();
|
||||
let _ = write!(out, "\nthread {} panicked", crate::thread::current_id());
|
||||
if let Some(loc) = info.location() {
|
||||
let _ = write!(out, " at {}:{}:{}", loc.file(), loc.line(), loc.column());
|
||||
}
|
||||
let _ = writeln!(out, ":\n{}", info.message());
|
||||
out.flush();
|
||||
crate::process::abort()
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Process control.
|
||||
|
||||
use crate::sys;
|
||||
|
||||
unsafe extern "C" {
|
||||
// libc exit(): runs atexit handlers (and .fini_array), then SYS_EXIT.
|
||||
#[link_name = "exit"]
|
||||
fn libc_exit(code: i32) -> !;
|
||||
}
|
||||
|
||||
/// Exit the process. From a sibling thread this ends only that thread
|
||||
/// (kernel semantics); use [`abort`] to take the whole process down.
|
||||
pub fn exit(code: i32) -> ! {
|
||||
unsafe { libc_exit(code) }
|
||||
}
|
||||
|
||||
pub fn id() -> u32 {
|
||||
unsafe { sys::syscall0(sys::SYS_GETPID) as u32 }
|
||||
}
|
||||
|
||||
/// Terminate the whole process from any thread.
|
||||
pub fn abort() -> ! {
|
||||
unsafe {
|
||||
if crate::thread::current_id() == id() {
|
||||
sys::syscall1(sys::SYS_EXIT, 134);
|
||||
} else {
|
||||
// Sibling: SYS_EXIT would end only this thread. Self-kill flags
|
||||
// every thread; park here until the kernel reaps us.
|
||||
sys::syscall1(sys::SYS_KILL, id() as u64);
|
||||
}
|
||||
loop {
|
||||
sys::syscall0(sys::SYS_YIELD);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Futex-backed synchronization (SYS_FUTEX_WAIT / SYS_FUTEX_WAKE).
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
use core::time::Duration;
|
||||
|
||||
use crate::sys::{self, Status};
|
||||
|
||||
/// Block while `*word == expected`. `Ok(())` means woken by [`futex_wake`];
|
||||
/// `Err(WouldBlock)` means the word had already changed; `Err(TimedOut)`
|
||||
/// means the deadline passed. Always re-check your condition afterwards.
|
||||
pub fn futex_wait(word: &AtomicU32, expected: u32, timeout: Option<Duration>) -> Result<(), Status> {
|
||||
let ns = match timeout {
|
||||
None => sys::FUTEX_WAIT_FOREVER,
|
||||
Some(d) => d.as_nanos().min((sys::FUTEX_WAIT_FOREVER - 1) as u128) as u64,
|
||||
};
|
||||
let rc = unsafe {
|
||||
sys::syscall3(sys::SYS_FUTEX_WAIT, word.as_ptr() as u64, expected as u64, ns)
|
||||
};
|
||||
Status::from_result(rc).map(|_| ())
|
||||
}
|
||||
|
||||
/// Wake up to `count` waiters on `word`, longest-waiting first.
|
||||
/// Returns how many were woken.
|
||||
pub fn futex_wake(word: &AtomicU32, count: u32) -> u32 {
|
||||
let rc = unsafe { sys::syscall2(sys::SYS_FUTEX_WAKE, word.as_ptr() as u64, count as u64) };
|
||||
Status::from_result(rc).unwrap_or(0) as u32
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Mutex: Drepper's three-state futex mutex ("Futexes Are Tricky", #3).
|
||||
// 0 = unlocked, 1 = locked, 2 = locked with possible waiters.
|
||||
// ====================================================================
|
||||
|
||||
pub struct Mutex<T: ?Sized> {
|
||||
state: AtomicU32,
|
||||
data: UnsafeCell<T>,
|
||||
}
|
||||
|
||||
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
|
||||
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
|
||||
|
||||
impl<T> Mutex<T> {
|
||||
pub const fn new(value: T) -> Self {
|
||||
Mutex { state: AtomicU32::new(0), data: UnsafeCell::new(value) }
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.data.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Mutex<T> {
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err() {
|
||||
self.lock_contended();
|
||||
}
|
||||
MutexGuard { mutex: self }
|
||||
}
|
||||
|
||||
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
|
||||
self.state
|
||||
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.ok()
|
||||
.map(|_| MutexGuard { mutex: self })
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn lock_contended(&self) {
|
||||
// Brief spin first: most critical sections are short.
|
||||
for _ in 0..100 {
|
||||
if self.state.load(Ordering::Relaxed) == 0
|
||||
&& self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
core::hint::spin_loop();
|
||||
}
|
||||
while self.state.swap(2, Ordering::Acquire) != 0 {
|
||||
let _ = futex_wait(&self.state, 2, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn unlock(&self) {
|
||||
if self.state.swap(0, Ordering::Release) == 2 {
|
||||
futex_wake(&self.state, 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
self.data.get_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Mutex<T> {
|
||||
fn default() -> Self {
|
||||
Mutex::new(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MutexGuard<'a, T: ?Sized> {
|
||||
mutex: &'a Mutex<T>,
|
||||
}
|
||||
|
||||
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}
|
||||
|
||||
impl<T: ?Sized> Deref for MutexGuard<'_, T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
unsafe { &*self.mutex.data.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
unsafe { &mut *self.mutex.data.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.mutex.unlock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Raw syscall interface. Numbers and status codes are generated from
|
||||
//! programs/include/Api/Syscall.hpp and kernel/src/Common/StatusCodes.hpp.
|
||||
|
||||
use core::arch::asm;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/abi.rs"));
|
||||
|
||||
/// `SYS_FUTEX_WAIT` timeout meaning "no deadline".
|
||||
pub const FUTEX_WAIT_FOREVER: u64 = !0;
|
||||
/// `SYS_FUTEX_WAKE` count meaning "every waiter".
|
||||
pub const FUTEX_WAKE_ALL: u32 = u32::MAX;
|
||||
|
||||
impl Status {
|
||||
/// Convert a raw syscall result: negative values are negated status
|
||||
/// codes, everything else (handles, counts, sizes) is success.
|
||||
pub fn from_result(rc: i64) -> Result<u64, Status> {
|
||||
if rc >= 0 {
|
||||
Ok(rc as u64)
|
||||
} else if rc < i32::MIN as i64 {
|
||||
Err(Status::GenericFailure)
|
||||
} else {
|
||||
Err(Status::from_code(-(rc as i32)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The kernel's SYSCALL path clobbers rcx/r11 (return RIP/RFLAGS) and, like
|
||||
// the C++ wrappers in montauk/syscall.h, is treated as clobbering every
|
||||
// argument register.
|
||||
|
||||
#[inline(always)]
|
||||
pub unsafe fn syscall0(nr: u64) -> i64 {
|
||||
let ret: i64;
|
||||
unsafe {
|
||||
asm!("syscall", inlateout("rax") nr as i64 => ret,
|
||||
out("rcx") _, out("r11") _, out("rdi") _, out("rsi") _, out("rdx") _,
|
||||
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub unsafe fn syscall1(nr: u64, a1: u64) -> i64 {
|
||||
let ret: i64;
|
||||
unsafe {
|
||||
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
|
||||
out("rcx") _, out("r11") _, out("rsi") _, out("rdx") _,
|
||||
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub unsafe fn syscall2(nr: u64, a1: u64, a2: u64) -> i64 {
|
||||
let ret: i64;
|
||||
unsafe {
|
||||
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
|
||||
inlateout("rsi") a2 => _,
|
||||
out("rcx") _, out("r11") _, out("rdx") _,
|
||||
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub unsafe fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {
|
||||
let ret: i64;
|
||||
unsafe {
|
||||
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
|
||||
inlateout("rsi") a2 => _, inlateout("rdx") a3 => _,
|
||||
out("rcx") _, out("r11") _,
|
||||
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||
}
|
||||
ret
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Threads (SYS_THREAD_SPAWN / SYS_THREAD_JOIN).
|
||||
//!
|
||||
//! The kernel uses the caller's stack verbatim and delivers one argument in
|
||||
//! %rdi. A thread cannot free the stack it runs on, so the [`JoinHandle`]
|
||||
//! owns it and frees it after a successful join; dropping an unjoined
|
||||
//! handle leaks the stack (the thread may still be running on it).
|
||||
|
||||
use alloc::alloc::{alloc, dealloc, Layout};
|
||||
use alloc::boxed::Box;
|
||||
|
||||
use crate::sys::{self, Status};
|
||||
|
||||
pub const DEFAULT_STACK_SIZE: usize = 256 * 1024;
|
||||
const STACK_ALIGN: usize = 16;
|
||||
|
||||
pub struct JoinHandle {
|
||||
tid: u32,
|
||||
stack: *mut u8,
|
||||
stack_layout: Layout,
|
||||
}
|
||||
|
||||
unsafe impl Send for JoinHandle {}
|
||||
|
||||
impl JoinHandle {
|
||||
pub fn id(&self) -> u32 {
|
||||
self.tid
|
||||
}
|
||||
|
||||
/// Wait for the thread to finish and return its exit code.
|
||||
pub fn join(self) -> Result<i32, Status> {
|
||||
let mut code: i32 = 0;
|
||||
let rc = unsafe {
|
||||
sys::syscall2(sys::SYS_THREAD_JOIN, self.tid as u64, &mut code as *mut i32 as u64)
|
||||
};
|
||||
// The kernel reports a bad tid as -1 (GenericFailure).
|
||||
Status::from_result(rc)?;
|
||||
unsafe { dealloc(self.stack, self.stack_layout) };
|
||||
Ok(code)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a thread running `f`; its return value becomes the exit code.
|
||||
pub fn spawn<F>(f: F) -> Result<JoinHandle, Status>
|
||||
where
|
||||
F: FnOnce() -> i32 + Send + 'static,
|
||||
{
|
||||
spawn_with_stack(DEFAULT_STACK_SIZE, f)
|
||||
}
|
||||
|
||||
pub fn spawn_with_stack<F>(stack_size: usize, f: F) -> Result<JoinHandle, Status>
|
||||
where
|
||||
F: FnOnce() -> i32 + Send + 'static,
|
||||
{
|
||||
let stack_layout = Layout::from_size_align(stack_size.max(4096), STACK_ALIGN)
|
||||
.map_err(|_| Status::InvalidArgument)?;
|
||||
let stack = unsafe { alloc(stack_layout) };
|
||||
if stack.is_null() {
|
||||
return Err(Status::OutOfMemory);
|
||||
}
|
||||
let closure = Box::into_raw(Box::new(f));
|
||||
let top = (stack as usize + stack_layout.size()) & !(STACK_ALIGN - 1);
|
||||
|
||||
let rc = unsafe {
|
||||
sys::syscall3(sys::SYS_THREAD_SPAWN, trampoline::<F> as *const () as u64, closure as u64, top as u64)
|
||||
};
|
||||
match Status::from_result(rc) {
|
||||
Ok(tid) => Ok(JoinHandle { tid: tid as u32, stack, stack_layout }),
|
||||
Err(e) => unsafe {
|
||||
drop(Box::from_raw(closure));
|
||||
dealloc(stack, stack_layout);
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn trampoline<F>(arg: *mut F) -> !
|
||||
where
|
||||
F: FnOnce() -> i32,
|
||||
{
|
||||
let f = unsafe { Box::from_raw(arg) };
|
||||
let code = f();
|
||||
unsafe { sys::syscall1(sys::SYS_THREAD_EXIT, code as u32 as u64) };
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// The calling thread's id (equals the process id on the main thread).
|
||||
pub fn current_id() -> u32 {
|
||||
unsafe { sys::syscall0(sys::SYS_THREAD_SELF) as u32 }
|
||||
}
|
||||
|
||||
pub fn yield_now() {
|
||||
unsafe { sys::syscall0(sys::SYS_YIELD) };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//! Time and sleeping.
|
||||
|
||||
use core::time::Duration;
|
||||
|
||||
use crate::sys;
|
||||
|
||||
/// Milliseconds since boot (monotonic).
|
||||
pub fn uptime_ms() -> u64 {
|
||||
unsafe { sys::syscall0(sys::SYS_GETMILLISECONDS) as u64 }
|
||||
}
|
||||
|
||||
/// Sleep for at least `d`, rounded up to the 1 ms scheduler tick.
|
||||
pub fn sleep(d: Duration) {
|
||||
let ms = d.as_millis().min(u64::MAX as u128) as u64 + u64::from(d.subsec_nanos() % 1_000_000 != 0);
|
||||
if ms > 0 {
|
||||
unsafe { sys::syscall1(sys::SYS_SLEEP_MS, ms) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src"]
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "rusthello"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
montauk-rt = { path = "../montauk-rt" }
|
||||
@@ -0,0 +1,148 @@
|
||||
//! rusthello - Phase 1 Rust smoke test for MontaukOS.
|
||||
//!
|
||||
//! Exercises alloc collections, formatting, argv, a #[thread_local]
|
||||
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
|
||||
//! `rusthello panic` / `rusthello panic-thread` check that a panic on
|
||||
//! either thread takes the whole process down; `rusthello fault-thread`
|
||||
//! checks the same for a page fault on a worker thread.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![feature(thread_local)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::Cell;
|
||||
use core::time::Duration;
|
||||
|
||||
use montauk_rt::sync::Mutex;
|
||||
use montauk_rt::{entry, println, thread, time};
|
||||
|
||||
entry!(main);
|
||||
|
||||
static mut FAILURES: u32 = 0;
|
||||
|
||||
fn check(ok: bool, name: &str) {
|
||||
println!("{} {}", if ok { "PASS" } else { "FAIL" }, name);
|
||||
if !ok {
|
||||
unsafe { FAILURES += 1 };
|
||||
}
|
||||
}
|
||||
|
||||
#[thread_local]
|
||||
static TLS_COUNTER: Cell<u64> = Cell::new(7);
|
||||
|
||||
const THREADS: usize = 4;
|
||||
const ITERS: u64 = 20_000;
|
||||
|
||||
fn main() -> i32 {
|
||||
println!("rusthello: Rust on MontaukOS (no_std + alloc)");
|
||||
|
||||
let args: Vec<&str> = montauk_rt::env::args().collect();
|
||||
println!("argv = {:?}", args);
|
||||
match args.get(1).copied() {
|
||||
Some("panic") => panic!("requested panic on the main thread"),
|
||||
Some("fault-thread") => {
|
||||
// A CPU fault (not a panic) on a worker must still kill the
|
||||
// whole process, with the SIGSEGV exit code (267).
|
||||
let h = thread::spawn(|| {
|
||||
unsafe { core::ptr::write_volatile(8 as *mut u64, 0xdead) };
|
||||
0
|
||||
})
|
||||
.unwrap();
|
||||
let _ = h.join();
|
||||
thread::yield_now();
|
||||
println!("FAIL process survived a worker-thread fault");
|
||||
return 1;
|
||||
}
|
||||
Some("panic-thread") => {
|
||||
let h = thread::spawn(|| panic!("requested panic on a worker thread")).unwrap();
|
||||
let _ = h.join();
|
||||
println!("FAIL process survived a worker-thread panic");
|
||||
return 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// --- alloc ---
|
||||
let mut v: Vec<u64> = (1..=100).collect();
|
||||
v.retain(|x| x % 3 == 0);
|
||||
check(v.len() == 33 && v.iter().sum::<u64>() == 1683, "Vec collect/retain/sum");
|
||||
|
||||
let mut s = String::new();
|
||||
for word in ["Montauk", "speaks", "Rust"] {
|
||||
if !s.is_empty() {
|
||||
s.push(' ');
|
||||
}
|
||||
s.push_str(word);
|
||||
}
|
||||
check(s == "Montauk speaks Rust", "String building");
|
||||
check(format!("{:>6.2}|{:#x}|{:?}", 3.14159f64, 255, Some("x")) == " 3.14|0xff|Some(\"x\")",
|
||||
"format! with floats, hex, Debug");
|
||||
|
||||
let mut map = BTreeMap::new();
|
||||
for (i, w) in s.split(' ').enumerate() {
|
||||
map.insert(w, i);
|
||||
}
|
||||
check(map.get("Rust") == Some(&2) && map.len() == 3, "BTreeMap");
|
||||
|
||||
let big: Vec<u8> = alloc::vec![0xA5; 1 << 20];
|
||||
check(big.iter().all(|&b| b == 0xA5), "1 MiB allocation");
|
||||
|
||||
#[repr(align(4096))]
|
||||
struct PageAligned([u8; 64]);
|
||||
let page = alloc::boxed::Box::new(PageAligned([1; 64]));
|
||||
check((&*page as *const PageAligned as usize) % 4096 == 0 && page.0[63] == 1, "over-aligned Box (4096)");
|
||||
|
||||
// --- thread-local storage ---
|
||||
check(TLS_COUNTER.get() == 7, "#[thread_local] initial value on main thread");
|
||||
TLS_COUNTER.set(1000);
|
||||
let tls_seen = thread::spawn(|| {
|
||||
let initial = TLS_COUNTER.get();
|
||||
TLS_COUNTER.set(55);
|
||||
initial as i32
|
||||
})
|
||||
.unwrap()
|
||||
.join();
|
||||
check(tls_seen == Ok(7), "worker thread gets a fresh TLS block");
|
||||
check(TLS_COUNTER.get() == 1000, "main thread TLS untouched by worker");
|
||||
|
||||
// --- threads + futex mutex ---
|
||||
let counter = Arc::new(Mutex::new(0u64));
|
||||
let mut handles = Vec::new();
|
||||
for t in 0..THREADS {
|
||||
let counter = counter.clone();
|
||||
handles.push(
|
||||
thread::spawn(move || {
|
||||
for i in 0..ITERS {
|
||||
let mut g = counter.lock();
|
||||
let v = *g;
|
||||
if i % 512 == 0 {
|
||||
thread::yield_now(); // widen the race window under the lock
|
||||
}
|
||||
*g = v + 1;
|
||||
}
|
||||
t as i32 + 10
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
let codes: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
|
||||
check(codes.iter().enumerate().all(|(t, c)| *c == Ok(t as i32 + 10)), "thread exit codes via join");
|
||||
check(*counter.lock() == THREADS as u64 * ITERS, "futex Mutex keeps the counter exact");
|
||||
|
||||
// --- time ---
|
||||
let t0 = time::uptime_ms();
|
||||
time::sleep(Duration::from_millis(25));
|
||||
let dt = time::uptime_ms() - t0;
|
||||
check((25..1000).contains(&dt), "sleep(25 ms)");
|
||||
|
||||
let failures = unsafe { FAILURES };
|
||||
println!("rusthello: {}", if failures == 0 { "all passed" } else { "FAILURES" });
|
||||
if failures == 0 { 0 } else { 1 }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"arch": "x86_64",
|
||||
"cpu": "x86-64",
|
||||
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
|
||||
"llvm-target": "x86_64-unknown-none-elf",
|
||||
"os": "montauk",
|
||||
"vendor": "unknown",
|
||||
"target-pointer-width": 64,
|
||||
"max-atomic-width": 64,
|
||||
"executables": true,
|
||||
"linker-flavor": "gnu-cc",
|
||||
"linker": "x86_64-montauk-gcc",
|
||||
"no-default-libraries": false,
|
||||
"crt-objects-fallback": "false",
|
||||
"relocation-model": "static",
|
||||
"code-model": "small",
|
||||
"position-independent-executables": false,
|
||||
"static-position-independent-executables": false,
|
||||
"relro-level": "off",
|
||||
"disable-redzone": true,
|
||||
"has-thread-local": true,
|
||||
"tls-model": "local-exec",
|
||||
"panic-strategy": "abort",
|
||||
"plt-by-default": false,
|
||||
"stack-probes": { "kind": "inline" },
|
||||
"metadata": {
|
||||
"description": "MontaukOS userspace (static ET_EXEC, Montauk libc)",
|
||||
"host_tools": false,
|
||||
"std": false,
|
||||
"tier": 3
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user