Files
blog_os/_drafts/printing-to-screen.md
Philipp Oppermann 9d34da7c13 WIP
2015-09-15 15:26:31 +02:00

12 KiB

layout, title, category
layout title category
post Printing to Screen rust-os

TODO Introduction

The VGA Text Buffer

The text buffer starts at physical address 0xb8000 and contains the characters displayed on screen. It has 80 rows and 25 columns. Each screen character has the following format:

Bit(s) Value
0-7 ASCII code point
8-11 Foreground color
12-14 Background color
15 Blink

The following colors are available:

Number Color Number + Bright Bit Bright Color
0x0 Black 0x8 Dark Gray
0x1 Blue 0x9 Light Blue
0x2 Green 0xa Light Green
0x3 Cyan 0xb Light Cyan
0x4 Red 0xc Light Red
0x5 Magenta 0xd Light Magenta
0x6 Brown 0xe Yellow
0x7 Light Gray 0xf White

Bit 4 is the bright bit, which turns for example blue into light blue. It is unavailable in background color as the bit is used to enable blinking. However, it's possible to disable blinking through a BIOS function. Then the full 16 colors can be used as background.

Creating a Rust Module

Let's create the Rust module vga_buffer. Therefor we create a file named src/vga_buffer.rs and add a mod vga_buffer line to src/lib.rs. Now we can create an enum for the colors (full file):

#[repr(u8)]
pub enum Color {
    Black      = 0,
    Blue       = 1,
    ...
    Yellow     = 14,
    White      = 15,
}

We use a C-like enum here to explicitly specify the number for each color. Because of the repr(u8) attribute each enum variant is stored as an u8. Actually 4 bits would be sufficient, but Rust doesn't have an u4 type.

To represent a full color code that specifies foreground and background color, we create a newtype on top of u8:

struct ColorCode(u8);

impl ColorCode {
    const fn new(foreground: Color, background: Color) -> ColorCode {
        ColorCode((background as u8) << 4 | (foreground as u8))
    }
}

The ColorCode contains the full color byte, containing foreground and background color. Blinking is enabled implicitly by using a bright background color (soon we will disable blinking anyway). The new function is a const function to allow it in static initializers. As const functions are unstable we need to add the const_fn feature in src/lib.rs.

Now we can add structures to represent a screen character and the text buffer:

struct ScreenChar {
    ascii_character: u8,
    color_code: ColorCode,
}

const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;

struct Buffer {
    chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}

To ensure that ScreenChar is exactly 16-bits, one might be tempted to use the repr(packed) attribute. But Rust does not insert any padding around two u8 values, so it's not needed here. And repr(packed) can cause undefined behavior and that's always bad.

To actually write to screen, we now create a writer type:

pub struct Writer<'a> {
    column_position: usize,
    color_code: ColorCode,
    buffer: &'a mut Buffer,
}

The explicit lifetime tells Rust that the writer lives as long as the mutual buffer reference. Thus Rust ensures statically that the writer does not write to invalid memory.

The writer will always write to the last line and shift lines up when a line is full (or on \n). So the current row is always the last row and just the current column position needs to be stored. The current foreground and background colors are specified by color_code.

Printing Characters

Now we can use the Writer to modify the buffer's characters. First we create a method to write a single ASCII byte:

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;

                self.buffer.chars[row][col] = ScreenChar {
                    ascii_character: byte,
                    color_code: self.color_code,
                };
                self.column_position += 1;
            }
        }
    }
    fn new_line(&mut self) {}
}

If the byte is the newline byte \n, the writer does not print anything. Instead it calls a new_line method, which we'll implement later. Other bytes get printed to the screen in the second match case.

When printing a byte, the writer checks if the current line is full. In that case, a new_line call is required before to wrap the line. Since the writer always writes to the last line, row is just the last line's index. The writer uses the mutable reference stored in buffer to set the screen character at row and col. Then the column position is advanced by one.

To test it, we add can add a test function to the module:

pub fn test() {
    const BUFFER: *mut Buffer = 0xb8000 as *mut _;
    let buffer = unsafe{&mut *BUFFER};

    let writer = Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::LightGreen, Color::Black),
        buffer: buffer,
    };
    writer.write_byte(b'H');
}

First, we create a mutable reference to the VGA text buffer at 0xb8000. The const defines a raw pointer that we convert to a reference using the &mut * pattern. The unsafe block is needed because Rust doesn't know if the raw pointer is valid. Notice that creating a raw pointer is completely safe, only dereferencing it is unsafe. After creating the reference, we use it to create a new writer.

Finally, we write the byte b'H'. The b in front specifies that it's a byte character, which represents an ASCII code point. When we call vga_buffer::test in main, it's printed in the lower left corner of the screen in light green.

Printing Strings

To print whole strings, we can convert them to bytes and print them one-by-one:

pub fn write_str(&mut self, s: &str) {
    for byte in s.bytes() {
      self.write_byte(byte)
    }
}

You can try it yourself in the test function. When you try strings with some special characters like ä or λ, you'll notice that they cause weird symbols on screen. That's because they are represented by multiple bytes in UTF-8. By converting them to bytes, we of course get strange results. But since the VGA buffer doesn't support UTF-8, it's not possible to display these characters anyway. So let's just stick to ASCII strings for now.

Providing an Interface

Synchronization

Support Formatting Macros

It would be nice to support Rust's formatting macros, too. That way, we can easily print different types like integers or floats. To support them, we need to implement the core::fmt::Write trait. The only required method of this trait is write_str and looks quite similar to our write_str method. We just need to move it into an impl ::core::fmt::Write for Writer block and add a return type:

impl ::core::fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> ::core::fmt::Result {
        for byte in s.bytes() {
          self.write_byte(byte)
        }
        Ok(())
    }
}

The Ok(()) is just the Ok Result containing the () type. We can drop the pub because trait methods are always public.

Now we can use Rust's built-in write!/writeln! formatting macros:

...
let mut writer = Writer::new(Color::Blue, Color::LightGreen);
writer.write_byte(b'H');
writer.write_str("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0);

Now you should see a Hello! The numbers are 42 and 0.3333333333333333 in strange colors at the bottom of the screen.

Newlines

Right now, we just ignore newlines and characters that don't fit into the line anymore. Instead we want to move every character one line up (the top line gets deleted) and start at the beginning of the last line again. To do this, we add a new_line method to Writer:

fn new_line(&mut self) {
    for row in 0..(BUFFER_HEIGHT-1) {
        Self::buffer().chars[row] = Self::buffer().chars[row + 1]
    }
    self.clear_row(BUFFER_HEIGHT-1);
    self.column_position = 0;
}

We just move each line to the line above. Notice that the range notation (..) is exclusive the upper bound.

The clear_row method looks like this:

fn clear_row(&mut self, row: usize) {
    let blank = ScreenChar {
        ascii_character: b' ',
        color_code: self.color_code,
    };
    Self::buffer().chars[row] = [blank; BUFFER_WIDTH];
}

Now we just need to call the new_line() method in the 2 cases marked with //TODO and our writer supports newlines.

A println! macro

Rust's macro syntax is a bit strange, so we won't try to write a macro from scratch. Instead we look at the source of the println! macro in the standard library:

macro_rules! println {
    ($fmt:expr) => (print!(concat!($fmt, "\n")));
    ($fmt:expr, $($arg:tt)*) => (print!(concat!($fmt, "\n"), $($arg)*));
}

It just refers to the print! macro that is defined as:

macro_rules! print {
    ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

It just calls the print method in the io module of the current crate ($crate), which is std. The [_print function] is rather complicated, as it supports different Stdouts.

To print to the VGA buffer, we just copy both macros and replace the io module with the vga_buffer buffer in the print! macro:

macro_rules! print {
    ($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}

Now we can write our own _print function:

pub fn _print(fmt: ::core::fmt::Arguments) {
    use core::fmt::Write;
    static mut WRITER: Writer = Writer::new(Color::LightGreen, Color::Black);
    unsafe{WRITER.write_fmt(fmt)};
}

The function needs to be public because every print!(…) is expanded to ::vga_buffer::_print(…). It uses a static mut to store a writer and calls the write_fmt method of the core::fmt::Write trait (hence the import). It's highly discouraged to use static muts because they introduce all kinds of data races (that's why every access is unsafe). We use it here anyway, as we have only a single thread at the moment. But we already have another data race: We can create multiple Writers, that write to the same memory at 0xb8000. So as soon as we add multithreading, we need to revisit this module again and find better solutions.

Clearing the screen

We can now add a rather trivial last function:

pub fn clear_screen() {
    for _ in 0..BUFFER_HEIGHT {
        println!("");
    }
}

What's next?

Soon we will tackle virtual memory management and map the kernel sections correctly. This will cause many strange bugs and boot loops. To understand what's going on a real debugger is indispensable. In the [next post] we will setup [GDB] to work with QEMU.