Demilade Sonuga's blog

All posts

Some Important Exceptions

2023-02-22

In this post, We're going to take a look at some exceptions easy to trigger and others that we actually have to pay attention to and we'll further check out the interrupts mechanism with the IDT.

The Breakpoint Exception

This was already mentioned in the previous post. It's used by debuggers to stop the processor at points during code execution so that memory and other things can be inspected. We've already been here and we won't go down this path any longer.

Page Fault

To understand this exception, you have to be familiar with paging. Paging is a memory management scheme. In paging, there are two types of memory: virtual and physical. Virtual memory is memory as the programs see it. When address 0x333 is accessed by a program, it's a virtual address in virtual memory that they're accessing. Virtual memory is divided up into discrete chunks called pages.

Physical memory, on the other hand, is the actual memory of a computer. It, too, is split up into discrete chunks: frames.

Pages of virtual memory can then be mapped to frames of physical memory. For example, if a page with addresses 0x0 to 0xfff is mapped to a frame with addresses 0x1000 to 0x1fff, then a program accessing address 0x0 is actually accessing address 0x1000 in physical memory.

The whole idea behind this split between how a program views memory and the actual physical memory has several benefits to the OS. User programs can be prevented from accessing areas of memory that they're not supposed to access simply by never mapping their virtual memory pages to those sensitive frames. Read-write permissions can also be enabled or disabled on pages to prevent programs from writing to memory that's not supposed to be overwritten, like the program instructions.

The main idea behind paging is creating an illusion of how memory is to programs (virtual memory) and finding a way to make that illusion happen using real memory (physical memory). If you want to learn more about paging, check the references.

This disconnect between the way a program views memory and the actual memory can also lead to an error called a page fault. This is what happens when a program tries to access memory that has not been mapped to any frames of physical memory.

In this project, we haven't needed to think about any of this paging stuff because the UEFI firmware did that for us. Now that we have full control over the computer we can decide to mess around with this stuff but it's really not needed in this project. Besides, messing around with memory mappings, if one isn't careful, can lead to some serious mental shakeups.

But we can still have page faults if we decide to access addresses of memory that aren't mapped.

Let's try that out now:

In main.rs

// ...Others
mod interrupts;
// DELETED: use interrupts::{IDT, Entry, ServiceRoutine};
use interrupts::{IDT, Entry, ServiceRoutine, ServiceRoutineWithErrCode}; // NEW
// ...Others

fn setup_idt(sel: SegmentSelector) {
    // ...Others
    idt.breakpoint = Entry::exception(ServiceRoutine(breakpoint_handler), sel);
    idt.page_fault = Entry::exception(ServiceRoutineWithErrCode(page_fault_handler), sel); // NEW
    let pointer = idt.as_pointer();
    // ...Others
}

// NEW
extern "x86-interrupt" fn page_fault_handler(frame: interrupts::InterruptStackFrame, err_code: u64) {
    let screen = get_screen().unwrap();
    write!(screen, "In the page fault handler");
    loop {}
}

// ...Others

Now, triggering a page fault:

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

    let cs = setup_gdt();

    setup_idt(cs);
    
    /* DELETED:
    unsafe { core::arch::asm!("int3"); }
    loop {}
    */
    
    // NEW:
    unsafe {
        let ptr = usize::MAX as *mut u8;
        let x = *ptr;
    }

    game::blasterball(screen);

    // ...Others
}

usize::MAX is a number that is too big to be a mapped address. Attempting to read that location in memory will lead to a page fault.

Running:

Triggering a Page Fault

Double Fault

You can view this exception as a kind of catch-all default case. Double faults occur either when an exception occurs and there's no entry to handle it or when an exception that can't be handled happens during the handling of another exception. For example, if a breakpoint exception is triggered with the int3 instruction and there is no entry in the IDT for the breakpoint exception, a double fault will occur.

Triggering a double fault for an exception with no entry in the IDT:

// ...Others

mod interrupts;
// DELETED: use interrupts::{IDT, Entry, ServiceRoutine, ServiceRoutineWithErrCode};
use interrupts::{IDT, Entry, ServiceRoutine, ServiceRoutineWithErrCode, ServiceRoutineWithNoReturn}; // NEW

// ...Others

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

    let cs = setup_gdt();

    setup_idt(cs);

    /* DELETED:
    unsafe {
        let ptr = usize::MAX as *mut u8;
        let x = *ptr;
    }
    */
    unsafe { core::arch::asm!("int 0") };

    game::blasterball(screen);
}

// ...Others

fn setup_idt(sel: SegmentSelector) {
    // ...Others

    idt.breakpoint = Entry::exception(ServiceRoutine(breakpoint_handler), sel);
    idt.page_fault = Entry::exception(ServiceRoutineWithErrCode(page_fault_handler), sel);
    idt.double_fault = Entry::exception(ServiceRoutineWithNoReturn(double_fault_handler), sel); // NEW
    let pointer = idt.as_pointer();

    // ...Others
}

extern "x86-interrupt" fn double_fault_handler(frame: interrupts::InterruptStackFrame, err_code: u64) -> ! {
    let screen = get_screen().unwrap();
    write!(screen, "In the double fault handler");
    loop {}
}

// ...Others

The int 0 instruction triggers an interrupt that will result in the IDT's entry 0 being invoked. Entry 0 is supposed to be the entry for the divide-by-zero exception. The absence of an entry will result in a double fault.

Running:

Triggering a Double Fault

Triple Fault

Although there is no entry for this in the IDT, it's still something we need to look at. While a double fault is like the backup of other exceptions, triple faults are the backups of double faults. Invoking an exception when there is no entry for that exception in the IDT results in a double fault. But what if there is no double fault entry either? Enter triple fault.

Triple faults aren't handled by us. They're handled by the processor. When a triple fault occurs, the system resets, that is, it reboots and the program starts execution from the beginning all over again.

Triggering a triple fault is as easy as not adding a double fault entry and triggering an exception without an IDT entry:

fn setup_idt(sel: SegmentSelector) {
    // ...Others

    idt.breakpoint = Entry::exception(ServiceRoutine(breakpoint_handler), sel);
    idt.page_fault = Entry::exception(ServiceRoutineWithErrCode(page_fault_handler), sel);
    // DELETED: idt.double_fault = Entry::exception(ServiceRoutineWithNoReturn(double_fault_handler), sel);
    let pointer = idt.as_pointer();

    // ...Others
}

The divide-by-zero exception is already being triggered in efi_main. The absence of an entry for it and for double faults can only result in a triple fault.

If you run this in the emulator, what you'll meet is a blank screen followed by a return to the starting shell indicating that the system has reset.

For The Game

With this knowledge of exceptions, it's best we keep our page and double fault handlers for our game and panic with helpful error messages:

fn setup_idt(sel: SegmentSelector) {
    // ...Others

    idt.breakpoint = Entry::exception(ServiceRoutine(breakpoint_handler), sel);
    idt.page_fault = Entry::exception(ServiceRoutineWithErrCode(page_fault_handler), sel);
    idt.double_fault = Entry::exception(ServiceRoutineWithNoReturn(double_fault_handler), sel); // NEW
    let pointer = idt.as_pointer();

    // ...Others
}

extern "x86-interrupt" fn page_fault_handler(frame: interrupts::InterruptStackFrame, err_code: u64) {
    /* DELETED:
    let screen = get_screen().unwrap();
    write!(screen, "In the page fault handler");
    loop {}
    */
    panic!("Page faulted with error code {}", err_code); // NEW
}

// ...Others

extern "x86-interrupt" fn double_fault_handler(frame: interrupts::InterruptStackFrame, err_code: u64) -> ! {
    /* DELETED:
    let screen = get_screen().unwrap();
    write!(screen, "In the double fault handler");
    loop {}
    */
    panic!("Double fault with error code {}", err_code); // NEW
}

// ...Others

Take Away

  • Page faults are the result of attempting to access unmapped virtual memory.
  • Double faults are the result of something irrecoverable going wrong while an exception is invoked.
  • Triple faults are the result of something going wrong while a double fault occurs.

For the full code, go to the repo

In The Next Post

We'll get started with the Programmable Interrupt Controller.

References

  • Paging: https://os.phil-opp.com/paging-introduction/
  • Exceptions: https://wiki.osdev.org/Exceptions
  • Triple fault: https://wiki.osdev.org/Triple_Fault