mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 22:37:49 +00:00
Update the VGA buffer post for the second edition
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
@@ -3,6 +3,7 @@ title = "VGA Text Mode"
|
||||
order = 3
|
||||
path = "vga-text-mode"
|
||||
date = 2018-02-26
|
||||
template = "second-edition/page.html"
|
||||
+++
|
||||
|
||||
The [VGA text mode] is a simple way to print text to the screen. In this post, we create an interface that makes its usage safe and simple, by encapsulating all unsafety in a separate module and providing support for Rust's [formatting macros].
|
||||
@@ -17,14 +18,8 @@ This blog is openly developed on [Github]. If you have any problems or questions
|
||||
[Github]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
|
||||
## Text Buffer Format
|
||||
To print a character to the screen in VGA text mode, one has to write it to the text buffer of the VGA hardware.
|
||||
|
||||
|
||||
|
||||
The text buffer is an array of 16-bit integers that describes the screen contents, line by line, tha
|
||||
|
||||
starts at physical address `0xb8000` and contains the characters displayed on screen. It has 25 rows and 80 columns. Each screen character has the following format:
|
||||
## The VGA Text Buffer
|
||||
To print a character to the screen in VGA text mode, one has to write it to the text buffer of the VGA hardware. The VGA text buffer is a two-dimensional array with typically 25 rows and 80 columns, which is directly rendered to the screen. Each array entry describes a single screen character through the following format:
|
||||
|
||||
Bit(s) | Value
|
||||
------ | ----------------
|
||||
@@ -46,19 +41,23 @@ Number | Color | Number + Bright Bit | Bright Color
|
||||
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 control if the text should blink. If you want to use a light background color (e.g. white) you have to disable blinking through a [BIOS function][disable blinking].
|
||||
Bit 4 is the _bright bit_, which turns for example blue into light blue.
|
||||
|
||||
[disable blinking]: http://www.ctyme.com/intr/rb-0117.htm
|
||||
The VGA text buffer is accessible via [memory-mapped I/O] to the address `0xb8000`. This means that reads and writes to that address don't access the RAM, but directly the text buffer on the VGA hardware. This means that we can read and write it through normal memory operations to that address.
|
||||
|
||||
## A basic Rust Module
|
||||
[memory-mapped I/O]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
|
||||
|
||||
Note that memory-mapped hardware might not support all normal RAM operations. For example, a device could only support byte-wise reads and return junk when an `u64` is read. Fortunately, the text buffer supports normal reads and writes (and also bitwise operations), so that we don't have to treat it in special way.
|
||||
|
||||
## A Rust Module
|
||||
Now that we know how the VGA buffer works, we can create a Rust module to handle printing:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
// in src/main.rs
|
||||
mod vga_buffer;
|
||||
```
|
||||
|
||||
The content of this module can live either in `src/vga_buffer.rs` or `src/vga_buffer/mod.rs`. The latter supports submodules while the former does not. But our module does not need any submodules so we create it as `src/vga_buffer.rs`.
|
||||
The content of this module can live either in `src/vga_buffer.rs` or `src/vga_buffer/mod.rs`. The latter supports submodules while the former does not. Our module does not need any submodules so we create it as `src/vga_buffer.rs`.
|
||||
|
||||
All of the code below goes into our new module (unless specified otherwise).
|
||||
|
||||
@@ -67,6 +66,7 @@ First, we represent the different colors using an enum:
|
||||
|
||||
```rust
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
@@ -93,11 +93,20 @@ We use a [C-like enum] here to explicitly specify the number for each color. Bec
|
||||
|
||||
Normally the compiler would issue a warning for each unused variant. By using the `#[allow(dead_code)]` attribute we disable these warnings for the `Color` enum.
|
||||
|
||||
By [deriving] the [`Copy`], [`Clone`] and [`Debug`] traits, we enable [copy semantics] for the type and make it printable.
|
||||
|
||||
[deriving]: http://rustbyexample.com/trait/derive.html
|
||||
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
|
||||
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
|
||||
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
|
||||
[copy semantics]: https://doc.rust-lang.org/book/first-edition/ownership.html#copy-types
|
||||
|
||||
To represent a full color code that specifies foreground and background color, we create a [newtype] on top of `u8`:
|
||||
|
||||
[newtype]: https://aturon.github.io/features/types/newtype.html
|
||||
[newtype]: https://rustbyexample.com/generics/new_types.html
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ColorCode(u8);
|
||||
|
||||
impl ColorCode {
|
||||
@@ -106,14 +115,20 @@ impl ColorCode {
|
||||
}
|
||||
}
|
||||
```
|
||||
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`.
|
||||
The `ColorCode` contains the full color byte, containing foreground and background color. Like before, we derive the `Copy` and `Debug` traits for it. 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/main.rs`:
|
||||
|
||||
[const function]: https://github.com/rust-lang/rfcs/blob/master/text/0911-const-fn.md
|
||||
[const function]: https://doc.rust-lang.org/unstable-book/language-features/const-fn.html
|
||||
|
||||
### The Text Buffer
|
||||
```rust
|
||||
// in src/main.rs
|
||||
#![feature(const_fn)]
|
||||
```
|
||||
|
||||
### Text Buffer
|
||||
Now we can add structures to represent a screen character and the text buffer:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
struct ScreenChar {
|
||||
ascii_character: u8,
|
||||
@@ -127,31 +142,25 @@ struct Buffer {
|
||||
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
Since the field ordering in default structs is undefined in Rust, we need the [repr(C\)] attribute. It guarantees that the struct's fields are laid out exactly like in a C struct and thus guarantees the correct field ordering.
|
||||
Since the field ordering in default structs is undefined in Rust, we need the [`repr(C)`] attribute. It guarantees that the struct's fields are laid out exactly like in a C struct and thus guarantees the correct field ordering.
|
||||
|
||||
[repr(C\)]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
|
||||
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
|
||||
|
||||
To actually write to screen, we now create a writer type:
|
||||
|
||||
```rust
|
||||
use core::ptr::Unique;
|
||||
|
||||
pub struct Writer {
|
||||
column_position: usize,
|
||||
color_code: ColorCode,
|
||||
buffer: Unique<Buffer>,
|
||||
buffer: &'static mut Buffer,
|
||||
}
|
||||
```
|
||||
The writer will always write to the last line and shift lines up when a line is full (or on `\n`). The `column_position` field keeps track of the current position in the last row. The current foreground and background colors are specified by `color_code` and a pointer to the VGA buffer is stored in `buffer`. To make it possible to create a `static` Writer later, the `buffer` field stores an `Unique<Buffer>` instead of a plain `*mut Buffer`. [Unique] is a wrapper that implements Send/Sync and is thus usable as a `static`. Since it's unstable, you may need to add the `unique` feature to `lib.rs`:
|
||||
The writer will always write to the last line and shift lines up when a line is full (or on `\n`). The `column_position` field keeps track of the current position in the last row. The current foreground and background colors are specified by `color_code` and a reference to the VGA buffer is stored in `buffer`. Note that we need an [explicit lifetime] here to tell the compiler how long the reference is valid. The [`'static`] lifetime specifies that the reference is valid for the whole program run time (which is true for the VGA text buffer).
|
||||
|
||||
[Unique]: https://doc.rust-lang.org/nightly/core/ptr/struct.Unique.html
|
||||
[explicit lifetime]: https://doc.rust-lang.org/book/first-edition/lifetimes.html#syntax
|
||||
[`'static`]: https://doc.rust-lang.org/book/first-edition/lifetimes.html#static
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
#![feature(unique)]
|
||||
```
|
||||
|
||||
## Printing Characters
|
||||
### Printing
|
||||
Now we can use the `Writer` to modify the buffer's characters. First we create a method to write a single ASCII byte (it doesn't compile yet):
|
||||
|
||||
```rust
|
||||
@@ -168,7 +177,7 @@ impl Writer {
|
||||
let col = self.column_position;
|
||||
|
||||
let color_code = self.color_code;
|
||||
self.buffer().chars[row][col] = ScreenChar {
|
||||
self.buffer.chars[row][col] = ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code: color_code,
|
||||
};
|
||||
@@ -177,10 +186,6 @@ impl Writer {
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer(&mut self) -> &mut Buffer {
|
||||
unsafe{ self.buffer.as_mut() }
|
||||
}
|
||||
|
||||
fn new_line(&mut self) {/* TODO */}
|
||||
}
|
||||
```
|
||||
@@ -190,85 +195,53 @@ If the byte is the [newline] byte `\n`, the writer does not print anything. Inst
|
||||
|
||||
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. Then it writes a new `ScreenChar` to the buffer at the current position. Finally, the current column position is advanced.
|
||||
|
||||
The `buffer()` auxiliary method converts the raw pointer in the `buffer` field into a safe mutable buffer reference. The unsafe block is needed because the [as_mut()] method of `Unique` is unsafe. But our `buffer()` method itself isn't marked as unsafe, so it must not introduce any unsafety (e.g. cause segfaults). To guarantee that, it's very important that the `buffer` field always points to a valid `Buffer`. It's like a contract that we must stand to every time we create a `Writer`. To ensure that it's not possible to create an invalid `Writer` from outside of the module, the struct must have at least one private field and public creation functions are not allowed either.
|
||||
|
||||
[as_mut()]: https://doc.rust-lang.org/nightly/core/ptr/struct.Unique.html#method.as_mut
|
||||
|
||||
### Cannot Move out of Borrowed Content
|
||||
When we try to compile it, we get the following error:
|
||||
|
||||
```
|
||||
error[E0507]: cannot move out of borrowed content
|
||||
--> src/vga_buffer.rs:79:34
|
||||
|
|
||||
79 | let color_code = self.color_code;
|
||||
| ^^^^ cannot move out of borrowed content
|
||||
```
|
||||
The reason it that Rust _moves_ values by default instead of copying them like other languages. And we cannot move `color_code` out of `self` because we only borrowed `self`. For more information check out the [ownership section] in the Rust book.
|
||||
|
||||
[ownership section]: https://doc.rust-lang.org/book/ownership.html
|
||||
[by reference]: http://rust-lang.github.io/book/ch04-02-references-and-borrowing.html
|
||||
|
||||
To fix it, we can implement the [Copy] trait for the `ColorCode` type. The easiest way to do this is to use the built-in [derive macro]:
|
||||
|
||||
[Copy]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
|
||||
[derive macro]: http://rustbyexample.com/trait/derive.html
|
||||
To print whole strings, we can convert them to bytes and print them one-by-one:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ColorCode(u8);
|
||||
impl Writer {
|
||||
pub fn write_str(&mut self, s: &str) {
|
||||
for byte in s.bytes() {
|
||||
self.write_byte(byte)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We also derive the [Clone] trait, since it's a requirement for `Copy`, and the [Debug] trait, which allows us to print this field for debugging purposes.
|
||||
|
||||
[Clone]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
|
||||
[Debug]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
|
||||
|
||||
Now our project should compile again.
|
||||
|
||||
However, the [documentation for Copy] says: _“if your type can implement Copy, it should”_. Therefore we also derive Copy for `Color` and `ScreenChar`:
|
||||
|
||||
[documentation for Copy]: https://doc.rust-lang.org/core/marker/trait.Copy.html#when-should-my-type-be-copy
|
||||
|
||||
```rust
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum Color {...}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
struct ScreenChar {...}
|
||||
```
|
||||
|
||||
### Try it out!
|
||||
#### Try it out!
|
||||
To write some characters to the screen, you can create a temporary function:
|
||||
|
||||
```rust
|
||||
pub fn print_something() {
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::LightGreen, Color::Black),
|
||||
buffer: unsafe { Unique::new_unchecked(0xb8000 as *mut _) },
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_str("ello");
|
||||
}
|
||||
```
|
||||
It just creates a new Writer that points to the VGA buffer at `0xb8000`. To use the unstable `Unique::new_unchecked` function, we need to add the feature flag `#![feature(const_unique_new)]` to the top of our `src/lib.rs`.
|
||||
It first creates a new Writer that points to the VGA buffer at `0xb8000`. The syntax for this might seem a bit strange: First, we cast the integer `0xb8000` as an mutable [raw pointer]. Then we convert it to a mutable reference by dereferencing it (through `*`) and immediately borrowing it again (through `&mut`). This conversion requires an [`unsafe` block], since the compiler can't guarantee that the raw pointer is valid.
|
||||
|
||||
Then it writes the byte `b'H'` to it. The `b` prefix creates a [byte character], which represents an ASCII code point. When we call `vga_buffer::print_something` in main, a `H` should be printed in the _lower_ left corner of the screen in light green:
|
||||
[raw pointer]: https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
|
||||
[`unsafe` block]: https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html#unsafe-rust
|
||||
|
||||
[byte character]: https://doc.rust-lang.org/reference.html#characters-and-strings
|
||||
Secondly, it writes the bytes `b'H'` and the string `"ello"'` to it. The `b` prefix creates a [byte literal], which represents an ASCII character. When we call `vga_buffer::print_something` in our `_start` function (in `src/main.rs`), a `Hello` should be printed in the _lower_ left corner of the screen in yellow:
|
||||
|
||||

|
||||
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals
|
||||
|
||||

|
||||
|
||||
When printing 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.
|
||||
|
||||
[core tracking issue]: https://github.com/rust-lang/rust/issues/27701
|
||||
[UTF-8]: http://www.fileformat.info/info/unicode/utf8.htm
|
||||
|
||||
### Volatile
|
||||
We just saw that our `H` was printed correctly. However, it might not work with future Rust compilers that optimize more aggressively.
|
||||
We just saw that our `Hello` was printed correctly. However, it might not work with future Rust compilers that optimize more aggressively.
|
||||
|
||||
The problem is that we only write to the `Buffer` and never read from it again. The compiler doesn't know about the side effect that some characters appear on the screen. So it might decide that these writes are unnecessary and can be omitted.
|
||||
|
||||
To avoid this erroneous optimization, we need to specify these writes as _[volatile]_. This tells the compiler that the write has side effects and should not be optimized away.
|
||||
The problem is that we only write to the `Buffer` and never read from it again. The compiler doesn't know that we really access VGA buffer memory (instead of normal RAM) and knows nothing about the side effect that some characters appear on the screen. So it might decide that these writes are unnecessary and can be omitted. To avoid this erroneous optimization, we need to specify these writes as _[volatile]_. This tells the compiler that the write has side effects and should not be optimized away.
|
||||
|
||||
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
|
||||
|
||||
@@ -284,18 +257,18 @@ We can add a dependency on the `volatile` crate by adding it to the `dependencie
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
volatile = "0.1.0"
|
||||
volatile = "0.2.3"
|
||||
```
|
||||
|
||||
The `0.1.0` is the [semantic] version number. For more information, see the [Specifying Dependencies] guide of the cargo documentation.
|
||||
The `0.2.3` is the [semantic] version number. For more information, see the [Specifying Dependencies] guide of the cargo documentation.
|
||||
|
||||
[semantic]: http://semver.org/
|
||||
[Specifying Dependencies]: http://doc.crates.io/specifying-dependencies.html
|
||||
|
||||
Now we've declared that our project depends on the `volatile` crate and are able to import it in `src/lib.rs`:
|
||||
Now we've declared that our project depends on the `volatile` crate and are able to import it in `src/main.rs`:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
// in src/main.rs
|
||||
|
||||
extern crate volatile;
|
||||
```
|
||||
@@ -339,28 +312,11 @@ impl Writer {
|
||||
|
||||
Instead of a normal assignment using `=`, we're now using the `write` method. This guarantees that the compiler will never optimize away this write.
|
||||
|
||||
## Printing Strings
|
||||
|
||||
To print whole strings, we can convert them to bytes and print them one-by-one:
|
||||
|
||||
```rust
|
||||
// in `impl Writer`
|
||||
pub fn write_str(&mut self, s: &str) {
|
||||
for byte in s.bytes() {
|
||||
self.write_byte(byte)
|
||||
}
|
||||
}
|
||||
```
|
||||
You can try it yourself in the `print_something` function.
|
||||
|
||||
When you print 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.
|
||||
|
||||
[core tracking issue]: https://github.com/rust-lang/rust/issues/27701
|
||||
[UTF-8]: http://www.fileformat.info/info/unicode/utf8.htm
|
||||
|
||||
### Support Formatting Macros
|
||||
### 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` that looks quite similar to our `write_str` method. To implement the trait, we just need to move it into an `impl fmt::Write for Writer` block and add a return type:
|
||||
|
||||
[core::fmt::Write]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
|
||||
|
||||
```rust
|
||||
use core::fmt;
|
||||
|
||||
@@ -378,81 +334,140 @@ The `Ok(())` is just a `Ok` Result containing the `()` type. We can drop the `pu
|
||||
Now we can use Rust's built-in `write!`/`writeln!` formatting macros:
|
||||
|
||||
```rust
|
||||
// in the `print_something` function
|
||||
use core::fmt::Write;
|
||||
let mut writer = Writer {...};
|
||||
writer.write_byte(b'H');
|
||||
writer.write_str("ello! ");
|
||||
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0);
|
||||
pub fn print_something() {
|
||||
use core::fmt::Write;
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_str("ello! ").unwrap();
|
||||
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
Now you should see a `Hello! The numbers are 42 and 0.3333333333333333` at the bottom of the screen.
|
||||
Now you should see a `Hello! The numbers are 42 and 0.3333333333333333` at the bottom of the screen. The `write!` call returns a `Result` which causes a warning if not used, so we call the [`unwrap`] function on it, which panics if an error occurs. This isn't a problem in our case, since writes to the VGA buffer never fail.
|
||||
|
||||
[core::fmt::Write]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
|
||||
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
|
||||
|
||||
### 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 an implementation for the `new_line` method of `Writer`:
|
||||
|
||||
```rust
|
||||
// in `impl Writer`
|
||||
|
||||
fn new_line(&mut self) {
|
||||
for row in 1..BUFFER_HEIGHT {
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
let buffer = self.buffer();
|
||||
let character = buffer.chars[row][col].read();
|
||||
buffer.chars[row - 1][col].write(character);
|
||||
impl Writer {
|
||||
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;
|
||||
}
|
||||
self.clear_row(BUFFER_HEIGHT-1);
|
||||
self.column_position = 0;
|
||||
}
|
||||
|
||||
fn clear_row(&mut self, row: usize) {/* TODO */}
|
||||
fn clear_row(&mut self, row: usize) {/* TODO */}
|
||||
}
|
||||
```
|
||||
We iterate over all screen characters and move each characters one row up. Note that the range notation (`..`) is exclusive the upper bound. We also omit the 0th row (the first range starts at `1`) because it's the row that is shifted off screen.
|
||||
|
||||
Now we only need to implement the `clear_row` method to finish the newline code:
|
||||
To finish the newline code, we add the `clear_row` method:
|
||||
|
||||
```rust
|
||||
// in `impl Writer`
|
||||
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);
|
||||
impl Writer {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
This method clears a row by overwriting all of its characters with a space character.
|
||||
|
||||
## Providing an Interface
|
||||
To provide a global writer that can used as an interface from other modules, we can add a `static` writer:
|
||||
## A Global Interface
|
||||
To provide a global writer that can used as an interface from other modules without carrying a `Writer` instance around, we try to create a static `WRITER`:
|
||||
|
||||
```rust
|
||||
pub static WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::LightGreen, Color::Black),
|
||||
buffer: unsafe { Unique::new_unchecked(0xb8000 as *mut _) },
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
```
|
||||
|
||||
But we can't use it to print anything! You can try it yourself in the `print_something` function. The reason is that we try to take a mutable reference (`&mut`) to a immutable `static` when calling `WRITER.print_byte`.
|
||||
However, if we try to compile it now, the following errors occur:
|
||||
|
||||
To resolve it, we could use a [mutable static]. But then every read and write to it would be unsafe since it could easily introduce data races and other bad things. Using `static mut` is highly discouraged, there are even proposals to [remove it][remove static mut].
|
||||
```
|
||||
error[E0396]: raw pointers cannot be dereferenced in statics
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
|
||||
|
||||
[mutable static]: https://doc.rust-lang.org/book/const-and-static.html#mutability
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
--> src/vga_buffer.rs:8:13
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
```
|
||||
|
||||
The problem is that Rust's const evaluator is not able to convert raw pointers to references at compile time. Maybe it will work someday when `const` functions become more powerful. But until then, we have to find another solution.
|
||||
|
||||
### Lazy Statics
|
||||
Fortunately the `lazy_static` macro exists. Instead of evaluating a `static` at compile time, the macro performs the initialization when the `static` is referenced the first time. Thus, we can do almost everything in the initialization block and are even able to read runtime values.
|
||||
|
||||
Let's add the `lazy_static` crate to our project:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
```
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies.lazy_static]
|
||||
version = "1.0"
|
||||
features = ["spin_no_std"]
|
||||
```
|
||||
We need the `spin_no_std` feature, since we don't link the standard library.
|
||||
|
||||
With `lazy_static`, we can define our static `WRITER` without problems:
|
||||
|
||||
```rust
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
However, this `WRITER` is pretty useless since it is immutable. This means that we can't write anything to it (since all the write methods take `&mut self`). One possible solution would be to use a [mutable static]. But then every read and write to it would be unsafe since it could easily introduce data races and other bad things. Using `static mut` is highly discouraged, there were even proposals to [remove it][remove static mut]. But what are the alternatives? We could try to use a immutable static with a cell type like [RefCell] or even [UnsafeCell] that provides [interior mutability]. But these types aren't [Sync] \(with good reason), so we can't use them in statics.
|
||||
|
||||
[mutable static]: https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
|
||||
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
|
||||
|
||||
But what are the alternatives? We could try to use a cell type like [RefCell] or even [UnsafeCell] to provide [interior mutability]. But these types aren't [Sync] \(with good reason), so we can't use them in statics.
|
||||
|
||||
[RefCell]: https://doc.rust-lang.org/nightly/core/cell/struct.RefCell.html
|
||||
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
|
||||
[interior mutability]: https://doc.rust-lang.org/book/mutability.html#interior-vs.-exterior-mutability
|
||||
[interior mutability]: https://doc.rust-lang.org/book/first-edition/mutability.html#interior-vs-exterior-mutability
|
||||
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
|
||||
|
||||
### Spinlocks
|
||||
To get synchronized interior mutability, users of the standard library can use [Mutex]. It provides mutual exclusion by blocking threads when the resource is already locked. But our basic kernel does not have any blocking support or even a concept of threads, so we can't use it either. However there is a really basic kind of mutex in computer science that requires no operating system features: the [spinlock]. Instead of blocking, the threads simply try to lock it again and again in a tight loop and thus burn CPU time until the mutex is free again.
|
||||
|
||||
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
|
||||
@@ -465,46 +480,48 @@ To use a spinning mutex, we can add the [spin crate] as a dependency:
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
rlibc = "0.1.4"
|
||||
spin = "0.4.5"
|
||||
spin = "0.4.6"
|
||||
```
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
// in src/main.rs
|
||||
extern crate spin;
|
||||
```
|
||||
|
||||
Then we can use the spinning Mutex to add interior mutability to our static writer:
|
||||
Then we can use the spinning Mutex to add safe [interior mutability] to our static `WRITER`:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs again
|
||||
use spin::Mutex;
|
||||
...
|
||||
pub static WRITER: Mutex<Writer> = Mutex::new(Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::LightGreen, Color::Black),
|
||||
buffer: unsafe { Unique::new_unchecked(0xb8000 as *mut _) },
|
||||
});
|
||||
```
|
||||
[Mutex::new] is a const function, too, so it can be used in statics.
|
||||
|
||||
Now we can easily print from our main function:
|
||||
|
||||
[Mutex::new]: https://docs.rs/spin/0.4.5/spin/struct.Mutex.html#method.new
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
pub extern fn rust_main() {
|
||||
use core::fmt::Write;
|
||||
vga_buffer::WRITER.lock().write_str("Hello again");
|
||||
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337);
|
||||
loop{}
|
||||
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) },
|
||||
});
|
||||
}
|
||||
```
|
||||
Note that we need to import the `Write` trait if we want to use its functions.
|
||||
Now we can delete the `print_something` function and print directly from our `_start` function:
|
||||
|
||||
## 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:
|
||||
```rust
|
||||
// in src/main.rs
|
||||
#[no_mangle]
|
||||
pub fn _start() -> ! {
|
||||
use core::fmt::Write;
|
||||
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
|
||||
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
We need to import the `fmt::Write` trait in order to be able to use its functions.
|
||||
|
||||
### Safety
|
||||
Note that we only have a single unsafe block in our code, which is needed to create a `Buffer` reference pointing to `0xb8000`. Afterwards, all operations are safe. Rust uses bounds checking for array accesses by default, so we can't accidentally write outside the buffer. Thus, we encoded the required conditions in the type system and are able to provide a safe interface to the outside.
|
||||
|
||||
### A println Macro
|
||||
Now that we have a global writer, we can add a `println` macro that can be used from anywhere in the codebase. 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 syntax]: https://doc.rust-lang.org/nightly/book/macros.html
|
||||
[`println!` macro]: https://doc.rust-lang.org/nightly/std/macro.println!.html
|
||||
@@ -528,102 +545,25 @@ macro_rules! print {
|
||||
```
|
||||
The macro expands to a call of the [`_print` function] in the `io` module. The [`$crate` variable] ensures that the macro also works from outside the `std` crate. For example, it expands to `::std` when it's used in other crates.
|
||||
|
||||
The [`format_args` macro] builds a [fmt::Arguments] type from the passed arguments, which is passed to `_print`. The [`_print` function] of libstd is rather complicated, as it supports different `Stdout` devices. We don't need that complexity since we just want to print to the VGA buffer.
|
||||
The [`format_args` macro] builds a [fmt::Arguments] type from the passed arguments, which is passed to `_print`. The [`_print` function] of libstd calls `print_to`, which is rather complicated because it supports different `Stdout` devices. We don't need that complexity since we just want to print to the VGA buffer.
|
||||
|
||||
[`_print` function]: https://github.com/rust-lang/rust/blob/46d39f3329487115e7d7dcd37bc64eea6ef9ba4e/src/libstd/io/stdio.rs#L631
|
||||
[`$crate` variable]: https://doc.rust-lang.org/book/macros.html#the-variable-crate
|
||||
[`_print` function]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
|
||||
[`$crate` variable]: https://doc.rust-lang.org/book/first-edition/macros.html#the-variable-crate
|
||||
[`format_args` macro]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
|
||||
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
|
||||
|
||||
To print to the VGA buffer, we just copy the `println!` macro and modify the `print!` macro to use our static `WRITER` instead of `_print`:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ({
|
||||
use core::fmt::Write;
|
||||
let mut writer = $crate::vga_buffer::WRITER.lock();
|
||||
writer.write_fmt(format_args!($($arg)*)).unwrap();
|
||||
});
|
||||
}
|
||||
```
|
||||
Instead of a `_print` function, we call the `write_fmt` method of our static `Writer`. Since we're using a method from the `Write` trait, we need to import it before. The additional `unwrap()` at the end panics if printing isn't successful. But since we always return `Ok` in `write_str`, that should not happen.
|
||||
|
||||
Note the additional `{}` scope around the macro: We write `=> ({…})` instead of `=> (…)`. The additional `{}` avoids that the `Write` trait is silently imported to the parent scope when `print` is used.
|
||||
|
||||
### Clearing the screen
|
||||
We can now use `println!` to add a rather trivial function to clear the screen:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
pub fn clear_screen() {
|
||||
for _ in 0..BUFFER_HEIGHT {
|
||||
println!("");
|
||||
}
|
||||
}
|
||||
```
|
||||
### Hello World using `println`
|
||||
To use `println` in `lib.rs`, we need to import the macros of the VGA buffer module first. Therefore we add a `#[macro_use]` attribute to the module declaration:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
#[macro_use]
|
||||
mod vga_buffer;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern fn rust_main() {
|
||||
// ATTENTION: we have a very small stack and no guard page
|
||||
vga_buffer::clear_screen();
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
loop{}
|
||||
}
|
||||
```
|
||||
|
||||
Since we imported the macros at crate level, they are available in all modules and thus provide an easy and safe interface to the VGA buffer.
|
||||
|
||||
As expected, we now see a _“Hello World!”_ on a cleared screen:
|
||||
|
||||

|
||||
|
||||
### Deadlocks
|
||||
Whenever we use locks, we must be careful to not accidentally introduce _deadlocks_. A [deadlock] occurs when a thread/program waits for a lock that will never be released. Normally, this happens when multiple threads access multiple locks. For example, when thread A holds lock 1 and tries to acquire lock 2 and -- at the same time -- thread B holds lock 2 and tries to acquire lock 1.
|
||||
|
||||
[deadlock]: https://en.wikipedia.org/wiki/Deadlock
|
||||
|
||||
However, a deadlock can also occur when a thread tries to acquire the same lock twice. This way we can trigger a deadlock in our VGA driver:
|
||||
|
||||
```rust
|
||||
// in rust_main in src/lib.rs
|
||||
|
||||
println!("{}", { println!("inner"); "outer" });
|
||||
```
|
||||
The argument passed to `println` is new block that resolves to the string _“outer”_ (a block always returns the result of the last expression). But before returning “outer”, the block tries to print the string _“inner”_.
|
||||
|
||||
When we try this code in QEMU, we see that neither of the strings are printed. To understand what's happening, we take a look at our `print` macro again:
|
||||
|
||||
```rust
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ({
|
||||
use core::fmt::Write;
|
||||
let mut writer = $crate::vga_buffer::WRITER.lock();
|
||||
writer.write_fmt(format_args!($($arg)*)).unwrap();
|
||||
});
|
||||
}
|
||||
```
|
||||
So we _first_ lock the `WRITER` and then we evaluate the arguments using `format_args`. The problem is that the argument in our code example contains another `println`, which tries to lock the `WRITER` again. So now the inner `println` waits for the outer `println` and vice versa. Thus, a deadlock occurs and the CPU spins endlessly.
|
||||
|
||||
### Fixing the Deadlock
|
||||
In order to fix the deadlock, we need to evaluate the arguments _before_ locking the `WRITER`. We can do so by moving the locking and printing logic into a new `print` function (like it's done in the standard library):
|
||||
To print to the VGA buffer, we just copy the `println!` and `print!` macros, but modify them to use a `print` function:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ({
|
||||
$crate::vga_buffer::print(format_args!($($arg)*));
|
||||
});
|
||||
($($arg:tt)*) => ($crate::vga_buffer::print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
macro_rules! println {
|
||||
($fmt:expr) => (print!(concat!($fmt, "\n")));
|
||||
($fmt:expr, $($arg:tt)*) => (print!(concat!($fmt, "\n"), $($arg)*));
|
||||
}
|
||||
|
||||
pub fn print(args: fmt::Arguments) {
|
||||
@@ -631,40 +571,35 @@ pub fn print(args: fmt::Arguments) {
|
||||
WRITER.lock().write_fmt(args).unwrap();
|
||||
}
|
||||
```
|
||||
Now the macro only evaluates the arguments (through `format_args!`) and passes them to the new `print` function. The `print` function then locks the `WRITER` and prints the formatting arguments using `write_fmt`. So now the arguments are evaluated before locking the `WRITER`.
|
||||
The `print` function locks our static `WRITER` and calls the `write_fmt` method on it. This method is from the `Write` trait, we need to import that trait. The additional `unwrap()` at the end panics if printing isn't successful. But since we always return `Ok` in `write_str`, that should not happen.
|
||||
|
||||
Thus, we fixed the deadlock:
|
||||
### Hello World using `println`
|
||||
To use `println` in `lib.rs`, we need to import the macros of the VGA buffer module first. Therefore we add a `#[macro_use]` attribute to the module declaration:
|
||||
|
||||

|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
We see that both “inner” and “outer” are printed.
|
||||
#[macro_use]
|
||||
mod vga_buffer;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern fn rust_main() {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Since we imported the macros at crate level, they are available in all modules and thus provide an easy and safe interface to the VGA buffer.
|
||||
|
||||
As expected, we now see a _“Hello World!”_ on the screen:
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
In this post we learned about the structure of the VGA text buffer and how it can be written through the memory mapping at address `0xb8000`. We created a Rust module that encapsulates the unsafety of writing to this memory mapped buffer and presents a safe and convenient interface to the outside.
|
||||
|
||||
We also saw how easy it is to add dependencies on third-party libraries, thanks to cargo. The two dependencies that we added, `lazy_static` and `spin`, are very useful in OS development and we will use them in more places in future posts.
|
||||
|
||||
## What's next?
|
||||
In the next posts we will map the kernel pages correctly so that accessing `0x0` or writing to `.rodata` is not possible anymore. To obtain the loaded kernel sections we will read the Multiboot information structure. Then we will create a paging module and use it to switch to a new page table where the kernel sections are mapped correctly.
|
||||
|
||||
The [next post] describes the Multiboot information structure and creates a frame allocator using the information about memory areas.
|
||||
|
||||
[next post]: ./posts/05-allocating-frames/index.md
|
||||
|
||||
## Other Rust OS Projects
|
||||
Now that you know the very basics of OS development in Rust, you should also check out the following projects:
|
||||
|
||||
- [Rust Bare-Bones Kernel]: A basic kernel with roughly the same functionality as ours. Writes output to the serial port instead of the VGA buffer and maps the kernel to the [higher half] \(instead of our identity mapping).
|
||||
_Note_: You need to [cross compile binutils] to build it (or you create some symbolic links[^fn-symlink] if you're on x86_64).
|
||||
|
||||
- [RustOS]: More advanced kernel that supports allocation, keyboard inputs, and threads. It also has a scheduler and a basic network driver.
|
||||
|
||||
- ["Tifflin" Experimental Kernel]: Big kernel project by thepowersgang, that is actively developed and has over 650 commits. It has a separate userspace and supports multiple file systems, even a GUI is included. Needs a cross compiler.
|
||||
|
||||
- [Redox]: Probably the most complete Rust OS today. It has an active community and over 1000 Github stars. File systems, network, an audio player, a picture viewer, and much more. Just take a look at the [screenshots][redox screenshots].
|
||||
|
||||
[Rust Bare-Bones Kernel]: https://github.com/thepowersgang/rust-barebones-kernel
|
||||
[higher half]: http://wiki.osdev.org/Higher_Half_Kernel
|
||||
[cross compile binutils]: ./extra/cross-compile-binutils.md
|
||||
[RustOS]: https://github.com/RustOS-Fork-Holding-Ground/RustOS
|
||||
["Tifflin" Experimental Kernel]:https://github.com/thepowersgang/rust_os
|
||||
[Redox]: https://github.com/redox-os/redox
|
||||
[redox screenshots]: https://github.com/redox-os/redox#what-it-looks-like
|
||||
|
||||
## Footnotes
|
||||
[^fn-symlink]: You will need to symlink `x86_64-none_elf-XXX` to `/usr/bin/XXX` where `XXX` is in {`as`, `ld`, `objcopy`, `objdump`, `strip`}. The `x86_64-none_elf-XXX` files must be in some folder that is in your `$PATH`. But then you can only build for your x86_64 host architecture, so use this hack only for testing.
|
||||
In the next post, we will explore _CPU exceptions_. These exceptions are thrown by the CPU when something illegal happens, such as a division by zero or an access to an unmapped memory page (a so-called “page fault”). Being able to catch and examine these exceptions is very important for debugging future errors. Exception handling is also very similar to the handling of hardware interrupts, which is required for keyboard support.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Reference in New Issue
Block a user