; 7],
+}
+```
+
+For each exception handler, we can choose an stack from the IST through the `options` field in the corresponding [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 whenever a double fault occurs. This switch would happen before anything is pushed, so it would prevent the triple fault.
+
+[IDT entry]: ./second-edition/posts/06-cpu-exceptions/index.md#the-interrupt-descriptor-table
+
+### 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 mode 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
+[hardware context switching]: http://wiki.osdev.org/Context_Switching#Hardware_Context_Switching
+
+On x86_64, the TSS no longer holds any task specific information at all. Instead, it holds two stack tables (the IST is one of them). The only common field between the 32-bit and 64-bit TSS is the pointer to the [I/O port permissions bitmap].
+
+[I/O port permissions bitmap]: https://en.wikipedia.org/wiki/Task_state_segment#I.2FO_port_permissions
+
+The 64-bit TSS has the following format:
+
+Field | Type
+------ | ----------------
+(reserved) | `u32`
+Privilege Stack Table | `[u64; 3]`
+(reserved) | `u64`
+Interrupt Stack Table | `[u64; 7]`
+(reserved) | `u64`
+(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 ignore this table for now.
+
+### Creating a TSS
+Let's create a new TSS that contains a separate double fault stack in its interrupt stack table. For that we need a TSS struct. Fortunately, the `x86_64` crate already contains a [`TaskStateSegment` struct] that we can use.
+
+[`TaskStateSegment` struct]: https://docs.rs/x86_64/0.2.3/x86_64/structures/tss/struct.TaskStateSegment.html
+
+We create the TSS in a new `gdt` module (the name will make sense later):
+
+```rust
+// in src/lib.rs
+
+pub mod gdt;
+
+// in src/gdt.rs
+
+use x86_64::VirtAddr;
+use x86_64::structures::tss::TaskStateSegment;
+
+pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;
+
+lazy_static! {
+ static ref TSS: TaskStateSegment = {
+ let mut tss = TaskStateSegment::new();
+ tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
+ const STACK_SIZE: usize = 4096;
+ static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
+
+ let stack_start = VirtAddr::from_ptr(unsafe { &STACK });
+ let stack_end = stack_start + STACK_SIZE;
+ stack_end
+ };
+ tss
+ };
+}
+```
+
+We use `lazy_static` because Rust's const evaluator is not yet powerful enough to do this initialization at compile time. We define that the 0th IST entry is the double fault stack (any other IST index would work too). Then we write the top address of a double fault stack to the 0th entry. We write the top address because stacks on x86 grow downwards, i.e. from high addresses to low addresses.
+
+We don't have implemented memory management yet, so we don't have a proper way to allocate a new stack. Instead, we use a `static` array as stack storage for now. We will replace this with a proper stack allocation in a later post. It is important that it is a `static mut` and not an immutable `static`, because otherwise the bootloader will map it to a read-only page.
+
+Note that this double fault stack has no guard page that protects against stack overflow. This means that we should not do anything stack intensive in our double fault handler because a stack overflow might corrupt the memory below the stack.
+
+#### Loading the TSS
+Now that we created a new TSS, we need a way to tell the CPU that it should use it. Unfortunately this is a bit cumbersome, since the TSS uses the segmentation system (for historical reasons). Instead of loading the table directly, we need to add a new segment descriptor to the [Global Descriptor Table] \(GDT). Then we can load our TSS invoking the [`ltr` instruction] with the respective GDT index. (This is the reason why we named our module `gdt`.)
+
+[Global Descriptor Table]: http://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/
+[`ltr` instruction]: http://x86.renejeschke.de/html/file_module_x86_id_163.html
+
+### The Global Descriptor Table
+The Global Descriptor Table (GDT) is a relict that was used for [memory segmentation] before paging became the de facto standard. It is still needed in 64-bit mode for various things such as kernel/user mode configuration or TSS loading.
+
+[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
+
+The GDT is a structure that contains the _segments_ of the program. It was used on older architectures to isolate programs from each other, before paging became the standard. For more information about segmentation check out the equally named chapter of the free [“Three Easy Pieces” book]. While segmentation is no longer supported in 64-bit mode, the GDT still exists. It is mostly used for two things: Switching between kernel space and user space, and loading a TSS structure.
+
+[“Three Easy Pieces” book]: http://pages.cs.wisc.edu/~remzi/OSTEP/
+
+#### Creating a GDT
+Let's create a static `GDT` that includes a segment for our `TSS` static:
+
+```rust
+// in src/gdt.rs
+
+use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor};
+
+lazy_static! {
+ static ref GDT: GlobalDescriptorTable = {
+ let mut gdt = GlobalDescriptorTable::new();
+ gdt.add_entry(Descriptor::kernel_code_segment());
+ gdt.add_entry(Descriptor::tss_segment(&TSS));
+ gdt
+ };
+}
+```
+
+We use `lazy_static` again, because Rust's const evaluator is not powerful enough yet. We create a new GDT with a code segment and a TSS segment.
+
+#### Loading the GDT
+
+To load our GDT we create a new `gdt::init` function, that we call from our `_start` function:
+
+```rust
+// in src/gdt.rs
+
+pub fn init() {
+ GDT.load();
+}
+
+// in src/main.rs
+
+#[cfg(not(test))]
+#[no_mangle]
+pub extern "C" fn _start() -> ! {
+ println!("Hello World{}", "!");
+
+ blog_os::gdt::init();
+ init_idt();
+
+ […]
+}
+
+```
+
+Now our GDT is loaded, but we still see the boot loop on stack overflow.
+
+### The final Steps
+
+The problem is that the GDT segments are not yet active becaues the segment and TSS registers still contain the values from the old GDT. We also need to modify the double fault IDT entry so that it uses the new stack.
+
+In summary, we need to do the following:
+
+1. **Reload code segment register**: We changed our GDT, so we should reload `cs`, the code segment register. This required since the old segment selector could point a different GDT descriptor now (e.g. a TSS descriptor).
+2. **Load the TSS** : We loaded a GDT that contains a TSS selector, but we still need to tell the CPU that it should use that TSS.
+3. **Update the IDT entry**: As soon as our TSS is loaded, the CPU has access to a valid interrupt stack table (IST). Then we can tell the CPU that it should use our new double fault stack by modifying our double fault IDT entry.
+
+For the first two steps, we need access to the `code_selector` and `tss_selector` variables in our `gdt::init` function. We can achieve this by making them part of the static through a new `Selectors` struct:
+
+```rust
+// in src/gdt.rs
+
+use x86_64::structures::gdt::SegmentSelector;
+
+lazy_static! {
+ static ref GDT: (GlobalDescriptorTable, Selectors) = {
+ let mut gdt = GlobalDescriptorTable::new();
+ let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());
+ let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS));
+ (gdt, Selectors { code_selector, tss_selector })
+ };
+}
+
+struct Selectors {
+ code_selector: SegmentSelector,
+ tss_selector: SegmentSelector,
+}
+```
+
+Now we can use the selectors to reload the `cs` segment register and load our `TSS`:
+
+```rust
+// in src/gdt.rs
+
+pub fn init() {
+ use x86_64::instructions::segmentation::set_cs;
+ use x86_64::instructions::tables::load_tss;
+
+ GDT.0.load();
+ unsafe {
+ set_cs(GDT.1.code_selector);
+ load_tss(GDT.1.tss_selector);
+ }
+}
+```
+
+We reload the code segment register using [`set_cs`] and to load the TSS using [`load_tss`].
+
+[`set_cs`]: https://docs.rs/x86_64/0.2.3/x86_64/instructions/segmentation/fn.set_cs.html
+[`load_tss`]: https://docs.rs/x86_64/0.2.3/x86_64/instructions/tables/fn.load_tss.html
+
+Now that we loaded a valid TSS and interrupt stack table, we can set the stack index for our double fault handler in the IDT:
+
+```rust
+// in src/main.rs
+
+lazy_static! {
+ static ref IDT: Idt = {
+ let mut idt = Idt::new();
+ idt.breakpoint.set_handler_fn(breakpoint_handler);
+ unsafe {
+ idt.double_fault.set_handler_fn(double_fault_handler)
+ .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX); // new
+ }
+
+ idt
+ };
+}
+```
+
+The `set_stack_index` method is unsafe because the the caller must ensure that the used index is valid and not already used for another exception.
+
+That's it! Now the CPU should switch to the double fault stack whenever a double fault occurs. Thus, we are able to catch _all_ double faults, including kernel stack overflows:
+
+
+
+From now on we should never see a triple fault again!
+
+To ensure that we don't accidentally break the above, we should add a integration test for this. We don't show the code here for space reasons, but you can find it [in this gist][stack overflow test]. The idea is to do a `serial_println!("ok");` from the double fault handler to ensure that it is called. The rest of the file is is very similar to our `main.rs`.
+
+[stack overflow test]: https://gist.github.com/phil-opp/9600f367f10615219f3f22110a9a92eb
+
+## Summary
+In this post we learned what a double fault is and under which conditions it occurs. We added a basic double fault handler that prints an error message and added an integration test for it.
+
+We also enabled the hardware supported stack switching on double fault exceptions so that it also works on stack overflow. While implementing it, we learned about the task state segment (TSS), the contained interrupt stack table (IST), and the global descriptor table (GDT), which was used for segmentation on older architectures.
+
+## What's next?
+The next posts will explain how to handle interrupts from external devices such as timers, keyboards, or network controllers. These hardware interrupts are very similar to exceptions, e.g. they are also dispatched through the IDT. However, unlike exceptions, they don't arise directly on the CPU. Instead, an _interrupt controller_ aggregates these interrupts and forwards them to CPU depending on their priority. In the next posts we will explore the two interrupt controller variants on x86: the [Intel 8259] \(“PIC”) and the [APIC]. This will allow us to react to keyboard and mouse input.
+
+[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259
+[APIC]: https://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller
diff --git a/blog/content/second-edition/posts/07-double-faults/qemu-catch-double-fault.png b/blog/content/second-edition/posts/07-double-faults/qemu-catch-double-fault.png
new file mode 100644
index 00000000..7ed508d4
Binary files /dev/null and b/blog/content/second-edition/posts/07-double-faults/qemu-catch-double-fault.png differ
diff --git a/blog/content/second-edition/posts/07-double-faults/qemu-double-fault-on-stack-overflow.png b/blog/content/second-edition/posts/07-double-faults/qemu-double-fault-on-stack-overflow.png
new file mode 100644
index 00000000..07084a85
Binary files /dev/null and b/blog/content/second-edition/posts/07-double-faults/qemu-double-fault-on-stack-overflow.png differ
diff --git a/blog/templates/second-edition/index.html b/blog/templates/second-edition/index.html
index a5cca2b5..601192e8 100644
--- a/blog/templates/second-edition/index.html
+++ b/blog/templates/second-edition/index.html
@@ -37,6 +37,7 @@
Exceptions
{{ macros::post_link(page=posts.5) }}
+ {{ macros::post_link(page=posts.6) }}
diff --git a/src/bin/test-exception-double-fault-stack-overflow.rs b/src/bin/test-exception-double-fault-stack-overflow.rs
new file mode 100644
index 00000000..50ea960f
--- /dev/null
+++ b/src/bin/test-exception-double-fault-stack-overflow.rs
@@ -0,0 +1,84 @@
+#![feature(panic_implementation)]
+#![feature(abi_x86_interrupt)]
+#![no_std]
+#![cfg_attr(not(test), no_main)]
+#![cfg_attr(test, allow(dead_code, unused_macros, unused_imports))]
+
+#[macro_use]
+extern crate blog_os;
+extern crate x86_64;
+#[macro_use]
+extern crate lazy_static;
+
+use blog_os::exit_qemu;
+use core::panic::PanicInfo;
+
+#[cfg(not(test))]
+#[no_mangle]
+#[allow(unconditional_recursion)]
+pub extern "C" fn _start() -> ! {
+ blog_os::gdt::init();
+ init_idt();
+
+ fn stack_overflow() {
+ stack_overflow(); // for each recursion, the return address is pushed
+ }
+
+ // trigger a stack overflow
+ stack_overflow();
+
+ serial_println!("failed");
+ serial_println!("No exception occured");
+
+ unsafe {
+ exit_qemu();
+ }
+
+ loop {}
+}
+
+/// This function is called on panic.
+#[cfg(not(test))]
+#[panic_implementation]
+#[no_mangle]
+pub fn panic(info: &PanicInfo) -> ! {
+ serial_println!("failed");
+ serial_println!("{}", info);
+
+ unsafe {
+ exit_qemu();
+ }
+
+ loop {}
+}
+
+use x86_64::structures::idt::{ExceptionStackFrame, Idt};
+
+lazy_static! {
+ static ref IDT: Idt = {
+ let mut idt = Idt::new();
+ unsafe {
+ idt.double_fault
+ .set_handler_fn(double_fault_handler)
+ .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX);
+ }
+
+ idt
+ };
+}
+
+pub fn init_idt() {
+ IDT.load();
+}
+
+extern "x86-interrupt" fn double_fault_handler(
+ _stack_frame: &mut ExceptionStackFrame,
+ _error_code: u64,
+) {
+ serial_println!("ok");
+
+ unsafe {
+ exit_qemu();
+ }
+ loop {}
+}
diff --git a/src/gdt.rs b/src/gdt.rs
new file mode 100644
index 00000000..be448606
--- /dev/null
+++ b/src/gdt.rs
@@ -0,0 +1,48 @@
+use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};
+use x86_64::structures::tss::TaskStateSegment;
+use x86_64::VirtAddr;
+
+pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;
+
+lazy_static! {
+ static ref TSS: TaskStateSegment = {
+ let mut tss = TaskStateSegment::new();
+ tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
+ const STACK_SIZE: usize = 4096;
+ static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
+
+ let stack_start = VirtAddr::from_ptr(unsafe { &STACK });
+ let stack_end = stack_start + STACK_SIZE;
+ stack_end
+ };
+ tss
+ };
+ static ref GDT: (GlobalDescriptorTable, Selectors) = {
+ let mut gdt = GlobalDescriptorTable::new();
+ let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());
+ let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS));
+ (
+ gdt,
+ Selectors {
+ code_selector,
+ tss_selector,
+ },
+ )
+ };
+}
+
+struct Selectors {
+ code_selector: SegmentSelector,
+ tss_selector: SegmentSelector,
+}
+
+pub fn init() {
+ use x86_64::instructions::segmentation::set_cs;
+ use x86_64::instructions::tables::load_tss;
+
+ GDT.0.load();
+ unsafe {
+ set_cs(GDT.1.code_selector);
+ load_tss(GDT.1.tss_selector);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8260af01..affb8720 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,6 +13,7 @@ extern crate array_init;
#[cfg(test)]
extern crate std;
+pub mod gdt;
pub mod serial;
pub mod vga_buffer;
diff --git a/src/main.rs b/src/main.rs
index 47b305ab..361b7e7b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,10 +19,15 @@ use core::panic::PanicInfo;
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
+ blog_os::gdt::init();
init_idt();
- // invoke a breakpoint exception
- x86_64::instructions::int3();
+ fn stack_overflow() {
+ stack_overflow(); // for each recursion, the return address is pushed
+ }
+
+ // trigger a stack overflow
+ stack_overflow();
println!("It did not crash!");
loop {}
@@ -43,6 +48,12 @@ lazy_static! {
static ref IDT: Idt = {
let mut idt = Idt::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
+ unsafe {
+ idt.double_fault
+ .set_handler_fn(double_fault_handler)
+ .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX);
+ }
+
idt
};
}
@@ -54,3 +65,11 @@ pub fn init_idt() {
extern "x86-interrupt" fn breakpoint_handler(stack_frame: &mut ExceptionStackFrame) {
println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}
+
+extern "x86-interrupt" fn double_fault_handler(
+ stack_frame: &mut ExceptionStackFrame,
+ _error_code: u64,
+) {
+ println!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame);
+ loop {}
+}