ax_cpu/arch/aarch64/asm.rs
1//! Wrapper functions for assembly instructions.
2
3use core::arch::asm;
4
5use aarch64_cpu::{asm::barrier, registers::*};
6use ax_memory_addr::{PhysAddr, VirtAddr};
7
8use super::asid::configured_tag_capacity;
9#[cfg(kernel_tls)]
10use crate::KernelTlsBase;
11#[cfg(feature = "uspace")]
12use crate::mmu::HardwareAddressSpace;
13
14/// Returns the number of AArch64 ASIDs, including reserved ASID 0.
15///
16/// The result reflects both the hardware capability and the ASID width selected
17/// by the boot owner in `TCR_EL1.AS`.
18pub fn address_space_tag_capacity() -> u32 {
19 configured_tag_capacity(
20 ID_AA64MMFR0_EL1.read(ID_AA64MMFR0_EL1::ASIDBits),
21 TCR_EL1.read(TCR_EL1::AS),
22 )
23}
24
25#[cfg(feature = "uspace")]
26fn flush_tlb_asid(asid: u16) {
27 let operand = u64::from(asid) << 48;
28 // SAFETY: the caller runs at EL1. The barriers match Linux's ASID
29 // invalidation ordering: page-table stores, TLBI, completion, then fetch.
30 unsafe {
31 asm!(
32 "dsb ishst; tlbi aside1is, {operand}; dsb ish; isb",
33 operand = in(reg) operand,
34 )
35 }
36}
37
38/// Installs one complete userspace identity into TTBR0_EL1.
39///
40/// Tagged installation invalidates the incoming ASID before publishing the
41/// root. Untagged installation invalidates every EL1 stage-one translation.
42///
43/// # Safety
44///
45/// The caller must own the current CPU with interrupts disabled and the root
46/// must remain alive for the complete activation lease.
47#[cfg(feature = "uspace")]
48pub unsafe fn install_user_address_space(address_space: HardwareAddressSpace) {
49 if address_space.hardware_tag() != 0 {
50 let capacity = address_space_tag_capacity();
51 if u32::from(address_space.hardware_tag()) < capacity {
52 flush_tlb_asid(address_space.hardware_tag());
53 let value = address_space.root().as_usize() as u64
54 | (u64::from(address_space.hardware_tag()) << 48);
55 TTBR0_EL1.set(value);
56 barrier::isb(barrier::SY);
57 return;
58 }
59 }
60
61 TTBR0_EL1.set(address_space.root().as_usize() as u64);
62 flush_tlb(None);
63}
64
65/// Allows the current CPU to respond to interrupts.
66///
67/// In AArch64, it unmasks IRQs by clearing the I bit in the `DAIF` register.
68#[inline]
69pub fn enable_irqs() {
70 unsafe { asm!("msr daifclr, #2") };
71}
72
73/// Makes the current CPU to ignore interrupts.
74///
75/// In AArch64, it masks IRQs by setting the I bit in the `DAIF` register.
76#[inline]
77pub fn disable_irqs() {
78 unsafe { asm!("msr daifset, #2") };
79}
80
81/// Returns whether the current CPU is allowed to respond to interrupts.
82///
83/// In AArch64, it checks the I bit in the `DAIF` register.
84#[inline]
85pub fn irqs_enabled() -> bool {
86 !DAIF.matches_all(DAIF::I::Masked)
87}
88
89/// Relaxes the current CPU and waits for interrupts.
90///
91/// It must be called with interrupts enabled, otherwise it will never return.
92#[inline]
93pub fn wait_for_irqs() {
94 aarch64_cpu::asm::wfi();
95}
96
97/// Waits for an interrupt after the caller masks local IRQ delivery.
98///
99/// AArch64 `WFI` observes enabled pending interrupt sources even while
100/// `DAIF.I` masks delivery. Keeping delivery masked through `WFI` closes the
101/// scheduler wake-loss window. The function returns with local IRQs enabled.
102#[inline]
103pub fn wait_for_irqs_disabled() {
104 debug_assert!(!irqs_enabled());
105 barrier::dsb(barrier::SY);
106 aarch64_cpu::asm::wfi();
107 enable_irqs();
108}
109
110/// Halt the current CPU.
111#[inline]
112pub fn halt() {
113 disable_irqs();
114 aarch64_cpu::asm::wfi(); // should never return
115}
116
117/// Reads the current page table root register for kernel space (`TTBR1_EL1`).
118///
119/// Returns the physical address of the page table root.
120#[inline]
121pub fn read_kernel_page_table() -> PhysAddr {
122 super::mmu::El1::read_kernel_page_table()
123}
124
125/// Reads the current page table root register for user space (`TTBR0_EL1`).
126///
127/// Returns the physical address of the page table root.
128#[inline]
129pub fn read_user_page_table() -> PhysAddr {
130 const TTBR_BADDR_MASK: u64 = (1 << 48) - 1;
131 let root = TTBR0_EL1.get() & TTBR_BADDR_MASK;
132 pa!(root as usize)
133}
134
135/// Writes the register to update the current page table root for kernel space
136/// (`TTBR1_EL1`).
137///
138/// Note that the TLB is **NOT** flushed after this operation.
139///
140/// # Safety
141///
142/// This function is unsafe as it changes the virtual memory address space.
143#[inline]
144pub unsafe fn write_kernel_page_table(root_paddr: PhysAddr) {
145 // SAFETY: this forwards the caller's EL1 mapping lifetime and execution contract.
146 unsafe { super::mmu::El1::write_kernel_page_table(root_paddr) };
147}
148
149/// Writes the register to update the current page table root for user space
150/// (`TTBR0_EL1`).
151/// Note that the TLB is **NOT** flushed after this operation.
152///
153/// # Safety
154///
155/// This function is unsafe as it changes the virtual memory address space.
156#[inline]
157pub unsafe fn write_user_page_table(root_paddr: PhysAddr) {
158 TTBR0_EL1.set(root_paddr.as_usize() as _);
159}
160
161/// Makes page-table writes visible to the inner-shareable domain.
162///
163/// Cross-CPU shootdown must execute this before sending any IPI. A barrier on
164/// the remote CPU cannot order page-table writes performed by the initiating
165/// CPU.
166#[inline]
167pub fn synchronize_page_table_writes() {
168 unsafe { asm!("dsb ishst") };
169}
170
171/// Flushes the local TLB.
172///
173/// If `vaddr` is [`None`], flushes the entire TLB. Otherwise, flushes the TLB
174/// entry that maps the given virtual address.
175#[inline]
176pub fn flush_tlb(vaddr: Option<VirtAddr>) {
177 super::mmu::El1::flush_tlb(vaddr);
178}
179
180/// Makes a page-table entry installed by the local page-fault handler visible
181/// before retrying the faulting instruction.
182///
183/// AArch64 page-table updates are coherent with the hardware walker. As in
184/// Linux, avoiding an unconditional barrier here keeps the minor-fault fast
185/// path cheap; a rare spurious refault is safe to handle again.
186#[inline]
187pub fn update_mmu_cache(_vaddr: VirtAddr) {}
188
189/// Invalidates instruction caches in the Inner Shareable domain.
190/// Modified bytes must first be cleaned to PoU; remote execution must cross a
191/// context-synchronization event (such as exception return) after publication.
192#[inline]
193pub fn flush_icache_all() {
194 unsafe { asm!("ic ialluis; dsb ish; isb") };
195}
196
197#[inline]
198fn read_ctr_el0() -> u64 {
199 let value;
200 unsafe {
201 asm!("mrs {}, ctr_el0", out(reg) value);
202 }
203 value
204}
205
206/// Reads the data cache line size from `CTR_EL0` and returns it in bytes.
207#[inline]
208pub fn dcache_line_size_from_ctr() -> usize {
209 let ctr = read_ctr_el0();
210
211 // CTR_EL0.DminLine: bits [19:16]
212 // bytes = 4 << DminLine
213 let dminline = ((ctr >> 16) & 0xf) as usize;
214
215 4usize << dminline
216}
217
218/// Reads the instruction cache line size from `CTR_EL0` and returns it in bytes.
219#[inline]
220pub fn icache_line_size_from_ctr() -> usize {
221 let ctr = read_ctr_el0();
222
223 // CTR_EL0.IminLine: bits [3:0]
224 // bytes = 4 << IminLine
225 let iminline = (ctr & 0xf) as usize;
226
227 4usize << iminline
228}
229
230/// Reads the current kernel task's TLS base (`TPIDR_EL0`).
231///
232/// It is used to implement TLS (Thread Local Storage).
233#[inline]
234#[cfg(kernel_tls)]
235pub fn read_thread_pointer() -> KernelTlsBase {
236 KernelTlsBase::new(TPIDR_EL0.get() as usize)
237}
238
239/// Writes the current kernel task's TLS base (`TPIDR_EL0`).
240///
241/// It is used to implement TLS (Thread Local Storage).
242///
243/// # Safety
244///
245/// This function is unsafe as it changes the current CPU states.
246#[inline]
247#[cfg(kernel_tls)]
248pub unsafe fn write_thread_pointer(kernel_tls: KernelTlsBase) {
249 TPIDR_EL0.set(kernel_tls.as_usize() as _)
250}
251
252/// Enable FP/SIMD instructions by setting the `FPEN` field in `CPACR_EL1`.
253#[inline]
254pub fn enable_fp() {
255 CPACR_EL1.write(CPACR_EL1::FPEN::TrapNothing);
256 barrier::isb(barrier::SY);
257}
258
259#[cfg(feature = "uspace")]
260core::arch::global_asm!(include_str!("user_copy.S"), include_str!("user_atomic.S"),);
261
262#[cfg(feature = "uspace")]
263unsafe extern "C" {
264 /// Copies data from source to destination, where addresses may be in user
265 /// space. Equivalent to memcpy.
266 ///
267 /// # Safety
268 /// This function is unsafe because it performs raw memory operations.
269 ///
270 /// # Returns
271 /// Returns the number of bytes not copied. This means 0 indicates success,
272 /// while a value > 0 indicates failure.
273 pub fn user_copy(dst: *mut u8, src: *const u8, size: usize) -> usize;
274}
275
276/// Probes whether EL0 is permitted to access the page containing `vaddr` under
277/// the *current* user translation regime (`TTBR0_EL1`), without taking any lock.
278///
279/// Uses the `AT S1E0R` / `AT S1E0W` address-translation instruction, which asks
280/// the MMU to translate `vaddr` for the requested EL0 read or write access
281/// and reports the result in `PAR_EL1`. `PAR_EL1.F == 0` means the translation
282/// succeeded and the access is permitted — exactly the permission the CPU itself
283/// enforces for a user-mode access, read lock-free. A not-present page or one
284/// lacking the requested EL0 permission (e.g. a copy-on-write page probed for
285/// write) reports `F == 1`.
286///
287/// Returns `true` iff the MMU would permit the EL0 access.
288///
289/// # Safety
290///
291/// The caller MUST invoke this with interrupts disabled. `PAR_EL1` is a per-CPU
292/// scratch register shared across contexts; an interrupt executing another `AT`
293/// between this `AT` and the `mrs` would clobber the result. On the
294/// pointer-validation path that could turn an inaccessible page into a `true`
295/// result and thus a raw kernel dereference of an unchecked address. IRQs-off
296/// guarantees no other `AT` runs on this CPU in between. Because violating this
297/// precondition is a memory-safety hazard (not merely a wrong answer), the
298/// function is `unsafe` so every call site must establish it.
299#[cfg(feature = "uspace")]
300#[inline]
301pub unsafe fn user_access_ok_page(vaddr: usize, access: crate::UserAccessType) -> bool {
302 let par: u64;
303 // SAFETY: `AT` reads the current translation tables and writes `PAR_EL1`;
304 // `mrs` reads it back. No memory is accessed and no flags are clobbered. The
305 // caller holds IRQs off so the `AT`/`mrs` pair is not split by another `AT`.
306 unsafe {
307 if access == crate::UserAccessType::Write {
308 asm!(
309 "at s1e0w, {vaddr}",
310 "isb",
311 "mrs {par}, par_el1",
312 vaddr = in(reg) vaddr,
313 par = out(reg) par,
314 options(nostack, preserves_flags),
315 );
316 } else {
317 asm!(
318 "at s1e0r, {vaddr}",
319 "isb",
320 "mrs {par}, par_el1",
321 vaddr = in(reg) vaddr,
322 par = out(reg) par,
323 options(nostack, preserves_flags),
324 );
325 }
326 }
327 // PAR_EL1.F (bit 0): 0 = translation succeeded and the EL0 access is allowed.
328 par & 1 == 0
329}