Impl VGA buffer, serial output, testing and basic interrupts
This commit is contained in:
parent
656d15e5d5
commit
a7f5b8476f
9 changed files with 498 additions and 16 deletions
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
24
Cargo.toml
24
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
|
||||
|
||||
|
|
33
src/interrupts.rs
Normal file
33
src/interrupts.rs
Normal 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
58
src/lib.rs
Normal 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();
|
||||
}
|
38
src/main.rs
38
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)
|
||||
}
|
||||
|
|
42
src/serial.rs
Normal file
42
src/serial.rs
Normal 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
38
src/test_harness.rs
Normal 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
175
src/vga_buffer.rs
Normal 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
33
tests/basic_boot.rs
Normal 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.
|
||||
//
|
Loading…
Reference in a new issue