异常与函数调用非常相似:CPU跳转到被调用函数的第一条指令并执行它。 之后,CPU跳转到返回地址并继续执行父功能。 但是,异常和函数调用之间存在主要区别:函数调用是由编译器插入的调用指令自愿调用的,而任何指令都可能发生异常。 为了了解这种差异的后果,我们需要更详细地研究函数调用。

1
2
3
4
5
6
7
8
调用约定将寄存器分为两部分:保留寄存器和临时寄存器。
在函数调用之间,保留寄存器的值必须保持不变。
因此,仅当被调用函数(“被调用方”)在返回之前恢复其原始值时,才可以覆盖这些寄存器。
因此,这些寄存器称为“被调用者保存”。
一种常见的模式是在函数开始时将这些寄存器保存到堆栈中,并在返回之前将其还原。
相反,允许调用的函数无限制地覆盖暂存寄存器。
如果调用方希望在函数调用期间保留暂存寄存器的值,则它需要在函数调用之前备份和还原它(例如,通过将其压入堆栈)。
因此暂存寄存器是调用者保存的。

与函数调用相反,任何指令都可能发生异常。 在大多数情况下,我们甚至在编译时都不知道生成的代码是否会导致异常。 例如,编译器无法知道指令是否导致堆栈溢出或页面错误。 由于我们不知道什么时候发生异常,因此我们之前无法备份任何寄存器。 这意味着我们不能使用依赖于调用者保存的寄存器的调用约定作为异常处理程序。 相反,我们需要保留所有寄存器的调用约定。 x86中断调用约定就是这样的调用约定,因此它可以保证在函数返回时所有寄存器值都恢复为其原始值。 注意,这并不意味着所有寄存器都在函数入口处保存到堆栈中。 而是,编译器仅备份该函数覆盖的寄存器。 这样,可以为仅使用几个寄存器的短函数生成非常有效的代码。

1
2
3
4
5
pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
    idt.load();
}

上面代码让cpu使用自己的idt时会有报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
error[E0597]: `idt` does not live long enough
 --> src/interrupts.rs:7:5
  |
7 |     idt.load();
  |     ^^^-------
  |     |
  |     borrowed value does not live long enough
  |     argument requires that `idt` is borrowed for `'static`
8 | }
  | - `idt` dropped here while still borrowed

error[E0597]: `idt` does not live long enough

原因:

装入方法需要一个&static self,该self对于程序的完整运行时有效。 原因是CPU将在每次中断时访问该表,直到我们加载不同的IDT。 因此,使用比“静态”更短的生存期可能会导致使用后使用错误。 实际上,这正是这里发生的情况。 我们的idt是在堆栈上创建的,因此仅在init函数内部有效。 之后,堆栈存储器将重用于其他功能,因此CPU会将随机堆栈存储器解释为IDT。

为了解决此问题,我们需要将idt存储在具有“静态寿命”的位置。 为此,我们可以使用Box在堆上分配IDT,然后将其转换为“静态引用”,但是由于我们正在编写OS内核,因此还没有堆。 作为替代方案,我们可以尝试将IDT存储为静态:

1
2
3
4
5
6
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();

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

但是,存在一个问题:静态变量是不可变的,因此我们无法通过init函数修改断点条目。 我们可以通过使用静态mut解决此问题:

1
2
3
4
5
6
7
8
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();

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

该变体编译没有错误,但绝不是惯用语言。 静态muts非常容易发生数据争用,因此我们每次访问都需要一个unsafe块。

lazy_static宏的存在可以让我们不需要使用上面的不够安全的方法。 当在第一次引用静态变量时,宏会执行初始化,而不是在编译时评估静态变量。 因此,我们几乎可以在初始化块中执行所有操作,甚至可以读取运行时值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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,但是它在安全接口中被抽象掉了。(Note how this solution requires no unsafe blocks. The lazy_static! macro does use unsafe behind the scenes, but it is abstracted away in a safe interface.)

使异常在我们的内核中起作用的最后一步是从main.rs中调用init_idt函数。 而不是直接调用它,我们在lib.rs中引入了一个通用的init函数:

1
2
3
4
5
// in src/lib.rs

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