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 --- .gitignore | 4 + CONTEXT.md | 100 ++++++++ Cargo.lock | 7 + Cargo.toml | 10 + Makefile | 124 ++++++++++ build.rs | 70 ++++++ docs/adr/0001-static-buffer-for-memory-map.md | 54 +++++ rust-toolchain.toml | 3 + src/font.rs | 5 + src/gop.rs | 232 ++++++++++++++++++ src/main.rs | 181 ++++++++++++++ src/memory.rs | 196 +++++++++++++++ src/ps2.rs | 329 ++++++++++++++++++++++++++ src/serial.rs | 118 +++++++++ src/shell.rs | 274 +++++++++++++++++++++ src/uefi.rs | 289 ++++++++++++++++++++++ tools/boot-image.sh | 62 +++++ tools/debug-kernel.py | 64 +++++ tools/qemu.sh | 96 ++++++++ 19 files changed, 2218 insertions(+) create mode 100644 .gitignore create mode 100644 CONTEXT.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 build.rs create mode 100644 docs/adr/0001-static-buffer-for-memory-map.md create mode 100644 rust-toolchain.toml create mode 100644 src/font.rs create mode 100644 src/gop.rs create mode 100644 src/main.rs create mode 100644 src/memory.rs create mode 100644 src/ps2.rs create mode 100644 src/serial.rs create mode 100644 src/shell.rs create mode 100644 src/uefi.rs create mode 100755 tools/boot-image.sh create mode 100755 tools/debug-kernel.py create mode 100755 tools/qemu.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2353fdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/esp/ +*.img +NvVars diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..283467a --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,100 @@ +# CONTEXT.md — raisernics Kernel Glossary + +## raisernics + +Custom x86_64 kernel for the RNG ecosystem. Toy project — self-contained, +no strict userspace separation, TempleOS-inspired design philosophy. +Built in Rust, compiled for the `x86_64-unknown-uefi` target (nightly). + +## Boot interface + +The kernel is a **PE32+ EFI application** loaded via UEFI `LoadImage`/`StartImage`. +The existing bootloader (rsbl) loads us from `\bzImage` and passes the UEFI +system table. No bootloader changes needed. + +## Output strategy + +Two-phase console model: + +- **Phase 1 (boot services alive):** UEFI `con_out->OutputString()` — works + with whatever UEFI firmware routes (serial + framebuffer). +- **Phase 2 (native):** Raw serial UART 16550 driver on COM1 (I/O port + `0x3F8`). Activated after `ExitBootServices` when we own the hardware. + +## Dependency philosophy + +Aligns with RNG philosophy: minimal dependencies. Freestanding Rust, +no libc, no external crates at the kernel/core level. UEFI protocol +structs are hand-defined (not from `uefi-rs` crate) to stay lightweight. + +## Paging strategy + +Long mode mandates paging (`CR0.PG = 1`), so we accept that tax but keep it +simple. Retain UEFI's identity-mapped 4-level page tables. No higher-half, +no demand paging, no guard pages. When the kernel needs more physical pages, +entries are added 1:1 to the existing page tables. + +## Physical memory + +Before `ExitBootServices`, the kernel reads the UEFI memory map to know +which physical regions are free. Post-`ExitBootServices`, a simple physical +page allocator (bitmap or free-stack) manages free pages. A bump allocator +serves as the initial kernel heap. + +## Input strategy + +**PS/2 keyboard polling.** Port I/O to `0x60`/`0x64`. Scancode set 1 +(US QWERTY layout in the initial build). No interrupts — polling loop +inside the shell. + +## Display strategy + +Two-phase model matching output: + +- **Phase 1:** UEFI `con_out->OutputString()` (framebuffer + serial handled + by firmware). +- **Phase 2:** GOP (Graphics Output Protocol) framebuffer obtained before + `ExitBootServices`, driven by a built-in 8×16 bitmap font. Text rendered + by drawing glyphs directly into the framebuffer. + +The framebuffer address stays valid after `ExitBootServices` — the GPU's +linear frame buffer is system memory that we own once boot services are gone. + +## Keyboard layout + +Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table. +Other layouts can be added later by swapping the translation table. + +## Development loop + +Standalone project — no integration with the RNG initramfs build system. +Testing via QEMU with OVMF. The kernel PE binary is placed on a FAT +image as `\bzImage` (matching rsbl's expected path). + +## Project layout + +Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: + +- `main.rs` — entry point, panic handler, top-level init +- `uefi.rs` — UEFI protocol type definitions +- `serial.rs` — UART 16550 driver +- `paging.rs` — identity page table helpers +- `memory.rs` — physical page allocator + heap +- `gop.rs` — framebuffer access and text rendering +- `font.rs` — 8×16 bitmap font data +- `ps2.rs` — PS/2 keyboard driver (polling) +- `shell.rs` — command loop and built-in commands + +## Milestone status (v0.1) + +✅ **Booting** — kernel loads via rsbl, prints banner via UEFI console +✅ **ExitBootServices** — memory map captured via static `.bss` buffer + (see `docs/adr/0001-static-buffer-for-memory-map.md`) +✅ **Serial** — UART 16550 at COM1, 115200 8N1 +✅ **Framebuffer** — GOP located, 8×16 bitmap font renders text +✅ **Keyboard** — PS/2 polling, scancode set 1, US QWERTY +✅ **Shell** — commands: `help`, `echo`, `clear`, `hello`, `info`, `mem`, `halt` + +## Target audience + +Learning 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 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "raisernics" +version = "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 @@ +[package] +name = "raisernics" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +codegen-units = 1 +strip = "symbols" +opt-level = "z" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9f03f93 --- /dev/null +++ b/Makefile @@ -0,0 +1,124 @@ +# shellcheck shell=sh disable=SC2046,SC2091,SC2086,SC2155 +# NOTE: $(VAR) in this file is MAKE syntax, not shell substitution. +export TARGET=x86_64-unknown-uefi +export PROFILE=debug +export OUT_DIR=target/$(TARGET)/$(PROFILE) +export ESP_DIR=esp +export KERNEL=$(OUT_DIR)/raisernics.efi +export BZIMAGE=$(ESP_DIR)/bzImage +export INITRD=$(ESP_DIR)/initramfs.cpio.gz +export BOOTLOADER=../rng/rsbl/../build/bootloader/BOOTX64.EFI +export OVMF=/usr/share/edk2/x64/OVMF.4m.fd +export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI + +.PHONY: all run run-gui clean image debug release fmt + +# ---- Build --------------------------------------------------------------- + +all: $(BZIMAGE) $(INITRD) $(BOOT_EFI) + +release: PROFILE=release +release: export PROFILE=release +release: export OUT_DIR=target/$(TARGET)/release +release: CARGO_FLAGS=--release +release: all + +# shellcheck disable=SC2034 +CARGO_FLAGS= + +$(OUT_DIR)/raisernics.efi: $(shell find src -name '*.rs') Cargo.toml build.rs + cargo build --target $(TARGET) $(CARGO_FLAGS) + +$(BZIMAGE): $(KERNEL) | $(ESP_DIR) + cp "$<" "$@" + +$(BOOT_EFI): $(BOOTLOADER) | $(ESP_DIR) + mkdir -p $(ESP_DIR)/EFI/BOOT + cp "$<" "$@" + +$(ESP_DIR): + mkdir -p $(ESP_DIR) + +$(INITRD): | $(ESP_DIR) + printf '' | gzip > "$@" + +# ---- QEMU runners -------------------------------------------------------- + +run: all + qemu-system-x86_64 \ + -bios $(OVMF) \ + -drive file=fat:rw:$(ESP_DIR),format=raw \ + -serial stdio \ + -m 256M \ + -machine q35 \ + -cpu qemu64 \ + -no-reboot \ + -display none + +run-gui: all + qemu-system-x86_64 \ + -bios $(OVMF) \ + -drive file=fat:rw:$(ESP_DIR),format=raw \ + -serial stdio \ + -m 256M \ + -machine q35 \ + -cpu qemu64 \ + -no-reboot \ + -vga std + +# GDB stub on :1234 +debug: all + qemu-system-x86_64 \ + -bios $(OVMF) \ + -drive file=fat:rw:$(ESP_DIR),format=raw \ + -serial stdio \ + -m 256M \ + -machine q35 \ + -cpu qemu64 \ + -no-reboot \ + -display none \ + -s -S + +# Boot from a raw disk image (created by tools/boot-image.sh) +run-image: all + qemu-system-x86_64 \ + -bios $(OVMF) \ + -drive file=$(IMG),format=raw,if=none,id=hd \ + -device ide-hd,drive=hd \ + -serial stdio \ + -m 256M \ + -machine q35 \ + -cpu qemu64 \ + -no-reboot \ + -display none + +# ---- Disk image ---------------------------------------------------------- + +image: all + ./tools/boot-image.sh raisernics.img + +# ---- Utilities ----------------------------------------------------------- + +fmt: + cargo fmt + +clean: + cargo clean + rm -rf $(ESP_DIR) raisernics.img + +help: + @echo "raisernics — Make targets" + @echo "" + @echo " make build kernel + ESP (default)" + @echo " make release build with --release (smaller binary)" + @echo " make image create bootable USB image (raisernics.img)" + @echo " make run QEMU serial console (no graphics)" + @echo " make run-gui QEMU with graphical window (GOP/PS2)" + @echo " make debug QEMU + GDB stub on :1234" + @echo " make clean remove build artifacts" + @echo " make fmt format Rust source" + @echo "" + @echo "Scripts:" + @echo " ./tools/qemu.sh flexible QEMU launcher" + @echo " ./tools/boot-image.sh create bootable disk image" + @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 @@ +/// Build script — extracts glyph data from a PSF2 font file. +/// Uses zcat (system gzip) to decompress — zero dependencies. +use std::env; +use std::fs; +use std::path::Path; +use std::process::{Command, Output}; + +fn main() -> Result<(), Box> { + let font_path = "/usr/share/kbd/consolefonts/lat1-16.psfu.gz"; + println!("cargo::rerun-if-changed={}", font_path); + + // Decompress via zcat (available on any Linux system) + let Output { stdout, status, .. } = Command::new("zcat").arg(font_path).output()?; + if !status.success() { + return Err(format!("zcat exited with {status:?}").into()); + } + let raw = stdout; + + if raw.len() < 32 { + return Err("PSF font too small".into()); + } + + // PSF2 magic: 0x864ab572 (little-endian) + let magic = u32::from_le_bytes(raw[..4].try_into()?); + if magic != 0x864ab572 { + return Err("not a PSF2 font".into()); + } + + let hdr_size = u32::from_le_bytes(raw[8..12].try_into()?) as usize; + let num_glyphs = u32::from_le_bytes(raw[16..20].try_into()?) as usize; + let bytes_per_glyph = u32::from_le_bytes(raw[20..24].try_into()?) as usize; + + if num_glyphs != 256 { + return Err(format!("expected 256 glyphs, got {num_glyphs}").into()); + } + if bytes_per_glyph != 16 { + return Err(format!("expected 16 bytes/glyph, got {bytes_per_glyph}").into()); + } + + let glyph_data = &raw[hdr_size..hdr_size + num_glyphs * bytes_per_glyph]; + + // Write generated Rust source into OUT_DIR + let out_dir = env::var("OUT_DIR")?; + let dest = Path::new(&out_dir).join("font_data.rs"); + + let mut code = String::from( + "/// 8×16 bitmap font glyph data (256 glyphs, 16 bytes each). +#[rustfmt::skip] +#[allow(clippy::unreadable_literal)] +pub const GLYPH_8X16: [[u8; 16]; 256] = [", + ); + + for g in 0..256 { + let offset = g * 16; + code.push_str("\n ["); + for row in 0..16 { + if row % 4 == 0 { + code.push('\n'); + } + let byte = glyph_data[offset + row]; + code.push_str(&format!(" 0x{byte:02X},")); + } + code.push_str("\n ],"); + } + + code.push_str("\n];\n"); + + fs::write(&dest, code)?; + Ok(()) +} 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 @@ +# ADR 0001 — Static .bss Buffer for UEFI Memory Map Capture + +## Status + +Accepted. + +## Context + +Before calling `ExitBootServices`, the kernel must obtain the UEFI memory map +to know which physical memory regions are available. The standard UEFI recipe +is: + +1. Call `GetMemoryMap` with a null buffer to get the required buffer size. +2. Allocate a buffer (via `AllocatePool`). +3. Call `GetMemoryMap` again to fill the buffer, receiving a `MapKey`. +4. Call `ExitBootServices(image_handle, MapKey)`. + +**Problem:** Steps 2 and 3 change the memory map themselves (the pool +allocation adds entries). After step 3 we have a valid `MapKey` for the +map *as it was during step 3*, but calling `FreePool` on the buffer +(before step 4) would change the map again and invalidate the key. +Not freeing the buffer leaks pool memory, but more importantly, the +default approach of `AllocatePool + GetMemoryMap + ExitBootServices` +between two `GetMemoryMap` calls makes the key fragile. + +## Decision + +Use a **pre-allocated static buffer in `.bss`** (64 KB) instead of +dynamically allocating a pool buffer. Because the buffer already exists, +no `AllocatePool`/`FreePool` calls happen between `GetMemoryMap` and +`ExitBootServices`. + +The sequence becomes: + +1. `GetMemoryMap(null)` → determine required size. +2. `GetMemoryMap(static_buf)` → fill the map, receive `MapKey`. +3. `ExitBootServices(image_handle, MapKey)` — no pool allocation + happened after step 2, so the key is guaranteed valid. + +If the static buffer is too small (it shouldn't be — 64 KB holds ~1300 +descriptors, real firmware typically uses ~50–150), the caller falls +back to a pool-allocated buffer. + +## Consequences + +- **Positive:** The `MapKey` is never invalidated between capture and + `ExitBootServices`. The boot sequence is simpler and more reliable. +- **Positive:** No pool memory leaks — the static buffer lives in `.bss` + and costs nothing to keep. +- **Negative:** 64 KB of `.bss` is permanently reserved (irrelevant on a + kernel that owns all memory). +- **Negative:** The buffer size is fixed at compile time; pathological + firmware with thousands of entries would require a fallback path + (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 @@ +[toolchain] +channel = "stable" +targets = ["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 @@ +/// 8×16 bitmap font. +/// +/// Glyphs are generated at build time by `build.rs` from the system PSF2 font +/// (`/usr/share/kbd/consolefonts/lat1-16.psfu.gz`). +include!(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 @@ +/// GOP framebuffer driver — renders text via a bitmap font. +use crate::font; +use crate::uefi::{GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; +use core::fmt; +use core::ptr; + +/// Framebuffer state captured before ExitBootServices. +pub struct Framebuffer { + base: *mut u8, + #[allow(dead_code)] + width: usize, + height: usize, + pitch: usize, // bytes per scan line + bytes_per_pixel: usize, + bg: u32, // packed BGRA or RGBA background colour + fg: u32, // packed foreground colour + cursor_x: usize, + cursor_y: usize, + rows: usize, + cols: usize, +} + +static mut FB: Option = None; + +/// Initialise the framebuffer from GOP. Call *before* ExitBootServices. +pub fn init(gop: &mut GraphicsOutputProtocol) { + let mode = unsafe { &*gop.mode }; + let info = unsafe { &*mode.info }; + let base = mode.framebuffer_base as *mut u8; + let width = info.horizontal_resolution as usize; + let height = info.vertical_resolution as usize; + let pitch = info.pixels_per_scan_line as usize * 4; // 32-bit pixel + let bpp = 4; + + // Pick colours based on pixel format + let (fg, bg) = match info.pixel_format { + PIXEL_BGR => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), // white fg, black bg + PIXEL_RGB => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), + _ => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), + }; + + let rows = height / 16; + let cols = width / 8; + + let fb = Framebuffer { + base, + width, + height, + pitch, + bytes_per_pixel: bpp, + bg, + fg, + cursor_x: 0, + cursor_y: 0, + rows, + cols, + }; + + fb.clear(); + + unsafe { + FB = Some(fb); + } +} + +/// Draw a character at the current cursor position and advance. +pub fn put_char(c: char) { + let fb = unsafe { + match FB.as_mut() { + Some(f) => f, + None => return, + } + }; + + if c == '\x08' { + // Backspace - move cursor back and erase + if fb.cursor_x > 0 { + fb.cursor_x -= 1; + } else if fb.cursor_y > 0 { + fb.cursor_y -= 1; + fb.cursor_x = fb.cols - 1; + } else { + return; + } + // Draw glyph 32 (space) to erase — glyph 0 is NUL, not blank + fb.draw_glyph(32, fb.cursor_x, fb.cursor_y); + return; + } + if c == '\n' { + fb.cursor_x = 0; + fb.cursor_y += 1; + if fb.cursor_y >= fb.rows { + fb.scroll(); + } + return; + } + if c == '\r' { + fb.cursor_x = 0; + return; + } + + let idx = c as usize; + if idx >= 256 { + // Unmapped character: draw a space + fb.draw_glyph(0, fb.cursor_x, fb.cursor_y); + } else { + fb.draw_glyph(idx, fb.cursor_x, fb.cursor_y); + } + + fb.cursor_x += 1; + if fb.cursor_x >= fb.cols { + fb.cursor_x = 0; + fb.cursor_y += 1; + if fb.cursor_y >= fb.rows { + fb.scroll(); + } + } +} + +/// Write a string to the framebuffer. +pub fn write_str(s: &str) { + for c in s.chars() { + put_char(c); + } +} + +/// Clear the entire screen to background colour and reset cursor to (0, 0). +pub fn clear_screen() { + let fb = unsafe { + match FB.as_mut() { + Some(f) => f, + None => return, + } + }; + fb.clear(); + // Reset cursor back to top-left so the next prompt appears at (0, 0) + fb.cursor_x = 0; + fb.cursor_y = 0; +} + +// --------------------------------------------------------------------------- +// Framebuffer implementation +// --------------------------------------------------------------------------- +impl Framebuffer { + fn clear(&self) { + let size = self.pitch * self.height; + // Fill with background colour (32-bit writes) + let mut p = self.base; + let end = unsafe { p.add(size) }; + unsafe { + while p < end { + ptr::write_volatile(p as *mut u32, self.bg); + p = p.add(4); + } + } + } + + fn scroll(&mut self) { + // Move everything up one row (16 pixels) + let row_bytes = self.pitch * 16; + let total = self.pitch * self.height; + unsafe { + ptr::copy(self.base.add(row_bytes), self.base, total - row_bytes); + // Clear last row + let last_row = self.base.add(total - row_bytes); + for i in 0..(self.pitch * 16 / 4) { + ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); + } + } + self.cursor_y -= 1; + } + + fn draw_glyph(&self, glyph_idx: usize, col: usize, row: usize) { + let glyph = font::GLYPH_8X16; + let data = match glyph.get(glyph_idx) { + Some(d) => *d, + None => return, + }; + + let x = col * 8; + let y = row * 16; + + for cy in 0..16 { + let row_bits = data[cy]; + for cx in 0..8 { + if row_bits & (0x80 >> cx) != 0 { + // Set foreground pixel + let offset = (y + cy) * self.pitch + (x + cx) * self.bytes_per_pixel; + unsafe { + ptr::write_volatile(self.base.add(offset) as *mut u32, self.fg); + } + } else { + // Set background pixel + let offset = (y + cy) * self.pitch + (x + cx) * self.bytes_per_pixel; + unsafe { + ptr::write_volatile(self.base.add(offset) as *mut u32, self.bg); + } + } + } + } + } +} + +// --------------------------------------------------------------------------- +// fmt::Write wrapper +// --------------------------------------------------------------------------- +pub struct GopWriter; + +impl fmt::Write for GopWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + write_str(s); + Ok(()) + } +} + +/// Write a formatted string to the framebuffer. +#[macro_export] +macro_rules! gop_print { + ($($arg:tt)*) => {{ + use core::fmt::Write; + let _ = write!(&mut $crate::gop::GopWriter, $($arg)*); + }}; +} + +#[macro_export] +macro_rules! gop_println { + () => { $crate::gop::write_str("\n"); }; + ($($arg:tt)*) => {{ + $crate::gop_print!($($arg)*); + $crate::gop::write_str("\n"); + }}; +} 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 @@ +#![no_std] +#![no_main] +#![allow(static_mut_refs, unsafe_code, dead_code, unused_doc_comments)] + +// SAFETY: single-core kernel, no preemption — static mut access is sound +// Toy kernel: dead code and unused items during active development + +extern crate alloc; + +mod font; +mod gop; +mod memory; +mod ps2; +mod serial; +mod shell; +mod uefi; + +use core::panic::PanicInfo; +use uefi::*; + +// --------------------------------------------------------------------------- +// Panic handler +// --------------------------------------------------------------------------- +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + use core::fmt::Write; + serial::write_str("\r\n*** PANIC ***\r\n"); + let _ = write!(&mut serial::SerialWriter, "{}\r\n", info.message()); + if let Some(loc) = info.location() { + let _ = write!( + &mut serial::SerialWriter, + " at {}:{}\r\n", + loc.file(), + loc.line() + ); + } + loop { + unsafe { + core::arch::asm!("cli; hlt"); + } + } +} + +// --------------------------------------------------------------------------- +// EFI entry point +// --------------------------------------------------------------------------- +#[no_mangle] +pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemTable) -> ! { + let st = unsafe { &mut *system_table }; + let bs = unsafe { &mut *st.boot_services }; + let con_out = unsafe { &mut *st.con_out }; + + // ---- Phase 1: UEFI boot services alive ---- + + // Print banner via UEFI console + unsafe { + let banner: [u16; 28] = [ + 'r' as u16, 'a' as u16, 'i' as u16, 's' as u16, 'e' as u16, 'r' as u16, 'n' as u16, + 'i' as u16, 'c' as u16, 's' as u16, ' ' as u16, 'v' as u16, '0' as u16, '.' as u16, + '1' as u16, ' ' as u16, '-' as u16, ' ' as u16, 'b' as u16, 'o' as u16, 'o' as u16, + 't' as u16, 'i' as u16, 'n' as u16, 'g' as u16, '.' as u16, '.' as u16, 0x0A, + ]; + (con_out.output_string)(con_out, banner.as_ptr()); + } + + // Init serial early so we can debug-print before EBS + serial::init(); + + serial_println!("=== raisernics boot ==="); + + // Stash the memory map + let map_key = memory::capture_memory_map(bs); + let map_key = match map_key { + Ok(k) => k, + Err(e) => { + serial_println!("mem: capture_memory_map failed 0x{:x}", e); + 0 + } + }; + + // Locate GOP (Graphics Output Protocol) + let mut gop_interface: *mut core::ffi::c_void = core::ptr::null_mut(); + let gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + let gop_result = unsafe { + (bs.locate_protocol)( + &gop_guid as *const Guid, + core::ptr::null_mut(), + &mut gop_interface, + ) + }; + if gop_result == EFI_SUCCESS { + let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) }; + serial_println!("gop: framebuffer found"); + gop::init(gop); + } else { + serial_println!("gop: not found (0x{:x})", gop_result); + } + + // ExitBootServices — we now own the machine + let ebs_result = unsafe { (bs.exit_boot_services)(image_handle, map_key) }; + if ebs_result != EFI_SUCCESS { + // Memory map may have changed; try again with fresh map + // (This is the standard UEFI EBS retry dance.) + serial_println!( + "WARN: ExitBootServices failed (0x{:x}), retrying...", + ebs_result + ); + let _map_key = match memory::refresh_and_exit(bs, image_handle) { + Ok(k) => k, + Err(e) => { + serial_println!("FATAL: cannot ExitBootServices (0x{:x})", e); + loop { + unsafe { + core::arch::asm!("cli; hlt"); + } + } + } + }; + } + + // ---- Phase 2: Native mode ---- + + serial::init(); + serial_println!("serial: COM1 115200 8N1"); + + // PS/2 keyboard + ps2::init(); + serial_println!("ps2: keyboard initialized"); + + // Memory: the bump allocator was set up during capture_memory_map. + // `alloc::Vec`, `alloc::String`, `format!` etc. are now usable. + + // Print memory summary + dump_memory_summary(); + + serial_println!("boot: entering shell"); + shell::run(); +} + +// --------------------------------------------------------------------------- +// Print a memory summary from the stashed memory map +// --------------------------------------------------------------------------- +fn dump_memory_summary() { + let (map, key) = memory::get_map(); + let mut conventional_4k = 0u64; + let mut addr_space_4k = 0u64; + for desc in map.iter() { + let pages = desc.number_of_pages; + addr_space_4k += pages; + if desc.kind == 7 { + conventional_4k += pages; + } + } + serial_println!( + "mem: {} entries, key=0x{:x}, usable: {} KB ({} MB)", + map.len(), + key, + conventional_4k * 4, + (conventional_4k * 4) / 1024, + ); + serial_println!( + "mem: address space: {} MB (incl. PCIe MMIO)", + (addr_space_4k * 4) / 1024, + ); +} + +// --------------------------------------------------------------------------- +// Helper: write a panic-proof string via UEFI console (used before serial) +// --------------------------------------------------------------------------- +#[allow(dead_code)] +unsafe fn uefi_puts(con_out: *mut SimpleTextOutput, s: &str) { + // Simple ASCII → UEFI-CHAR16 conversion (stack buffer, panic-safe) + let mut buf: [u16; 128] = [0; 128]; + let len = s.len().min(127); + for (i, &b) in s.as_bytes()[..len].iter().enumerate() { + buf[i] = b as u16; + } + buf[len] = 0x0A; // LF + let out = unsafe { &mut *con_out }; + (out.output_string)(out, buf.as_ptr()); +} 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 @@ +use crate::uefi::*; +use core::alloc::{GlobalAlloc, Layout}; +use core::mem; +use core::ptr; + +const MAX_MAP_ENTRIES: usize = 512; + +static mut MEMORY_MAP: [MemoryDescriptor; MAX_MAP_ENTRIES] = unsafe { mem::zeroed() }; +static mut MAP_ENTRIES: usize = 0; + +/// The map key returned by GetMemoryMap, needed for ExitBootServices. +static mut MAP_KEY: usize = 0; + +/// Static working buffer for GetMemoryMap (64 KB, way more than enough). +/// Lives in .bss; no pool allocation, so no map-key staleness. +static mut MAP_WORK_BUF: [u8; 65536] = [0u8; 65536]; + +// --------------------------------------------------------------------------- +// Bump allocator — no deallocation, just pointer advancement. +// Used as Rust's #[global_allocator] once the heap region is set up. +// --------------------------------------------------------------------------- +struct BumpAllocator; + +unsafe impl Sync for BumpAllocator {} + +unsafe impl GlobalAlloc for BumpAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let align = layout.align().max(16); + let size = layout.size(); + unsafe { + let a = (HEAP_CURSOR + align - 1) & !(align - 1); + let e = a + size; + if e > HEAP_END || e < a { + return ptr::null_mut(); + } + HEAP_CURSOR = e; + a as *mut u8 + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + // Bump allocator — cannot free individual allocations. + // Memory is reclaimed on reboot. Good enough for now. + unsafe { + core::ptr::read_volatile(&_ptr); + } + let _ = _layout; + } +} + +#[global_allocator] +static ALLOCATOR: BumpAllocator = BumpAllocator; + +/// Heap bump allocator bounds (`static mut` accessed only inside +/// `GlobalAlloc` impl, which is already `unsafe`). +static mut HEAP_START: usize = 0; +static mut HEAP_END: usize = 0; +static mut HEAP_CURSOR: usize = 0; + +/// Capture the UEFI memory map into the static buffer. Uses a pre-allocated +/// .bss working buffer so no pool allocation (and thus no memory-map change) +/// occurs between GetMemoryMap and ExitBootServices. +pub fn capture_memory_map(bs: &BootServices) -> Result { + let mut map_key: usize = 0; + let mut desc_size: usize = 0; + let mut desc_ver: u32 = 0; + + // Probe with null to get required size + descriptor size + let mut buf_size: usize = 0; + let status = unsafe { + (bs.get_memory_map)( + &mut buf_size, + ptr::null_mut(), + &mut map_key, + &mut desc_size, + &mut desc_ver, + ) + }; + if status != EFI_BUFFER_TOO_SMALL { + // Fallback: allocate a sensible size + buf_size = 64 * 1024; + } + if desc_size == 0 { + desc_size = mem::size_of::(); + } + + // Round up to desc_size alignment + let buf_size = (buf_size + desc_size - 1) / desc_size * desc_size; + + // If our static buffer is too small, fall back to pool (rare) + if buf_size > unsafe { MAP_WORK_BUF.len() } { + return Err(EFI_BUFFER_TOO_SMALL); + } + + let mut actual_size = buf_size; + let status = unsafe { + (bs.get_memory_map)( + &mut actual_size, + &raw mut MAP_WORK_BUF[0] as *mut MemoryDescriptor, + &mut map_key, + &mut desc_size, + &mut desc_ver, + ) + }; + + if status == EFI_BUFFER_TOO_SMALL { + // The map grew between the probe and the real call. Try a larger + // static buffer (can't grow here without pool, so return error to + // let the caller retry if they can allocate pool). + // For our 64 KB buffer this should never happen on OVMF. + return Err(EFI_BUFFER_TOO_SMALL); + } + if status != EFI_SUCCESS { + return Err(status); + } + + // Copy descriptors into our indexed array + let count = actual_size / desc_size; + let count = count.min(MAX_MAP_ENTRIES); + unsafe { + for i in 0..count { + let p = (&raw const MAP_WORK_BUF[..]) as *const u8; + let off = i * desc_size; + let src = p.add(off) as *const MemoryDescriptor; + MEMORY_MAP[i] = ptr::read(src); + } + MAP_ENTRIES = count; + MAP_KEY = map_key; + } + + // Do NOT free anything — the static buffer stays. Map key is still + // valid because no pool allocation/free happened after GetMemoryMap. + + init_bump_allocator(); + Ok(map_key) +} + +/// Retry ExitBootServices with a fresh memory map. +pub fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result { + let key = capture_memory_map(bs)?; + let status = unsafe { (bs.exit_boot_services)(image_handle, key) }; + if status == EFI_SUCCESS { + Ok(key) + } else { + Err(status) + } +} + +fn init_bump_allocator() { + unsafe { + let mut best_start = 0usize; + let mut best_size = 0usize; + for i in 0..MAP_ENTRIES { + let d = &MEMORY_MAP[i]; + if d.kind == MEMORY_TYPE_CONVENTIONAL { + let sz = d.number_of_pages as usize * 4096; + if sz > best_size { + best_start = d.physical_start as usize; + best_size = sz; + } + } + } + if best_size > 2 * 1024 * 1024 { + let s = best_start + 2 * 1024 * 1024; + let e = best_start + best_size; + if s < e { + HEAP_START = s; + HEAP_END = e; + HEAP_CURSOR = s; + } + } + } +} + +#[allow(dead_code)] +pub fn allocate(size: usize) -> *mut u8 { + unsafe { + let a = (HEAP_CURSOR + 15) & !15; + let e = a + size; + if e > HEAP_END || e < a { + return ptr::null_mut(); + } + HEAP_CURSOR = e; + a as *mut u8 + } +} + +#[allow(dead_code)] +pub fn get_map() -> (&'static [MemoryDescriptor], usize) { + unsafe { + ( + core::slice::from_raw_parts(&MEMORY_MAP[0], MAP_ENTRIES), + MAP_KEY, + ) + } +} 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 @@ +/// PS/2 keyboard driver — polling, scancode set 1, US QWERTY layout. +// Port addresses +const DATA_PORT: u16 = 0x60; +const STATUS_PORT: u16 = 0x64; + +// Status register bits +const STAT_OUTPUT_FULL: u8 = 0x01; + +// Modifier bit constants +const MOD_LSHIFT: u8 = 0x01; +const MOD_RSHIFT: u8 = 0x02; +const MOD_LCTRL: u8 = 0x04; +const MOD_RCTRL: u8 = 0x08; +const MOD_LALT: u8 = 0x10; +const MOD_RALT: u8 = 0x20; +const MOD_CAPS: u8 = 0x40; + +/// A decoded key event. +pub enum KeyEvent { + Char(char), + Enter, + Backspace, + Tab, + Escape, + ArrowUp, + ArrowDown, + ArrowLeft, + ArrowRight, + Home, + End, + PageUp, + PageDown, + Delete, + F(u8), + Unknown(u8), +} + +// ----- scancode-set-1 translation tables (US QWERTY) ----- + +/// Normal (unshifted) scancode → ASCII mapping. 0 = unmapped. +static SCAN_NORMAL: [u8; 128] = { + let mut t = [0u8; 128]; + t[0x02] = b'1'; + t[0x03] = b'2'; + t[0x04] = b'3'; + t[0x05] = b'4'; + t[0x06] = b'5'; + t[0x07] = b'6'; + t[0x08] = b'7'; + t[0x09] = b'8'; + t[0x0A] = b'9'; + t[0x0B] = b'0'; + t[0x0C] = b'-'; + t[0x0D] = b'='; + t[0x10] = b'q'; + t[0x11] = b'w'; + t[0x12] = b'e'; + t[0x13] = b'r'; + t[0x14] = b't'; + t[0x15] = b'y'; + t[0x16] = b'u'; + t[0x17] = b'i'; + t[0x18] = b'o'; + t[0x19] = b'p'; + t[0x1A] = b'['; + t[0x1B] = b']'; + t[0x1E] = b'a'; + t[0x1F] = b's'; + t[0x20] = b'd'; + t[0x21] = b'f'; + t[0x22] = b'g'; + t[0x23] = b'h'; + t[0x24] = b'j'; + t[0x25] = b'k'; + t[0x26] = b'l'; + t[0x27] = b';'; + t[0x28] = b'\''; + t[0x29] = b'`'; + t[0x2B] = b'\\'; + t[0x2C] = b'z'; + t[0x2D] = b'x'; + t[0x2E] = b'c'; + t[0x2F] = b'v'; + t[0x30] = b'b'; + t[0x31] = b'n'; + t[0x32] = b'm'; + t[0x33] = b','; + t[0x34] = b'.'; + t[0x35] = b'/'; + t[0x37] = b'*'; + t[0x39] = b' '; + t +}; + +/// Shifted scancode → ASCII. +static SCAN_SHIFT: [u8; 128] = { + let mut t = [0u8; 128]; + t[0x02] = b'!'; + t[0x03] = b'@'; + t[0x04] = b'#'; + t[0x05] = b'$'; + t[0x06] = b'%'; + t[0x07] = b'^'; + t[0x08] = b'&'; + t[0x09] = b'*'; + t[0x0A] = b'('; + t[0x0B] = b')'; + t[0x0C] = b'_'; + t[0x0D] = b'+'; + t[0x10] = b'Q'; + t[0x11] = b'W'; + t[0x12] = b'E'; + t[0x13] = b'R'; + t[0x14] = b'T'; + t[0x15] = b'Y'; + t[0x16] = b'U'; + t[0x17] = b'I'; + t[0x18] = b'O'; + t[0x19] = b'P'; + t[0x1A] = b'{'; + t[0x1B] = b'}'; + t[0x1E] = b'A'; + t[0x1F] = b'S'; + t[0x20] = b'D'; + t[0x21] = b'F'; + t[0x22] = b'G'; + t[0x23] = b'H'; + t[0x24] = b'J'; + t[0x25] = b'K'; + t[0x26] = b'L'; + t[0x27] = b':'; + t[0x28] = b'"'; + t[0x29] = b'~'; + t[0x2B] = b'|'; + t[0x2C] = b'Z'; + t[0x2D] = b'X'; + t[0x2E] = b'C'; + t[0x2F] = b'V'; + t[0x30] = b'B'; + t[0x31] = b'N'; + t[0x32] = b'M'; + t[0x33] = b'<'; + t[0x34] = b'>'; + t[0x35] = b'?'; + t[0x37] = b'*'; + t[0x39] = b' '; + t +}; + +/// Internal keyboard state +struct KbdState { + modifiers: u8, + got_e0: bool, +} + +static mut KBD: KbdState = KbdState { + modifiers: 0, + got_e0: false, +}; + +/// Initialise the PS/2 port for the keyboard — just drain stale bytes. +pub fn init() { + drain_input(); +} + +fn drain_input() { + unsafe { + loop { + let status: u8; + core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, + options(nostack, preserves_flags)); + if status & STAT_OUTPUT_FULL == 0 { + break; + } + let _data: u8; + core::arch::asm!("in al, dx", out("al") _data, in("dx") DATA_PORT, + options(nostack, preserves_flags)); + } + } +} + +/// Poll for a key event. Blocks until a key is pressed. +pub fn poll_key() -> KeyEvent { + loop { + let byte = read_data(); + if let Some(ev) = process_byte(byte) { + return ev; + } + } +} + +/// Non-blocking poll. +pub fn try_poll_key() -> Option { + unsafe { + let status: u8; + core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, + options(nostack, preserves_flags)); + if status & STAT_OUTPUT_FULL == 0 { + return None; + } + let data: u8; + core::arch::asm!("in al, dx", out("al") data, in("dx") DATA_PORT, + options(nostack, preserves_flags)); + process_byte(data) + } +} + +/// Read a byte from the keyboard data port (blocking). +fn read_data() -> u8 { + unsafe { + loop { + let status: u8; + core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, + options(nostack, preserves_flags)); + if status & STAT_OUTPUT_FULL != 0 { + let data: u8; + core::arch::asm!("in al, dx", out("al") data, in("dx") DATA_PORT, + options(nostack, preserves_flags)); + return data; + } + } + } +} + +/// Process a single scancode byte. Returns Some(KeyEvent) when a visible +/// event happens; returns None for modifier changes, releases, and prefixes. +fn process_byte(byte: u8) -> Option { + let state = unsafe { &mut KBD }; + + if byte == 0xE0 { + state.got_e0 = true; + return None; + } + + if state.got_e0 { + state.got_e0 = false; + return process_e0(byte); + } + + // Break code (key release) — bit 7 set + if byte & 0x80 != 0 { + update_modifiers(byte & 0x7F, false, false); + return None; + } + + // Make code (key press) + update_modifiers(byte, true, false); + + match byte { + 0x01 => Some(KeyEvent::Escape), + 0x0E => Some(KeyEvent::Backspace), + 0x0F => Some(KeyEvent::Tab), + 0x1C => Some(KeyEvent::Enter), + 0x37 => Some(KeyEvent::Char('*')), + 0x3B..=0x44 => Some(KeyEvent::F(byte - 0x3B + 1)), + _ => { + let shifted = (state.modifiers & (MOD_LSHIFT | MOD_RSHIFT)) != 0; + // CapsLock inverts case for letters + let caps = state.modifiers & MOD_CAPS != 0; + let table = if shifted { SCAN_SHIFT } else { SCAN_NORMAL }; + let mut ch = table[byte as usize]; + // Handle CapsLock for letters + if caps && ch.is_ascii_alphabetic() { + // Swap case: if table gives lowercase, make uppercase; etc. + // Since normal table has lowercase, shift table has uppercase: + ch = if shifted { + ch.to_ascii_lowercase() + } else { + ch.to_ascii_uppercase() + }; + } + if ch != 0 { + Some(KeyEvent::Char(ch as char)) + } else { + Some(KeyEvent::Unknown(byte)) + } + } + } +} + +/// Process an E0-prefixed scancode. +fn process_e0(byte: u8) -> Option { + if byte & 0x80 != 0 { + update_modifiers(byte & 0x7F, false, true); + return None; + } + + update_modifiers(byte, true, true); + + match byte { + 0x48 => Some(KeyEvent::ArrowUp), + 0x50 => Some(KeyEvent::ArrowDown), + 0x4B => Some(KeyEvent::ArrowLeft), + 0x4D => Some(KeyEvent::ArrowRight), + 0x47 => Some(KeyEvent::Home), + 0x4F => Some(KeyEvent::End), + 0x49 => Some(KeyEvent::PageUp), + 0x51 => Some(KeyEvent::PageDown), + 0x53 => Some(KeyEvent::Delete), + 0x35 => Some(KeyEvent::Char('/')), + _ => Some(KeyEvent::Unknown(byte)), + } +} + +fn update_modifiers(scancode: u8, pressed: bool, e0: bool) { + let state = unsafe { &mut KBD }; + match (scancode, e0) { + (0x2A, false) => set_mod(state, MOD_LSHIFT, pressed), + (0x36, false) => set_mod(state, MOD_RSHIFT, pressed), + (0x1D, false) => set_mod(state, MOD_LCTRL, pressed), + (0x1D, true) => set_mod(state, MOD_RCTRL, pressed), + (0x38, false) => set_mod(state, MOD_LALT, pressed), + (0x38, true) => set_mod(state, MOD_RALT, pressed), + (0x3A, false) => { + if pressed { + state.modifiers ^= MOD_CAPS; + } + } + _ => {} + } +} + +fn set_mod(state: &mut KbdState, bit: u8, pressed: bool) { + if pressed { + state.modifiers |= bit; + } else { + state.modifiers &= !bit; + } +} 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 @@ +/// UART 16550 serial driver — port-mapped I/O on COM1 (0x3F8) +use core::fmt; + +const COM1: u16 = 0x3F8; + +const DATA_REG: u16 = COM1; +const INTR_ENABLE: u16 = COM1 + 1; +const FIFO_CONTROL: u16 = COM1 + 2; +const LINE_CONTROL: u16 = COM1 + 3; +const MODEM_CONTROL: u16 = COM1 + 4; +const LINE_STATUS: u16 = COM1 + 5; + +const LSR_TX_EMPTY: u8 = 0x20; +const LSR_DATA_READY: u8 = 0x01; + +/// Initialise the UART with 115200 baud, 8N1. +pub fn init() { + unsafe { + // Disable interrupts + port_out(INTR_ENABLE, 0x00); + // Set DLAB=1 (baud rate divisor) + port_out(LINE_CONTROL, 0x80); + // Divisor = 1 → 115200 baud (with 1.8432 MHz clock) + port_out(DATA_REG, 0x01); + port_out(INTR_ENABLE, 0x00); + // 8-bit, 1 stop, no parity, DLAB=0 + port_out(LINE_CONTROL, 0x03); + // Enable FIFO, clear, 14-byte threshold + port_out(FIFO_CONTROL, 0xC7); + // RTS/DSR set + port_out(MODEM_CONTROL, 0x0B); + } +} + +/// Write a single byte to the serial port (blocking poll). +pub fn put_byte(b: u8) { + unsafe { + // Wait for transmitter holding register empty + while port_in(LINE_STATUS) & LSR_TX_EMPTY == 0 {} + port_out(DATA_REG, b); + } +} + +/// Read a single byte from the serial port (blocking poll). +pub fn get_byte() -> u8 { + unsafe { + while port_in(LINE_STATUS) & LSR_DATA_READY == 0 {} + port_in(DATA_REG) + } +} + +/// Non-blocking read: returns Some(byte) if data ready, None otherwise. +pub fn try_get_byte() -> Option { + unsafe { + if port_in(LINE_STATUS) & LSR_DATA_READY != 0 { + Some(port_in(DATA_REG)) + } else { + None + } + } +} + +/// Write a string to serial. +pub fn write_str(s: &str) { + for &b in s.as_bytes() { + if b == b'\n' { + put_byte(b'\r'); + } + put_byte(b); + } +} + +// --------------------------------------------------------------------------- +// fmt::Write implementation +// --------------------------------------------------------------------------- +pub struct SerialWriter; + +impl fmt::Write for SerialWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + write_str(s); + Ok(()) + } +} + +/// Helper macro — prints to serial via format_args!. +#[macro_export] +macro_rules! serial_print { + ($($arg:tt)*) => {{ + use core::fmt::Write; + let _ = write!(&mut $crate::serial::SerialWriter, $($arg)*); + }}; +} + +#[macro_export] +macro_rules! serial_println { + () => { $crate::serial::write_str("\r\n"); }; + ($($arg:tt)*) => {{ + $crate::serial_print!($($arg)*); + $crate::serial::write_str("\r\n"); + }}; +} + +// --------------------------------------------------------------------------- +// Port I/O helpers +// --------------------------------------------------------------------------- +unsafe fn port_in(port: u16) -> u8 { + let val: u8; + unsafe { + core::arch::asm!("in al, dx", out("al") val, in("dx") port, options(nostack, preserves_flags)); + } + val +} + +unsafe fn port_out(port: u16, val: u8) { + unsafe { + core::arch::asm!("out dx, al", in("al") val, in("dx") port, options(nostack, preserves_flags)); + } +} 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()) + } + } +} 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 @@ +use core::ffi::c_void; + +/// Opaque EFI handle +pub type Handle = *mut c_void; + +/// EFI status (0 = success, bit 63 = error code) +pub type Status = u64; + +/// Helper: UEFI error codes always have bit 63 set +const fn efi_err(n: u64) -> u64 { + n | (1u64 << 63) +} + +pub const EFI_SUCCESS: Status = 0; +pub const EFI_LOAD_ERROR: Status = efi_err(1); +pub const EFI_INVALID_PARAMETER: Status = efi_err(2); +#[allow(dead_code)] +pub const EFI_UNSUPPORTED: Status = efi_err(3); +pub const EFI_BUFFER_TOO_SMALL: Status = efi_err(5); +#[allow(dead_code)] +pub const EFI_NOT_FOUND: Status = efi_err(14); +pub const EFI_OUT_OF_RESOURCES: Status = efi_err(9); + +// --------------------------------------------------------------------------- +// GUID +// --------------------------------------------------------------------------- +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct Guid { + pub a: u32, + pub b: u16, + pub c: u16, + pub d: [u8; 8], +} + +pub const EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID: Guid = Guid { + a: 0x9042a9de, + b: 0x23dc, + c: 0x4a38, + d: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a], +}; + +// --------------------------------------------------------------------------- +// Table header +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct TableHeader { + pub signature: u64, + pub revision: u32, + pub header_size: u32, + pub crc32: u32, + pub reserved: u32, +} + +// --------------------------------------------------------------------------- +// Simple text output protocol (console output) +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct SimpleTextOutput { + _reset: *mut c_void, + pub output_string: unsafe extern "efiapi" fn(*mut SimpleTextOutput, *const u16) -> Status, + _test_string: *mut c_void, + _query_mode: *mut c_void, + _set_mode: *mut c_void, + _set_attribute: *mut c_void, + _clear_screen: *mut c_void, + _set_cursor_position: *mut c_void, + _enable_cursor: *mut c_void, + _mode: *mut c_void, +} + +// --------------------------------------------------------------------------- +// Simple text input protocol (keyboard input) +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct InputKey { + pub scan_code: u16, + pub unicode_char: u16, +} + +#[repr(C)] +pub struct SimpleTextInput { + _reset: *mut c_void, + pub read_key_stroke: unsafe extern "efiapi" fn(*mut SimpleTextInput, *mut InputKey) -> Status, + _wait_for_key: Handle, +} + +// --------------------------------------------------------------------------- +// Runtime services (stub — we don't use them) +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct RuntimeServices { + _hdr: TableHeader, + _reserved: [*mut c_void; 30], +} + +// --------------------------------------------------------------------------- +// Configuration table entry +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct ConfigurationTable { + pub vendor_guid: Guid, + pub vendor_table: *mut c_void, +} + +// --------------------------------------------------------------------------- +/// UEFI memory descriptor (from GetMemoryMap) +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct MemoryDescriptor { + pub kind: u32, + pub pad: u32, + pub physical_start: u64, + pub virtual_start: u64, + pub number_of_pages: u64, + pub attribute: u64, +} + +pub const MEMORY_TYPE_CONVENTIONAL: u32 = 7; +#[allow(dead_code)] +pub const MEMORY_TYPE_LOADER_DATA: u32 = 6; +#[allow(dead_code)] +pub const MEMORY_TYPE_BOOT_SERVICES_CODE: u32 = 3; +#[allow(dead_code)] +pub const MEMORY_TYPE_BOOT_SERVICES_DATA: u32 = 4; + +// --------------------------------------------------------------------------- +// Boot services table +// --------------------------------------------------------------------------- +pub type EfiGetMemoryMap = unsafe extern "efiapi" fn( + *mut usize, + *mut MemoryDescriptor, + *mut usize, + *mut usize, + *mut u32, +) -> Status; + +pub type EfiExitBootServices = unsafe extern "efiapi" fn(Handle, usize) -> Status; + +pub type EfiHandleProtocol = + unsafe extern "efiapi" fn(Handle, *const Guid, *mut *mut c_void) -> Status; + +pub type EfiLocateProtocol = + unsafe extern "efiapi" fn(*const Guid, *mut c_void, *mut *mut c_void) -> Status; + +pub type EfiAllocatePool = unsafe extern "efiapi" fn(u8, usize, *mut *mut c_void) -> Status; + +pub type EfiFreePool = unsafe extern "efiapi" fn(*mut c_void) -> Status; + +pub type EfiStall = unsafe extern "efiapi" fn(usize) -> Status; + +#[repr(C)] +pub struct BootServices { + pub hdr: TableHeader, + + // 0-1: task priority + pub raise_tpl: *mut c_void, + pub restore_tpl: *mut c_void, + + // 2-3: memory allocation + pub allocate_pages: *mut c_void, + pub free_pages: *mut c_void, + + // 4: GetMemoryMap + pub get_memory_map: EfiGetMemoryMap, + + // 5-6: pool + pub allocate_pool: EfiAllocatePool, + pub free_pool: EfiFreePool, + + // 7-12: events + pub create_event: *mut c_void, + pub set_timer: *mut c_void, + pub wait_for_event: *mut c_void, + pub signal_event: *mut c_void, + pub close_event: *mut c_void, + pub check_event: *mut c_void, + + // 13-14: protocol install + pub install_protocol_interface: *mut c_void, + pub reinstall_protocol_interface: *mut c_void, + pub uninstall_protocol_interface: *mut c_void, + + // 16: HandleProtocol + pub handle_protocol: EfiHandleProtocol, + + pub _reserved: *mut c_void, + + // 18: register protocol notify + pub register_protocol_notify: *mut c_void, + + // 19-21: locate + pub locate_handle: *mut c_void, + pub locate_device_path: *mut c_void, + pub install_configuration_table: *mut c_void, + + // 22-23: image loading + pub load_image: *mut c_void, + pub start_image: *mut c_void, + + // 24-25 + pub exit: *mut c_void, + pub unload_image: *mut c_void, + + // 26: ExitBootServices + pub exit_boot_services: EfiExitBootServices, + + // 27-29 + pub get_next_monotonic_count: *mut c_void, + pub stall: EfiStall, + pub set_watchdog_timer: *mut c_void, + + // 30-36 + pub connect_controller: *mut c_void, + pub disconnect_controller: *mut c_void, + pub open_protocol: *mut c_void, + pub close_protocol: *mut c_void, + pub open_protocol_information: *mut c_void, + pub protocols_per_handle: *mut c_void, + pub locate_handle_buffer: *mut c_void, + + // 37: LocateProtocol + pub locate_protocol: EfiLocateProtocol, + + // 38-43 + pub install_multiple_protocol_interfaces: *mut c_void, + pub uninstall_multiple_protocol_interfaces: *mut c_void, + pub calculate_crc32: *mut c_void, + pub copy_mem: *mut c_void, + pub set_mem: *mut c_void, + pub create_event_ex: *mut c_void, +} + +// --------------------------------------------------------------------------- +// Graphics Output Protocol (GOP) +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct GopModeInfo { + pub version: u32, + pub horizontal_resolution: u32, + pub vertical_resolution: u32, + pub pixel_format: u32, // PixelFormat enum + pub red_mask: u32, + pub green_mask: u32, + pub blue_mask: u32, + pub reserved_mask: u32, + pub pixels_per_scan_line: u32, +} + +pub const PIXEL_RGB: u32 = 0; +pub const PIXEL_BGR: u32 = 1; + +#[repr(C)] +pub struct GopMode { + pub max_mode: u32, + pub mode: u32, + pub info: *mut GopModeInfo, + pub info_size: usize, + pub framebuffer_base: *mut c_void, + pub framebuffer_size: usize, +} + +#[repr(C)] +pub struct GraphicsOutputProtocol { + pub query_mode: *mut c_void, + pub set_mode: *mut c_void, + pub get_current_mode: *mut c_void, + pub mode: *mut GopMode, +} + +// --------------------------------------------------------------------------- +// System table +// --------------------------------------------------------------------------- +#[repr(C)] +pub struct SystemTable { + pub hdr: TableHeader, + pub firmware_vendor: *const u16, + pub firmware_revision: u32, + pub console_in_handle: Handle, + pub con_in: *mut SimpleTextInput, + pub console_out_handle: Handle, + pub con_out: *mut SimpleTextOutput, + pub stderr_handle: Handle, + pub std_err: *mut SimpleTextOutput, + pub runtime_services: *mut RuntimeServices, + pub boot_services: *mut BootServices, + pub number_of_table_entries: usize, + pub configuration_table: *const ConfigurationTable, +} 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 @@ +#!/usr/bin/env bash +# ============================================================================= +# tools/boot-image.sh — Create a bootable disk image from the ESP directory +# +# Produces a raw disk image (bootable via UEFI) for dd-ing to a USB stick +# or attaching to QEMU as a hard disk. +# +# Usage: +# ./tools/boot-image.sh [output.img] +# +# Default output: raisernics.img +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$SCRIPT_DIR" + +OUTPUT="${1:-raisernics.img}" +ESP_DIR="${ESP_DIR:-esp}" +MBR_SZ=1 # 1 MB reserved at start for GPT/pmbr +SECTOR_SZ=512 + +# Rebuild ESP if stale +make all + +if [ ! -d "$ESP_DIR" ]; then + echo "ERROR: ESP directory '$ESP_DIR' not found. Run 'make all' first." + exit 1 +fi + +# Calculate required size: ESP contents + MBR gap + some slack +ESP_SIZE_MB=$(du -sm "$ESP_DIR" 2>/dev/null | cut -f1 || echo 1) +TOTAL_MB=$((ESP_SIZE_MB + MBR_SZ + 1)) # +1 MB slack + +echo "Creating ${TOTAL_MB} MB disk image: ${OUTPUT}" + +# Create empty image with MBR gap +dd if=/dev/zero of="$OUTPUT" bs=1M count="$TOTAL_MB" status=none 2>/dev/null + +# Partition table: single GPT partition, type EFI System (C12A7328-F81F-11D2-BA4B-00A0C93EC93B) +sgdisk --clear \ + --new=1:${MBR_SZ}M:0 \ + --typecode=1:C12A7328-F81F-11D2-BA4B-00A0C93EC93B \ + --change-name=1:"raisernics" \ + "$OUTPUT" >/dev/null + +# Find partition offset +PART_START=$(sgdisk --info=1 "$OUTPUT" 2>/dev/null | grep "Partition start sector" | awk '{print $NF}') +if [ -z "$PART_START" ]; then + PART_START=$((MBR_SZ * 1024 * 1024 / SECTOR_SZ)) +fi + +# Format as FAT32 +mformat -i "$OUTPUT"@$((PART_START * SECTOR_SZ)) -h 32 -s 32 -t "$((TOTAL_MB * 2))" >/dev/null 2>&1 || true + +# Copy the ESP contents onto the partition +mcopy -i "$OUTPUT"@$((PART_START * SECTOR_SZ)) -s "$ESP_DIR"/* ::/ 2>/dev/null + +echo " -> ${OUTPUT} ($(ls -lh "$OUTPUT" | awk '{print $5}'))" +echo "" +echo "To write to USB: sudo dd if=${OUTPUT} of=/dev/sdX bs=4M status=progress" +echo "To test with QEMU: qemu-system-x86_64 -bios -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 @@ +""" +GDB Python script for debugging raisernics under QEMU. + +Usage: + 1. Terminal 1: ./tools/qemu.sh --debug + 2. Terminal 2: gdb -x tools/debug-kernel.py target/x86_64-unknown-uefi/debug/raisernics.efi +""" + +import gdb +import os + +# Find the kernel binary +script_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(script_dir) +binary = os.path.join( + project_root, + "target/x86_64-unknown-uefi/debug/raisernics.efi" +) + +# Connect to QEMU's GDB stub +gdb.execute("target remote :1234") + +# Load the binary (add symbol table) +if os.path.exists(binary): + gdb.execute(f"symbol-file {binary}") + print(f"Loaded symbols from {binary}") +else: + print(f"WARNING: {binary} not found. Build with debug symbols first.") + +gdb.execute("set breakpoint pending on") + + +# Helper commands +class DumpBootServices(gdb.Command): + """Dump EFI boot services signature and key pointers""" + + def __init__(self): + super().__init__("dump-bs", gdb.COMMAND_USER) + + def invoke(self, argument, from_tty): + del argument, from_tty + gdb.execute("printf \"SystemTable, BootServices, and key function pointers\\n\"") + gdb.execute("info functions efi_main") + + +class DumpGop(gdb.Command): + """Dump GOP framebuffer info""" + + def __init__(self): + super().__init__("dump-gop", gdb.COMMAND_USER) + + def invoke(self, argument, from_tty): + del argument, from_tty + gdb.execute("printf \"Check the gop::Framebuffer static\\n\"") + + +DumpBootServices() +DumpGop() + +print("") +print("raisernics GDB session ready.") +print(" target remote :1234 (connected)") +print(" Commands: continue, stepi, bt, info registers, etc.") +print(" 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 @@ +#!/usr/bin/env bash +# ============================================================================= +# tools/qemu.sh — Run raisernics in QEMU with various options +# +# Usage: +# ./tools/qemu.sh # serial console (no graphics) +# ./tools/qemu.sh --gui # graphical window +# ./tools/qemu.sh --debug # GDB stub on :1234 +# ./tools/qemu.sh --image foo.img # boot from raw disk image +# ./tools/qemu.sh --bios /path/to.fd # custom OVMF +# ./tools/qemu.sh --mem 512M # memory size +# ./tools/qemu.sh --smp 2 # SMP cores +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$SCRIPT_DIR" + +# Defaults +GUI_FLAG="" +DEBUG_FLAG="" +DRIVE_ARGS=(-drive file=fat:rw:esp,format=raw) +OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" +MEM="${MEM:-256M}" +SMP="${SMP:-1}" + +# Pre-build +make all + +# Parse args +while [ $# -gt 0 ]; do + case "$1" in + --gui | -g) + GUI_FLAG="-vga std" + shift + ;; + --debug | -d) + DEBUG_FLAG="-s -S" + shift + ;; + --image | -i) + _img_file="$(printf -- '-drive file=%s,format=raw,if=none,id=hd' "$2")" + DRIVE_ARGS=("$_img_file" '-device' 'ide-hd,drive=hd') + shift 2 + ;; + --bios | -b) + OVMF="$2" + shift 2 + ;; + --mem) + MEM="$2" + shift 2 + ;; + --smp) + SMP="$2" + shift 2 + ;; + --help | -h) + head -20 "$0" | grep "^# " | sed 's/^# //' + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +QEMU_CMD=( + qemu-system-x86_64 + -bios "$OVMF" + "${DRIVE_ARGS[@]}" + -serial stdio + -m "$MEM" + -machine q35 + -cpu qemu64 + -no-reboot + -smp "$SMP" +) + +[ -n "$GUI_FLAG" ] && QEMU_CMD+=("$GUI_FLAG") +if [ -n "$DEBUG_FLAG" ]; then + # Split the flag string safely into words + read -ra dbg <<< "$DEBUG_FLAG" + QEMU_CMD+=("${dbg[@]}") +fi + +echo " OVMF : $OVMF" +echo " Drive : ${DRIVE_ARGS[*]}" +echo " Mem : $MEM" +echo " SMP : $SMP" +echo " GDB : $([ -n "$DEBUG_FLAG" ] && echo ':1234' || echo 'off')" +echo " GUI : $([ -n "$GUI_FLAG" ] && echo 'yes' || echo 'no')" +echo "" + +exec "${QEMU_CMD[@]}" -- cgit v1.2.3