You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

教程11 - 异常第一部分: 基础工作

tl;dr

  • 我们为所有的架构CPU exceptions奠定了基础。
  • 目前,仅通过panic!调用打印详细的系统状态,并停止执行
  • 这将有助于在开发和运行时发现错误。
  • 出于演示目的MMU的page faults用于演示(i)从异常返回,以及 (ii)默认的panic!行为。

目录

介绍

现在我们正在EL1中执行,并且已经激活了MMU,是时候实现CPU exceptions了。 目前,我们只建立了一个具有非常基本功能的脚手架,它将帮助我们一路发现错误。 后续的Interrupt教程将继续我们在这里开始的工作。

请注意,本教程特定于AArch64架构。 它还不包含任何通用异常处理代码。

异常类型

AArch64中,区分了四种类型的异常。它们是:

  • Synchronous
    • 例如,data abortpage faultsystem call. 它们的发生是执行某个 CPU 指令的直接结果 因此是synchronously的。
  • Interrupt Request (IRQ)
    • 例如外部设备如定时器正在声明物理中断线。IRQsasynchronously发生。
  • Fast Interrupt Request (FIQ)
    • 这些基本上是优先于普通 IRQ 的中断,并且具有更多特征,使它们适合实现超快速处理。 但是,这超出了本教程的范围。 为了保持这些教程的紧凑和简洁我们将或多或少地忽略FIQ 并仅实现一个会停止 CPU 内核的虚拟处理程序。
  • System Error (SError)
    • 与IRQ一样SErrors也是异步发生的并且在技术上或多或少是相同的。它们的目的是发出系统中相当致命的错误信号 例如,如果SoC互相连接的事务超时。它们是非常特定于实现的,由SoC供应商决定哪些事件作为SError 而不是普通的IRQ来传递。

异常条目

我建议阅读ARMv8 架构参考手册的第 1874-1876 页来了解异常处理机制。

以下是本教程重要功能的摘录:

  • 异常条目将处理器移至相同或更高的Exception Level,但绝不会移至较低的EL
  • 程序状态保存在目标EL处的SPSR_ELx寄存器中。
  • 首选返回地址保存在ELR_ELx寄存器中。
    • 这里的"Preferred"是指ELR_ELx可以保存引起异常(synchronous case)的指令的指令地址,或者由于asynchronous 异常而未完成的第一条指令的指令地址。详细信息请参见ARMv8 架构参考手册的D1.10.1 章。
  • 所有类型的异常都会在发生异常时关闭,因此默认情况下,异常处理程序本身不会被中断。
  • 发生异常将选择目标EL的专用堆栈指针。
    • 例如,如果EL0中发生异常,堆栈指针选择寄存器SPSel将从0切换到1,这意味着除非您明确将其切换回SP_EL0 否则异常向量代码将使用SP_EL1

异常向量

AArch64共有16个异常向量。已经引入的四种类型中的每一种都有一个,此外,还考虑了例外的来源和情况。

以下是ARMv8 架构参考手册的D1.10.2 章中所示决策表的副本:

Exception taken from Offset for exception type
Synchronous IRQ or vIRQ FIQ or vFIQ SError or vSError
Current Exception level with SP_EL0. 0x000 0x080 0x100 0x180
Current Exception level with SP_ELx, x>0. 0x200 0x280 0x300 0x380
Lower Exception level, where the implemented level immediately lower than the target level is using AArch64. 0x400 0x480 0x500 0x580
Lower Exception level, where the implemented level immediately lower than the target level is using AArch32. 0x600 0x680 0x700 0x780

由于我们的内核在EL1中运行,使用SP_EL1,如果我们会导致同步异常,则会执行偏移量为0x200的异常向量。 但这到底意味着什么?

处理程序代码和偏移量

在许多体系结构中,操作系统通过编译一个体系结构定义的数据结构来注册其异常处理程序(也称为向量), 该数据结构存储指向不同处理程序的函数指针。这可以像普通的函数指针数组一样简单。 然后,该数据结构的base address 被存储到专用寄存器中以便CPU可以在发生异常时跳转到相应的处理函数。例如经典的x86_64架构就遵循这一原则。

AArch64中,情况有点不同。在这里,我们还有一个特殊用途的寄存器,称为VBAR_EL1:向量基地址寄存器。

但是它不存储函数指针数组的基地址而是存储包含16个处理程序的内存位置的代码的内存位置。一个处理程序紧接着 另一个处理程序。每个处理程序最多可以占用0x80字节,即128字节的空间。这就是为什么上面的表格显示offsets 为了指示某个处理程序从哪个偏移量开始。

当然您没有义务将所有处理程序代码都塞进128个字节中。您可以随时自由地跳转到任何其他功能。实际上无论如何 在大多数情况下这是需要的,因为上下文保存代码本身就会占用大部分可用空间(您很快就会了解什么是上下文保存)。

此外,还要求Vector Base Address0x800(即2048字节)对齐。

Rust和Assembly实现

该实现混合使用了RustAssembly代码。

上下文保存和还原

与任何其他代码一样,异常向量使用一堆公共共享的处理器资源。最重要的是AArch64中每个核心提供的 General Purpose Registers(GPRs)集合 (x0-x30)。

为了在执行异常向量代码时不污染这些寄存器,通常的做法是将这些共享资源保存在内存中(准确地说是堆栈)作为第一个操作。 这通常被描述为保存上下文。 然后,异常向量代码可以毫不费力地在自己的代码中使用共享资源, 并且作为从异常处理代码返回之前的最后一个操作,恢复上下文,以便处理器可以在处理异常之前从中断处继续。

上下文保存和恢复是系统软件中为数不多的无法绕过手动组装的地方之一。引入exception.s:

/// Call the function provided by parameter `\handler` after saving the exception context. Provide
/// the context as the first parameter to '\handler'.
.macro CALL_WITH_CONTEXT handler
__vector_\handler:
	// Make room on the stack for the exception context.
	sub	sp,  sp,  #16 * 17

	// Store all general purpose registers on the stack.
	stp	x0,  x1,  [sp, #16 * 0]
	stp	x2,  x3,  [sp, #16 * 1]
	stp	x4,  x5,  [sp, #16 * 2]
	stp	x6,  x7,  [sp, #16 * 3]
	stp	x8,  x9,  [sp, #16 * 4]
	stp	x10, x11, [sp, #16 * 5]
	stp	x12, x13, [sp, #16 * 6]
	stp	x14, x15, [sp, #16 * 7]
	stp	x16, x17, [sp, #16 * 8]
	stp	x18, x19, [sp, #16 * 9]
	stp	x20, x21, [sp, #16 * 10]
	stp	x22, x23, [sp, #16 * 11]
	stp	x24, x25, [sp, #16 * 12]
	stp	x26, x27, [sp, #16 * 13]
	stp	x28, x29, [sp, #16 * 14]

	// Add the exception link register (ELR_EL1), saved program status (SPSR_EL1) and exception
	// syndrome register (ESR_EL1).
	mrs	x1,  ELR_EL1
	mrs	x2,  SPSR_EL1
	mrs	x3,  ESR_EL1

	stp	lr,  x1,  [sp, #16 * 15]
	stp	x2,  x3,  [sp, #16 * 16]

	// x0 is the first argument for the function called through `\handler`.
	mov	x0,  sp

	// Call `\handler`.
	bl	\handler

	// After returning from exception handling code, replay the saved context and return via
	// `eret`.
	b	__exception_restore_context

.size	__vector_\handler, . - __vector_\handler
.type	__vector_\handler, function
.endm

首先,定义一个用于保存上下文的宏。 它最终跳转到后续处理程序代码,并最终恢复上下文。事先,我们在堆栈上为上下文预留空间。 也就是说30个GPRslink registerexception link register(保存首选返回地址), saved program statusexception syndrome register。之后,我们存储这些寄存器,将当前堆栈地址保存在 x0中,并跳转到后续处理程序代码,其函数名作为参数提供给宏(\handler)。

处理程序代码将用Rust编写但使用平台的C ABI。这样我们可以定义一个函数签名函数签名将指向堆栈上的上下文数据 的指针作为其第一个参数,并且知道该参数预计位于x0寄存器中。我们需要在这里使用C ABI因为Rust没有稳定的实现 参考(Issue).

异常矢量表

接下来,我们制作异常向量表:

// Align by 2^11 bytes, as demanded by ARMv8-A. Same as ALIGN(2048) in an ld script.
.align 11

// Export a symbol for the Rust code to use.
__exception_vector_start:

// Current exception level with SP_EL0.
//
// .org sets the offset relative to section start.
//
// # Safety
//
// - It must be ensured that `CALL_WITH_CONTEXT` <= 0x80 bytes.
.org 0x000
	CALL_WITH_CONTEXT current_el0_synchronous
.org 0x080
	CALL_WITH_CONTEXT current_el0_irq
.org 0x100
	FIQ_SUSPEND
.org 0x180
	CALL_WITH_CONTEXT current_el0_serror

// Current exception level with SP_ELx, x > 0.
.org 0x200
	CALL_WITH_CONTEXT current_elx_synchronous
.org 0x280
	CALL_WITH_CONTEXT current_elx_irq
.org 0x300
	FIQ_SUSPEND
.org 0x380
	CALL_WITH_CONTEXT current_elx_serror

[...]

请注意每个向量如何使用.org指令从节开始处所需的偏移量开始。每个宏调用都会引入一个显式处理函数名称,该函数名称在 exception.rs中用Rust实现。

实现处理程序

文件exception.rs首先定义了异常上下文的struct,该结构由汇编代码存储在堆栈上:

/// The exception context as it is stored on the stack on exception entry.
#[repr(C)]
struct ExceptionContext {
    /// General Purpose Registers.
    gpr: [u64; 30],

    /// The link register, aka x30.
    lr: u64,

    /// Exception link register. The program counter at the time the exception happened.
    elr_el1: u64,

    /// Saved program status.
    spsr_el1: SpsrEL1,

    // Exception syndrome register.
    esr_el1: EsrEL1,
}

处理程序采用struct ExceptionContext参数。由于我们还不打算为每个异常实现处理程序,因此提供了一个默认处理程序:

/// Prints verbose information about the exception and then panics.
fn default_exception_handler(exc: &ExceptionContext) {
    panic!(
        "CPU Exception!\n\n\
        {}",
        exc
    );
}

从程序集中引用的实际处理程序现在可以暂时跳转到它,例如:

#[no_mangle]
extern "C" fn current_elx_irq(e: &mut ExceptionContext) {
    default_exception_handler(e);
}

引发异常 - 测试代码

我们希望看到两个实际案例:

  1. 异常的获取、处理和返回是如何工作的。
  2. 未处理异常的panic!宏打印是什么样子的。

因此,通过调用在main.rs中设置异常之后的函数来引发异常:

exception::handling_init();

我们通过从内存地址8 GiB读取来引发数据中止异常:

// Cause an exception by accessing a virtual address for which no translation was set up. This
// code accesses the address 8 GiB, which is outside the mapped address space.
//
// For demo purposes, the exception handler will catch the faulting 8 GiB address and allow
// execution to continue.
info!("");
info!("Trying to read from address 8 GiB...");
let mut big_addr: u64 = 8 * 1024 * 1024 * 1024;
unsafe { core::ptr::read_volatile(big_addr as *mut u64) };

这会触发我们的异常代码,因为我们尝试从尚未安装映射的虚拟地址读取。请记住,在上一教程中我们仅映射了最多 4 GiB的地址空间。

为了避免出现这种异常,相应的处理程序有一个特殊的演示案例:

#[no_mangle]
extern "C" fn current_elx_synchronous(e: &mut ExceptionContext) {
    if e.fault_address_valid() {
        let far_el1 = FAR_EL1.get();

        // This catches the demo case for this tutorial. If the fault address happens to be 8 GiB,
        // advance the exception link register for one instruction, so that execution can continue.
        if far_el1 == 8 * 1024 * 1024 * 1024 {
            e.elr_el1 += 4;

            return;
        }
    }

    default_exception_handler(e);
}

它检查错误地址是否等于8 GiB如果是,则将ELR的副本前进4以便它指向引起异常的指令之后的下一条指令。 当处理程序返回时,我们之前介绍的汇编宏将继续执行。该宏只剩下一行: b __exception_restore_context 它跳转到一个汇编函数,该函数在最终执行eret返回异常之前演示我们保存的上下文。

这将使我们回到main.rs。但我们也想看到panic!宏打印。

因此,第二次读取完成,这次是从地址9 GiB开始。处理程序无法捕获的情况,最终引发panic!从默认处理程序调用。

测试

$ make chainboot
[...]
Minipush 1.0

[MP] ⏳ Waiting for /dev/ttyUSB0
[MP] ✅ Serial connected
[MP] 🔌 Please power the target now

 __  __ _      _ _                 _
|  \/  (_)_ _ (_) |   ___  __ _ __| |
| |\/| | | ' \| | |__/ _ \/ _` / _` |
|_|  |_|_|_||_|_|____\___/\__,_\__,_|

           Raspberry Pi 3

[ML] Requesting binary
[MP] ⏩ Pushing 64 KiB =========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now

[    0.798323] mingo version 0.11.0
[    0.798530] Booting on: Raspberry Pi 3
[    0.798985] MMU online. Special regions:
[    0.799462]       0x00080000 - 0x0008ffff |  64 KiB | C   RO PX  | Kernel code and RO data
[    0.800480]       0x3f000000 - 0x4000ffff |  17 MiB | Dev RW PXN | Device MMIO
[    0.801369] Current privilege level: EL1
[    0.801845] Exception handling state:
[    0.802290]       Debug:  Masked
[    0.802680]       SError: Masked
[    0.803069]       IRQ:    Masked
[    0.803459]       FIQ:    Masked
[    0.803849] Architectural timer resolution: 52 ns
[    0.804423] Drivers loaded:
[    0.804759]       1. BCM PL011 UART
[    0.805182]       2. BCM GPIO
[    0.805539] Timer test, spinning for 1 second
[    1.806070]
[    1.806074] Trying to read from address 8 GiB...
[    1.806624] ************************************************
[    1.807316] Whoa! We recovered from a synchronous exception!
[    1.808009] ************************************************
[    1.808703]
[    1.808876] Let's try again
[    1.809212] Trying to read from address 9 GiB...
[    1.809776] Kernel panic!

Panic location:
      File 'src/_arch/aarch64/exception.rs', line 58, column 5

CPU Exception!

ESR_EL1: 0x96000004
      Exception Class         (EC) : 0x25 - Data Abort, current EL
      Instr Specific Syndrome (ISS): 0x4
FAR_EL1: 0x0000000240000000
SPSR_EL1: 0x600003c5
      Flags:
            Negative (N): Not set
            Zero     (Z): Set
            Carry    (C): Set
            Overflow (V): Not set
      Exception handling state:
            Debug  (D): Masked
            SError (A): Masked
            IRQ    (I): Masked
            FIQ    (F): Masked
      Illegal Execution State (IL): Not set
ELR_EL1: 0x00000000000845f8

General purpose register:
      x0 : 0x0000000000000000         x1 : 0x0000000000086187
      x2 : 0x0000000000000027         x3 : 0x0000000000081280
      x4 : 0x0000000000000006         x5 : 0x1e27329c00000000
      x6 : 0x0000000000000000         x7 : 0xd3d18908028f0243
      x8 : 0x0000000240000000         x9 : 0x0000000000086187
      x10: 0x0000000000000443         x11: 0x000000003f201000
      x12: 0x0000000000000019         x13: 0x00000000ffffd8f0
      x14: 0x000000000000147b         x15: 0x00000000ffffff9c
      x16: 0x000000000007fd38         x17: 0x0000000005f5e0ff
      x18: 0x00000000000c58fc         x19: 0x0000000000090008
      x20: 0x0000000000085fc0         x21: 0x000000003b9aca00
      x22: 0x0000000000082238         x23: 0x00000000000813d4
      x24: 0x0000000010624dd3         x25: 0xffffffffc4653600
      x26: 0x0000000000086988         x27: 0x0000000000086080
      x28: 0x0000000000085f10         x29: 0x0000000000085c00
      lr : 0x00000000000845ec

相比之前的变化

请检查英文版本,这是最新的。