Skip to content

Commit

Permalink
kernel+x86_64: implement page fault ISR and scheduler callback
Browse files Browse the repository at this point in the history
  • Loading branch information
Qix- committed Jan 25, 2025
1 parent 3d1ecbb commit acd45b1
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 42 deletions.
9 changes: 9 additions & 0 deletions examples/no-std/page-alloc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ fn main() {

println!("token type: {:?}", Key(&ty));

const TARGET_ADDR: usize = 0x400_0000_0000;

// Map it in.
// TODO: This is not yet implemented.

// Try to read and write from it.
unsafe {
*(TARGET_ADDR as *mut u64) = 0x1234_5678_9ABC_DEF0;
let value = *(TARGET_ADDR as *const u64);
println!("value: {value:#016X}");
}
}
39 changes: 39 additions & 0 deletions oro-arch-x86_64/src/interrupt/isr_page_fault.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! The page fault exception ISR (Interrupt Service Routine).
use core::arch::asm;
use oro_kernel::scheduler::PageFaultType;
use oro_sync::Lock;

crate::isr! {
/// The ISR (Interrupt Service Routine) for page fault exceptions.
unsafe fn isr_page_fault(kernel, _user_task, err_code) -> Option<Switch> {
let cr2: usize;
// SAFETY: `cr2` is a register that holds the faulting address. It is safe to read.
unsafe {
asm!("mov {}, cr2", out(reg) cr2, options(nostack, preserves_flags));
}

// Decode the error code.
// TODO(qix-): Use a register definition for this.
let is_ifetch = err_code & (1 << 4) != 0;
let is_user = err_code & (1 << 2) != 0;
let is_write = err_code & (1 << 1) != 0;

if !is_user {
panic!("kernel page fault: err={err_code:#032b} cr2={cr2:#016X} core={}", kernel.id());
}

let err_type = if is_ifetch {
PageFaultType::Execute
} else if is_write {
PageFaultType::Write
} else {
PageFaultType::Read
};

// SAFETY: `event_page_fault` specifies that, in the event we return back to the task,
// SAFETY: that the task has been instructed to re-try the memory operation. x86_64
// SAFETY: does this by design, so we must do no special handling here.
Some(unsafe { kernel.scheduler().lock().event_page_fault(err_type, cr2) })
}
}
31 changes: 24 additions & 7 deletions oro-arch-x86_64/src/interrupt/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,25 @@ macro_rules! isr_table {
/// when the corresponding interrupt is triggered.
#[macro_export]
macro_rules! isr {
($(#[$meta:meta])* unsafe fn $isr_name:ident($kernel:ident, $user_task:ident) -> Option<Switch> { $($stmt:stmt);* }) => {
// NOTE(qix-): "@" prefixed match patterns are PRIVATE. Do not use them publicly.
(@ $isr_name:ident, $err_code:ident) => {
::oro_macro::paste! {
$crate::isr_store_task_and_jmp_err!($isr_name %% _rust);
}
};

(@ $isr_name:ident) => {
::oro_macro::paste! {
$crate::isr_store_task_and_jmp!($isr_name %% _rust);
}
};

($(#[$meta:meta])* unsafe fn $isr_name:ident($kernel:ident, $user_task:ident $(, $err_code:ident)?) -> Option<Switch> $blk:block) => {
#[doc = concat!("The ISR (Interrupt Service Routine) trampoline stub for [`", stringify!($isr_name), "_rust`].")]
#[naked]
#[no_mangle]
pub unsafe extern "C" fn $isr_name() -> ! {
::oro_macro::paste! {
$crate::isr_store_task_and_jmp!($isr_name %% _rust);
}
$crate::isr!(@ $isr_name $(, $err_code)?);
}

::oro_macro::paste! {
Expand All @@ -91,6 +102,14 @@ macro_rules! isr {
let irq_stack_ptr: u64;
::core::arch::asm!("", out("rcx") irq_stack_ptr, options(nostack, preserves_flags));

$(
let $err_code = {
let err_code: u64;
::core::arch::asm!("", out("rdx") err_code, options(nostack, preserves_flags));
err_code
};
)?

let $kernel = $crate::Kernel::get();

let $user_task = {
Expand All @@ -110,9 +129,7 @@ macro_rules! isr {
}
};

let switch: Option<::oro_kernel::scheduler::Switch<$crate::Arch>> = {
$($stmt);*
};
let switch: Option<::oro_kernel::scheduler::Switch<$crate::Arch>> = $blk;

let switch = match (switch, $user_task) {
(Some(s), _) => s,
Expand Down
1 change: 1 addition & 0 deletions oro-arch-x86_64/src/interrupt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod macros;
crate::isr_table! {
/// The IDT (Interrupt Descriptor Table) for the kernel.
static IDT = {
PAGE_FAULT[14] => isr_page_fault,
TIMER_VECTOR[32] => isr_sys_timer,
APIC_SVR_VECTOR[255] => isr_apic_svr,
};
Expand Down
46 changes: 40 additions & 6 deletions oro-arch-x86_64/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ pub fn initialize_user_irq_stack(page_slice: &mut [u64], entry_point: u64, stack
write_u64(0); // r13
write_u64(0); // r14
write_u64(0); // r15
// Not needed, technically, but kept so that interrupts can be entered from
// either the kernel or a user task (since kernel rflags must be saved).
write_u64(0); // rflags

written
Expand Down Expand Up @@ -388,8 +386,6 @@ pub unsafe extern "C" fn oro_x86_64_user_to_user() {
"mov es, ax",
"mov fs, ax",
"mov gs, ax",
// Not needed, technically, but kept so that interrupts can be entered from
// either the kernel or a user task.
"popfq",
"pop r15",
"pop r14",
Expand Down Expand Up @@ -448,8 +444,46 @@ macro_rules! isr_store_task_and_jmp {
"push r13",
"push r14",
"push r15",
// Not needed, technically, but kept so that interrupts can be entered from
// either the kernel or a user task.
"pushfq",
"mov rcx, rsp",
concat!("jmp ", stringify!($jmp_to)),
"ud2",
);
};
}

/// [`isr_store_task_and_jmp`] but with an error code stored to `rdx`.
///
/// All the same rules apply.
#[macro_export]
macro_rules! isr_store_task_and_jmp_err {
($jmp_to:ident) => {
::core::arch::naked_asm!(
"cli",
// RDX is what we're using for the error code.
// However, it currently holds an application's value.
// We need to save it before we clobber it.
"push rdx",
// Now we can load the error code into RDX.
"mov rdx, [rsp + 8]",
// We can't pop twice, and a sub is needless here,
// so for the two first "pushes", we store directly
// to the stack.
"mov [rsp + 8], rax",
"mov [rsp], rbx",
"push rcx",
"push rdx",
"push rsi",
"push rdi",
"push rbp",
"push r8",
"push r9",
"push r10",
"push r11",
"push r12",
"push r13",
"push r14",
"push r15",
"pushfq",
"mov rcx, rsp",
concat!("jmp ", stringify!($jmp_to)),
Expand Down
99 changes: 70 additions & 29 deletions oro-kernel/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,40 +206,70 @@ impl<A: Arch> Scheduler<A> {
/// disabled before calling this function.** At no point
/// can other scheduler methods be invoked while this function
/// is running.
///
/// # Panics
/// Will panic if the kernel ever gets into an invalid state
/// (indicating a bug in the scheduler logic / thread state machine).
#[expect(clippy::missing_panics_doc)]
#[must_use]
pub unsafe fn event_system_call(&mut self, request: &SystemCallRequest) -> Switch<A> {
let coming_from_user = if let Some(thread) = self.current.take() {
let response = crate::syscall::dispatch(&thread, request);
let Some(thread) = self.current.take() else {
panic!("event_system_call() called with no current thread");
};

// If the thread was stopped or terminated by the syscall, we need to
// handle it specially.
match (thread.with(|t| t.run_state()), response) {
(RunState::Running, InterfaceResponse::Immediate(response)) => {
self.current = Some(thread.clone());
// No timer scheduling necessary.
return Switch::UserResume(thread.clone(), Some(response));
}
(RunState::Stopped, InterfaceResponse::Immediate(response)) => {
let (sub, handle) = InFlightSystemCall::new();
thread.with_mut(|t| t.await_system_call_response(self.kernel.id(), handle));
sub.submit(response);
Some(thread.id())
}
(RunState::Terminated, _) => Some(thread.id()),
(RunState::Running | RunState::Stopped, InterfaceResponse::Pending(handle)) => {
thread.with_mut(|t| t.await_system_call_response(self.kernel.id(), handle));
Some(thread.id())
}
let response = crate::syscall::dispatch(&thread, request);

// If the thread was stopped or terminated by the syscall, we need to
// handle it specially.
match (thread.with(|t| t.run_state()), response) {
(RunState::Running, InterfaceResponse::Immediate(response)) => {
self.current = Some(thread.clone());
// No timer scheduling necessary.
return Switch::UserResume(thread.clone(), Some(response));
}
} else {
None
};
(RunState::Stopped, InterfaceResponse::Immediate(response)) => {
let (sub, handle) = InFlightSystemCall::new();
thread.with_mut(|t| t.await_system_call_response(self.kernel.id(), handle));
sub.submit(response);
}
(RunState::Terminated, _) => {}
(RunState::Running | RunState::Stopped, InterfaceResponse::Pending(handle)) => {
thread.with_mut(|t| t.await_system_call_response(self.kernel.id(), handle));
}
}

let switch = Switch::from_schedule_action(self.pick_user_thread(), coming_from_user);
let switch = Switch::from_schedule_action(self.pick_user_thread(), Some(thread.id()));
self.kernel.handle().schedule_timer(1000);
switch
}

/// Indicates to the kernel that a page fault has occurred.
///
/// # Safety
/// Calling architectures **must** treat "return back to same task"
/// [`Switch`]es as to mean "retry the faulting memory operation". The
/// kernel will NOT attempt to recover from fatal or unexpected page faults.
///
/// **Interrupts or any other asynchronous events must be disabled before
/// calling this function.** At no point can other scheduler methods be
/// invoked while this function is running.
#[expect(clippy::missing_panics_doc)]
#[must_use]
pub unsafe fn event_page_fault(
&mut self,
fault_type: PageFaultType,
vaddr: usize,
) -> Switch<A> {
// TODO(qix-): For now, we terminate the thread.
// TODO(qix-): This will be fleshed out much more in the future.
let Some(thread) = self.current.take() else {
panic!("event_page_fault() called with no current thread");
};
let id = thread.id();
unsafe {
thread.with_mut(|t| t.terminate());
}
dbg_warn!(
"thread {:#016X} terminated due to page fault: {fault_type:?} at {vaddr:016X}",
id
);
let switch = Switch::from_schedule_action(self.pick_user_thread(), Some(id));
self.kernel.handle().schedule_timer(1000);
switch
}
Expand Down Expand Up @@ -321,3 +351,14 @@ impl<A: Arch> Switch<A> {
}
}
}

/// The type of page fault that is being handled.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageFaultType {
/// A read is being performed.
Read,
/// A write is being performed.
Write,
/// Instructions are being fetched for execution.
Execute,
}
10 changes: 10 additions & 0 deletions oro-kernel/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,16 @@ impl<A: Arch> Thread<A> {
}
}

/// Terminates the thread immediately.
///
/// # Safety
/// Caller must be ready to switch to a different thread.
pub(crate) unsafe fn terminate(&mut self) {
// TODO(qix-): This isn't very fleshed out for now; the bigger
// TODO(qix-): goal is to get it working.
self.set_run_state_unchecked(RunState::Terminated);
}

/// Tells the thread it's waiting for an in-flight system call response.
///
/// # Panics
Expand Down

0 comments on commit acd45b1

Please sign in to comment.