summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Ringo <nathan@remexre.com>2024-02-25 01:18:10 -0600
committerNathan Ringo <nathan@remexre.com>2024-02-25 10:59:35 -0600
commit178cb5bb0755064a0824b1b3d564260db412ea92 (patch)
treec9917506abe9d2b9910f01a383b9320513a69c53
parentc8de43bf43242c4ebac3d0ecb8e7951fe2371506 (diff)
[console] Adds the start of a console subsystem to handle kernel logs.
The idea is that this is an output-only facility that log messages from the log crate are written to. It has a small buffer, and relies on a log destination regularly reading from the buffer. In the next commit, platform-specific code will be able to provide a "strict flush" routine, which runs after every write to the buffer and drains the buffer. This is mainly to help kernel development, where it's not assured that the kernel will even get as far in boot as initializing its allocator. Eventually, there will be a console fd that userspace can request, wait on, and read from.
-rw-r--r--boards/qemu-virt/qemu-virt.s3
-rw-r--r--flake.nix8
-rw-r--r--kernel/Cargo.lock63
-rw-r--r--kernel/Cargo.toml6
-rw-r--r--kernel/default.nix1
-rw-r--r--kernel/src/console.rs179
-rw-r--r--kernel/src/lib.rs15
-rw-r--r--kernel/src/panic.rs14
-rw-r--r--kernel/src/util.rs50
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.
diff --git a/flake.nix b/flake.nix
index 9e812b8..c2bcf13 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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)),+,)
+ };
+}