blob: 36ea4547fad777f0b0bc8eb2de75b0854016cf16 [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). As such, for efficiency, we need
// two raw memop calls: one for operations without an argument and one for
// operations with an argument.
//
// The success type for Memop calls depends on the operation perform. 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.
//
// 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:
// 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) -> 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)`.
/// # 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);
// 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 u32 so
// that if the kernel adds an unsafe memop -- or one that can clobber r2/r3
// -- this API doesn't become unsound.
fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, 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 u32 so
// that if the kernel adds an unsafe memop -- or one that can clobber r2/r3
// -- this API doesn't become unsound.
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.
}