ax_cpu/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
8#[cfg(not(feature = "arm-el2"))]
9use super::asid::configured_tag_capacity;
10#[cfg(kernel_tls)]
11use crate::KernelTlsBase;
12#[cfg(feature = "uspace")]
13use crate::{InstalledAddressSpace, InstalledAddressSpaceMode};
14
15/// Returns the number of AArch64 ASIDs, including reserved ASID 0.
16///
17/// The result reflects both the hardware capability and the ASID width selected
18/// by the boot owner in `TCR_EL1.AS`. EL2 builds retain the conservative
19/// full-flush path because their userspace translation register contract is
20/// different from TTBR0_EL1.
21pub fn address_space_tag_capacity(_cpu_count: usize) -> u32 {
22 #[cfg(feature = "arm-el2")]
23 {
24 1
25 }
26 #[cfg(not(feature = "arm-el2"))]
27 {
28 configured_tag_capacity(
29 ID_AA64MMFR0_EL1.read(ID_AA64MMFR0_EL1::ASIDBits),
30 TCR_EL1.read(TCR_EL1::AS),
31 )
32 }
33}
34
35#[cfg(all(feature = "uspace", not(feature = "arm-el2")))]
36fn flush_tlb_asid(asid: u16) {
37 let operand = u64::from(asid) << 48;
38 // SAFETY: the caller runs at EL1. The barriers match Linux's ASID
39 // invalidation ordering: page-table stores, TLBI, completion, then fetch.
40 unsafe {
41 asm!(
42 "dsb ishst; tlbi aside1is, {operand}; dsb ish; isb",
43 operand = in(reg) operand,
44 )
45 }
46}
47
48/// Installs one complete userspace identity into TTBR0_EL1.
49///
50/// Tagged installation invalidates the incoming ASID before publishing the
51/// root. Full-flush and EL2 fallback paths install ASID 0 and invalidate every
52/// stage-1 translation.
53///
54/// # Safety
55///
56/// The caller must own the current CPU with interrupts disabled and the root
57/// must remain alive for the complete activation lease.
58#[cfg(feature = "uspace")]
59pub unsafe fn install_user_address_space(address_space: InstalledAddressSpace) {
60 address_space.validate_architecture_support();
61 #[cfg(not(feature = "arm-el2"))]
62 if matches!(address_space.mode(), InstalledAddressSpaceMode::Tagged) {
63 let capacity = address_space_tag_capacity(1);
64 if u32::from(address_space.hardware_tag()) < capacity {
65 flush_tlb_asid(address_space.hardware_tag());
66 let value = address_space.root().as_usize() as u64
67 | (u64::from(address_space.hardware_tag()) << 48);
68 TTBR0_EL1.set(value);
69 barrier::isb(barrier::SY);
70 return;
71 }
72 }
73
74 TTBR0_EL1.set(address_space.root().as_usize() as u64);
75 flush_tlb(None);
76}
77
78/// Allows the current CPU to respond to interrupts.
79///
80/// In AArch64, it unmasks IRQs by clearing the I bit in the `DAIF` register.
81#[inline]
82pub fn enable_irqs() {
83 unsafe { asm!("msr daifclr, #2") };
84}
85
86/// Makes the current CPU to ignore interrupts.
87///
88/// In AArch64, it masks IRQs by setting the I bit in the `DAIF` register.
89#[inline]
90pub fn disable_irqs() {
91 unsafe { asm!("msr daifset, #2") };
92}
93
94/// Returns whether the current CPU is allowed to respond to interrupts.
95///
96/// In AArch64, it checks the I bit in the `DAIF` register.
97#[inline]
98pub fn irqs_enabled() -> bool {
99 !DAIF.matches_all(DAIF::I::Masked)
100}
101
102/// Relaxes the current CPU and waits for interrupts.
103///
104/// It must be called with interrupts enabled, otherwise it will never return.
105#[inline]
106pub fn wait_for_irqs() {
107 aarch64_cpu::asm::wfi();
108}
109
110/// Waits for an interrupt after the caller masks local IRQ delivery.
111///
112/// AArch64 `WFI` observes enabled pending interrupt sources even while
113/// `DAIF.I` masks delivery. Keeping delivery masked through `WFI` closes the
114/// scheduler wake-loss window. The function returns with local IRQs enabled.
115#[inline]
116pub fn wait_for_irqs_disabled() {
117 debug_assert!(!irqs_enabled());
118 barrier::dsb(barrier::SY);
119 aarch64_cpu::asm::wfi();
120 enable_irqs();
121}
122
123/// Halt the current CPU.
124#[inline]
125pub fn halt() {
126 disable_irqs();
127 aarch64_cpu::asm::wfi(); // should never return
128}
129
130/// Reads the current page table root register for kernel space (`TTBR1_EL1`).
131///
132/// When the "arm-el2" feature is enabled,
133/// TTBR0_EL2 is dedicated to the Hypervisor's Stage-2 page table base address.
134///
135/// Returns the physical address of the page table root.
136#[inline]
137pub fn read_kernel_page_table() -> PhysAddr {
138 #[cfg(not(feature = "arm-el2"))]
139 let root = TTBR1_EL1.get();
140
141 #[cfg(feature = "arm-el2")]
142 let root = TTBR0_EL2.get();
143
144 pa!(root as usize)
145}
146
147/// Reads the current page table root register for user space (`TTBR0_EL1`).
148///
149/// When the "arm-el2" feature is enabled, for user-mode programs,
150/// virtualization is completely transparent to them, so there is no need to modify
151///
152/// Returns the physical address of the page table root.
153#[inline]
154pub fn read_user_page_table() -> PhysAddr {
155 const TTBR_BADDR_MASK: u64 = (1 << 48) - 1;
156 let root = TTBR0_EL1.get() & TTBR_BADDR_MASK;
157 pa!(root as usize)
158}
159
160/// Writes the register to update the current page table root for kernel space
161/// (`TTBR1_EL1`).
162///
163/// When the "arm-el2" feature is enabled,
164/// TTBR0_EL2 is dedicated to the Hypervisor's Stage-2 page table base address.
165///
166/// Note that the TLB is **NOT** flushed after this operation.
167///
168/// # Safety
169///
170/// This function is unsafe as it changes the virtual memory address space.
171#[inline]
172pub unsafe fn write_kernel_page_table(root_paddr: PhysAddr) {
173 #[cfg(not(feature = "arm-el2"))]
174 {
175 // kernel space page table use TTBR1 (0xffff_0000_0000_0000..0xffff_ffff_ffff_ffff)
176 TTBR1_EL1.set(root_paddr.as_usize() as _);
177 }
178
179 #[cfg(feature = "arm-el2")]
180 {
181 // kernel space page table at EL2 use TTBR0_EL2 (0x0000_0000_0000_0000..0x0000_ffff_ffff_ffff)
182 TTBR0_EL2.set(root_paddr.as_usize() as _);
183 }
184}
185
186/// Writes the register to update the current page table root for user space
187/// (`TTBR1_EL0`).
188/// When the "arm-el2" feature is enabled, for user-mode programs,
189/// virtualization is completely transparent to them, so there is no need to modify
190///
191/// Note that the TLB is **NOT** flushed after this operation.
192///
193/// # Safety
194///
195/// This function is unsafe as it changes the virtual memory address space.
196#[inline]
197pub unsafe fn write_user_page_table(root_paddr: PhysAddr) {
198 TTBR0_EL1.set(root_paddr.as_usize() as _);
199}
200
201/// Makes page-table writes visible to the inner-shareable domain.
202///
203/// Cross-CPU shootdown must execute this before sending any IPI. A barrier on
204/// the remote CPU cannot order page-table writes performed by the initiating
205/// CPU.
206#[inline]
207pub fn synchronize_page_table_writes() {
208 unsafe { asm!("dsb ishst") };
209}
210
211/// Flushes the local TLB.
212///
213/// If `vaddr` is [`None`], flushes the entire TLB. Otherwise, flushes the TLB
214/// entry that maps the given virtual address.
215#[inline]
216pub fn flush_tlb(vaddr: Option<VirtAddr>) {
217 if let Some(vaddr) = vaddr {
218 const VA_MASK: usize = (1 << 44) - 1; // VA[55:12] => bits[43:0]
219 let operand = (vaddr.as_usize() >> 12) & VA_MASK;
220
221 #[cfg(not(feature = "arm-el2"))]
222 unsafe {
223 // TLB Invalidate by VA, All ASID, EL1, local PE. The runtime owns
224 // cross-CPU targeting and invokes this function on every selected
225 // CPU only after the initiator publishes its page-table writes.
226 asm!("dsb nshst; tlbi vaae1, {}; dsb nsh; isb", in(reg) operand)
227 }
228 #[cfg(feature = "arm-el2")]
229 unsafe {
230 // TLB Invalidate by VA, EL2, local PE.
231 asm!("dsb nshst; tlbi vae2, {}; dsb nsh; isb", in(reg) operand)
232 }
233 } else {
234 // flush the entire TLB
235 #[cfg(not(feature = "arm-el2"))]
236 unsafe {
237 // TLB Invalidate by VMID, All at stage 1, EL1, local PE.
238 asm!("dsb nshst; tlbi vmalle1; dsb nsh; isb")
239 }
240 #[cfg(feature = "arm-el2")]
241 unsafe {
242 // TLB Invalidate All, EL2, local PE.
243 asm!("dsb nshst; tlbi alle2; dsb nsh; isb")
244 }
245 }
246}
247
248/// Makes a page-table entry installed by the local page-fault handler visible
249/// before retrying the faulting instruction.
250///
251/// AArch64 page-table updates are coherent with the hardware walker. As in
252/// Linux, avoiding an unconditional barrier here keeps the minor-fault fast
253/// path cheap; a rare spurious refault is safe to handle again.
254#[inline]
255pub fn update_mmu_cache(_vaddr: VirtAddr) {}
256
257/// Flushes the entire instruction cache.
258#[inline]
259pub fn flush_icache_all() {
260 unsafe { asm!("ic iallu; dsb sy; isb") };
261}
262
263#[inline]
264fn read_ctr_el0() -> u64 {
265 let value;
266 unsafe {
267 asm!("mrs {}, ctr_el0", out(reg) value);
268 }
269 value
270}
271
272/// Reads the data cache line size from `CTR_EL0` and returns it in bytes.
273#[inline]
274pub fn dcache_line_size_from_ctr() -> usize {
275 let ctr = read_ctr_el0();
276
277 // CTR_EL0.DminLine: bits [19:16]
278 // bytes = 4 << DminLine
279 let dminline = ((ctr >> 16) & 0xf) as usize;
280
281 4usize << dminline
282}
283
284/// Reads the instruction cache line size from `CTR_EL0` and returns it in bytes.
285#[inline]
286pub fn icache_line_size_from_ctr() -> usize {
287 let ctr = read_ctr_el0();
288
289 // CTR_EL0.IminLine: bits [3:0]
290 // bytes = 4 << IminLine
291 let iminline = (ctr & 0xf) as usize;
292
293 4usize << iminline
294}
295
296/// Cleans a data cache range to the point of unification.
297#[inline]
298pub fn clean_dcache_range_to_pou(vaddr: VirtAddr, size: usize) {
299 if size == 0 {
300 return;
301 }
302
303 let line_size = dcache_line_size_from_ctr();
304 let start = vaddr.as_usize() & !(line_size - 1);
305 let end = (vaddr.as_usize() + size + line_size - 1) & !(line_size - 1);
306
307 for line in (start..end).step_by(line_size) {
308 unsafe { asm!("dc cvau, {0:x}", in(reg) line) };
309 }
310
311 unsafe { asm!("dsb sy") };
312}
313
314/// Cleans and invalidates the data cache line that covers the given address.
315///
316/// This is useful for publishing small pieces of data to other agents that may
317/// observe memory outside the local D-cache, such as spin tables used to start
318/// secondary CPUs.
319#[inline]
320pub fn flush_dcache_line(vaddr: VirtAddr) {
321 unsafe { asm!("dc ivac, {0:x}; dsb sy; isb", in(reg) vaddr.as_usize()) };
322}
323
324/// Writes exception vector base address register (`VBAR_EL1`).
325///
326/// # Safety
327///
328/// This function is unsafe as it changes the exception handling behavior of the
329/// current CPU.
330#[inline]
331pub unsafe fn write_exception_vector_base(vbar: usize) {
332 #[cfg(not(feature = "arm-el2"))]
333 VBAR_EL1.set(vbar as _);
334 #[cfg(feature = "arm-el2")]
335 VBAR_EL2.set(vbar as _);
336}
337
338/// Reads the current kernel task's TLS base (`TPIDR_EL0`).
339///
340/// It is used to implement TLS (Thread Local Storage).
341#[inline]
342#[cfg(kernel_tls)]
343pub fn read_thread_pointer() -> KernelTlsBase {
344 KernelTlsBase::new(TPIDR_EL0.get() as usize)
345}
346
347/// Writes the current kernel task's TLS base (`TPIDR_EL0`).
348///
349/// It is used to implement TLS (Thread Local Storage).
350///
351/// # Safety
352///
353/// This function is unsafe as it changes the current CPU states.
354#[inline]
355#[cfg(kernel_tls)]
356pub unsafe fn write_thread_pointer(kernel_tls: KernelTlsBase) {
357 TPIDR_EL0.set(kernel_tls.as_usize() as _)
358}
359
360/// Enable FP/SIMD instructions by setting the `FPEN` field in `CPACR_EL1`.
361#[inline]
362pub fn enable_fp() {
363 CPACR_EL1.write(CPACR_EL1::FPEN::TrapNothing);
364 barrier::isb(barrier::SY);
365}
366
367#[cfg(feature = "uspace")]
368core::arch::global_asm!(include_str!("user_copy.S"), include_str!("user_atomic.S"),);
369
370#[cfg(feature = "uspace")]
371unsafe extern "C" {
372 /// Copies data from source to destination, where addresses may be in user
373 /// space. Equivalent to memcpy.
374 ///
375 /// # Safety
376 /// This function is unsafe because it performs raw memory operations.
377 ///
378 /// # Returns
379 /// Returns the number of bytes not copied. This means 0 indicates success,
380 /// while a value > 0 indicates failure.
381 pub fn user_copy(dst: *mut u8, src: *const u8, size: usize) -> usize;
382}
383
384/// Probes whether EL0 is permitted to access the page containing `vaddr` under
385/// the *current* user translation regime (`TTBR0_EL1`), without taking any lock.
386///
387/// Uses the `AT S1E0R` / `AT S1E0W` address-translation instruction, which asks
388/// the MMU to translate `vaddr` for the requested EL0 read or write access
389/// and reports the result in `PAR_EL1`. `PAR_EL1.F == 0` means the translation
390/// succeeded and the access is permitted — exactly the permission the CPU itself
391/// enforces for a user-mode access, read lock-free. A not-present page or one
392/// lacking the requested EL0 permission (e.g. a copy-on-write page probed for
393/// write) reports `F == 1`.
394///
395/// Returns `true` iff the MMU would permit the EL0 access.
396///
397/// # Safety
398///
399/// The caller MUST invoke this with interrupts disabled. `PAR_EL1` is a per-CPU
400/// scratch register shared across contexts; an interrupt executing another `AT`
401/// between this `AT` and the `mrs` would clobber the result. On the
402/// pointer-validation path that could turn an inaccessible page into a `true`
403/// result and thus a raw kernel dereference of an unchecked address. IRQs-off
404/// guarantees no other `AT` runs on this CPU in between. Because violating this
405/// precondition is a memory-safety hazard (not merely a wrong answer), the
406/// function is `unsafe` so every call site must establish it.
407#[cfg(all(feature = "uspace", not(feature = "arm-el2")))]
408#[inline]
409pub unsafe fn user_access_ok_page(vaddr: usize, access: crate::UserAccessType) -> bool {
410 let par: u64;
411 // SAFETY: `AT` reads the current translation tables and writes `PAR_EL1`;
412 // `mrs` reads it back. No memory is accessed and no flags are clobbered. The
413 // caller holds IRQs off so the `AT`/`mrs` pair is not split by another `AT`.
414 unsafe {
415 if access == crate::UserAccessType::Write {
416 asm!(
417 "at s1e0w, {vaddr}",
418 "isb",
419 "mrs {par}, par_el1",
420 vaddr = in(reg) vaddr,
421 par = out(reg) par,
422 options(nostack, preserves_flags),
423 );
424 } else {
425 asm!(
426 "at s1e0r, {vaddr}",
427 "isb",
428 "mrs {par}, par_el1",
429 vaddr = in(reg) vaddr,
430 par = out(reg) par,
431 options(nostack, preserves_flags),
432 );
433 }
434 }
435 // PAR_EL1.F (bit 0): 0 = translation succeeded and the EL0 access is allowed.
436 par & 1 == 0
437}
438
439/// `arm-el2` builds run the hypervisor at EL2, where the EL1&0 `AT` probe does
440/// not describe guest-user access, so always fall back to the locked slow path.
441///
442/// # Safety
443///
444/// No precondition — this stub reads nothing and always returns `false`. It is
445/// `unsafe` only to share the signature of the aarch64 EL1 probe (which requires
446/// IRQs-off), so callers can use one `unsafe` block across all targets.
447#[cfg(all(feature = "uspace", feature = "arm-el2"))]
448#[inline]
449pub unsafe fn user_access_ok_page(_vaddr: usize, _access: crate::UserAccessType) -> bool {
450 false
451}