Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
resolver = "3"
members = ["kernel"]

[features]
exception-self-test = ["kernel/exception-self-test"]

[package]
name = "rustalis"
version = "0.1.0"
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
67 changes: 67 additions & 0 deletions kernel/src/arch/x86_64/idt.rs
Original file line number Diff line number Diff line change
@@ -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<InterruptDescriptorTable> = 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);
}
}
2 changes: 2 additions & 0 deletions kernel/src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
@@ -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();
}
8 changes: 8 additions & 0 deletions kernel/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![feature(abi_x86_interrupt)]

#![no_std]
#![no_main]

Expand All @@ -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);
Expand Down
Loading