Impl VGA buffer, serial output, testing and basic interrupts

This commit is contained in:
Felix Suchert 2022-07-18 21:32:01 +02:00
parent 656d15e5d5
commit a7f5b8476f
Signed by: feliix42
GPG key ID: 24363525EA0E8A99
9 changed files with 498 additions and 16 deletions

73
Cargo.lock generated
View file

@ -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",
]

View file

@ -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

33
src/interrupts.rs Normal file
View file

@ -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();
}

58
src/lib.rs Normal file
View file

@ -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();
}

View file

@ -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)
}

42
src/serial.rs Normal file
View file

@ -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<SerialPort> = {
// 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)*));
}

38
src/test_harness.rs Normal file
View file

@ -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<T> Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::<T>());
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 {}
}

175
src/vga_buffer.rs Normal file
View file

@ -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<Writer> = 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<ScreenChar>; 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);
}
}

33
tests/basic_boot.rs Normal file
View file

@ -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.
//