blob: 5f95d2b45a3152e3cee84e4e1052ad4fe6699ca3 [file] [log] [blame]
// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are
// referenced in the comment on `RawSyscalls`.
/// `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`
/// 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. There are a few major
// factors affecting its design:
// 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.
//
// (*) 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.
//
// Currently, yield takes exactly one argument, to specify what yield type to
// do. Therefore we only need one raw yield call.
//
// Subscribe, command, read-write allow, and read-only allow all take four
// argument types. Even when calling command IDs that have unused arguments, we
// still need to clear the argument registers so as to avoid passing
// confidential data to capsules (this is in line with Tock's threat model). As
// such, four_arg_syscall() is used for all subscribe, command, read-only allow,
// and read-write allow system calls.
//
// Memop takes 1 or 2 arguments (operation and an optional argument). Because it
// is part of the core kernel, it is okay for us to leave arbitrary data in the
// argument register for operations where the argument register is unused
// (again, in line with Tock's threat model). Memop returns up to 2 return
// arguments, so we don't need to mark r2 and r3 as clobbered. As such, we need
// two raw memop calls: one for operations without an argument and one for
// operations with an argument.
//
// Because the variables passed in and out of raw system calls represent
// register values, they are of type usize. In cases where it doesn't make sense
// to pass a pointer-sized value, libtock_unittest::FakeSyscalls may panic if a
// too-large value is passed.
pub trait RawSyscalls {
// raw_yield 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.
// 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)
//
// 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).
fn raw_yield(r0_in: YieldType) -> usize;
// 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)
//
/// # 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: usize,
r1: usize,
r2: usize,
r3: usize,
class: u8,
) -> (usize, usize, usize, usize);
// 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.
//
// 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:
// 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:
// 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 usize
// so that if the kernel adds an unsafe memop this API doesn't become
// unsound.
fn zero_arg_memop(r0_in: ZeroArgMemop) -> (usize, usize);
// 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.
//
// one_arg_memop's inline assembly should:
// 1. Call syscall class 5
// 2. Specify r0 and r1 as inlateout registers, and return (r0, r1)
// 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:
// pure Two invocations of sbrk 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 usize
// so that if the kernel adds an unsafe memop this API doesn't become
// unsound.
fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (usize, usize);
}
#[non_exhaustive]
#[repr(usize)]
pub enum OneArgMemop {
Brk = 0,
Sbrk = 1,
FlashRegionStart = 8,
FlashRegionEnd = 9,
SpecifyStackTop = 10,
SpecifyHeapStart = 11,
}
// TODO: When the numeric values (0 and 1) are assigned to the yield types,
// specify those values here.
#[non_exhaustive]
#[repr(usize)]
pub enum YieldType {
Wait,
NoWait,
}
#[non_exhaustive]
#[repr(usize)]
pub enum ZeroArgMemop {
MemoryStart = 2,
MemoryEnd = 3,
FlashStart = 4,
FlashEnd = 5,
GrantStart = 6,
FlashRegions = 7,
}