Overhaul RawSyscalls to work with ARM, -Zmiri-track-raw-pointers, the revised Yield, and Exit. 1. RawSyscalls now supports Yield's new return value semantics as well as the yield-no-wait variant of Yield. 2. RawSyscalls now supports Exit. 3. RawSyscalls is now usable in Miri with the `-Zmiri-track-raw-pointers` flag. 4. RawSyscalls can now be implemented on ARM: previously, `class` was a runtime value, but it needs to be an immediate value on ARM. 5. The explanation for the design of RawSyscalls was completely overhauled. Instead of listing a bunch of "design considerations" with no connection to the final design, it shows how testing and efficiency considerations incrementally lead to its design. The new design should be a bit more future-proof than the previous design as well.
diff --git a/core/platform/src/lib.rs b/core/platform/src/lib.rs index d57644b..78293e3 100644 --- a/core/platform/src/lib.rs +++ b/core/platform/src/lib.rs
@@ -1,3 +1,4 @@ +#![feature(min_const_generics)] // TODO: Remove when toolchain updated. #![no_std] mod async_traits; @@ -7,13 +8,15 @@ pub mod return_variant; mod syscalls; mod syscalls_impl; +mod yield_types; pub use async_traits::{CallbackContext, FreeCallback, Locator, MethodCallback}; pub use command_return::CommandReturn; pub use error_code::ErrorCode; -pub use raw_syscalls::{OneArgMemop, RawSyscalls, YieldType, ZeroArgMemop}; +pub use raw_syscalls::RawSyscalls; pub use return_variant::ReturnVariant; pub use syscalls::Syscalls; +pub use yield_types::YieldNoWaitReturn; #[cfg(test)] mod command_return_tests;
diff --git a/core/platform/src/raw_syscalls.rs b/core/platform/src/raw_syscalls.rs index f4e37b8..cd5115f 100644 --- a/core/platform/src/raw_syscalls.rs +++ b/core/platform/src/raw_syscalls.rs
@@ -3,98 +3,94 @@ /// `RawSyscalls` allows a fake Tock kernel to be injected into components for /// unit testing. It is implemented by `libtock_runtime::TockSyscalls` and -/// `libtock_unittest::FakeSyscalls`. **Components should not use `RawSyscalls` +/// `libtock_unittest::fake::Kernel`. **Components should not use `RawSyscalls` /// directly; instead, use the `Syscalls` trait, which provides higher-level /// interfaces to the system calls.** -// RawSyscalls is designed to minimize the amount of handwritten assembly code -// needed without generating unnecessary instructions. This comment describes -// the thought process that led to the choice of methods for RawSyscalls. There -// are a few major considerations affecting its design: +// The RawSyscalls trait is designed to minimize the complexity and size of its +// implementation, as its implementation is difficult to test (it cannot be used +// in unit tests, with sanitizers, or in Miri). It is also designed to minimize +// the number of unnecessary instructions it generates. // -// 1. Most system calls only clobber r0-r4 (*), while yield has a far longer -// clobber list. As such, yield must have its own assembly -// implementation. -// 2. The compiler is unable to optimize away unused arguments. For example, -// memop's "get process RAM start address" operation only needs r0 set, -// while memop's "break" operation needs both r0 and r1 set. If our -// inline assembly calls "get process RAM start address" but sets both r0 -// and r1, the compiler doesn't know that r1 will be -// ignored so setting that register will not be optimized away. Therefore -// we want to set the minimum number of argument registers possible. -// 3. The cost of specifying unused return registers is only that of -// unnecessarily marking a register as clobbered. Explanation: After -// inlining, an unused register is marked as "changed by the assembly" -// but can immediately be re-used by the compiler, which is the same as a -// clobbered register. System calls should generally be -// inlined -- and even if they aren't, the unused return values will -// probably be passed in caller-saved registers (this is true for the C -// ABI, so probably true for the Rust ABI), which are treated as -// clobbered regardless. +// Convention: This file uses the same register naming conventions as the Tock +// 2.0 syscall TRD. Registers r0-r4 correspond to ARM registers r0-r4 and RISC-V +// registers a0-a4. // -// (*) When this file refers to registers, it uses the same naming convention as -// the Tock 2.0 syscalls TRD. Registers r0-r4 correspond to ARM registers r0-r4 -// and RISC-V registers a0-a4. +// Theoretically, RawSyscalls could consist of a single raw system call. To +// start, something like this should work: // -// Currently, yield takes exactly one argument (to specify what yield type to -// do). Therefore we only need one raw yield call. +// unsafe fn syscall<const CLASS: usize>([usize; 4]) -> [usize; 4]; // -// Based on these considerations, it would make sense to have the following -// methods: -// yield -// zero_arg_syscall -// one_arg_syscall -// two_arg_syscall -// three_arg_syscall -// four_arg_syscall +// However, this will not work with Miri's -Zmiri-track-raw-pointers flag, as it +// causes pointers passed to the kernel via the Allow system calls to be +// untagged. In order to work with -Zmiri-track-raw-pointers, we need to pass +// pointers for the register values. Rust's closest analogue to C's void pointer +// is *mut () or *const (); we use *mut () because it is shorter: // -// However, there are no system calls that take 0 or 3 arguments, so we do not -// need the corresponding methods. This leaves yield, 1-arg, 2-arg, and 4-arg -// system calls. +// unsafe fn syscall<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4]; // -// The 1-arg and 2-arg system calls are only used for memop. Memop currently has -// the property that none of its operations can lead to undefined behavior. -// Therefore, we can rename the 1-arg and 2-arg system calls to zero_arg_memop -// and one_arg_memop and make them safe methods (the argument counts change -// because the number of system call arguments is one greater than the number of -// arguments passed to the specific op). +// Using a single system call has a major inefficiency. The single raw system +// call would need to clobber every register that any system call can clobber. +// Yield has a far longer clobber list than most system calls, so this would be +// inefficient for the majority of system calls. As a result, we can split yield +// out into its own function, giving the following API: // -// This only leaves four_arg_syscall, which is used to implement subscribe, -// command, read-write allow, and read-only allow. +// unsafe fn yield([*mut (); 4]) -> [*mut (); 4]; +// unsafe fn syscall<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4]; // -// Therefore the final design has 4 methods: -// yield -// zero_arg_memop -// one_arg_memop -// four_arg_syscall +// There is one significant inefficiency remaining. Many system calls, such as +// memop's "get RAM start address" operation, do not need to set all four +// arguments. The compiler cannot optimize away this inefficiency, so to remove +// it we need to split the system calls up based on the number of arguments they +// take: // -// If a new system call class that uses fewer than four arguments is added, then -// the above list will need to be revised. +// unsafe fn yield0([*mut (); 0]) -> [*mut (); 4]; +// unsafe fn yield1([*mut (); 1]) -> [*mut (); 4]; +// unsafe fn yield2([*mut (); 2]) -> [*mut (); 4]; +// unsafe fn yield3([*mut (); 3]) -> [*mut (); 4]; +// unsafe fn yield4([*mut (); 4]) -> [*mut (); 4]; +// unsafe fn syscall0<const CLASS: usize>([*mut (); 0]) -> [*mut (); 4]; +// unsafe fn syscall1<const CLASS: usize>([*mut (); 1]) -> [*mut (); 4]; +// unsafe fn syscall2<const CLASS: usize>([*mut (); 2]) -> [*mut (); 4]; +// unsafe fn syscall3<const CLASS: usize>([*mut (); 3]) -> [*mut (); 4]; +// unsafe fn syscall4<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4]; // -// Note that `command` always needs to use four_arg_syscall, even when calling a -// command with fewer arguments, because we don't want to leak the -// (possibly secret) values in the r2 and r3 registers to untrusted capsules. -// Yield and memop do not have this concern and can leave arbitrary data in r2 -// and r3, because they are implemented by the core kernel, which is trusted. +// However, not all of these are used! If we remove the system calls that are +// unused, we are left with the following: // -// The success type for Memop calls depends on the operation performed. However, -// all *currently defined* memop operations return either Success or Success -// with u32. Therefore, the memop implementations only need to mark r0 and r1 as -// clobbered, not r2 and r3. This choice of clobbers will need to be revisited -// if and when a memop operation that returns more data is added. +// unsafe fn yield1([*mut (); 1]) -> [*mut (); 4]; +// unsafe fn yield2([*mut (); 2]) -> [*mut (); 4]; +// unsafe fn syscall1<const CLASS: usize>([*mut (); 1]) -> [*mut (); 4]; +// unsafe fn syscall2<const CLASS: usize>([*mut (); 2]) -> [*mut (); 4]; +// unsafe fn syscall4<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4]; // -// The decision of where to use u32 and usize can be a bit tricky. The Tock -// syscall ABI is currently only specified for 32-bit systems, so on real Tock -// systems both types match the size of a register, but the unit test -// environment can be either 32 bit or 64 bit. This interface uses usize for -// values that can contain pointers, so that pointers are not truncated in the -// unit test environment. To keep types as consistent as possible, it uses u32 -// for all values that cannot be pointers. -pub trait RawSyscalls { - // raw_yield should: +// To avoid making the RawSyscalls implementation index into arrays, we replace +// the arrays in the input with multiple arguments. For symmetry, we also +// replace the output with a tuple of individual values. This gives: +// +// unsafe fn yield1(*mut ()) -> (*mut (), *mut (), *mut (), *mut ()); +// +// unsafe fn yield2(*mut (), *mut ()) -> (*mut (), *mut (), *mut (), *mut ()); +// +// unsafe fn syscall1<const CLASS: usize>(*mut ()) +// -> (*mut (), *mut (), *mut (), *mut ()); +// +// unsafe fn syscall2<const CLASS: usize>(*mut (), *mut ()) +// -> (*mut (), *mut (), *mut (), *mut ()); +// +// unsafe fn syscall4<const CLASS: usize>(*mut (), *mut (), *mut (), *mut ()) +// -> (*mut (), *mut (), *mut (), *mut ()); +// +// These system calls are refined further individually, which is documented on +// a per-function basis. +pub unsafe trait RawSyscalls { + // yield1 can only be used to call `yield-wait`, which does not have a + // return value. To simplify the assembly implementation, we remove its + // return value. + // + // yield1 should: // 1. Call syscall class 0 - // 2. Use register r0 for input and output as an inlateout register, - // passing in r0_in and returning its value. + // 2. Pass in r0 as an inlateout register. // 3. Mark all caller-saved registers as lateout clobbers. // 4. NOT provide any of the following options: // pure (yield has side effects) @@ -103,142 +99,111 @@ // preserves_flags (a callback can change flags) // noreturn (yield is expected to return) // nostack (a callback needs the stack) - // - // Design note: This is safe because the yield types that currently exist - // are safe. If an unsafe yield type is added, we will need to make - // raw_yield unsafe. Although raw_yield shouldn't be called by code outside - // this crate, it can be, so that is a backwards-incompatible change. We - // pass YieldType rather than a usize because if we used usize directly then - // this API becomes unsound if the kernel adds support for an unsafe yield - // type (or even one that takes one more argument). - /// `raw_yield` should only be called by `libtock_platform`. - fn raw_yield(r0_in: YieldType) -> u32; - - // four_arg_syscall is used to invoke the subscribe, command, read-write - // allow, and read-only allow system calls. - // - // four_arg_syscall's inline assembly should have the following properties: - // 1. Calls the syscall class specified by class - // 2. Passes r0-r3 in the corresponding registers as inlateout - // registers. Returns r0-r3 in order. - // 3. Does not mark any registers as clobbered. - // 4. Has all of the following options: - // preserves_flags (these system calls do not touch flags) - // nostack (these system calls do not touch the stack) - // 5. Does NOT have any of the following options: - // pure (these system calls have side effects) - // nomem (the compiler needs to write to globals before allow) - // readonly (rw allow can modify memory) - // noreturn (all these system calls are expected to return) - // - // Note that subscribe's application data argument can potentially contain a - // pointer, so r3 can contain a pointer (in addition to r1 and r2, which - // more obviously contain pointers for subscribe and memop). - // - // For subscribe(), the callback pointer should be either 0 (for the null - // callback) or an `unsafe extern fn(u32, u32, u32, usize)`. - /// `four_arg_syscall` should only be called by `libtock_platform`. - /// + /// `yield1` should only be called by `libtock_platform`. /// # Safety - /// `four_arg_syscall` must NOT be used to invoke yield. Otherwise, it has - /// the same safety invariants as the underlying system call, which varies - /// depending on the system call class. - unsafe fn four_arg_syscall( - r0: u32, - r1: u32, - r2: usize, - r3: usize, - class: u8, - ) -> (u32, usize, usize, usize); + /// yield1 may only be used for yield operations that do not return a value. + /// It is exactly as safe as the underlying system call. + unsafe fn yield1(r0: *mut ()); - // zero_arg_memop is used to invoke memop operations that do not accept an - // argument register. Because there are no memop commands that set r2 or r3, - // this only needs to return r0 and r1. + // yield2 can only be used to call `yield-no-wait`. `yield-no-wait` does not + // return any values, so to simplify the assembly we omit return arguments. + // + // yield2 should: + // 1. Call syscall class 0 + // 2. Pass in r0 and r1 as inlateout registers. + // 3. Mark all caller-saved registers as lateout clobbers. + // 4. NOT provide any of the following options: + // pure (yield has side effects) + // nomem (a callback can read + write globals) + // readonly (a callback can write globals) + // preserves_flags (a callback can change flags) + // noreturn (yield is expected to return) + // nostack (a callback needs the stack) + /// `yield2` should only be called by `libtock_platform`. + /// # Safety + /// yield2 may only be used for yield operations that do not return a value. + /// It has the same safety invariants as the underlying system call. + unsafe fn yield2(r0: *mut (), r1: *mut ()); + + // syscall1 is only used to invoke Memop operations. Because there are no + // Memop commands that set r2 or r3, raw_syscall1 only needs to return r0 + // and r1. // // Memop commands may panic in the unit test environment, as not all memop // calls can be sensibly implemented in that environment. // - // zero_arg_memop's inline assembly should have the following properties: - // 1. Calls syscall class 5 - // 2. Specifies r0 as an inlateout register, and r1 as a lateout - // register. - // 3. Does not mark any registers as clobbered. - // 4. Has all of the following options: + // syscall1 should: + // 1. Call the syscall class specified by CLASS. + // 2. Pass r0 as an inlateout register. + // 3. Specify r1 as a lateout register and return its value. + // 4. Not mark any registers as clobbered. + // 5. Have all of the following options: // preserves_flags // nostack // nomem (it is okay for the compiler to cache globals // across memop calls) - // 5. Does NOT have any of the following options: + // 6. NOT have any of the following options: // pure (two invocations of the same memop can return // different values) // readonly (incompatible with nomem) // noreturn - // - // Design note: like raw_yield, this is safe because memops that currently - // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so - // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3 - // -- this API doesn't become unsound. - /// `four_arg_syscall` should only be called by `libtock_platform`. - fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, usize); + /// `syscall1` should only be called by `libtock_platform`. + /// # Safety + /// This directly makes a system call. It can only be used for core kernel + /// system calls that accept 1 argument and only overwrite r0 and r1 on + /// return. It is unsafe any time the underlying system call is unsafe. + unsafe fn syscall1<const CLASS: usize>(r0: *mut ()) -> (*mut (), *mut ()); - // one_arg_memop is used to invoke memop operations that take an argument. - // Because there are no memop operations that set r2 or r3, this only needs - // to return r0 and r1. + // syscall2 is used to invoke Exit as well as Memop operations that take an + // argument. Memop does not currently use more than 2 registers for its + // return value, and Exit does not return, so syscall2 only returns 2 + // values. // - // one_arg_memop's inline assembly should: - // 1. Call syscall class 5 - // 2. Specify r0 and r1 as inlateout registers, and return (r0, r1) + // syscall2 should: + // 1. Call the syscall class specified by CLASS. + // 2. Pass r0 and r1 as inlateout registers. // 3. Not mark any registers as clobbered. // 4. Have all of the following options: // preserves_flags // nostack // nomem (the compiler can cache globals across memop // calls) - // 5. Does NOT have any of the following options: + // 5. NOT have any of the following options: // pure Two invocations of sbrk can return different values // readonly Incompatible with nomem // noreturn + /// `syscall2` should only be called by `libtock_platform`. + /// # Safety + /// `syscall2` directly makes a system call. It can only be used for core + /// kernel system calls that accept 2 arguments and only overwrite r0 and r1 + /// on return. It is unsafe any time the underlying system call is unsafe. + unsafe fn syscall2<const CLASS: usize>(r0: *mut (), r1: *mut ()) -> (*mut (), *mut ()); + + // syscall4 should: + // 1. Call the syscall class specified by CLASS. + // 2. Pass r0-r3 in the corresponding registers as inlateout registers. + // 3. Not mark any registers as clobbered. + // 4. Have all of the following options: + // preserves_flags (these system calls do not touch flags) + // nostack (these system calls do not touch the stack) + // 5. NOT have any of the following options: + // pure (these system calls have side effects) + // nomem (the compiler needs to write to globals before allow) + // readonly (rw allow can modify memory) + // noreturn (all these system calls are expected to return) // - // Design note: like raw_yield, this is safe because memops that currently - // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so - // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3 - // -- this API doesn't become unsound. - /// `four_arg_syscall` should only be called by `libtock_platform`. - fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (u32, usize); -} - -#[non_exhaustive] -#[repr(u32)] -pub enum OneArgMemop { - Brk = 0, - Sbrk = 1, - FlashRegionStart = 8, - FlashRegionEnd = 9, - SpecifyStackTop = 10, - SpecifyHeapStart = 11, - // Note: before adding new memop operations, make sure the assumptions in - // the design notes on `one_arg_memop` are valid for the new operation type. -} - -// TODO: When the numeric values (0 and 1) are assigned to the yield types, -// specify those values here. -#[non_exhaustive] -#[repr(u32)] -pub enum YieldType { - Wait, - NoWait, -} - -#[non_exhaustive] -#[repr(u32)] -pub enum ZeroArgMemop { - MemoryStart = 2, - MemoryEnd = 3, - FlashStart = 4, - FlashEnd = 5, - GrantStart = 6, - FlashRegions = 7, - // Note: before adding new memop operations, make sure the assumptions in - // the design notes on `zero_arg_memop` are valid for the new operation - // type. + // For subscribe(), the callback pointer should be either 0 (for the null + // callback) or an `unsafe extern fn(u32, u32, u32, Userdata)`. + /// `syscall4` should only be called by `libtock_platform`. + /// + /// # Safety + /// `syscall4` must NOT be used to invoke yield. Otherwise, it has the same + /// safety invariants as the underlying system call, which varies depending + /// on the system call class. + unsafe fn syscall4<const CLASS: usize>( + r0: *mut (), + r1: *mut (), + r2: *mut (), + r3: *mut (), + ) -> (*mut (), *mut (), *mut (), *mut ()); }
diff --git a/core/platform/src/syscalls.rs b/core/platform/src/syscalls.rs index 8d85380..d5ff0bc 100644 --- a/core/platform/src/syscalls.rs +++ b/core/platform/src/syscalls.rs
@@ -5,15 +5,15 @@ /// implemented for `libtock_runtime::TockSyscalls` and /// `libtock_unittest::FakeSyscalls` (by way of `RawSyscalls`). pub trait Syscalls { + /// Runs the next pending callback, if a callback is pending. Unlike + /// `yield_wait`, `yield_no_wait` returns immediately if no callback is + /// pending. + fn yield_no_wait() -> crate::YieldNoWaitReturn; + /// Puts the process to sleep until a callback becomes pending, invokes the /// callback, then returns. fn yield_wait(); - /// Runs the next pending callback, if a callback is pending. Unlike - /// `yield_wait`, `yield_no_wait` returns immediately if no callback is - /// pending. Returns true if a callback was executed, false otherwise. - fn yield_no_wait() -> bool; - // TODO: Add a subscribe interface. // TODO: Add a command interface.
diff --git a/core/platform/src/syscalls_impl.rs b/core/platform/src/syscalls_impl.rs index 506ad64..c86da40 100644 --- a/core/platform/src/syscalls_impl.rs +++ b/core/platform/src/syscalls_impl.rs
@@ -1,17 +1,40 @@ //! Implements `Syscalls` for all types that implement `RawSyscalls`. -use crate::{return_variant, RawSyscalls, Syscalls, YieldType}; +use crate::{RawSyscalls, Syscalls, YieldNoWaitReturn}; + +mod yield_op { + pub const NO_WAIT: u32 = 0; + pub const WAIT: u32 = 1; +} impl<S: RawSyscalls> Syscalls for S { // ------------------------------------------------------------------------- // Yield // ------------------------------------------------------------------------- - fn yield_wait() { - Self::raw_yield(YieldType::Wait); + fn yield_no_wait() -> YieldNoWaitReturn { + unsafe { + // flag can be uninitialized because it is not read before the yield + // system call, and the kernel promises to only write to it (not + // read it). + let mut flag = core::mem::MaybeUninit::<YieldNoWaitReturn>::uninit(); + + // flag is safe to write a YieldNoWaitReturn to, as guaranteed by + // MaybeUninit. + Self::yield2(yield_op::NO_WAIT as *mut (), flag.as_mut_ptr() as *mut ()); + + // yield-no-wait guarantees it sets (initializes) flag before + // returning. + flag.assume_init() + } } - fn yield_no_wait() -> bool { - Self::raw_yield(YieldType::NoWait) != return_variant::FAILURE.into() + fn yield_wait() { + // Safety: yield-wait does not return a value, which satisfies yield1's + // requirement. The yield-wait system call cannot trigger undefined + // behavior on its own in any other way. + unsafe { + Self::yield1(yield_op::WAIT as *mut ()); + } } }
diff --git a/core/platform/src/yield_types.rs b/core/platform/src/yield_types.rs new file mode 100644 index 0000000..12dd961 --- /dev/null +++ b/core/platform/src/yield_types.rs
@@ -0,0 +1,15 @@ +/// The return value from a yield_no_wait call. +// Calling yield-no-wait passes a *mut YieldNoWaitReturn to the kernel, which +// the kernel writes to. We cannot safely pass a `*mut bool` to the kernel, +// because the representation of `bool` in Rust is undefined (although it is +// likely false == 0, true == 1, based on `bool`'s conversions). Using *mut +// YieldNoWaitReturn rather than a *mut u8 allows the compiler to assume the +// kernel will never write a value other than 0 or 1 into the pointee. Assuming +// the likely representation of `bool`, this makes the conversion into `bool` +// free. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum YieldNoWaitReturn { + NoCallback = 0, + Callback = 1, +}
diff --git a/core/runtime/src/lib.rs b/core/runtime/src/lib.rs index 7e2e1ca..bd890ea 100644 --- a/core/runtime/src/lib.rs +++ b/core/runtime/src/lib.rs
@@ -20,6 +20,9 @@ #![feature(asm)] #![no_std] +#![warn(unsafe_op_in_unsafe_fn)] +// TODO: Remove after toolchain upgrade, these have been stabilized. +#![feature(min_const_generics, unsafe_block_in_unsafe_fn)] mod startup;
diff --git a/core/runtime/src/syscalls_impl_riscv.rs b/core/runtime/src/syscalls_impl_riscv.rs index 3e635e8..2ae90c2 100644 --- a/core/runtime/src/syscalls_impl_riscv.rs +++ b/core/runtime/src/syscalls_impl_riscv.rs
@@ -1,11 +1,12 @@ -use libtock_platform::{OneArgMemop, RawSyscalls, YieldType, ZeroArgMemop}; +use libtock_platform::RawSyscalls; -impl RawSyscalls for crate::TockSyscalls { - // This yield implementation is currently limited RISC-V versions without +unsafe impl RawSyscalls for crate::TockSyscalls { + // This yield implementation is currently limited to RISC-V versions without // floating-point registers, as it does not mark them clobbered. #[cfg(not(any(target_feature = "d", target_feature = "f")))] - fn raw_yield(r0_in: YieldType) -> u32 { - let mut r0 = r0_in as u32; + unsafe fn yield1(r0: *mut ()) { + // Safety: This matches the invariants required by the documentation on + // RawSyscalls::yield1 unsafe { asm!("ecall", // x0 is the zero register. @@ -16,14 +17,14 @@ lateout("x6") _, // t1 lateout("x7") _, // t2 // x8 and x9 are s0 and s1 and are callee-saved. - inlateout("x10") r0, // a0 - lateout("x11") _, // a1 - lateout("x12") _, // a2 - lateout("x13") _, // a3 - inlateout("x14") 0 => _, // a4 - lateout("x15") _, // a5 - lateout("x16") _, // a6 - lateout("x17") _, // a7 + inlateout("x10") r0 => _, // a0 + lateout("x11") _, // a1 + lateout("x12") _, // a2 + lateout("x13") _, // a3 + inlateout("x14") 0 => _, // a4 + lateout("x15") _, // a5 + lateout("x16") _, // a6 + lateout("x17") _, // a7 // x18-27 are s2-s11 and are callee-saved lateout("x28") _, // t3 lateout("x29") _, // t4 @@ -31,51 +32,88 @@ lateout("x31") _, // t6 ); } - r0 } - unsafe fn four_arg_syscall( - mut r0: u32, - mut r1: u32, - mut r2: usize, - mut r3: usize, - class: u8, - ) -> (u32, usize, usize, usize) { - asm!("ecall", - inlateout("a0") r0, - inlateout("a1") r1, - inlateout("a2") r2, - inlateout("a3") r3, - in("a4") class, - options(preserves_flags, nostack), - ); - (r0, r1 as usize, r2, r3) + // This yield implementation is currently limited to RISC-V versions without + // floating-point registers, as it does not mark them clobbered. + #[cfg(not(any(target_feature = "d", target_feature = "f")))] + unsafe fn yield2(r0: *mut (), r1: *mut ()) { + // Safety: This matches the invariants required by the documentation on + // RawSyscalls::yield2 + unsafe { + asm!("ecall", + // x0 is the zero register. + lateout("x1") _, // Return address + // x2-x4 are stack, global, and thread pointers. sp is + // callee-saved. + lateout("x5") _, // t0 + lateout("x6") _, // t1 + lateout("x7") _, // t2 + // x8 and x9 are s0 and s1 and are callee-saved. + inlateout("x10") r0 => _, // a0 + inlateout("x11") r1 => _, // a1 + lateout("x12") _, // a2 + lateout("x13") _, // a3 + inlateout("x14") 0 => _, // a4 + lateout("x15") _, // a5 + lateout("x16") _, // a6 + lateout("x17") _, // a7 + // x18-27 are s2-s11 and are callee-saved + lateout("x28") _, // t3 + lateout("x29") _, // t4 + lateout("x30") _, // t5 + lateout("x31") _, // t6 + ); + } } - fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, usize) { - let mut r0 = r0_in as u32; + unsafe fn syscall1<const CLASS: usize>(mut r0: *mut ()) -> (*mut (), *mut ()) { let r1; + // Safety: This matches the invariants required by the documentation on + // RawSyscalls::syscall1 unsafe { asm!("ecall", inlateout("a0") r0, lateout("a1") r1, - in("a4") 5, + in("a4") CLASS, options(preserves_flags, nostack, nomem), ); } (r0, r1) } - fn one_arg_memop(r0_in: OneArgMemop, mut r1: usize) -> (u32, usize) { - let mut r0 = r0_in as u32; + unsafe fn syscall2<const CLASS: usize>(mut r0: *mut (), mut r1: *mut ()) -> (*mut (), *mut ()) { + // Safety: This matches the invariants required by the documentation on + // RawSyscalls::syscall2 unsafe { asm!("ecall", inlateout("a0") r0, inlateout("a1") r1, - in("a4") 5, + in("a4") CLASS, options(preserves_flags, nostack, nomem) ); } (r0, r1) } + + unsafe fn syscall4<const CLASS: usize>( + mut r0: *mut (), + mut r1: *mut (), + mut r2: *mut (), + mut r3: *mut (), + ) -> (*mut (), *mut (), *mut (), *mut ()) { + // Safety: This matches the invariants required by the documentation on + // RawSyscalls::syscall4 + unsafe { + asm!("ecall", + inlateout("a0") r0, + inlateout("a1") r1, + inlateout("a2") r2, + inlateout("a3") r3, + in("a4") CLASS, + options(preserves_flags, nostack), + ); + } + (r0, r1, r2, r3) + } }