Update blog post

This commit is contained in:
Philipp Oppermann
2016-05-27 21:18:16 +02:00
parent cbe034f3a5
commit 968ae00de7

View File

@@ -27,19 +27,26 @@ For the full list of exceptions check out the [OSDev wiki][exceptions].
### The Interrupt Descriptor Table
In order to catch and handle exceptions, we have to set up a so-called _Interrupt Descriptor Table_ (IDT). In this table we can specify a handler function for each CPU exception. The hardware uses this table directly, so we need to follow a predefined format. Each entry must have the following 16-byte structure:
Bits | Name | Description
--------|-----------------------------------|-----------------------------------
0-15 | Function Pointer [0:15] | The lower bits of the pointer to the handler function.
16-31 | GDT selector | Selector of a code segment in the GDT.
32-34 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called.
35-39 | Reserved (ignored) |
40 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called.
41-43 | must be one |
44 | must be zero |
45-46 | Descriptor Privilege Level (DPL) | The minimal required privilege level required for calling this handler.
47 | Present |
48-95 | Function Pointer [16:63] | The remaining bits of the pointer to the handler function.
95-127 | Reserved (ignored) |
Type| Name | Description
----|--------------------------|-----------------------------------
u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function.
u16 | GDT selector | Selector of a code segment in the GDT.
u16 | Options | (see below)
u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function.
u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function.
u32 | Reserved |
The options field has the following format:
Bits | Name | Description
------|-----------------------------------|-----------------------------------
0-2 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called.
3-7 | Reserved |
8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called.
9-11 | must be one |
12 | must be zero |
1314 | Descriptor Privilege Level (DPL) | The minimal required privilege level required for calling this handler.
15 | Present |
Each exception has a predefined IDT index. For example the invalid opcode exception has table index 6 and the page fault exception has table index 14. Thus, the hardware can automatically load the corresponding IDT entry for each exception. The [Exception Table][exceptions] in the OSDev wiki shows the IDT indexes of all exceptions in the “Vector nr.” column.
@@ -159,7 +166,7 @@ impl EntryOptions {
EntryOptions(options)
}
pub fn new() -> Self {
fn new() -> Self {
let mut options = Self::minimal();
options.set_present(true).disable_interrupts(true);
options
@@ -186,18 +193,14 @@ impl EntryOptions {
}
}
```
Note that the ranges are _exclusive_ the upper bound. The bit indexes are different from the values in the [above table], because the `option` field starts at bit 32. Thus e.g. the privilege level bits are bits 13 (`= 4532`) and 14 (`= 4632`).
The `minimal` function creates an `EntryOptions` type with only the “must-be-one” bits set. The `new` function, on the other hand, chooses reasonable defaults: It sets the present bit (why would you want to create a non-present entry?) and disables interrupts (normally we don't want that our exception handlers can be interrupted). By returning the self pointer from the `set_*` methods, we allow easy method chaining such as `options.set_present(true).disable_interrupts(true)`.
[above table]: {{% relref "#the-interrupt-descriptor-table" %}}
Note that the ranges are _exclusive_ the upper bound. The `minimal` function creates an `EntryOptions` type with only the “must-be-one” bits set. The `new` function, on the other hand, chooses reasonable defaults: It sets the present bit (why would you want to create a non-present entry?) and disables interrupts (normally we don't want that our exception handlers can be interrupted). By returning the self pointer from the `set_*` methods, we allow easy method chaining such as `options.set_present(true).disable_interrupts(true)`.
### Creating IDT Entries
Now we can add a function to create new IDT entries:
```rust
impl Entry {
pub fn new(gdt_selector: SegmentSelector, handler: HandlerFunc) -> Self {
fn new(gdt_selector: SegmentSelector, handler: HandlerFunc) -> Self {
let pointer = handler as u64;
Entry {
gdt_selector: gdt_selector,
@@ -231,24 +234,16 @@ It is important that the function is [diverging], i.e. it must never return. The
If our handler function returned normally, it would try to pop the return address from the stack. But it might get some completely different value then. For example, the CPU pushes an error code for some exceptions. Bad things would happen if we interpreted this error code as return address and jumped to it. Therefore interrupt handler functions must diverge[^fn-must-diverge].
[^fn-must-diverge]: Another reason is that overwrite the current register values by executing the handler function. Thus, the interrupted function looses its state and can't proceed anyway.
[^fn-must-diverge]: Another reason is that we overwrite the current register values by executing the handler function. Thus, the interrupted function looses its state and can't proceed anyway.
### IDT methods
TODO
Let's add a function to create new interrupt descriptor tables:
```rust
impl Idt {
pub fn new() -> Idt {
Idt([Entry::missing(); 16])
}
pub fn set_handler(&mut self, entry: u8, handler: extern "C" fn() -> !) {
self.0[entry as usize] = Entry::new(segmentation::cs(), handler);
}
pub fn options(&mut self, entry: u8) -> &mut EntryOptions {
&mut self.0[entry as usize].options
}
}
impl Entry {
@@ -264,14 +259,198 @@ impl Entry {
}
}
```
The `missing` function creates a non-present Entry. We could choose any values for the pointer and GDT selector fields as long as the present bit is not set.
### A static IDT
TODO lazy_static etc
However, a table with non-present entries is not very useful. So we create a `set_handler` method to add new handler functions:
```rust
impl Idt {
pub fn set_handler(&mut self, entry: u8, handler: HandlerFunc)
-> &mut EntryOptions
{
self.0[entry as usize] = Entry::new(segmentation::cs(), handler);
&mut self.0[entry as usize].options
}
}
```
The method overwrites the specified entry with the given handler function. We use the `segmentation::cs`[^fn-segmentation-cs] function of the [x86 crate] to get the current code segment descriptor. There's no need for different kernel code segments in long mode, so the current `cs` value should be always the right choice.
[x86 crate]: https://github.com/gz/rust-x86
[^fn-segmentation-cs]: The `segmentation::cs` function was [added](https://github.com/gz/rust-x86/pull/12) in version 0.7.0, so you might need to update your `x86` version in your `Cargo.toml`.
By returning a mutual reference to the entry's options, we allow the caller to override the default settings. For example, the caller could add a non-present entry by executing: `idt.set_handler(11, handler_fn).set_present(false)`.
### Loading the IDT
TODO
Now we're able to create new interrupt descriptor tables with registered handler functions. We just need a way to load an IDT, so that the CPU uses it. The x86 architecture uses a special register to store the active IDT and its length. In order to load a new IDT we need to update this register through the [lidt] instruction.
### Testing it
[lidt]: http://x86.renejeschke.de/html/file_module_x86_id_156.html
The `lidt` instruction expects a pointer to a special data structure, which specifies the start address of the IDT and its length:
Type | Name | Description
--------|---------|-----------------------------------
u16 | Limit | The maximum addressable byte in the table. Equal to the table size in bytes minus 1.
u64 | Offset | Virtual start address of the table.
This structure is already contained [in the x86 crate], so we don't need to create it ourselves. The same is true for the [lidt function]. So we just need to put the pieces together to create a `load` method:
[in the x86 crate]: http://gz.github.io/rust-x86/x86/dtables/struct.DescriptorTablePointer.html
[lidt function]: http://gz.github.io/rust-x86/x86/dtables/fn.lidt.html
```rust
impl Idt {
pub fn load(&self) {
use x86::dtables::{DescriptorTablePointer, lidt};
use core::mem::size_of;
let ptr = DescriptorTablePointer {
base: self as *const _ as u64,
limit: (size_of::<Self>() - 1) as u16,
};
unsafe { lidt(&ptr) };
}
}
```
The method does not need to modify the IDT, so it takes `self` by immutable reference. We convert this reference to an u64 and calculate the table size using [mem::size_of]. The additional `-1` is needed because the limit field has to be the maximum addressable byte.
[mem::size_of]: https://doc.rust-lang.org/nightly/core/mem/fn.size_of.html
Then we pass a pointer to our `ptr` structure to the `lidt` function, which calls the `lidt` assembly instruction in order to reload the IDT register. We need an unsafe block here, because the `lidt` assumes that the specified handler addresses are valid.
### Safety
But can we really guarantee that handler addresses are always valid? Let's see:
- The `Idt::new` function creates a new table populated with non-present entries. There's no way to set these entries to present from outside of this module, so this function is fine.
- The `set_handler` method allows us to overwrite a specified entry and point it to some handler function. Rust's type system guarantees that function pointers are always valid (as long as no `unsafe` is involved), so this function is fine, too.
There are no other public functions in the `idt` module (except `load`), so it should be safe… right?
Wrong! Imagine the following scenario:
```rust
pub fn init() {
load_idt();
cause_page_fault();
}
fn load_idt() {
let mut idt = idt::Idt::new();
idt.set_handler(14, page_fault_handler);
idt.load();
}
fn cause_page_fault() {
let x = [1,2,3,4,5,6,7,8,9];
unsafe{ *(0xdeadbeaf as *mut u64) = x[4]};
}
```
This won't work. If we're lucky, we get a triple fault and a boot loop. If we're unlucky, our kernel does strange things and fails at some completely unrelated place. So what's the problem here?
Well, we construct an IDT _on the stack_ and load it. It is perfectly valid until the end of the `load_idt` function. But as soon as the function returns, its stack frame can be reused by other functions. Thus, the IDT gets overwritten by the stack frame of the `cause_page_fault` function. It declares an array of integers, which overwrite the entries of our loaded IDT. So when the page fault occurs and the CPU tries to read the entry, it only sees some garbage values and issues a double fault, which escalates to a triple fault and a CPU reset.
Now imagine that the `cause_page_fault` function declared an array of pointers instead. If the present was coincidentally set, the CPU would jump to some random pointer and interpret random memory as code. This would be a clear violation of memory safety.
### Fixing the load method
So how do we fix it? We could make the load function itself `unsafe` and push the unsafety to the caller. However, there is a much better solution in this case. Let's formulate our requirement:
> The referenced IDT must be valid until a new IDT is loaded.
We can't know when the next IDT will be loaded. Maybe never. So in the worst case:
> The referenced IDT must be valid as long as our kernel runs.
This is exactly the definition of a [static lifetime]. So we can easily ensure that the IDT lives long enough by adding a `'static` requirement to the signature of the `load` function:
[static lifetime]: http://rustbyexample.com/scope/lifetime/static_lifetime.html
```rust
pub fn load(&'static self) {...}
```
That's it! Now the Rust compiler ensures that the above error can't happen anymore:
```
error: `idt` does not live long enough
--> src/interrupts/mod.rs:78:5
78 |> idt.load();
|> ^^^
note: reference must be valid for the static lifetime...
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 75:34
--> src/interrupts/mod.rs:75:35
75 |> let mut idt = idt::Idt::new();
|> ^
```
### A static IDT
So a valid IDT needs to have the `'static` lifetime. We can either create a `static` IDT or [deliberately leak a Box][into_raw]. We will most likely only need a single IDT for the foreseeable future, so let's try the `static` approach:
[into_raw]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.into_raw
```rust
// in src/interrupts/mod.rs
static IDT: idt::Idt = {
let mut idt = idt::Idt::new();
idt.set_handler(14, page_fault_handler);
idt
};
extern "C" fn page_fault_handler() -> ! {
println!("EXCEPTION: PAGE FAULT");
loop {}
}
```
Well… this doesn't work:
```
error: calls in statics are limited to constant functions, struct and enum
constructors [E0015]
...
error: blocks in statics are limited to items and tail expressions [E0016]
...
error: references in statics may only refer to immutable values [E0017]
...
```
Maybe it will work someday when `const` functions become more powerful. But until then, we have to find another solution.
### Lazy Statics to the Rescue
Fortunately the `lazy_static` macro exists. Instead of evaluating a `static` at compile time, the macro evaluates it when it's referenced the first time. Thus, we can do almost everything in it and are even able to read runtime values (e.g. the number of cores).
With `lazy_static`, we can define our IDT without problems:
```rust
lazy_static! {
static ref IDT: idt::Idt = {
let mut idt = idt::Idt::new();
idt.set_handler(14, page_fault_handler);
idt
};
}
```
Now we're ready to load it! We add a `interrupts::init` function, which takes care of it:
```rust
// in src/interrupts/mod.rs
pub fn init() {
assert_has_not_been_called!();
IDT.load();
}
```
We're using our `assert_has_not_been_called` macro to ensure that the `init` function is called only once. It doesn't really matter in this case since we would just load the table again. However, calling an initialization function twice is a sign of some bug, so we leave it in.
## Testing it
TODO page fault, some other fault to trigger double fault, kernel stack overflow
## Switching stacks