Demilade Sonuga's blog
All postsThe Interrupt Descriptor Table IV
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
:
exception
: For creating a new entry for an exception.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
- BitAnd: https://doc.rust-lang.org/core/ops/trait.BitAnd.html
- Shr: https://doc.rust-lang.org/core/ops/trait.Shr.html