+++
title = "CPU异常处理"
weight = 5
path = "zh-CN/cpu-exceptions"
date  = 2018-06-17

[extra]
# Please update this when updating the translation
translation_based_on_commit = "096c044b4f3697e91d8e30a2e817e567d0ef21a2"
# GitHub usernames of the people that translated this post
translators = ["liuyuran"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["JiangengDong", "Byacrya"]
+++

CPU异常在很多情况下都有可能发生，比如访问无效的内存地址，或者在除法运算里除以0。为了处理这些错误，我们需要设置一个 _中断描述符表_ 来提供异常处理函数。在文章的最后，我们的内核将能够捕获 [断点异常][breakpoint exceptions] 并在处理后恢复正常执行。

[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint

<!-- more -->

这个系列的blog在[GitHub]上开放开发，如果你有任何问题，请在这里开一个issue来讨论。当然你也可以在[底部][at the bottom]留言。你可以在[`post-05`][post branch]找到这篇文章的完整源码。

[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05

<!-- toc -->

## 简述
异常信号会在当前指令触发错误时被触发，例如执行了除数为0的除法。当异常发生后，CPU会中断当前的工作，并立即根据异常类型调用对应的错误处理函数。

在x86架构中，存在20种不同的CPU异常类型，以下为最重要的几种：

- **Page Fault**: 页错误是被非法内存访问触发的，例如当前指令试图访问未被映射过的页，或者试图写入只读页。
- **Invalid Opcode**: 该错误是说当前指令操作符无效，比如在不支持SSE的旧式CPU上执行了 [SSE 指令][SSE instructions]。
- **General Protection Fault**: 该错误的原因有很多，主要原因就是权限异常，即试图使用用户态代码执行核心指令，或是修改配置寄存器的保留字段。
- **Double Fault**: 当错误发生时，CPU会尝试调用错误处理函数，但如果 _在调用错误处理函数过程中_ 再次发生错误，CPU就会触发该错误。另外，如果没有注册错误处理函数也会触发该错误。
- **Triple Fault**: 如果CPU调用了对应 `Double Fault` 异常的处理函数依然没有成功，该错误会被抛出。这是一个致命级别的 _三重异常_，这意味着我们已经无法捕捉它，对于大多数操作系统而言，此时就应该重置数据并重启操作系统。

[SSE instructions]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions

在 [OSDev wiki][exceptions] 可以看到完整的异常类型列表。

[exceptions]: https://wiki.osdev.org/Exceptions

### 中断描述符表
要捕捉CPU异常，我们需要设置一个 _中断描述符表_ (_Interrupt Descriptor Table_, IDT)，用来捕获每一个异常。由于硬件层面会不加验证的直接使用，所以我们需要根据预定义格式直接写入数据。符表的每一行都遵循如下的16字节结构。

| Type | Name                     | Description                                             |
| ---- | ------------------------ | ------------------------------------------------------- |
| u16  | Function Pointer [0:15]  | 处理函数地址的低位（最后16位）                          |
| u16  | GDT selector             | [全局描述符表][global descriptor table]中的代码段标记。 |
| u16  | Options                  | （如下所述）                                            |
| u16  | Function Pointer [16:31] | 处理函数地址的中位（中间16位）                          |
| u32  | Function Pointer [32:63] | 处理函数地址的高位（剩下的所有位）                      |
| u32  | Reserved                 |

[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table

Options字段的格式如下：

| Bits  | Name                             | Description                                                     |
| ----- | -------------------------------- | --------------------------------------------------------------- |
| 0-2   | Interrupt Stack Table Index      | 0: 不要切换栈, 1-7: 当处理函数被调用时，切换到中断栈表的第n层。 |
| 3-7   | Reserved                         |
| 8     | 0: Interrupt Gate, 1: Trap Gate  | 如果该比特被置为0，当处理函数被调用时，中断会被禁用。           |
| 9-11  | must be one                      |
| 12    | must be zero                     |
| 13‑14 | Descriptor Privilege Level (DPL) | 执行处理函数所需的最小特权等级。                                |
| 15    | Present                          |

每个异常都具有一个预定义的IDT序号，比如 invalid opcode 异常对应6号，而 page fault 异常对应14号，因此硬件可以直接寻找到对应的IDT条目。 OSDev wiki中的 [异常对照表][exceptions] 可以查到所有异常的IDT序号（在Vector nr.列）。

通常而言，当异常发生时，CPU会执行如下步骤：

1. 将一些寄存器数据入栈，包括指令指针以及 [RFLAGS] 寄存器。（我们会在文章稍后些的地方用到这些数据。）
2. 读取中断描述符表（IDT）的对应条目，比如当发生 page fault 异常时，调用14号条目。
3. 判断该条目确实存在，如果不存在，则触发 double fault 异常。
4. 如果该条目属于中断门（interrupt gate，bit 40 被设置为0），则禁用硬件中断。
5. 将 [GDT] 选择器载入代码段寄存器（CS segment）。
6. 跳转执行处理函数。

[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table

不过现在我们不必为4和5多加纠结，未来我们会单独讲解全局描述符表和硬件中断的。

## IDT类型
与其创建我们自己的IDT类型映射，不如直接使用 `x86_64` crate 内置的 [`InterruptDescriptorTable` 结构][`InterruptDescriptorTable` struct]，其实现是这样的：

[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html

``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
    pub divide_by_zero: Entry<HandlerFunc>,
    pub debug: Entry<HandlerFunc>,
    pub non_maskable_interrupt: Entry<HandlerFunc>,
    pub breakpoint: Entry<HandlerFunc>,
    pub overflow: Entry<HandlerFunc>,
    pub bound_range_exceeded: Entry<HandlerFunc>,
    pub invalid_opcode: Entry<HandlerFunc>,
    pub device_not_available: Entry<HandlerFunc>,
    pub double_fault: Entry<HandlerFuncWithErrCode>,
    pub invalid_tss: Entry<HandlerFuncWithErrCode>,
    pub segment_not_present: Entry<HandlerFuncWithErrCode>,
    pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
    pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
    pub page_fault: Entry<PageFaultHandlerFunc>,
    pub x87_floating_point: Entry<HandlerFunc>,
    pub alignment_check: Entry<HandlerFuncWithErrCode>,
    pub machine_check: Entry<HandlerFunc>,
    pub simd_floating_point: Entry<HandlerFunc>,
    pub virtualization: Entry<HandlerFunc>,
    pub security_exception: Entry<HandlerFuncWithErrCode>,
    // some fields omitted
}
```

每一个字段都是 [`idt::Entry<F>`] 类型，这个类型包含了一条完整的IDT条目（定义参见上文）。 其泛型参数 `F` 定义了中断处理函数的类型，在有些字段中该参数为 [`HandlerFunc`]，而有些则是 [`HandlerFuncWithErrCode`]，而对于 page fault 这种特殊异常，则为 [`PageFaultHandlerFunc`]。

[`idt::Entry<F>`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html
[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html
[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html
[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html

首先让我们看一看 `HandlerFunc` 类型的定义：

```rust
type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame);
```

这是一个针对 `extern "x86-interrupt" fn` 类型的 [类型别名][type alias]。`extern` 关键字使用 [外部调用约定][foreign calling convention] 定义了一个函数，这种定义方式多用于和C语言代码通信（`extern "C" fn`），那么这里的外部调用约定又究竟调用了哪些东西？

[type alias]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases
[foreign calling convention]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions

## 中断调用约定
异常触发十分类似于函数调用：CPU会直接跳转到处理函数的第一个指令处开始执行，执行结束后，CPU会跳转到返回地址，并继续执行之前的函数调用。

然而两者最大的不同点是：函数调用是由编译器通过 `call` 指令主动发起的，而错误处理函数则可能会由 _任何_ 指令触发。要了解这两者所造成影响的不同，我们需要更深入的追踪函数调用。

[调用约定][Calling conventions] 指定了函数调用的详细信息，比如可以指定函数的参数存放在哪里（寄存器，或者栈，或者别的什么地方）以及如何返回结果。在 x86_64 Linux 中，以下规则适用于C语言函数（指定于 [System V ABI] 标准）：

[Calling conventions]: https://en.wikipedia.org/wiki/Calling_convention
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf

- 前六个整型参数从寄存器传入 `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`
- 其他参数从栈传入
- 函数返回值存放在 `rax` 和 `rdx`

注意，Rust并不遵循C ABI，而是遵循自己的一套规则，即 [尚未正式发布的 Rust ABI 草案][rust abi]，所以这些规则仅在使用 `extern "C" fn` 对函数进行定义时才会使用。

[rust abi]: https://github.com/rust-lang/rfcs/issues/600

### 保留寄存器和临时寄存器
调用约定将寄存器分为两部分：_保留寄存器_ 和 _临时寄存器_ 。

_保留寄存器_ 的值应当在函数调用时保持不变，所以被调用的函数（ _"callee"_ ）只有在保证"返回之前将这些寄存器的值恢复到初始值"的前提下，才被允许覆写这些寄存器的值， 在函数开始时将这类寄存器的值存入栈中，并在返回之前将之恢复到寄存器中是一种十分常见的做法。

而 _临时寄存器_ 则相反，被调用函数可以无限制的反复写入寄存器，若调用者希望此类寄存器在函数调用后保持数值不变，则需要自己来处理备份和恢复过程（例如将其数值保存在栈中），因而这类寄存器又被称为 _caller-saved_。

在 x86_64 架构下，C调用约定指定了这些寄存器分类：

| 保留寄存器                                      | 临时寄存器                                                  |
| ----------------------------------------------- | ----------------------------------------------------------- |
| `rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11` |
| _callee-saved_                                  | _caller-saved_                                              |

编译器已经内置了这些规则，因而可以自动生成保证程序正常执行的指令。例如绝大多数函数的汇编指令都以 `push rbp` 开头，也就是将 `rbp` 的值备份到栈中（因为它是 `callee-saved` 型寄存器）。

### 保存所有寄存器数据
区别于函数调用，异常在执行 _任何_ 指令时都有可能发生。在大多数情况下，我们在编译期不可能知道程序跑起来会发生什么异常。比如编译器无法预知某条指令是否会触发 page fault 或者 stack overflow。

正因我们不知道异常会何时发生，所以我们无法预先保存寄存器。这意味着我们无法使用依赖调用方备份 (caller-saved) 的寄存器的调用传统作为异常处理程序。因此，我们需要一个保存所有寄存器的传统。x86-interrupt 恰巧就是其中之一，它可以保证在函数返回时，寄存器里的值均返回原样。

但请注意，这并不意味着所有寄存器都会在进入函数时备份入栈。编译器仅会备份被函数覆写的寄存器，继而为只使用几个寄存器的短小函数生成高效的代码。

### 中断栈帧
当一个常规函数调用发生时（使用 `call` 指令），CPU会在跳转目标函数之前，将返回地址入栈。当函数返回时（使用 `ret` 指令），CPU会在跳回目标函数之前弹出返回地址。所以常规函数调用的栈帧看起来是这样的：

![function stack frame](function-stack-frame.svg)

对于错误和中断处理函数，仅仅压入一个返回地址并不足够，因为中断处理函数通常会运行在一个不那么一样的上下文中（栈指针、CPU flags等等）。所以CPU在遇到中断发生时是这么处理的：

1. **对齐栈指针**: 任何指令都有可能触发中断，所以栈指针可能是任何值，而部分CPU指令（比如部分SSE指令）需要栈指针16字节边界对齐，因此CPU会在中断触发后立刻为其进行对齐。
2. **切换栈** （部分情况下）: 当CPU特权等级改变时，例如当一个用户态程序触发CPU异常时，会触发栈切换。该行为也可能被所谓的 _中断栈表_ 配置，在特定中断中触发，关于该表，我们会在下一篇文章做出讲解。
3. **压入旧的栈指针**: 当中断发生后，栈指针对齐之前，CPU会将栈指针寄存器（`rsp`）和栈段寄存器（`ss`）的数据入栈，由此可在中断处理函数返回后，恢复上一层的栈指针。
4. **压入并更新 `RFLAGS` 寄存器**: [`RFLAGS`] 寄存器包含了各式各样的控制位和状态位，当中断发生时，CPU会改变其中的部分数值，并将旧值入栈。
5. **压入指令指针**: 在跳转中断处理函数之前，CPU会将指令指针寄存器（`rip`）和代码段寄存器（`cs`）的数据入栈，此过程与常规函数调用中返回地址入栈类似。
6. **压入错误码** （针对部分异常）: 对于部分特定的异常，比如 page faults ，CPU会推入一个错误码用于标记错误的成因。
7. **执行中断处理函数**: CPU会读取对应IDT条目中描述的中断处理函数对应的地址和段描述符，将两者载入 `rip` 和 `cs` 以开始运行处理函数。

[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register

所以 _中断栈帧_ 看起来是这样的：

![interrupt stack frame](exception-stack-frame.svg)

在 `x86_64` crate 中，中断栈帧已经被 [`InterruptStackFrame`] 结构完整表达，该结构会以 `&mut` 的形式传入处理函数，并可以用于查询错误发生的更详细的原因。但该结构并不包含错误码字段，因为只有极少量的错误会传入错误码，所以对于这类需要传入 `error_code` 的错误，其函数类型变为了 [`HandlerFuncWithErrCode`]。

[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html

### 幕后花絮
`x86-interrupt` 调用约定是一个十分厉害的抽象，它几乎隐藏了所有错误处理函数中的凌乱细节，但尽管如此，了解一下水面下发生的事情还是有用的。我们来简单介绍一下被 `x86-interrupt` 隐藏起来的行为：

- **传递参数**: 绝大多数指定参数的调用约定都是期望通过寄存器取得参数的，但事实上这是无法实现的，因为我们不能在备份寄存器数据之前就将其复写。`x86-interrupt` 的解决方案时，将参数以指定的偏移量放到栈上。
- **使用 `iretq` 返回**: 由于中断栈帧和普通函数调用的栈帧是完全不同的，我们无法通过 `ret` 指令直接返回，所以此时必须使用 `iretq` 指令。
- **处理错误码**: 部分异常传入的错误码会让错误处理更加复杂，它会造成栈指针对齐失效（见下一条），而且需要在返回之前从栈中弹出去。好在 `x86-interrupt` 为我们挡住了这些额外的复杂度。但是它无法判断哪个异常对应哪个处理函数，所以它需要从函数参数数量上推断一些信息，因此程序员需要为每个异常使用正确的函数类型。当然你已经不需要烦恼这些， `x86_64` crate 中的 `InterruptDescriptorTable` 已经帮助你完成了定义。
- **对齐栈**: 对于一些指令（尤其是SSE指令）而言，它们需要提前进行16字节边界对齐操作，通常而言CPU在异常发生之后就会自动完成这一步。但是部分异常会由于传入错误码而破坏掉本应完成的对齐操作，此时 `x86-interrupt` 会为我们重新完成对齐。

如果你对更多细节有兴趣：我们还有关于使用 [裸函数][naked functions] 展开异常处理的一个系列章节，参见 [文末][too-much-magic]。

[naked functions]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
[too-much-magic]: #hei-mo-fa-you-dian-duo

## 实现
那么理论知识暂且到此为止，该开始为我们的内核实现CPU异常处理了。首先我们在 `src/interrupts.rs` 创建一个模块，并加入函数 `init_idt` 用来创建一个新的 `InterruptDescriptorTable`：

``` rust
// in src/lib.rs

pub mod interrupts;

// in src/interrupts.rs

use x86_64::structures::idt::InterruptDescriptorTable;

pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
}
```

现在我们就可以添加处理函数了，首先给 [breakpoint exception] 添加。该异常是一个绝佳的测试途径，因为它唯一的目的就是在 `int3` 指令执行时暂停程序运行。

[breakpoint exception]: https://wiki.osdev.org/Exceptions#Breakpoint

breakpoint exception 通常被用在调试器中：当程序员为程序打上断点，调试器会将对应的位置覆写为 `int3` 指令，CPU执行该指令后，就会抛出 breakpoint exception 异常。在调试完毕，需要程序继续运行时，调试器就会将原指令覆写回 `int3` 的位置。如果要了解更多细节，请查阅 ["_调试器是如何工作的_"]["_How debuggers work_"] 系列。

["_How debuggers work_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints

不过现在我们还不需要覆写指令，只需要打印一行日志，表明接收到了这个异常，然后让程序继续运行即可。那么我们就来创建一个简单的 `breakpoint_handler` 方法并加入IDT中：

```rust
// in src/interrupts.rs

use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::println;

pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
}

extern "x86-interrupt" fn breakpoint_handler(
    stack_frame: InterruptStackFrame)
{
    println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}
```

现在，我们的处理函数应当会输出一行信息以及完整的栈帧。

但当我们尝试编译的时候，报出了下面的错误：

```
error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
  --> src/main.rs:53:1
   |
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
54 | |     println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
55 | | }
   | |_^
   |
   = help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
```

这是因为 `x86-interrupt` 并不是稳定特性，需要手动启用，只需要在我们的 `lib.rs` 中加入 `#![feature(abi_x86_interrupt)]` 开关即可。

### 载入 IDT
要让CPU使用新的中断描述符表，我们需要使用 [`lidt`] 指令来装载一下，`x86_64` 的 `InterruptDescriptorTable` 结构提供了 [`load`][InterruptDescriptorTable::load] 函数用来实现这个需求。让我们来试一下：

[`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

```rust
// in src/interrupts.rs

pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
    idt.load();
}
```

再次尝试编译，又出现了新的错误：

```
error: `idt` does not live long enough
  --> src/interrupts/mod.rs:43:5
   |
43 |     idt.load();
   |     ^^^ does not live long enough
44 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...
```

原来 `load` 函数要求的生命周期为 `&'static self` ，也就是整个程序的生命周期，其原因就是CPU在接收到下一个IDT之前会一直使用这个描述符表。如果生命周期小于 `'static` ，很可能就会出现使用已释放对象的bug。

问题至此已经很清晰了，我们的 `idt` 是创建在栈上的，它的生命周期仅限于 `init` 函数执行期间，之后这部分栈内存就会被其他函数调用，CPU再来访问IDT的话，只会读取到一段随机数据。好在 `InterruptDescriptorTable::load` 被严格定义了函数生命周期限制，这样 Rust 编译器就可以在编译时就发现这些潜在问题。

要修复这些错误很简单，让 `idt` 具备 `'static` 类型的生命周期即可，我们可以使用 [`Box`] 在堆上申请一段内存，并转化为 `'static` 指针即可，但问题是我们正在写的东西是操作系统内核，（暂时）并没有堆这种东西。

[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html


作为替代，我们可以试着直接将IDT定义为 `'static` 变量：

```rust
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();

pub fn init_idt() {
    IDT.breakpoint.set_handler_fn(breakpoint_handler);
    IDT.load();
}
```

然而这样就会引入一个新问题：静态变量是不可修改的，这样我们就无法在 `init` 函数中修改里面的数据了，所以需要把变量类型修改为 [`static mut`]：

[`static mut`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable

```rust
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();

pub fn init_idt() {
    unsafe {
        IDT.breakpoint.set_handler_fn(breakpoint_handler);
        IDT.load();
    }
}
```

这样就不会有编译错误了，但是这并不符合官方推荐的编码习惯，因为理论上说 `static mut` 类型的变量很容易形成数据竞争，所以需要用 [`unsafe` 代码块][`unsafe` block] 修饰调用语句。

[`unsafe` block]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers

#### 懒加载拯救世界
好在还有 `lazy_static` 宏可以用，区别于普通 `static` 变量在编译器求值，这个宏可以使代码块内的 `static` 变量在第一次取值时求值。所以，我们完全可以把初始化代码写在变量定义的代码块里，同时也不影响后续的取值。

在 [创建VGA字符缓冲的单例][vga text buffer lazy static] 时我们已经引入了 `lazy_static` crate，所以我们可以直接使用 `lazy_static!` 来创建IDT：

[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics

```rust
// in src/interrupts.rs

use lazy_static::lazy_static;

lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        idt
    };
}

pub fn init_idt() {
    IDT.load();
}
```

现在碍眼的 `unsafe` 代码块成功被去掉了，尽管 `lazy_static!` 的内部依然使用了 `unsafe` 代码块，但是至少它已经抽象为了一个安全接口。

### 跑起来

最后一步就是在 `main.rs` 里执行 `init_idt` 函数以在我们的内核里装载IDT，但不要直接调用，而应在 `lib.rs` 里封装一个 `init` 函数出来：

```rust
// in src/lib.rs

pub fn init() {
    interrupts::init_idt();
}
```

这样我们就可以把所有初始化逻辑都集中在一个函数里，从而让 `main.rs` 、 `lib.rs` 以及单元测试中的 `_start` 共享初始化逻辑。

现在我们更新一下 `main.rs` 中的 `_start` 函数，调用 `init` 并手动触发一次 breakpoint exception：

```rust
// in src/main.rs

#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello World{}", "!");

    blog_os::init(); // new

    // invoke a breakpoint exception
    x86_64::instructions::interrupts::int3(); // new

    // as before
    #[cfg(test)]
    test_main();

    println!("It did not crash!");
    loop {}
}
```

当我们在QEMU中运行之后（`cargo run`），效果是这样的：

![QEMU printing `EXCEPTION: BREAKPOINT` and the interrupt stack frame](qemu-breakpoint-exception.png)

成功了！CPU成功调用了中断处理函数并打印出了信息，然后返回 `_start` 函数打印出了 `It did not crash!`。

我们可以看到，中断栈帧告诉了我们当错误发生时指令和栈指针的具体数值，这些信息在我们调试意外错误的时候非常有用。

### 添加测试

那么让我们添加一个测试用例，用来确保以上工作成果可以顺利运行。首先需要在 `_start` 函数中调用 `init`：

```rust
// in src/lib.rs

/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
    init();      // new
    test_main();
    loop {}
}
```

注意，这里的 `_start` 会在 `cargo test --lib` 这条命令的上下文中运行，而 `lib.rs` 的执行环境完全独立于 `main.rs`，所以我们需要在运行测试之前调用 `init` 装载IDT。

那么我们接着创建一个测试用例 `test_breakpoint_exception`：

```rust
// in src/interrupts.rs

#[test_case]
fn test_breakpoint_exception() {
    // invoke a breakpoint exception
    x86_64::instructions::interrupts::int3();
}
```

该测试仅调用了 `int3` 函数以触发 breakpoint exception，通过查看这个函数是否能够继续运行下去，就可以确认我们对应的中断处理函数是否工作正常。

现在，你可以执行 `cargo test` 来运行所有测试，或者执行 `cargo test --lib` 来运行 `lib.rs` 及其子模块中包含的测试，最终输出如下：

```
blog_os::interrupts::test_breakpoint_exception...	[ok]
```

## 黑魔法有点多？
相对来说，`x86-interrupt` 调用约定和 [`InterruptDescriptorTable`] 类型让错误处理变得直截了当，如果这对你来说太过于神奇，进而想要了解错误处理中的所有隐秘细节，我们推荐读一下这些：[“使用裸函数处理错误”][“Handling Exceptions with Naked Functions”] 系列文章展示了如何在不使用 `x86-interrupt` 的前提下创建IDT。但是需要注意的是，这些文章都是在 `x86-interrupt` 调用约定和 `x86_64` crate 出现之前的产物，这些东西属于博客的 [第一版][first edition]，不排除信息已经过期了的可能。

[“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

## 接下来是？
我们已经成功捕获了第一个异常，并从异常中成功恢复，下一步就是试着捕获所有异常，如果有未捕获的异常就会触发致命的[triple fault]，那就只能重启整个系统了。下一篇文章会展开说我们如何通过正确捕捉[double faults]来避免这种情况。

[triple fault]: https://wiki.osdev.org/Triple_Fault
[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault
