RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / README.md
12025 bytes · raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# rust-kernel

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 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

## What's installed (on this box)

| Tool | Version | Why |
|---|---|---|
| rustup | 1.29.2 | toolchain manager (in `~/.cargo/bin`, ahead of Debian's `/usr/bin/cargo`) |
| Rust nightly | rustc 1.101.0-nightly (2026-10-29) | custom JSON targets + `build-std` are nightly-only |
| `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`, `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,
`pic8259` 0.11 (interrupt controller), `pc-keyboard` 0.7 (scancode decoding).

Fresh setup on another machine:

```sh
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 qemu-system-gui
```

## Build and run

```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
```

**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                 # 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
```

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)
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 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 + 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` / `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 + 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 (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; 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,
the [blog_os repo]https://github.com/phil-opp/blog_os (one branch per post, e.g. `post-04`),
and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11]https://github.com/rust-osdev/bootloader crate.

## Notes / gotchas

- The target JSON changes occasionally with new nightlies (e.g. `target-pointer-width`
  became an integer, `rustc-abi` was added). If a nightly update breaks the build,
  compare with the latest `x86_64-blog_os.json` in the blog_os repo, or pin a date in
  `rust-toolchain.toml` (`channel = "nightly-2026-10-29"`).
- The "packages contain code that will be rejected by a future version of Rust: core"
  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.