From 41c87636e053b085350efd8e811f3f6c0965edf8 Mon Sep 17 00:00:00 2001 From: Philipp Oppermann Date: Sun, 30 Oct 2016 17:23:17 +0100 Subject: [PATCH] Improve wording and add some more QEMU screenshots --- blog/post/04-printing-to-screen.md | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/blog/post/04-printing-to-screen.md b/blog/post/04-printing-to-screen.md index c39ebaab..36661ce6 100644 --- a/blog/post/04-printing-to-screen.md +++ b/blog/post/04-printing-to-screen.md @@ -253,10 +253,12 @@ pub fn print_something() { writer.write_byte(b'H'); } ``` -It just creates a new Writer that points to the VGA buffer at `0xb8000`. 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. +It just creates a new Writer that points to the VGA buffer at `0xb8000`. 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: [byte character]: https://doc.rust-lang.org/reference.html#characters-and-strings +![QEMU output with a green `H` in the lower left corner](images/vga-H-lower-left.png) + ### Volatile We just saw that our `H` was printed correctly. However, it might not work with future Rust compilers that optimize more aggressively. @@ -371,15 +373,16 @@ 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 +{{< highlight rust "hl_lines=2 4 5 6" >}} // 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); -``` -Now you should see a `Hello! The numbers are 42 and 0.3333333333333333` in strange colors at the bottom of the screen. +{{< / highlight >}} + +Now you should see a `Hello! The numbers are 42 and 0.3333333333333333` at the bottom of the screen. [core::fmt::Write]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html @@ -558,7 +561,7 @@ pub fn clear_screen() { ### 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 +{{< highlight rust "hl_lines=3 9 10" >}} // in src/lib.rs #[macro_use] @@ -572,24 +575,29 @@ pub extern fn rust_main() { loop{} } -``` +{{< / highlight >}} + 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. -### Deadlock -Whenever 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. +As expected, we now see a _“Hello World!”_ on a cleared screen: + +![QEMU printing “Hello World!” on a cleared screen](images/vga-hello-world.png) + +### 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. Thus, we can trigger a deadlock in our VGA driver: +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 first argument 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”_. +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 it, we see that neither of the strings are printed. To understand what's happening, we take a look at our `print` macro again: +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 { @@ -600,10 +608,10 @@ macro_rules! print { }); } ``` -So we _first_ lock the `WRITER` and then we evaluate the arguments using `format_args`. The problem is that the first 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. +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 using the same approach as the standard library: +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): ```rust // in src/vga_buffer.rs @@ -619,12 +627,14 @@ pub fn print(args: fmt::Arguments) { WRITER.lock().write_fmt(args).unwrap(); } ``` -Now the macro evaluates the arguments (through `format_args!`) and passes it to the new `print` function. The function then locks the `WRITER` and prints the formatting arguments using `write_fmt`. +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`. -So the macro evaluates the arguments before locking the `WRITER` now. Thus, we fixed the deadlock problem: +Thus, we fixed the deadlock: ![QEMU printing “inner” and then “outer”](images/fixed-println-deadlock.png) +We see that both “inner” and “outer” are printed. + ## 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.