Demilade Sonuga's blog

All posts

Panicking I

2022-12-30

We've gone a long way in this project. So far, we've been able to animate a bitmap on screen. We could just keep going on right now to the next thing but that's not really a good idea because there's a major thing we're missing out: that is sensible panic behavior. When a panic occurs, the computer goes into an eternal loop, without giving any information on why the panic occurred:

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

If an unexpected error is encountered during execution and a panic occurs, tracing the cause of the error will be unnecessarily hard.

To get a better idea of what I'm talking about:

#[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 panic occurred here"); // NEW
        draw_bitmap(screen, &block, block_position);
        erase_bitmap(screen, &block, block_position);
        block_position.1 += 1;
    }

    0
}

If you run the game now, you'll get a blank screen. This is because when the panic occurs, the computer goes into an infinite loop without giving any information about why the panic occurred.

As we proceed in the process of building this game, we will definitely need something to tell us what went wrong in the case of an unexpected error.

Our new task now, before we get on with the game, is to figure out how to get that information about why a panic occurred on screen.

A look at the signature of the panic handler tells us that the function takes one argument of type PanicInfo. It's this struct, unsurprisingly, that holds information about the panic.

A briefing through the docs shows that PanicInfo has some methods that can be used to retrieve the information. The location function returns a struct that contains information about where the panic occurred in the file. The message function returns a struct representing the arguments that panic! was called with.

Putting 1 and 2 together, we can print panic info:

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    // NEW:
    if let Some(panic_msg) = panic_info.message() {
        print_str(no screen, panic_msg.as_str().unwrap()); // No access to the screen
    } else {
        print_str(no screen, "Message returned None for some unknown reason"); // No access to the screen
    }
    loop {}
}

Now, we've encountered a problem that is stopping us from making progress.

We no longer have access to the screen reference that we were using to print. This means we can no longer print. So, how can we get the information on the screen if we can't even access the screen anymore?

To solve this problem, we need to rethink our code.

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

    let framebuffer_base = mode.framebuffer_base;

    let screen = framebuffer_base as *mut Screen;
    
    let screen = unsafe { &mut *screen };

    // ... Others
}

This is the point where we retrieve the address of the screen. This address is saved in a local variable. Local variables are saved on the stack. The problem here is that the addresses of values saved on the stack will not be known until runtime when the program is executing.

But for the screen to be accessed in the panic handler, the reference has to be saved somewhere known beforehand to both efi_main and the handler so they can both access it when they need to. One way of resolving this is to create a static variable. The address of a static variable is known at compile time because the variable is saved in a location reserved by the compiler in the output binary.

Using this idea, we now modify main.rs:

static mut SCREEN: Option<&mut Screen> = None;

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

    let framebuffer_base = mode.framebuffer_base;

    let screen = framebuffer_base as *mut Screen;
    
    let screen = unsafe { &mut *screen };

    // Initializing the screen static
    unsafe { SCREEN = Some(screen); } // NEW

    // The SCREEN static now holds the mutable reference to screen
    // Since there can't be more than one mutable reference, a reference
    // to the screen reference is used instead
    let screen = unsafe { SCREEN.as_mut().unwrap() };

    // ... Others
}

Since the address of the screen is not known at compile time, we set the SCREEN static to None. When the address of the screen is finally known, we initialize the static with the appropriate reference. After this initialization, for the rest of our code to keep working, we have to create a new variable screen which is a reference to the screen reference now in the SCREEN static. We have to do this because there can't be more than one mutable reference to a value at once. Saving the mutable reference in the static SCREEN means that the reference is going to be active for the rest of the program's execution. From this point on, there can't be another mutable reference.

We now have access to the screen reference in 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() {
        // DELETED: print_str(no screen, panic_msg.as_str().unwrap());
        print_str(screen, panic_msg.as_str().unwrap()); // NEW
    } else {
        // DELETED: print_str(no screen, "Message returned None for some unknown reason");
        print_str(screen, "Message returned None for some unknown reason"); // NEW
    }
    loop {}
}

There's just one more thing we need to add for this to work:

In main.rs

#![no_std]
#![no_main]
#![feature(abi_efiapi)]
#![feature(panic_info_message)] // NEW

The PanicInfo's message function is an experimental feature and to use it, we need to add the feature gate, which is this #![feature(panic_info_message)].

Running the code now will give this output:

Panicking With A Message

Our panic function can now print a simple message but the problem now is that it's too limited. To see what I'm talking about:

#[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 panic occured here");
        panic!("A panic occurred {}", "here"); // NEW
        draw_bitmap(screen, &block, block_position);
        erase_bitmap(screen, &block, block_position);
        block_position.1 += 1;
    }

    0
}

The expected output should be "A panic occurred here". But instead, this is what we have:

Unexpected Output

To understand why we're having this output, we return 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() {
        print_str(screen, panic_msg.as_str().unwrap());
    } else {
        print_str(screen, "Message returned None for some unknown reason"); // NEW
    }
    loop {}
}

The PanicInfo's message function returns an instance of core::fmt::Arguments. The Arguments struct is what is used to represent format strings and their arguments. Whenever you call println!("I am {} years old", 19), the combination of the string slices "I am ", " years old" and argument 19 is fully represented as an instance of the Arguments struct.

The same thing goes for our panic! macro. When you passed "A panic occurred here" as the to the panic! macro, it was represented as an Arguments instance with no arguments. When you passed "A panic occurred {}", "here" to panic!, it was represented as an Arguments instance with a single argument "here".

The Arguments's as_str function returns a string slice only when the struct instance has no arguments. If it has arguments, None is returned. This is why the first time we panicked, the message was printed. There were no arguments, so as_str simply returned Some("A panic occurred here").

The second time, there is an argument, so as_str returns a None, which is then unwrapped, causing another panic. The error message that ought to be outputted was: "called Option::unwrap() on a None value", but only "called" gets printed.

This means that in our handler's current state, we can't panic with format arguments, which renders the handler useless, because those arguments are needed to get useful information on the screen.

Figure out a way to resolve this.

Take Away

  • The core::fmt::Arguments is the representation of a format string and its arguments.

For the full code, go to the repo

In The Next Post

We'll continue setting up mechanisms to print panic messages

References

  • https://doc.rust-lang.org/core/panic/struct.PanicInfo.html
  • https://doc.rust-lang.org/core/fmt/struct.Arguments.html