rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-29 23:48:16 -0700 |
|---|---|---|
| commit | 866aaa3978d692723cabcfa91d2ada0517c190b8 | |
| parent | dd01e731c4dc78d44c54c1f6c1abd9108e5b6a50 | |
| tree | browse files at this commit | |
Add paging, frame allocator, heap + allocator designs, lib.rs and cargo test suite
| M | .cargo/config.toml | +1 -1 |
| M | Cargo.lock | +34 -0 |
| M | Cargo.toml | +37 -6 |
| M | README.md | +163 -26 |
| M | boot.png | +0 -0 |
| A | src/allocator.rs | +102 -0 |
| A | src/allocator/bump.rs | +53 -0 |
| A | src/allocator/fixed_size_block.rs | +100 -0 |
| M | src/interrupts.rs | +18 -3 |
| A | src/lib.rs | +131 -0 |
| M | src/main.rs | +98 -62 |
| A | src/memory.rs | +102 -0 |
| M | src/vga_buffer.rs | +21 -0 |
| A | tests/basic_boot.rs | +25 -0 |
| A | tests/heap_allocation.rs | +80 -0 |
| A | tests/should_panic.rs | +27 -0 |
| A | tests/stack_overflow.rs | +51 -0 |
17 files changed, 1043 insertions, 98 deletions
@@ -1,6 +1,6 @@ [unstable] json-target-spec = true # allow our custom x86_64-rust_kernel.json-build-std = ["core", "compiler_builtins"] # compile `core` for that target ourselves+build-std = ["core", "compiler_builtins", "alloc"] # compile core (+ alloc for the heap) for our target build-std-features = ["compiler-builtins-mem"] # provide memcpy/memset/etc. [build]
@@ -42,6 +42,24 @@ dependencies = [ ] [[package]]+name = "linked_list_allocator"+version = "0.9.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "549ce1740e46b291953c4340adcd74c59bcf4308f4cac050fd33ba91b7168f4a"+dependencies = [+ "spinning_top",+]++[[package]]+name = "lock_api"+version = "0.4.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"+dependencies = [+ "scopeguard",+]++[[package]] name = "pc-keyboard" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -71,6 +89,7 @@ version = "0.1.0" dependencies = [ "bootloader", "lazy_static",+ "linked_list_allocator", "pc-keyboard", "pic8259", "spin 0.5.2",@@ -86,6 +105,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]]+name = "scopeguard"+version = "1.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"++[[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -98,6 +123,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" [[package]]+name = "spinning_top"+version = "0.2.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0"+dependencies = [+ "lock_api",+]++[[package]] name = "uart_16550" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6,13 +6,17 @@ edition = "2024" [dependencies] # Bootloader 0.9.x: the version used by the 2nd edition of "Writing an OS in Rust". # `cargo bootimage` links it with our kernel into a bootable BIOS disk image.-bootloader = "0.9"+# `map_physical_memory`: the bootloader maps *all* physical RAM somewhere in+# virtual memory and tells us where (BootInfo.physical_memory_offset), so we+# can read/write page tables, which live at physical addresses.+bootloader = { version = "0.9", features = ["map_physical_memory"] } 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+linked_list_allocator = "0.9" # ready-made heap allocator (fallback for ours) [dependencies.lazy_static] version = "1.0"@@ -26,12 +30,31 @@ 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 = []+# Build with `--features page-fault` to deliberately write to an unmapped+# address after boot and watch the page-fault handler report it.+page-fault = [] -# No unwinding in a kernel: just abort on panic.-[profile.dev]-panic = "abort"-[profile.release]-panic = "abort"+# The library (src/lib.rs) holds all kernel code so integration tests in+# tests/ can use it; src/main.rs is the small binary that boots it.+[lib]+doctest = false # doc tests would need std; our tests run in QEMU instead++[[bin]]+name = "rust_kernel"+test = false # all tests live in the library and tests/+bench = false++# These two tests replace the test framework with their own `_start`.+[[test]]+name = "should_panic"+harness = false+[[test]]+name = "stack_overflow"+harness = false++# Note: no `panic = "abort"` profile settings: the target JSON already sets+# "panic-strategy": "abort", and the profile setting breaks `cargo test` with+# build-std ("duplicate lang item in crate core"). # Extra QEMU arguments used by `cargo run` (via `bootimage runner`). [package.metadata.bootimage]@@ -39,3 +62,11 @@ run-args = [ "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", # lets the kernel exit QEMU "-serial", "stdio", # serial port -> your terminal ]+# Used by `cargo test`: headless, results on serial, exit code 33 = pass.+test-args = [+ "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",+ "-serial", "stdio",+ "-display", "none",+]+test-success-exit-code = 33 # (0x10 << 1) | 1+test-timeout = 120 # seconds
@@ -4,8 +4,14 @@ 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.+stack), runs the timer interrupt (uptime clock top-right), echoes what you+type on the PS/2 keyboard, walks and edits its own page tables, and has a+kernel heap (`Box`, `Vec`, `Rc` work) backed by a fixed-size-block allocator.+It has a `cargo test` suite that runs inside QEMU.++Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra+comments; small additions are the serial mirroring, uptime clock, demo+features and scripts.  @@ -25,7 +31,8 @@ type on the PS/2 keyboard. 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).+`pic8259` 0.11 (interrupt controller), `pc-keyboard` 0.7 (scancode decoding),+`linked_list_allocator` 0.9 (fallback heap allocator). Fresh setup on another machine: @@ -57,6 +64,7 @@ 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+./scripts/run-headless.sh page-fault # write to unmapped 0xdeadbeaf; passes if the page-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)@@ -71,14 +79,29 @@ Cargo features (in `Cargo.toml`): |---|---| | `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) |+| `page-fault` | after reading CR3, write to unmapped address `0xdeadbeaf` (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)+Hello from Tyler's Rust kernel! EXCEPTION: BREAKPOINT InterruptStackFrame { instruction_pointer: ..., ... } Survived the breakpoint, still running.+[paging] CR3 (level 4 table) at phys 0x1000+[paging] physical memory mapped at virt 0x18000000000+[memory] 16 memory map regions, 123732 KiB usable RAM+[paging] translate VirtAddr(0xb8000) -> Some(PhysAddr(0xb8000))+[paging] translate VirtAddr(0x201008) -> Some(PhysAddr(0x401008))+[paging] translate VirtAddr(0x10000201a10) -> Some(PhysAddr(0x27ea10))+[paging] translate VirtAddr(0x18000000000) -> Some(PhysAddr(0x0))+[paging] mapped page 0xdeadbeaf000 -> VGA frame, wrote "New!" through it+[heap] 100 KiB heap at 0x444444440000 (fixed-size-block allocator)+[heap] Box::new(41) = 41 at 0x444444440000+[heap] Vec of 500 numbers at 0x444444441000, sum = 124750+[heap] Rc count after clone = 2+[heap] Rc count after drop = 1+Type something! (keyboard echo) Timer + keyboard interrupts enabled. .................. 18 ticks .................. 36 ticks@@ -86,6 +109,40 @@ Timer + keyboard interrupts enabled. [ok] timer works, exiting ``` +## Tests++```sh+cargo test # everything (lib unit tests + tests/*.rs), each booted in QEMU headless+cargo test --lib # just the unit tests inside src/+cargo test --test heap_allocation # one integration test+```++Each test binary is its own little kernel: `bootimage runner` boots it with the+`test-args` from `Cargo.toml` (no display, serial to stdout, isa-debug-exit) and+treats QEMU exit code 33 as a pass. Output looks like:++```+Running 5 tests+heap_allocation::large_allocation_uses_fallback... [ok]+heap_allocation::large_vec... [ok]+heap_allocation::many_boxes... [ok]+heap_allocation::many_boxes_long_lived... [ok]+heap_allocation::simple_allocation... [ok]+```++| Test | What it checks |+|---|---|+| lib (`src/*.rs` `#[test_case]`s) | trivial assertion, breakpoint handler returns, VGA `println!` writes the right cells |+| `tests/basic_boot.rs` | printing works straight after boot, before any `init()` |+| `tests/heap_allocation.rs` | Box, big Vec, 100 K short-lived boxes (memory reuse), same with one long-lived box, >2 KiB fallback allocation |+| `tests/should_panic.rs` | a failing `assert_eq!` reaches the panic handler (`harness = false`) |+| `tests/stack_overflow.rs` | stack overflow lands in the double-fault handler on the IST stack (`harness = false`) |++How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]`+function and passes them to `rust_kernel::test_runner` (in `src/lib.rs`), which+prints each name to serial, runs it, prints `[ok]`, then exits QEMU with success.+A panic goes to `test_panic_handler`, which prints `[failed]` and exits with failure.+ 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.@@ -93,20 +150,26 @@ 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+.cargo/config.toml build-std (core, alloc) + 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, features, test settings, QEMU run/test args+src/lib.rs the kernel library: module list, init(), kprintln!, test framework, exit_qemu+src/main.rs the bootable binary: entry_point!(kernel_main), paging + heap demos, panic handler+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/memory.rs OffsetPageTable setup from CR3, example mapping, BootInfoFrameAllocator+src/allocator.rs heap location/size, init_heap (maps heap pages), #[global_allocator], Locked<A>+src/allocator/bump.rs bump allocator (not active, for learning)+src/allocator/fixed_size_block.rs fixed-size-block allocator (ACTIVE), falls back to linked_list_allocator+src/vga_buffer.rs VGA text-mode writer, print!/println!, uptime display+src/serial.rs COM1 UART writer + serial_print!/serial_println!+tests/ integration tests, one QEMU-booted kernel each+scripts/runner.sh `cargo run`/`cargo test` 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)@@ -131,9 +194,57 @@ boot.png screenshot of the VGA screen `_start` holds the lock would deadlock. The idle loop uses `hlt`, so QEMU doesn't burn a host CPU core. +## Memory (chapters 8-11)++**Paging.** Every address the kernel uses is *virtual*. The CPU translates it with+a 4-level page table tree (level 4 -> 3 -> 2 -> 1 -> 4 KiB page); 9 bits of the+address index each level, the low 12 bits are the offset in the page. CR3 holds the+*physical* address of the level-4 table (here `0x1000`). The `page-fault` feature+writes to `0xdeadbeaf`, which no table maps, and the page-fault handler reports+the address (from CR2) and the reason (`CAUSED_BY_WRITE`, not `PROTECTION_VIOLATION`+-> the page simply isn't present).++**Reaching page tables.** Page tables contain physical addresses, but we can only+dereference virtual ones. With the bootloader's `map_physical_memory` feature, all of+physical RAM is also mapped at `physical_memory_offset` (`0x18000000000` here), so+`virt = offset + phys`. `memory::init` wraps that in the x86_64 crate's+`OffsetPageTable`, which can `translate_addr` (walk the tables) and `map_to` (add entries).++**Virtual memory layout** (as set up by bootloader 0.9 in this build):++| Virtual address | What |+|---|---|+| `0xb8000` | VGA text buffer (identity-mapped: virt = phys) |+| `0x200000`... | kernel code + data (the ELF; maps to phys around `0x400000`) |+| `0x0100_0000_0000`... | kernel stack (80 pages, guard page below it) |+| `0x0180_0000_0000`... | all physical memory (`physical_memory_offset`) |+| `0x4444_4444_0000` | kernel heap, 100 KiB (we map it in `allocator::init_heap`) |+| `0xdead_beaf_000` | demo page we map to the VGA frame (writes "New!" on screen) |++The "New!" demo writes to screen row 20 through the new mapping; by the time you look,+later lines have scrolled it up, so in `boot.png` it overwrote the start of the first line.++**Physical frames.** `BootInfoFrameAllocator` hands out 4 KiB frames from the regions+the BIOS memory map marks `Usable` (~121 MiB of QEMU's default 128 MiB). It never frees;+fine for now.++**Heap.** `allocator::init_heap` maps the 25 pages of `HEAP_START..+HEAP_SIZE` to fresh+frames, then gives the range to the `#[global_allocator]`. From then on the `alloc`+crate (`Box`, `Vec`, `Rc`, `String`, `BTreeMap`...) works. `alloc` is compiled for our+target by `build-std` in `.cargo/config.toml`.++**Allocator designs** (`src/allocator/`): the *bump* allocator just moves a pointer+forward and can only reclaim memory when every allocation is freed (it fails the+`many_boxes_long_lived` test). The `linked_list_allocator` crate keeps a list of free+regions (first fit, slower, but reuses any freed memory). The active+*fixed-size-block* allocator rounds each request up to 8/16/.../2048 bytes and keeps+one free list per size, so alloc/free are O(1) pops/pushes; bigger requests and empty+lists fall back to the linked-list allocator. Switch allocators by editing one line in+`src/allocator.rs`.+ ## Roadmap -Already done (chapters 1-3 and 5-7, plus the serial/QEMU-exit bits of chapter 4):+Done: chapters 1-11 of the 2nd edition (everything up to Async/Await). - [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)@@ -141,17 +252,40 @@ Already done (chapters 1-3 and 5-7, plus the serial/QEMU-exit bits of chapter 4) - [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] Testing — `cargo test` in QEMU, custom test framework, integration tests. [phil-opp: Testing](https://os.phil-opp.com/testing/) - [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)) +- [x] Introduction to paging — CR3, page fault demo. [phil-opp](https://os.phil-opp.com/paging-introduction/), [OSDev: Paging](https://wiki.osdev.org/Paging)+- [x] Paging implementation — OffsetPageTable, translation, frame allocator, example mapping. [phil-opp](https://os.phil-opp.com/paging-implementation/), [OSDev: Page Frame Allocation](https://wiki.osdev.org/Page_Frame_Allocation)+- [x] Heap allocation — `alloc`, heap mapping, global allocator. [phil-opp](https://os.phil-opp.com/heap-allocation/), [OSDev: Memory Allocation](https://wiki.osdev.org/Memory_Allocation)+- [x] Allocator designs — bump, linked list (crate), fixed-size block (active). [phil-opp](https://os.phil-opp.com/allocator-designs/)+ 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)+1. **Async/await multitasking** — a cooperative executor; turn the keyboard handler+ into a task that reads scancodes from a lock-free queue (also gives you a place to+ handle Backspace properly). [phil-opp: Async/Await](https://os.phil-opp.com/async-await/),+ [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems)+2. **Preemptive threads** — timer-driven context switches between kernel threads with+ their own stacks. [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial),+ [OSDev: Context Switching](https://wiki.osdev.org/Context_Switching)+3. **User mode & syscalls** — per-process page tables, ring 3 code/data segments in+ the GDT, `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)+4. **Filesystem** — a VFS layer, a RAM disk / initrd first, then FAT on an ATA/virtio disk.+ [OSDev: File Systems](https://wiki.osdev.org/File_Systems), [OSDev: VFS](https://wiki.osdev.org/VFS),+ [OSDev: Initrd](https://wiki.osdev.org/Initrd), [OSDev: FAT](https://wiki.osdev.org/FAT),+ [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode)+5. **Shell** — line editing on top of the keyboard task, a few built-in commands+ (`help`, `mem`, `uptime`, `ls`, `cat`), later launching user programs.+ [OSDev: Shell](https://wiki.osdev.org/Shell)++Small exercises along the way: handle Backspace, speed up the PIT+([OSDev: PIT](https://wiki.osdev.org/Programmable_Interval_Timer)), add handlers for+divide error / general protection fault, make `BootInfoFrameAllocator` O(1), add a+`mem` stat (bytes allocated) to the allocator. 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),@@ -168,6 +302,9 @@ 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.+- `cargo test` with build-std breaks ("duplicate lang item in crate `core`: `sized`")+ if `Cargo.toml` sets `panic = "abort"` in `[profile.*]`. The target JSON already+ sets `"panic-strategy": "abort"`, so the profile settings were removed. - 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
Binary file changed.
@@ -0,0 +1,102 @@+//! The kernel heap.+//!+//! 1. Pick a virtual address range for the heap (HEAP_START..+HEAP_SIZE).+//! 2. Map every page in it to a fresh physical frame (`init_heap`).+//! 3. Hand the range to an allocator and register it with+//! `#[global_allocator]`, so `Box`, `Vec`, `Rc`, `String`... just work.+//!+//! Three allocator designs, from the "Allocator Designs" chapter:+//! * `bump::BumpAllocator` -- simplest, can only free everything at once+//! * `linked_list_allocator` -- crate: free-list, first fit, reuses memory+//! * `fixed_size_block::FixedSizeBlockAllocator` (ACTIVE) -- fast lists of+//! fixed-size blocks, falls back to the linked-list allocator for big ones+//!+//! To try another one, swap the `ALLOCATOR` line below (examples in comments).++use alloc::alloc::{GlobalAlloc, Layout};+use core::ptr::null_mut;+use x86_64::{+ VirtAddr,+ structures::paging::{+ FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB, mapper::MapToError,+ },+};++pub mod bump;+pub mod fixed_size_block;++/// Where the heap lives in *virtual* memory. Any unused, easy-to-recognise+/// address works; this one stands out in a debugger.+pub const HEAP_START: usize = 0x_4444_4444_0000;+pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB++#[global_allocator]+static ALLOCATOR: Locked<fixed_size_block::FixedSizeBlockAllocator> =+ Locked::new(fixed_size_block::FixedSizeBlockAllocator::new());+// Alternatives:+// static ALLOCATOR: Locked<bump::BumpAllocator> = Locked::new(bump::BumpAllocator::new());+// static ALLOCATOR: linked_list_allocator::LockedHeap = linked_list_allocator::LockedHeap::empty();+// (then in init_heap: `ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE)` works for all three)++/// Map the heap's pages to physical frames, then initialise the allocator.+pub fn init_heap(+ mapper: &mut impl Mapper<Size4KiB>,+ frame_allocator: &mut impl FrameAllocator<Size4KiB>,+) -> Result<(), MapToError<Size4KiB>> {+ let page_range = {+ let heap_start = VirtAddr::new(HEAP_START as u64);+ let heap_end = heap_start + HEAP_SIZE as u64 - 1u64;+ Page::range_inclusive(+ Page::containing_address(heap_start),+ Page::containing_address(heap_end),+ )+ };++ for page in page_range {+ let frame = frame_allocator+ .allocate_frame()+ .ok_or(MapToError::FrameAllocationFailed)?;+ let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;+ unsafe { mapper.map_to(page, frame, flags, frame_allocator)?.flush() };+ }++ unsafe {+ ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE);+ }+ Ok(())+}++/// A wrapper around `spin::Mutex`. `GlobalAlloc` methods take `&self`, so the+/// allocator needs interior mutability; and Rust only lets us implement a+/// foreign trait (`GlobalAlloc`) for a type defined in our own crate.+pub struct Locked<A> {+ inner: spin::Mutex<A>,+}++impl<A> Locked<A> {+ pub const fn new(inner: A) -> Self {+ Locked { inner: spin::Mutex::new(inner) }+ }+ pub fn lock(&self) -> spin::MutexGuard<'_, A> {+ self.inner.lock()+ }+}++/// Round `addr` up to a multiple of `align` (which must be a power of two).+/// Example: align_up(13, 8) == 16.+fn align_up(addr: usize, align: usize) -> usize {+ (addr + align - 1) & !(align - 1)+}++/// The "allocator" from the start of the Heap Allocation chapter: always+/// fails. Kept to show the smallest possible `GlobalAlloc`.+pub struct Dummy;++unsafe impl GlobalAlloc for Dummy {+ unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {+ null_mut() // null = "out of memory"+ }+ unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {+ panic!("dealloc should never be called")+ }+}
@@ -0,0 +1,53 @@+//! Bump allocator: hand out memory by moving a pointer forward ("bumping").+//! Super fast and tiny, but memory is only reclaimed when *every*+//! allocation has been freed. Not active; kept for learning.++use super::{Locked, align_up};+use alloc::alloc::{GlobalAlloc, Layout};+use core::ptr;++pub struct BumpAllocator {+ heap_start: usize,+ heap_end: usize,+ next: usize, // start of the unused part of the heap+ allocations: usize, // number of live allocations+}++impl BumpAllocator {+ pub const fn new() -> Self {+ BumpAllocator { heap_start: 0, heap_end: 0, next: 0, allocations: 0 }+ }++ /// Unsafe: the range must be mapped and unused; call only once.+ pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) {+ self.heap_start = heap_start;+ self.heap_end = heap_start.saturating_add(heap_size);+ self.next = heap_start;+ }+}++unsafe impl GlobalAlloc for Locked<BumpAllocator> {+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+ let mut bump = self.lock();+ let alloc_start = align_up(bump.next, layout.align());+ let alloc_end = match alloc_start.checked_add(layout.size()) {+ Some(end) => end,+ None => return ptr::null_mut(),+ };+ if alloc_end > bump.heap_end {+ ptr::null_mut() // out of memory+ } else {+ bump.next = alloc_end;+ bump.allocations += 1;+ alloc_start as *mut u8+ }+ }++ unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {+ let mut bump = self.lock();+ bump.allocations -= 1;+ if bump.allocations == 0 {+ bump.next = bump.heap_start; // everything freed: start over+ }+ }+}
@@ -0,0 +1,100 @@+//! Fixed-size block allocator (the ACTIVE allocator).+//!+//! Every request is rounded up to one of a few block sizes (8, 16, ... 2048+//! bytes). For each size we keep a linked list of free blocks; the "next"+//! pointer is stored *inside* the free block itself, so the lists need no+//! extra memory. alloc = pop from the list, dealloc = push onto it: O(1).+//! When a list is empty, or a request is bigger than 2048 bytes, we ask the+//! fallback `linked_list_allocator::Heap`.+//! Trade-off: up to ~50% wasted space per block (e.g. 33 bytes -> 64).++use super::Locked;+use alloc::alloc::{GlobalAlloc, Layout};+use core::{+ mem,+ ptr::{self, NonNull},+};++/// Block sizes. Each is a power of two, so it can double as the alignment.+const BLOCK_SIZES: &[usize] = &[8, 16, 32, 64, 128, 256, 512, 1024, 2048];++/// Index into BLOCK_SIZES of the smallest block that fits `layout`.+fn list_index(layout: &Layout) -> Option<usize> {+ let required_block_size = layout.size().max(layout.align());+ BLOCK_SIZES.iter().position(|&s| s >= required_block_size)+}++/// A free block. Lives inside the free memory it describes.+struct ListNode {+ next: Option<&'static mut ListNode>,+}++pub struct FixedSizeBlockAllocator {+ list_heads: [Option<&'static mut ListNode>; BLOCK_SIZES.len()],+ fallback_allocator: linked_list_allocator::Heap,+}++impl FixedSizeBlockAllocator {+ pub const fn new() -> Self {+ const EMPTY: Option<&'static mut ListNode> = None;+ FixedSizeBlockAllocator {+ list_heads: [EMPTY; BLOCK_SIZES.len()],+ fallback_allocator: linked_list_allocator::Heap::empty(),+ }+ }++ /// Unsafe: the range must be mapped and unused; call only once.+ pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) {+ unsafe { self.fallback_allocator.init(heap_start, heap_size) };+ }++ fn fallback_alloc(&mut self, layout: Layout) -> *mut u8 {+ match self.fallback_allocator.allocate_first_fit(layout) {+ Ok(ptr) => ptr.as_ptr(),+ Err(_) => ptr::null_mut(),+ }+ }+}++unsafe impl GlobalAlloc for Locked<FixedSizeBlockAllocator> {+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+ let mut allocator = self.lock();+ match list_index(&layout) {+ Some(index) => match allocator.list_heads[index].take() {+ // A free block of the right size exists: pop it off the list.+ Some(node) => {+ allocator.list_heads[index] = node.next.take();+ node as *mut ListNode as *mut u8+ }+ // List empty: carve a new block out of the fallback heap.+ None => {+ let block_size = BLOCK_SIZES[index];+ let layout = Layout::from_size_align(block_size, block_size).unwrap();+ allocator.fallback_alloc(layout)+ }+ },+ None => allocator.fallback_alloc(layout), // too big for any block+ }+ }++ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+ let mut allocator = self.lock();+ match list_index(&layout) {+ Some(index) => {+ // Push the freed block onto the front of its size's list.+ let new_node = ListNode { next: allocator.list_heads[index].take() };+ assert!(mem::size_of::<ListNode>() <= BLOCK_SIZES[index]);+ assert!(mem::align_of::<ListNode>() <= BLOCK_SIZES[index]);+ let new_node_ptr = ptr as *mut ListNode;+ unsafe {+ new_node_ptr.write(new_node);+ allocator.list_heads[index] = Some(&mut *new_node_ptr);+ }+ }+ None => {+ let ptr = NonNull::new(ptr).unwrap();+ unsafe { allocator.fallback_allocator.deallocate(ptr, layout) };+ }+ }+ }+}
@@ -78,8 +78,9 @@ extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { 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.+/// Page fault: accessing unmapped/protected memory. CR2 holds the bad address,+/// the error code says why (not present? write? user mode?). A real OS would+/// now load the page from disk or kill the process; we just report and stop. extern "x86-interrupt" fn page_fault_handler( stack_frame: InterruptStackFrame, error_code: PageFaultErrorCode,@@ -92,6 +93,14 @@ extern "x86-interrupt" fn page_fault_handler( error_code, stack_frame );++ // In the deliberate page-fault demo, reaching here means success.+ #[cfg(all(feature = "headless", feature = "page-fault"))]+ {+ serial_println!("[ok] page fault was caught");+ crate::exit_qemu(crate::QemuExitCode::Success);+ }+ crate::hlt_loop(); } @@ -138,7 +147,7 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFr if ticks % 18 == 0 { serial_println!(" {} ticks", ticks); }- #[cfg(not(feature = "stack-overflow"))]+ #[cfg(not(any(feature = "stack-overflow", feature = "page-fault")))] if ticks >= 54 { serial_println!("[ok] timer works, exiting"); crate::exit_qemu(crate::QemuExitCode::Success);@@ -200,3 +209,9 @@ extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStac PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); } }++// Runs under `cargo test`: if the handler were missing, this would fault.+#[test_case]+fn test_breakpoint_exception() {+ x86_64::instructions::interrupts::int3();+}
@@ -0,0 +1,131 @@+//! The kernel as a library.+//!+//! Everything lives here so that both the bootable binary (src/main.rs) and+//! the integration tests (tests/*.rs, each its own tiny kernel) can use it.+//!+//! Testing: Rust's normal test harness needs `std`, so we use the nightly+//! `custom_test_frameworks` feature. `cargo test` builds each test kernel,+//! boots it in QEMU (see `test-args` in Cargo.toml), results go to serial,+//! and the kernel exits QEMU through the `isa-debug-exit` device.++#![no_std]+#![cfg_attr(test, no_main)] // when testing the lib itself, it is a kernel too+#![feature(custom_test_frameworks)] // nightly: our own test runner+#![feature(abi_x86_interrupt)] // nightly: `extern "x86-interrupt" fn`+#![test_runner(crate::test_runner)]+#![reexport_test_harness_main = "test_main"] // name the generated test entry fn++// `alloc` = the heap-using half of the standard library (Box, Vec, Rc, ...).+// It works once we register a global allocator (see allocator.rs).+extern crate alloc;++use core::panic::PanicInfo;++pub mod allocator; // heap: maps heap pages + the #[global_allocator]+pub mod gdt; // GDT + TSS: gives the double-fault handler its own stack+pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard+pub mod memory; // paging: page tables, address translation, frame allocator+pub mod serial; // COM1 serial port+pub mod vga_buffer; // 80x25 VGA text mode screen++/// 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`).+pub fn init() {+ gdt::init();+ interrupts::init_idt();+ unsafe { interrupts::PICS.lock().initialize() };+ x86_64::instructions::interrupts::enable();+}++/// Print a line to *both* the VGA screen and the serial port.+#[macro_export]+macro_rules! kprintln {+ ($($arg:tt)*) => {{+ $crate::println!($($arg)*);+ $crate::serial_println!($($arg)*);+ }};+}++/// 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();+ }+}++/// Exit codes for QEMU's `isa-debug-exit` device. QEMU exits with+/// status `(value << 1) | 1`, so Success = 0x10 -> shell exit code 33.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+#[repr(u32)]+pub enum QemuExitCode {+ Success = 0x10,+ Failed = 0x11,+}++/// Write to I/O port 0xf4 (where we put `isa-debug-exit`) to quit QEMU.+pub fn exit_qemu(exit_code: QemuExitCode) {+ use x86_64::instructions::port::Port;+ unsafe {+ let mut port = Port::new(0xf4);+ port.write(exit_code as u32);+ }+}++// ---------------------------------------------------------------------------+// Test framework+// ---------------------------------------------------------------------------++/// Anything callable as a test: prints its name, runs it, prints [ok].+/// (If it panics, the panic handler prints [failed] instead.)+pub trait Testable {+ fn run(&self);+}++impl<T: Fn()> Testable for T {+ fn run(&self) {+ serial_print!("{}...\t", core::any::type_name::<T>());+ self();+ serial_println!("[ok]");+ }+}++/// Called with every `#[test_case]` function in the crate being tested.+pub fn test_runner(tests: &[&dyn Testable]) {+ serial_println!("Running {} tests", tests.len());+ for test in tests {+ test.run();+ }+ exit_qemu(QemuExitCode::Success);+}++/// Panic handler for test kernels: report the failure and exit QEMU.+pub fn test_panic_handler(info: &PanicInfo) -> ! {+ serial_println!("[failed]\n");+ serial_println!("Error: {}\n", info);+ exit_qemu(QemuExitCode::Failed);+ hlt_loop();+}++// When running `cargo test --lib`, the library itself is booted as a kernel:+#[cfg(test)]+bootloader::entry_point!(test_kernel_main);++#[cfg(test)]+fn test_kernel_main(_boot_info: &'static bootloader::BootInfo) -> ! {+ init();+ test_main();+ hlt_loop();+}++#[cfg(test)]+#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ test_panic_handler(info)+}++#[test_case]+fn trivial_assertion() {+ assert_eq!(1 + 1, 2);+}
@@ -1,38 +1,34 @@-//! A small freestanding x86_64 kernel.+//! The bootable kernel binary. All the real code is in the library+//! (src/lib.rs and friends); this file just runs the demos in order. //! //! 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-//! `main`, and no heap. Everything we print goes straight to hardware.+//! paging, maps all physical memory) -> `kernel_main` below, with a+//! `BootInfo` describing the memory map. #![no_std] // don't link the Rust standard library (it needs an OS)-#![no_main] // no normal `main`: the bootloader jumps to `_start`-#![feature(abi_x86_interrupt)] // nightly: lets us write `extern "x86-interrupt" fn`+#![no_main] // no normal `main`: the bootloader calls our entry point -// 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+extern crate alloc; // Box, Vec, Rc... once the heap is set up +use alloc::{boxed::Box, rc::Rc, vec, vec::Vec};+use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo;+use rust_kernel::{allocator, kprintln, memory, println, serial_println};+use x86_64::{VirtAddr, structures::paging::{Page, Translate}};++// `entry_point!` defines the real `_start` for us and checks at compile time+// that `kernel_main` has the right signature (takes `&'static BootInfo`).+entry_point!(kernel_main); -/// Kernel entry point. The bootloader calls this after setting up the CPU.-/// `no_mangle` keeps the symbol name `_start` so the linker can find it.-#[unsafe(no_mangle)]-pub extern "C" fn _start() -> ! {- println!("Hello from Tyler's Rust kernel!");- serial_println!("Hello from Tyler's Rust kernel! (via serial COM1)");+fn kernel_main(boot_info: &'static BootInfo) -> ! {+ kprintln!("Hello from Tyler's Rust kernel!"); - init();+ rust_kernel::init(); // GDT, IDT, PICs, enable interrupts // 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.");+ kprintln!("Survived the breakpoint, still running."); #[cfg(feature = "stack-overflow")] {@@ -40,22 +36,90 @@ pub extern "C" fn _start() -> ! { stack_overflow(); } - println!("Type something! (keyboard echo)");+ paging_demo(boot_info);++ kprintln!("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();+ rust_kernel::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();+/// Chapters "Introduction to Paging", "Paging Implementation", "Heap Allocation".+fn paging_demo(boot_info: &'static BootInfo) {+ use x86_64::registers::control::Cr3;++ // --- Introduction to Paging -------------------------------------------+ // CR3 = physical address of the level-4 page table currently in use.+ let (level_4_frame, _) = Cr3::read();+ kprintln!("[paging] CR3 (level 4 table) at phys {:#x}", level_4_frame.start_address().as_u64());++ #[cfg(feature = "page-fault")]+ {+ kprintln!("[paging] writing to unmapped address 0xdeadbeaf...");+ unsafe { *(0xdeadbeaf as *mut u8) = 42 }; // -> page fault handler+ }++ // --- Paging Implementation --------------------------------------------+ let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);+ serial_println!("[paging] physical memory mapped at virt {:#x}", phys_mem_offset.as_u64());+ serial_println!(+ "[memory] {} memory map regions, {} KiB usable RAM",+ boot_info.memory_map.iter().count(),+ memory::usable_memory_bytes(&boot_info.memory_map) / 1024+ );++ // Safety: the bootloader mapped all physical memory at that offset, and+ // this is the only place we call `init`.+ let mut mapper = unsafe { memory::init(phys_mem_offset) };+ let mut frame_allocator = unsafe { memory::BootInfoFrameAllocator::init(&boot_info.memory_map) };++ // Translate some virtual addresses to physical ones by walking the tables.+ let addresses = [+ 0xb8000, // VGA buffer: identity-mapped+ 0x201008, // somewhere in our kernel's code+ 0x0100_0020_1a10, // somewhere in our stack+ boot_info.physical_memory_offset, // -> physical address 0+ ];+ for &address in &addresses {+ let virt = VirtAddr::new(address);+ let phys = mapper.translate_addr(virt);+ serial_println!("[paging] translate {:?} -> {:?}", virt, phys);+ }++ // Map a brand-new page (0xdeadbeaf000, previously unmapped) to the VGA+ // buffer frame, then write "New!" (white on red) through it.+ let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000));+ memory::create_example_mapping(page, &mut mapper, &mut frame_allocator);+ let page_ptr: *mut u64 = page.start_address().as_mut_ptr();+ // Offset 400 u64s = 3200 bytes = row 20 of the screen (160 bytes/row).+ unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e) };+ kprintln!("[paging] mapped page 0xdeadbeaf000 -> VGA frame, wrote \"New!\" through it");++ // --- Heap Allocation --------------------------------------------------+ allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+ kprintln!(+ "[heap] {} KiB heap at {:#x} (fixed-size-block allocator)",+ allocator::HEAP_SIZE / 1024,+ allocator::HEAP_START+ );++ let heap_value = Box::new(41);+ kprintln!("[heap] Box::new(41) = {} at {:p}", heap_value, heap_value);++ let mut vec = Vec::new();+ for i in 0..500 {+ vec.push(i);+ }+ let sum: u64 = vec.iter().sum();+ kprintln!("[heap] Vec of 500 numbers at {:p}, sum = {}", vec.as_ptr(), sum);++ let reference_counted = Rc::new(vec![1, 2, 3]);+ let cloned_reference = reference_counted.clone();+ kprintln!("[heap] Rc count after clone = {}", Rc::strong_count(&cloned_reference));+ core::mem::drop(reference_counted);+ kprintln!("[heap] Rc count after drop = {}", Rc::strong_count(&cloned_reference)); } /// Recurse forever until we run off the end of the stack into the guard page.@@ -73,34 +137,6 @@ fn panic(info: &PanicInfo) -> ! { println!("{}", info); serial_println!("KERNEL PANIC: {}", info); #[cfg(feature = "headless")]- exit_qemu(QemuExitCode::Failed);- hlt_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();- }-}--/// Exit codes for QEMU's `isa-debug-exit` device. QEMU exits with-/// status `(value << 1) | 1`, so Success = 0x10 -> shell exit code 33.-#[allow(dead_code)]-#[derive(Debug, Clone, Copy, PartialEq, Eq)]-#[repr(u32)]-pub enum QemuExitCode {- Success = 0x10,- Failed = 0x11,-}--/// Write to I/O port 0xf4 (where we put `isa-debug-exit`) to quit QEMU.-#[allow(dead_code)]-pub fn exit_qemu(exit_code: QemuExitCode) {- use x86_64::instructions::port::Port;- unsafe {- let mut port = Port::new(0xf4);- port.write(exit_code as u32);- }+ rust_kernel::exit_qemu(rust_kernel::QemuExitCode::Failed);+ rust_kernel::hlt_loop(); }
@@ -0,0 +1,102 @@+//! Paging: page tables, address translation and a physical frame allocator.+//!+//! x86_64 uses 4-level page tables (L4 -> L3 -> L2 -> L1 -> 4 KiB page).+//! CR3 holds the *physical* address of the active level-4 table. But the CPU+//! (and our code) only uses *virtual* addresses, so to read or edit a page+//! table we need it mapped somewhere. The bootloader's `map_physical_memory`+//! feature maps all of physical RAM at `physical_memory_offset`, so+//! virtual address = physical_memory_offset + physical address.+//! `OffsetPageTable` from the x86_64 crate implements exactly that.++use bootloader::bootinfo::{MemoryMap, MemoryRegionType};+use x86_64::{+ PhysAddr, VirtAddr,+ structures::paging::{+ FrameAllocator, Mapper, OffsetPageTable, Page, PageTable, PageTableFlags, PhysFrame,+ Size4KiB,+ },+};++/// Create an `OffsetPageTable` for the currently active page tables.+///+/// Unsafe: the caller must guarantee that all physical memory really is+/// mapped at `physical_memory_offset`, and must call this only once (two+/// `&mut` to the same page table would be undefined behaviour).+pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {+ unsafe {+ let level_4_table = active_level_4_table(physical_memory_offset);+ OffsetPageTable::new(level_4_table, physical_memory_offset)+ }+}++/// Find the active level-4 table via CR3 and return a reference to it.+unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {+ use x86_64::registers::control::Cr3;++ let (level_4_table_frame, _flags) = Cr3::read();+ let phys = level_4_table_frame.start_address();+ let virt = physical_memory_offset + phys.as_u64();+ let page_table_ptr: *mut PageTable = virt.as_mut_ptr();+ unsafe { &mut *page_table_ptr }+}++/// Demo: map the virtual page containing `page` to the VGA text buffer's+/// physical frame (0xb8000). Writing through that page then shows on screen.+pub fn create_example_mapping(+ page: Page,+ mapper: &mut OffsetPageTable,+ frame_allocator: &mut impl FrameAllocator<Size4KiB>,+) {+ let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000));+ let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;++ // Unsafe because mapping the same frame twice could create aliasing+ // `&mut`s. Fine here: it's only the VGA buffer, for a demo.+ // `map_to` may need new page tables (L3/L2/L1) -> takes a frame allocator.+ let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) };+ // `flush` removes the stale entry from the TLB (the CPU's mapping cache).+ map_to_result.expect("map_to failed").flush();+}++/// Hands out unused physical 4 KiB frames, taken from the memory map the+/// bootloader got from the BIOS (E820). Very simple: it never frees, and+/// walks the map from the start on every call (fine for now).+pub struct BootInfoFrameAllocator {+ memory_map: &'static MemoryMap,+ next: usize, // how many frames we've handed out so far+}++impl BootInfoFrameAllocator {+ /// Unsafe: the caller must guarantee that every frame marked `Usable` in+ /// the memory map really is unused.+ pub unsafe fn init(memory_map: &'static MemoryMap) -> Self {+ BootInfoFrameAllocator { memory_map, next: 0 }+ }++ /// All usable frames, in order.+ fn usable_frames(&self) -> impl Iterator<Item = PhysFrame> {+ self.memory_map+ .iter()+ .filter(|r| r.region_type == MemoryRegionType::Usable) // only free RAM+ .map(|r| r.range.start_addr()..r.range.end_addr()) // -> address ranges+ .flat_map(|r| r.step_by(4096)) // -> each frame's start address+ .map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))+ }+}++unsafe impl FrameAllocator<Size4KiB> for BootInfoFrameAllocator {+ fn allocate_frame(&mut self) -> Option<PhysFrame> {+ let frame = self.usable_frames().nth(self.next);+ self.next += 1;+ frame+ }+}++/// Total usable RAM in bytes, according to the memory map (for printing).+pub fn usable_memory_bytes(memory_map: &MemoryMap) -> u64 {+ memory_map+ .iter()+ .filter(|r| r.region_type == MemoryRegionType::Usable)+ .map(|r| r.range.end_addr() - r.range.start_addr())+ .sum()+}
@@ -177,3 +177,24 @@ macro_rules! println { () => ($crate::print!("\n")); ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*))); }++#[test_case]+fn test_println_simple() {+ println!("test_println_simple output");+}++#[test_case]+fn test_println_output() {+ // The printed line ends up on the second-to-last row (println adds "\n").+ use core::fmt::Write;+ let s = "Some test string that fits on a single line";+ // Hold the lock with interrupts off, so the timer can't redraw meanwhile.+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut writer = WRITER.lock();+ writeln!(writer, "\n{}", s).expect("writeln failed");+ for (i, c) in s.chars().enumerate() {+ let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read();+ assert_eq!(char::from(screen_char.ascii_character), c);+ }+ });+}
@@ -0,0 +1,25 @@+//! Boots with nothing initialised and checks printing works.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++use core::panic::PanicInfo;+use rust_kernel::println;++#[unsafe(no_mangle)] // a plain `_start`: we don't need BootInfo here+pub extern "C" fn _start() -> ! {+ test_main();+ rust_kernel::hlt_loop();+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}++#[test_case]+fn test_println() {+ println!("test_println output");+}
@@ -0,0 +1,80 @@+//! Sets up paging + the heap, then stresses the allocator.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{boxed::Box, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rust_kernel::allocator::{self, HEAP_SIZE};+use rust_kernel::memory::{self, BootInfoFrameAllocator};+use x86_64::VirtAddr;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rust_kernel::init();+ let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);+ let mut mapper = unsafe { memory::init(phys_mem_offset) };+ let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };+ allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");++ test_main();+ rust_kernel::hlt_loop();+}++#[test_case]+fn simple_allocation() {+ let heap_value_1 = Box::new(41);+ let heap_value_2 = Box::new(13);+ assert_eq!(*heap_value_1, 41);+ assert_eq!(*heap_value_2, 13);+}++#[test_case]+fn large_vec() {+ let n = 1000;+ let mut vec = Vec::new();+ for i in 0..n {+ vec.push(i);+ }+ assert_eq!(vec.iter().sum::<u64>(), (n - 1) * n / 2);+}++/// Allocating far more than HEAP_SIZE in total only works if freed memory+/// is reused.+#[test_case]+fn many_boxes() {+ for i in 0..HEAP_SIZE {+ let x = Box::new(i);+ assert_eq!(*x, i);+ }+}++/// Same, while one allocation stays alive the whole time. (This is the one+/// the bump allocator fails: it can't reuse memory while anything is live.)+#[test_case]+fn many_boxes_long_lived() {+ let long_lived = Box::new(1);+ for i in 0..HEAP_SIZE {+ let x = Box::new(i);+ assert_eq!(*x, i);+ }+ assert_eq!(*long_lived, 1);+}++/// Bigger than the largest fixed block (2048) -> goes to the fallback.+#[test_case]+fn large_allocation_uses_fallback() {+ let big = alloc::vec![7u8; 10_000];+ assert_eq!(big.iter().map(|&b| b as usize).sum::<usize>(), 70_000);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,27 @@+//! Passes only if the test panics. No test harness (`harness = false` in+//! Cargo.toml): a single test function, called directly from `_start`.+#![no_std]+#![no_main]++use core::panic::PanicInfo;+use rust_kernel::{QemuExitCode, exit_qemu, serial_print, serial_println};++#[unsafe(no_mangle)]+pub extern "C" fn _start() -> ! {+ should_fail();+ serial_println!("[test did not panic]");+ exit_qemu(QemuExitCode::Failed);+ rust_kernel::hlt_loop();+}++fn should_fail() {+ serial_print!("should_panic::should_fail...\t");+ assert_eq!(0, 1);+}++#[panic_handler]+fn panic(_info: &PanicInfo) -> ! {+ serial_println!("[ok]");+ exit_qemu(QemuExitCode::Success);+ rust_kernel::hlt_loop();+}
@@ -0,0 +1,51 @@+//! Overflows the stack and passes if the double-fault handler (running on+//! its own IST stack) gets control instead of the CPU triple-faulting.+#![no_std]+#![no_main]+#![feature(abi_x86_interrupt)]++use core::panic::PanicInfo;+use lazy_static::lazy_static;+use rust_kernel::{QemuExitCode, exit_qemu, serial_print, serial_println};+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};++#[unsafe(no_mangle)]+pub extern "C" fn _start() -> ! {+ serial_print!("stack_overflow::stack_overflow...\t");+ rust_kernel::gdt::init();+ TEST_IDT.load(); // our own IDT whose double-fault handler reports success+ stack_overflow();+ panic!("Execution continued after stack overflow");+}++#[allow(unconditional_recursion)]+fn stack_overflow() {+ stack_overflow(); // each call pushes a return address+ unsafe { core::ptr::read_volatile(&0u8) }; // prevent tail-call optimisation+}++lazy_static! {+ static ref TEST_IDT: InterruptDescriptorTable = {+ let mut idt = InterruptDescriptorTable::new();+ unsafe {+ idt.double_fault+ .set_handler_fn(test_double_fault_handler)+ .set_stack_index(rust_kernel::gdt::DOUBLE_FAULT_IST_INDEX);+ }+ idt+ };+}++extern "x86-interrupt" fn test_double_fault_handler(+ _stack_frame: InterruptStackFrame,+ _error_code: u64,+) -> ! {+ serial_println!("[ok]");+ exit_qemu(QemuExitCode::Success);+ rust_kernel::hlt_loop();+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}