From 76103215711d268b4b02f62e900f2be3032cf295 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 02:18:30 +0200 Subject: Initial commit: raisernics toy kernel - Bare-metal x86_64 UEFI kernel (PE32+, no libc) - GOP framebuffer + 8x16 bitmap font - PS/2 keyboard (polling, scancode set 1) - UART 16550 serial driver (COM1, 115200 8N1) - Bump allocator as #[global_allocator] (alloc::String, Vec, format!) - UEFI memory map capture (static buffer, ADR 0001) - Shell with 7 commands (help, echo, clear, hello, info, mem, halt) - Dual output: GOP framebuffer + serial console - Makefile: run, run-gui, debug, release, image targets - tools/: qemu.sh, boot-image.sh, debug-kernel.py --- src/shell.rs | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 src/shell.rs (limited to 'src/shell.rs') 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 @@ +/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. +use crate::ps2::{self, KeyEvent}; +use crate::{gop_print, gop_println, serial_print, serial_println}; + +/// Maximum input line length. +const LINE_BUF_SIZE: usize = 256; + +/// Run the shell forever. +pub fn run() -> ! { + gop_println!("raisernics v0.1 — type 'help' for commands"); + serial_println!("========== raisernics v0.1 =========="); + + loop { + // Print prompt + gop_print!("> "); + serial_print!("> "); + + // Read one line from keyboard + let line = read_line(); + + // Parse and execute + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + let (cmd, args) = split_first_word(trimmed); + + match cmd { + "help" => cmd_help(), + "echo" => cmd_echo(args), + "clear" => cmd_clear(), + "halt" | "poweroff" | "exit" => cmd_halt(), + "hello" => { + gop_println!("World!"); + serial_println!("World!"); + } + "info" => cmd_info(), + "mem" => cmd_mem(), + _ => { + gop_println!("unknown command: {}", cmd); + serial_println!("unknown command: {}", cmd); + } + } + } +} + +// --------------------------------------------------------------------------- +// Line input (PS/2 keyboard, no heap) +// --------------------------------------------------------------------------- +fn read_line() -> heapless_str::String { + let mut buf = heapless_str::String::::new(); + loop { + match ps2::poll_key() { + KeyEvent::Enter => { + crate::gop::put_char('\n'); + crate::serial::write_str("\r\n"); + return buf; + } + KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { + if buf.push(c).is_ok() { + crate::gop::put_char(c); + // Echo to serial too + let mut s = [0u8; 4]; + let s = c.encode_utf8(&mut s); + crate::serial::write_str(s); + } + } + KeyEvent::Backspace => { + if buf.pop().is_some() { + // GOP backspace: move cursor back and erase + crate::gop::put_char('\x08'); + // Serial backspace: send BS SP BS (standard terminal protocol) + crate::serial::write_str("\x08 \x08"); + } + } + KeyEvent::Tab => { + for _ in 0..4 { + if buf.push(' ').is_ok() { + crate::gop::put_char(' '); + crate::serial::write_str(" "); + } + } + } + _ => {} + } + } +} + +/// Split a command line into the first word and the rest. +fn split_first_word(s: &str) -> (&str, &str) { + let s = s.trim_start(); + if let Some(i) = s.find(char::is_whitespace) { + (&s[..i], s[i..].trim_start()) + } else { + (s, "") + } +} + +// --------------------------------------------------------------------------- +// Built-in commands +// --------------------------------------------------------------------------- + +fn cmd_help() { + gop_println!("available commands:"); + gop_println!(" help — show this message"); + gop_println!(" echo — echo arguments"); + gop_println!(" clear — clear screen"); + gop_println!(" hello — print 'World!'"); + gop_println!(" info — show system info"); + gop_println!(" mem — show memory info"); + gop_println!(" halt — halt the system"); + gop_println!(" exit — alias for halt"); + serial_println!("available commands:"); + serial_println!(" help, echo, clear, hello, info, mem, halt, exit"); +} + +fn cmd_echo(args: &str) { + gop_println!("{}", args); +} + +fn cmd_clear() { + crate::gop::clear_screen(); +} + +fn cmd_info() { + // Test that the global allocator works: build a String on the heap + let mut s = alloc::string::String::from("raisernics v0.1"); + s.push_str(" — RNG custom kernel"); + gop_println!("{}", s); + gop_println!("arch: x86_64, freestanding, no userspace"); + serial_println!("{}", s); + serial_println!("arch: x86_64, freestanding, no userspace"); + // String is freed (dropped) when this scope ends — tests dealloc too +} + +fn cmd_mem() { + let (map, key) = crate::memory::get_map(); + + let mut total_kb = 0u64; + let mut conventional_kb = 0u64; + let mut loader_kb = 0u64; + let mut boot_code_kb = 0u64; + let mut boot_data_kb = 0u64; + let mut reserved_kb = 0u64; + let mut acpi_kb = 0u64; + let mut mmio_kb = 0u64; + + for desc in map { + let pages = desc.number_of_pages; + let bytes_4k = pages * 4; + total_kb += bytes_4k; + match desc.kind { + 1 => reserved_kb += bytes_4k, + 3 => boot_code_kb += bytes_4k, + 4 => boot_data_kb += bytes_4k, + 5 | 10 => acpi_kb += bytes_4k, + 6 => loader_kb += bytes_4k, + 7 => conventional_kb += bytes_4k, + 11 | 12 => mmio_kb += bytes_4k, + _ => reserved_kb += bytes_4k, + } + } + + // Build a big heap string with format! to test the allocator + let mut out = alloc::string::String::new(); + use alloc::fmt::Write; + let _ = writeln!(out, "memory map (key=0x{:x}, {} entries)", key, map.len()); + let _ = writeln!( + out, + " usable {:4} MB Conventional", + conventional_kb / 1024 + ); + let _ = writeln!( + out, + " boot code {:4} MB BootServicesCode", + boot_code_kb / 1024 + ); + let _ = writeln!( + out, + " boot data {:4} MB BootServicesData", + boot_data_kb / 1024 + ); + let _ = writeln!(out, " loader {:4} MB LoaderData", loader_kb / 1024); + let _ = writeln!(out, " ACPI {:4} MB", acpi_kb / 1024); + let _ = writeln!(out, " MMIO {:4} MB", mmio_kb / 1024); + let _ = writeln!(out, " reserved {:4} MB", reserved_kb / 1024); + let _ = writeln!( + out, + " raw total {:4} MB (incl. PCIe MMIO)", + total_kb / 1024 + ); + + gop_print!("{}", out); + serial_print!("{}", out); +} + +fn cmd_halt() -> ! { + gop_println!("halting..."); + serial_println!("halting..."); + loop { + unsafe { + core::arch::asm!("cli; hlt"); + } + } +} + +// --------------------------------------------------------------------------- +// Minimal fixed-capacity string for no-stack use (avoids alloc dependency) +// --------------------------------------------------------------------------- +mod heapless_str { + use core::fmt; + + /// A fixed-capacity string allocated entirely on the stack. + pub struct String { + buf: [u8; N], + len: usize, + } + + impl String { + pub const fn new() -> Self { + String { + buf: [0u8; N], + len: 0, + } + } + + pub fn as_str(&self) -> &str { + // SAFETY: we only write valid ASCII bytes. + unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) } + } + + pub fn push(&mut self, c: char) -> Result<(), ()> { + if self.len >= N { + return Err(()); + } + self.buf[self.len] = c as u8; + self.len += 1; + Ok(()) + } + + pub fn pop(&mut self) -> Option { + if self.len == 0 { + return None; + } + self.len -= 1; + Some(self.buf[self.len] as char) + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn clear(&mut self) { + self.len = 0; + } + + pub fn trim(&self) -> &str { + self.as_str().trim() + } + } + + impl fmt::Display for String { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } + } + + impl fmt::Debug for String { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.as_str()) + } + } +} -- cgit v1.2.3