Demilade Sonuga's blog

All posts

The Interrupt Descriptor Table IV

2023-02-16 · 41 min read

We're almost done with the IDT model. All that's left is to add functions for setting entry fields and setting up the IDT.

The Needed Functions

Take a look at the Entry structure:

#[repr(C)]
#[derive(Clone, Copy)]
pub struct Entry<T> {
handler_ptr_low: u16,
segment_selector: SegmentSelector,
options: Options,
handler_ptr_middle: u16,
handler_ptr_upper: u32,
reserved: u32,
phantom: PhantomData<T>
}

For an Entry instance to be complete, the function pointer of the service routine, the segment selector and the options must have been specified.

The Options structure itself must have some fields specified: the privilege levels (bits 13..=14), the present bit (bit 15), and the entry type (bits 8..=11) which says whether an entry is for an exception or a maskable interrupt (interrupt from the PIC).

Because of this, it is clear that the default entries in a new IDT instance are not sufficient for our purposes. When a new IDT is created, we'll need to set up new entries for the exceptions and interrupts that we'll have to deal with.

From these requirements, we can say we need the following functions in Entry:

  1. exception: For creating a new entry for an exception.
  2. interrupt: For creating a new entry for a maskable interrupt.

Both functions will have to accept 2 arguments: the function pointer of the service routine (to set the handler pointer fields) and the segment selector (to set the segment selector field).

A question that comes next: why these two functions? There definitely are other ways of doing this and it seems like this one was just chosen randomly. Well, the reason I concluded with this one is because of options field.

For each entry, the options are going to be mostly the same. There are no user processes, so all the entries we're using should have the highest privilege level. All entries we're using must also be marked present to be valid so the present bit will be set for them all.

The only fields in Options which may vary with the entries are the interrupt stack table offset and the entry type. At this point, we really don't need to worry about the interrupt stack table so we can just forget about it (for now). The entry type will definitely vary. It will be 0b1111 for exceptions and 0b1110 for maskable interrupts.

It's this split in type that seemed to warrant the two functions exception and interrupt.

Entries created with exception will have the entry type field in their Options set to 0b1111 (the value for exceptions) and the ones created with interrupt will have their entry type field in their Options set to 0b1110 (the value for maskable interrupts).

The exception Function

We start with the exception function.

In interrupts.rs

impl<T> Entry<T> {
fn empty() -> Self { /* ...Others */ }

// Creates a new entry for exceptions
pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self {

}
}

This signifies that handler_ptr can be of type T. So when T == ServiceRoutine, handler_ptr must be of type ServiceRoutine. When T == ServiceRoutineWithErrCode, handler_ptr must be of type ServiceRoutineWithErrCode. When T == ServiceRoutineWithNoReturn, handler_ptr must be of type ServiceRoutineWithReturn.

The fields that need to be set in the Entry instance: the lower, middle and upper bits of the handler, the segment selector and the options.

Setting the handler bits:

impl<T> Entry<T> {
fn empty() -> Self { /* ...Others */ }

pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self {
let mut entry = Self::empty();
// Clearing out all bits except the lower 16 bits of the routine
// function pointer and setting it as the entry's handler pointer lower bits
entry.handler_ptr_low = handler_ptr & 0xffff;
// Shifting out the lower 16 bits leaving the next 16 bits in its
// place and setting it as the entry's handler pointer middle bits
entry.handler_ptr_middle = (handler_ptr >> 16) & 0xffff;
// Clearing out the lower 32 bits and retaining only the upper 32
// Then setting it as the entry's handler pointer higher bits
entry.handler_ptr_upper = (handler_ptr >> 32) & 0xffffffff;
}
}

As for the segment selector:

impl<T> Entry<T> {
fn empty() -> Self { /* ...Others */ }

pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self {
let mut entry = Self::empty();
entry.handler_ptr_low = handler_ptr & 0xffff;
entry.handler_ptr_middle = (handler_ptr >> 16) & 0xffff;
entry.handler_ptr_upper = (handler_ptr >> 32) & 0xffffffff;

// Setting the segment selector of the entry
entry.segment_selector = segment_selector;
}
}

And finally, the options. For the options, we can create 2 functions like we're doing for the entries: one for exceptions and the other for maskable interrupts.

impl<T> Entry<T> {
fn empty() -> Self { /* ...Others */ }

pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self {
let mut entry = Self::empty();
entry.handler_ptr_low = handler_ptr & 0xffff;
entry.handler_ptr_middle = (handler_ptr >> 16) & 0xffff;
entry.handler_ptr_upper = (handler_ptr >> 32) & 0xffffffff;

// Setting the segment selector of the entry
entry.segment_selector = segment_selector;

entry.options = Options::exception();

entry
}
}

As for the Options::exception function, it must return valid options for an exception entry. That is, it must have bit 15 (present bit) set, bits 13..=14 (privilege levels) set to 0b00 (indicating the highest privilege), bits 8..=11 set to 0b1111 (indicating that the type is an exception). We can ignore the interrupt stack table offset for now.

impl Options {
// Creates the options for an exception entry in the IDT
fn exception() -> Self {
Self(0b1000111100000000)
}
}

Now providing a function pointer of a service routine and a segment selector is enough to create a valid entry that can be used in the IDT.

If you try compiling now, you'll get a bunch of errors about restricting the type parameter T for the Entry. We'll deal with this later.

The interrupt Function

This function is going to be very similar to the exception function. The only actual difference between this and exception is that this will return an entry whose Options's entry type field will indicate a maskable interrupt.

We can factor out this similarity before we even write the interrupt function:

impl<T> Entry<T> {
fn empty() -> Self { /* ...Others */ }

pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self {
/* DELETED
let mut entry = Self::empty();
entry.handler_ptr_low = handler_ptr & 0xffff;
entry.handler_ptr_middle = (handler_ptr >> 16) & 0xffff;
entry.handler_ptr_upper = (handler_ptr >> 32) & 0xffffffff;

// Setting the segment selector of the entry
entry.segment_selector = segment_selector;

entry.options = Options::exception();

entry
*/

// NEW:
Self::new(handler_ptr, segment_selector, Options::exception())
}

// NEW:
// Creates a new entry with the specified options
fn new(handler_ptr: T, segment_selector: SegmentSelector, options: Options) -> Self {
let mut entry = Self::empty();
entry.handler_ptr_low = handler_ptr & 0xffff;
entry.handler_ptr_middle = (handler_ptr >> 16) & 0xffff;
entry.handler_ptr_upper = (handler_ptr >> 32) & 0xffffffff;

entry.segment_selector = segment_selector;

entry.options = options;

entry
}
}

Creating a function for interrupt is now as simple as:

impl<T> Entry<T> {
fn empty() -> Self { /* ...Others */ }

pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self { /* ...Others */ }

// NEW:
// Creates a new entry for interrupts
pub fn interrupt(handler_ptr: T, segment_selector: SegmentSelector) -> Self {
Self::new(handler_ptr, segment_selector, Options::interrupt())
}

fn new(handler_ptr: T, segment_selector: SegmentSelector, options: Options) -> Self { /* ...Others */}
}

The options for a maskable interrupt, like the one for an exception, must have bit 15 (present bit) set, bits 13..=14 (privilege levels) set to 0b00 (indicating the highest privilege). Unlike the exception, the entry type bits 8..=11 must be set to 0b1110.

impl Options {
fn exception() -> Self {
Self(0b1000111100000000)
}

// Creates the options for a maskable interrupt entry in the IDT
fn interrupt() -> Self {
Self(0b1000111000000000)
}
}

Type Restrictions

Compiling the code now will give these errors:

error[E0369]: no implementation for `T & {integer}`
  --> blasterball/src/interrupts.rs:67:45
   |
67 |         entry.handler_ptr_low = handler_ptr & 0xffff;
   |                                 ----------- ^ ------ {integer}
   |                                 |
   |                                 T
   |
help: consider restricting type parameter `T`
   |
38 | impl<T: core::ops::BitAnd<i32, Output = u16>> Entry<T> {    
error[E0369]: no implementation for `T >> {integer}`
  --> blasterball/src/interrupts.rs:70:49
   |
70 |         entry.handler_ptr_middle = (handler_ptr >> 16) & 0xffff;
   |                                     ----------- ^^ -- {integer}
   |                                     |
   |                                     T
   |
help: consider restricting type parameter `T`
   |
38 | impl<T: core::ops::Shr<i32>> Entry<T> {
error[E0369]: no implementation for `T >> {integer}`
  --> blasterball/src/interrupts.rs:73:48
   |
73 |         entry.handler_ptr_upper = (handler_ptr >> 32) & 0xffffffff;
   |                                    ----------- ^^ -- {integer}
   |                                    |
   |                                    T
   |
help: consider restricting type parameter `T`
   |
38 | impl<T: core::ops::Shr<i32>> Entry<T> {

We're receiving these errors because T means any type and not just any type can perform bitwise operations on integers.

Type restrictions are mechanisms that Rust uses to ensure that the type passed to a generic function like this can actually do all the operations that the function requires that type to perform.

The first error points us to this line: entry.handler_ptr_low = handler_ptr & 0xffff;. The handler_ptr_low field is a u16. This means that the type T must be a type that can be involved in a bitwise operation and return a u16 as the result.

The type recommendation tells us to do this: impl<T: core::ops::BitAnd<i32, Output = u16>> Entry<T> {, meaning that type T must have implemented the BitAnd trait taking an i32 on the right-hand side of the & and outputting a u16.

The BitAnd trait indicates anything that the bitwise AND operator can be performed on. The reason we can use the bitwise AND operator on integers is that all integers implement this trait.

The hint specifies an i32 on the right-hand side because i32 is the default type that Rust assigns to an integer whose type hasn't been specified.

The same reasoning follows for the remaining errors. We're trying to perform the shift-bits-right operation with T but the compiler needs to be sure that T can perform those operations.

The BitAnd Trait

This trait defines behavior for any type that the bitwise AND operator can be used on.

It's defined like this:

pub trait BitAnd<Rhs = Self> {
type Output;

fn bitand(self, rhs: Rhs) -> Self::Output;
}

Implementing BitAnd<U> for a type T implies that x & y can be performed, where x is of type T and y is of type U. The type of the output will be the type specified in type Output.

The Shr Trait

This trait defines behavior for any type that the shift-right operator can be used on.

It's defined like this:

pub trait Shr<Rhs = Self> {
type Output;

fn shr(self, rhs: Rhs) -> Self::Output;
}

Implementing Shr<U> for a type T implies that x >> y can be performed, where x is of type T and y is of type U. The type of the output will be the type specified in type Output.

Resolving This Problem

Firstly, just for the purpose of making our lives easier, we first add types to the integers the handler pointer is operated on with:

impl<T> Entry<T> {
fn empty() -> Self {/* ...Others */ }

pub fn exception(handler_ptr: T, segment_selector: SegmentSelector) -> Self {/* ...Others */}

pub fn interrupt(handler_ptr: T, segment_selector: SegmentSelector) -> Self {/* ...Others */}

fn new(handler_ptr: T, segment_selector: SegmentSelector, options: Options) -> Self {
let mut entry = Self::empty();
// DELETED: entry.handler_ptr_low = handler_ptr & 0xffff;
entry.handler_ptr_low = handler_ptr & 0xffffu16; // NEW
entry.handler_ptr_middle = (handler_ptr >> 16u64) & 0xffffu16;
entry.handler_ptr_upper = (handler_ptr >> 32u32) & 0xffffffffu32;

entry.segment_selector = segment_selector;

entry.options = options;

entry
}
}

For entry.handler_ptr_low = handler_ptr & 0xffffu16; to work, the type must be able to bitwise AND with a u16 and output a u16.

For entry.handler_ptr_middle = (handler_ptr >> 16u64) & 0xffffu16; to work, the type must be able to perform a shift-right operation with a u64 on the right-hand side and output a u16.

For entry.handler_ptr_upper = (handler_ptr >> 32u32) & 0xffffffffu32; to work, the type must be able to perform a shift-right operation with a u32 and output a u32.

Adding the type restrictions:

use core::arch::asm;
use crate::gdt::SegmentSelector;
use core::marker::PhantomData;
use core::ops::{BitAnd, Shr}; // NEW

// DELETED: impl<T> Entry<T> {/* ...Others */}
impl<T: BitAnd<u16, Output=u16>
+ Shr<u64, Output=u16>
+ Shr<u32, Output=u32>> Entry<T> { /* ...Others */ }

Another compilation throws ownership errors of movement.

Function pointers can be copied just like regular pointers, so this shouldn't be a problem.

We just add another type restriction saying the type must implement Copy.

// DELETED: impl<T: BitAnd<u16, Output=u16> + Shr<u64, Output=u16> + Shr<u32, Output=u32>> Entry<T> { /* ...Others */ }
impl<T: BitAnd<u16, Output=u16>
+ Shr<u64, Output=u16>
+ Shr<u32, Output=u32>
+ Copy> Entry<T> { /* ...Others */ } // NEW

Compiling now yields no errors.

Implementing Required Traits For The Service Routine Types

The requirements specified above dictate that the service routine types must implement BitAnd and Shr.

We have to create our own implementations. Let's start with ServiceRoutine:

impl BitAnd<u16> for ServiceRoutine {
type Output = u16;

fn bitand(self, rhs: u16) -> u16 {
(self as u16) & rhs
}
}

Compiling will give an error:

error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
   --> blasterball/src/interrupts.rs:159:1
    |
159 | impl BitAnd<u16> for ServiceRoutine {
    | ^^^^^-----------^^^^^--------------
    | |    |               |
    | |    |               `extern "x86-interrupt" fn(InterruptStackFrame)` is not defined in the current crate
    | |    `u16` is not defined in the current crate
    | impl doesn't use only types from inside the current crate
    |
    = note: define and implement a trait or new type instead

We can't implement external traits on external types (and for very good reasons). This is known as the orphan rule.

To resolve this, we need to create new types that act as wrappers.

// DELETED: pub type ServiceRoutine = extern "x86-interrupt" fn(InterruptStackFrame);
#[derive(Clone, Copy)]
pub struct ServiceRoutine(extern "x86-interrupt" fn(InterruptStackFrame)); // NEW

// DELETED: pub type ServiceRoutineWithErrCode = extern "x86-interrupt" fn(InterruptStackFrame, u64);
#[derive(Clone, Copy)]
pub struct ServiceRoutineWithErrCode(extern "x86-interrupt" fn(InterruptStackFrame, u64)); // NEW

// DELETED: pub type ServiceRoutineWithNoReturn = extern "x86-interrupt" fn(InterruptStackFrame, u64) -> !;
#[derive(Clone, Copy)]
pub struct ServiceRoutineWithNoReturn(extern "x86-interrupt" fn(InterruptStackFrame, u64) -> !); // NEW

We can now implement the external traits on these new types.

impl BitAnd<u16> for ServiceRoutine {
type Output = u16;

fn bitand(self, rhs: u16) -> u16 {
// DELETED: (self as u16) & rhs
(self.0 as u16) & rhs // NEW
}
}

The exact same thing for the others:

impl BitAnd<u16> for ServiceRoutineWithErrCode {
type Output = u16;

fn bitand(self, rhs: u16) -> u16 {
(self.0 as u16) & rhs
}
}

impl BitAnd<u16> for ServiceRoutineWithNoReturn {
type Output = u16;

fn bitand(self, rhs: u16) -> u16 {
(self.0 as u16) & rhs
}
}

For the Shr<u64, Output=u16>:

impl Shr<u64> for ServiceRoutine {
type Output = u16;

fn shr(self, rhs: u64) -> u16 {
(self.0 as u64 >> rhs) as u16
}
}

impl Shr<u64> for ServiceRoutineWithErrCode {
type Output = u16;

fn shr(self, rhs: u64) -> u16 {
(self.0 as u64 >> rhs) as u16
}
}

impl Shr<u64> for ServiceRoutineWithNoReturn {
type Output = u16;

fn shr(self, rhs: u64) -> u16 {
(self.0 as u64 >> rhs) as u16
}
}

Lastly, for the Shr<u32, Output=u32>:

impl Shr<u32> for ServiceRoutine {
type Output = u32;

fn shr(self, rhs: u32) -> u32 {
(self.0 as u64 >> rhs as u64) as u32
}
}

impl Shr<u32> for ServiceRoutineWithErrCode {
type Output = u32;

fn shr(self, rhs: u32) -> u32 {
(self.0 as u64 >> rhs as u64) as u32
}
}

impl Shr<u32> for ServiceRoutineWithNoReturn {
type Output = u32;

fn shr(self, rhs: u32) -> u32 {
(self.0 as u64 >> rhs as u64) as u32
}
}

With these implementations, the service routines now meet all the requirements for using the Entry's exception and interrupt functions.

Take Away

  • The BitAnd trait defines behavior for any type that the bitwise AND operator (&) can be used on.
  • The Shr trait defines behavior for any type that the shift-right operator (>>) can be used on.

For the full code, go to the repo

In The Next Post

We'll be setting up the IDT.

References