blob: 285d2fc0fd2789f0729bfe4178dac67c6d703036 [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
use core::arch::{asm, naked_asm};
use core::mem;
use core::ptr::NonNull;
use exit_status::ExitStatus;
use kernel::Arch;
use kernel::interrupt_controller::InterruptController;
use kernel::scheduler::{self, SchedulerState, Stack, ThreadLocalState};
use kernel::sync::spinlock::SpinLockGuard;
use log_if::debug_if;
#[cfg(feature = "user_space")]
use pw_status::Result;
use crate::protection::MemoryConfig;
use crate::regs::MStatusVal;
#[cfg(feature = "user_space")]
use crate::regs::PrivilegeLevel;
use crate::spinlock::BareSpinLock;
const LOG_CONTEXT_SWITCH: bool = false;
const LOG_THREAD_CREATE: bool = false;
static BOOT_THREAD_LOCAL_STATE: ThreadLocalState<crate::Arch> = ThreadLocalState::new();
static mut THREAD_LOCAL_STATE: NonNull<ThreadLocalState<crate::Arch>> =
NonNull::from_ref(&BOOT_THREAD_LOCAL_STATE);
#[repr(C)]
struct ContextSwitchFrame {
ra: usize,
s0: usize,
s1: usize,
s2: usize,
s3: usize,
s4: usize,
s5: usize,
s6: usize,
s7: usize,
s8: usize,
s9: usize,
s10: usize,
s11: usize,
}
pub struct ArchThreadState {
frame: *mut ContextSwitchFrame,
#[cfg(feature = "user_space")]
pub(crate) memory_config: *const MemoryConfig,
local: ThreadLocalState<crate::Arch>,
}
#[cfg(feature = "user_space")]
extern "C" fn prepare_userspace_thread(memory_config: *const MemoryConfig) {
// Break the scheduler lock.
// Safety: This is called on thread start, which is one of the allowed
// scenarios for breaking the lock. The scheduler lock was held by the
// previous thread/scheduler and we need to release it so this thread
// can use it (or anything it calls).
unsafe {
kernel::scheduler::break_scheduler_lock(crate::Arch);
}
// Enable interrupts per contract in `ThreadState::initialize_user_frame`.
<crate::Arch as Arch>::InterruptController::enable_interrupts();
// If we need to reload the PMP to for exceptions, this also means we
// do not perform the PMP load in the context switch function. We
// defer it instead to the trampoline function. Stash the pointers
// we'll need to make the call in the trampoline.
if cfg!(feature = "exceptions_reload_pmp") {
unsafe {
(*memory_config).write();
}
}
}
impl ArchThreadState {
#[inline(never)]
fn initialize(
&mut self,
kernel_stack: Stack,
trampoline: extern "C" fn(),
initial_mstatus: MStatusVal,
initial_sp: usize,
(s0, s1, s2, s3): (usize, usize, usize, usize),
) {
self.local = ThreadLocalState::new();
let frame: *mut ContextSwitchFrame =
Stack::aligned_stack_allocation_mut(unsafe { kernel_stack.end_mut() }, 8);
unsafe {
// Clear the stack and set up the exception frame such that it would
// return to the function passed in with arg0 and arg1 passed in the
// first two argument slots.
(*frame) = mem::zeroed();
(*frame).ra = trampoline as usize;
// The `s` registers are used here instead of the ABI prescribed `a`
// registers because the `a` registers are in the exception frame,
// not the context switch frame.
(*frame).s0 = s0;
(*frame).s1 = s1;
(*frame).s2 = s2;
(*frame).s3 = s3;
(*frame).s5 = initial_sp;
(*frame).s6 = initial_mstatus.0;
#[cfg(feature = "user_space")]
{
(*frame).s7 = self.memory_config as usize;
(*frame).s8 = prepare_userspace_thread as *const () as usize;
}
#[cfg(not(feature = "user_space"))]
{
(*frame).s7 = 0;
(*frame).s8 = 0;
}
}
self.frame = frame
}
}
impl Arch for super::Arch {
type ThreadState = ArchThreadState;
type BareSpinLock = BareSpinLock;
type Clock = super::timer::Clock;
#[cfg(not(feature = "disable_interrupts_atomic"))]
type AtomicBool = core::sync::atomic::AtomicBool;
#[cfg(not(feature = "disable_interrupts_atomic"))]
type AtomicUsize = core::sync::atomic::AtomicUsize;
#[cfg(feature = "disable_interrupts_atomic")]
type AtomicBool = crate::disable_interrupts_atomic::AtomicBool;
#[cfg(feature = "disable_interrupts_atomic")]
type AtomicUsize = crate::disable_interrupts_atomic::AtomicUsize;
#[cfg(feature = "user_space")]
type SyscallArgs<'a> = crate::syscall::RiscVSyscallArgs<'a>;
type InterruptController = crate::InterruptController;
#[inline(never)]
unsafe fn context_switch<'a>(
self,
sched_state: SpinLockGuard<'a, Self, SchedulerState<Self>>,
old_thread_state: *mut ArchThreadState,
new_thread_state: *mut ArchThreadState,
) -> (SpinLockGuard<'a, Self, SchedulerState<Self>>, bool) {
debug_if!(
LOG_CONTEXT_SWITCH,
"context switch from frame {:#08x} to frame {:#08x}",
unsafe { (*old_thread_state).frame } as usize,
unsafe { (*new_thread_state).frame } as usize,
);
// Memory config is swapped before the context switch instead of after.
// This avoids needing a special case in the user mode thread init to
// initialize the config.
//
// Memory context switch overhead is avoided for threads in the same
// memory config space.
//
// In the case where the target needs to reload the PMP upon exception
// handler entry and exit, we skip loading the PMP here and perform the
// load in the exception handler and the assembly trampoline, deferring
// the PMP load to very close to the return to userspace. This keeps
// kernel memory mappings active until nearly the last moment before
// returning into userspace.
#[cfg(all(feature = "user_space", not(feature = "exceptions_reload_pmp")))]
if unsafe { (*new_thread_state).memory_config }
!= unsafe { (*old_thread_state).memory_config }
{
unsafe { (*(*new_thread_state).memory_config).write() };
}
unsafe { THREAD_LOCAL_STATE = NonNull::from_ref(&(*new_thread_state).local) }
// Note: there is a small window of time where the new memory configuration
// is active (above) and the new thread is active (below). Since this code
// always executes in M-Mode, it bypasses the memory config and by the
// time control is returned to user space, the memory config is correct.
let old_thread_frame = unsafe { &mut (*old_thread_state).frame };
let new_thread_frame = unsafe { (*new_thread_state).frame };
riscv_context_switch(old_thread_frame, new_thread_frame);
(sched_state, true)
}
fn thread_local_state(self) -> &'static ThreadLocalState<Self> {
unsafe {
#[allow(static_mut_refs)]
THREAD_LOCAL_STATE.as_ref()
}
}
fn now(self) -> pw_time_core::Instant<super::timer::Clock> {
use pw_time_core::Clock as _;
super::timer::Clock::now()
}
fn idle(self) {
riscv::asm::wfi();
}
fn early_init(self) {
// Make sure interrupts are disabled
<Self as Arch>::InterruptController::disable_interrupts();
crate::exceptions::early_init();
crate::timer::early_init();
}
fn init(self) {
crate::timer::init();
}
fn panic() -> ! {
unsafe {
asm!("ebreak");
}
#[allow(clippy::empty_loop)]
loop {}
}
}
impl kernel::scheduler::ThreadState for ArchThreadState {
type MemoryConfig = crate::protection::MemoryConfig;
const NEW: Self = Self {
frame: core::ptr::null_mut(),
#[cfg(feature = "user_space")]
memory_config: core::ptr::null(),
local: ThreadLocalState::new(),
};
#[inline(never)]
#[allow(unused_variables)]
unsafe fn initialize_kernel_state(
&mut self,
kernel_stack: Stack,
memory_config: *const MemoryConfig,
initial_function: extern "C" fn(usize, usize, usize),
args: (usize, usize, usize),
) {
#[cfg(feature = "user_space")]
{
self.memory_config = memory_config;
}
self.initialize(
kernel_stack,
asm_trampoline,
MStatusVal::default(),
0x0,
(initial_function as usize, args.0, args.1, args.2),
);
}
#[cfg(feature = "user_space")]
unsafe fn initialize_user_state(
&mut self,
kernel_stack: Stack,
memory_config: *const MemoryConfig,
initial_sp: usize,
initial_pc: usize,
args: (usize, usize, usize),
) -> Result<()> {
self.memory_config = memory_config;
let mstatus = MStatusVal::default()
.with_mpie(true)
.with_spie(true)
.with_mpp(PrivilegeLevel::User);
self.initialize(
kernel_stack,
asm_user_trampoline,
mstatus,
initial_sp,
(initial_pc, args.0, args.1, args.2),
);
Ok(())
}
}
#[unsafe(no_mangle)]
#[unsafe(naked)]
extern "C" fn riscv_context_switch(
old_frame: *mut *mut ContextSwitchFrame,
new_frame: *mut ContextSwitchFrame,
) {
naked_asm!(
"
// Push all of the s registers + ra onto the stack,
// save the stack pointer into the old frame pointer.
addi sp, sp, -52
sw ra, 0(sp)
sw s0, 4(sp)
sw s1, 8(sp)
sw s2, 12(sp)
sw s3, 16(sp)
sw s4, 20(sp)
sw s5, 24(sp)
sw s6, 28(sp)
sw s7, 32(sp)
sw s8, 36(sp)
sw s9, 40(sp)
sw s10, 44(sp)
sw s11, 48(sp)
sw sp, (a0)
// Pop ra and s registers off the stack.
lw ra, 0(a1)
lw s0, 4(a1)
lw s1, 8(a1)
lw s2, 12(a1)
lw s3, 16(a1)
lw s4, 20(a1)
lw s5, 24(a1)
lw s6, 28(a1)
lw s7, 32(a1)
lw s8, 36(a1)
lw s9, 40(a1)
lw s10, 44(a1)
lw s11, 48(a1)
addi sp, a1, 52
ret
"
)
}
// Since the context switch frame does not contain the function arg registers,
// pass the initial function and arguments via two of the saved s registers.
#[unsafe(no_mangle)]
#[unsafe(naked)]
extern "C" fn asm_user_trampoline() {
naked_asm!(
"
// If we have a stashed memory config pointer in s7,
// then perform the call memory_config_write(memory_config)
// via the stashed function address in s8.
beqz s7, 1f
mv a0, s7
jalr ra, s8, 0
// Store the kernel stack pointer in mscratch.
1:
csrw mscratch, sp
// Set initial SP as passed in by `initialize_frame()`.
mv sp, s5
// Set args for function call. `s` registers are offset by 1
// because `initial_pc` is stored in s0.
mv a0, s1
mv a1, s2
mv a2, s3
// Mstatus and Mepc are set up for a return to U-Mode.
csrw mstatus, s6
csrw mepc, s0
mret
"
)
}
// Since the context switch frame does not contain the function arg registers,
// pass the initial function and arguments via two of the saved s registers.
#[unsafe(no_mangle)]
#[unsafe(naked)]
extern "C" fn asm_trampoline() {
naked_asm!(
"
// Zero out mscratch to signify that this is a kernel thread.
csrw mscratch, zero
// Set args for function call.
mv a0, s0
mv a1, s1
mv a2, s2
mv a3, s3
tail trampoline
"
)
}
#[allow(unused)]
#[unsafe(no_mangle)]
extern "C" fn trampoline(
initial_function: extern "C" fn(usize, usize, usize),
arg0: usize,
arg1: usize,
arg2: usize,
) {
debug_if!(
LOG_THREAD_CREATE,
"riscv trampoline: initial function {:#x} arg0 {:#x} arg1 {:#x} arg2 {:#x}",
initial_function as usize,
arg0 as usize,
arg1 as usize,
arg2 as usize,
);
// Break the scheduler lock.
// Safety: This is called on thread start, which is one of the allowed
// scenarios for breaking the lock. The scheduler lock was held by the
// previous thread/scheduler and we need to release it so this thread
// can use it (or anything it calls).
unsafe {
kernel::scheduler::break_scheduler_lock(crate::Arch);
}
// Enable interrupts per contract in `ThreadState::initialize_kernel_frame`.
<crate::Arch as Arch>::InterruptController::enable_interrupts();
// Call the actual initial function of the thread.
initial_function(arg0, arg1, arg2);
// Get a pointer to the current thread and call exit.
// Note: must let the scope of the lock guard close,
// since exit_thread() does not return.
scheduler::exit_thread(crate::Arch, ExitStatus::Success(0));
// Does not reach.
}