blob: e1e320e7305424ada22f03c58a0dcf5954f2a4c3 [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.
//! # Proc macros for RISC-V support in the kernel
//!
//! # Exceptions
//!
//! Two attribute macros ([`kernel_only_exception`] and [`user_space_exception`])
//! are provided for generating exception wrappers. They have the same syntax
//! and are intended to be conditionally imported based on the kernel's
//! `user_space` feature:
//!
//! ```
//! #[cfg(not(feature = "user_space"))]
//! pub(crate) use riscv_macro::kernel_only_exception as exception;
//! #[cfg(feature = "user_space")]
//! pub(crate) use riscv_macro::user_space_exception as exception;
//! ```
//!
//! To use the macros, an exception name must be provided. This should match
//! the symbol name for the main trap handler (aka `_start_trap` for `riscv-rt`)
//! ```
//! #[exception(exception = "_start_trap")]
//! #[unsafe(no_mangle)]
//! unsafe extern "C" fn trap_handler(mcause: MCauseVal, mepc: usize, frame: &mut TrapFrame) {
//! }
//! ```
use arch_macro_helpers::{validate_handler_abi, validate_interrupt_handler_args};
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::parse::{Parse, ParseStream, Result};
use syn::punctuated::Punctuated;
use syn::{Ident, ItemFn, Token, parse_macro_input};
#[derive(Eq, PartialEq, Hash, Debug)]
/// Determines whether the generated exception handler will include code to support userspace.
enum KernelMode {
UserSpace,
KernelOnly,
}
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
/// Determines whether the generated save/restore frame code will handle an exception from
/// userspace or the kernel.
enum FrameType {
UserSpace,
Kernel,
}
#[derive(Eq, PartialEq, Hash, Debug)]
enum Attribute {
Exception(String),
}
impl Attribute {
fn parse_value(input: ParseStream) -> Result<String> {
let _ = input.parse::<Token![=]>()?;
let value = input.parse::<syn::LitStr>()?;
Ok(value.value())
}
}
impl Parse for Attribute {
fn parse(input: ParseStream) -> Result<Self> {
let name = input.parse::<Ident>()?;
let name_str = name.to_string();
match name_str.as_str() {
"exception" => {
let value = Self::parse_value(input)?;
Ok(Attribute::Exception(value))
}
_ => Err(input.error(format!("unknown attribute {name_str}"))),
}
}
}
struct Attributes {
exception: String,
}
impl Parse for Attributes {
fn parse(input: ParseStream) -> Result<Self> {
let attributes = Punctuated::<Attribute, Token![,]>::parse_terminated(input)?;
let mut exception = None;
for attr in attributes {
match attr {
Attribute::Exception(name) => exception = Some(name),
}
}
let exception = exception.ok_or_else(|| input.error("missing exception name"))?;
Ok(Attributes { exception })
}
}
enum Register {
GeneralPurpose(&'static str),
UserSpace(&'static str),
Csr(&'static str, &'static str),
StackPointer,
}
const REGS: &[(Register, usize)] = &[
(Register::Csr("mepc", "a1"), 0x00),
(Register::Csr("mstatus", "t0"), 0x04),
(Register::GeneralPurpose("ra"), 0x08),
(Register::GeneralPurpose("a0"), 0x0c),
(Register::GeneralPurpose("a1"), 0x10),
(Register::GeneralPurpose("a2"), 0x14),
(Register::GeneralPurpose("a3"), 0x18),
(Register::GeneralPurpose("a4"), 0x1c),
(Register::GeneralPurpose("a5"), 0x20),
(Register::GeneralPurpose("a6"), 0x24),
(Register::GeneralPurpose("a7"), 0x28),
(Register::GeneralPurpose("t0"), 0x2c),
(Register::GeneralPurpose("t1"), 0x30),
(Register::GeneralPurpose("t2"), 0x34),
(Register::GeneralPurpose("t3"), 0x38),
(Register::GeneralPurpose("t4"), 0x3c),
(Register::GeneralPurpose("t5"), 0x40),
(Register::GeneralPurpose("t6"), 0x44),
(Register::UserSpace("tp"), 0x48),
(Register::UserSpace("gp"), 0x4c),
(Register::StackPointer, 0x50),
];
const STACK_FRAME_LEN: usize = 0x60;
fn general_purpose_regs(
regs: &[(Register, usize)],
) -> impl DoubleEndedIterator<Item = (&'static str, usize)> + use<'_> {
regs.iter().filter_map(|reg| match reg.0 {
Register::GeneralPurpose(name) => Some((name, reg.1)),
_ => None,
})
}
fn csr_regs(
regs: &[(Register, usize)],
) -> impl DoubleEndedIterator<Item = (&'static str, &'static str, usize)> + use<'_> {
regs.iter().filter_map(|reg| match reg.0 {
Register::Csr(name, temp_reg) => Some((name, temp_reg, reg.1)),
_ => None,
})
}
fn user_space_regs(
regs: &[(Register, usize)],
) -> impl DoubleEndedIterator<Item = (&'static str, usize)> + use<'_> {
regs.iter().filter_map(|reg| match reg.0 {
Register::UserSpace(name) => Some((name, reg.1)),
_ => None,
})
}
fn stack_pointer_offset(regs: &[(Register, usize)]) -> usize {
regs.iter()
.filter_map(|reg| match reg.0 {
Register::StackPointer => Some(reg.1),
_ => None,
})
.next()
.expect("register set contains stack pointer")
}
fn save_exception_frame(asm: &mut String, frame_type: FrameType) {
asm.push_str(&format!("addi sp, sp, -{STACK_FRAME_LEN:#x}\n"));
for (reg, offset) in general_purpose_regs(REGS).rev() {
asm.push_str(&format!("sw {reg}, {offset:#x}(sp)\n"));
}
let mut reads = Vec::new();
let mut stores = Vec::new();
for (reg, temp_reg, offset) in csr_regs(REGS).rev() {
reads.push(format!("csrr {temp_reg}, {reg}\n"));
stores.push(format!("sw {temp_reg}, {offset:#x}(sp)\n"));
}
// Do all reads before stores for better pipelining.
for statement in reads.iter().chain(stores.iter()) {
asm.push_str(statement);
}
if frame_type == FrameType::UserSpace {
for (reg, offset) in user_space_regs(REGS).rev() {
asm.push_str(&format!("sw {reg}, {offset:#x}(sp)\n"));
}
let sp_offset = stack_pointer_offset(REGS);
asm.push_str(&format!(
"
// Save the stack pointer to the frame and zero out mscratch to
// signify execution in kernel mode.
csrrw t0, mscratch, zero
sw t0, {sp_offset:#x}(sp)
"
));
} else {
let sp_offset = stack_pointer_offset(REGS);
asm.push_str(&format!("sw zero, {sp_offset:#x}(sp)\n"));
}
}
fn restore_exception_frame(asm: &mut String, frame_type: FrameType) {
let mut loads = Vec::new();
let mut writes = Vec::new();
if frame_type == FrameType::UserSpace {
asm.push_str(&format!(
"
// Load the kernel stack pointer without the exception frame back
// into mscratch in preparation for returning from the exception.
addi t0, sp, {STACK_FRAME_LEN:#x}
csrw mscratch, t0
"
));
for (reg, offset) in user_space_regs(REGS) {
asm.push_str(&format!("lw {reg}, {offset:#x}(sp)\n"));
}
}
for (reg, temp_reg, offset) in csr_regs(REGS) {
loads.push(format!("lw {temp_reg}, {offset:#x}(sp)\n"));
writes.push(format!("csrw {reg}, {temp_reg}\n"));
}
// Do all loads before writes for better pipelining.
for statement in loads.iter().chain(writes.iter()) {
asm.push_str(statement);
}
for (reg, offset) in general_purpose_regs(REGS) {
asm.push_str(&format!("lw {reg}, {offset:#x}(sp)\n"));
}
if frame_type == FrameType::UserSpace {
let sp_offset = stack_pointer_offset(REGS);
asm.push_str(&format!(
"
// Load the user stack pointer as the last time the kernel stack
// pointer is touched.
lw sp, {sp_offset:#x}(sp)
"
));
} else {
asm.push_str(&format!("addi sp, sp, {STACK_FRAME_LEN:#x}\n"));
}
}
fn call_handler(asm: &mut String, handler_name: &str) {
// At this point `mepc` will be in `a1`
asm.push_str(&format!(
"
csrr a0, mcause
mv a2, sp
// Call into the handler with:
// a0: mcause
// a1: mepc
// a2: &TrapFrame
call {handler_name}
"
));
}
fn exception_handler(asm: &mut String, frame_type: FrameType, handler_name: &str) {
save_exception_frame(asm, frame_type);
call_handler(asm, handler_name);
restore_exception_frame(asm, frame_type);
asm.push_str("mret\n");
}
fn exception(attr: TokenStream, item: TokenStream, kernel_mode: KernelMode) -> TokenStream {
let handler = parse_macro_input!(item as ItemFn);
let attributes = parse_macro_input!(attr as Attributes);
let exception_ident = format_ident!("{}", attributes.exception);
let handler_ident = &handler.sig.ident;
let handler_name = handler_ident.clone().to_string();
let mut asm = String::new();
if kernel_mode == KernelMode::UserSpace {
// When built with user space support, the handler can check if it's in
// kernel or user space mode by swapping sp with mscratch. If mscratch
// contained zero, the handler is in kernel mode. There are two
// separate paths for user and kernel handler because user handlers need
// more state saved and restored.
asm.push_str(
"
// Start by exchanging the current (original) stack pointer with mscratch
csrrw sp, mscratch, sp
bnez sp, 1f // If non-zero branch to user handler
// If mscratch was zero, we're in kernel mode. Exchange the stack pointer
// with mscratch again to recover the original stack pointer.
csrrw sp, mscratch, sp
",
);
}
exception_handler(&mut asm, FrameType::Kernel, &handler_name);
if kernel_mode == KernelMode::UserSpace {
asm.push_str("1:\n");
exception_handler(&mut asm, FrameType::UserSpace, &handler_name);
}
quote! {
#[unsafe(no_mangle)]
#[unsafe(naked)]
pub unsafe extern "C" fn #exception_ident() -> ! {
unsafe {
core::arch::naked_asm!(#asm)
}
}
// Compile time assert that the handler function signature matches.
const _: crate::exceptions::ExceptionHandler = #handler_ident;
#handler
}
.into()
}
/// Generate an exception wrapper with kernel only support.
#[proc_macro_attribute]
pub fn kernel_only_exception(attr: TokenStream, item: TokenStream) -> TokenStream {
exception(attr, item, KernelMode::KernelOnly)
}
/// Generate an exception wrapper with user-space support.
#[proc_macro_attribute]
pub fn user_space_exception(attr: TokenStream, item: TokenStream) -> TokenStream {
exception(attr, item, KernelMode::UserSpace)
}
/// Passthrough interrupt macro for RISC-V that passes through the function after validation.
#[proc_macro_attribute]
pub fn interrupt(_attr: TokenStream, item: TokenStream) -> TokenStream {
let handler = syn::parse_macro_input!(item as syn::ItemFn);
if let Err(err) = validate_handler_abi(&handler) {
return err.to_compile_error().into();
}
if let Err(err) = validate_interrupt_handler_args(&handler) {
return err.to_compile_error().into();
}
quote::quote! { #handler }.into()
}