Demilade Sonuga's blog

All posts

Panicking II

2023-01-02

One potential way of resolving the uselessness of our panic handler is to try to extract the string chunks and arguments from the struct directly and print them one by one. This won't work because, as it is clearly in the

docs, the Arguments struct has no public fields. The only public method it offers is the as_str function, which won't cut it for us because it can't handle format string arguments.

The key to effectively using the Arguments struct lies in a trait: core::fmt::Write.

If your memory of traits is hazy, all you need to remember is that traits are just a mechanism for defining shared behavior.

Understanding Traits

For example, consider the following structs:

struct Dog {
    fn bark(&self) {
        println!("bark");
    }
}

struct Cow {
    fn moo(&self) {
        println!("moo");
    }
}

struct Cat {
    fn meow(&self) {
        println!("meow");
    }
}

In our conceptual understanding of animals, we all know that animals make sounds. Clearly, from the names, we see that the structs represent some animals and the functions they have are used solely for making sounds.

If we want to create a function that can take an animal and make a sound, then we'll have to create separate functions for each of the animals:

fn animal_dog(dog: Dog) {
    dog.bark();
}

fn animal_cow(cow: Cow) {
    cow.moo();
}

fn animal_cat(cat: Cat) {
    cat.meow();
}

This is a contrived example, but I don't think it's hard to see how this problem could play out in a real code scenario.

What we just need here is a function that can take anything that behaves like an animal, and invoke the make sound behavior. In other words, something like this:

fn animal(animal: Anything that behaves like an animal)
    animal.make_sound()

In Rust, one mechanism we can use to do this is the trait system.

Some things behave like animals: we create a trait called Animal. All animals make sounds: the Animal trait will have a make_sound function.

trait Animal {
    fn make_sound(&self);
}

Now, anything that can behave like an animal simply implements the Animal trait:

struct Dog;

impl Animal for Dog {
    fn make_sound(&self) {
        println!("bark");
    }
}

struct Cow;

impl Animal for Cow {
    fn make_sound(&self) {
        println!("moo");
    }
}

struct Cat;

impl Animal for Cat {
    fn make_sound(&self) {
        println!("meow");
    }
}

And we can write our animal function like so:

fn animal<T: Animal>(animal: T) {
    animal.make_sound();
}

Now, the function can take anything that behaves like an animal and make its sound, whether it's a dog or a cat or a cow or any other animal that floats your boat.

Another thing all animals do is die. In code:

trait Animal {
    fn make_sound(&self);

    fn die(&self);
}

struct Dog;

impl Animal for Dog {
    fn make_sound(&self) {
        println!("bark");
    }

    fn die(&self) {
        println!("died");
    }
}

struct Cow;

impl Animal for Cow {
    fn make_sound(&self) {
        println!("moo");
    }

    fn die(&self) {
        println!("died");
    }
}

struct Cat;

impl Animal for Cat {
    fn make_sound(&self) {
        println!("meow");
    }

    fn die(&self) {
        println!("died");
    }
}

All the animals die the same way. Instead of repeating the die implementation for each animal, we can just include a default implementation in the Animal trait:

trait Animal {
    fn make_sound(&self);

    fn die(&self) {
        println!("died");
    }
}

Now, whenever the Animal trait is implemented for a thing, only the make_sound function needs to be written. All the animals will automatically have the die implementation provided.

The core::fmt::Write trait

In the same way that the Animal trait indicates anything that behaves like an animal, the Write trait indicates anything that characters can be written into.

The declaration for the Write trait looks like this in the docs:

pub trait Write {
    fn write_str(&mut self, s: &str) -> Result;

    fn write_char(&mut self, c: char) -> Result { ... }
    fn write_fmt(&mut self, args: Arguments<'_>) -> Result { ... }
}

This means that anything that implements the Write trait must provide a write_str function. The write_fmt and write_char functions have default implementations, so the only function we need to provide is write_str.

Taking a good look at the arguments of these functions, we see that the last one takes an instance of Arguments. This write_fmt function handles the gluing together of the pieces of a format string and its arguments and prints out the resulting character sequence. This is exactly what we need and all we need to do to use it is to implement write_str.

But implement Write for what? Remember that this is a trait. Traits define behavior for other things. So, to make use of the write_fmt function, we first have to implement this trait for something.

The Write trait signifies something that can be written into. Since we aim to write on the screen, it will be reasonable to implement Write for Screen. This makes sense because we can view the screen as some kind of object which characters are being written into.

In uefi.rs:

use crate::FONT;
use core::fmt::Write; // NEW

// ... Others

pub const NO_OF_PIXELS_IN_A_ROW: usize = 640;
pub const NO_OF_PIXELS_IN_A_COLUMN: usize = 480;

pub struct Screen {
    pub pixels: [[Pixel; NO_OF_PIXELS_IN_A_ROW]; NO_OF_PIXELS_IN_A_COLUMN],
}

// NEW:
// This implementation makes sense because we can view
// the screen as an object which characters are being written
// into
impl Write for Screen {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        print_str(self, s);
        Ok(())
    }
}

This is all we need to start using write_fmt with Screen.

In our main.rs:

mod font;
use font::FONT;

mod uefi;
use uefi::{SystemTable, Screen, PixelFormat, Pixel, pre_graphics_print_str, print_str, printint, 
NO_OF_PIXELS_IN_A_ROW};

mod bitmap;
use bitmap::{FileHeader, DIBHeader, ColorTable, Color, Bitmap, draw_bitmap, erase_bitmap};

// This needs to be imported so that the functions it defines may
// be used
use core::fmt::Write; // NEW

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    // ... Others

    while block_position.1 < NO_OF_PIXELS_IN_A_ROW {
        panic!("{}", "formatted");
        draw_bitmap(screen, &block, block_position);
        erase_bitmap(screen, &block, block_position);
        block_position.1 += 1;
    }

    // ... Others
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    let screen = unsafe { SCREEN.as_mut().unwrap() };
    if let Some(panic_msg) = panic_info.message() {
        // DELETED: print_str(screen, panic_msg.as_str().unwrap());
        screen.write_fmt(*panic_msg); // NEW
    } else {
        // DELETED: print_str(screen, "Message returned None for some unknown reason");
        screen.write_str("Message returned None for some unknown reason");
    }
    loop {}
}

Upon running this, you'll have the following output:

Formatted

This is a big improvement over the useless handler we had before because now, we can print the values of variables and things on screen with the format arguments.

There are still a few problems we need to address.

Try this:

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    // ... Others

    while block_position.1 < NO_OF_PIXELS_IN_A_ROW {
        panic!("A number: {}", 2);
        draw_bitmap(screen, &block, block_position);
        erase_bitmap(screen, &block, block_position);
        block_position.1 += 1;
    }

    // ... Others
}

And you'll get:

Overflow Error

Instead of panicking with "A number: 2", we panic with "Attempt to subtract with overflow".

To understand why we're getting this error, we have to trace back the steps of execution in our code to the point where a subtraction took place.

We start here:

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    let screen = unsafe { SCREEN.as_mut().unwrap() };
    if let Some(panic_msg) = panic_info.message() {
        screen.write_fmt(*panic_msg);
    } else {
        screen.write_str("Message returned None for some unknown reason");
    }
    loop {}
}

Why do we start here? Well, without the panic! invocation in the while loop in efi_main, an animation of a block on the screen will be displayed first, before anything else. The error must have taken place in the panic handler.

The question now is, where exactly in the handler did this error occur? Was it in the write_fmt invocation or the write_str invocation?

The answer is write_str. It has to be write_str because, if you can remember, we didn't write our write_fmt implementation. We're using the default battle-tested one. This write_fmt function makes use of our write_str implementation, which we wrote ourselves. So, this must be (should be (anything is possible)) where the problem is.

Our write_str implementation:

impl Write for Screen {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        print_str(self, s);
        Ok(())
    }
}

All this function does is call print_str, so the problem should be from there.

print_str:

pub fn print_str(screen: &mut Screen, s: &str) {
    let mut screen_pos = (0, 0);
    for c in s.as_bytes() {
        print_char(screen, &FONT[char_to_font_index(*c)], screen_pos);
        screen_pos.1 += 16;
        if screen_pos.1 >= NO_OF_PIXELS_IN_A_ROW {
            screen_pos.0 += 16;
            screen_pos.1 = 0;
        }
        if screen_pos.0 >= NO_OF_PIXELS_IN_A_COLUMN {
            break;
        }
    }
}

The problem is not here, because there is no subtraction in this function. The only other functions called from here s's as_bytes, print_char and char_to_font_index. The as_bytes function is a battle-tested function from the core library, so it's probably not the problem.

The print_char function:

pub fn print_char(
    screen: &mut Screen,
    font_description: &[[bool; 16]; 16],
    curr_screen_pos: (usize, usize),
) {
    for i in 0..16 {
        for j in 0..16 {
            if font_description[i][j] {
                screen.pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                    red: 255,
                    green: 255,
                    blue: 0,
                    reserved: 0,
                };
            } else {
                screen.pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                    red: 0,
                    green: 0,
                    blue: 0,
                    reserved: 0,
                };
            }
        }
    }
}

There is no subtraction here, so this is definitely not it.

The char_to_font_index function:

fn char_to_font_index(c: u8) -> usize {
    if c >= 97 {
        // Small letters to big letters
        char_to_font_index(c - 32)
    } else if c == 32 {
        // Space
        26
    } else if c == 33 {
        // Exclamation mark
        27
    } else {
        // FONT array index for big letters
        (c - 65) as usize
    }
}

There is subtraction here and no other functions are called, so this is probably where the problem is. To figure out whether or not the problem is really from here, we have to remember what exactly we did to trigger the error. When we panicked with panic!("A panic occurred {}", "here"), there were no errors. But when we panicked with panic!("A number: {}", 2), we have this subtraction overflow error. This leads us to hypothesize that argument 2 is the cause of the problem.

If argument 2 really is the cause of the problem, then we need to see how exactly it causes this problem. In this function, the c variable is the Unicode scalar value of the character to be printed.

Consider a typical execution of this function for c == 97, the value for the letter 'a'. Since 97 >= 97, the function is called again with c == 97 - 32 == 65. In the second call, the last block of code in the else block is executed and 65 - 65 == 0 is returned.

The Unicode scalar value for number 2 is 50. When this function is called with c == 50, all the if-conditions fail until the last else block is reached. 50 - 65 == -15. Since u8 is an unsigned integer, -15 can't be represented with it, hence the "Attempt to subtract with overflow" error.

Now, we have to ask ourselves, why exactly do we even have this problem? What was the purpose of this function in the first place?

Going back to print_str:

pub fn print_str(screen: &mut Screen, s: &str) {
    let mut screen_pos = (0, 0);
    for c in s.as_bytes() {
        print_char(screen, &FONT[char_to_font_index(*c)], screen_pos);
        screen_pos.1 += 16;
        if screen_pos.1 >= NO_OF_PIXELS_IN_A_ROW {
            screen_pos.0 += 16;
            screen_pos.1 = 0;
        }
        if screen_pos.0 >= NO_OF_PIXELS_IN_A_COLUMN {
            break;
        }
    }
}

We see that char_to_font_index is supposed to return the index of the character's description in the FONT array. And now, we see the problem: the number 2 has no description in the array. There are no numbers in our font.

Get your hands dirty and add those numbers right away; it's just the digits 0..=9.

Take Away

  • A trait is used to define shared behavior.
  • The core::fmt::Write trait is used to define behavior for something that a character stream can be written into.

For the full code up to this point, go to the repo

In The Next Post

We'll finish up the panicking and printing mechanisms.

References

  • https://doc.rust-lang.org/core/fmt/struct.Arguments.html
  • https://doc.rust-lang.org/reference/items/traits.html
  • https://en.wikipedia.org/wiki/List_of_Unicode_characters