From a7f5b8476f79aaccb75b2fe986ee7fde0b95e436 Mon Sep 17 00:00:00 2001 From: Felix Wittwer Date: Mon, 18 Jul 2022 21:32:01 +0200 Subject: [PATCH] Impl VGA buffer, serial output, testing and basic interrupts --- Cargo.lock | 73 ++++++++++++++++++ Cargo.toml | 24 +++++- src/interrupts.rs | 33 +++++++++ src/lib.rs | 58 +++++++++++++++ src/main.rs | 38 ++++++---- src/serial.rs | 42 +++++++++++ src/test_harness.rs | 38 ++++++++++ src/vga_buffer.rs | 175 ++++++++++++++++++++++++++++++++++++++++++++ tests/basic_boot.rs | 33 +++++++++ 9 files changed, 498 insertions(+), 16 deletions(-) create mode 100644 src/interrupts.rs create mode 100644 src/lib.rs create mode 100644 src/serial.rs create mode 100644 src/test_harness.rs create mode 100644 src/vga_buffer.rs create mode 100644 tests/basic_boot.rs diff --git a/Cargo.lock b/Cargo.lock index 22b0362..f889cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,88 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bootloader" version = "0.9.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de78decc37247c7cfac5dbf3495c7298c6ac97cb355161caa7e15969c6648e6c" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + [[package]] name = "plain_os" version = "0.1.0" dependencies = [ "bootloader", + "lazy_static", + "spin", + "uart_16550", + "volatile 0.2.7", + "x86_64", +] + +[[package]] +name = "rustversion" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "uart_16550" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b074eb9300ad949edd74c529c0e8d451625af71bb948e6b65fe69f72dc1363d9" +dependencies = [ + "bitflags", + "rustversion", + "x86_64", +] + +[[package]] +name = "volatile" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b06ad3ed06fef1713569d547cdbdb439eafed76341820fb0e0344f29a41945" + +[[package]] +name = "volatile" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ca98349dda8a60ae74e04fd90c7fb4d6a4fbe01e6d3be095478aa0b76f6c0c" + +[[package]] +name = "x86_64" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "100555a863c0092238c2e0e814c1096c1e5cf066a309c696a87e907b5f8c5d69" +dependencies = [ + "bit_field", + "bitflags", + "rustversion", + "volatile 0.4.5", ] diff --git a/Cargo.toml b/Cargo.toml index ee7c812..d20a4a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,32 @@ resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.dev] -panic = "abort" # disable stack unwinding on panic +#panic = "abort" # disable stack unwinding on panic [profile.release] panic = "abort" # disable stack unwinding on panic [dependencies] +# FIXME(feliix42): Update to newer version bootloader = "0.9" + +# FIXME(feliix42): "newer versions of this library are not compatible with this post" +volatile = "0.2.6" +lazy_static = { version = "1.4", features = ["spin_no_std"] } +spin = "0.5.2" + +# Data structures for interrupt handling (Interrupt Descriptor Tables, ...) +x86_64 = "0.14.2" + +# Dependency for talking through the Serial Port +uart_16550 = "0.2.0" + +[package.metadata.bootimage] +# enables a special `isa-debug-exit` device in QEMU which allows us to terminate the OS from the outside +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", + "-display", "none" +] +# define the correct `success` exit code +test-success-exit-code = 33 # (0x10 << 1) | 1 + diff --git a/src/interrupts.rs b/src/interrupts.rs new file mode 100644 index 0000000..5438a13 --- /dev/null +++ b/src/interrupts.rs @@ -0,0 +1,33 @@ +use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame}; +use crate::println; +use lazy_static::lazy_static; + +// TODO(feliix42): For best experience, work through +// https://os.phil-opp.com/edition-1/extra/naked-exceptions/ for manual exception handling without +// the interrupt descriptor table :) + +// Creating the IDT is actually a bit tricky. The location of the IDT in memory needs to be static, +// as the CPU will store its address in memory. We could declare it `static mut`, but that would +// require the use of `unsafe {}` to mutate it, which is kinda meh. Hence, we use lazy_static, +// which allows us to create a static ref lazily. +lazy_static!{ + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt + }; +} + +/// Load the interrupt descriptor table. +pub fn init_idt() { + IDT.load(); +} + +extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { + println!("[EXCEPTION] BREAKPOINT\n{:#?}", stack_frame); +} + +#[test_case] +fn test_breakpoint_exception() { + x86_64::instructions::interrupts::int3(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9ac7855 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,58 @@ +#![no_std] +// in the test enviroment, there is still no main function here +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![feature(abi_x86_interrupt)] +#![test_runner(crate::test_harness::test_runner)] +// the `main` function generated by `cargo test` is never called due to our `no_main` macro +#![reexport_test_harness_main = "test_main"] + +pub mod interrupts; +pub mod serial; +pub mod test_harness; +pub mod vga_buffer; + +use core::panic::PanicInfo; +pub use test_harness::{test_runner, test_panic_handler}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +/// Exits QEMU and provides the supplied exit code. +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + // 0xf4 is a generally unused I/O port on x86's IO bus, so we may use it to send the + // shutdown signal (as defined in `Cargo.toml`). + let mut port = Port::new(0xf4); + + // writing to an IO port is generally unsafe. + port.write(exit_code as u32); + } +} + +/// Entry point for `cargo test` +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + init(); + test_main(); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + test_harness::test_panic_handler(info) +} + +/// Performs all kernel initialization +pub fn init() { + // load interrupt descriptor table + interrupts::init_idt(); +} diff --git a/src/main.rs b/src/main.rs index 5cec221..c66b48f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,36 @@ #![no_std] #![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(plain_os::test_runner)] +#![reexport_test_harness_main = "test_main"] use core::panic::PanicInfo; - -static HELLO: &[u8] = b"Hello World!"; - -/// This function defines the panic handler -#[panic_handler] -fn panic(_info: &PanicInfo) -> ! { - loop{} -} +use plain_os::println; // this function is the entry point, since the linker looks for a function // named `_start` by default #[no_mangle] pub extern "C" fn _start() -> ! { - let vga_buffer = 0xb8000 as *mut u8; + println!("Commencing boot process"); - for (i, &byte) in HELLO.iter().enumerate() { - unsafe { - *vga_buffer.offset(i as isize * 2) = byte; - *vga_buffer.offset(i as isize * 2 + 1) = 0xb; - } - } + plain_os::init(); + + #[cfg(test)] + test_main(); loop {} } + +/// This function defines the panic handler +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + plain_os::test_panic_handler(info) +} diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000..051af6f --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,42 @@ +//! Module for communication using the serial port. + +use lazy_static::lazy_static; +use spin::Mutex; +use uart_16550::SerialPort; + +// like with accessing the VGA Text buffer, guard the (global) serial port by a Mutex to make it +// globally available. +lazy_static! { + pub static ref SERIAL1: Mutex = { + // 0x3F8 is the standard port number for the first serial interface + let mut serial_port = unsafe { SerialPort::new(0x3F8) }; + serial_port.init(); + Mutex::new(serial_port) + }; +} + +/// Convenience function to print to the serial port +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + SERIAL1 + .lock() + .write_fmt(args) + .expect("Printing to serial console failed"); +} + +/// Prints to the host through the serial interface +#[macro_export] +macro_rules! serial_print { + ($($arg:tt)*) => { + $crate::serial::_print(format_args!($($arg)*)); + }; +} + +/// Prints to the host through the serial interface, ending with a newline. +#[macro_export] +macro_rules! serial_println { + () => ($crate::serial_print!("\n")); + ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n"))); + ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(concat!($fmt, "\n"), $($arg)*)); +} diff --git a/src/test_harness.rs b/src/test_harness.rs new file mode 100644 index 0000000..4778f3c --- /dev/null +++ b/src/test_harness.rs @@ -0,0 +1,38 @@ +//! The kernels testing harness. +//! +//! Defines a simple test runner and a corresponding panic handler that both communicate using the +//! serial interface of QEMU, printing test messages to the host system terminal. + +use crate::{serial_print, serial_println, QemuExitCode}; +use core::panic::PanicInfo; + +pub trait Testable { + fn run(&self) -> (); +} + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} + +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); + } + crate::exit_qemu(QemuExitCode::Success); +} + +/// Panic handler for testing environments (gives feedback via serial port) +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + crate::exit_qemu(QemuExitCode::Failed); + loop {} +} diff --git a/src/vga_buffer.rs b/src/vga_buffer.rs new file mode 100644 index 0000000..e6a0ec1 --- /dev/null +++ b/src/vga_buffer.rs @@ -0,0 +1,175 @@ +use core::fmt; +use volatile::Volatile; +use lazy_static::lazy_static; +use spin::Mutex; + +lazy_static! { + pub static ref WRITER: Mutex = Mutex::new(Writer { + column_position: 0, + color_code: ColorCode::new(Color::Yellow, Color::Black), + buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + }); +} + +/// A single screen character, laid out as defined in the [VGA Text +/// Mode](https://en.wikipedia.org/wiki/VGA_text_mode) specification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +struct ScreenChar { + ascii_character: u8, + color_code: ColorCode, +} + +const BUFFER_HEIGHT: usize = 25; +const BUFFER_WIDTH: usize = 80; + +/// Abstraction for a VGA screen buffer. +#[repr(transparent)] +struct Buffer { + chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +struct ColorCode(u8); + +impl ColorCode { + fn new(foreground: Color, background: Color) -> ColorCode { + ColorCode((background as u8) << 4 | (foreground as u8)) + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Color { + Black = 0, + Blue = 1, + Green = 2, + Cyan = 3, + Red = 4, + Magenta = 5, + Brown = 6, + LightGray = 7, + DarkGray = 8, + LightBlue = 9, + LightGreen = 10, + LightCyan = 11, + LightRed = 12, + Pink = 13, + Yellow = 14, + White = 15, +} + +pub struct Writer { + column_position: usize, + color_code: ColorCode, + buffer: &'static mut Buffer, +} + +impl Writer { + pub fn write_byte(&mut self, byte: u8) { + match byte { + b'\n' => self.new_line(), + byte => { + if self.column_position >= BUFFER_WIDTH { + self.new_line(); + } + + let row = BUFFER_HEIGHT - 1; + let col = self.column_position; + + let color_code = self.color_code; + self.buffer.chars[row][col].write(ScreenChar { + ascii_character: byte, + color_code, + }); + self.column_position += 1; + } + } + } + + /// Make room for a new line by moving up all output by 1 line and resetting the column + /// position to 1 + fn new_line(&mut self) { + for row in 1..BUFFER_HEIGHT { + for col in 0..BUFFER_WIDTH { + let character = self.buffer.chars[row][col].read(); + self.buffer.chars[row - 1][col].write(character); + } + } + + self.clear_row(BUFFER_HEIGHT - 1); + self.column_position = 0; + } + + fn clear_row(&mut self, row: usize) { + let blank = ScreenChar { + ascii_character: b' ', + color_code: self.color_code, + }; + + for col in 0..BUFFER_WIDTH { + self.buffer.chars[row][col].write(blank); + } + } + + pub fn write_string(&mut self, s: &str) { + for byte in s.bytes() { + match byte { + // printable ASCII byte or newline + 0x20..=0x7e | b'\n' => self.write_byte(byte), + // not part of printable ASCII range + _ => self.write_byte(0xfe), + } + } + } +} + +impl fmt::Write for Writer { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.write_string(s); + Ok(()) + } +} + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*))); +} + +#[macro_export] +macro_rules! println { + () => ($crate::print!("\n")); + ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*))); +} + +#[doc(hidden)] +pub fn _print(args: fmt::Arguments) { + use core::fmt::Write; + WRITER.lock().write_fmt(args).unwrap(); +} + +#[test_case] +fn test_println_simple() { + println!("test_println_simple output"); +} + +#[test_case] +fn test_println_many() { + for _ in 0..200 { + println!("test_println_many output"); + } +} + +#[test_case] +fn test_println_output() { + let s = "Some test string that fits on a single line"; + println!("{}", s); + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } +} + + diff --git a/tests/basic_boot.rs b/tests/basic_boot.rs new file mode 100644 index 0000000..2d327ba --- /dev/null +++ b/tests/basic_boot.rs @@ -0,0 +1,33 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(plain_os::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; +use plain_os::println; + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + plain_os::test_panic_handler(info) +} + +#[test_case] +fn test_println() { + println!("test_println output"); +} + +// TODO(feliix42): Future Test Ideas +// - CPU Exceptions: validate that performing invalid operations (e.g., division by zero) leads to +// the correct exception handlers being called +// - Page Tables: validate/verify page table operations +// - Userspace Programs:Test userspace programs trying to perform illegal operations and see +// whether the kernel prevents them. +//