summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--CONTEXT.md100
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml10
-rw-r--r--Makefile124
-rw-r--r--build.rs70
-rw-r--r--docs/adr/0001-static-buffer-for-memory-map.md54
-rw-r--r--rust-toolchain.toml3
-rw-r--r--src/font.rs5
-rw-r--r--src/gop.rs232
-rw-r--r--src/main.rs181
-rw-r--r--src/memory.rs196
-rw-r--r--src/ps2.rs329
-rw-r--r--src/serial.rs118
-rw-r--r--src/shell.rs274
-rw-r--r--src/uefi.rs289
-rwxr-xr-xtools/boot-image.sh62
-rwxr-xr-xtools/debug-kernel.py64
-rwxr-xr-xtools/qemu.sh96
19 files changed, 2218 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2353fdc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
1/target
2/esp/
3*.img
4NvVars
diff --git a/CONTEXT.md b/CONTEXT.md
new file mode 100644
index 0000000..283467a
--- /dev/null
+++ b/CONTEXT.md
@@ -0,0 +1,100 @@
1# CONTEXT.md — raisernics Kernel Glossary
2
3## raisernics
4
5Custom x86_64 kernel for the RNG ecosystem. Toy project — self-contained,
6no strict userspace separation, TempleOS-inspired design philosophy.
7Built in Rust, compiled for the `x86_64-unknown-uefi` target (nightly).
8
9## Boot interface
10
11The kernel is a **PE32+ EFI application** loaded via UEFI `LoadImage`/`StartImage`.
12The existing bootloader (rsbl) loads us from `\bzImage` and passes the UEFI
13system table. No bootloader changes needed.
14
15## Output strategy
16
17Two-phase console model:
18
19- **Phase 1 (boot services alive):** UEFI `con_out->OutputString()` — works
20 with whatever UEFI firmware routes (serial + framebuffer).
21- **Phase 2 (native):** Raw serial UART 16550 driver on COM1 (I/O port
22 `0x3F8`). Activated after `ExitBootServices` when we own the hardware.
23
24## Dependency philosophy
25
26Aligns with RNG philosophy: minimal dependencies. Freestanding Rust,
27no libc, no external crates at the kernel/core level. UEFI protocol
28structs are hand-defined (not from `uefi-rs` crate) to stay lightweight.
29
30## Paging strategy
31
32Long mode mandates paging (`CR0.PG = 1`), so we accept that tax but keep it
33simple. Retain UEFI's identity-mapped 4-level page tables. No higher-half,
34no demand paging, no guard pages. When the kernel needs more physical pages,
35entries are added 1:1 to the existing page tables.
36
37## Physical memory
38
39Before `ExitBootServices`, the kernel reads the UEFI memory map to know
40which physical regions are free. Post-`ExitBootServices`, a simple physical
41page allocator (bitmap or free-stack) manages free pages. A bump allocator
42serves as the initial kernel heap.
43
44## Input strategy
45
46**PS/2 keyboard polling.** Port I/O to `0x60`/`0x64`. Scancode set 1
47(US QWERTY layout in the initial build). No interrupts — polling loop
48inside the shell.
49
50## Display strategy
51
52Two-phase model matching output:
53
54- **Phase 1:** UEFI `con_out->OutputString()` (framebuffer + serial handled
55 by firmware).
56- **Phase 2:** GOP (Graphics Output Protocol) framebuffer obtained before
57 `ExitBootServices`, driven by a built-in 8×16 bitmap font. Text rendered
58 by drawing glyphs directly into the framebuffer.
59
60The framebuffer address stays valid after `ExitBootServices` — the GPU's
61linear frame buffer is system memory that we own once boot services are gone.
62
63## Keyboard layout
64
65Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table.
66Other layouts can be added later by swapping the translation table.
67
68## Development loop
69
70Standalone project — no integration with the RNG initramfs build system.
71Testing via QEMU with OVMF. The kernel PE binary is placed on a FAT
72image as `\bzImage` (matching rsbl's expected path).
73
74## Project layout
75
76Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown:
77
78- `main.rs` — entry point, panic handler, top-level init
79- `uefi.rs` — UEFI protocol type definitions
80- `serial.rs` — UART 16550 driver
81- `paging.rs` — identity page table helpers
82- `memory.rs` — physical page allocator + heap
83- `gop.rs` — framebuffer access and text rendering
84- `font.rs` — 8×16 bitmap font data
85- `ps2.rs` — PS/2 keyboard driver (polling)
86- `shell.rs` — command loop and built-in commands
87
88## Milestone status (v0.1)
89
90✅ **Booting** — kernel loads via rsbl, prints banner via UEFI console
91✅ **ExitBootServices** — memory map captured via static `.bss` buffer
92 (see `docs/adr/0001-static-buffer-for-memory-map.md`)
93✅ **Serial** — UART 16550 at COM1, 115200 8N1
94✅ **Framebuffer** — GOP located, 8×16 bitmap font renders text
95✅ **Keyboard** — PS/2 polling, scancode set 1, US QWERTY
96✅ **Shell** — commands: `help`, `echo`, `clear`, `hello`, `info`, `mem`, `halt`
97
98## Target audience
99
100Learning exercise and toy. Not a Linux replacement. Not POSIX-compatible.
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..6254de9
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,7 @@
1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
3version = 4
4
5[[package]]
6name = "raisernics"
7version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..46de2e1
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,10 @@
1[package]
2name = "raisernics"
3version = "0.1.0"
4edition = "2021"
5
6[profile.release]
7lto = true
8codegen-units = 1
9strip = "symbols"
10opt-level = "z"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9f03f93
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,124 @@
1# shellcheck shell=sh disable=SC2046,SC2091,SC2086,SC2155
2# NOTE: $(VAR) in this file is MAKE syntax, not shell substitution.
3export TARGET=x86_64-unknown-uefi
4export PROFILE=debug
5export OUT_DIR=target/$(TARGET)/$(PROFILE)
6export ESP_DIR=esp
7export KERNEL=$(OUT_DIR)/raisernics.efi
8export BZIMAGE=$(ESP_DIR)/bzImage
9export INITRD=$(ESP_DIR)/initramfs.cpio.gz
10export BOOTLOADER=../rng/rsbl/../build/bootloader/BOOTX64.EFI
11export OVMF=/usr/share/edk2/x64/OVMF.4m.fd
12export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI
13
14.PHONY: all run run-gui clean image debug release fmt
15
16# ---- Build ---------------------------------------------------------------
17
18all: $(BZIMAGE) $(INITRD) $(BOOT_EFI)
19
20release: PROFILE=release
21release: export PROFILE=release
22release: export OUT_DIR=target/$(TARGET)/release
23release: CARGO_FLAGS=--release
24release: all
25
26# shellcheck disable=SC2034
27CARGO_FLAGS=
28
29$(OUT_DIR)/raisernics.efi: $(shell find src -name '*.rs') Cargo.toml build.rs
30 cargo build --target $(TARGET) $(CARGO_FLAGS)
31
32$(BZIMAGE): $(KERNEL) | $(ESP_DIR)
33 cp "$<" "$@"
34
35$(BOOT_EFI): $(BOOTLOADER) | $(ESP_DIR)
36 mkdir -p $(ESP_DIR)/EFI/BOOT
37 cp "$<" "$@"
38
39$(ESP_DIR):
40 mkdir -p $(ESP_DIR)
41
42$(INITRD): | $(ESP_DIR)
43 printf '' | gzip > "$@"
44
45# ---- QEMU runners --------------------------------------------------------
46
47run: all
48 qemu-system-x86_64 \
49 -bios $(OVMF) \
50 -drive file=fat:rw:$(ESP_DIR),format=raw \
51 -serial stdio \
52 -m 256M \
53 -machine q35 \
54 -cpu qemu64 \
55 -no-reboot \
56 -display none
57
58run-gui: all
59 qemu-system-x86_64 \
60 -bios $(OVMF) \
61 -drive file=fat:rw:$(ESP_DIR),format=raw \
62 -serial stdio \
63 -m 256M \
64 -machine q35 \
65 -cpu qemu64 \
66 -no-reboot \
67 -vga std
68
69# GDB stub on :1234
70debug: all
71 qemu-system-x86_64 \
72 -bios $(OVMF) \
73 -drive file=fat:rw:$(ESP_DIR),format=raw \
74 -serial stdio \
75 -m 256M \
76 -machine q35 \
77 -cpu qemu64 \
78 -no-reboot \
79 -display none \
80 -s -S
81
82# Boot from a raw disk image (created by tools/boot-image.sh)
83run-image: all
84 qemu-system-x86_64 \
85 -bios $(OVMF) \
86 -drive file=$(IMG),format=raw,if=none,id=hd \
87 -device ide-hd,drive=hd \
88 -serial stdio \
89 -m 256M \
90 -machine q35 \
91 -cpu qemu64 \
92 -no-reboot \
93 -display none
94
95# ---- Disk image ----------------------------------------------------------
96
97image: all
98 ./tools/boot-image.sh raisernics.img
99
100# ---- Utilities -----------------------------------------------------------
101
102fmt:
103 cargo fmt
104
105clean:
106 cargo clean
107 rm -rf $(ESP_DIR) raisernics.img
108
109help:
110 @echo "raisernics — Make targets"
111 @echo ""
112 @echo " make build kernel + ESP (default)"
113 @echo " make release build with --release (smaller binary)"
114 @echo " make image create bootable USB image (raisernics.img)"
115 @echo " make run QEMU serial console (no graphics)"
116 @echo " make run-gui QEMU with graphical window (GOP/PS2)"
117 @echo " make debug QEMU + GDB stub on :1234"
118 @echo " make clean remove build artifacts"
119 @echo " make fmt format Rust source"
120 @echo ""
121 @echo "Scripts:"
122 @echo " ./tools/qemu.sh flexible QEMU launcher"
123 @echo " ./tools/boot-image.sh create bootable disk image"
124 @echo " ./tools/debug-kernel.py GDB plugin (attach after 'make debug')"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..44e6344
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,70 @@
1/// Build script — extracts glyph data from a PSF2 font file.
2/// Uses zcat (system gzip) to decompress — zero dependencies.
3use std::env;
4use std::fs;
5use std::path::Path;
6use std::process::{Command, Output};
7
8fn main() -> Result<(), Box<dyn std::error::Error>> {
9 let font_path = "/usr/share/kbd/consolefonts/lat1-16.psfu.gz";
10 println!("cargo::rerun-if-changed={}", font_path);
11
12 // Decompress via zcat (available on any Linux system)
13 let Output { stdout, status, .. } = Command::new("zcat").arg(font_path).output()?;
14 if !status.success() {
15 return Err(format!("zcat exited with {status:?}").into());
16 }
17 let raw = stdout;
18
19 if raw.len() < 32 {
20 return Err("PSF font too small".into());
21 }
22
23 // PSF2 magic: 0x864ab572 (little-endian)
24 let magic = u32::from_le_bytes(raw[..4].try_into()?);
25 if magic != 0x864ab572 {
26 return Err("not a PSF2 font".into());
27 }
28
29 let hdr_size = u32::from_le_bytes(raw[8..12].try_into()?) as usize;
30 let num_glyphs = u32::from_le_bytes(raw[16..20].try_into()?) as usize;
31 let bytes_per_glyph = u32::from_le_bytes(raw[20..24].try_into()?) as usize;
32
33 if num_glyphs != 256 {
34 return Err(format!("expected 256 glyphs, got {num_glyphs}").into());
35 }
36 if bytes_per_glyph != 16 {
37 return Err(format!("expected 16 bytes/glyph, got {bytes_per_glyph}").into());
38 }
39
40 let glyph_data = &raw[hdr_size..hdr_size + num_glyphs * bytes_per_glyph];
41
42 // Write generated Rust source into OUT_DIR
43 let out_dir = env::var("OUT_DIR")?;
44 let dest = Path::new(&out_dir).join("font_data.rs");
45
46 let mut code = String::from(
47 "/// 8×16 bitmap font glyph data (256 glyphs, 16 bytes each).
48#[rustfmt::skip]
49#[allow(clippy::unreadable_literal)]
50pub const GLYPH_8X16: [[u8; 16]; 256] = [",
51 );
52
53 for g in 0..256 {
54 let offset = g * 16;
55 code.push_str("\n [");
56 for row in 0..16 {
57 if row % 4 == 0 {
58 code.push('\n');
59 }
60 let byte = glyph_data[offset + row];
61 code.push_str(&format!(" 0x{byte:02X},"));
62 }
63 code.push_str("\n ],");
64 }
65
66 code.push_str("\n];\n");
67
68 fs::write(&dest, code)?;
69 Ok(())
70}
diff --git a/docs/adr/0001-static-buffer-for-memory-map.md b/docs/adr/0001-static-buffer-for-memory-map.md
new file mode 100644
index 0000000..75a4059
--- /dev/null
+++ b/docs/adr/0001-static-buffer-for-memory-map.md
@@ -0,0 +1,54 @@
1# ADR 0001 — Static .bss Buffer for UEFI Memory Map Capture
2
3## Status
4
5Accepted.
6
7## Context
8
9Before calling `ExitBootServices`, the kernel must obtain the UEFI memory map
10to know which physical memory regions are available. The standard UEFI recipe
11is:
12
131. Call `GetMemoryMap` with a null buffer to get the required buffer size.
142. Allocate a buffer (via `AllocatePool`).
153. Call `GetMemoryMap` again to fill the buffer, receiving a `MapKey`.
164. Call `ExitBootServices(image_handle, MapKey)`.
17
18**Problem:** Steps 2 and 3 change the memory map themselves (the pool
19allocation adds entries). After step 3 we have a valid `MapKey` for the
20map *as it was during step 3*, but calling `FreePool` on the buffer
21(before step 4) would change the map again and invalidate the key.
22Not freeing the buffer leaks pool memory, but more importantly, the
23default approach of `AllocatePool + GetMemoryMap + ExitBootServices`
24between two `GetMemoryMap` calls makes the key fragile.
25
26## Decision
27
28Use a **pre-allocated static buffer in `.bss`** (64 KB) instead of
29dynamically allocating a pool buffer. Because the buffer already exists,
30no `AllocatePool`/`FreePool` calls happen between `GetMemoryMap` and
31`ExitBootServices`.
32
33The sequence becomes:
34
351. `GetMemoryMap(null)` → determine required size.
362. `GetMemoryMap(static_buf)` → fill the map, receive `MapKey`.
373. `ExitBootServices(image_handle, MapKey)` — no pool allocation
38 happened after step 2, so the key is guaranteed valid.
39
40If the static buffer is too small (it shouldn't be — 64 KB holds ~1300
41descriptors, real firmware typically uses ~50–150), the caller falls
42back to a pool-allocated buffer.
43
44## Consequences
45
46- **Positive:** The `MapKey` is never invalidated between capture and
47 `ExitBootServices`. The boot sequence is simpler and more reliable.
48- **Positive:** No pool memory leaks — the static buffer lives in `.bss`
49 and costs nothing to keep.
50- **Negative:** 64 KB of `.bss` is permanently reserved (irrelevant on a
51 kernel that owns all memory).
52- **Negative:** The buffer size is fixed at compile time; pathological
53 firmware with thousands of entries would require a fallback path
54 (implemented but never hit in practice).
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..75d2233
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
1[toolchain]
2channel = "stable"
3targets = ["x86_64-unknown-uefi"]
diff --git a/src/font.rs b/src/font.rs
new file mode 100644
index 0000000..e188513
--- /dev/null
+++ b/src/font.rs
@@ -0,0 +1,5 @@
1/// 8×16 bitmap font.
2///
3/// Glyphs are generated at build time by `build.rs` from the system PSF2 font
4/// (`/usr/share/kbd/consolefonts/lat1-16.psfu.gz`).
5include!(concat!(env!("OUT_DIR"), "/font_data.rs"));
diff --git a/src/gop.rs b/src/gop.rs
new file mode 100644
index 0000000..9645bb0
--- /dev/null
+++ b/src/gop.rs
@@ -0,0 +1,232 @@
1/// GOP framebuffer driver — renders text via a bitmap font.
2use crate::font;
3use crate::uefi::{GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB};
4use core::fmt;
5use core::ptr;
6
7/// Framebuffer state captured before ExitBootServices.
8pub struct Framebuffer {
9 base: *mut u8,
10 #[allow(dead_code)]
11 width: usize,
12 height: usize,
13 pitch: usize, // bytes per scan line
14 bytes_per_pixel: usize,
15 bg: u32, // packed BGRA or RGBA background colour
16 fg: u32, // packed foreground colour
17 cursor_x: usize,
18 cursor_y: usize,
19 rows: usize,
20 cols: usize,
21}
22
23static mut FB: Option<Framebuffer> = None;
24
25/// Initialise the framebuffer from GOP. Call *before* ExitBootServices.
26pub fn init(gop: &mut GraphicsOutputProtocol) {
27 let mode = unsafe { &*gop.mode };
28 let info = unsafe { &*mode.info };
29 let base = mode.framebuffer_base as *mut u8;
30 let width = info.horizontal_resolution as usize;
31 let height = info.vertical_resolution as usize;
32 let pitch = info.pixels_per_scan_line as usize * 4; // 32-bit pixel
33 let bpp = 4;
34
35 // Pick colours based on pixel format
36 let (fg, bg) = match info.pixel_format {
37 PIXEL_BGR => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), // white fg, black bg
38 PIXEL_RGB => (0x00_FF_FF_FFu32, 0x00_00_00_00u32),
39 _ => (0x00_FF_FF_FFu32, 0x00_00_00_00u32),
40 };
41
42 let rows = height / 16;
43 let cols = width / 8;
44
45 let fb = Framebuffer {
46 base,
47 width,
48 height,
49 pitch,
50 bytes_per_pixel: bpp,
51 bg,
52 fg,
53 cursor_x: 0,
54 cursor_y: 0,
55 rows,
56 cols,
57 };
58
59 fb.clear();
60
61 unsafe {
62 FB = Some(fb);
63 }
64}
65
66/// Draw a character at the current cursor position and advance.
67pub fn put_char(c: char) {
68 let fb = unsafe {
69 match FB.as_mut() {
70 Some(f) => f,
71 None => return,
72 }
73 };
74
75 if c == '\x08' {
76 // Backspace - move cursor back and erase
77 if fb.cursor_x > 0 {
78 fb.cursor_x -= 1;
79 } else if fb.cursor_y > 0 {
80 fb.cursor_y -= 1;
81 fb.cursor_x = fb.cols - 1;
82 } else {
83 return;
84 }
85 // Draw glyph 32 (space) to erase — glyph 0 is NUL, not blank
86 fb.draw_glyph(32, fb.cursor_x, fb.cursor_y);
87 return;
88 }
89 if c == '\n' {
90 fb.cursor_x = 0;
91 fb.cursor_y += 1;
92 if fb.cursor_y >= fb.rows {
93 fb.scroll();
94 }
95 return;
96 }
97 if c == '\r' {
98 fb.cursor_x = 0;
99 return;
100 }
101
102 let idx = c as usize;
103 if idx >= 256 {
104 // Unmapped character: draw a space
105 fb.draw_glyph(0, fb.cursor_x, fb.cursor_y);
106 } else {
107 fb.draw_glyph(idx, fb.cursor_x, fb.cursor_y);
108 }
109
110 fb.cursor_x += 1;
111 if fb.cursor_x >= fb.cols {
112 fb.cursor_x = 0;
113 fb.cursor_y += 1;
114 if fb.cursor_y >= fb.rows {
115 fb.scroll();
116 }
117 }
118}
119
120/// Write a string to the framebuffer.
121pub fn write_str(s: &str) {
122 for c in s.chars() {
123 put_char(c);
124 }
125}
126
127/// Clear the entire screen to background colour and reset cursor to (0, 0).
128pub fn clear_screen() {
129 let fb = unsafe {
130 match FB.as_mut() {
131 Some(f) => f,
132 None => return,
133 }
134 };
135 fb.clear();
136 // Reset cursor back to top-left so the next prompt appears at (0, 0)
137 fb.cursor_x = 0;
138 fb.cursor_y = 0;
139}
140
141// ---------------------------------------------------------------------------
142// Framebuffer implementation
143// ---------------------------------------------------------------------------
144impl Framebuffer {
145 fn clear(&self) {
146 let size = self.pitch * self.height;
147 // Fill with background colour (32-bit writes)
148 let mut p = self.base;
149 let end = unsafe { p.add(size) };
150 unsafe {
151 while p < end {
152 ptr::write_volatile(p as *mut u32, self.bg);
153 p = p.add(4);
154 }
155 }
156 }
157
158 fn scroll(&mut self) {
159 // Move everything up one row (16 pixels)
160 let row_bytes = self.pitch * 16;
161 let total = self.pitch * self.height;
162 unsafe {
163 ptr::copy(self.base.add(row_bytes), self.base, total - row_bytes);
164 // Clear last row
165 let last_row = self.base.add(total - row_bytes);
166 for i in 0..(self.pitch * 16 / 4) {
167 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg);
168 }
169 }
170 self.cursor_y -= 1;
171 }
172
173 fn draw_glyph(&self, glyph_idx: usize, col: usize, row: usize) {
174 let glyph = font::GLYPH_8X16;
175 let data = match glyph.get(glyph_idx) {
176 Some(d) => *d,
177 None => return,
178 };
179
180 let x = col * 8;
181 let y = row * 16;
182
183 for cy in 0..16 {
184 let row_bits = data[cy];
185 for cx in 0..8 {
186 if row_bits & (0x80 >> cx) != 0 {
187 // Set foreground pixel
188 let offset = (y + cy) * self.pitch + (x + cx) * self.bytes_per_pixel;
189 unsafe {
190 ptr::write_volatile(self.base.add(offset) as *mut u32, self.fg);
191 }
192 } else {
193 // Set background pixel
194 let offset = (y + cy) * self.pitch + (x + cx) * self.bytes_per_pixel;
195 unsafe {
196 ptr::write_volatile(self.base.add(offset) as *mut u32, self.bg);
197 }
198 }
199 }
200 }
201 }
202}
203
204// ---------------------------------------------------------------------------
205// fmt::Write wrapper
206// ---------------------------------------------------------------------------
207pub struct GopWriter;
208
209impl fmt::Write for GopWriter {
210 fn write_str(&mut self, s: &str) -> fmt::Result {
211 write_str(s);
212 Ok(())
213 }
214}
215
216/// Write a formatted string to the framebuffer.
217#[macro_export]
218macro_rules! gop_print {
219 ($($arg:tt)*) => {{
220 use core::fmt::Write;
221 let _ = write!(&mut $crate::gop::GopWriter, $($arg)*);
222 }};
223}
224
225#[macro_export]
226macro_rules! gop_println {
227 () => { $crate::gop::write_str("\n"); };
228 ($($arg:tt)*) => {{
229 $crate::gop_print!($($arg)*);
230 $crate::gop::write_str("\n");
231 }};
232}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..2b1df77
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,181 @@
1#![no_std]
2#![no_main]
3#![allow(static_mut_refs, unsafe_code, dead_code, unused_doc_comments)]
4
5// SAFETY: single-core kernel, no preemption — static mut access is sound
6// Toy kernel: dead code and unused items during active development
7
8extern crate alloc;
9
10mod font;
11mod gop;
12mod memory;
13mod ps2;
14mod serial;
15mod shell;
16mod uefi;
17
18use core::panic::PanicInfo;
19use uefi::*;
20
21// ---------------------------------------------------------------------------
22// Panic handler
23// ---------------------------------------------------------------------------
24#[panic_handler]
25fn panic(info: &PanicInfo) -> ! {
26 use core::fmt::Write;
27 serial::write_str("\r\n*** PANIC ***\r\n");
28 let _ = write!(&mut serial::SerialWriter, "{}\r\n", info.message());
29 if let Some(loc) = info.location() {
30 let _ = write!(
31 &mut serial::SerialWriter,
32 " at {}:{}\r\n",
33 loc.file(),
34 loc.line()
35 );
36 }
37 loop {
38 unsafe {
39 core::arch::asm!("cli; hlt");
40 }
41 }
42}
43
44// ---------------------------------------------------------------------------
45// EFI entry point
46// ---------------------------------------------------------------------------
47#[no_mangle]
48pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemTable) -> ! {
49 let st = unsafe { &mut *system_table };
50 let bs = unsafe { &mut *st.boot_services };
51 let con_out = unsafe { &mut *st.con_out };
52
53 // ---- Phase 1: UEFI boot services alive ----
54
55 // Print banner via UEFI console
56 unsafe {
57 let banner: [u16; 28] = [
58 'r' as u16, 'a' as u16, 'i' as u16, 's' as u16, 'e' as u16, 'r' as u16, 'n' as u16,
59 'i' as u16, 'c' as u16, 's' as u16, ' ' as u16, 'v' as u16, '0' as u16, '.' as u16,
60 '1' as u16, ' ' as u16, '-' as u16, ' ' as u16, 'b' as u16, 'o' as u16, 'o' as u16,
61 't' as u16, 'i' as u16, 'n' as u16, 'g' as u16, '.' as u16, '.' as u16, 0x0A,
62 ];
63 (con_out.output_string)(con_out, banner.as_ptr());
64 }
65
66 // Init serial early so we can debug-print before EBS
67 serial::init();
68
69 serial_println!("=== raisernics boot ===");
70
71 // Stash the memory map
72 let map_key = memory::capture_memory_map(bs);
73 let map_key = match map_key {
74 Ok(k) => k,
75 Err(e) => {
76 serial_println!("mem: capture_memory_map failed 0x{:x}", e);
77 0
78 }
79 };
80
81 // Locate GOP (Graphics Output Protocol)
82 let mut gop_interface: *mut core::ffi::c_void = core::ptr::null_mut();
83 let gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
84 let gop_result = unsafe {
85 (bs.locate_protocol)(
86 &gop_guid as *const Guid,
87 core::ptr::null_mut(),
88 &mut gop_interface,
89 )
90 };
91 if gop_result == EFI_SUCCESS {
92 let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) };
93 serial_println!("gop: framebuffer found");
94 gop::init(gop);
95 } else {
96 serial_println!("gop: not found (0x{:x})", gop_result);
97 }
98
99 // ExitBootServices — we now own the machine
100 let ebs_result = unsafe { (bs.exit_boot_services)(image_handle, map_key) };
101 if ebs_result != EFI_SUCCESS {
102 // Memory map may have changed; try again with fresh map
103 // (This is the standard UEFI EBS retry dance.)
104 serial_println!(
105 "WARN: ExitBootServices failed (0x{:x}), retrying...",
106 ebs_result
107 );
108 let _map_key = match memory::refresh_and_exit(bs, image_handle) {
109 Ok(k) => k,
110 Err(e) => {
111 serial_println!("FATAL: cannot ExitBootServices (0x{:x})", e);
112 loop {
113 unsafe {
114 core::arch::asm!("cli; hlt");
115 }
116 }
117 }
118 };
119 }
120
121 // ---- Phase 2: Native mode ----
122
123 serial::init();
124 serial_println!("serial: COM1 115200 8N1");
125
126 // PS/2 keyboard
127 ps2::init();
128 serial_println!("ps2: keyboard initialized");
129
130 // Memory: the bump allocator was set up during capture_memory_map.
131 // `alloc::Vec`, `alloc::String`, `format!` etc. are now usable.
132
133 // Print memory summary
134 dump_memory_summary();
135
136 serial_println!("boot: entering shell");
137 shell::run();
138}
139
140// ---------------------------------------------------------------------------
141// Print a memory summary from the stashed memory map
142// ---------------------------------------------------------------------------
143fn dump_memory_summary() {
144 let (map, key) = memory::get_map();
145 let mut conventional_4k = 0u64;
146 let mut addr_space_4k = 0u64;
147 for desc in map.iter() {
148 let pages = desc.number_of_pages;
149 addr_space_4k += pages;
150 if desc.kind == 7 {
151 conventional_4k += pages;
152 }
153 }
154 serial_println!(
155 "mem: {} entries, key=0x{:x}, usable: {} KB ({} MB)",
156 map.len(),
157 key,
158 conventional_4k * 4,
159 (conventional_4k * 4) / 1024,
160 );
161 serial_println!(
162 "mem: address space: {} MB (incl. PCIe MMIO)",
163 (addr_space_4k * 4) / 1024,
164 );
165}
166
167// ---------------------------------------------------------------------------
168// Helper: write a panic-proof string via UEFI console (used before serial)
169// ---------------------------------------------------------------------------
170#[allow(dead_code)]
171unsafe fn uefi_puts(con_out: *mut SimpleTextOutput, s: &str) {
172 // Simple ASCII → UEFI-CHAR16 conversion (stack buffer, panic-safe)
173 let mut buf: [u16; 128] = [0; 128];
174 let len = s.len().min(127);
175 for (i, &b) in s.as_bytes()[..len].iter().enumerate() {
176 buf[i] = b as u16;
177 }
178 buf[len] = 0x0A; // LF
179 let out = unsafe { &mut *con_out };
180 (out.output_string)(out, buf.as_ptr());
181}
diff --git a/src/memory.rs b/src/memory.rs
new file mode 100644
index 0000000..d223f69
--- /dev/null
+++ b/src/memory.rs
@@ -0,0 +1,196 @@
1use crate::uefi::*;
2use core::alloc::{GlobalAlloc, Layout};
3use core::mem;
4use core::ptr;
5
6const MAX_MAP_ENTRIES: usize = 512;
7
8static mut MEMORY_MAP: [MemoryDescriptor; MAX_MAP_ENTRIES] = unsafe { mem::zeroed() };
9static mut MAP_ENTRIES: usize = 0;
10
11/// The map key returned by GetMemoryMap, needed for ExitBootServices.
12static mut MAP_KEY: usize = 0;
13
14/// Static working buffer for GetMemoryMap (64 KB, way more than enough).
15/// Lives in .bss; no pool allocation, so no map-key staleness.
16static mut MAP_WORK_BUF: [u8; 65536] = [0u8; 65536];
17
18// ---------------------------------------------------------------------------
19// Bump allocator — no deallocation, just pointer advancement.
20// Used as Rust's #[global_allocator] once the heap region is set up.
21// ---------------------------------------------------------------------------
22struct BumpAllocator;
23
24unsafe impl Sync for BumpAllocator {}
25
26unsafe impl GlobalAlloc for BumpAllocator {
27 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
28 let align = layout.align().max(16);
29 let size = layout.size();
30 unsafe {
31 let a = (HEAP_CURSOR + align - 1) & !(align - 1);
32 let e = a + size;
33 if e > HEAP_END || e < a {
34 return ptr::null_mut();
35 }
36 HEAP_CURSOR = e;
37 a as *mut u8
38 }
39 }
40
41 unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
42 // Bump allocator — cannot free individual allocations.
43 // Memory is reclaimed on reboot. Good enough for now.
44 unsafe {
45 core::ptr::read_volatile(&_ptr);
46 }
47 let _ = _layout;
48 }
49}
50
51#[global_allocator]
52static ALLOCATOR: BumpAllocator = BumpAllocator;
53
54/// Heap bump allocator bounds (`static mut` accessed only inside
55/// `GlobalAlloc` impl, which is already `unsafe`).
56static mut HEAP_START: usize = 0;
57static mut HEAP_END: usize = 0;
58static mut HEAP_CURSOR: usize = 0;
59
60/// Capture the UEFI memory map into the static buffer. Uses a pre-allocated
61/// .bss working buffer so no pool allocation (and thus no memory-map change)
62/// occurs between GetMemoryMap and ExitBootServices.
63pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> {
64 let mut map_key: usize = 0;
65 let mut desc_size: usize = 0;
66 let mut desc_ver: u32 = 0;
67
68 // Probe with null to get required size + descriptor size
69 let mut buf_size: usize = 0;
70 let status = unsafe {
71 (bs.get_memory_map)(
72 &mut buf_size,
73 ptr::null_mut(),
74 &mut map_key,
75 &mut desc_size,
76 &mut desc_ver,
77 )
78 };
79 if status != EFI_BUFFER_TOO_SMALL {
80 // Fallback: allocate a sensible size
81 buf_size = 64 * 1024;
82 }
83 if desc_size == 0 {
84 desc_size = mem::size_of::<MemoryDescriptor>();
85 }
86
87 // Round up to desc_size alignment
88 let buf_size = (buf_size + desc_size - 1) / desc_size * desc_size;
89
90 // If our static buffer is too small, fall back to pool (rare)
91 if buf_size > unsafe { MAP_WORK_BUF.len() } {
92 return Err(EFI_BUFFER_TOO_SMALL);
93 }
94
95 let mut actual_size = buf_size;
96 let status = unsafe {
97 (bs.get_memory_map)(
98 &mut actual_size,
99 &raw mut MAP_WORK_BUF[0] as *mut MemoryDescriptor,
100 &mut map_key,
101 &mut desc_size,
102 &mut desc_ver,
103 )
104 };
105
106 if status == EFI_BUFFER_TOO_SMALL {
107 // The map grew between the probe and the real call. Try a larger
108 // static buffer (can't grow here without pool, so return error to
109 // let the caller retry if they can allocate pool).
110 // For our 64 KB buffer this should never happen on OVMF.
111 return Err(EFI_BUFFER_TOO_SMALL);
112 }
113 if status != EFI_SUCCESS {
114 return Err(status);
115 }
116
117 // Copy descriptors into our indexed array
118 let count = actual_size / desc_size;
119 let count = count.min(MAX_MAP_ENTRIES);
120 unsafe {
121 for i in 0..count {
122 let p = (&raw const MAP_WORK_BUF[..]) as *const u8;
123 let off = i * desc_size;
124 let src = p.add(off) as *const MemoryDescriptor;
125 MEMORY_MAP[i] = ptr::read(src);
126 }
127 MAP_ENTRIES = count;
128 MAP_KEY = map_key;
129 }
130
131 // Do NOT free anything — the static buffer stays. Map key is still
132 // valid because no pool allocation/free happened after GetMemoryMap.
133
134 init_bump_allocator();
135 Ok(map_key)
136}
137
138/// Retry ExitBootServices with a fresh memory map.
139pub fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result<usize, Status> {
140 let key = capture_memory_map(bs)?;
141 let status = unsafe { (bs.exit_boot_services)(image_handle, key) };
142 if status == EFI_SUCCESS {
143 Ok(key)
144 } else {
145 Err(status)
146 }
147}
148
149fn init_bump_allocator() {
150 unsafe {
151 let mut best_start = 0usize;
152 let mut best_size = 0usize;
153 for i in 0..MAP_ENTRIES {
154 let d = &MEMORY_MAP[i];
155 if d.kind == MEMORY_TYPE_CONVENTIONAL {
156 let sz = d.number_of_pages as usize * 4096;
157 if sz > best_size {
158 best_start = d.physical_start as usize;
159 best_size = sz;
160 }
161 }
162 }
163 if best_size > 2 * 1024 * 1024 {
164 let s = best_start + 2 * 1024 * 1024;
165 let e = best_start + best_size;
166 if s < e {
167 HEAP_START = s;
168 HEAP_END = e;
169 HEAP_CURSOR = s;
170 }
171 }
172 }
173}
174
175#[allow(dead_code)]
176pub fn allocate(size: usize) -> *mut u8 {
177 unsafe {
178 let a = (HEAP_CURSOR + 15) & !15;
179 let e = a + size;
180 if e > HEAP_END || e < a {
181 return ptr::null_mut();
182 }
183 HEAP_CURSOR = e;
184 a as *mut u8
185 }
186}
187
188#[allow(dead_code)]
189pub fn get_map() -> (&'static [MemoryDescriptor], usize) {
190 unsafe {
191 (
192 core::slice::from_raw_parts(&MEMORY_MAP[0], MAP_ENTRIES),
193 MAP_KEY,
194 )
195 }
196}
diff --git a/src/ps2.rs b/src/ps2.rs
new file mode 100644
index 0000000..e972658
--- /dev/null
+++ b/src/ps2.rs
@@ -0,0 +1,329 @@
1/// PS/2 keyboard driver — polling, scancode set 1, US QWERTY layout.
2// Port addresses
3const DATA_PORT: u16 = 0x60;
4const STATUS_PORT: u16 = 0x64;
5
6// Status register bits
7const STAT_OUTPUT_FULL: u8 = 0x01;
8
9// Modifier bit constants
10const MOD_LSHIFT: u8 = 0x01;
11const MOD_RSHIFT: u8 = 0x02;
12const MOD_LCTRL: u8 = 0x04;
13const MOD_RCTRL: u8 = 0x08;
14const MOD_LALT: u8 = 0x10;
15const MOD_RALT: u8 = 0x20;
16const MOD_CAPS: u8 = 0x40;
17
18/// A decoded key event.
19pub enum KeyEvent {
20 Char(char),
21 Enter,
22 Backspace,
23 Tab,
24 Escape,
25 ArrowUp,
26 ArrowDown,
27 ArrowLeft,
28 ArrowRight,
29 Home,
30 End,
31 PageUp,
32 PageDown,
33 Delete,
34 F(u8),
35 Unknown(u8),
36}
37
38// ----- scancode-set-1 translation tables (US QWERTY) -----
39
40/// Normal (unshifted) scancode → ASCII mapping. 0 = unmapped.
41static SCAN_NORMAL: [u8; 128] = {
42 let mut t = [0u8; 128];
43 t[0x02] = b'1';
44 t[0x03] = b'2';
45 t[0x04] = b'3';
46 t[0x05] = b'4';
47 t[0x06] = b'5';
48 t[0x07] = b'6';
49 t[0x08] = b'7';
50 t[0x09] = b'8';
51 t[0x0A] = b'9';
52 t[0x0B] = b'0';
53 t[0x0C] = b'-';
54 t[0x0D] = b'=';
55 t[0x10] = b'q';
56 t[0x11] = b'w';
57 t[0x12] = b'e';
58 t[0x13] = b'r';
59 t[0x14] = b't';
60 t[0x15] = b'y';
61 t[0x16] = b'u';
62 t[0x17] = b'i';
63 t[0x18] = b'o';
64 t[0x19] = b'p';
65 t[0x1A] = b'[';
66 t[0x1B] = b']';
67 t[0x1E] = b'a';
68 t[0x1F] = b's';
69 t[0x20] = b'd';
70 t[0x21] = b'f';
71 t[0x22] = b'g';
72 t[0x23] = b'h';
73 t[0x24] = b'j';
74 t[0x25] = b'k';
75 t[0x26] = b'l';
76 t[0x27] = b';';
77 t[0x28] = b'\'';
78 t[0x29] = b'`';
79 t[0x2B] = b'\\';
80 t[0x2C] = b'z';
81 t[0x2D] = b'x';
82 t[0x2E] = b'c';
83 t[0x2F] = b'v';
84 t[0x30] = b'b';
85 t[0x31] = b'n';
86 t[0x32] = b'm';
87 t[0x33] = b',';
88 t[0x34] = b'.';
89 t[0x35] = b'/';
90 t[0x37] = b'*';
91 t[0x39] = b' ';
92 t
93};
94
95/// Shifted scancode → ASCII.
96static SCAN_SHIFT: [u8; 128] = {
97 let mut t = [0u8; 128];
98 t[0x02] = b'!';
99 t[0x03] = b'@';
100 t[0x04] = b'#';
101 t[0x05] = b'$';
102 t[0x06] = b'%';
103 t[0x07] = b'^';
104 t[0x08] = b'&';
105 t[0x09] = b'*';
106 t[0x0A] = b'(';
107 t[0x0B] = b')';
108 t[0x0C] = b'_';
109 t[0x0D] = b'+';
110 t[0x10] = b'Q';
111 t[0x11] = b'W';
112 t[0x12] = b'E';
113 t[0x13] = b'R';
114 t[0x14] = b'T';
115 t[0x15] = b'Y';
116 t[0x16] = b'U';
117 t[0x17] = b'I';
118 t[0x18] = b'O';
119 t[0x19] = b'P';
120 t[0x1A] = b'{';
121 t[0x1B] = b'}';
122 t[0x1E] = b'A';
123 t[0x1F] = b'S';
124 t[0x20] = b'D';
125 t[0x21] = b'F';
126 t[0x22] = b'G';
127 t[0x23] = b'H';
128 t[0x24] = b'J';
129 t[0x25] = b'K';
130 t[0x26] = b'L';
131 t[0x27] = b':';
132 t[0x28] = b'"';
133 t[0x29] = b'~';
134 t[0x2B] = b'|';
135 t[0x2C] = b'Z';
136 t[0x2D] = b'X';
137 t[0x2E] = b'C';
138 t[0x2F] = b'V';
139 t[0x30] = b'B';
140 t[0x31] = b'N';
141 t[0x32] = b'M';
142 t[0x33] = b'<';
143 t[0x34] = b'>';
144 t[0x35] = b'?';
145 t[0x37] = b'*';
146 t[0x39] = b' ';
147 t
148};
149
150/// Internal keyboard state
151struct KbdState {
152 modifiers: u8,
153 got_e0: bool,
154}
155
156static mut KBD: KbdState = KbdState {
157 modifiers: 0,
158 got_e0: false,
159};
160
161/// Initialise the PS/2 port for the keyboard — just drain stale bytes.
162pub fn init() {
163 drain_input();
164}
165
166fn drain_input() {
167 unsafe {
168 loop {
169 let status: u8;
170 core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT,
171 options(nostack, preserves_flags));
172 if status & STAT_OUTPUT_FULL == 0 {
173 break;
174 }
175 let _data: u8;
176 core::arch::asm!("in al, dx", out("al") _data, in("dx") DATA_PORT,
177 options(nostack, preserves_flags));
178 }
179 }
180}
181
182/// Poll for a key event. Blocks until a key is pressed.
183pub fn poll_key() -> KeyEvent {
184 loop {
185 let byte = read_data();
186 if let Some(ev) = process_byte(byte) {
187 return ev;
188 }
189 }
190}
191
192/// Non-blocking poll.
193pub fn try_poll_key() -> Option<KeyEvent> {
194 unsafe {
195 let status: u8;
196 core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT,
197 options(nostack, preserves_flags));
198 if status & STAT_OUTPUT_FULL == 0 {
199 return None;
200 }
201 let data: u8;
202 core::arch::asm!("in al, dx", out("al") data, in("dx") DATA_PORT,
203 options(nostack, preserves_flags));
204 process_byte(data)
205 }
206}
207
208/// Read a byte from the keyboard data port (blocking).
209fn read_data() -> u8 {
210 unsafe {
211 loop {
212 let status: u8;
213 core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT,
214 options(nostack, preserves_flags));
215 if status & STAT_OUTPUT_FULL != 0 {
216 let data: u8;
217 core::arch::asm!("in al, dx", out("al") data, in("dx") DATA_PORT,
218 options(nostack, preserves_flags));
219 return data;
220 }
221 }
222 }
223}
224
225/// Process a single scancode byte. Returns Some(KeyEvent) when a visible
226/// event happens; returns None for modifier changes, releases, and prefixes.
227fn process_byte(byte: u8) -> Option<KeyEvent> {
228 let state = unsafe { &mut KBD };
229
230 if byte == 0xE0 {
231 state.got_e0 = true;
232 return None;
233 }
234
235 if state.got_e0 {
236 state.got_e0 = false;
237 return process_e0(byte);
238 }
239
240 // Break code (key release) — bit 7 set
241 if byte & 0x80 != 0 {
242 update_modifiers(byte & 0x7F, false, false);
243 return None;
244 }
245
246 // Make code (key press)
247 update_modifiers(byte, true, false);
248
249 match byte {
250 0x01 => Some(KeyEvent::Escape),
251 0x0E => Some(KeyEvent::Backspace),
252 0x0F => Some(KeyEvent::Tab),
253 0x1C => Some(KeyEvent::Enter),
254 0x37 => Some(KeyEvent::Char('*')),
255 0x3B..=0x44 => Some(KeyEvent::F(byte - 0x3B + 1)),
256 _ => {
257 let shifted = (state.modifiers & (MOD_LSHIFT | MOD_RSHIFT)) != 0;
258 // CapsLock inverts case for letters
259 let caps = state.modifiers & MOD_CAPS != 0;
260 let table = if shifted { SCAN_SHIFT } else { SCAN_NORMAL };
261 let mut ch = table[byte as usize];
262 // Handle CapsLock for letters
263 if caps && ch.is_ascii_alphabetic() {
264 // Swap case: if table gives lowercase, make uppercase; etc.
265 // Since normal table has lowercase, shift table has uppercase:
266 ch = if shifted {
267 ch.to_ascii_lowercase()
268 } else {
269 ch.to_ascii_uppercase()
270 };
271 }
272 if ch != 0 {
273 Some(KeyEvent::Char(ch as char))
274 } else {
275 Some(KeyEvent::Unknown(byte))
276 }
277 }
278 }
279}
280
281/// Process an E0-prefixed scancode.
282fn process_e0(byte: u8) -> Option<KeyEvent> {
283 if byte & 0x80 != 0 {
284 update_modifiers(byte & 0x7F, false, true);
285 return None;
286 }
287
288 update_modifiers(byte, true, true);
289
290 match byte {
291 0x48 => Some(KeyEvent::ArrowUp),
292 0x50 => Some(KeyEvent::ArrowDown),
293 0x4B => Some(KeyEvent::ArrowLeft),
294 0x4D => Some(KeyEvent::ArrowRight),
295 0x47 => Some(KeyEvent::Home),
296 0x4F => Some(KeyEvent::End),
297 0x49 => Some(KeyEvent::PageUp),
298 0x51 => Some(KeyEvent::PageDown),
299 0x53 => Some(KeyEvent::Delete),
300 0x35 => Some(KeyEvent::Char('/')),
301 _ => Some(KeyEvent::Unknown(byte)),
302 }
303}
304
305fn update_modifiers(scancode: u8, pressed: bool, e0: bool) {
306 let state = unsafe { &mut KBD };
307 match (scancode, e0) {
308 (0x2A, false) => set_mod(state, MOD_LSHIFT, pressed),
309 (0x36, false) => set_mod(state, MOD_RSHIFT, pressed),
310 (0x1D, false) => set_mod(state, MOD_LCTRL, pressed),
311 (0x1D, true) => set_mod(state, MOD_RCTRL, pressed),
312 (0x38, false) => set_mod(state, MOD_LALT, pressed),
313 (0x38, true) => set_mod(state, MOD_RALT, pressed),
314 (0x3A, false) => {
315 if pressed {
316 state.modifiers ^= MOD_CAPS;
317 }
318 }
319 _ => {}
320 }
321}
322
323fn set_mod(state: &mut KbdState, bit: u8, pressed: bool) {
324 if pressed {
325 state.modifiers |= bit;
326 } else {
327 state.modifiers &= !bit;
328 }
329}
diff --git a/src/serial.rs b/src/serial.rs
new file mode 100644
index 0000000..03fa570
--- /dev/null
+++ b/src/serial.rs
@@ -0,0 +1,118 @@
1/// UART 16550 serial driver — port-mapped I/O on COM1 (0x3F8)
2use core::fmt;
3
4const COM1: u16 = 0x3F8;
5
6const DATA_REG: u16 = COM1;
7const INTR_ENABLE: u16 = COM1 + 1;
8const FIFO_CONTROL: u16 = COM1 + 2;
9const LINE_CONTROL: u16 = COM1 + 3;
10const MODEM_CONTROL: u16 = COM1 + 4;
11const LINE_STATUS: u16 = COM1 + 5;
12
13const LSR_TX_EMPTY: u8 = 0x20;
14const LSR_DATA_READY: u8 = 0x01;
15
16/// Initialise the UART with 115200 baud, 8N1.
17pub fn init() {
18 unsafe {
19 // Disable interrupts
20 port_out(INTR_ENABLE, 0x00);
21 // Set DLAB=1 (baud rate divisor)
22 port_out(LINE_CONTROL, 0x80);
23 // Divisor = 1 → 115200 baud (with 1.8432 MHz clock)
24 port_out(DATA_REG, 0x01);
25 port_out(INTR_ENABLE, 0x00);
26 // 8-bit, 1 stop, no parity, DLAB=0
27 port_out(LINE_CONTROL, 0x03);
28 // Enable FIFO, clear, 14-byte threshold
29 port_out(FIFO_CONTROL, 0xC7);
30 // RTS/DSR set
31 port_out(MODEM_CONTROL, 0x0B);
32 }
33}
34
35/// Write a single byte to the serial port (blocking poll).
36pub fn put_byte(b: u8) {
37 unsafe {
38 // Wait for transmitter holding register empty
39 while port_in(LINE_STATUS) & LSR_TX_EMPTY == 0 {}
40 port_out(DATA_REG, b);
41 }
42}
43
44/// Read a single byte from the serial port (blocking poll).
45pub fn get_byte() -> u8 {
46 unsafe {
47 while port_in(LINE_STATUS) & LSR_DATA_READY == 0 {}
48 port_in(DATA_REG)
49 }
50}
51
52/// Non-blocking read: returns Some(byte) if data ready, None otherwise.
53pub fn try_get_byte() -> Option<u8> {
54 unsafe {
55 if port_in(LINE_STATUS) & LSR_DATA_READY != 0 {
56 Some(port_in(DATA_REG))
57 } else {
58 None
59 }
60 }
61}
62
63/// Write a string to serial.
64pub fn write_str(s: &str) {
65 for &b in s.as_bytes() {
66 if b == b'\n' {
67 put_byte(b'\r');
68 }
69 put_byte(b);
70 }
71}
72
73// ---------------------------------------------------------------------------
74// fmt::Write implementation
75// ---------------------------------------------------------------------------
76pub struct SerialWriter;
77
78impl fmt::Write for SerialWriter {
79 fn write_str(&mut self, s: &str) -> fmt::Result {
80 write_str(s);
81 Ok(())
82 }
83}
84
85/// Helper macro — prints to serial via format_args!.
86#[macro_export]
87macro_rules! serial_print {
88 ($($arg:tt)*) => {{
89 use core::fmt::Write;
90 let _ = write!(&mut $crate::serial::SerialWriter, $($arg)*);
91 }};
92}
93
94#[macro_export]
95macro_rules! serial_println {
96 () => { $crate::serial::write_str("\r\n"); };
97 ($($arg:tt)*) => {{
98 $crate::serial_print!($($arg)*);
99 $crate::serial::write_str("\r\n");
100 }};
101}
102
103// ---------------------------------------------------------------------------
104// Port I/O helpers
105// ---------------------------------------------------------------------------
106unsafe fn port_in(port: u16) -> u8 {
107 let val: u8;
108 unsafe {
109 core::arch::asm!("in al, dx", out("al") val, in("dx") port, options(nostack, preserves_flags));
110 }
111 val
112}
113
114unsafe fn port_out(port: u16, val: u8) {
115 unsafe {
116 core::arch::asm!("out dx, al", in("al") val, in("dx") port, options(nostack, preserves_flags));
117 }
118}
diff --git a/src/shell.rs b/src/shell.rs
new file mode 100644
index 0000000..f21525e
--- /dev/null
+++ b/src/shell.rs
@@ -0,0 +1,274 @@
1/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial.
2use crate::ps2::{self, KeyEvent};
3use crate::{gop_print, gop_println, serial_print, serial_println};
4
5/// Maximum input line length.
6const LINE_BUF_SIZE: usize = 256;
7
8/// Run the shell forever.
9pub fn run() -> ! {
10 gop_println!("raisernics v0.1 — type 'help' for commands");
11 serial_println!("========== raisernics v0.1 ==========");
12
13 loop {
14 // Print prompt
15 gop_print!("> ");
16 serial_print!("> ");
17
18 // Read one line from keyboard
19 let line = read_line();
20
21 // Parse and execute
22 let trimmed = line.trim();
23 if trimmed.is_empty() {
24 continue;
25 }
26
27 let (cmd, args) = split_first_word(trimmed);
28
29 match cmd {
30 "help" => cmd_help(),
31 "echo" => cmd_echo(args),
32 "clear" => cmd_clear(),
33 "halt" | "poweroff" | "exit" => cmd_halt(),
34 "hello" => {
35 gop_println!("World!");
36 serial_println!("World!");
37 }
38 "info" => cmd_info(),
39 "mem" => cmd_mem(),
40 _ => {
41 gop_println!("unknown command: {}", cmd);
42 serial_println!("unknown command: {}", cmd);
43 }
44 }
45 }
46}
47
48// ---------------------------------------------------------------------------
49// Line input (PS/2 keyboard, no heap)
50// ---------------------------------------------------------------------------
51fn read_line() -> heapless_str::String<LINE_BUF_SIZE> {
52 let mut buf = heapless_str::String::<LINE_BUF_SIZE>::new();
53 loop {
54 match ps2::poll_key() {
55 KeyEvent::Enter => {
56 crate::gop::put_char('\n');
57 crate::serial::write_str("\r\n");
58 return buf;
59 }
60 KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => {
61 if buf.push(c).is_ok() {
62 crate::gop::put_char(c);
63 // Echo to serial too
64 let mut s = [0u8; 4];
65 let s = c.encode_utf8(&mut s);
66 crate::serial::write_str(s);
67 }
68 }
69 KeyEvent::Backspace => {
70 if buf.pop().is_some() {
71 // GOP backspace: move cursor back and erase
72 crate::gop::put_char('\x08');
73 // Serial backspace: send BS SP BS (standard terminal protocol)
74 crate::serial::write_str("\x08 \x08");
75 }
76 }
77 KeyEvent::Tab => {
78 for _ in 0..4 {
79 if buf.push(' ').is_ok() {
80 crate::gop::put_char(' ');
81 crate::serial::write_str(" ");
82 }
83 }
84 }
85 _ => {}
86 }
87 }
88}
89
90/// Split a command line into the first word and the rest.
91fn split_first_word(s: &str) -> (&str, &str) {
92 let s = s.trim_start();
93 if let Some(i) = s.find(char::is_whitespace) {
94 (&s[..i], s[i..].trim_start())
95 } else {
96 (s, "")
97 }
98}
99
100// ---------------------------------------------------------------------------
101// Built-in commands
102// ---------------------------------------------------------------------------
103
104fn cmd_help() {
105 gop_println!("available commands:");
106 gop_println!(" help — show this message");
107 gop_println!(" echo — echo arguments");
108 gop_println!(" clear — clear screen");
109 gop_println!(" hello — print 'World!'");
110 gop_println!(" info — show system info");
111 gop_println!(" mem — show memory info");
112 gop_println!(" halt — halt the system");
113 gop_println!(" exit — alias for halt");
114 serial_println!("available commands:");
115 serial_println!(" help, echo, clear, hello, info, mem, halt, exit");
116}
117
118fn cmd_echo(args: &str) {
119 gop_println!("{}", args);
120}
121
122fn cmd_clear() {
123 crate::gop::clear_screen();
124}
125
126fn cmd_info() {
127 // Test that the global allocator works: build a String on the heap
128 let mut s = alloc::string::String::from("raisernics v0.1");
129 s.push_str(" — RNG custom kernel");
130 gop_println!("{}", s);
131 gop_println!("arch: x86_64, freestanding, no userspace");
132 serial_println!("{}", s);
133 serial_println!("arch: x86_64, freestanding, no userspace");
134 // String is freed (dropped) when this scope ends — tests dealloc too
135}
136
137fn cmd_mem() {
138 let (map, key) = crate::memory::get_map();
139
140 let mut total_kb = 0u64;
141 let mut conventional_kb = 0u64;
142 let mut loader_kb = 0u64;
143 let mut boot_code_kb = 0u64;
144 let mut boot_data_kb = 0u64;
145 let mut reserved_kb = 0u64;
146 let mut acpi_kb = 0u64;
147 let mut mmio_kb = 0u64;
148
149 for desc in map {
150 let pages = desc.number_of_pages;
151 let bytes_4k = pages * 4;
152 total_kb += bytes_4k;
153 match desc.kind {
154 1 => reserved_kb += bytes_4k,
155 3 => boot_code_kb += bytes_4k,
156 4 => boot_data_kb += bytes_4k,
157 5 | 10 => acpi_kb += bytes_4k,
158 6 => loader_kb += bytes_4k,
159 7 => conventional_kb += bytes_4k,
160 11 | 12 => mmio_kb += bytes_4k,
161 _ => reserved_kb += bytes_4k,
162 }
163 }
164
165 // Build a big heap string with format! to test the allocator
166 let mut out = alloc::string::String::new();
167 use alloc::fmt::Write;
168 let _ = writeln!(out, "memory map (key=0x{:x}, {} entries)", key, map.len());
169 let _ = writeln!(
170 out,
171 " usable {:4} MB Conventional",
172 conventional_kb / 1024
173 );
174 let _ = writeln!(
175 out,
176 " boot code {:4} MB BootServicesCode",
177 boot_code_kb / 1024
178 );
179 let _ = writeln!(
180 out,
181 " boot data {:4} MB BootServicesData",
182 boot_data_kb / 1024
183 );
184 let _ = writeln!(out, " loader {:4} MB LoaderData", loader_kb / 1024);
185 let _ = writeln!(out, " ACPI {:4} MB", acpi_kb / 1024);
186 let _ = writeln!(out, " MMIO {:4} MB", mmio_kb / 1024);
187 let _ = writeln!(out, " reserved {:4} MB", reserved_kb / 1024);
188 let _ = writeln!(
189 out,
190 " raw total {:4} MB (incl. PCIe MMIO)",
191 total_kb / 1024
192 );
193
194 gop_print!("{}", out);
195 serial_print!("{}", out);
196}
197
198fn cmd_halt() -> ! {
199 gop_println!("halting...");
200 serial_println!("halting...");
201 loop {
202 unsafe {
203 core::arch::asm!("cli; hlt");
204 }
205 }
206}
207
208// ---------------------------------------------------------------------------
209// Minimal fixed-capacity string for no-stack use (avoids alloc dependency)
210// ---------------------------------------------------------------------------
211mod heapless_str {
212 use core::fmt;
213
214 /// A fixed-capacity string allocated entirely on the stack.
215 pub struct String<const N: usize> {
216 buf: [u8; N],
217 len: usize,
218 }
219
220 impl<const N: usize> String<N> {
221 pub const fn new() -> Self {
222 String {
223 buf: [0u8; N],
224 len: 0,
225 }
226 }
227
228 pub fn as_str(&self) -> &str {
229 // SAFETY: we only write valid ASCII bytes.
230 unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
231 }
232
233 pub fn push(&mut self, c: char) -> Result<(), ()> {
234 if self.len >= N {
235 return Err(());
236 }
237 self.buf[self.len] = c as u8;
238 self.len += 1;
239 Ok(())
240 }
241
242 pub fn pop(&mut self) -> Option<char> {
243 if self.len == 0 {
244 return None;
245 }
246 self.len -= 1;
247 Some(self.buf[self.len] as char)
248 }
249
250 pub fn is_empty(&self) -> bool {
251 self.len == 0
252 }
253
254 pub fn clear(&mut self) {
255 self.len = 0;
256 }
257
258 pub fn trim(&self) -> &str {
259 self.as_str().trim()
260 }
261 }
262
263 impl<const N: usize> fmt::Display for String<N> {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 write!(f, "{}", self.as_str())
266 }
267 }
268
269 impl<const N: usize> fmt::Debug for String<N> {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 write!(f, "{:?}", self.as_str())
272 }
273 }
274}
diff --git a/src/uefi.rs b/src/uefi.rs
new file mode 100644
index 0000000..2e3ed6b
--- /dev/null
+++ b/src/uefi.rs
@@ -0,0 +1,289 @@
1use core::ffi::c_void;
2
3/// Opaque EFI handle
4pub type Handle = *mut c_void;
5
6/// EFI status (0 = success, bit 63 = error code)
7pub type Status = u64;
8
9/// Helper: UEFI error codes always have bit 63 set
10const fn efi_err(n: u64) -> u64 {
11 n | (1u64 << 63)
12}
13
14pub const EFI_SUCCESS: Status = 0;
15pub const EFI_LOAD_ERROR: Status = efi_err(1);
16pub const EFI_INVALID_PARAMETER: Status = efi_err(2);
17#[allow(dead_code)]
18pub const EFI_UNSUPPORTED: Status = efi_err(3);
19pub const EFI_BUFFER_TOO_SMALL: Status = efi_err(5);
20#[allow(dead_code)]
21pub const EFI_NOT_FOUND: Status = efi_err(14);
22pub const EFI_OUT_OF_RESOURCES: Status = efi_err(9);
23
24// ---------------------------------------------------------------------------
25// GUID
26// ---------------------------------------------------------------------------
27#[repr(C)]
28#[derive(Debug, Clone, Copy)]
29pub struct Guid {
30 pub a: u32,
31 pub b: u16,
32 pub c: u16,
33 pub d: [u8; 8],
34}
35
36pub const EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID: Guid = Guid {
37 a: 0x9042a9de,
38 b: 0x23dc,
39 c: 0x4a38,
40 d: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a],
41};
42
43// ---------------------------------------------------------------------------
44// Table header
45// ---------------------------------------------------------------------------
46#[repr(C)]
47pub struct TableHeader {
48 pub signature: u64,
49 pub revision: u32,
50 pub header_size: u32,
51 pub crc32: u32,
52 pub reserved: u32,
53}
54
55// ---------------------------------------------------------------------------
56// Simple text output protocol (console output)
57// ---------------------------------------------------------------------------
58#[repr(C)]
59pub struct SimpleTextOutput {
60 _reset: *mut c_void,
61 pub output_string: unsafe extern "efiapi" fn(*mut SimpleTextOutput, *const u16) -> Status,
62 _test_string: *mut c_void,
63 _query_mode: *mut c_void,
64 _set_mode: *mut c_void,
65 _set_attribute: *mut c_void,
66 _clear_screen: *mut c_void,
67 _set_cursor_position: *mut c_void,
68 _enable_cursor: *mut c_void,
69 _mode: *mut c_void,
70}
71
72// ---------------------------------------------------------------------------
73// Simple text input protocol (keyboard input)
74// ---------------------------------------------------------------------------
75#[repr(C)]
76pub struct InputKey {
77 pub scan_code: u16,
78 pub unicode_char: u16,
79}
80
81#[repr(C)]
82pub struct SimpleTextInput {
83 _reset: *mut c_void,
84 pub read_key_stroke: unsafe extern "efiapi" fn(*mut SimpleTextInput, *mut InputKey) -> Status,
85 _wait_for_key: Handle,
86}
87
88// ---------------------------------------------------------------------------
89// Runtime services (stub — we don't use them)
90// ---------------------------------------------------------------------------
91#[repr(C)]
92pub struct RuntimeServices {
93 _hdr: TableHeader,
94 _reserved: [*mut c_void; 30],
95}
96
97// ---------------------------------------------------------------------------
98// Configuration table entry
99// ---------------------------------------------------------------------------
100#[repr(C)]
101pub struct ConfigurationTable {
102 pub vendor_guid: Guid,
103 pub vendor_table: *mut c_void,
104}
105
106// ---------------------------------------------------------------------------
107/// UEFI memory descriptor (from GetMemoryMap)
108// ---------------------------------------------------------------------------
109#[repr(C)]
110pub struct MemoryDescriptor {
111 pub kind: u32,
112 pub pad: u32,
113 pub physical_start: u64,
114 pub virtual_start: u64,
115 pub number_of_pages: u64,
116 pub attribute: u64,
117}
118
119pub const MEMORY_TYPE_CONVENTIONAL: u32 = 7;
120#[allow(dead_code)]
121pub const MEMORY_TYPE_LOADER_DATA: u32 = 6;
122#[allow(dead_code)]
123pub const MEMORY_TYPE_BOOT_SERVICES_CODE: u32 = 3;
124#[allow(dead_code)]
125pub const MEMORY_TYPE_BOOT_SERVICES_DATA: u32 = 4;
126
127// ---------------------------------------------------------------------------
128// Boot services table
129// ---------------------------------------------------------------------------
130pub type EfiGetMemoryMap = unsafe extern "efiapi" fn(
131 *mut usize,
132 *mut MemoryDescriptor,
133 *mut usize,
134 *mut usize,
135 *mut u32,
136) -> Status;
137
138pub type EfiExitBootServices = unsafe extern "efiapi" fn(Handle, usize) -> Status;
139
140pub type EfiHandleProtocol =
141 unsafe extern "efiapi" fn(Handle, *const Guid, *mut *mut c_void) -> Status;
142
143pub type EfiLocateProtocol =
144 unsafe extern "efiapi" fn(*const Guid, *mut c_void, *mut *mut c_void) -> Status;
145
146pub type EfiAllocatePool = unsafe extern "efiapi" fn(u8, usize, *mut *mut c_void) -> Status;
147
148pub type EfiFreePool = unsafe extern "efiapi" fn(*mut c_void) -> Status;
149
150pub type EfiStall = unsafe extern "efiapi" fn(usize) -> Status;
151
152#[repr(C)]
153pub struct BootServices {
154 pub hdr: TableHeader,
155
156 // 0-1: task priority
157 pub raise_tpl: *mut c_void,
158 pub restore_tpl: *mut c_void,
159
160 // 2-3: memory allocation
161 pub allocate_pages: *mut c_void,
162 pub free_pages: *mut c_void,
163
164 // 4: GetMemoryMap
165 pub get_memory_map: EfiGetMemoryMap,
166
167 // 5-6: pool
168 pub allocate_pool: EfiAllocatePool,
169 pub free_pool: EfiFreePool,
170
171 // 7-12: events
172 pub create_event: *mut c_void,
173 pub set_timer: *mut c_void,
174 pub wait_for_event: *mut c_void,
175 pub signal_event: *mut c_void,
176 pub close_event: *mut c_void,
177 pub check_event: *mut c_void,
178
179 // 13-14: protocol install
180 pub install_protocol_interface: *mut c_void,
181 pub reinstall_protocol_interface: *mut c_void,
182 pub uninstall_protocol_interface: *mut c_void,
183
184 // 16: HandleProtocol
185 pub handle_protocol: EfiHandleProtocol,
186
187 pub _reserved: *mut c_void,
188
189 // 18: register protocol notify
190 pub register_protocol_notify: *mut c_void,
191
192 // 19-21: locate
193 pub locate_handle: *mut c_void,
194 pub locate_device_path: *mut c_void,
195 pub install_configuration_table: *mut c_void,
196
197 // 22-23: image loading
198 pub load_image: *mut c_void,
199 pub start_image: *mut c_void,
200
201 // 24-25
202 pub exit: *mut c_void,
203 pub unload_image: *mut c_void,
204
205 // 26: ExitBootServices
206 pub exit_boot_services: EfiExitBootServices,
207
208 // 27-29
209 pub get_next_monotonic_count: *mut c_void,
210 pub stall: EfiStall,
211 pub set_watchdog_timer: *mut c_void,
212
213 // 30-36
214 pub connect_controller: *mut c_void,
215 pub disconnect_controller: *mut c_void,
216 pub open_protocol: *mut c_void,
217 pub close_protocol: *mut c_void,
218 pub open_protocol_information: *mut c_void,
219 pub protocols_per_handle: *mut c_void,
220 pub locate_handle_buffer: *mut c_void,
221
222 // 37: LocateProtocol
223 pub locate_protocol: EfiLocateProtocol,
224
225 // 38-43
226 pub install_multiple_protocol_interfaces: *mut c_void,
227 pub uninstall_multiple_protocol_interfaces: *mut c_void,
228 pub calculate_crc32: *mut c_void,
229 pub copy_mem: *mut c_void,
230 pub set_mem: *mut c_void,
231 pub create_event_ex: *mut c_void,
232}
233
234// ---------------------------------------------------------------------------
235// Graphics Output Protocol (GOP)
236// ---------------------------------------------------------------------------
237#[repr(C)]
238pub struct GopModeInfo {
239 pub version: u32,
240 pub horizontal_resolution: u32,
241 pub vertical_resolution: u32,
242 pub pixel_format: u32, // PixelFormat enum
243 pub red_mask: u32,
244 pub green_mask: u32,
245 pub blue_mask: u32,
246 pub reserved_mask: u32,
247 pub pixels_per_scan_line: u32,
248}
249
250pub const PIXEL_RGB: u32 = 0;
251pub const PIXEL_BGR: u32 = 1;
252
253#[repr(C)]
254pub struct GopMode {
255 pub max_mode: u32,
256 pub mode: u32,
257 pub info: *mut GopModeInfo,
258 pub info_size: usize,
259 pub framebuffer_base: *mut c_void,
260 pub framebuffer_size: usize,
261}
262
263#[repr(C)]
264pub struct GraphicsOutputProtocol {
265 pub query_mode: *mut c_void,
266 pub set_mode: *mut c_void,
267 pub get_current_mode: *mut c_void,
268 pub mode: *mut GopMode,
269}
270
271// ---------------------------------------------------------------------------
272// System table
273// ---------------------------------------------------------------------------
274#[repr(C)]
275pub struct SystemTable {
276 pub hdr: TableHeader,
277 pub firmware_vendor: *const u16,
278 pub firmware_revision: u32,
279 pub console_in_handle: Handle,
280 pub con_in: *mut SimpleTextInput,
281 pub console_out_handle: Handle,
282 pub con_out: *mut SimpleTextOutput,
283 pub stderr_handle: Handle,
284 pub std_err: *mut SimpleTextOutput,
285 pub runtime_services: *mut RuntimeServices,
286 pub boot_services: *mut BootServices,
287 pub number_of_table_entries: usize,
288 pub configuration_table: *const ConfigurationTable,
289}
diff --git a/tools/boot-image.sh b/tools/boot-image.sh
new file mode 100755
index 0000000..2592afa
--- /dev/null
+++ b/tools/boot-image.sh
@@ -0,0 +1,62 @@
1#!/usr/bin/env bash
2# =============================================================================
3# tools/boot-image.sh — Create a bootable disk image from the ESP directory
4#
5# Produces a raw disk image (bootable via UEFI) for dd-ing to a USB stick
6# or attaching to QEMU as a hard disk.
7#
8# Usage:
9# ./tools/boot-image.sh [output.img]
10#
11# Default output: raisernics.img
12# =============================================================================
13set -euo pipefail
14
15SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
16cd "$SCRIPT_DIR"
17
18OUTPUT="${1:-raisernics.img}"
19ESP_DIR="${ESP_DIR:-esp}"
20MBR_SZ=1 # 1 MB reserved at start for GPT/pmbr
21SECTOR_SZ=512
22
23# Rebuild ESP if stale
24make all
25
26if [ ! -d "$ESP_DIR" ]; then
27 echo "ERROR: ESP directory '$ESP_DIR' not found. Run 'make all' first."
28 exit 1
29fi
30
31# Calculate required size: ESP contents + MBR gap + some slack
32ESP_SIZE_MB=$(du -sm "$ESP_DIR" 2>/dev/null | cut -f1 || echo 1)
33TOTAL_MB=$((ESP_SIZE_MB + MBR_SZ + 1)) # +1 MB slack
34
35echo "Creating ${TOTAL_MB} MB disk image: ${OUTPUT}"
36
37# Create empty image with MBR gap
38dd if=/dev/zero of="$OUTPUT" bs=1M count="$TOTAL_MB" status=none 2>/dev/null
39
40# Partition table: single GPT partition, type EFI System (C12A7328-F81F-11D2-BA4B-00A0C93EC93B)
41sgdisk --clear \
42 --new=1:${MBR_SZ}M:0 \
43 --typecode=1:C12A7328-F81F-11D2-BA4B-00A0C93EC93B \
44 --change-name=1:"raisernics" \
45 "$OUTPUT" >/dev/null
46
47# Find partition offset
48PART_START=$(sgdisk --info=1 "$OUTPUT" 2>/dev/null | grep "Partition start sector" | awk '{print $NF}')
49if [ -z "$PART_START" ]; then
50 PART_START=$((MBR_SZ * 1024 * 1024 / SECTOR_SZ))
51fi
52
53# Format as FAT32
54mformat -i "$OUTPUT"@$((PART_START * SECTOR_SZ)) -h 32 -s 32 -t "$((TOTAL_MB * 2))" >/dev/null 2>&1 || true
55
56# Copy the ESP contents onto the partition
57mcopy -i "$OUTPUT"@$((PART_START * SECTOR_SZ)) -s "$ESP_DIR"/* ::/ 2>/dev/null
58
59echo " -> ${OUTPUT} ($(ls -lh "$OUTPUT" | awk '{print $5}'))"
60echo ""
61echo "To write to USB: sudo dd if=${OUTPUT} of=/dev/sdX bs=4M status=progress"
62echo "To test with QEMU: qemu-system-x86_64 -bios <OVMF> -drive file=${OUTPUT},format=raw"
diff --git a/tools/debug-kernel.py b/tools/debug-kernel.py
new file mode 100755
index 0000000..c794ad6
--- /dev/null
+++ b/tools/debug-kernel.py
@@ -0,0 +1,64 @@
1"""
2GDB Python script for debugging raisernics under QEMU.
3
4Usage:
5 1. Terminal 1: ./tools/qemu.sh --debug
6 2. Terminal 2: gdb -x tools/debug-kernel.py target/x86_64-unknown-uefi/debug/raisernics.efi
7"""
8
9import gdb
10import os
11
12# Find the kernel binary
13script_dir = os.path.dirname(os.path.abspath(__file__))
14project_root = os.path.dirname(script_dir)
15binary = os.path.join(
16 project_root,
17 "target/x86_64-unknown-uefi/debug/raisernics.efi"
18)
19
20# Connect to QEMU's GDB stub
21gdb.execute("target remote :1234")
22
23# Load the binary (add symbol table)
24if os.path.exists(binary):
25 gdb.execute(f"symbol-file {binary}")
26 print(f"Loaded symbols from {binary}")
27else:
28 print(f"WARNING: {binary} not found. Build with debug symbols first.")
29
30gdb.execute("set breakpoint pending on")
31
32
33# Helper commands
34class DumpBootServices(gdb.Command):
35 """Dump EFI boot services signature and key pointers"""
36
37 def __init__(self):
38 super().__init__("dump-bs", gdb.COMMAND_USER)
39
40 def invoke(self, argument, from_tty):
41 del argument, from_tty
42 gdb.execute("printf \"SystemTable, BootServices, and key function pointers\\n\"")
43 gdb.execute("info functions efi_main")
44
45
46class DumpGop(gdb.Command):
47 """Dump GOP framebuffer info"""
48
49 def __init__(self):
50 super().__init__("dump-gop", gdb.COMMAND_USER)
51
52 def invoke(self, argument, from_tty):
53 del argument, from_tty
54 gdb.execute("printf \"Check the gop::Framebuffer static\\n\"")
55
56
57DumpBootServices()
58DumpGop()
59
60print("")
61print("raisernics GDB session ready.")
62print(" target remote :1234 (connected)")
63print(" Commands: continue, stepi, bt, info registers, etc.")
64print(" Custom: dump-bs, dump-gop")
diff --git a/tools/qemu.sh b/tools/qemu.sh
new file mode 100755
index 0000000..428954f
--- /dev/null
+++ b/tools/qemu.sh
@@ -0,0 +1,96 @@
1#!/usr/bin/env bash
2# =============================================================================
3# tools/qemu.sh — Run raisernics in QEMU with various options
4#
5# Usage:
6# ./tools/qemu.sh # serial console (no graphics)
7# ./tools/qemu.sh --gui # graphical window
8# ./tools/qemu.sh --debug # GDB stub on :1234
9# ./tools/qemu.sh --image foo.img # boot from raw disk image
10# ./tools/qemu.sh --bios /path/to.fd # custom OVMF
11# ./tools/qemu.sh --mem 512M # memory size
12# ./tools/qemu.sh --smp 2 # SMP cores
13# =============================================================================
14set -euo pipefail
15
16SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
17cd "$SCRIPT_DIR"
18
19# Defaults
20GUI_FLAG=""
21DEBUG_FLAG=""
22DRIVE_ARGS=(-drive file=fat:rw:esp,format=raw)
23OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
24MEM="${MEM:-256M}"
25SMP="${SMP:-1}"
26
27# Pre-build
28make all
29
30# Parse args
31while [ $# -gt 0 ]; do
32 case "$1" in
33 --gui | -g)
34 GUI_FLAG="-vga std"
35 shift
36 ;;
37 --debug | -d)
38 DEBUG_FLAG="-s -S"
39 shift
40 ;;
41 --image | -i)
42 _img_file="$(printf -- '-drive file=%s,format=raw,if=none,id=hd' "$2")"
43 DRIVE_ARGS=("$_img_file" '-device' 'ide-hd,drive=hd')
44 shift 2
45 ;;
46 --bios | -b)
47 OVMF="$2"
48 shift 2
49 ;;
50 --mem)
51 MEM="$2"
52 shift 2
53 ;;
54 --smp)
55 SMP="$2"
56 shift 2
57 ;;
58 --help | -h)
59 head -20 "$0" | grep "^# " | sed 's/^# //'
60 exit 0
61 ;;
62 *)
63 echo "Unknown option: $1"
64 exit 1
65 ;;
66 esac
67done
68
69QEMU_CMD=(
70 qemu-system-x86_64
71 -bios "$OVMF"
72 "${DRIVE_ARGS[@]}"
73 -serial stdio
74 -m "$MEM"
75 -machine q35
76 -cpu qemu64
77 -no-reboot
78 -smp "$SMP"
79)
80
81[ -n "$GUI_FLAG" ] && QEMU_CMD+=("$GUI_FLAG")
82if [ -n "$DEBUG_FLAG" ]; then
83 # Split the flag string safely into words
84 read -ra dbg <<< "$DEBUG_FLAG"
85 QEMU_CMD+=("${dbg[@]}")
86fi
87
88echo " OVMF : $OVMF"
89echo " Drive : ${DRIVE_ARGS[*]}"
90echo " Mem : $MEM"
91echo " SMP : $SMP"
92echo " GDB : $([ -n "$DEBUG_FLAG" ] && echo ':1234' || echo 'off')"
93echo " GUI : $([ -n "$GUI_FLAG" ] && echo 'yes' || echo 'no')"
94echo ""
95
96exec "${QEMU_CMD[@]}"