use core::{
arch::naked_asm,
fmt,
mem::{align_of, offset_of, size_of},
ptr::NonNull,
};
use ax_memory_addr::VirtAddr;
use cpu_local::{ExecutionContextHeader, PreparedContextSwitch};
use crate::{KernelTlsBase, TaskLocalState};
#[allow(missing_docs)]
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
pub struct TrapFrame {
pub rax: u64,
pub rcx: u64,
pub rdx: u64,
pub rbx: u64,
pub rbp: u64,
pub rsi: u64,
pub rdi: u64,
pub r8: u64,
pub r9: u64,
pub r10: u64,
pub r11: u64,
pub r12: u64,
pub r13: u64,
pub r14: u64,
pub r15: u64,
pub vector: u64,
pub error_code: u64,
pub rip: u64,
pub cs: u64,
pub rflags: u64,
pub rsp: u64,
pub ss: u64,
}
impl TrapFrame {
pub const fn origin(&self) -> crate::TrapOrigin {
if self.cs & 0b11 == 0 {
crate::TrapOrigin::Kernel
} else {
crate::TrapOrigin::User
}
}
pub const fn arg0(&self) -> usize {
self.rdi as _
}
pub const fn set_arg0(&mut self, rdi: usize) {
self.rdi = rdi as _;
}
pub const fn arg1(&self) -> usize {
self.rsi as _
}
pub const fn set_arg1(&mut self, rsi: usize) {
self.rsi = rsi as _;
}
pub const fn arg2(&self) -> usize {
self.rdx as _
}
pub const fn set_arg2(&mut self, rdx: usize) {
self.rdx = rdx as _;
}
pub const fn arg3(&self) -> usize {
self.r10 as _
}
pub const fn set_arg3(&mut self, r10: usize) {
self.r10 = r10 as _;
}
pub const fn arg4(&self) -> usize {
self.r8 as _
}
pub const fn set_arg4(&mut self, r8: usize) {
self.r8 = r8 as _;
}
pub const fn arg5(&self) -> usize {
self.r9 as _
}
pub const fn set_arg5(&mut self, r9: usize) {
self.r9 = r9 as _;
}
pub const fn ip(&self) -> usize {
self.rip as _
}
pub const fn set_ip(&mut self, rip: usize) {
self.rip = rip as _;
}
pub const fn sp(&self) -> usize {
self.rsp as _
}
pub const fn set_sp(&mut self, rsp: usize) {
self.rsp = rsp as _;
}
pub const fn sysno(&self) -> usize {
self.rax as usize
}
pub const fn set_sysno(&mut self, rax: usize) {
self.rax = rax as _;
}
pub const fn retval(&self) -> usize {
self.rax as _
}
pub const fn set_retval(&mut self, rax: usize) {
self.rax = rax as _;
}
pub fn backtrace(&self) -> axbacktrace::Backtrace {
axbacktrace::Backtrace::capture_trap(self.rbp as _, self.rip as _, 0)
}
}
#[repr(C)]
#[derive(Debug, Default)]
struct ContextSwitchFrame {
r15: u64,
r14: u64,
r13: u64,
r12: u64,
rbx: u64,
rbp: u64,
rip: u64,
}
#[allow(missing_docs)]
#[repr(C, align(16))]
#[derive(Clone, Copy, Debug)]
pub struct FxsaveArea {
pub fcw: u16,
pub fsw: u16,
pub ftw: u16,
pub fop: u16,
pub fip: u64,
pub fdp: u64,
pub mxcsr: u32,
pub mxcsr_mask: u32,
pub st: [u64; 16],
pub xmm: [u64; 32],
_padding: [u64; 12],
}
const _: () = assert!(core::mem::size_of::<FxsaveArea>() == 512);
const XSAVE_AREA_SIZE: usize = 1024;
#[cfg(feature = "fp-simd")]
const XSAVE_HEADER_OFFSET: usize = 512;
#[cfg(feature = "fp-simd")]
const XSAVE_HEADER_SIZE: usize = 64;
#[cfg(feature = "fp-simd")]
const XSAVE_XCOMP_BV_OFFSET: usize = XSAVE_HEADER_OFFSET + size_of::<u64>();
#[cfg(feature = "fp-simd")]
const XSAVE_HEADER_RESERVED_OFFSET: usize = XSAVE_XCOMP_BV_OFFSET + size_of::<u64>();
#[cfg(feature = "fp-simd")]
const XFEATURE_MASK_FPSSE: u64 = (1 << 0) | (1 << 1);
#[cfg(feature = "fp-simd")]
const MXCSR_FALLBACK_MASK: u32 = 0x0000_ffbf;
#[repr(C, align(64))]
#[derive(Clone, Copy)]
pub struct UserXstate {
legacy: FxsaveArea,
rest: [u8; XSAVE_AREA_SIZE - 512],
}
const _: () = assert!(core::mem::size_of::<UserXstate>() == XSAVE_AREA_SIZE);
#[cfg(feature = "fp-simd")]
impl UserXstate {
pub const fn initial() -> Self {
ExtendedState::default().area
}
pub fn user_size() -> Option<usize> {
if !ExtendedState::xsave_enabled() {
return None;
}
let size = core::arch::x86_64::__cpuid_count(0x0d, 0).ebx as usize;
assert!(
(XSAVE_HEADER_OFFSET + XSAVE_HEADER_SIZE..=XSAVE_AREA_SIZE).contains(&size),
"enabled x86 user xstate exceeds the task-owned XSAVE area",
);
Some(size)
}
pub fn user_feature_mask() -> u64 {
if ExtendedState::xsave_enabled() {
ExtendedState::xsave_mask()
} else {
XFEATURE_MASK_FPSSE
}
}
pub const fn fxsave_area(&self) -> &FxsaveArea {
&self.legacy
}
pub fn fxsave_bytes(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
(&self.legacy as *const FxsaveArea).cast::<u8>(),
size_of::<FxsaveArea>(),
)
}
}
pub fn user_bytes(&self) -> Option<&[u8]> {
let size = Self::user_size()?;
Some(unsafe { core::slice::from_raw_parts((self as *const Self).cast::<u8>(), size) })
}
pub fn replace_fxsave_area(&mut self, area: FxsaveArea) -> bool {
if !Self::mxcsr_is_valid(area.mxcsr) {
return false;
}
self.legacy = area;
if ExtendedState::xsave_enabled() {
let features = self.xstate_bv() | XFEATURE_MASK_FPSSE;
self.write_xstate_bv(features);
}
true
}
pub fn replace_fxsave_bytes(&mut self, bytes: &[u8]) -> bool {
if bytes.len() != size_of::<FxsaveArea>() {
return false;
}
let mut area = core::mem::MaybeUninit::<FxsaveArea>::zeroed();
unsafe {
core::ptr::copy_nonoverlapping(
bytes.as_ptr(),
area.as_mut_ptr().cast::<u8>(),
bytes.len(),
);
self.replace_fxsave_area(area.assume_init())
}
}
pub fn replace_user_bytes(&mut self, bytes: &[u8]) -> bool {
let Some(user_size) = Self::user_size() else {
return false;
};
if bytes.len() != user_size {
return false;
}
let xstate_bv = read_u64(bytes, XSAVE_HEADER_OFFSET);
let xcomp_bv = read_u64(bytes, XSAVE_XCOMP_BV_OFFSET);
if xstate_bv & !ExtendedState::xsave_mask() != 0
|| xcomp_bv != 0
|| bytes[XSAVE_HEADER_RESERVED_OFFSET..XSAVE_HEADER_OFFSET + XSAVE_HEADER_SIZE]
.iter()
.any(|byte| *byte != 0)
{
return false;
}
let mxcsr = u32::from_ne_bytes(
bytes[24..28]
.try_into()
.expect("the FXSAVE MXCSR field has a fixed width"),
);
if !Self::mxcsr_is_valid(mxcsr) {
return false;
}
let destination = unsafe {
core::slice::from_raw_parts_mut((self as *mut Self).cast::<u8>(), XSAVE_AREA_SIZE)
};
destination.fill(0);
destination[..user_size].copy_from_slice(bytes);
true
}
pub fn replace_user_bytes_prefix(&mut self, bytes: &[u8]) -> bool {
let Some(user_size) = Self::user_size() else {
return false;
};
if !(XSAVE_HEADER_OFFSET + XSAVE_HEADER_SIZE..=user_size).contains(&bytes.len()) {
return false;
}
let xstate_bv = read_u64(bytes, XSAVE_HEADER_OFFSET);
if xstate_bv & !ExtendedState::xsave_mask() != 0
|| !xstate_components_fit(xstate_bv, bytes.len())
{
return false;
}
let mut complete = [0; XSAVE_AREA_SIZE];
complete[..bytes.len()].copy_from_slice(bytes);
self.replace_user_bytes(&complete[..user_size])
}
fn xstate_bv(&self) -> u64 {
read_u64(
self.user_bytes()
.expect("xstate header requires XSAVE support"),
XSAVE_HEADER_OFFSET,
)
}
fn write_xstate_bv(&mut self, value: u64) {
let bytes = value.to_ne_bytes();
unsafe {
core::ptr::copy_nonoverlapping(
bytes.as_ptr(),
(self as *mut Self).cast::<u8>().add(XSAVE_HEADER_OFFSET),
bytes.len(),
)
};
}
fn mxcsr_is_valid(mxcsr: u32) -> bool {
let mut feature_image =
unsafe { core::mem::MaybeUninit::<FxsaveArea>::zeroed().assume_init() };
unsafe {
core::arch::x86_64::_fxsave64((&mut feature_image as *mut FxsaveArea).cast::<u8>())
};
let mask = if feature_image.mxcsr_mask == 0 {
MXCSR_FALLBACK_MASK
} else {
feature_image.mxcsr_mask
};
mxcsr & !mask == 0
}
}
#[cfg(feature = "fp-simd")]
fn xstate_components_fit(xstate_bv: u64, supplied_size: usize) -> bool {
for feature in 2..u64::BITS {
if xstate_bv & (1 << feature) == 0 {
continue;
}
let component = core::arch::x86_64::__cpuid_count(0x0d, feature);
let offset = component.ebx as usize;
let size = component.eax as usize;
if size == 0
|| offset
.checked_add(size)
.is_none_or(|end| end > supplied_size)
{
return false;
}
}
true
}
#[cfg(feature = "fp-simd")]
fn read_u64(bytes: &[u8], offset: usize) -> u64 {
u64::from_ne_bytes(
bytes[offset..offset + size_of::<u64>()]
.try_into()
.expect("the XSAVE header field has a fixed width"),
)
}
pub struct ExtendedState {
area: UserXstate,
}
#[cfg(feature = "fp-simd")]
impl ExtendedState {
#[inline]
pub fn fxsave_area(&self) -> &FxsaveArea {
&self.area.legacy
}
#[inline]
#[cfg(not(feature = "host-test"))]
fn xsave_enabled() -> bool {
let cr4 = unsafe { x86::controlregs::cr4() };
cr4.contains(x86::controlregs::Cr4::CR4_ENABLE_OS_XSAVE)
}
#[inline]
#[cfg(feature = "host-test")]
fn xsave_enabled() -> bool {
false
}
#[inline]
fn xsave_mask() -> u64 {
unsafe { x86::controlregs::xcr0().bits() }
}
#[inline]
pub fn save(&mut self) {
let ptr = &mut self.area as *mut _ as *mut u8;
#[cfg(feature = "uspace")]
if let Some((mask, xsaveopt_enabled)) = super::local_state::current_cpu_user_xsave_config()
{
unsafe {
if xsaveopt_enabled {
core::arch::x86_64::_xsaveopt64(ptr, mask)
} else {
core::arch::x86_64::_xsave64(ptr, mask)
}
}
return;
}
if Self::xsave_enabled() {
unsafe { core::arch::x86_64::_xsave64(ptr, Self::xsave_mask()) }
} else {
unsafe { core::arch::x86_64::_fxsave64(ptr) }
}
}
#[inline]
pub fn restore(&self) {
let ptr = &self.area as *const _ as *const u8;
#[cfg(feature = "uspace")]
if let Some((mask, _)) = super::local_state::current_cpu_user_xsave_config() {
unsafe { core::arch::x86_64::_xrstor64(ptr, mask) }
return;
}
if Self::xsave_enabled() {
unsafe { core::arch::x86_64::_xrstor64(ptr, Self::xsave_mask()) }
} else {
unsafe { core::arch::x86_64::_fxrstor64(ptr) }
}
}
pub const fn default() -> Self {
let mut area: UserXstate = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
area.legacy.fcw = 0x37f;
area.legacy.ftw = 0x0000;
area.legacy.mxcsr = 0x1f80;
Self { area }
}
}
impl fmt::Debug for ExtendedState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("ExtendedState")
.field("fxsave_area", &self.area.legacy)
.finish()
}
}
#[repr(C)]
#[derive(Debug)]
pub struct TaskContext {
kstack_top: VirtAddr,
rsp: u64,
task_local: TaskLocalState,
#[cfg(feature = "fp-simd")]
ext_state: ExtendedState,
}
const _: () = {
assert!(size_of::<KernelTlsBase>() == size_of::<usize>());
assert!(align_of::<KernelTlsBase>() == align_of::<usize>());
assert!(offset_of!(TaskContext, kstack_top) == 0);
assert!(offset_of!(TaskContext, rsp) == size_of::<VirtAddr>());
assert!(offset_of!(TaskContext, task_local) == offset_of!(TaskContext, rsp) + size_of::<u64>());
};
impl TaskContext {
pub fn new() -> Self {
Self {
kstack_top: va!(0),
rsp: 0,
task_local: TaskLocalState::new(),
#[cfg(feature = "fp-simd")]
ext_state: ExtendedState::default(),
}
}
pub fn init(&mut self, entry: usize, kstack_top: VirtAddr, kernel_tls: KernelTlsBase) {
unsafe {
let frame_ptr = (kstack_top.as_mut_ptr() as *mut u64).sub(1);
let frame_ptr = (frame_ptr as *mut ContextSwitchFrame).sub(1);
core::ptr::write(
frame_ptr,
ContextSwitchFrame {
rip: entry as _,
..Default::default()
},
);
self.rsp = frame_ptr as u64;
}
self.kstack_top = kstack_top;
self.task_local.set_kernel_tls(kernel_tls);
}
pub fn set_context_header(&mut self, header: NonNull<ExecutionContextHeader>) {
self.task_local.set_context_header(header);
}
pub const fn context_header(&self) -> Option<NonNull<ExecutionContextHeader>> {
self.task_local.context_header()
}
pub fn prepare_switch_to(&mut self, _next_ctx: &Self) {
#[cfg(all(feature = "fp-simd", feature = "uspace"))]
{
let Some(current) = self.context_header() else {
super::local_state::assert_current_user_fp_unowned();
return;
};
let current = current.as_ptr().expose_provenance();
if super::local_state::current_user_fp_is_owner(current) {
self.ext_state.save();
super::local_state::clear_current_user_fp_owner_after_save(current);
}
}
#[cfg(all(feature = "fp-simd", not(feature = "uspace")))]
{
self.ext_state.save();
_next_ctx.ext_state.restore();
}
}
pub fn prepare_user_return_fp(&self) {
#[cfg(all(feature = "fp-simd", feature = "uspace"))]
{
let current = self
.context_header()
.expect("a userspace FPU owner requires a bound execution context")
.as_ptr()
.expose_provenance();
if super::local_state::current_user_fp_needs_restore(current) {
self.ext_state.restore();
super::local_state::publish_current_user_fp_owner(current);
}
}
}
#[cfg(all(feature = "fp-simd", feature = "uspace"))]
pub fn clone_user_fp_state_into(&self, child: &mut Self) {
assert!(
!core::ptr::eq(self, child),
"a cloned user FPU image requires a distinct task context",
);
assert!(
child.context_header().is_none(),
"a cloned user FPU image must be installed before context binding",
);
let current = self
.context_header()
.expect("a userspace FPU clone requires a bound execution context")
.as_ptr()
.expose_provenance();
if super::local_state::current_user_fp_needs_restore(current) {
self.ext_state.restore();
super::local_state::publish_current_user_fp_owner(current);
}
child.ext_state.save();
}
#[cfg(all(feature = "fp-simd", feature = "uspace"))]
pub fn capture_user_fp_state(&self) -> UserXstate {
let current = self
.context_header()
.expect("a userspace FPU snapshot requires a bound execution context")
.as_ptr()
.expose_provenance();
if super::local_state::current_user_fp_needs_restore(current) {
self.ext_state.restore();
super::local_state::publish_current_user_fp_owner(current);
}
let mut snapshot = ExtendedState::default();
snapshot.save();
snapshot.area
}
#[cfg(all(feature = "fp-simd", feature = "uspace"))]
pub fn replace_user_fp_state(&mut self, state: UserXstate) {
let current = self
.context_header()
.expect("a userspace FPU replacement requires a bound execution context")
.as_ptr()
.expose_provenance();
super::local_state::assert_current_user_fp_resettable(current);
self.ext_state.area = state;
self.ext_state.restore();
super::local_state::publish_current_user_fp_owner(current);
}
pub fn reset_user_fp_state(&mut self) {
#[cfg(all(feature = "fp-simd", feature = "uspace"))]
{
let current = self
.context_header()
.expect("a userspace FPU reset requires a bound execution context")
.as_ptr()
.expose_provenance();
super::local_state::assert_current_user_fp_resettable(current);
self.ext_state = ExtendedState::default();
self.ext_state.restore();
super::local_state::publish_current_user_fp_owner(current);
}
}
#[inline(always)]
pub unsafe fn switch_to_prepared(
&mut self,
next_ctx: &Self,
prepared: PreparedContextSwitch<'_>,
) {
unsafe { prepared.commit() };
unsafe { context_switch_raw(self, next_ctx) }
}
}
#[cfg(kernel_tls)]
#[unsafe(naked)]
unsafe extern "C" fn context_switch_raw(_current_task: &mut TaskContext, _next_task: &TaskContext) {
naked_asm!(
"
.code64
push rbp
push rbx
push r12
push r13
push r14
push r15
mov [rdi + {rsp_offset}], rsp
// Save and restore task TLS only after all Rust helpers have finished.
mov ecx, {fs_base_msr}
rdmsr
shl rdx, 32
or rax, rdx
mov [rdi + {kernel_tls_offset}], rax
mov rax, [rsi + {kernel_tls_offset}]
mov rdx, rax
shr rdx, 32
mov ecx, {fs_base_msr}
wrmsr
mov rsp, [rsi + {rsp_offset}]
pop r15
pop r14
pop r13
pop r12
pop rbx
pop rbp
ret",
rsp_offset = const offset_of!(TaskContext, rsp),
kernel_tls_offset = const offset_of!(TaskContext, task_local)
+ offset_of!(TaskLocalState, kernel_tls),
fs_base_msr = const 0xc000_0100_u32,
)
}
#[cfg(all(test, feature = "host-test", feature = "uspace"))]
mod tests {
use super::*;
#[test]
fn context_prepare_does_not_override_the_runtime_address_space_commit() {
unsafe { crate::asm::write_user_page_table(0x1000.into()) };
let mut previous = TaskContext::new();
unsafe { crate::asm::write_user_page_table(0x2000.into()) };
let next = TaskContext::new();
unsafe { crate::asm::write_user_page_table(0x3000.into()) };
previous.prepare_switch_to(&next);
assert_eq!(crate::asm::read_user_page_table().as_usize(), 0x3000);
}
}
#[cfg(not(kernel_tls))]
#[unsafe(naked)]
unsafe extern "C" fn context_switch_raw(_current_task: &mut TaskContext, _next_task: &TaskContext) {
naked_asm!(
"
.code64
push rbp
push rbx
push r12
push r13
push r14
push r15
mov [rdi + {rsp_offset}], rsp
// LinuxCurrent uses the already-published kernel GS slot. FS remains
// userspace-owned and must not be touched by a kernel task switch.
mov rsp, [rsi + {rsp_offset}]
pop r15
pop r14
pop r13
pop r12
pop rbx
pop rbp
ret",
rsp_offset = const offset_of!(TaskContext, rsp),
)
}