10 Commits

Author SHA1 Message Date
1a41d087a9 Strucutring unit-tests
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 22:19:20 +02:00
bd035f1c9a Rename test to tests indicating that it will contain more than one test
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:30:20 +02:00
548acbbb5e Apply bootloader config correctly
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:27:01 +02:00
4605190a4e Rename logger to logging and fix formatting
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:21:51 +02:00
cd817d2013 Add dummy functoin that does a simple addition to verify that unit tests are working
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:12:45 +02:00
a91d52890a Move the qemu helper to the integration tests
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:12:13 +02:00
34283780c0 Create Qemu helpers
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:03:01 +02:00
f1e6ef14b2 Split into main and kernel lib
Signed-off-by: Noah Knegt <git@noahknegt.com>
2025-07-22 20:02:47 +02:00
4bd1e3bc6a feat: Add serial logging (#3)
Reviewed-on: #3
2025-07-22 19:17:20 +02:00
d8159a373e feat(kernel): Add logging to the kernel (#2)
This PR will allow the kernel to have a simple logging using a FrameBuffer created by the bootloader crate.

Reviewed-on: #2
2025-07-21 22:58:31 +02:00
11 changed files with 463 additions and 16 deletions

113
Cargo.lock generated
View File

@@ -17,6 +17,12 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@@ -26,6 +32,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -99,6 +111,21 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "conquer-once"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d008a441c0f269f36ca13712528069a86a3e60dffee1d98b976eb3b0b2160b4"
dependencies = [
"conquer-util",
]
[[package]]
name = "conquer-util"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582"
[[package]] [[package]]
name = "crc" name = "crc"
version = "3.3.0" version = "3.3.0"
@@ -192,6 +219,12 @@ name = "kernel"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bootloader_api", "bootloader_api",
"conquer-once",
"log",
"noto-sans-mono-bitmap",
"spinning_top",
"uart_16550",
"x86_64",
] ]
[[package]] [[package]]
@@ -212,6 +245,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" 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]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.27"
@@ -237,6 +280,12 @@ version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "noto-sans-mono-bitmap"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1064d564ae026ae123bf5d607318b42a5b31d70a3c48e6ea7ee44cce4cdb095e"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -279,6 +328,15 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "raw-cpuid"
version = "10.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.0.8" version = "1.0.8"
@@ -304,6 +362,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@@ -345,6 +409,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "spinning_top"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.104" version = "2.0.104"
@@ -395,6 +468,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "uart_16550"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e492212ac378a5e00da953718dafb1340d9fbaf4f27d6f3c5cab03d931d1c049"
dependencies = [
"bitflags 2.9.1",
"rustversion",
"x86",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -412,6 +496,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "volatile"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.2+wasi-0.2.4" version = "0.14.2+wasi-0.2.4"
@@ -642,3 +732,26 @@ checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "x86"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2781db97787217ad2a2845c396a5efe286f87467a5810836db6d74926e94a385"
dependencies = [
"bit_field",
"bitflags 1.3.2",
"raw-cpuid",
]
[[package]]
name = "x86_64"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae"
dependencies = [
"bit_field",
"bitflags 2.9.1",
"rustversion",
"volatile",
]

View File

@@ -9,11 +9,15 @@ fn main() {
// create an UEFI disk image (optional) // create an UEFI disk image (optional)
let uefi_path = out_dir.join("uefi.img"); let uefi_path = out_dir.join("uefi.img");
bootloader::UefiBoot::new(&kernel).create_disk_image(&uefi_path).unwrap(); bootloader::UefiBoot::new(&kernel)
.create_disk_image(&uefi_path)
.unwrap();
// create a BIOS disk image // create a BIOS disk image
let bios_path = out_dir.join("bios.img"); let bios_path = out_dir.join("bios.img");
bootloader::BiosBoot::new(&kernel).create_disk_image(&bios_path).unwrap(); bootloader::BiosBoot::new(&kernel)
.create_disk_image(&bios_path)
.unwrap();
// pass the disk image paths as env variables to the `main.rs` // pass the disk image paths as env variables to the `main.rs`
println!("cargo:rustc-env=UEFI_PATH={}", uefi_path.display()); println!("cargo:rustc-env=UEFI_PATH={}", uefi_path.display());

View File

@@ -10,3 +10,19 @@ publish.workspace = true
[dependencies] [dependencies]
bootloader_api = { version = "0.11.10" } bootloader_api = { version = "0.11.10" }
conquer-once = { version = "0.4.0", default-features = false }
log = "0.4.17"
spinning_top = "0.3.0"
uart_16550 = "0.3.2"
x86_64 = "0.15.2"
[dependencies.noto-sans-mono-bitmap]
version = "0.3.1"
default-features = false
features = [
"regular",
"size_16",
"unicode-basic-latin",
# required for the fallback char '<27>'
"unicode-specials",
]

13
kernel/src/adder.rs Normal file
View File

@@ -0,0 +1,13 @@
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn add_test() {
assert_eq!(add(1, 2), 3);
}
}

30
kernel/src/lib.rs Normal file
View File

@@ -0,0 +1,30 @@
#![cfg_attr(not(test), no_std)]
use bootloader_api::BootInfo;
mod logging;
mod adder;
pub fn 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();
logging::init_logger(buffer, info);
log::info!("Hello World from KERNEL");
log::debug!("ADDING 1 & 6, result == {}", adder::add(1, 6));
// Endless loop as the kernel must stay running
#[allow(clippy::empty_loop)]
loop {}
}
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
logging::force_unlock();
log::error!("{info}");
#[allow(clippy::empty_loop)]
loop {}
}

View File

@@ -0,0 +1,154 @@
use bootloader_api::info::{FrameBufferInfo, PixelFormat};
use core::{fmt, ptr};
use font_constants::BACKUP_CHAR;
use noto_sans_mono_bitmap::{
FontWeight, RasterHeight, RasterizedChar, get_raster, get_raster_width,
};
/// 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 '<27>' character requires the feature "unicode-specials".
pub const BACKUP_CHAR: char = '<27>';
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<RasterizedChar> {
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 {other:?} not supported in logger")
}
};
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(())
}
}

View File

@@ -0,0 +1,82 @@
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<LockedLogger> = OnceCell::uninit();
/// A logger instance protected by a spinlock.
struct LockedLogger {
framebuffer: Option<Spinlock<framebuffer::FrameBufferWriter>>,
serial: Option<Spinlock<serial::SerialPort>>,
}
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,
)));
let serial = Some(Spinlock::new(unsafe { serial::SerialPort::init() }));
LockedLogger {
framebuffer,
serial,
}
}
/// 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() };
}
if let Some(serial) = &self.serial {
unsafe { serial.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();
}
if let Some(serial) = &self.serial {
let mut serial = serial.lock();
writeln!(serial, "{: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()) };
}

View File

@@ -0,0 +1,6 @@
pub mod logger;
mod framebuffer;
mod serial;
pub use logger::*;

View File

@@ -0,0 +1,28 @@
use core::fmt;
pub struct SerialPort {
port: uart_16550::SerialPort,
}
impl SerialPort {
/// # Safety
///
/// unsafe because this function must only be called once
pub unsafe fn init() -> Self {
let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) };
port.init();
Self { port }
}
}
impl fmt::Write for SerialPort {
fn write_str(&mut self, s: &str) -> fmt::Result {
for char in s.bytes() {
match char {
b'\n' => self.port.write_str("\r\n").unwrap(),
byte => self.port.send(byte),
}
}
Ok(())
}
}

View File

@@ -1,16 +1,15 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
use bootloader_api::{entry_point, BootInfo}; use bootloader_api::{
config::{BootloaderConfig, Mapping},
entry_point,
};
entry_point!(kernel_main); static BOOTLOADER_CONFIG: BootloaderConfig = {
let mut config = BootloaderConfig::new_default();
config.mappings.physical_memory = Some(Mapping::Dynamic);
config
};
fn kernel_main(boot_info: &'static mut BootInfo) -> ! { entry_point!(kernel::main, config = &BOOTLOADER_CONFIG);
loop {}
}
#[panic_handler]
#[cfg(not(test))]
fn panic(info: &core::panic::PanicInfo) -> ! {
loop {}
}

View File

@@ -4,16 +4,18 @@ fn main() {
// read env variables that were set in build script // read env variables that were set in build script
let uefi_path = env!("UEFI_PATH"); let uefi_path = env!("UEFI_PATH");
let bios_path = env!("BIOS_PATH"); let bios_path = env!("BIOS_PATH");
// choose whether to start the UEFI or BIOS image // choose whether to start the UEFI or BIOS image
let uefi = true; let uefi = true;
let mut cmd = std::process::Command::new("qemu-system-x86_64"); let mut cmd = std::process::Command::new("qemu-system-x86_64");
if uefi { if uefi {
cmd.arg("-bios").arg(ovmf_prebuilt::ovmf_pure_efi()); cmd.arg("-bios").arg(ovmf_prebuilt::ovmf_pure_efi());
cmd.arg("-drive").arg(format!("format=raw,file={uefi_path}")); cmd.arg("-drive")
.arg(format!("format=raw,file={uefi_path}"));
} else { } else {
cmd.arg("-drive").arg(format!("format=raw,file={bios_path}")); cmd.arg("-drive")
.arg(format!("format=raw,file={bios_path}"));
} }
let mut child = cmd.spawn().unwrap(); let mut child = cmd.spawn().unwrap();
child.wait().unwrap(); child.wait().unwrap();