mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
fix: check writing of 05
This commit is contained in:
@@ -8,7 +8,7 @@ date = 2018-06-17
|
||||
chapter = "Interrupts"
|
||||
+++
|
||||
|
||||
CPU exceptions occur in various erroneous situations, for example when accessing an invalid memory address or when dividing by zero. To react to them we have to set up an _interrupt descriptor table_ that provides handler functions. At the end of this post, our kernel will be able to catch [breakpoint exceptions] and to resume normal execution afterwards.
|
||||
CPU exceptions occur in various erroneous situations, for example, when accessing an invalid memory address or when dividing by zero. To react to them, we have to set up an _interrupt descriptor table_ that provides handler functions. At the end of this post, our kernel will be able to catch [breakpoint exceptions] and resume normal execution afterward.
|
||||
|
||||
[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
@@ -26,22 +26,22 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
|
||||
## Overview
|
||||
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.
|
||||
|
||||
On x86 there are about 20 different CPU exception types. The most important are:
|
||||
On x86, there are about 20 different CPU exception types. The most important are:
|
||||
|
||||
- **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.
|
||||
- **Invalid Opcode**: This exception occurs when the current instruction is invalid, for example when we try to use newer [SSE instructions] on an old CPU that does not support them.
|
||||
- **General Protection Fault**: This is the exception with the broadest range of causes. It occurs on various kinds of access violations such as trying to execute a privileged instruction in user level code or writing reserved fields in configuration registers.
|
||||
- **Invalid Opcode**: This exception occurs when the current instruction is invalid, for example, when we try to use new [SSE instructions] on an old CPU that does not support them.
|
||||
- **General Protection Fault**: This is the exception with the broadest range of causes. It occurs on various kinds of access violations, such as trying to execute a privileged instruction in user-level code or writing reserved fields in configuration registers.
|
||||
- **Double Fault**: When an exception occurs, the CPU tries to call the corresponding handler function. If another 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.
|
||||
|
||||
[SSE instructions]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
|
||||
|
||||
For the full list of exceptions check out the [OSDev wiki][exceptions].
|
||||
For the full list of exceptions, check out the [OSDev wiki][exceptions].
|
||||
|
||||
[exceptions]: https://wiki.osdev.org/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:
|
||||
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
|
||||
----|--------------------------|-----------------------------------
|
||||
@@ -66,21 +66,21 @@ Bits | Name | Description
|
||||
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][exceptions] in the OSDev wiki shows the IDT indexes of all exceptions in the “Vector nr.” column.
|
||||
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.
|
||||
|
||||
When an exception occurs, the CPU roughly does the following:
|
||||
|
||||
1. Push some registers on the stack, including the instruction pointer and the [RFLAGS] register. (We will use these values later in this post.)
|
||||
2. Read the corresponding entry from the Interrupt Descriptor Table (IDT). For example, the CPU reads the 14-th entry when a page fault occurs.
|
||||
3. Check if the entry is present. Raise a double fault if not.
|
||||
2. Read the corresponding entry from the Interrupt Descriptor Table (IDT). For example, the CPU reads the 14th entry when a page fault occurs.
|
||||
3. Check if the entry is present and, if not, raise a double fault.
|
||||
4. Disable hardware interrupts if the entry is an interrupt gate (bit 40 not set).
|
||||
5. Load the specified [GDT] selector into the CS segment.
|
||||
5. Load the specified [GDT] selector into the CS (code segment).
|
||||
6. Jump to the specified handler function.
|
||||
|
||||
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
|
||||
|
||||
Don't worry about steps 4 and 5 for now, we will learn about the global descriptor table and hardware interrupts in future posts.
|
||||
Don't worry about steps 4 and 5 for now; we will learn about the global descriptor table and hardware interrupts in future posts.
|
||||
|
||||
## An IDT Type
|
||||
Instead of creating our own IDT type, we will use the [`InterruptDescriptorTable` struct] of the `x86_64` crate, which looks like this:
|
||||
@@ -133,9 +133,9 @@ It's a [type alias] for an `extern "x86-interrupt" fn` type. The `extern` keywor
|
||||
[foreign calling convention]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
|
||||
|
||||
## The Interrupt Calling Convention
|
||||
Exceptions are quite similar to function calls: The CPU jumps to the first instruction of the called function and executes it. Afterwards the CPU jumps to the return address and continues the execution of the parent function.
|
||||
Exceptions are quite similar to function calls: The CPU jumps to the first instruction of the called function and executes it. Afterwards, the CPU jumps to the return address and continues the execution of the parent function.
|
||||
|
||||
However, there is a major difference between exceptions and function calls: A function call is invoked voluntarily by a compiler inserted `call` instruction, while an exception might occur at _any_ instruction. In order to understand the consequences of this difference, we need to examine function calls in more detail.
|
||||
However, there is a major difference between exceptions and function calls: A function call is invoked voluntarily by a compiler-inserted `call` instruction, while an exception might occur at _any_ instruction. In order to understand the consequences of this difference, we need to examine function calls in more detail.
|
||||
|
||||
[Calling conventions] specify the details of a function call. For example, they specify where function parameters are placed (e.g. in registers or on the stack) and how results are returned. On x86_64 Linux, the following rules apply for C functions (specified in the [System V ABI]):
|
||||
|
||||
@@ -151,11 +151,11 @@ Note that Rust does not follow the C ABI (in fact, [there isn't even a Rust ABI
|
||||
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
|
||||
|
||||
### Preserved and Scratch Registers
|
||||
The calling convention divides the registers in two parts: _preserved_ and _scratch_ registers.
|
||||
The calling convention divides the registers into two parts: _preserved_ and _scratch_ registers.
|
||||
|
||||
The values of _preserved_ registers must remain unchanged across function calls. So a called function (the _“callee”_) is only allowed to overwrite these registers if it restores their original values before returning. Therefore these registers are called _“callee-saved”_. A common pattern is to save these registers to the stack at the function's beginning and restore them just before returning.
|
||||
The values of _preserved_ registers must remain unchanged across function calls. So a called function (the _“callee”_) is only allowed to overwrite these registers if it restores their original values before returning. Therefore, these registers are called _“callee-saved”_. A common pattern is to save these registers to the stack at the function's beginning and restore them just before returning.
|
||||
|
||||
In contrast, a called function is allowed to overwrite _scratch_ registers without restrictions. If the caller wants to preserve the value of a scratch register across a function call, it needs to backup and restore it before the function call (e.g. by pushing it to the stack). So the scratch registers are _caller-saved_.
|
||||
In contrast, a called function is allowed to overwrite _scratch_ registers without restrictions. If the caller wants to preserve the value of a scratch register across a function call, it needs to backup and restore it before the function call (e.g., by pushing it to the stack). So the scratch registers are _caller-saved_.
|
||||
|
||||
On x86_64, the C calling convention specifies the following preserved and scratch registers:
|
||||
|
||||
@@ -167,11 +167,11 @@ _callee-saved_ | _caller-saved_
|
||||
The compiler knows these rules, so it generates the code accordingly. For example, most functions begin with a `push rbp`, which backups `rbp` on the stack (because it's a callee-saved register).
|
||||
|
||||
### Preserving all Registers
|
||||
In contrast to function calls, exceptions can occur on _any_ instruction. In most cases we don't even know at compile time if the generated code will cause an exception. For example, the compiler can't know if an instruction causes a stack overflow or a page fault.
|
||||
In contrast to function calls, exceptions can occur on _any_ instruction. In most cases, we don't even know at compile time if the generated code will cause an exception. For example, the compiler can't know if an instruction causes a stack overflow or a page fault.
|
||||
|
||||
Since we don't know when an exception occurs, we can't backup any registers before. This means that we can't use a calling convention that relies on caller-saved registers for exception handlers. Instead, we need a calling convention means that preserves _all registers_. The `x86-interrupt` calling convention is such a calling convention, so it guarantees that all register values are restored to their original values on function return.
|
||||
Since we don't know when an exception occurs, we can't backup any registers before. This means we can't use a calling convention that relies on caller-saved registers for exception handlers. Instead, we need a calling convention that preserves _all registers_. The `x86-interrupt` calling convention is such a calling convention, so it guarantees that all register values are restored to their original values on function return.
|
||||
|
||||
Note that this does not mean that all registers are saved to the stack at function entry. Instead, the compiler only backs up the registers that are overwritten by the function. This way, very efficient code can be generated for short functions that only use a few registers.
|
||||
Note that this does not mean all registers are saved to the stack at function entry. Instead, the compiler only backs up the registers that are overwritten by the function. This way, very efficient code can be generated for short functions that only use a few registers.
|
||||
|
||||
### The Interrupt Stack Frame
|
||||
On a normal function call (using the `call` instruction), the CPU pushes the return address before jumping to the target function. On function return (using the `ret` instruction), the CPU pops this return address and jumps to it. So the stack frame of a normal function call looks like this:
|
||||
@@ -180,12 +180,12 @@ On a normal function call (using the `call` instruction), the CPU pushes the ret
|
||||
|
||||
For exception and interrupt handlers, however, pushing a return address would not suffice, since interrupt handlers often run in a different context (stack pointer, CPU flags, etc.). Instead, the CPU performs the following steps when an interrupt occurs:
|
||||
|
||||
1. **Aligning the stack pointer**: An interrupt can occur at any instructions, so the stack pointer can have any value, too. However, some CPU instructions (e.g. some SSE instructions) require that the stack pointer is aligned on a 16 byte boundary, therefore the CPU performs such an alignment right after the interrupt.
|
||||
2. **Switching stacks** (in some cases): A stack switch occurs when the CPU privilege level changes, for example when a CPU exception occurs in a user mode program. It is also possible to configure stack switches for specific interrupts using the so-called _Interrupt Stack Table_ (described in the next post).
|
||||
3. **Pushing the old stack pointer**: The CPU pushes the values of the stack pointer (`rsp`) and the stack segment (`ss`) registers at the time when the interrupt occurred (before the alignment). This makes it possible to restore the original stack pointer when returning from an interrupt handler.
|
||||
1. **Aligning the stack pointer**: An interrupt can occur at any instruction, so the stack pointer can have any value, too. However, some CPU instructions (e.g., some SSE instructions) require that the stack pointer be aligned on a 16-byte boundary, so the CPU performs such an alignment right after the interrupt.
|
||||
2. **Switching stacks** (in some cases): A stack switch occurs when the CPU privilege level changes, for example, when a CPU exception occurs in a user-mode program. It is also possible to configure stack switches for specific interrupts using the so-called _Interrupt Stack Table_ (described in the next post).
|
||||
3. **Pushing the old stack pointer**: When the interrupt occurs (before the alignment), the CPU pushes the values of the stack pointer (`rsp`) and stack segment (`ss`) registers. This makes it possible to restore the original stack pointer when returning from an interrupt handler.
|
||||
4. **Pushing and updating the `RFLAGS` register**: The [`RFLAGS`] register contains various control and status bits. On interrupt entry, the CPU changes some bits and pushes the old value.
|
||||
5. **Pushing the instruction pointer**: Before jumping to the interrupt handler function, the CPU pushes the instruction pointer (`rip`) and the code segment (`cs`). This is comparable to the return address push of a normal function call.
|
||||
6. **Pushing an error code** (for some exceptions): For some specific exceptions such as page faults, the CPU pushes an error code, which describes the cause of the exception.
|
||||
6. **Pushing an error code** (for some exceptions): For some specific exceptions, such as page faults, the CPU pushes an error code, which describes the cause of the exception.
|
||||
7. **Invoking the interrupt handler**: The CPU reads the address and the segment descriptor of the interrupt handler function from the corresponding field in the IDT. It then invokes this handler by loading the values into the `rip` and `cs` registers.
|
||||
|
||||
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
@@ -194,19 +194,19 @@ So the _interrupt stack frame_ looks like this:
|
||||
|
||||

|
||||
|
||||
In the `x86_64` crate, the interrupt stack frame is represented by the [`InterruptStackFrame`] struct. It is passed to interrupt handlers as `&mut` and can be used to retrieve additional information about the exception's cause. The struct contains no error code field, since only some few exceptions push an error code. These exceptions use the separate [`HandlerFuncWithErrCode`] function type, which has an additional `error_code` argument.
|
||||
In the `x86_64` crate, the interrupt stack frame is represented by the [`InterruptStackFrame`] struct. It is passed to interrupt handlers as `&mut` and can be used to retrieve additional information about the exception's cause. The struct contains no error code field, since only a few exceptions push an error code. These exceptions use the separate [`HandlerFuncWithErrCode`] function type, which has an additional `error_code` argument.
|
||||
|
||||
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
|
||||
|
||||
### Behind the Scenes
|
||||
The `x86-interrupt` calling convention is a powerful abstraction that hides almost all of the messy details of the exception handling process. However, sometimes it's useful to know what's happening behind the curtain. Here is a short overview of the things that the `x86-interrupt` calling convention takes care of:
|
||||
|
||||
- **Retrieving the arguments**: Most calling conventions expect that the arguments are passed in registers. This is not possible for exception handlers, since we must not overwrite any register values before backing them up on the stack. Instead, the `x86-interrupt` calling convention is aware that the arguments already lie on the stack at a specific offset.
|
||||
- **Returning using `iretq`**: Since the interrupt stack frame completely differs from stack frames of normal function calls, we can't return from handlers functions through the normal `ret` instruction. Instead, the `iretq` instruction must be used.
|
||||
- **Handling the error code**: The error code, which is pushed for some exceptions, makes things much more complex. It changes the stack alignment (see the next point) and needs to be popped off the stack before returning. The `x86-interrupt` calling convention handles all that complexity. However, it doesn't know which handler function is used for which exception, so it needs to deduce that information from the number of function arguments. That means that the programmer is still responsible to use the correct function type for each exception. Luckily, the `InterruptDescriptorTable` type defined by the `x86_64` crate ensures that the correct function types are used.
|
||||
- **Aligning the stack**: There are some instructions (especially SSE instructions) that require a 16-byte stack alignment. The CPU ensures this alignment whenever an exception occurs, but for some exceptions it destroys it again later when it pushes an error code. The `x86-interrupt` calling convention takes care of this by realigning the stack in this case.
|
||||
- **Retrieving the arguments**: Most calling conventions expect that the arguments are passed in registers. This is not possible for exception handlers since we must not overwrite any register values before backing them up on the stack. Instead, the `x86-interrupt` calling convention is aware that the arguments already lie on the stack at a specific offset.
|
||||
- **Returning using `iretq`**: Since the interrupt stack frame completely differs from stack frames of normal function calls, we can't return from handler functions through the normal `ret` instruction. So instead, the `iretq` instruction must be used.
|
||||
- **Handling the error code**: The error code, which is pushed for some exceptions, makes things much more complex. It changes the stack alignment (see the next point) and needs to be popped off the stack before returning. The `x86-interrupt` calling convention handles all that complexity. However, it doesn't know which handler function is used for which exception, so it needs to deduce that information from the number of function arguments. That means the programmer is still responsible for using the correct function type for each exception. Luckily, the `InterruptDescriptorTable` type defined by the `x86_64` crate ensures that the correct function types are used.
|
||||
- **Aligning the stack**: Some instructions (especially SSE instructions) require a 16-byte stack alignment. The CPU ensures this alignment whenever an exception occurs, but for some exceptions it destroys it again later when it pushes an error code. The `x86-interrupt` calling convention takes care of this by realigning the stack in this case.
|
||||
|
||||
If you are interested in more details: We also have a series of posts that explains exception handling using [naked functions] linked [at the end of this post][too-much-magic].
|
||||
If you are interested in more details, we also have a series of posts that explain exception handling using [naked functions] linked [at the end of this post][too-much-magic].
|
||||
|
||||
[naked functions]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
|
||||
[too-much-magic]: #too-much-magic
|
||||
@@ -272,10 +272,10 @@ error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue
|
||||
= help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
|
||||
```
|
||||
|
||||
This error occurs because the `x86-interrupt` calling convention is still unstable. To use it anyway, we have to explicitly enable it by adding `#![feature(abi_x86_interrupt)]` on the top of our `lib.rs`.
|
||||
This error occurs because the `x86-interrupt` calling convention is still unstable. To use it anyway, we have to explicitly enable it by adding `#![feature(abi_x86_interrupt)]` at the top of our `lib.rs`.
|
||||
|
||||
### Loading the IDT
|
||||
In order that the CPU uses our new interrupt descriptor table, we need to load it using the [`lidt`] instruction. The `InterruptDescriptorTable` struct of the `x86_64` provides a [`load`][InterruptDescriptorTable::load] method function for that. Let's try to use it:
|
||||
In order for the CPU to use our new interrupt descriptor table, we need to load it using the [`lidt`] instruction. The `InterruptDescriptorTable` struct of the `x86_64` crate provides a [`load`][InterruptDescriptorTable::load] method for that. Let's try to use it:
|
||||
|
||||
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
|
||||
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
|
||||
@@ -304,16 +304,16 @@ error: `idt` does not live long enough
|
||||
= note: borrowed value must be valid for the static lifetime...
|
||||
```
|
||||
|
||||
So the `load` methods expects a `&'static self`, that is a reference that is valid for the complete runtime of the program. The reason is that the CPU will access this table on every interrupt until we load a different IDT. So using a shorter lifetime than `'static` could lead to use-after-free bugs.
|
||||
So the `load` method expects a `&'static self`, that is, a reference valid for the complete runtime of the program. The reason is that the CPU will access this table on every interrupt until we load a different IDT. So using a shorter lifetime than `'static` could lead to use-after-free bugs.
|
||||
|
||||
In fact, this is exactly what happens here. Our `idt` is created on the stack, so it is only valid inside the `init` function. Afterwards the stack memory is reused for other functions, so the CPU would interpret random stack memory as IDT. Luckily, the `InterruptDescriptorTable::load` method encodes this lifetime requirement in its function definition, so that the Rust compiler is able to prevent this possible bug at compile time.
|
||||
In fact, this is exactly what happens here. Our `idt` is created on the stack, so it is only valid inside the `init` function. Afterwards, the stack memory is reused for other functions, so the CPU would interpret random stack memory as IDT. Luckily, the `InterruptDescriptorTable::load` method encodes this lifetime requirement in its function definition, so that the Rust compiler is able to prevent this possible bug at compile time.
|
||||
|
||||
In order to fix this problem, we need to store our `idt` at a place where it has a `'static` lifetime. To achieve this we could allocate our IDT on the heap using [`Box`] and then convert it to a `'static` reference, but we are writing an OS kernel and thus don't have a heap (yet).
|
||||
In order to fix this problem, we need to store our `idt` at a place where it has a `'static` lifetime. To achieve this, we could allocate our IDT on the heap using [`Box`] and then convert it to a `'static` reference, but we are writing an OS kernel and thus don't have a heap (yet).
|
||||
|
||||
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
|
||||
|
||||
|
||||
As an alternative we could try to store the IDT as a `static`:
|
||||
As an alternative, we could try to store the IDT as a `static`:
|
||||
|
||||
```rust
|
||||
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
|
||||
@@ -344,7 +344,7 @@ This variant compiles without errors but it's far from idiomatic. `static mut`s
|
||||
[`unsafe` block]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
|
||||
|
||||
#### Lazy Statics to the Rescue
|
||||
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.
|
||||
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.
|
||||
|
||||
We already imported the `lazy_static` crate when we [created an abstraction for the VGA text buffer][vga text buffer lazy static]. So we can directly use the `lazy_static!` macro to create our static IDT:
|
||||
|
||||
@@ -382,7 +382,7 @@ pub fn init() {
|
||||
}
|
||||
```
|
||||
|
||||
With this function we now have a central place for initialization routines that can be shared between the different `_start` functions in our `main.rs`, `lib.rs`, and integration tests.
|
||||
With this function, we now have a central place for initialization routines that can be shared between the different `_start` functions in our `main.rs`, `lib.rs`, and integration tests.
|
||||
|
||||
Now we can update the `_start` function of our `main.rs` to call `init` and then trigger a breakpoint exception:
|
||||
|
||||
@@ -446,7 +446,7 @@ fn test_breakpoint_exception() {
|
||||
}
|
||||
```
|
||||
|
||||
The test invokes the `int3` function to trigger a breakpoint exception. By checking that the execution continues afterwards, we verify that our breakpoint handler is working correctly.
|
||||
The test invokes the `int3` function to trigger a breakpoint exception. By checking that the execution continues afterward, we verify that our breakpoint handler is working correctly.
|
||||
|
||||
You can try this new test by running `cargo test` (all tests) or `cargo test --lib` (only tests of `lib.rs` and its modules). You should see the following in the output:
|
||||
|
||||
@@ -455,14 +455,14 @@ blog_os::interrupts::test_breakpoint_exception... [ok]
|
||||
```
|
||||
|
||||
## Too much Magic?
|
||||
The `x86-interrupt` calling convention and the [`InterruptDescriptorTable`] type made the exception handling process relatively straightforward and painless. If this was too much magic for you and you like to learn all the gory details of exception handling, we got you covered: Our [“Handling Exceptions with Naked Functions”] series shows how to handle exceptions without the `x86-interrupt` calling convention and also creates its own IDT type. Historically, these posts were the main exception handling posts before the `x86-interrupt` calling convention and the `x86_64` crate existed. Note that these posts are based on the [first edition] of this blog and might be out of date.
|
||||
The `x86-interrupt` calling convention and the [`InterruptDescriptorTable`] type made the exception handling process relatively straightforward and painless. If this was too much magic for you and you like to learn all the gory details of exception handling, we've got you covered: Our [“Handling Exceptions with Naked Functions”] series shows how to handle exceptions without the `x86-interrupt` calling convention and also creates its own IDT type. Historically, these posts were the main exception handling posts before the `x86-interrupt` calling convention and the `x86_64` crate existed. Note that these posts are based on the [first edition] of this blog and might be out of date.
|
||||
|
||||
[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md
|
||||
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
|
||||
[first edition]: @/edition-1/_index.md
|
||||
|
||||
## What's next?
|
||||
We've successfully caught our first exception and returned from it! The next step is to ensure that we catch all exceptions, because an uncaught exception causes a fatal [triple fault], which leads to a system reset. The next post explains how we can avoid this by correctly catching [double faults].
|
||||
We've successfully caught our first exception and returned from it! The next step is to ensure that we catch all exceptions because an uncaught exception causes a fatal [triple fault], which leads to a system reset. The next post explains how we can avoid this by correctly catching [double faults].
|
||||
|
||||
[triple fault]: https://wiki.osdev.org/Triple_Fault
|
||||
[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault
|
||||
|
||||
Reference in New Issue
Block a user