diff --git a/blog/post/double-faults.md b/blog/post/double-faults.md index 2b9f13e2..13b5d79e 100644 --- a/blog/post/double-faults.md +++ b/blog/post/double-faults.md @@ -12,7 +12,7 @@ In simplified terms, a double fault is a special exception that occurs when the [IDT]: {{% relref "09-catching-exceptions.md#the-interrupt-descriptor-table" %}} -A double fault behaves like a normal exception. It has the vector number `8` and we can define a normal handler function for it in the IDT. It is really important to provide a double fault handler, because if a double faults is unhandled a fatal _triple fault_ occurs. Triple faults can't be caught and most hardware reacts with a system reset. +A double fault behaves like a normal exception. It has the vector number `8` and we can define a normal handler function for it in the IDT. It is really important to provide a double fault handler, because if a double fault is unhandled a fatal _triple fault_ occurs. Triple faults can't be caught and most hardware reacts with a system reset. ### Triggering a Double Fault Let's provoke a double fault by triggering an exception for that we didn't define a handler function yet: @@ -189,7 +189,9 @@ When we try this code in QEMU, we see that the system enters a boot-loop again. So how can we avoid this problem? We can't omit the pushing of the exception stack frame, since the CPU itself does it. So we need to ensure somehow that the stack is always valid when a double fault exception occurs. Fortunately, the x86_64 architecture has a solution to this problem. ## Switching Stacks -The x86_64 architecture is able to switch to a predefined stack for some exceptions through an _Interrupt Stack Table_ (IST). The IST is a table of 7 pointers to known-good stacks. In Rust-like pseudo code: +The x86_64 architecture is able to switch to a predefined, known-good stack when an exception occurs. This switch happens at hardware level, so it can be performed before the CPU pushes the exception stack frame. + +This switching mechanism is implemented as an _Interrupt Stack Table_ (IST). The IST is a table of 7 pointers to known-good stacks. In Rust-like pseudo code: ```rust struct InterruptStackTable { @@ -197,11 +199,249 @@ struct InterruptStackTable { } ``` -For each exception handler, we can choose an IST stack through the options field in the [IDT entry]. For example, we could use the first stack in the IST for our double fault handler. Then the CPU would automatically switch to this stack _before_ it pushes anything. Thus, we are able to avoid the triple fault. +For each exception handler, we can choose an stack from the IST through the `options` field in the corresponding [Interrupt Descriptor Table entry]. For example, we could use the first stack in the IST for our double fault handler. Then the CPU would automatically switch to this stack whenever a double fault occurs. This switch would happen before anything is pushed, so it would prevent the triple fault. -[IDT entry]: {{% relref "09-catching-exceptions.md#the-interrupt-descriptor-table" %}} +[Interrupt Descriptor Table entry]: {{% relref "09-catching-exceptions.md#the-interrupt-descriptor-table" %}} -### The Task State Segment +### Allocating a new Stack +In order to fill an Interrupt Stack Table later, we need a way to allocate new stacks. Therefore we extend our `memory` module with a new `stack_allocator` submodule: + +```rust +// in src/memory/mod.rs + +mod stack_allocator; + +``` + +#### The `stack_allocator` Module +First, we create a new `StackAllocator` struct and a constructor function: + +```rust +// in src/memory/stack_allocator.rs + +use memory::paging::PageIter; + +pub struct StackAllocator { + range: PageIter, +} + +impl StackAllocator { + pub fn new(page_range: PageIter) -> StackAllocator { + StackAllocator { range: page_range } + } +} +``` +We create a simple `StackAllocator` that allocates stacks from a given range of pages (`PageIter` is an Iterator over a range of pages; we introduced it [in the kernel heap post].). +TODO: Instead of adding a `StackAllocator::new` function, we use a separate `new_stack_allocator` function. This way, we can re-export `StackAllocator` from the `memory` module without re-exporting the `new` function. + +[in the kernel heap post]: {{% relref "08-kernel-heap.md#mapping-the-heap" %}} + +In order to allocate new stacks, we add a `alloc_stack` method: + +```rust +// in src/memory/stack_allocator.rs + +use memory::paging::{self, Page, ActivePageTable}; +use memory::{PAGE_SIZE, FrameAllocator}; + +impl StackAllocator { + pub fn alloc_stack(&mut self, + active_table: &mut ActivePageTable, + frame_allocator: &mut FA, + size_in_pages: usize) + -> Option { + if size_in_pages == 0 { + return None; /* a zero sized stack makes no sense */ + } + + // clone the range, since we only want to change it on success + let mut range = self.range.clone(); + + // try to allocate the stack pages and a guard page + let guard_page = range.next(); + let stack_start = range.next(); + let stack_end = if size_in_pages == 1 { + stack_start + } else { + // choose the (size_in_pages-2)th element, since index + // starts at 0 and we already allocated the start page + range.nth(size_in_pages - 2) + }; + + match (guard_page, stack_start, stack_end) { + (Some(_), Some(start), Some(end)) => { + // success! write back updated range + self.range = range; + + // map stack pages to physical frames + for page in Page::range_inclusive(start, end) { + active_table.map(page, paging::WRITABLE, frame_allocator); + } + + // create a new stack + let top_of_stack = end.start_address() + PAGE_SIZE; + Some(Stack::new(top_of_stack, start.start_address())) + } + _ => None, /* not enough pages */ + } + } +} +``` +The method takes mutable references to the [ActivePageTable] and a [FrameAllocator], since it needs to map the new virtual stack pages to physical frames. The stack size is a multiple of the page size. + +Instead of operating directly on `self.range`, we [clone] it and only write it back on success. This way, subsequent stack allocations can still succeed if there are pages left. For example, a call with `size_in_pages = 3` can still succeed after a failed call with `size_in_pages = 100`. In order to be able to clone `PageIter`, we add a `#[derive(Clone)]` to its definition in `src/memory/paging/mod.rs`. + +The actual allocation is straightforward: First, we choose the next page as [guard page]. Then we choose the next `size_in_pages` pages as stack pages using [Iterator::nth]. If all three variables are `Some`, the allocation succeeded and we map the stack pages to physical frames using [ActivePageTable::map]. The guard page remains unmapped. + +Finally, we create and return a new `Stack`, which is defined as follows: + +```rust +// in src/memory/stack_allocator.rs + +#[derive(Debug)] +pub struct Stack { + top: StackPointer, + bottom: StackPointer, +} + +impl Stack { + fn new(top: usize, bottom: usize) -> Stack { + assert!(top > bottom); + Stack { + top: StackPointer::new(top), + bottom: StackPointer::new(bottom), + } + } + + pub fn top(&self) -> StackPointer { + self.top + } +} + +use core::nonzero::NonZero; + +#[derive(Debug, Clone, Copy)] +pub struct StackPointer(NonZero); + +impl StackPointer { + fn new(ptr: usize) -> StackPointer { + assert!(ptr != 0); + StackPointer(unsafe { NonZero::new(ptr) }) + } +} + +impl Into for StackPointer { + fn into(self) -> usize { + *self.0 + } +} +``` +The `Stack` struct describes a stack though its top and bottom pointers. A stack pointer can never be `0`, so we use the unstable [NonZero] wrapper for `StackPointer`. This wrapper is an optimization that tells the compiler that it can use the value `0` to differentiate enum variants. Thus, an `Option` has always the same size as a bare `usize` (the value `0` is used to store the `None` case). We will require this property when we create the Interrupt Stack Table later. + +Since `NonZero` is unstable, we need to add `#![feature(nonzero)]` in our `lib.rs`. + +[NonZero]: https://doc.rust-lang.org/nightly/core/nonzero/struct.NonZero.html + +#### The Memory Controller +Now we're already able to allocate a new double fault stack. However, we add one more level of abstraction to make things nicer. For that we add a `MemoryController` type to our `memory` module: + +```rust +// in src/memory/mod.rs + +pub use self::stack_allocator::{Stack, StackPointer}; + +pub struct MemoryController { + active_table: paging::ActivePageTable, + frame_allocator: AreaFrameAllocator, + stack_allocator: stack_allocator::StackAllocator, +} + +impl MemoryController { + pub fn alloc_stack(&mut self, size_in_pages: usize) -> Option { + let &mut MemoryController { ref mut active_table, + ref mut frame_allocator, + ref mut stack_allocator } = self; + stack_allocator.alloc_stack(active_table, frame_allocator, + size_in_pages) + } +} +``` +The `MemoryController` struct holds the three types that are required for `alloc_stack` and provides a simpler interface (only one argument). The `alloc_stack` wrapper just takes the tree types as `&mut` through [destructuring] and forwards them to the `stack_allocator`. Note that we're re-exporting the `Stack` and `StackPointer` types since they are returned by `alloc_stack`. + +The last step is to create a `stack_allocator` and return a `MemoryController` from `memory::init`: + +```rust +// in src/memory/mod.rs + +pub fn init(boot_info: &BootInformation) -> MemoryController { + ... + + let stack_allocator = { + let stack_alloc_start = heap_end_page + 1; + let stack_alloc_end = stack_alloc_start + 100; + let stack_alloc_range = Page::range_inclusive(stack_alloc_start, + stack_alloc_end); + stack_allocator::new_stack_allocator(stack_alloc_range) + }; + + MemoryController { + active_table: active_table, + frame_allocator: frame_allocator, + stack_allocator: stack_allocator, + } +} +``` +We create a new `StackAllocator` with a range of 100 pages starting right after the last heap page. + +In order to do arithmetic on pages (e.g. calculate the hundredth page after `stack_alloc_start`), we implement `Add` for `Page`: + +```rust +// in src/memory/paging/mod.rs + +impl Add for Page { + type Output = Page; + + fn add(self, rhs: usize) -> Page { + Page { number: self.number + rhs } + } +} +``` + +#### Allocating a Double Fault Stack +Now we can allocate a new double fault stack by passing the memory controller to our `interrupts::init` function: + +{{< highlight rust "hl_lines=8 11 12 21 22 23" >}} +// in src/lib.rs + +#[no_mangle] +pub extern "C" fn rust_main(multiboot_information_address: usize) { + ... + + // set up guard page and map the heap pages + let mut memory_controller = memory::init(boot_info); // new return type + + // initialize our IDT + interrupts::init(&mut memory_controller); // new argument + + ... +} + + +// in src/interrupts/mod.rs + +use memory::MemoryController; + +pub fn init(memory_controller: &mut MemoryController) { + let double_fault_stack = memory_controller.alloc_stack(1) + .expect("could not allocate double fault stack"); + + IDT.load(); +} +{{< / highlight >}} + +We allocate a 4096 bytes stack (one page) for our double fault handler. Now we just need some way to tell the CPU that it should use this stack for handling double faults. + +### The IST and TSS The Interrupt Stack Table (IST) is part of an old legacy structure called [Task State Segment] (TSS). The TSS used to hold various information (e.g. processor register state) about a task in 32-bit x86 and was for example used for [hardware context switching]. However, hardware context switching is no longer supported in 64-bit mode and the format of the TSS changed completely. [Task State Segment]: https://en.wikipedia.org/wiki/Task_state_segment @@ -223,9 +463,9 @@ Interrupt Stack Table | `[u64; 7]` (reserved) | `u16` I/O Map Base Address | `u16` -The _Privilege Stack Table_ is used by the CPU when the privilege level changes. For example, if an exception occurs while the CPU is in user mode (privilege level 3), the CPU normally switches to kernel mode (privilege level 0) before invoking the exception handler. In that case, the CPU would switch to the 0th stack in the Privilege Stack Table (since 0 is the target privilege level). We don't have any user mode programs yet, so we can safely ignore this table for now. +The _Privilege Stack Table_ is used by the CPU when the privilege level changes. For example, if an exception occurs while the CPU is in user mode (privilege level 3), the CPU normally switches to kernel mode (privilege level 0) before invoking the exception handler. In that case, the CPU would switch to the 0th stack in the Privilege Stack Table (since 0 is the target privilege level). We don't have any user mode programs yet, so we ignore this table for now. -Let's create a `TaskStateSegment` struct in new tss submodule: +Let's create a `TaskStateSegment` struct in a new `tss` submodule: ```rust // in src/interrupts/mod.rs @@ -234,8 +474,6 @@ mod tss; // in src/interrupts/tss.rs -use core::nonzero::NonZero; - #[derive(Debug)] #[repr(C, packed)] pub struct TaskStateSegment { @@ -248,27 +486,61 @@ pub struct TaskStateSegment { iomap_base: u16, } +use memory::StackPointer; + #[derive(Debug)] pub struct PrivilegeStackTable([Option; 3]); #[derive(Debug)] pub struct InterruptStackTable([Option; 7]); - -#[derive(Debug)] -pub struct StackPointer(NonZero); ``` -TODO lang item +We use [repr\(C)] for the struct since the order is fields is important. We also use `[repr(packed)]` because otherwise the compiler might insert additional padding between the `reserved_0` and `privilege_stacks` fields. +The `PrivilegeStackTable` and `InterruptStackTable` types are just newtype wrappers for arrays of `Option`. Here it becomes important that we implemented `NonZero` for `StackPointer`: Thus, an `Option` still has the required size of 64 bits. - However, it is a bit cumbersome to setup this mechanism. +Let's add a `TaskStateSegment::new` function that creates an empty TSS: -The mechanism consists of two main components: An _Interrupt Stack Table_ and a _Task State Segment_. +```rust +impl TaskStateSegment { + pub fn new() -> TaskStateSegment { + TaskStateSegment { + privilege_stacks: PrivilegeStackTable([None, None, None]), + interrupt_stacks: InterruptStackTable( + [None, None, None, None, None, None, None]), + iomap_base: 0, + reserved_0: 0, + reserved_1: 0, + reserved_2: 0, + reserved_3: 0, + } + } +} +``` +We also add a `InterruptStackTable::insert_stack` method, that inserts a given stack into a free table entry: + +```rust +use memory::Stack; + +impl InterruptStackTable { + pub fn insert_stack(&mut self, stack: Stack) -> Result { + // TSS index starts at 1, so we do a `zip(1..)` + for (entry, i) in self.0.iter_mut().zip(1..) { + if entry.is_none() { + *entry = Some(stack.top()); + return Ok(i); + } + } + Err(stack) + } +} +``` +The function iterates over the table and places the stack pointer in the first free entry. In the case of success, we return the table index of the inserted pointer. If there's no free entry left, we return the stack back to the caller as `Err`. + +#### Creating a TSS +Let's build a new TSS that contains our double fault stack in its Interrupt Stack Table. -Switching stacks -The Interrupt Stack Table -The Task State Segment The Global Descriptor Table (again) Putting it together What’s next? @@ -282,11 +554,3 @@ Let's start by creating a handler function for double faults: ``` Next, we need to register the double fault handler in our IDT: - - -Double faults also occur when an exception occurs while the CPU is trying to invoke an exception handler. For example, let's assume a divide-by-zero exception occurs but the OS accidentally [swapped out] the corresponding handler function. Now the CPU tries to call the divide-by-zero handler, which - - -A double fault occurs whenever the CPU fails to call an exception handler. On a high level it's like a catch-all handler, similar to `catch(...)` in C++ or `catch(Exception e)` in Java or C#. - -The most common case is that there isn't a handler defined in the IDT. However, a double fault also occurs if the exception handler lies on a unaccessible page of if the CPU fails to push the exception stack frame. diff --git a/src/interrupts/mod.rs b/src/interrupts/mod.rs index 246a70cb..fd4bfef4 100644 --- a/src/interrupts/mod.rs +++ b/src/interrupts/mod.rs @@ -8,7 +8,7 @@ // except according to those terms. use spin::Once; -use memory::StackPointer; +use memory::MemoryController; mod idt; mod tss; @@ -95,7 +95,10 @@ static IDT: Once = Once::new(); static TSS: Once = Once::new(); static GDT: Once = Once::new(); -pub fn init(double_fault_stack: StackPointer) { +pub fn init(memory_controller: &mut MemoryController) { + let double_fault_stack = memory_controller.alloc_stack(1) + .expect("could not allocate double fault stack"); + let mut double_fault_ist_index = 0; let tss = TSS.call_once(|| { diff --git a/src/interrupts/tss.rs b/src/interrupts/tss.rs index 47e38a53..ab9273bc 100644 --- a/src/interrupts/tss.rs +++ b/src/interrupts/tss.rs @@ -1,4 +1,4 @@ -use memory::StackPointer; +use memory::{Stack, StackPointer}; #[derive(Debug)] #[repr(C, packed)] @@ -16,7 +16,7 @@ impl TaskStateSegment { pub fn new() -> TaskStateSegment { TaskStateSegment { privilege_stacks: PrivilegeStackTable([None, None, None]), - interrupt_stacks: InterruptStackTable::new(), + interrupt_stacks: InterruptStackTable([None, None, None, None, None, None, None]), iomap_base: 0, reserved_0: 0, reserved_1: 0, @@ -33,18 +33,14 @@ pub struct PrivilegeStackTable([Option; 3]); pub struct InterruptStackTable([Option; 7]); impl InterruptStackTable { - pub fn new() -> InterruptStackTable { - InterruptStackTable([None, None, None, None, None, None, None]) - } - - pub fn insert_stack(&mut self, stack_pointer: StackPointer) -> Result { + pub fn insert_stack(&mut self, stack: Stack) -> Result { // TSS index starts at 1 for (entry, i) in self.0.iter_mut().zip(1..) { if entry.is_none() { - *entry = Some(stack_pointer); + *entry = Some(stack.top()); return Ok(i); } } - Err(stack_pointer) + Err(stack) } } diff --git a/src/lib.rs b/src/lib.rs index d53a90d3..74f77159 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,11 +55,8 @@ pub extern "C" fn rust_main(multiboot_information_address: usize) { // set up guard page and map the heap pages let mut memory_controller = memory::init(boot_info); - // initialize our IDT - let double_fault_stack = memory_controller.alloc_stack(1) - .expect("could not allocate double fault stack"); - interrupts::init(double_fault_stack); + interrupts::init(&mut memory_controller); unsafe { int!(3) }; diff --git a/src/memory/mod.rs b/src/memory/mod.rs index 25c11f4f..aa94d33b 100644 --- a/src/memory/mod.rs +++ b/src/memory/mod.rs @@ -9,7 +9,7 @@ pub use self::area_frame_allocator::AreaFrameAllocator; pub use self::paging::remap_the_kernel; -pub use self::stack_allocator::{StackAllocator, StackPointer}; +pub use self::stack_allocator::{Stack, StackPointer}; use self::paging::PhysicalAddress; use multiboot2::BootInformation; @@ -62,11 +62,10 @@ pub fn init(boot_info: &BootInformation) -> MemoryController { } let stack_allocator = { - let stack_alloc_start_page = heap_end_page + 1; - let stack_alloc_end_page = stack_alloc_start_page + 100; - let stack_alloc_page_range = Page::range_inclusive(stack_alloc_start_page, - stack_alloc_end_page); - stack_allocator::new_stack_allocator(stack_alloc_page_range) + let stack_alloc_start = heap_end_page + 1; + let stack_alloc_end = stack_alloc_start + 100; + let stack_alloc_range = Page::range_inclusive(stack_alloc_start, stack_alloc_end); + stack_allocator::new_stack_allocator(stack_alloc_range) }; MemoryController { @@ -79,11 +78,11 @@ pub fn init(boot_info: &BootInformation) -> MemoryController { pub struct MemoryController { active_table: paging::ActivePageTable, frame_allocator: AreaFrameAllocator, - stack_allocator: StackAllocator, + stack_allocator: stack_allocator::StackAllocator, } impl MemoryController { - pub fn alloc_stack(&mut self, size_in_pages: usize) -> Result { + pub fn alloc_stack(&mut self, size_in_pages: usize) -> Option { let &mut MemoryController { ref mut active_table, ref mut frame_allocator, ref mut stack_allocator } = self; diff --git a/src/memory/paging/mod.rs b/src/memory/paging/mod.rs index 777d6202..2d7b48d7 100644 --- a/src/memory/paging/mod.rs +++ b/src/memory/paging/mod.rs @@ -70,6 +70,7 @@ impl Add for Page { } } +#[derive(Debug, Clone)] pub struct PageIter { start: Page, end: Page, diff --git a/src/memory/stack_allocator.rs b/src/memory/stack_allocator.rs index 2e7496fb..cc29d577 100644 --- a/src/memory/stack_allocator.rs +++ b/src/memory/stack_allocator.rs @@ -15,37 +15,73 @@ impl StackAllocator { active_table: &mut ActivePageTable, frame_allocator: &mut FA, size_in_pages: usize) - -> Result { + -> Option { if size_in_pages == 0 { - return Err(()); + return None; } - let _guard_page = self.range.next().ok_or(())?; + let mut range = self.range.clone(); - let stack_start = self.range.next().ok_or(())?; + // try to allocate the stack pages and a guard page + let guard_page = range.next(); + let stack_start = range.next(); let stack_end = if size_in_pages == 1 { stack_start } else { - self.range.nth(size_in_pages - 1).ok_or(())? + range.nth(size_in_pages - 2) }; - for page in Page::range_inclusive(stack_start, stack_end) { - active_table.map(page, paging::WRITABLE, frame_allocator); - } + match (guard_page, stack_start, stack_end) { + (Some(_), Some(start), Some(end)) => { + // success! write back updated range + self.range = range; - let top_of_stack = stack_end.start_address() + PAGE_SIZE; - StackPointer::new(top_of_stack).ok_or(()) + // map stack pages to physical frames + for page in Page::range_inclusive(start, end) { + active_table.map(page, paging::WRITABLE, frame_allocator); + } + + // create a new stack + let top_of_stack = end.start_address() + PAGE_SIZE; + Some(Stack::new(top_of_stack, start.start_address())) + } + _ => None, /* not enough pages */ + } } } #[derive(Debug)] +pub struct Stack { + top: StackPointer, + bottom: StackPointer, +} + +impl Stack { + fn new(top: usize, bottom: usize) -> Stack { + assert!(top > bottom); + Stack { + top: StackPointer::new(top), + bottom: StackPointer::new(bottom), + } + } + + pub fn top(&self) -> StackPointer { + self.top + } +} + +#[derive(Debug, Clone, Copy)] pub struct StackPointer(NonZero); impl StackPointer { - fn new(ptr: usize) -> Option { - match ptr { - 0 => None, - ptr => Some(StackPointer(unsafe { NonZero::new(ptr) })), - } + fn new(ptr: usize) -> StackPointer { + assert!(ptr != 0); + StackPointer(unsafe { NonZero::new(ptr) }) + } +} + +impl Into for StackPointer { + fn into(self) -> usize { + *self.0 } }