Demilade Sonuga's blog

All posts

The Interrupt Descriptor Table V

2023-02-19

In this post, we're going to set up the IDT.

Setting Up The IDT

Setting up the IDT is similar to setting up the GDT:

  1. We create a new IDT structure.
  2. Add whatever entries we want to add.
  3. Create a new DescriptorTablePointer pointing to the IDT.
  4. Use a special instruction (lidt) to tell the CPU where the descriptor table pointer is.

In main.rs

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

    setup_gdt();

    setup_idt(); // NEW

    // ...Others
}

// Creates a new IDT and sets it up
fn setup_idt() {

}

Just like the GDT and TSS, our IDT needs to last throughout the whole program so we place it in another static.

// ...Others
mod tss;
use tss::{TSS, load_tss};

mod interrupts;
use interrupts::{IDT, Entry}; // NEW

mod game;

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

static mut GDT: Option<GDT> = None;
static mut TSS: Option<TSS> = None;
static mut IDT: Option<IDT> = None; // NEW

Initializing:

fn setup_idt() {
    let idt: &mut IDT;
    unsafe {
        IDT = Some(IDT::new());
        idt = IDT.as_mut().unwrap();
    }
}

We don't have entries to add yet so we skip to step 3: creating a new DescriptorTablePointer. We'll make an associated function for this as we did for GDT.

In interrupts.rs

// ...Others
use core::ops::{BitAnd, Shr};
use crate::gdt::DescriptorTablePointer; // NEW

impl IDT {
    // Creates a descriptor table pointer that tells to tell
    // the processor where the IDT is located
    pub fn as_pointer(&self) -> DescriptorTablePointer {
        DescriptorTablePointer {
            limit: (core::mem::size_of::<Self>() - 1) as u16,
            base: self
        }
    }
}

As for loading, we'll just make a regular function:

pub fn load_idt(pointer: &DescriptorTablePointer) {
    unsafe {
        asm!("lidt [{}]", in(reg) pointer);
    }
}

In main.rs

fn setup_idt() {
    let idt: &mut IDT;
    unsafe {
        IDT = Some(IDT::new());
        idt = IDT.as_mut().unwrap();
    }
    // NEW:
    let pointer = idt.as_pointer();
    interrupts::disable_interrupts();
    interrupts::load_idt(&pointer);
    interrupts::enable_interrupts();
}

If you try compiling, you'll get an error about the DescriptorTablePointer expecting a GDT reference, instead of IDT. This is because of the &'static GDT type specified as the type of the DescriptorTablePointer base.

In gdt.rs

#[repr(C, packed)]
pub struct DescriptorTablePointer {
    limit: u16,
    // DELETED: base: &'static GDT
    base: *const u8 // NEW
}

The base field is now a pointer to bytes. To make this work:

impl GDT {
    // ...Others

    pub fn as_pointer(&'static self) -> DescriptorTablePointer {
        DescriptorTablePointer {
            // DELETED: base: self,
            base: self as *const _ as *const u8, // NEW
            limit: (mem::size_of::<Self>() - 1) as u16
        }
    }

    // ...Others
}

In interrupts.rs

impl IDT {
    // ...Others

    pub fn as_pointer(&'static self) -> DescriptorTablePointer {
        DescriptorTablePointer {
            limit: (core::mem::size_of::<Self>() - 1) as u16,
            // DELETED: base: self
            base: self as *const _ as *const u8 // NEW
        }
    }
}

Another attempt at compilation throws visibility errors.

In gdt.rs

#[repr(C, packed)]
pub struct DescriptorTablePointer {
    // DELETED: limit: u16,
    pub limit: u16, // NEW
    // DELETED: base: *const u8
    pub base: *const u8 // NEW
}

All errors resolved.

A Bit of Testing

We know that whenever an interrupt occurs, the processor looks up its entry in the IDT and executes the service routine specified.

To see this in action, we'll use the breakpoint exception. The breakpoint exception is normally used by debuggers to stop programs while executing and inspecting their environment.

To trigger the breakpoint exception, we can generate the interrupt from the software using the int instruction. The breakpoint exception is the third entry, so int 3 is the instruction we need to invoke it.

First, we create the service routine itself:

In main.rs

extern "x86-interrupt" fn breakpoint_handler(frame: interrupts::InterruptStackFrame) {
    let screen = get_screen().unwrap();
    write!(screen, "In the breakpoint handler");
}

Notice that the function has a type that is compatible with the ServiceRoutine struct. It doesn't take an error code and it returns. If the function had a slightly different type, it would have resulted in errors later and that's a good thing. It means that the compiler knows what type of functions should be with which entries.

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

fn setup_idt() {
    let idt: &mut IDT;
    unsafe {
        IDT = Some(IDT::new());
        idt = IDT.as_mut().unwrap();
    }
    idt.breakpoint = Entry::exception(ServiceRoutine(breakpoint_handler), sel); // NEW
    let pointer = idt.as_pointer();
    interrupts::disable_interrupts();
    interrupts::load_idt(&pointer);
    interrupts::enable_interrupts();
}

To get the segment selector, we can return it from the setup_gdt function and pass it as an argument here.

// ...Others
mod gdt;
// DELETED: use gdt::{Descriptor, GDT, SegmentRegister};
use gdt::{Descriptor, GDT, SegmentRegister, SegmentSelector}; // NEW
// ...Others

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

    // DELETED: setup_gdt();
    let cs = setup_gdt(); // NEW
    // DELETED: setup_idt();
    setup_idt(cs); // NEW

    // ...Others
}

// DELETED: fn setup_gdt() {
fn setup_gdt() -> SegmentSelector { // NEW
    // ...Others

    gdt::CS.set(cs);
    gdt::DS.set(ds);
    gdt::SS.set(ds);
    cs
}

// DELETED: fn setup_idt() {
fn setup_idt(sel: SegmentSelector) { // NEW
    // ...Others
    idt.breakpoint = Entry<ServiceRoutine>::exception(breakpoint_handler, sel);
    let pointer = idt.as_pointer();
    interrupts::disable_interrupts();
    interrupts::load_idt(&pointer);
    interrupts::enable_interrupts();
}

Finally, invoking the exception:

#[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);
    // Invoking the breakpoint exception
    unsafe { core::arch::asm!("int3"); }
    loop {}

    game::blasterball(screen);

    // ...Others
}

If you run the code now, you'll get an "In the breakpoint handler" message on screen:

Breakpoint Message

Take Away

For the full code, go to the repo

In The Next Post

We'll be taking a look at some important exceptions