diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 307acca..40c4455 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt, clippy - name: Format @@ -39,4 +39,4 @@ jobs: - name: Install QEMU run: sudo apt-get update && sudo apt-get install -y qemu-system-x86 - name: Build boot image - run: cargo build --verbose + run: cargo build --features exception-self-test --verbose diff --git a/Cargo.toml b/Cargo.toml index a198f84..df93081 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,9 @@ resolver = "3" members = ["kernel"] +[features] +exception-self-test = ["kernel/exception-self-test"] + [package] name = "rustalis" version = "0.1.0" diff --git a/README.md b/README.md index ec3c6a9..316b57f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The project is deliberately incremental. A subsystem is only marked complete aft - [x] x86_64 architecture boundary - [x] Interrupt subsystem boundary - [ ] GDT / TSS -- [ ] IDT +- [x] IDT (breakpoint and invalid-opcode entries) - [ ] Exception handlers - [ ] Double-fault stack @@ -130,6 +130,12 @@ cargo test --workspace ```bash cargo build + +Optional breakpoint exception smoke test: + +```bash +cargo build --features exception-self-test +``` ``` ### Run in QEMU diff --git a/docs/architecture.md b/docs/architecture.md index 45393e7..53394be 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,6 +28,15 @@ arch -> interrupt -> scheduler -> process -> syscall -> user space These are architectural targets, not claims that those subsystems are implemented yet. +## Exception handling + +The x86_64 interrupt layer now installs a static IDT with handlers for: + +- Vector 3: breakpoint (`#BP` / `INT3`), which logs the saved interrupt frame and resumes execution. +- Vector 6: invalid opcode (`#UD`), which logs the faulting instruction pointer and enters the kernel panic path. + +The `exception-self-test` Cargo feature triggers `INT3` during boot so the breakpoint path can be exercised without making the normal boot path intentionally fault. + ## Boot invariant Every milestone must leave a bootable kernel or a clearly documented reason why the milestone temporarily cannot boot. No placeholder subsystem should report successful initialization. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 6414a56..871c5b9 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -4,7 +4,11 @@ version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" +[features] +exception-self-test = [] + [dependencies] bootloader_api = "0.11.17" uart_16550 = "0.3.2" spin = "0.9" +x86_64 = "0.15.5" diff --git a/kernel/src/arch/x86_64/idt.rs b/kernel/src/arch/x86_64/idt.rs new file mode 100644 index 0000000..8e9d94d --- /dev/null +++ b/kernel/src/arch/x86_64/idt.rs @@ -0,0 +1,67 @@ +//! Interrupt Descriptor Table setup and CPU exception handlers. + +use spin::Once; +use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame}; + +static IDT: Once = Once::new(); + +/// Install the Rustalis exception handlers and load the IDT. +/// +/// The IDT is initialized exactly once and stored in a static so its address +/// remains stable for as long as the CPU can dispatch interrupts. +pub fn init() { + let idt = IDT.call_once(|| { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.invalid_opcode.set_handler_fn(invalid_opcode_handler); + idt + }); + + // SAFETY: IDT lives in a static Once and is therefore never moved or + // destroyed after initialization. Interrupts remain disabled until the + // table has been loaded. + idt.load(); +} + +/// Trigger INT3 for the optional boot-time exception self-test. +#[cfg(feature = "exception-self-test")] +pub fn self_test() { + x86_64::instructions::interrupts::int3(); +} + +/// Handle INT3/#BP without modifying the saved execution state. +extern "x86-interrupt" fn breakpoint_handler(frame: InterruptStackFrame) { + crate::logger::info("BREAKPOINT exception (#BP)"); + crate::logger::info_u64("Breakpoint RIP", frame.instruction_pointer.as_u64()); + crate::logger::info_u64("Breakpoint CS", frame.code_segment.bits() as u64); + crate::logger::info_u64("Breakpoint RFLAGS", frame.cpu_flags.bits()); + crate::logger::info_u64("Breakpoint RSP", frame.stack_pointer.as_u64()); + crate::logger::info_u64("Breakpoint SS", frame.stack_segment.bits() as u64); +} + +/// Handle an invalid opcode as a controlled kernel failure. +/// +/// Returning would retry the faulting instruction because #UD reports the +/// instruction that caused the exception. Entering the kernel panic path +/// avoids silently looping on the same invalid instruction. +extern "x86-interrupt" fn invalid_opcode_handler(frame: InterruptStackFrame) -> ! { + crate::logger::error("INVALID OPCODE exception (#UD)"); + crate::logger::info_u64("Invalid opcode RIP", frame.instruction_pointer.as_u64()); + crate::logger::info_u64("Invalid opcode CS", frame.code_segment.bits() as u64); + panic!("invalid opcode exception"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn idt_registers_exception_handlers() { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.invalid_opcode.set_handler_fn(invalid_opcode_handler); + + assert_ne!(idt.breakpoint.handler_addr().as_u64(), 0); + assert_ne!(idt.invalid_opcode.handler_addr().as_u64(), 0); + } +} diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 28700d3..96a3e16 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -1,8 +1,10 @@ //! x86_64-specific kernel facilities. pub mod interrupts; +pub mod idt; /// Initialize CPU-specific facilities that are safe to establish at boot. pub fn init() { interrupts::init(); + idt::init(); } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 7d2fb3b..7703ec0 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -1,3 +1,5 @@ +#![feature(abi_x86_interrupt)] + #![no_std] #![no_main] @@ -17,6 +19,12 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! { logger::info("Boot information received"); arch::x86_64::init(); logger::info("x86_64 architecture layer initialized"); + #[cfg(feature = "exception-self-test")] + { + logger::info("Running breakpoint exception self-test"); + arch::x86_64::idt::self_test(); + logger::info("Breakpoint exception self-test passed"); + } let summary = memory::summarize(&boot_info.memory_regions); logger::info_u64("Memory regions", summary.region_count as u64); logger::info_u64("Usable memory bytes", summary.usable_bytes);