From 8dff3bd1d0fbd0156e88b4af3a8cdcb640f7dcb0 Mon Sep 17 00:00:00 2001 From: Noah Knegt Date: Mon, 21 Jul 2025 22:53:52 +0200 Subject: [PATCH] Add logging to the kernel Signed-off-by: Noah Knegt --- Cargo.lock | 56 +++++++++++ kernel/Cargo.toml | 14 +++ kernel/src/logger/framebuffer.rs | 154 +++++++++++++++++++++++++++++++ kernel/src/logger/logger.rs | 83 +++++++++++++++++ kernel/src/logger/mod.rs | 4 + kernel/src/main.rs | 21 ++++- 6 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 kernel/src/logger/framebuffer.rs create mode 100644 kernel/src/logger/logger.rs create mode 100644 kernel/src/logger/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 32ddbfa..6038381 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bincode" version = "1.3.3" @@ -99,6 +105,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "conquer-once" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6d3a9775a69f6d1fe2cc888999b67ed30257d3da4d2af91984e722f2ec918a" +dependencies = [ + "conquer-util", +] + +[[package]] +name = "conquer-util" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582" + [[package]] name = "crc" version = "3.3.0" @@ -192,6 +213,10 @@ name = "kernel" version = "0.1.0" dependencies = [ "bootloader_api", + "conquer-once", + "log", + "noto-sans-mono-bitmap", + "spinning_top", ] [[package]] @@ -212,6 +237,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -237,6 +272,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "noto-sans-mono-bitmap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27daf9557165efe1d09b52f97393bf6283cadb0a76fbe64a1061e15553a994a" + [[package]] name = "once_cell" version = "1.21.3" @@ -304,6 +345,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -345,6 +392,15 @@ dependencies = [ "serde", ] +[[package]] +name = "spinning_top" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" +dependencies = [ + "lock_api", +] + [[package]] name = "syn" version = "2.0.104" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index b1c9252..153370e 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -10,3 +10,17 @@ publish.workspace = true [dependencies] bootloader_api = { version = "0.11.10" } +conquer-once = { version = "0.3.2", default-features = false } +log = "0.4.17" +spinning_top = "0.2.4" + +[dependencies.noto-sans-mono-bitmap] +version = "0.2.0" +default-features = false +features = [ + "regular", + "size_16", + "unicode-basic-latin", + # required for the fallback char '�' + "unicode-specials", +] diff --git a/kernel/src/logger/framebuffer.rs b/kernel/src/logger/framebuffer.rs new file mode 100644 index 0000000..bfcb52a --- /dev/null +++ b/kernel/src/logger/framebuffer.rs @@ -0,0 +1,154 @@ +use bootloader_api::info::{FrameBufferInfo, PixelFormat}; +use core::{fmt, ptr}; +use font_constants::BACKUP_CHAR; +use noto_sans_mono_bitmap::{ + get_raster, get_raster_width, FontWeight, RasterHeight, RasterizedChar, +}; + +/// Additional vertical space between lines +const LINE_SPACING: usize = 2; +/// Additional horizontal space between characters. +const LETTER_SPACING: usize = 0; + +/// Padding from the border. Prevent that font is too close to border. +const BORDER_PADDING: usize = 1; + +/// Constants for the usage of the [`noto_sans_mono_bitmap`] crate. +mod font_constants { + use super::*; + + /// Height of each char raster. The font size is ~0.84% of this. Thus, this is the line height that + /// enables multiple characters to be side-by-side and appear optically in one line in a natural way. + pub const CHAR_RASTER_HEIGHT: RasterHeight = RasterHeight::Size16; + + /// The width of each single symbol of the mono space font. + pub const CHAR_RASTER_WIDTH: usize = get_raster_width(FontWeight::Regular, CHAR_RASTER_HEIGHT); + + /// Backup character if a desired symbol is not available by the font. + /// The '�' character requires the feature "unicode-specials". + pub const BACKUP_CHAR: char = '�'; + + pub const FONT_WEIGHT: FontWeight = FontWeight::Regular; +} + +/// Returns the raster of the given char or the raster of [`font_constants::BACKUP_CHAR`]. +fn get_char_raster(c: char) -> RasterizedChar { + fn get(c: char) -> Option { + get_raster( + c, + font_constants::FONT_WEIGHT, + font_constants::CHAR_RASTER_HEIGHT, + ) + } + get(c).unwrap_or_else(|| get(BACKUP_CHAR).expect("Should get raster of backup char.")) +} + +/// Allows logging text to a pixel-based framebuffer. +pub struct FrameBufferWriter { + framebuffer: &'static mut [u8], + info: FrameBufferInfo, + x_pos: usize, + y_pos: usize, +} + +impl FrameBufferWriter { + /// Creates a new logger that uses the given framebuffer. + pub fn new(framebuffer: &'static mut [u8], info: FrameBufferInfo) -> Self { + let mut logger = Self { + framebuffer, + info, + x_pos: 0, + y_pos: 0, + }; + logger.clear(); + logger + } + + fn newline(&mut self) { + self.y_pos += font_constants::CHAR_RASTER_HEIGHT.val() + LINE_SPACING; + self.carriage_return() + } + + fn carriage_return(&mut self) { + self.x_pos = BORDER_PADDING; + } + + /// Erases all text on the screen. Resets `self.x_pos` and `self.y_pos`. + pub fn clear(&mut self) { + self.x_pos = BORDER_PADDING; + self.y_pos = BORDER_PADDING; + self.framebuffer.fill(0); + } + + fn width(&self) -> usize { + self.info.width + } + + fn height(&self) -> usize { + self.info.height + } + + /// Writes a single char to the framebuffer. Takes care of special control characters, such as + /// newlines and carriage returns. + fn write_char(&mut self, c: char) { + match c { + '\n' => self.newline(), + '\r' => self.carriage_return(), + c => { + let new_xpos = self.x_pos + font_constants::CHAR_RASTER_WIDTH; + if new_xpos >= self.width() { + self.newline(); + } + let new_ypos = + self.y_pos + font_constants::CHAR_RASTER_HEIGHT.val() + BORDER_PADDING; + if new_ypos >= self.height() { + self.clear(); + } + self.write_rendered_char(get_char_raster(c)); + } + } + } + + /// Prints a rendered char into the framebuffer. + /// Updates `self.x_pos`. + fn write_rendered_char(&mut self, rendered_char: RasterizedChar) { + for (y, row) in rendered_char.raster().iter().enumerate() { + for (x, byte) in row.iter().enumerate() { + self.write_pixel(self.x_pos + x, self.y_pos + y, *byte); + } + } + self.x_pos += rendered_char.width() + LETTER_SPACING; + } + + fn write_pixel(&mut self, x: usize, y: usize, intensity: u8) { + let pixel_offset = y * self.info.stride + x; + let color = match self.info.pixel_format { + PixelFormat::Rgb => [intensity, intensity, intensity / 2, 0], + PixelFormat::Bgr => [intensity / 2, intensity, intensity, 0], + PixelFormat::U8 => [if intensity > 200 { 0xf } else { 0 }, 0, 0, 0], + other => { + // set a supported (but invalid) pixel format before panicking to avoid a double + // panic; it might not be readable though + self.info.pixel_format = PixelFormat::Rgb; + panic!("pixel format {:?} not supported in logger", other) + } + }; + let bytes_per_pixel = self.info.bytes_per_pixel; + let byte_offset = pixel_offset * bytes_per_pixel; + self.framebuffer[byte_offset..(byte_offset + bytes_per_pixel)] + .copy_from_slice(&color[..bytes_per_pixel]); + let _ = unsafe { ptr::read_volatile(&self.framebuffer[byte_offset]) }; + } +} + +unsafe impl Send for FrameBufferWriter {} +unsafe impl Sync for FrameBufferWriter {} + +impl fmt::Write for FrameBufferWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + for c in s.chars() { + self.write_char(c); + } + Ok(()) + } +} diff --git a/kernel/src/logger/logger.rs b/kernel/src/logger/logger.rs new file mode 100644 index 0000000..32cdeca --- /dev/null +++ b/kernel/src/logger/logger.rs @@ -0,0 +1,83 @@ +use bootloader_api::info::FrameBufferInfo; +use conquer_once::spin::OnceCell; +use core::fmt::Write; +use spinning_top::Spinlock; + +use super::*; + +/// The global logger instance used for the `log` crate. +static LOGGER: OnceCell = OnceCell::uninit(); + +/// A logger instance protected by a spinlock. +struct LockedLogger { + framebuffer: Option>, +} + +impl LockedLogger { + /// Create a new instance that logs to the given framebuffer. + pub fn new( + framebuffer: &'static mut [u8], + info: FrameBufferInfo, + ) -> Self { + let framebuffer = Some(Spinlock::new(framebuffer::FrameBufferWriter::new(framebuffer, info))); + + LockedLogger { + framebuffer, + } + } + + /// Force-unlocks the logger to prevent a deadlock. + /// + /// ## Safety + /// This method is not memory safe and should be only used when absolutely necessary. + pub unsafe fn force_unlock(&self) { + if let Some(framebuffer) = &self.framebuffer { + unsafe { framebuffer.force_unlock() }; + } + } +} + +impl log::Log for LockedLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if let Some(framebuffer) = &self.framebuffer { + let mut framebuffer = framebuffer.lock(); + writeln!(framebuffer, "{:5}: {}", record.level(), record.args()).unwrap(); + } + } + + fn flush(&self) {} +} + +/// Initialize a text-based logger using the given pixel-based framebuffer as output. +pub fn init_logger( + framebuffer: &'static mut [u8], + info: FrameBufferInfo, +) { + let logger = LOGGER.get_or_init(move || { + LockedLogger::new( + framebuffer, + info, + ) + }); + log::set_logger(logger).expect("logger already set"); + #[cfg(debug_assertions)] + log::set_max_level(log::LevelFilter::Debug); + + #[cfg(not(debug_assertions))] + log::set_max_level(log::LevelFilter::Info); + + log::debug!("Framebuffer info: {:?}", info); +} + +pub fn force_unlock() { + unsafe { + LOGGER + .get() + .map(|l| l.force_unlock()) + }; +} + diff --git a/kernel/src/logger/mod.rs b/kernel/src/logger/mod.rs new file mode 100644 index 0000000..fe29c1a --- /dev/null +++ b/kernel/src/logger/mod.rs @@ -0,0 +1,4 @@ +pub mod logger; +mod framebuffer; + +pub use logger::*; diff --git a/kernel/src/main.rs b/kernel/src/main.rs index d584a9f..4526e9c 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -1,16 +1,35 @@ #![no_std] #![no_main] -use bootloader_api::{entry_point, BootInfo}; +use bootloader_api::{ + config::{BootloaderConfig, Mapping}, + entry_point, BootInfo +}; + +mod logger; + +pub static BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::Dynamic); + config +}; entry_point!(kernel_main); fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + let info = boot_info.framebuffer.as_ref().unwrap().info(); + let buffer = boot_info.framebuffer.as_mut().unwrap().buffer_mut(); + + logger::init_logger(buffer, info); + + log::info!("Hello World from KERNEL"); loop {} } #[panic_handler] #[cfg(not(test))] fn panic(info: &core::panic::PanicInfo) -> ! { + logger::force_unlock(); + log::error!("{}", info); loop {} } -- 2.49.1