An exception signals that something is wrong with the current instruction. For example, the CPU issues an exception if the current instruction tries to divide by 0. When an exception occurs, the CPU interrupts its current work and immediately calls a specific exception handler function, depending on the exception type.

We've already seen several types of exceptions in our kernel:

Invalid Opcode: This exception occurs when the current instruction is invalid. For example, this exception occurred when we tried to use SSE instructions before enabling SSE. Without SSE, the CPU didn't know the movups and movaps instructions, so it throws an exception when it stumbles over them.

Page Fault: A page fault occurs on illegal memory accesses. For example, if the current instruction tries to read from an unmapped page or tries to write to a read-only page.

Double Fault: When an exception occurs, the CPU tries to call the corresponding handler function. If another exception exception occurs while calling the exception handler, the CPU raises a double fault exception. This exception also occurs when there is no handler function registered for an exception.

Triple Fault: If an exception occurs while the CPU tries to call the double fault handler function, it issues a fatal triple fault. We can't catch or handle a triple fault. Most processors react by resetting themselves and rebooting the operating system. This causes the bootloops we experienced in the previous posts.

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:

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

13‑14

Descriptor Privilege Level (DPL)

The minimal 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 in the OSDev wiki shows the IDT indexes of all exceptions in the “Vector nr.” column.

When an exception occurs, the CPU roughly does the following:

Read the corresponding entry from the Interrupt Descriptor Table (IDT). For example, the CPU reads the 14-th entry when a page fault occurs.

Check if the entry is present. Raise a double fault if not.

Push some registers on the stack, including the instruction pointer and the EFLAGS register. (We will use these values in a future post.)

Disable interrupts if the entry is an interrupt gate (bit 40 not set).

The IDT is variable sized and can have up to 256 entries. We only need the first 16 entries in this post, so we define the table as [Entry; 16]. The remaining 240 handlers are treated as non-present by the CPU.

The Entry type is the translation of the above table to Rust. The repr(C, packed) attribute ensures that the compiler keeps the field ordering and does not add any padding between them. Instead of describing the gdt_selector as a plain u16, we use the SegmentSelector type of the x86 crate. We also merge bits 32 to 47 into an option field, because Rust has no u3 or u1 type. The EntryOptions type is described below:

The implementations of these methods need to modify the correct bits of the u16 without touching the other bits. For example, we would need the following bit-fiddling to set the stack index:

self.0=(self.0&0xfff8)| stack_index;

Or alternatively:

self.0=(self.0&(!0b111))| stack_index;

Or:

self.0=((self.0>>3)<<3)| stack_index;

Well, none of these variants is really readable and it's very easy to make mistakes somewhere. Therefore I created a BitField trait that provides the following Range-based API:

self.0.set_bits(0..3, stack_index);

I think it is much more readable, since we abstracted away all bit-masking details. The BitField trait is contained in the bit_field crate. (It's pretty new, so it might still contain bugs.) To add it as dependency, we run cargo add bit_field and add extern crate bit_field; to our src/lib.rs.

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).

We take a GDT selector and a handler function as arguments and create a new IDT entry for it. The HandlerFunc type is described below. It is a function pointer that can be converted to an u64. We choose the lower 16 bits for pointer_low, the next 16 bits for pointer_middle and the remaining 32 bits for pointer_high. For the options field we choose our default options, i.e. present and disabled interrupts.

It needs to be a function with a defined calling convention, as it called directly by the hardware. The C calling convention is the de facto standard in OS development, so we're using it, too. The function takes no arguments, since the hardware doesn't supply any arguments when jumping to the handler function.

It is important that the function is diverging, i.e. it must never return. The reason is that the hardware doesn't call the handler functions, it just jumps to them after pushing some values to the stack. So our stack might look different:

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 diverge1.

1

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.

The method overwrites the specified entry with the given handler function. We use the segmentation::cs function of the x86_64 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.

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).

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.

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_64 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:

The method does not need to modify the IDT, so it takes self by immutable reference. First, we create a DescriptorTablePointer and then we pass it to lidt. The lidt function expects that the base field has the type u64, therefore we need to cast the self pointer. For calculating the limit we use mem::size_of. The additional -1 is needed because the limit field has to be the maximum addressable byte (inclusive bound). We need an unsafe block around lidt, because the function assumes that the specified handler addresses are valid.

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?

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. 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.

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. In order to see it, we formulate the requirement for the load method:

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:

pubfnload(&'staticself){...}// ^^^^^^^ ensure that the IDT reference has the 'static lifetime

So a valid IDT needs to have the 'static lifetime. We can either create a static IDT or deliberately leak a Box. We will most likely only need a single IDT for the foreseeable future, so let's try the static approach:

We register a single handler function for a divide by zero error (index 0). Like the name says, this exception occurs when dividing a number by 0. Thus we have an easy way to test our new exception handler.

However, it doesn't work this way:

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]
...

The reason is that the Rust compiler is not able to evaluate the value of the static at compile time. Maybe it will work someday when const functions become more powerful. But until then, we have to find another solution.

Fortunately the lazy_static macro exists. Instead of evaluating a static at compile time, the macro performs the initialization when the static is referenced the first time. Thus, we can do almost everything in the initialization block and are even able to read runtime values.

Let's add the lazy_static crate to our project:

// in src/lib.rs
#[macro_use]externcrate lazy_static;

# in Cargo.toml[dependencies.lazy_static]version="0.2.1"features=["spin_no_std"]

We need the spin_no_std feature, since we don't link the standard library.

That's a not our exception handler. The reason is that Rust itself checks for a possible division by zero and panics in that case. So in order to raise a divide-by-zero error in the CPU, we need to bypass the Rust compiler somehow.

In order to cause a divide-by-zero exception, we need to execute a div or idiv assembly instruction with operand 0. We could write a small assembly function and call it from our Rust code. An easier way is to use Rust's inline assembly macro.

Inline assembly allows us to write raw x86 assembly within a Rust function. The feature is unstable, so we need to add #![feature(asm)] to our src/lib.rs. Then we're able to write a divide_by_zero function:

The asm! macro emits raw assembly instructions, so it's unsafe to use it.

We insert two assembly instructions here: mov dx, 0 and div dx. The former loads a 0 into the dx register (a subset of rdx) and the latter divides the ax register by dx. (The div instruction always implicitly operates on the ax register).

The colons are separators. After the first : we could specify output operands and after the second : we could specify input operands. We need neither, so we leave these areas empty.

After the third colon, we specify the so-called clobbers. These tell the compiler that our assembly modifies the values of some registers. Otherwise, the compiler assumes that the registers preserve their value. In our case, we clobber dx (we load 0 to it) and ax (the div instruction places the result in it).

The last block (after the 4th colon) specifies some options. The volatile option tells the compiler: “This code has side effects. Do not delete it and do not move it elsewhere”. In our case, the “side effect” is the divide-by-zero exception. Finally, the intel option allows us to use the Intel assembly syntax instead of the default AT&T syntax.

Let's use our new divide_by_zero function to raise a CPU exception:

// in src/lib.rs
pubextern"C"fnrust_main(...){...// provoke a divide-by-zero fault
divide_by_zero();println!("It did not crash!");loop{}}

It works! We see a EXCEPTION: DIVIDE BY ZERO message at the bottom of our screen:

We've successfully caught our first exception! However, our EXCEPTION: DIVIDE BY ZERO message doesn't contain much information about the cause of the exception. The next post improves the situation by printing i.a. the current stack pointer and address of the causing instruction. We will also explore other exceptions such as page faults, for which the CPU pushes an error code on the stack.