Demilade Sonuga's blog

All posts

Panicking III

2023-01-05 · 55 min read

If you got stuck while putting down your digit descriptions in the font, you can get them from the repo.

After getting those descriptions into the code, we need to modify the FONT array to reflect the new characters:

In font.rs:

// ... Others

// DELETED: pub const FONT: [[[bool; 16]; 16]; 28] = [
pub const FONT: [[[bool; 16]; 16]; 38] = [ // NEW
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O , P, Q, R,
S, T, U, V, W, X, Y, Z, SPACE, EXCLAMATION,
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE // NEW
];

The FONT array now contains the digits 0..=9. To be able to print digits on the screen, we need to change our char_to_font_index in uefi.rs function which is in charge of mapping Unicode scalar values (characters) to the index of the position in the FONT array that describes that character.

This is the current state of the function:

// Takes the Unicode code point of a character in 'A'..='Z' or 'a'..='z' or "!" or  " "
// and returns its index into the FONT array
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
}
}

The Unicode scalar values for the digits 0..=9 are 48, 49, 50, ..., 57, in that order. In the FONT array, the digits 0..=9 start from index 28. So, the index of 0 will be 48 - 20 == 28. The description for 1 comes immediately after 0, so its index will be 49 - 20 == 29. The same goes for the digit 2 which is 50 - 20 == 30. Clearly, the digits index of the digits 0..=9 in our array are the digit's Unicode scalar values minus the number 20.

/* DELETED:
fn char_to_font_index(c: u8) -> usize {
if c >= 97 {
char_to_font_index(c - 32)
} else if c == 32 {
26
} else if c == 33 {
27
} else {
(c - 65) as usize
}
}
*/

// NEW:
// Takes the Unicode code point of a character in 'A'..='Z' or 'a'..='z' or "!" or " " or '0'..='9'
// and returns its index into the FONT array
fn char_to_font_index(c: u8) -> usize {
match c {
32 => 26, // Space
33 => 27, // Exclamation mark
48..=57 => (c - 20) as usize, // Digits
65..=91 => (c - 65) as usize, // Big letters
97..=123 => char_to_font_index(c - 32), // Small letters to big letters
_ => panic!("The Unicode character {} is not representable by the font", c)
}
}

Now, let's see how the font looks on the screen.

In main.rs:

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

let screen = framebuffer_base as *mut Screen;

let screen = unsafe { &mut *screen };

unsafe { SCREEN = Some(screen); }

let screen = unsafe { SCREEN.as_mut().unwrap() };

// NEW:
print_str(screen, "abcdefghijklmnopqrstuvwxyz0123456789");
loop {}

// ... Others
}

Upon running this, you should have:

Font With Digits

And now we have digits in our font.

Let's try panicking with a digit as a format argument:

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

let screen = framebuffer_base as *mut Screen;

let screen = unsafe { &mut *screen };

unsafe { SCREEN = Some(screen); }

let screen = unsafe { SCREEN.as_mut().unwrap() };

/* DELETED:
print_str(screen, "abcdefghijklmnopqrstuvwxyz0123456789");
loop {}
*/

//NEW:
panic!("{}", 2);

// ... Others
}

Running:

Panic With Digit

That's much better now.

In our new char_to_font_index, the function panics if the character has no description in the font. Let's see what happens when this scenario takes place:

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

let screen = framebuffer_base as *mut Screen;

let screen = unsafe { &mut *screen };

unsafe { SCREEN = Some(screen); }

let screen = unsafe { SCREEN.as_mut().unwrap() };

// DELETED: panic!("{}", 2);
print_str(screen, "+"); // NEW

// ... Others
}

The character '+' has no description in our font, so the result of this should be a panic with the message "The Unicode character 43 is not representable by the font", since the Unicode scalar value of '+' is 43.

Running:

Partially Printed

Only the last part of the message got printed. So, there's still something wrong somewhere.

To figure out where the problem is, we proceed by asking ourselves some relevant questions, answering those questions and moving in the direction of the logic.

We start by asking ourselves: what exactly is the problem here? Clearly, the problem is that only the last part of the message, after the format argument, is printed. Why is this so? I don't know. All I do know is that the point where this message gets printed is in the panic handler, where it's passed as an argument to the screen's write_fmt function.

So, this takes us to the panic handler:

#[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 {}
}

The panic_msg holds the arguments we passed to panic!, and write_fmt handles everything else. But how exactly does write_fmt do this handling, since it appears to be doing it wrongly?

To answer this question, we go to the docs.

The default implementation of write_fmt provided, according to the docs:

pub trait Write {
// ... Others

#[stable(feature = "rust1", since = "1.0.0")]
fn write_fmt(mut self: &mut Self, args: Arguments<'_>) -> Result {
write(&mut self, args)
}
}

Clearly, write_fmt works by passing the work over to some function called write. So, to understand write_fmt, we need to understand this function. To understand this function, we first need to see the code, but where is that code, exactly?

A little searching reveals that the write function is defined in the same file.

#[stable(feature = "rust1", since = "1.0.0")]
pub fn write(output: &mut dyn Write, args: Arguments<'_>) -> Result {
let mut formatter = Formatter::new(output);
let mut idx = 0;

match args.fmt {
None => {
// We can use default formatting parameters for all arguments.
for (i, arg) in args.args.iter().enumerate() {
// SAFETY: args.args and args.pieces come from the same Arguments,
// which guarantees the indexes are always within bounds.
let piece = unsafe { args.pieces.get_unchecked(i) };
if !piece.is_empty() {
formatter.buf.write_str(*piece)?;
}
(arg.formatter)(arg.value, &mut formatter)?;
idx += 1;
}
}
Some(fmt) => {
// Every spec has a corresponding argument that is preceded by
// a string piece.
for (i, arg) in fmt.iter().enumerate() {
// SAFETY: fmt and args.pieces come from the same Arguments,
// which guarantees the indexes are always within bounds.
let piece = unsafe { args.pieces.get_unchecked(i) };
if !piece.is_empty() {
formatter.buf.write_str(*piece)?;
}
// SAFETY: arg and args.args come from the same Arguments,
// which guarantees the indexes are always within bounds.
unsafe { run(&mut formatter, arg, args.args) }?;
idx += 1;
}
}
}

// There can be only one trailing string piece left.
if let Some(piece) = args.pieces.get(idx) {
formatter.buf.write_str(*piece)?;
}

Ok(())
}

By going through this function's code, we can see how write_fmt works. The first argument of the function is output, a Write trait object. What exactly a trait object is is something we don't need to start looking at now. All we need to know is that output refers to something that has implemented the Write trait. The second argument args is just the arguments.

From the way the function is written, we see that some variable idx is initialized to 0. In either branch of the match expression, a for loop is executed. In each iteration of the for loop, a piece of the argument is retrieved and is written with write_str. After this, the idx variable is incremented.

At the end of the for loop, the last piece of the argument is retrieved with idx and written with write_str.

Now it's clear how write_fmt works. The pieces that make up the full string are iterated over and printed with write_str.

In our code, panic! is invoked with the argument ("The Unicode character {} is not representable by the font", c), so the pieces will be something like ["The Unicode character ", the value of c, " is not representable by the font"].

Since all arguments are written with write_str, we head back there to see what went wrong:

In uefi.rs

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

print_str is the only function called here, so the problem must be in print_str (if there is a problem here at all).

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;
}
}
}

And here we have it. The problem. The print_str function works by printing characters across the screen starting from screen_pos. But screen_pos is initialized to (0, 0) at the beginning of every call, so every piece of the argument is written starting from the upper left corner of the screen. This is why only the last piece shows up because it overwrites the previous piece that was printed.

To resolve this problem, we need to keep the screen_pos out of the function, in a place where its value can be preserved across calls to print_str.

We can easily do this with statics:

static SCREEN_POS_ROW: AtomicUsize = AtomicUsize::new(0);
static SCREEN_POS_COL: AtomicUsize = AtomicUsize::new(0);

pub fn print_str(screen: &mut Screen, s: &str) {
// DELETED: let screen_pos = (0, 0);
let mut screen_pos = (
SCREEN_POS_ROW.load(Ordering::Relaxed),
SCREEN_POS_COL.load(Ordering::Relaxed)
); // NEW
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;
}
}
// NEW:
// The new value of the screen position
SCREEN_POS_ROW.store(screen_pos.0, Ordering::Relaxed);
SCREEN_POS_COL.store(screen_pos.1, Ordering::Relaxed);
}

The screen position is now saved in static variables, so the variables will remain there from the start of the program to the end of it, unlike local variables which can only last during their function's execution.

In the print_str function, the current screen positions are first loaded from the statics. After the drawing on the screen, the statics are updated to reflect the new screen position.

AtomicUsizes were used for the statics to avoid mutable statics. This is because mutable statics are unsafe to handle. Handling AtomicUsize in a static variable is safe because it can be mutated without having to declare the static mutable.

As for the Ordering::Relaxed used in the loads and stores of the atomic variables, these just tell the compiler that the order of storage and loading of the variable doesn't matter. This is only relevant in a multi-process scenario, where multiple executing programs are accessing the variable at the same time, so we don't have to worry about it.

For the code to compile:

In uefi.rs

use crate::FONT;
use core::fmt::Write;
// NEW:
use core::sync::atomic::{AtomicUsize, Ordering};

Running:

Panicking Properly

And we've finally gotten our panicking mechanism to work properly.

To see if the block animation is still working:

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

let screen = framebuffer_base as *mut Screen;

let screen = unsafe { &mut *screen };

unsafe { SCREEN = Some(screen); }

let screen = unsafe { SCREEN.as_mut().unwrap() };

// DELETED: print_str(screen, "+");

let block_bytes = include_bytes!("./block.bmp");

// ... Others
}

Running:

No Colon In Font

We have a panic message. Apparently, at some point, the character with Unicode scalar value 58 was supposed to be printed. According to the Unicode list here, the character with the value 58 is the colon ':'.

Go ahead and add a colon description to the font.

After adding it, we have to modify the FONT array:

// DELETED: pub const FONT: [[[bool; 16]; 16]; 38] = [
pub const FONT: [[[bool; 16]; 16]; 39] = [
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O , P, Q, R,
S, T, U, V, W, X, Y, Z, SPACE, EXCLAMATION,
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
COLON // NEW
];

And in uefi.rs, the char_to_font_index function:

fn char_to_font_index(c: u8) -> usize {
match c {
32 => 26, // Space
33 => 27, // Exclamation mark
48..=57 => (c - 20) as usize, // Digits
58 => 38, // Colon
65..=91 => (c - 65) as usize, // Big letters
97..=123 => char_to_font_index(c - 32), // Small letters to big letters
_ => panic!("The Unicode character {} is not representable by the font", c)
}
}

The array length is 39 and the colon is the last description in the array. So, the colon's description is at index 38.

Running again, we have:

A Number 2

Which is the expected output from the panic!("A number: {}", 2) in the animation while loop.

To get the animation back:

#[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 {
// DELETED: panic!("A number: {}", 2);
draw_bitmap(screen, &block, block_position);
erase_bitmap(screen, &block, block_position);
// Increase block position to the right
block_position.1 += 1;
}

// ... Others
}

Upon running again, the animation plays on, but ends with another panic:

Index Out Of Bounds

Now, we have to figure out where this error is coming from. We can do this again using our Sherlock Holmes method of tracing the error, but it will be much easier if the panicking information included the line number information.

And as a matter of fact, it does. The PanicInfo struct includes this information. To use it, we make use of the write! macro:

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
let screen = unsafe { SCREEN.as_mut().unwrap() };
write!(screen, "{}", panic_info);
/* DELETED:
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 {}
}

The write! macro takes anything that implements the Write trait as the first argument, then the format string and arguments come after. This works because PanicInfo implements core::fmt::Display, the trait that is implemented for anything that can be displayed on the screen.

The PanicInfo's Display implementation takes care of assembling the line number information and message in the struct so we don't have to do it ourselves.

Running gives us yet another panic:

Infinite Panic

An infinite panic loop. This output shouldn't be surprising. Whenever a character is not representable by our font, a panic occurs. So, if a string to be printed by the write! macro contains any character that we don't have a description of, a panic will occur again and again and again.

To see the actual character that we're missing, we need to devise a way to see the panic message after the first character-can't-be-represented panic occurs.

static mut panicked: bool = false;
#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
let screen = unsafe { SCREEN.as_mut().unwrap() };
// DELETED: write!(screen, "{}", panic_info);
// NEW:
if unsafe { !panicked } {
unsafe { panicked = true; }
write!(screen, "{}", panic_info);
} else {
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 {}
}

A panicked static is created and initialized to false. During the first panic, we attempt to use the write! macro. The panicked static is then set to true, so that if another panic occurs during the execution of write!, the else block will be executed to print write!'s panic message.

Running:

No Apostrophe

The Unicode scalar value of 39 represents the apostrophe " ' ". Go ahead and add that to the font.

Adding it to the FONT array:

// DELETED: pub const FONT: [[[bool; 16]; 16]; 39] = [
pub const FONT: [[[bool; 16]; 16]; 40] = [ // NEW
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O , P, Q, R,
S, T, U, V, W, X, Y, Z, SPACE, EXCLAMATION,
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
COLON, APOSTROPHE // NEW
];

In uefi.rs:

fn char_to_font_index(c: u8) -> usize {
match c {
32 => 26, // Space
33 => 27, // Exclamation mark
39 => 39, // Apostrophe
48..=57 => (c - 20) as usize, // Digits
58 => 38, // Colon
65..=91 => (c - 65) as usize, // Big letters
97..=123 => char_to_font_index(c - 32), // Small letters to big letters
_ => panic!("The Unicode character {} is not representable by the font", c)
}
}

Running:

No Comma

Now we have the index out-of-bounds message, but instead of the line information, we get another unrepresentable character message. The Unicode scalar value of 44 represents the comma ",". Add this one, too, to the font.

After adding the comma:

// DELETED: pub const FONT: [[[bool; 16]; 16]; 40] = [
pub const FONT: [[[bool; 16]; 16]; 41] = [
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O , P, Q, R,
S, T, U, V, W, X, Y, Z, SPACE, EXCLAMATION,
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
COLON, APOSTROPHE, COMMA // NEW
];

In uefi.rs

fn char_to_font_index(c: u8) -> usize {
match c {
32 => 26, // Space
33 => 27, // Exclamation mark
39 => 39, // Apostrophe
44 => 40, // Comma
48..=57 => (c - 20) as usize, // Digits
58 => 38, // Colon
65..=91 => (c - 65) as usize, // Big letters
97..=123 => char_to_font_index(c - 32), // Small letters to big letters
_ => panic!("The Unicode character {} is not representable by the font", c)
}
}

Running:

No Forward Slash

Forward slash, "/", is the character represented by Unicode 47. Add it to the font.

// DELETED: pub const FONT: [[[bool; 16]; 16]; 41] = [
pub const FONT: [[[bool; 16]; 16]; 42] = [ // NEW
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O , P, Q, R,
S, T, U, V, W, X, Y, Z, SPACE, EXCLAMATION,
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
COLON, APOSTROPHE, COMMA, FORWARD_SLASH // NEW
];

In uefi.rs

fn char_to_font_index(c: u8) -> usize {
match c {
32 => 26, // Space
33 => 27, // Exclamation mark
39 => 39, // Apostrophe
44 => 40, // Comma
47 => 41, // Forward slash
48..=57 => (c - 20) as usize, // Digits
58 => 38, // Colon
65..=91 => (c - 65) as usize, // Big letters
97..=123 => char_to_font_index(c - 32), // Small letters to big letters
_ => panic!("The Unicode character {} is not representable by the font", c)
}
}

Running:

No Full Stop

Full stop, ".", is the character represented by Unicode 46. Add it to the font.

// DELETED: pub const FONT: [[[bool; 16]; 16]; 42] = [
pub const FONT: [[[bool; 16]; 16]; 43] = [ // NEW
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O , P, Q, R,
S, T, U, V, W, X, Y, Z, SPACE, EXCLAMATION,
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
COLON, APOSTROPHE, COMMA, FORWARD_SLASH, FULL_STOP // NEW
];

In uefi.rs

fn char_to_font_index(c: u8) -> usize {
match c {
32 => 26, // Space
33 => 27, // Exclamation mark
39 => 39, // Apostrophe
44 => 40, // Comma
46 => 42, // Full stop
47 => 41, // Forward slash
48..=57 => (c - 20) as usize, // Digits
58 => 38, // Colon
65..=91 => (c - 65) as usize, // Big letters
97..=123 => char_to_font_index(c - 32), // Small letters to big letters
_ => panic!("The Unicode character {} is not representable by the font", c)
}
}

Running:

Full Panic Info

The full panic info, including the line number information, is now printed on the screen.

We can now get rid of this static:

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

write!(screen, "{}", panic_info); // NEW
loop {}
}

The panic info points us to the draw_bitmap function in bitmap.rs.

pub fn draw_bitmap(screen: &mut Screen, bitmap: &Bitmap, pos: (usize, usize)) {
for row in 0..bitmap.height() {
for col in 0..bitmap.width() {
let inverted_row = bitmap.height() - row - 1;
let color_table_index = bitmap.pixel_array[inverted_row * bitmap.width() + col];
let color = bitmap.color_table.0[color_table_index as usize];
screen.pixels[row + pos.0][col + pos.1] = Pixel {
red: color.red,
green: color.green,
blue: color.blue,
reserved: 0
};
}
}
}

Looking at this function, it's not so hard to see why it caused a panic. There is no assurance whatsoever that row + pos.0 < NO_OF_PIXELS_IN_A_COLUMN and col + pos.1 < NO_OF_PIXELS_IN_A_ROW. It's this unchecked indexing that caused the panic.

pub fn draw_bitmap(screen: &mut Screen, bitmap: &Bitmap, pos: (usize, usize)) {
for row in 0..bitmap.height() {
for col in 0..bitmap.width() {
let inverted_row = bitmap.height() - row - 1;
let color_table_index = bitmap.pixel_array[inverted_row * bitmap.width() + col];
let color = bitmap.color_table.0[color_table_index as usize];
if row + pos.0 < NO_OF_PIXELS_IN_A_COLUMN && col + pos.1 < NO_OF_PIXELS_IN_A_ROW { // NEW
screen.pixels[row + pos.0][col + pos.1] = Pixel {
red: color.red,
green: color.green,
blue: color.blue,
reserved: 0
};
}
}
}
}

The erase_bitmap function, too, suffers from the same bug:

pub fn erase_bitmap(screen: &mut Screen, bitmap: &Bitmap, pos: (usize, usize)) {
for row in 0..bitmap.width() {
for col in 0..bitmap.height() {
if row + pos.0 < NO_OF_PIXELS_IN_A_COLUMN && col + pos.1 < NO_OF_PIXELS_IN_A_ROW { // NEW
screen.pixels[row + pos.0][col + pos.1] = Pixel {
red: 0,
green: 0,
blue: 0,
reserved: 0
};
}
}
}
}

For the code to compile:

use crate::{Screen, Pixel};
use crate::uefi::{NO_OF_PIXELS_IN_A_COLUMN, NO_OF_PIXELS_IN_A_ROW};

Upon running again, the animation plays out with no errors.

Take Away

  • The Display trait defines behavior for something that can be displayed on the screen.

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

In The Next Post

We'll be refactoring.

References