Skip to content

Conversation

kevinbube
Copy link
Contributor

This adds a function to switch to program stack and unprivileged mode.

Fixes #583

@kevinbube kevinbube force-pushed the new/enter_unprivileged_fn branch from 93ae33a to 89152c5 Compare April 25, 2025 16:36
/// code, respectively.
#[cfg(cortex_m)]
#[inline(always)]
pub fn enter_unprivileged(psp: &u32, entry: fn() -> !) -> ! {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think PSP should be either a raw pointer, or a new type that requires appropriate alignment. Conceptually this is similar to https://docs.rs/rp2040-hal/latest/rp2040_hal/multicore/struct.Core.html#method.spawn and we could borrow the StackAllocation idea.

Unfortunately I don't immediately recall if PSP needs to be 4 or 8 byte aligned. We should check, and document.

The function also needs to be marked unsafe!

@kevinbube kevinbube force-pushed the new/enter_unprivileged_fn branch from 89152c5 to f8f611f Compare May 1, 2025 20:15
@kevinbube
Copy link
Contributor Author

kevinbube commented May 1, 2025 via email

@jonathanpallant
Copy link
Contributor

https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst#6212stack-constraints-at-a-public-interface

6.2.1.2 Stack constraints at a public interface

The stack must also conform to the following constraint at a public interface:

SP mod 8 = 0. The stack must be double-word aligned.

As this function will cause entry to the given function, the stack must have eight byte alignment - if I'm reading this correctly.

@kevinbube kevinbube force-pushed the new/enter_unprivileged_fn branch from f8f611f to f26be34 Compare May 2, 2025 08:40
@kevinbube
Copy link
Contributor Author

kevinbube commented May 2, 2025 via email

@jonathanpallant
Copy link
Contributor

Well it must always be 4 byte aligned, and if it is 8 byte aligned it is still 4 byte aligned as well. The 4 byte alignment I think is an Arm processor requirement the 8 byte over-alignment is from the Arm Architecture Procedure Call Standard - which is a thing we agree to do, but the processor does not care whether we do or we don't.

jonathanpallant
jonathanpallant previously approved these changes May 2, 2025
Copy link
Contributor

@jonathanpallant jonathanpallant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but we should write a test case (maybe using QEMU?)

@jonathanpallant
Copy link
Contributor

Some test code:

//! Check we can switch to the PSP.

#![no_std]
#![no_main]

use core::cell::UnsafeCell;

// Contains our panic handler
use qemu_thumbv7em as _;
// Does our demft output
use defmt_semihosting as _;

/// A stack you can use as your Process Stack (PSP)
///
/// The const-param N is the size **in 32-bit words**
#[repr(align(8), C)]
struct PspStack<const N: usize> {
    space: UnsafeCell<[u32; N]>,
}

impl<const N: usize> PspStack<N> {
    /// Const-initialise a PspStack
    ///
    /// Use a turbofish to specify the size, like:
    ///
    /// ```rust
    /// static PSP_STACK: PspStack::<4096> = PspStack::new();
    /// ```
    pub const fn new() -> PspStack<N> {
        PspStack {
            space: UnsafeCell::new([0; N]),
        }
    }

    /// Return the top of the PSP stack
    pub fn get_top(&self) -> *mut u32 {
        let start = self.space.get() as *mut u32;
        let end = unsafe { start.add(N) };
        end
    }
}

unsafe impl<const N: usize> Sync for PspStack<N> {}

static PSP_STACK: PspStack<4096> = PspStack::new();

#[cortex_m_rt::entry]
fn main() -> ! {
    let x = 5;
    defmt::info!(
        "Using MSP. addr(x) = {=usize:08x}",
        core::ptr::addr_of!(x) as usize
    );
    unsafe {
        let psp_stack = PSP_STACK.get_top();
        defmt::info!("PSP stack is at {=usize:08x}", psp_stack as usize);
        enter_unprivileged(psp_stack, user_mode);
    }
}

fn user_mode() -> ! {
    let x = 5;
    defmt::info!(
        "Using PSP. addr(x) = {=usize:08x}",
        core::ptr::addr_of!(x) as usize
    );
    panic!("All finished");
}

/// Switch to unprivileged mode.
///
/// Sets CONTROL.SPSEL (setting the program stack to be the active
/// stack) and CONTROL.nPRIV (setting unprivileged mode), updates the
/// program stack pointer to the address in `psp`, then jumps to the
/// address in `entry`.
///
/// # Safety
///
/// `psp` and `entry` must point to valid stack memory and executable
/// code, respectively. `psp` must be 8 bytes aligned.
#[inline(always)]
pub unsafe fn enter_unprivileged(psp: *const u32, entry: fn() -> !) -> ! {
    unsafe {
        core::arch::asm!(
            "mrs {tmp}, CONTROL",
            "orr {tmp}, #2",
            "msr PSP, {psp}",
            "msr CONTROL, {tmp}",
            "isb",
            "bx {ent}",
            tmp = in(reg) 0,
            psp = in(reg) psp,
            ent = in(reg) entry,
            options(noreturn, nomem, nostack)
        );
    }
}

Produces:

------------------------------------------------------------------------
[INFO ] Using MSP. addr(x) = 203fffe4 (bin/psp_test.rs:50)
[INFO ] PSP stack is at 20004000 (bin/psp_test.rs:56)
[INFO ] Using PSP. addr(x) = 20003fd8 (bin/psp_test.rs:63)
[ERROR] Panic! All finished (src/bin/psp_test.rs:67) (src/lib.rs:12)
------------------------------------------------------------------------

@jonathanpallant
Copy link
Contributor

jonathanpallant commented May 2, 2025

Having done this exercise, I now realise it's important to point out that the stack descends, and so they should pass a pointer to the top of the stack region. In fact, the word just above the stack region - because the SP is decremented before the first write. That is non-obvious.

We should probably just include the PspStack type I wrote, to make it easier.

@kevinbube
Copy link
Contributor Author

kevinbube commented May 3, 2025 via email

@thejpster
Copy link
Contributor

So I poked around a bit at this today, and I don't know why I thought my example worked - doing a bkpt 0xAB (a semihosting syscall) from unprivileged mode causes a HardFault. Therefore you cannot use defmt-semihosting from user mode.

Here's some patches for this PR: https://github.com/thejpster/cortex-m/tree/add-unprivileged-mode

Here's a working example: https://github.com/thejpster/psp-example

$ DEFMT_LOG=trace cargo run
    Blocking waiting for file lock on build directory
   Compiling defmt-macros v1.0.1
   Compiling cortex-m v0.7.7 (https://github.com/thejpster/cortex-m?branch=add-unprivileged-mode#d4e5a166)
   Compiling defmt v1.0.1
   Compiling defmt-semihosting v0.3.0
   Compiling psp-example v0.1.0 (/home/jonathan/Documents/github/rust-embedded/psp-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.45s
     Running `/home/jonathan/Documents/github/rust-embedded/psp-example/./qemu-run.sh target/thumbv7em-none-eabihf/debug/psp-example`
ELF_BINARY=target/thumbv7em-none-eabihf/debug/psp-example
Running on '-cpu cortex-m4 -machine mps2-an386'...
------------------------------------------------------------------------
[INFO ] Using MSP. addr(x) = 203fffd0 (src/main.rs:17)
[INFO ] PSP stack is at 20004000 (src/main.rs:22)
[INFO ] Got SVCall, ptr=20003fd4 (src/main.rs:44)
^Cqemu-system-arm: terminating on signal 2

I from unprivileged mode I use an SVC call to drop into privileged mode, where defmt-semihosting works fine.

@kevinbube
Copy link
Contributor Author

kevinbube commented Jul 27, 2025 via email

@thejpster
Copy link
Contributor

I think unit tests will be really hard given the test runner we have. Entering unpriv mode is kind of a one-time deal at the moment.

If there's no place in this crate currently for standalone example binaries run in QEMU (like cortex-ar does) then we can just skip the test for now.

@jonathanpallant
Copy link
Contributor

This is good but it'll need a rebase after #605 lands.

kevinbube and others added 2 commits August 27, 2025 15:36
This adds a function to switch to program stack and unprivileged mode.

Fixes rust-embedded#583
@jonathanpallant
Copy link
Contributor

Updated comments as above, and rebased.

@jonathanpallant jonathanpallant added this pull request to the merge queue Aug 27, 2025
@jonathanpallant jonathanpallant removed this pull request from the merge queue due to a manual request Aug 27, 2025
@jonathanpallant jonathanpallant force-pushed the new/enter_unprivileged_fn branch from fb53bc2 to 8b84fc8 Compare August 27, 2025 14:44
@jonathanpallant
Copy link
Contributor

I touched this, so I guess it needs another review.

@adamgreig
Copy link
Member

Looks good to me. Let's merge this now since it's rebased and then I'll worry about the other asm calls afterwards.

adamgreig
adamgreig previously approved these changes Aug 28, 2025
@adamgreig
Copy link
Member

Oh, we should add a changelog entry though! Would you mind adding it?

@jonathanpallant
Copy link
Contributor

Done @adamgreig

@adamgreig adamgreig enabled auto-merge August 31, 2025 19:58
@adamgreig adamgreig added this pull request to the merge queue Aug 31, 2025
Merged via the queue into rust-embedded:master with commit 8f347b7 Aug 31, 2025
11 checks passed
@kevinbube kevinbube deleted the new/enter_unprivileged_fn branch September 3, 2025 16:28
@kevinbube
Copy link
Contributor Author

kevinbube commented Sep 3, 2025 via email

@thejpster
Copy link
Contributor

There's already a feature called set-msplim which sets MSPLIM. If we set PSPLIM here, we should use a function from cortex_m::asm to do it.

I would leave this function as being unsafe and taking a pointer, and create a safe-to-use datatype which contains a valid 8-byte aligned stack frame, as I proposed above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Hard fault on debug compilation
5 participants