diff options
-rw-r--r-- | boards/qemu-virt/qemu-virt.s | 3 | ||||
-rw-r--r-- | flake.nix | 8 | ||||
-rw-r--r-- | kernel/Cargo.lock | 63 | ||||
-rw-r--r-- | kernel/Cargo.toml | 6 | ||||
-rw-r--r-- | kernel/default.nix | 1 | ||||
-rw-r--r-- | kernel/src/console.rs | 179 | ||||
-rw-r--r-- | kernel/src/lib.rs | 15 | ||||
-rw-r--r-- | kernel/src/panic.rs | 14 | ||||
-rw-r--r-- | kernel/src/util.rs | 50 |
9 files changed, 325 insertions, 14 deletions
diff --git a/boards/qemu-virt/qemu-virt.s b/boards/qemu-virt/qemu-virt.s index 4813b57..4628564 100644 --- a/boards/qemu-virt/qemu-virt.s +++ b/boards/qemu-virt/qemu-virt.s @@ -10,6 +10,9 @@ _start: # Set up hart0's stack. la sp, hart0_initial_stack_top + + # Call hart0_boot with the address of the DeviceTree. + c.mv a1, a0 call hart0_boot # Fall through to a spin loop. @@ -56,6 +56,11 @@ program = pkgsBuild.lib.getExe run-vm; }; + run = { + type = "app"; + program = pkgsBuild.lib.getExe run-vm; + }; + debug = { type = "app"; program = pkgsBuild.lib.getExe (pkgsBuild.writeShellApplication { @@ -78,7 +83,8 @@ rust-gdb ${packages.boards.qemu-virt}/kernel.elf \ -ex "target remote 127.0.0.1:$port" \ -ex "set riscv use-compressed-breakpoints yes" \ - -ex "layout src" \ + -ex "layout asm" \ + -ex "layout regs" \ -ex "focus cmd" \ -ex "tbreak hart0_boot" \ -ex "c" diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index 0dd1539..c4e38bd 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -3,5 +3,68 @@ version = 3 [[package]] +name = "contracts" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d1429e3bd78171c65aa010eabcdf8f863ba3254728dbfb0ad4b1545beac15c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "kernel" version = "0.1.0" +dependencies = [ + "contracts", + "log", + "spin", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index cef382e..d6449e8 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -7,6 +7,12 @@ edition = "2021" crate-type = ["staticlib"] [dependencies] +contracts = { version = "0.6.3", default-features = false } +log = { version = "0.4.20", default-features = false } +spin = { version = "0.9.8", default-features = false, features = ["mutex", "use_ticket_mutex"] } [profile.release] +codegen-units = 1 debug = true +lto = true +overflow-checks = true diff --git a/kernel/default.nix b/kernel/default.nix index 46352fb..0949b58 100644 --- a/kernel/default.nix +++ b/kernel/default.nix @@ -6,4 +6,5 @@ in rust.buildRustPackage { version = toml.package.version; src = ./.; cargoLock.lockFile = ./Cargo.lock; + dontFixup = true; } diff --git a/kernel/src/console.rs b/kernel/src/console.rs new file mode 100644 index 0000000..7364807 --- /dev/null +++ b/kernel/src/console.rs @@ -0,0 +1,179 @@ +//! The console subsystem, used for the kernel to log things. + +use core::fmt::Write; + +use crate::util::likely; +use contracts::{ensures, invariant}; +use log::Log; +use spin::Mutex; + +/// The console singleton. +static CONSOLE: Console = Console(Mutex::new(ConsoleInner::new())); + +/// Initializes the console subsystem. This must be done before any logging is +/// performed, and must +/// only be called once. +pub fn init() { + log::set_logger(&CONSOLE).expect("failed to set logger"); +} + +/// The point of interaction with the console. This is a singleton; see +/// `CONSOLE`. +struct Console(Mutex<ConsoleInner<4096>>); + +impl Log for Console { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let body = |line| { + let level = match record.level() { + log::Level::Error => "ERR", + log::Level::Warn => "WRN", + log::Level::Info => "INF", + log::Level::Debug => "DBG", + log::Level::Trace => "TRC", + }; + let file = record.file().unwrap_or("???"); + let args = record.args(); + + let result = if args.as_str() == Some("") { + // A silly convenience, but don't write the extra space if we're + // not going to write anything anyways. + writeln!(self.0.lock(), "[{level}][{file}:{line}]") + } else { + writeln!(self.0.lock(), "[{level}][{file}:{line}] {args}") + }; + + // Since the fmt::Write impl for ConsoleInner has a contract + // promising that it won't ever return an error, this should be + // unreachable. + result.expect("ConsoleInner failed to write"); + }; + + // Some contortions to avoid running afoul of the lifetime requirements + // of format_args... + match record.line() { + Some(line) => body(format_args!("{line}")), + None => body(format_args!("???")), + } + } + + fn flush(&self) { + todo!() + } +} + +/// The internals of the console. This is put behind a lock. +struct ConsoleInner<const BUFFER_SIZE: usize> { + /// The buffer that gets written to. Wraps on overflow. + buffer: [u8; BUFFER_SIZE], + + /// The number of bytes that have actually been written to. + len: usize, + + /// A flag for whether the log buffer has overflowed since the last time it + /// was cleared. + has_overflowed: bool, +} + +#[invariant(self.len <= BUFFER_SIZE)] +impl<const BUFFER_SIZE: usize> ConsoleInner<BUFFER_SIZE> { + const fn new() -> ConsoleInner<BUFFER_SIZE> { + ConsoleInner { + buffer: [0; BUFFER_SIZE], + len: 0, + has_overflowed: false, + } + } + + /// Writes bytes to the buffer, wrapping and setting the flag on overflow. + fn write(&mut self, bytes: &[u8]) { + // Check if there's enough room for the contents of the buffer. + let remaining = &mut self.buffer[self.len..]; + if likely(bytes.len() <= remaining.len()) { + // If there is, copy the whole buffer in. + remaining[..bytes.len()].copy_from_slice(bytes); + self.len += bytes.len(); + } else if bytes.len() >= BUFFER_SIZE { + // If not, and the bytes to write would fill up the entire buffer, + // just write over the whole thing. (This frees us from handling + // this case below.) + self.buffer + .copy_from_slice(&bytes[bytes.len() - BUFFER_SIZE..]); + self.len = BUFFER_SIZE; + self.has_overflowed = true; + } else { + // If there wasn't enough space, but there still will be enough + // space for some of the current contents, calculate how many bytes + // will be kept. + // + // This won't overflow (or even be zero), because we tested for + // that above. + let bytes_to_keep = BUFFER_SIZE - bytes.len(); + + // Copy those bytes down to the start of the buffer. + self.buffer + .copy_within(self.len - bytes_to_keep..self.len, 0); + + // Copy the new bytes to the end of the buffer. + self.buffer[bytes_to_keep..].copy_from_slice(bytes); + + // Update the length and mark the buffer as having overflowed. + self.len = BUFFER_SIZE; + self.has_overflowed = true; + } + } +} + +impl<const BUFFER_SIZE: usize> Write for ConsoleInner<BUFFER_SIZE> { + #[ensures(ret.is_ok())] + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.write(s.as_bytes()); + Ok(()) + } +} + +#[test] +fn console_handles_overflow() { + let mut console = ConsoleInner::<8>::new(); + assert_eq!(&console.buffer, b"\0\0\0\0\0\0\0\0"); + assert_eq!(console.len, 0); + assert_eq!(console.has_overflowed, false); + + console.write(b"Hello"); + assert_eq!(&console.buffer, b"Hello\0\0\0"); + assert_eq!(console.len, 5); + assert_eq!(console.has_overflowed, false); + + console.write(b", "); + assert_eq!(&console.buffer, b"Hello, \0"); + assert_eq!(console.len, 7); + assert_eq!(console.has_overflowed, false); + + console.write(b"world!"); + assert_eq!(&console.buffer, b", world!"); + assert_eq!(console.len, 8); + assert_eq!(console.has_overflowed, true); +} + +#[test] +fn console_handles_overflow_larger_than_buffer() { + let mut console = ConsoleInner::<8>::new(); + assert_eq!(&console.buffer, b"\0\0\0\0\0\0\0\0"); + assert_eq!(console.len, 0); + assert_eq!(console.has_overflowed, false); + + console.write("Hello, world!".as_bytes()); + assert_eq!(&console.buffer, b", world!"); + assert_eq!(console.len, 8); + assert_eq!(console.has_overflowed, true); +} + +/// The trait that a console backend must implement. +pub trait ConsoleBackend {} diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index bd5a19f..4a2483a 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -1,16 +1,19 @@ #![no_std] +pub mod console; +pub mod util; + +#[cfg(not(test))] mod panic; /// The entrypoint to the kernel. This should be executed by hart0 alone. It performs some early /// boot tasks, then wakes up any other harts. #[no_mangle] -pub extern "C" fn hart0_boot() { - for byte in "Hello, world!\n".bytes() { - unsafe { - core::ptr::write_volatile(0x10000000 as *mut u8, byte); - } - } +pub extern "C" fn hart0_boot(device_tree: *const u32) { + console::init(); + + log::info!("device_tree = {device_tree:?}"); + dbg!(42); todo!() } diff --git a/kernel/src/panic.rs b/kernel/src/panic.rs index ed6e5d4..9aabb01 100644 --- a/kernel/src/panic.rs +++ b/kernel/src/panic.rs @@ -1,12 +1,12 @@ -use core::panic::PanicInfo; +//! The kernel panic handler. + +use core::{arch::asm, panic::PanicInfo}; #[panic_handler] -fn panic(_info: &PanicInfo) -> ! { +fn panic(info: &PanicInfo) -> ! { + log::error!("{info:?}"); + loop { - for byte in "panic\n".bytes() { - unsafe { - core::ptr::write_volatile(0x10000000 as *mut u8, byte); - } - } + unsafe { asm!("wfi") } } } diff --git a/kernel/src/util.rs b/kernel/src/util.rs new file mode 100644 index 0000000..93c684d --- /dev/null +++ b/kernel/src/util.rs @@ -0,0 +1,50 @@ +//! Miscellaneous utilities. + +#[cold] +#[inline(always)] +fn cold() {} + +/// A hint that `b` is likely to be true. See `core::intrinsics::likely`. +#[inline(always)] +pub fn likely(b: bool) -> bool { + if !b { + cold() + } + b +} + +/// A hint that `b` is likely to be false. See `core::intrinsics::unlikely`. +#[inline(always)] +pub fn unlikely(b: bool) -> bool { + if b { + cold() + } + b +} + +/// A version of `std::dbg` built on top of `log::debug` instead of +/// `std::eprintln`. +/// +/// This code is copied from libstd, and inherits its copyright. +#[macro_export] +macro_rules! dbg { + // NOTE: We cannot use `concat!` to make a static string as a format + // argument of `log::debug!` because the `$expr` expression could be a + // block (`{ .. }`), in which case the format string will be malformed. + () => { + log::debug!("") + }; + ($expr:expr $(,)?) => { + // Use of `match` here is intentional because it affects the lifetimes + // of temporaries - https://stackoverflow.com/a/48732525/1063961 + match $expr { + tmp => { + log::debug!("{} = {:#?}", core::stringify!($expr), &tmp); + tmp + } + } + }; + ($($expr:expr),+ $(,)?) => { + ($($crate::dbg!($expr)),+,) + }; +} |