Demilade Sonuga's blog
All postsThe Interrupt Descriptor Table III
In the previous post, we started modeling the IDT but stopped at the problem of determining how to appropriately associate different types of service routines with entries. This is what we're going to resolve now.
The Current Unsafe Situation
The problem was mentioned in the previous post but to make it more concrete, consider this code:
fn add_double_fault_handler(idt: &mut IDT) {
idt.double_fault.set_handler(entry with address of breakpoint_handler);
}
extern "x86-interrupt" fn breakpoint_handler(frame: InterruptStackFrame) {
/* Some Code */
}
In the above code, the breakpoint service routine is used as the routine for the double fault entry. Depending on the situation at hand, this will could lead to hard-to-track bugs because projects like these are usually big and the problem could be anywhere.
We need to remember that the whole point and primary reason for Rust and all that it offers are to protect us from ourselves. The truth is we programmers are dangerous to ourselves and we're often the number one cause of our problems. One small mistake here and one small mistake there and BOOM! Nothing works anymore. Then hours are wasted looking for bugs hiding in the open.
The point of Rust is to prevent errors like that as much as possible and this silly mistake displayed here is one of those avoidable problems.
What We Need
First, we need to ask ourselves: what exactly is it that we need to tell the compiler that "only entries with this kind of service routines can be in this part of the table"?
In other words, we need our code to look like this:
#[repr(C, align(8))]
pub struct IDT {
pub divide_by_zero: Entry with regular ServiceRoutine,
pub debug: Entry with regular ServiceRoutine,
pub nmi_interrupt: Entry with regular ServiceRoutine,
pub breakpoint: Entry with regular ServiceRoutine,
pub overflow: Entry with regular ServiceRoutine,
pub bound_range_exceeded: Entry with regular ServiceRoutine,
pub invalid_opcode: Entry with regular ServiceRoutine,
pub device_not_available: Entry with regular ServiceRoutine,
pub double_fault: Entry with ServiceRoutineWithNoReturn,
pub coprocessor_segment_overrun: Entry with regular ServiceRoutine,
pub invalid_tss: Entry with ServiceRoutineWithErrCode,
pub segment_not_present: Entry with ServiceRoutineWithErrCode,
pub stack_segment_fault: Entry with ServiceRoutineWithErrCode,
pub general_protection: Entry with ServiceRoutineWithErrCode,
pub page_fault: Entry with ServiceRoutineWithErrCode,
reserved1: Entry with regular ServiceRoutine,
pub floating_point_error: Entry with regular ServiceRoutine,
pub alignment_check: Entry with ServiceRoutineWithErrCode,
pub machine_check: Entry with ServiceRoutineWithNoReturn,
pub simd_floating_point_exception: Entry with regular ServiceRoutine,
pub virtualization_exception: Entry with regular ServiceRoutine,
pub control_protection_exception: Entry with ServiceRoutineWithErrCode,
reserved2: [Entry with regular ServiceRoutine; 10],
pub interrupts: [Entry with regular ServiceRoutine; 256 - 32]
}
That is, we want the type Entry
to be associated with the type of service routine it's referring to.
An Answer
There are probably many ways of resolving this problem but the one we'll use here is Rust's generics. Generics is just one of Rust's mechanisms of polymorphism, that is allowing different types to be in the same context.
A Few Things About Generics
If you aren't too familiar with generics, take a look at this:
enum Option<T> {
Some(T),
None
}
This is the definition of the Option
enum. It has a single generic parameter T
. This definition just says
"An option is either a None
value or a Some(x)
where x
is a value of any type".
It's because of this generic definition we can have Option
s of many different types like Some(24)
,
Some("hello generics")
, Some(32.0f32)
, and so on.
Result
too is defined similarly:
enum Result<T, E> {
Ok(T),
Err(E)
}
This just says "a value of type Result
is either Ok(x)
where x
is a value of any type or it's Err(y)
where y
is a value of any type".
Imagine Result
was defined like this:
enum Result {
Ok(i32),
Err(i32)
}
If Result
was defined like this, then the Ok
and Err
variants will only be able to hold 32-bit signed
integers, not strings, not floats, not anything else. This completely defeats the usefulness of Result
because Result
is supposed to be a type that indicates a potential failure in any context and not just
when dealing with i32
s.
It's because of the generics that types like Option
and Result
can be used with so many types.
If you want to dive deeper into this generic stuff, check the references.
Entries With Generics
Just like we indicate a result holding an i32
with Result<i32>
, we can indicate an entry holding an
address of a regular service routine with Entry<ServiceRoutine>
, an entry with a service routine that takes
an error code with Entry<ServiceRoutineWithErrCode>
, an entry with a service routine that never returns
with Entry<ServiceRoutineWithNoReturn>
.
All we need to do is to add a generic parameter to Entry
and specify these entry restrictions in the IDT
definition.
In interrupts.rs
#[repr(C)]
#[derive(Clone, Copy)]
// DELETED: pub struct Entry { /* ...Others */ }
pub struct Entry<T> { /* ...Others */ } // NEW
// DELETED: impl Entry { /* ...Others */ }
impl<T> Entry<T> { /* ...Others */ } // NEW
/* DELETED:
#[repr(C, align(8))]
pub struct IDT { /* ...Others */ }
*/
// NEW:
#[repr(C, align(8))]
pub struct IDT {
pub divide_by_zero: Entry<ServiceRoutine>,
pub debug: Entry<ServiceRoutine>,
pub nmi_interrupt: Entry<ServiceRoutine>,
pub breakpoint: Entry<ServiceRoutine>,
pub overflow: Entry<ServiceRoutine>,
pub bound_range_exceeded: Entry<ServiceRoutine>,
pub invalid_opcode: Entry<ServiceRoutine>,
pub device_not_available: Entry<ServiceRoutine>,
pub double_fault: Entry<ServiceRoutineWithNoReturn>,
pub coprocessor_segment_overrun: Entry<ServiceRoutine>,
pub invalid_tss: Entry<ServiceRoutineWithErrCode>,
pub segment_not_present: Entry<ServiceRoutineWithErrCode>,
pub stack_segment_fault: Entry<ServiceRoutineWithErrCode>,
pub general_protection: Entry<ServiceRoutineWithErrCode>,
pub page_fault: Entry<ServiceRoutineWithErrCode>,
reserved1: Entry<ServiceRoutine>,
pub floating_point_error: Entry<ServiceRoutine>,
pub alignment_check: Entry<ServiceRoutineWithErrCode>,
pub machine_check: Entry<ServiceRoutineWithNoReturn>,
pub simd_floating_point_exception: Entry<ServiceRoutine>,
pub virtualization_exception: Entry<ServiceRoutine>,
pub control_protection_exception: Entry<ServiceRoutineWithErrCode>,
reserved2: [Entry<ServiceRoutine>; 10],
pub interrupts: [Entry<ServiceRoutine>; 256 - 32]
}
Now, we've been able to encode the restrictions on what type of service routines should be in each entry.
The fictional code snippet in the problem definition at the beginning will not even be compiled by the compiler
anymore because the type of breakpoint_handler
(Entry<ServiceRoutine>
) does not match the type required by
the double fault handler (Entry<ServiceRoutineWithNoReturn>
).
If you try running the code now, you'll get an error:
error[E0392]: parameter `T` is never used
--> blasterball/src/interrupts.rs:20:18
|
20 | pub struct Entry<T> {
| ^ unused parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`
= help: if you intended `T` to be a const parameter, use `const T: usize` instead
Apparently, a struct with a generic parameter must have that generic used in the struct.
The thing is we don't use the generic for anything in the struct, it's just there to provide some restrictions on what service routines can and can't be used with an entry.
To resolve this, we place a dummy field to make use of the generic:
use core::arch::asm;
use crate::gdt::SegmentSelector;
use core::marker::PhantomData; // NEW
// ...Others
#[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,
// Dummy field to resolve problem of no field making
// use of generic type
phantom: PhantomData<T> // NEW
}
impl<T> Entry<T> {
// Creates an empty entry
fn empty() -> Self {
Self {
handler_ptr_low: 0,
segment_selector: SegmentSelector(0),
options: Options(0),
handler_ptr_middle: 0,
handler_ptr_upper: 0,
reserved: 0,
phantom: PhantomData // NEW
}
}
}
PhantomData
is a zero-sized type. The whole purpose of this struct is to solve this problem.
If you want to learn more about it, check the references.
Compiling again will give another bunch of visibility errors.
Resolving:
#[repr(C)]
// DELETED: struct InterruptStackFrame { /* ...Others */ }
pub struct InterruptStackFrame { /* ...Others */ } // NEW
// DELETED: type ServiceRoutine = extern "x86-interrupt" fn(InterruptStackFrame);
pub type ServiceRoutine = extern "x86-interrupt" fn(InterruptStackFrame); // NEW
// DELETED: type ServiceRoutineWithErrCode = extern "x86-interrupt" fn(InterruptStackFrame, u64);
pub type ServiceRoutineWithErrCode = extern "x86-interrupt" fn(InterruptStackFrame, u64); // NEW
// DELETED: type ServiceRoutineWithNoReturn = extern "x86-interrupt" fn(InterruptStackFrame, u64) -> !;
pub type ServiceRoutineWithNoReturn = extern "x86-interrupt" fn(InterruptStackFrame, u64) -> !; // NEW
The compilation is now successful.
Take Away
- Generics are used when multiple types can be used in the same context.
For the full code, go to the repo
In The Next Post
We'll continue with the IDT
References
- Generics: https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/generics.html
- PhantomData: https://doc.rust-lang.org/std/marker/struct.PhantomData.html
- PhantomData: https://doc.rust-lang.org/nomicon/phantom-data.html