aarch64_rt/
lib.rs

1// Copyright 2025 The aarch64-rt Authors.
2// This project is dual-licensed under Apache 2.0 and MIT terms.
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Startup code for aarch64 Cortex-A processors.
6
7#![no_std]
8#![deny(clippy::undocumented_unsafe_blocks)]
9#![deny(unsafe_op_in_unsafe_fn)]
10
11#[cfg(any(
12    all(feature = "el1", feature = "el2"),
13    all(feature = "el1", feature = "el3"),
14    all(feature = "el2", feature = "el3"),
15))]
16compile_error!("Only one `el` feature may be enabled at once.");
17
18mod entry;
19#[cfg(feature = "exceptions")]
20mod exceptions;
21#[cfg(feature = "initial-pagetable")]
22mod pagetable;
23
24#[cfg(feature = "initial-pagetable")]
25#[doc(hidden)]
26pub mod __private {
27    pub use crate::pagetable::{__enable_mmu_el1, __enable_mmu_el2, __enable_mmu_el3};
28}
29
30#[cfg(any(feature = "exceptions", feature = "psci"))]
31use core::arch::asm;
32#[cfg(not(feature = "initial-pagetable"))]
33use core::arch::naked_asm;
34use core::mem::ManuallyDrop;
35pub use entry::secondary_entry;
36#[cfg(feature = "exceptions")]
37pub use exceptions::{ExceptionHandlers, RegisterState, RegisterStateRef};
38#[cfg(all(feature = "initial-pagetable", feature = "el1"))]
39pub use pagetable::DEFAULT_TCR_EL1 as DEFAULT_TCR;
40#[cfg(all(feature = "initial-pagetable", feature = "el2"))]
41pub use pagetable::DEFAULT_TCR_EL2 as DEFAULT_TCR;
42#[cfg(all(feature = "initial-pagetable", feature = "el3"))]
43pub use pagetable::DEFAULT_TCR_EL3 as DEFAULT_TCR;
44#[cfg(feature = "initial-pagetable")]
45pub use pagetable::{
46    DEFAULT_MAIR, DEFAULT_SCTLR, DEFAULT_TCR_EL1, DEFAULT_TCR_EL2, DEFAULT_TCR_EL3,
47    InitialPagetable,
48};
49
50#[cfg(not(feature = "initial-pagetable"))]
51#[unsafe(naked)]
52#[unsafe(link_section = ".init")]
53#[unsafe(export_name = "enable_mmu")]
54extern "C" fn enable_mmu() {
55    naked_asm!("ret")
56}
57
58/// Sets the appropriate vbar to point to our `vector_table`, if the `exceptions` feature is
59/// enabled.
60extern "C" fn set_exception_vector() {
61    // SAFETY: We provide a valid vector table.
62    #[cfg(all(feature = "el1", feature = "exceptions"))]
63    unsafe {
64        asm!(
65            "adr x9, vector_table_el1",
66            "msr vbar_el1, x9",
67            options(nomem, nostack),
68            out("x9") _,
69        );
70    }
71    // SAFETY: We provide a valid vector table.
72    #[cfg(all(feature = "el2", feature = "exceptions"))]
73    unsafe {
74        asm!(
75            "adr x9, vector_table_el2",
76            "msr vbar_el2, x9",
77            options(nomem, nostack),
78            out("x9") _,
79        );
80    }
81    // SAFETY: We provide a valid vector table.
82    #[cfg(all(feature = "el3", feature = "exceptions"))]
83    unsafe {
84        asm!(
85            "adr x9, vector_table_el3",
86            "msr vbar_el3, x9",
87            options(nomem, nostack),
88            out("x9") _,
89        );
90    }
91    #[cfg(all(
92        feature = "exceptions",
93        not(any(feature = "el1", feature = "el2", feature = "el3"))
94    ))]
95    {
96        let current_el: u64;
97        // SAFETY: Reading CurrentEL is always safe.
98        unsafe {
99            asm!(
100                "mrs {current_el}, CurrentEL",
101                options(nomem, nostack, preserves_flags),
102                current_el = out(reg) current_el,
103            );
104        }
105        match (current_el >> 2) & 0b11 {
106            // SAFETY: We provide a valid vector table.
107            1 => unsafe {
108                asm!(
109                    "adr x9, vector_table_el1",
110                    "msr vbar_el1, x9",
111                    options(nomem, nostack, preserves_flags),
112                    out("x9") _,
113                );
114            },
115            // SAFETY: We provide a valid vector table.
116            2 => unsafe {
117                asm!(
118                    "adr x9, vector_table_el2",
119                    "msr vbar_el2, x9",
120                    options(nomem, nostack, preserves_flags),
121                    out("x9") _,
122                );
123            },
124            // SAFETY: We provide a valid vector table.
125            3 => unsafe {
126                asm!(
127                    "adr x9, vector_table_el3",
128                    "msr vbar_el3, x9",
129                    options(nomem, nostack, preserves_flags),
130                    out("x9") _,
131                );
132            },
133            _ => {
134                panic!("Unexpected EL");
135            }
136        }
137    }
138}
139
140extern "C" fn rust_entry(arg0: u64, arg1: u64, arg2: u64, arg3: u64) -> ! {
141    set_exception_vector();
142    __main(arg0, arg1, arg2, arg3)
143}
144
145unsafe extern "Rust" {
146    /// Main function provided by the application using the `main!` macro.
147    safe fn __main(arg0: u64, arg1: u64, arg2: u64, arg3: u64) -> !;
148}
149
150/// Marks the main function of the binary and reserves space for the boot stack.
151///
152/// Example:
153///
154/// ```rust
155/// use aarch64_rt::entry;
156///
157/// entry!(main);
158/// fn main() -> ! {
159///     info!("Hello world");
160/// }
161/// ```
162///
163/// 40 pages (160 KiB) is reserved for the boot stack by default; a different size may be configured
164/// by passing the number of pages as a second argument to the macro, e.g. `entry!(main, 10);` to
165/// reserve only 10 pages.
166#[macro_export]
167macro_rules! entry {
168    ($name:path) => {
169        entry!($name, 40);
170    };
171    ($name:path, $boot_stack_pages:expr) => {
172        #[unsafe(export_name = "boot_stack")]
173        #[unsafe(link_section = ".stack.boot_stack")]
174        static mut __BOOT_STACK: $crate::Stack<$boot_stack_pages> = $crate::Stack::new();
175
176        // Export a symbol with a name matching the extern declaration above.
177        #[unsafe(export_name = "__main")]
178        fn __main(arg0: u64, arg1: u64, arg2: u64, arg3: u64) -> ! {
179            // Ensure that the main function provided by the application has the correct type.
180            $name(arg0, arg1, arg2, arg3)
181        }
182    };
183}
184
185/// A stack for some CPU core.
186///
187/// This is used by the [`entry!`] macro to reserve space for the boot stack.
188#[repr(C, align(4096))]
189pub struct Stack<const NUM_PAGES: usize>([StackPage; NUM_PAGES]);
190
191impl<const NUM_PAGES: usize> Stack<NUM_PAGES> {
192    /// Creates a new zero-initialised stack.
193    pub const fn new() -> Self {
194        Self([const { StackPage::new() }; NUM_PAGES])
195    }
196}
197
198impl<const NUM_PAGES: usize> Default for Stack<NUM_PAGES> {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[repr(C, align(4096))]
205struct StackPage([u8; 4096]);
206
207impl StackPage {
208    const fn new() -> Self {
209        Self([0; 4096])
210    }
211}
212
213#[repr(C)]
214pub(crate) struct StartCoreStack<F> {
215    entry_ptr: *mut ManuallyDrop<F>,
216    trampoline_ptr: unsafe extern "C" fn(&mut ManuallyDrop<F>) -> !,
217}
218
219#[cfg(feature = "psci")]
220/// Issues a PSCI CPU_ON call to start the CPU core with the given MPIDR.
221///
222/// This starts the core with an assembly entry point which will enable the MMU, disable trapping of
223/// floating point instructions, initialise the stack pointer to the given value, and then jump to
224/// the given Rust entry point function, passing it the given argument value.
225///
226/// The closure passed as `rust_entry` **should never return**. Because the
227/// [never type has not been stabilized](https://github.com/rust-lang/rust/issues/35121)), this
228/// cannot be enforced by the type system yet.
229///
230/// # Safety
231///
232/// `stack` must point to a region of memory which is reserved for this core's stack. It must remain
233/// valid as long as the core is running, and there must not be any other access to it during that
234/// time. It must be mapped both for the current core to write to it (to pass initial parameters)
235/// and in the initial page table which the core being started will used, with the same memory
236/// attributes for both.
237// TODO: change `F` generic bounds to `FnOnce() -> !` when the never type is stabilized:
238// https://github.com/rust-lang/rust/issues/35121
239pub unsafe fn start_core<C: smccc::Call, F: FnOnce() + Send + 'static, const N: usize>(
240    mpidr: u64,
241    stack: *mut Stack<N>,
242    rust_entry: F,
243) -> Result<(), smccc::psci::Error> {
244    const {
245        assert!(
246            size_of::<StartCoreStack<F>>()
247                + 2 * size_of::<F>()
248                + 2 * align_of::<F>()
249                + 1024 // trampoline stack frame overhead
250                <= size_of::<Stack<N>>(),
251            "the `rust_entry` closure is too big to fit in the core stack"
252        );
253    }
254
255    let rust_entry = ManuallyDrop::new(rust_entry);
256
257    let stack_start = stack.cast::<u8>();
258    let align_offfset = stack_start.align_offset(align_of::<F>());
259    let entry_ptr = stack_start
260        .wrapping_add(align_offfset)
261        .cast::<ManuallyDrop<F>>();
262
263    assert!(stack.is_aligned());
264    // The stack grows downwards on aarch64, so get a pointer to the end of the stack.
265    let stack_end = stack.wrapping_add(1);
266    let params = stack_end.cast::<StartCoreStack<F>>().wrapping_sub(1);
267
268    // Write the trampoline and entry closure, so the assembly entry point can jump to it.
269    // SAFETY: Our caller promised that the stack is valid and nothing else will access it.
270    unsafe {
271        entry_ptr.write(rust_entry);
272        *params = StartCoreStack {
273            entry_ptr,
274            trampoline_ptr: trampoline::<F>,
275        };
276    };
277
278    // Wait for the stores above to complete before starting the secondary CPU core.
279    dsb_st();
280
281    smccc::psci::cpu_on::<C>(
282        mpidr,
283        secondary_entry as usize as _,
284        stack_end as usize as _,
285    )
286}
287
288#[cfg(feature = "psci")]
289/// Used by [`start_core`] as an entry point for the secondary CPU core.
290///
291/// # Safety
292///
293/// This calls [`ManuallyDrop::take`] on the provided argument, so this function must be
294/// called at most once for a given instance of `F`.
295// TODO: change `F` generic bounds to `FnOnce() -> !` when the never type is stabilized:
296// https://github.com/rust-lang/rust/issues/35121
297unsafe extern "C" fn trampoline<F: FnOnce() + Send + 'static>(entry: &mut ManuallyDrop<F>) -> ! {
298    // SAFETY: the trampoline function is only ever called once after creating ManuallyDrop
299    // instance, so we won't call ManuallyDrop::take more than once.
300    let entry = unsafe { ManuallyDrop::take(entry) };
301    entry();
302
303    panic!("rust_entry function passed to start_core should never return");
304}
305
306/// Data synchronisation barrier that waits for stores to complete, for the full system.
307#[cfg(feature = "psci")]
308fn dsb_st() {
309    // SAFETY: A synchronisation barrier is always safe.
310    unsafe {
311        asm!("dsb st", options(nostack));
312    }
313}