RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-29 23:41:12 -0700
commitdd01e731c4dc78d44c54c1f6c1abd9108e5b6a50
parent9ebb4aa97444ff9dd043ab0c20797578cb73eee1
treebrowse files at this commit
Add IDT/exceptions, GDT+TSS double-fault stack, PIC timer + keyboard interrupts
M.cargo/config.toml+2 -1
MCargo.lock+17 -0
MCargo.toml+5 -0
MREADME.md+91 -32
Mboot.png+0 -0
Mscripts/run-headless.sh+7 -1
Ascripts/runner.sh+11 -0
Mscripts/screenshot.sh+36 -7
Asrc/gdt.rs+67 -0
Asrc/interrupts.rs+202 -0
Msrc/main.rs+46 -11
Msrc/serial.rs+6 -1
Msrc/vga_buffer.rs+38 -1

13 files changed, 528 insertions, 54 deletions

@@ -8,4 +8,5 @@ target = "x86_64-rust_kernel.json"  # `cargo run` -> build a bootable image and boot it in QEMU. [target.'cfg(target_os = "none")']-runner = "bootimage runner"+# scripts/runner.sh = `bootimage runner` + a cache fix (see the script).+runner = "scripts/runner.sh"
@@ -42,6 +42,21 @@ dependencies = [ ]  [[package]]+name = "pc-keyboard"+version = "0.7.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ed089a1fbffe3337a1a345501c981f1eb1e47e69de5a40e852433e12953c3174"++[[package]]+name = "pic8259"+version = "0.11.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "62d9a86c292b165f757e47e7fd66855def189b2564609bc4203727b27c33db22"+dependencies = [+ "x86_64",+]++[[package]] name = "raw-cpuid" version = "10.7.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -56,6 +71,8 @@ version = "0.1.0" dependencies = [  "bootloader",  "lazy_static",+ "pc-keyboard",+ "pic8259",  "spin 0.5.2",  "uart_16550",  "volatile 0.2.7",
@@ -11,6 +11,8 @@ volatile = "0.2.6"      # stops the compiler optimising away VGA buffer writes spin = "0.5.2"          # spinlock Mutex (no OS = no std::sync::Mutex) x86_64 = "0.15"         # safe wrappers for x86 instructions/ports (hlt, outb, ...) uart_16550 = "0.3"      # driver for the serial port (COM1)+pic8259 = "0.11"        # driver for the two chained 8259 interrupt controllers+pc-keyboard = "0.7"     # turns PS/2 scancodes into key events / characters  [dependencies.lazy_static] version = "1.0"@@ -21,6 +23,9 @@ features = ["spin_no_std"] # printing its greeting (used by scripts/run-headless.sh). Without it the # kernel just halts forever so you can look at the QEMU window. headless = []+# Build with `--features stack-overflow` to deliberately overflow the kernel+# stack after boot, to check the double-fault handler (GDT/TSS/IST) catches it.+stack-overflow = []  # No unwinding in a kernel: just abort on panic. [profile.dev]
@@ -2,8 +2,10 @@  A tiny hobby x86_64 kernel written in Rust, following the 2nd edition of Philipp Oppermann's [Writing an OS in Rust](https://os.phil-opp.com/).-Right now it boots in QEMU, prints a greeting on the VGA text screen and on-the serial port (COM1), and then halts.+Right now it boots in QEMU, prints to the VGA text screen and the serial port+(COM1), handles CPU exceptions (breakpoint, page fault, double fault on its own+stack), runs the timer interrupt (uptime clock top-right) and echoes what you+type on the PS/2 keyboard.  ![boot screenshot](boot.png) @@ -16,13 +18,14 @@ the serial port (COM1), and then halts. | `rust-src` component | – | source of `core`, recompiled for our bare-metal target | | `llvm-tools-preview` component | – | `llvm-objcopy` etc., used by `bootimage` | | `bootimage` (cargo subcommand) | 0.10.5 | links kernel + bootloader into a bootable disk image |-| QEMU (`qemu-system-x86`) | 10.0.13 | the emulator we boot in |+| QEMU (`qemu-system-x86`, `qemu-system-gui`) | 10.0.13 | the emulator we boot in (`-gui` adds the GTK/SDL window) |  `rust-toolchain.toml` pins the project to nightly with the two components, so `cargo` inside this directory picks them up automatically.  Crates: `bootloader` 0.9.35 (BIOS bootloader used by the 2nd edition),-`volatile` 0.2, `spin` 0.5, `x86_64` 0.15, `uart_16550` 0.3, `lazy_static` 1.5.+`volatile` 0.2, `spin` 0.5, `x86_64` 0.15, `uart_16550` 0.3, `lazy_static` 1.5,+`pic8259` 0.11 (interrupt controller), `pc-keyboard` 0.7 (scancode decoding).  Fresh setup on another machine: @@ -30,7 +33,7 @@ Fresh setup on another machine: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh   # rustup rustup toolchain install nightly -c rust-src -c llvm-tools-preview cargo install bootimage-sudo apt install qemu-system-x86+sudo apt install qemu-system-x86 qemu-system-gui ```  ## Build and run@@ -38,66 +41,117 @@ sudo apt install qemu-system-x86 ```sh cargo bootimage                     # build -> target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin cargo run                           # build + boot in a QEMU window; serial output also prints in the terminal-                                    # (kernel halts forever, close the window or press Ctrl+C to quit) ``` +**Trying the keyboard:** with `cargo run`, click into the QEMU window and type;+characters appear on the bottom line of the screen *and* in your terminal (via+serial). Shift works; Enter starts a new line; arrows/F-keys show as e.g.+`<ArrowUp>`. Backspace isn't handled yet (shows a block) -- a good first+exercise. To get the mouse/keyboard back from QEMU press Ctrl+Alt+G; close the+window or press Ctrl+C in the terminal to quit. (Over SSH without X, use+`cargo run -- -display curses` to see the VGA screen right in your terminal;+quit from another terminal with `pkill qemu-system`.)+ Headless (no window, good for SSH / scripts):  ```sh-./scripts/run-headless.sh           # builds with --features headless, boots with -display none,-                                    # kernel exits QEMU via isa-debug-exit; script exits 0 on success-cargo run --features headless -- -display none   # same thing via cargo-                                    # (QEMU's exit status is 33 = success; cargo just reports it as non-zero)-cargo run -- -display none          # headless but kernel keeps running; Ctrl+C to stop-./scripts/screenshot.sh [out.png]   # boots, saves the VGA screen to boot.png via QEMU monitor `screendump`+./scripts/run-headless.sh                 # boot, breakpoint, ~3 s of timer ticks, then QEMU exits (script exit 0)+./scripts/run-headless.sh stack-overflow  # deliberately overflow the stack; passes if the double-fault handler catches it+cargo run --features headless -- -display none                 # same as the first, via cargo+cargo run --features headless,stack-overflow -- -display none  # same as the second+                                          # (QEMU's exit status is 33 = success; cargo just reports it as non-zero)+cargo run -- -display none                # headless but kernel keeps running; Ctrl+C to stop+./scripts/screenshot.sh [out.png] [text]  # boot, "type" text via QEMU monitor `sendkey`, check it echoes+                                          # on serial, save the VGA screen to boot.png ``` -Expected serial output:+Cargo features (in `Cargo.toml`):++| Feature | Effect |+|---|---|+| `headless` | print a `.` per timer tick to serial and exit QEMU after 54 ticks (~3 s); panics exit with failure |+| `stack-overflow` | after boot, recurse forever to test the double-fault handler (with `headless`: exit success when caught) |++Expected serial output of `./scripts/run-headless.sh` (stack frame dump trimmed):  ``` Hello from Tyler's Rust kernel! (via serial COM1)-Kernel booted successfully.+EXCEPTION: BREAKPOINT+InterruptStackFrame { instruction_pointer: ..., ... }+Survived the breakpoint, still running.+Timer + keyboard interrupts enabled.+.................. 18 ticks+.................. 36 ticks+.................. 54 ticks+[ok] timer works, exiting ```  Extra args after `--` in `cargo run -- ...` are appended to the QEMU command-line, e.g. `cargo run -- -d int -no-reboot` to debug triple faults, or-`cargo run -- -s -S` to wait for `gdb` on port 1234.+line, e.g. `cargo run -- -d int -no-reboot` to log every interrupt and stop on+triple faults, or `cargo run -- -s -S` to wait for `gdb` on port 1234.  ## Project layout  ```-.cargo/config.toml       build-std + custom target + "bootimage runner" as the cargo runner+.cargo/config.toml       build-std + custom target + scripts/runner.sh as the cargo runner rust-toolchain.toml      pin nightly + rust-src + llvm-tools-preview x86_64-rust_kernel.json  custom target: no OS, no red zone, soft-float, abort on panic, rust-lld-Cargo.toml               deps, `headless` feature, QEMU run-args for `cargo run`-src/main.rs              entry point `_start`, panic handler, QEMU exit helper-src/vga_buffer.rs        VGA text-mode writer + print!/println! macros-src/serial.rs            COM1 UART writer + serial_print!/serial_println! macros+Cargo.toml               deps, `headless` / `stack-overflow` features, QEMU run-args for `cargo run`+src/main.rs              entry point `_start`, init order, panic handler, hlt_loop, QEMU exit helper+src/gdt.rs               GDT + TSS with a separate IST stack for the double-fault handler+src/interrupts.rs        IDT; breakpoint / page fault / double fault handlers; 8259 PIC;+                         timer (uptime + ticks) and keyboard (echo) interrupt handlers+src/vga_buffer.rs        VGA text-mode writer, print!/println!, uptime display+src/serial.rs            COM1 UART writer + serial_print!/serial_println!+scripts/runner.sh        `cargo run` runner: `bootimage runner` + stale-image fix scripts/run-headless.sh  headless boot with timeout + exit-code check-scripts/screenshot.sh    headless boot + PNG screenshot via QEMU monitor+scripts/screenshot.sh    headless boot + typed keys + PNG screenshot via QEMU monitor boot.png                 screenshot of the VGA screen ``` +## What the interrupt code does (chapters 5-7)++- **CPU exceptions / IDT** (`interrupts.rs`): the IDT maps vector numbers to handler+  functions written with `extern "x86-interrupt"` (nightly `abi_x86_interrupt`), so+  the compiler saves all registers and returns with `iretq`. `_start` fires an+  `int3` on purpose; the breakpoint handler prints the stack frame and execution+  continues. A page-fault handler prints the faulting address from CR2.+- **Double faults** (`gdt.rs`): a stack overflow hits the guard page -> page fault ->+  the CPU can't push the exception frame on the broken stack -> double fault ->+  same again -> *triple fault* = machine reset. We load our own GDT containing a+  TSS whose Interrupt Stack Table entry 0 points at a separate 20 KiB static stack,+  and mark the double-fault IDT entry to use it. Try `./scripts/run-headless.sh stack-overflow`.+- **Hardware interrupts**: the two 8259 PICs are remapped to vectors 32-47 (0-31 are+  CPU exceptions) and then `sti` enables interrupts. The timer (IRQ 0, ~18.2 Hz)+  bumps an atomic tick counter and redraws the uptime clock; the keyboard (IRQ 1)+  reads the scancode from port 0x60, decodes it with `pc-keyboard` (US layout,+  scancode set 1) and echoes it. Each handler must send End-Of-Interrupt to the+  PIC or no further interrupts arrive. `print!`/`serial_print!` disable interrupts+  while holding their lock, otherwise an interrupt handler printing while+  `_start` holds the lock would deadlock. The idle loop uses `hlt`, so QEMU+  doesn't burn a host CPU core.+ ## Roadmap -Already done (roughly chapters 1–3, plus the serial/QEMU-exit bits of chapter 4):+Already done (chapters 1-3 and 5-7, plus the serial/QEMU-exit bits of chapter 4):  - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones) - [x] VGA text mode printing — [phil-opp](https://os.phil-opp.com/vga-text-mode/), [OSDev: Printing to Screen](https://wiki.osdev.org/Printing_To_Screen) - [x] Serial output + QEMU exit — [phil-opp: Testing](https://os.phil-opp.com/testing/), [OSDev: Serial Ports](https://wiki.osdev.org/Serial_Ports)+- [x] CPU exceptions / IDT — breakpoint & page fault handlers. [phil-opp: CPU Exceptions](https://os.phil-opp.com/cpu-exceptions/), [OSDev: IDT](https://wiki.osdev.org/Interrupt_Descriptor_Table), [OSDev: Exceptions](https://wiki.osdev.org/Exceptions)+- [x] GDT / TSS / double faults — separate IST stack so stack overflows don't triple-fault. [phil-opp: Double Faults](https://os.phil-opp.com/double-fault-exceptions/), [OSDev: GDT](https://wiki.osdev.org/Global_Descriptor_Table), [OSDev: TSS](https://wiki.osdev.org/Task_State_Segment)+- [x] Hardware interrupts — 8259 PIC, timer, PS/2 keyboard echo, `hlt` idle loop. [phil-opp: Hardware Interrupts](https://os.phil-opp.com/hardware-interrupts/), [OSDev: 8259 PIC](https://wiki.osdev.org/8259_PIC), [OSDev: PIT](https://wiki.osdev.org/Programmable_Interval_Timer), [OSDev: PS/2 Keyboard](https://wiki.osdev.org/PS/2_Keyboard) (later: [APIC](https://wiki.osdev.org/APIC))  Next stages: -1. **Testing** — custom test framework (`cargo test` in QEMU). [phil-opp: Testing](https://os.phil-opp.com/testing/)-2. **CPU exceptions / IDT** — breakpoint & page fault handlers. [phil-opp: CPU Exceptions](https://os.phil-opp.com/cpu-exceptions/), [OSDev: IDT](https://wiki.osdev.org/Interrupt_Descriptor_Table), [OSDev: Exceptions](https://wiki.osdev.org/Exceptions)-3. **GDT / TSS / double faults** — separate stack so stack overflows don't triple-fault. [phil-opp: Double Faults](https://os.phil-opp.com/double-fault-exceptions/), [OSDev: GDT](https://wiki.osdev.org/Global_Descriptor_Table), [OSDev: TSS](https://wiki.osdev.org/Task_State_Segment)-4. **Hardware interrupts** — 8259 PIC, timer, PS/2 keyboard. [phil-opp: Hardware Interrupts](https://os.phil-opp.com/hardware-interrupts/), [OSDev: 8259 PIC](https://wiki.osdev.org/8259_PIC), [OSDev: PIT](https://wiki.osdev.org/Programmable_Interval_Timer), [OSDev: PS/2 Keyboard](https://wiki.osdev.org/PS/2_Keyboard) (later: [APIC](https://wiki.osdev.org/APIC))-5. **Paging** — 4-level page tables, mapping frames. [phil-opp: Introduction to Paging](https://os.phil-opp.com/paging-introduction/), [phil-opp: Paging Implementation](https://os.phil-opp.com/paging-implementation/), [OSDev: Paging](https://wiki.osdev.org/Paging)-6. **Heap allocation** — `alloc` crate, `Box`/`Vec`, allocator designs. [phil-opp: Heap Allocation](https://os.phil-opp.com/heap-allocation/), [phil-opp: Allocator Designs](https://os.phil-opp.com/allocator-designs/), [OSDev: Memory Allocation](https://wiki.osdev.org/Memory_Allocation)-7. **Multitasking** — cooperative async/await executor first, then preemptive threads. [phil-opp: Async/Await](https://os.phil-opp.com/async-await/), [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems), [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial)-8. **User mode & syscalls** — ring 3, `syscall`/`sysret`, loading ELF programs. (Beyond the phil-opp series.) [OSDev: Getting to Ring 3](https://wiki.osdev.org/Getting_to_Ring_3), [OSDev: System Calls](https://wiki.osdev.org/System_Calls), [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER), [OSDev: ELF](https://wiki.osdev.org/ELF)-9. **Filesystem & shell** — VFS, a RAM disk / FAT, a tiny shell reading the keyboard. [OSDev: File Systems](https://wiki.osdev.org/File_Systems), [OSDev: VFS](https://wiki.osdev.org/VFS), [OSDev: FAT](https://wiki.osdev.org/FAT), [OSDev: Initrd](https://wiki.osdev.org/Initrd)+1. **Testing** — custom test framework (`cargo test` in QEMU; needs a `lib.rs` split). [phil-opp: Testing](https://os.phil-opp.com/testing/)+2. **Small exercises** — handle Backspace, set the PIT to a faster rate ([OSDev: PIT](https://wiki.osdev.org/Programmable_Interval_Timer)), add handlers for more exceptions (divide error, general protection fault).+3. **Paging** — 4-level page tables, mapping frames. [phil-opp: Introduction to Paging](https://os.phil-opp.com/paging-introduction/), [phil-opp: Paging Implementation](https://os.phil-opp.com/paging-implementation/), [OSDev: Paging](https://wiki.osdev.org/Paging)+4. **Heap allocation** — `alloc` crate, `Box`/`Vec`, allocator designs. [phil-opp: Heap Allocation](https://os.phil-opp.com/heap-allocation/), [phil-opp: Allocator Designs](https://os.phil-opp.com/allocator-designs/), [OSDev: Memory Allocation](https://wiki.osdev.org/Memory_Allocation)+5. **Multitasking** — cooperative async/await executor first, then preemptive threads. [phil-opp: Async/Await](https://os.phil-opp.com/async-await/), [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems), [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial)+6. **User mode & syscalls** — ring 3, `syscall`/`sysret`, loading ELF programs. (Beyond the phil-opp series.) [OSDev: Getting to Ring 3](https://wiki.osdev.org/Getting_to_Ring_3), [OSDev: System Calls](https://wiki.osdev.org/System_Calls), [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER), [OSDev: ELF](https://wiki.osdev.org/ELF)+7. **Filesystem & shell** — VFS, a RAM disk / FAT, a tiny shell reading the keyboard. [OSDev: File Systems](https://wiki.osdev.org/File_Systems), [OSDev: VFS](https://wiki.osdev.org/VFS), [OSDev: FAT](https://wiki.osdev.org/FAT), [OSDev: Initrd](https://wiki.osdev.org/Initrd)  Other good references: [OSDev wiki main page](https://wiki.osdev.org/Expanded_Main_Page), [Intel SDM](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdms.html),@@ -114,3 +168,8 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi   warning comes from building `core` with build-std and is harmless. - `bootimage` prints "`CARGO_MANIFEST_DIR` env variable not set" when run outside   `cargo run`; also harmless.+- Stale disk image when switching cargo features: cargo re-uses a previously built+  kernel ELF with an *old* mtime, so bootimage thinks its cached image is current+  and boots the wrong build. `scripts/runner.sh` and the scripts `touch` the ELF+  first to avoid this. If you call `cargo bootimage --features ...` by hand after+  switching features, `touch target/x86_64-rust_kernel/debug/rust_kernel` first.

Binary file changed.

@@ -2,10 +2,16 @@ # Build the kernel with the `headless` feature and boot it in QEMU with no # window. Serial output goes to this terminal; the kernel powers QEMU off via # the isa-debug-exit device when done. A timeout guards against hangs.+#+# Usage: ./scripts/run-headless.sh                 # normal boot, exits after ~3 s of timer ticks+#        ./scripts/run-headless.sh stack-overflow  # extra cargo features, comma separated set -euo pipefail cd "$(dirname "$0")/.." -cargo bootimage --features headless+cargo build --features "headless${1:+,$1}"+# Force bootimage to repack the disk image (see scripts/runner.sh for why).+touch target/x86_64-rust_kernel/debug/rust_kernel+cargo bootimage --features "headless${1:+,$1}" IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin  set +e
@@ -0,0 +1,11 @@+#!/usr/bin/env bash+# Cargo runner (see .cargo/config.toml): `cargo run` calls this with the+# kernel ELF path followed by any extra args.+#+# Why not call `bootimage runner` directly? When you switch cargo features+# (e.g. --features headless and back), cargo re-uses an older kernel binary+# with an *old* timestamp, so bootimage thinks its cached disk image is still+# up to date and boots the wrong kernel. Touching the ELF forces a rebuild.+set -euo pipefail+touch "$1"+exec bootimage runner "$@"
@@ -1,25 +1,54 @@ #!/usr/bin/env bash-# Boot the (non-headless) kernel with no window, wait a moment, then ask the-# QEMU monitor to dump the VGA screen to boot.png. Handy on a headless box.+# Boot the normal (non-headless) kernel with no window, "type" some keys into+# it through the QEMU monitor's `sendkey` command, then dump the VGA screen to+# a PNG. Doubles as an automated keyboard-interrupt test: the typed text is+# echoed on the serial port too, so we can grep for it.+#+# Usage: ./scripts/screenshot.sh [out.png] [text-to-type] set -euo pipefail cd "$(dirname "$0")/.." +cargo build+# Force bootimage to repack the disk image (see scripts/runner.sh for why).+touch target/x86_64-rust_kernel/debug/rust_kernel cargo bootimage IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin OUT="${1:-$PWD/boot.png}"+TEXT="${2:-Hello, kernel!}" SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX)+SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)  qemu-system-x86_64 -drive format=raw,file="$IMG" \-    -serial stdio -display none -monitor unix:"$SOCK",server,nowait &+    -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$!-sleep 3   # give the BIOS + bootloader + kernel time to draw+sleep 3   # give the BIOS + bootloader + kernel time to boot  # Talk to the QEMU monitor over its UNIX socket (python avoids needing socat).-python3 - "$SOCK" "$OUT" <<'PY'+python3 - "$SOCK" "$OUT" "$TEXT" <<'PY' import socket, sys, time-s = socket.socket(socket.AF_UNIX); s.connect(sys.argv[1])-s.sendall(f"screendump {sys.argv[2]} -f png\n".encode()); time.sleep(1)+sock, out, text = sys.argv[1:]+# QEMU key names for the characters we might type.+names = {' ': 'spc', ',': 'comma', '.': 'dot', '!': 'shift-1', '?': 'shift-slash',+         '-': 'minus', '\n': 'ret'}+def key(c):+    if c in names: return names[c]+    if c.isupper(): return 'shift-' + c.lower()+    return c+s = socket.socket(socket.AF_UNIX); s.connect(sock)+for c in text:+    s.sendall(f"sendkey {key(c)}\n".encode()); time.sleep(0.1)+time.sleep(0.5)+s.sendall(f"screendump {out} -f png\n".encode()); time.sleep(1) s.sendall(b"quit\n"); time.sleep(0.5) PY wait $QPID || true+echo "---- serial output ----"+cat "$SERIAL_LOG"; echo+echo "-----------------------"+if grep -qF -- "$TEXT" "$SERIAL_LOG"; then+    echo "[screenshot] keyboard echo OK: '$TEXT'"+else+    echo "[screenshot] keyboard echo NOT found"+fi+rm -f "$SERIAL_LOG" echo "[screenshot] saved $OUT"
@@ -0,0 +1,67 @@+//! GDT (Global Descriptor Table) + TSS (Task State Segment).+//!+//! In 64-bit mode segmentation is mostly dead, but we still need a GDT for one+//! important reason: it is how the CPU finds the TSS, and the TSS holds the+//! Interrupt Stack Table (IST) -- a list of known-good stacks the CPU can+//! switch to when an exception happens.+//!+//! Why it matters: if the kernel stack overflows, the CPU hits the guard page+//! and raises a page fault. Pushing the exception frame onto the (already+//! overflowed) stack faults again -> double fault -> same problem -> TRIPLE+//! fault, which resets the machine. Giving the double-fault handler its own+//! IST stack breaks that chain.++use lazy_static::lazy_static;+use x86_64::VirtAddr;+use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};+use x86_64::structures::tss::TaskStateSegment;++/// Which IST slot (0..=6) the double-fault handler uses.+pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;++lazy_static! {+    static ref TSS: TaskStateSegment = {+        let mut tss = TaskStateSegment::new();+        tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {+            // No memory allocator yet, so the stack is just a static array.+            // (It has no guard page, so don't do anything recursive in the+            // double-fault handler!)+            const STACK_SIZE: usize = 4096 * 5;+            static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];++            // Stacks grow downwards on x86, so hand the CPU the *end* address.+            let stack_start = VirtAddr::from_ptr(&raw const STACK);+            stack_start + STACK_SIZE as u64+        };+        tss+    };+}++lazy_static! {+    /// The GDT, plus the selectors (indexes) of the entries we added.+    static ref GDT: (GlobalDescriptorTable, Selectors) = {+        let mut gdt = GlobalDescriptorTable::new();+        let code_selector = gdt.append(Descriptor::kernel_code_segment());+        let tss_selector = gdt.append(Descriptor::tss_segment(&TSS));+        (gdt, Selectors { code_selector, tss_selector })+    };+}++struct Selectors {+    code_selector: SegmentSelector,+    tss_selector: SegmentSelector,+}++/// Load our GDT, point CS at our code segment and load the TSS.+pub fn init() {+    use x86_64::instructions::segmentation::{CS, Segment};+    use x86_64::instructions::tables::load_tss;++    GDT.0.load();+    unsafe {+        // The old CS value points into the bootloader's GDT, which we just+        // replaced, so reload it. Then tell the CPU where our TSS is.+        CS::set_reg(GDT.1.code_selector);+        load_tss(GDT.1.tss_selector);+    }+}
@@ -0,0 +1,202 @@+//! Interrupt handling: the IDT, CPU exception handlers and hardware IRQs.+//!+//! The IDT (Interrupt Descriptor Table) maps each interrupt vector number to+//! a handler function:+//!   * vectors 0..=31  are CPU exceptions (divide error, page fault, ...)+//!   * vectors 32..=47 are where we remap the 8259 PIC's 16 hardware IRQs+//!+//! Handlers use the special `extern "x86-interrupt"` calling convention: the+//! compiler saves/restores all registers and returns with `iretq`.++use crate::{gdt, print, println, serial_print, serial_println};+use core::sync::atomic::{AtomicU64, Ordering};+use lazy_static::lazy_static;+use pic8259::ChainedPics;+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame, PageFaultErrorCode};++// ---------------------------------------------------------------------------+// 8259 PIC (Programmable Interrupt Controller)+// ---------------------------------------------------------------------------++/// The PICs deliver IRQ 0..15 on vectors 0..15 by default, which clash with+/// CPU exceptions. We remap them to 32..47.+pub const PIC_1_OFFSET: u8 = 32;+pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;++/// Two PICs: primary handles IRQ 0-7, secondary IRQ 8-15 (chained via IRQ 2).+pub static PICS: spin::Mutex<ChainedPics> =+    spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });++/// Vector numbers of the hardware interrupts we handle.+#[derive(Debug, Clone, Copy)]+#[repr(u8)]+pub enum InterruptIndex {+    Timer = PIC_1_OFFSET, // IRQ 0: Programmable Interval Timer, ~18.2 Hz by default+    Keyboard,             // IRQ 1: PS/2 keyboard+}++impl InterruptIndex {+    fn as_u8(self) -> u8 {+        self as u8+    }+}++// ---------------------------------------------------------------------------+// IDT setup+// ---------------------------------------------------------------------------++lazy_static! {+    /// The IDT must live forever (`'static`): the CPU keeps a pointer to it.+    static ref IDT: InterruptDescriptorTable = {+        let mut idt = InterruptDescriptorTable::new();+        idt.breakpoint.set_handler_fn(breakpoint_handler);+        idt.page_fault.set_handler_fn(page_fault_handler);+        unsafe {+            // Run the double-fault handler on its own known-good stack (IST).+            idt.double_fault+                .set_handler_fn(double_fault_handler)+                .set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX);+        }+        idt[InterruptIndex::Timer.as_u8()].set_handler_fn(timer_interrupt_handler);+        idt[InterruptIndex::Keyboard.as_u8()].set_handler_fn(keyboard_interrupt_handler);+        idt+    };+}++/// Load the IDT into the CPU (`lidt`).+pub fn init_idt() {+    IDT.load();+}++// ---------------------------------------------------------------------------+// CPU exception handlers+// ---------------------------------------------------------------------------++/// `int3` (what debuggers use). Harmless: print and carry on.+extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {+    println!("EXCEPTION: BREAKPOINT");+    serial_println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);+}++/// Page fault: accessing unmapped/protected memory. CR2 holds the bad address.+/// We can't fix it yet (no paging code), so report and stop.+extern "x86-interrupt" fn page_fault_handler(+    stack_frame: InterruptStackFrame,+    error_code: PageFaultErrorCode,+) {+    use x86_64::registers::control::Cr2;+    println!("EXCEPTION: PAGE FAULT at {:?}", Cr2::read());+    serial_println!(+        "EXCEPTION: PAGE FAULT\nAccessed address: {:?}\nError code: {:?}\n{:#?}",+        Cr2::read(),+        error_code,+        stack_frame+    );+    crate::hlt_loop();+}++/// Double fault: an exception happened while handling another one. Must not+/// return (hence `-> !`). Runs on the IST stack set up in gdt.rs.+extern "x86-interrupt" fn double_fault_handler(+    stack_frame: InterruptStackFrame,+    _error_code: u64, // always 0 for double faults+) -> ! {+    serial_println!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame);++    // In the deliberate stack-overflow test, reaching here means success.+    #[cfg(all(feature = "headless", feature = "stack-overflow"))]+    {+        serial_println!("[ok] stack overflow was caught by the double fault handler");+        crate::exit_qemu(crate::QemuExitCode::Success);+    }++    panic!("EXCEPTION: DOUBLE FAULT");+}++// ---------------------------------------------------------------------------+// Hardware interrupt handlers+// ---------------------------------------------------------------------------++/// Timer ticks since boot. Atomic, so it's safe to touch from an interrupt.+pub static TICKS: AtomicU64 = AtomicU64::new(0);++/// IRQ 0. Fires ~18.2 times per second (the PIT's power-on default).+extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFrame) {+    let ticks = TICKS.fetch_add(1, Ordering::Relaxed) + 1;++    // Show uptime in the top-right corner of the screen, updated each second.+    if ticks % 18 == 0 {+        crate::vga_buffer::show_uptime(ticks / 18);+    }++    // Headless runs: print a dot per tick to serial (proof the timer fires),+    // then stop after ~3 seconds so scripts finish on their own. (Not done in+    // normal runs, so the dots don't get mixed into your keyboard echo.)+    #[cfg(feature = "headless")]+    {+        serial_print!(".");+        if ticks % 18 == 0 {+            serial_println!(" {} ticks", ticks);+        }+        #[cfg(not(feature = "stack-overflow"))]+        if ticks >= 54 {+            serial_println!("[ok] timer works, exiting");+            crate::exit_qemu(crate::QemuExitCode::Success);+        }+    }++    // Tell the PIC we're done, or it will never send another timer interrupt.+    unsafe {+        PICS.lock().notify_end_of_interrupt(InterruptIndex::Timer.as_u8());+    }+}++/// IRQ 1. The keyboard controller has a scancode waiting at I/O port 0x60;+/// we must read it (or no more keyboard interrupts arrive), decode it with+/// `pc-keyboard`, and echo the character to the screen and serial port.+extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {+    use pc_keyboard::{DecodedKey, HandleControl, KeyCode, Keyboard, ScancodeSet1, layouts};+    use spin::Mutex;+    use x86_64::instructions::port::Port;++    // Decoder state (shift held? multi-byte scancode in progress?) persists+    // between interrupts, so it lives in a static.+    static KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> = Mutex::new(Keyboard::new(+        ScancodeSet1::new(),+        layouts::Us104Key,+        HandleControl::Ignore,+    ));++    let mut keyboard = KEYBOARD.lock();+    let mut port = Port::new(0x60);+    let scancode: u8 = unsafe { port.read() };++    // add_byte: scancode -> key event (press/release); process_keyevent:+    // key event -> character, applying shift/caps-lock etc.+    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {+        if let Some(key) = keyboard.process_keyevent(key_event) {+            match key {+                DecodedKey::Unicode(character) => {+                    print!("{}", character);+                    serial_print!("{}", character);+                }+                // Modifier keys only change how *other* keys decode; the+                // decoder already tracks them, so don't print anything.+                DecodedKey::RawKey(+                    KeyCode::LShift | KeyCode::RShift | KeyCode::LControl+                    | KeyCode::RControl | KeyCode::LAlt | KeyCode::RAltGr+                    | KeyCode::CapsLock,+                ) => {}+                DecodedKey::RawKey(key) => {+                    // Other keys without a character (arrows, F1, ...)+                    print!("<{:?}>", key);+                    serial_print!("<{:?}>", key);+                }+            }+        }+    }++    unsafe {+        PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());+    }+}
@@ -1,4 +1,4 @@-//! A minimal freestanding x86_64 kernel.+//! A small freestanding x86_64 kernel. //! //! Boot flow: BIOS -> `bootloader` crate (switches to 64-bit long mode, sets up //! paging) -> `_start` below. There is no OS underneath us, so no `std`, no@@ -6,14 +6,17 @@  #![no_std] // don't link the Rust standard library (it needs an OS) #![no_main] // no normal `main`: the bootloader jumps to `_start`--mod serial; // COM1 serial port -> shows up in your terminal via `-serial stdio`-mod vga_buffer; // 80x25 VGA text mode screen at physical address 0xb8000+#![feature(abi_x86_interrupt)] // nightly: lets us write `extern "x86-interrupt" fn`  // The bootloader is linked in by `cargo bootimage`, not called from our code; // this line just tells Rust/cargo the dependency is intentional. extern crate bootloader; +mod gdt; // GDT + TSS: gives the double-fault handler its own stack+mod interrupts; // IDT, exception handlers, PIC, timer + keyboard+mod serial; // COM1 serial port -> shows up in your terminal via `-serial stdio`+mod vga_buffer; // 80x25 VGA text mode screen at physical address 0xb8000+ use core::panic::PanicInfo;  /// Kernel entry point. The bootloader calls this after setting up the CPU.@@ -21,18 +24,49 @@ use core::panic::PanicInfo; #[unsafe(no_mangle)] pub extern "C" fn _start() -> ! {     println!("Hello from Tyler's Rust kernel!");-    println!("VGA text mode is working. Next stop: interrupts.");-     serial_println!("Hello from Tyler's Rust kernel! (via serial COM1)");-    serial_println!("Kernel booted successfully."); -    // In headless mode, power QEMU off so scripts/CI finish on their own.-    #[cfg(feature = "headless")]-    exit_qemu(QemuExitCode::Success);+    init(); +    // Fire a breakpoint exception on purpose: the handler prints a message,+    // returns, and we carry on as if nothing happened.+    x86_64::instructions::interrupts::int3();+    println!("Survived the breakpoint, still running.");+    serial_println!("Survived the breakpoint, still running.");++    #[cfg(feature = "stack-overflow")]+    {+        serial_println!("Overflowing the stack on purpose...");+        stack_overflow();+    }++    println!("Type something! (keyboard echo)");+    serial_println!("Timer + keyboard interrupts enabled.");++    // Nothing else to do: sleep until the next interrupt, forever. The timer+    // and keyboard handlers do their work in between.     hlt_loop(); } +/// Set up everything interrupt-related. Order matters:+/// GDT/TSS first (the IDT refers to the IST stack), then the IDT, then the+/// PICs, and only then enable interrupts (`sti`).+fn init() {+    gdt::init();+    interrupts::init_idt();+    unsafe { interrupts::PICS.lock().initialize() };+    x86_64::instructions::interrupts::enable();+}++/// Recurse forever until we run off the end of the stack into the guard page.+#[cfg(feature = "stack-overflow")]+#[allow(unconditional_recursion)]+fn stack_overflow() {+    stack_overflow();+    // Stop the compiler turning the recursion into a loop (tail call).+    unsafe { core::ptr::read_volatile(&0u8) };+}+ /// Called on `panic!`. Print the message to both outputs, then stop. #[panic_handler] fn panic(info: &PanicInfo) -> ! {@@ -43,7 +77,8 @@ fn panic(info: &PanicInfo) -> ! {     hlt_loop(); } -/// Sleep the CPU until the next interrupt, forever (cheaper than `loop {}`).+/// Sleep the CPU until the next interrupt, forever (cheaper than `loop {}`,+/// which would keep a host CPU core at 100%). pub fn hlt_loop() -> ! {     loop {         x86_64::instructions::hlt();
@@ -18,7 +18,12 @@ lazy_static! { #[doc(hidden)] pub fn _print(args: core::fmt::Arguments) {     use core::fmt::Write;-    SERIAL1.lock().write_fmt(args).expect("printing to serial failed");+    // Disable interrupts while we hold the lock. Otherwise an interrupt+    // handler that also prints would spin forever waiting for a lock held by+    // the code it interrupted (a deadlock).+    x86_64::instructions::interrupts::without_interrupts(|| {+        SERIAL1.lock().write_fmt(args).expect("printing to serial failed");+    }); }  /// Like `print!`, but to the serial port.
@@ -122,10 +122,47 @@ lazy_static! {     }); } +/// Draw " uptime: N s " right-aligned on the top row (row 0), in a different+/// colour. The normal Writer only writes the bottom row and scrolls up, so if+/// text scrolls over this, the next update simply redraws it. Called from the+/// timer interrupt handler (interrupts already off), so taking the lock is safe.+pub fn show_uptime(seconds: u64) {+    // Format the number by hand, right to left: no heap, so no `format!`.+    let mut digits = [0u8; 20]; // u64 has at most 20 decimal digits+    let mut start = digits.len();+    let mut n = seconds;+    loop {+        start -= 1;+        digits[start] = b'0' + (n % 10) as u8;+        n /= 10;+        if n == 0 {+            break;+        }+    }++    let color = ColorCode::new(Color::Black, Color::LightGray);+    let mut writer = WRITER.lock();+    // Write the pieces one after another, ending at the right screen edge.+    let parts: [&[u8]; 3] = [b" uptime: ", &digits[start..], b"s "];+    let len: usize = parts.iter().map(|p| p.len()).sum();+    let mut col = BUFFER_WIDTH - len;+    for part in parts {+        for &byte in part {+            writer.buffer.chars[0][col].write(ScreenChar { ascii_character: byte, color_code: color });+            col += 1;+        }+    }+}+ #[doc(hidden)] pub fn _print(args: fmt::Arguments) {     use core::fmt::Write;-    WRITER.lock().write_fmt(args).unwrap();+    // Disable interrupts while we hold the lock. Otherwise an interrupt+    // handler that also prints would spin forever waiting for a lock held by+    // the code it interrupted (a deadlock).+    x86_64::instructions::interrupts::without_interrupts(|| {+        WRITER.lock().write_fmt(args).unwrap();+    }); }  /// `print!` for the VGA screen.