minos 2.1 中断虚拟化——ARMv8 异常处理

首发公号:Rand_cs

该项目来自乐敏大佬:https://github.com/minosproject/minos

越往后,交叉的越多,大多都绕不开 ARMv8 的异常处理,所以必须得先了解了解 ARMv8 的异常处理流程

先说一下术语,从手册中的用词来看,在 x86 平台,一般将异常和中断统称为中断,在 ARM 平台,一般将中断和异常统称为异常

异常的流程,可以分为 3 个阶段,"设备"产生异常信号,中断控制器过滤转发异常,OS 处理异常。设备产生异常的部分我们不讨论,后两个阶段需要仔细说道说道,先来看 ARM 的中断控制器。

GIC(Generic Interrupt Controller)

ARM 使用的中断控制叫做 Generic Interrupt Controller,中断控制器主要作用就是转发设备的中断信号,因为外设可能很多,中断信号也很多,不可能每个外设都连一根线与 cpu 相连,所以要有中断控制器来作为中转站

gic 发展到现在有 4 个版本:

上述是手册给出的每个 gic 版本特性,以及对应的 ARM 芯片型号,目前手机端应该用的是 GICv3 居多,qemu 默认使用的似乎是 GICv2。

gicv1 只支持 8 个 PE,PE 是 arm 架构下定义的抽象机器,processing element,可以简单理解为最多支持 8 个 cpu。最多支持 1020 个中断,支持 2 种安全状态,arm 架构一直有个 安全模式,这个我们暂且不提

从 gicv2 开始便在硬件级别支持了虚拟化,有了这个扩展,可以向虚拟机注入中断,也就是向虚拟机发送中断信号

从 gicv3 开始,gic 的架构有了较大的变化,具体的见手册,从 gicv4 开始支持直接投递中断,具体含义后面详细说明,这里只是过过眼,本篇讲述 gicv2 的基本知识

gicv2

中断状态
  • inactive:中断处于无效不活跃的状态
  • pending:中断处于有效状态,但是 cpu 没有响应该中断,正在 pending 等待被响应中
  • active:cpu 正在响应该中断
  • active and pending:cpu 正在响应该中断,对应的中断源又发送了一个中断信号

后续代码讲述状态到底是如何转换的

中断类型
  • Shared Peripheral Interrupt (SPI) 共享外设中断,中断号为 32~1019,这类中断被共享意思是任何一个 cpu 都可以处理,但最终只会被路由到某一个 cpu。普通的外设中断类型基本都是 SPI
  • Private Peripheral Interrupt (PPI) 私有外设中断,中断号为 16~31,一个 CPU 私有外设中断,比如说本地计时器等等,对应着 x86 平台下 lapic 内部中断
  • Software-generated interrupt (SGI) 软件中断,也叫做核间中断,是 cpu 向 cpu/cpu组发送中断信号,中断号为 0~15,对应着 x86 平台下的 IPI 中断
  • Virtual interrupt,虚拟中断,并不是这个中断是软件模拟的,没有实际物理上的中断信号,而是说一个中断信号发送给了虚拟机
触发方式

edge 边沿触发,level 电平触发

gicv2 架构

gicv2 的总体架构图如上所示,主要分为两部分:

  • Distributor,主要作用是收集中断信号,有三个来源分别表示 SGI、PPI、SPI,然后按照一定的规则(寄存器配置)发送给目标 CPU Interface
  • Interface,一个 Interface 对应着一个 PE/CPU,Interface 按照一定的规则将中断信号发送给对应的 CPU
gicv2 寄存
Distributor,GICD_xxx 寄存器

有关 Distributor 的寄存器都是以 GICD_xxx 开头

主要寄存器如上所示,具体含义见手册,这里我就不做这个翻译工作了

Interface,GICC_xxx 寄存器

Interface 的寄存器都是以 GICC_xxx 开头:

同样的就不做翻译工作了,重点寄存器我后面都会在代码中提及

gicv3

TODO

ARMv8 异常模型

NOTE,以下大部分内容来自 ARM 手册 https://developer.arm.com/documentation/102412/0103,感兴趣的建议直接看原手册,更详尽。

ARMv8 相较于 ARMv7,在整体架构上有了较大的变化,ARMv8 实现了 4 个异常等级 EL0~EL3,EL0 运行用户态程序,EL1 运行 Guest OS,EL2 运行 hypervisor,EL3 运行 secure monitor 程序。

EL0、EL1 是 ARM 芯片必须实现的异常级别,EL2、EL3 异常级别是可选的

另外还有个安全世界,EL1、EL2 可以通过 smc 指令切换到安全世界,安全世界也运行了一个 OS,叫做 TrustOS,TrustOS 之上也运行了一些应用,通常称作为安全应用,比如手机中常见的支付操作,与安全强相关的操作都会调用到安全世界。可以这样简单理解,安全世界提供了一系列安全功能,需要使用 smc 系统调用来调用这些安全服务。安全世界的话题就此打住,后面有时间写个 TEE 系列(经典有时间)

执行状态

ARMv8 支持 AArch32 和 AArch64 两种执行状态,不同的执行状态下使用的指令集和寄存器都是不同的(是同一个物理硬件,但是使用的寄存器的位数命名等有所不同)

执行状态是可以切换的,但只有在系统重置或者异常级别更改的时候才能更改执行状态。在异常级别更改的时候更改执行状态也有两条规则:

  • 从较低的异常级别切换到较高的异常级别时,执行状态只能不变或者切换为 AArch64
  • 从较高的异常级别切换到较低的异常级别时,执行状态只能不变或者切换为 AArch32

这两条规则就是说 64 位层次可以托管 32 位层次,反之则不行,比如说 64 位的内核上可以运行 32 位程序,而 32 位的内核只能运行 32 位的应用程序。一图以蔽之:

异常类型

前面说过在 gic 的角度上来看,中断可以分为 SGI、SPI、PPI。现在从 CPU 角度来看,异常有哪些类型,主要分为两大类:Synchronous exceptions 和 Saynchronous exceptions,就是同步异常和异步异常。就是 x86 平台对异常和中断的分类,两个平台习惯叫法不同而已。

同步异常

同步异常有以下几个特点:

  1. 异常是由于指令执行造成的,比如执行 ld 指令,对目标地址做地址转换的时候发生缺页,导致缺页异常
  2. 异常处理后的返回地址与导致异常的指令有体系结构的关系,就是说不同的异常,其返回地址可能是不同的,可能是异常指令的地址,也可能是异常指令后面一条指令的地址
  3. 异常是精确的,对于芯片在异常部分的设计,有个很重要的概念就是精确异常,它指的是该指令之前进入流水线的所有指令都必须正常运行完毕,而该指令及之后进入流水线的指令都必须从流水线中清除,不影响任何处理器的状态,就好像什么事情都没有发生一样。这样的好处是可以准确找到异常处理后的返回地址

同步异常又分为以下几种情况:

  1. invalid instructions,指令无效的原因很多,包括未定义的指令,当前异常级别不允许的指令
  2. trap exceptions,通过设置某些控制寄存器拦截某些指令,比如 HCR 等寄存器就可以拦截某些指令执行,让其 trap 到 EL2
  3. memory access,MMU 执行检查的时候可能会产生一些异常,比如说写入一个只读地址
  4. Exception-generating instructions,这指的是 svc、hvc、smc 指令,就是系统调用指令,它们的关系,获得服务如下所示:
  1. Debug exceptions,有一些调试专用寄存器,可以设置这些寄存器,然后触发某些条件来触发异常。比如说硬件断点异常,可以设置某个地址到断点寄存器,执行到该地址表示的指令的时候就会触发一个异常,然后转移到断点处理程序。(我们平时打断点是软件断点不是硬件断点)
异步异常

异步异常是在 CPU 外部产生的,所以与当前的指令流不同步。异步异常与当前正在执行的指令没有直接关联,通常是来自处理器外部的系统事件,异步异常通常又被称为中断,异步异常有以下几种情况:

  1. 常见的 gic 中断控制发过来的中断,分为 irq 和 fiq,通过上面 gic 的架构图可以看出,它们都有实际的信号线连接着 cpu,比较紧急需要快速响应的中断通过 fiq 发送给 cpu
  2. SError,系统错误,比如访存的时候总线通信上遇到某些错误
  3. 虚拟中断,virq、vfiq、vserror,可以直接注入虚拟机,从上面的 gic 架构图来看,也是有实际的信号线连接着 cpu,这部分后文详细讨论

寄存器

这部分再过一遍异常流程涉及的一些寄存器

通用寄存器

31 个 64bit 通用寄存器 x0~x31,x29 是 fp 保存上一个栈帧底部地址,x30 保存返回地址,minos 中 x18 存放当前线程地址

特殊寄存器
  • zero register,全 0 值
  • PC,保存下一条指令地址
  • PSTATE,processor state,标志着当前 CPU 的状态
  • 4 个不同异常等级的 SP_ELx
    • 在其他任何异常级别执行时,可以将处理器配置为使用SP_EL0或配置为对应该异常级别的堆栈指针SP_ELx
    • 软件可以在目标异常级别执行的时候通过更新PSTATE.SP来指向SP_EL0的堆栈指针
  • 3 个不同异常等级的 SPSR_ELx
    • saved processor state register ,保存执行异常前的 PSTATE 状态值
  • 3 个不同异常等级的 ELR_RLx,exception link register ,保存异常返回地址
  • ESRx,Exception Syndrome Register异常综合表征寄存器,简单来说就是存放异常原因
  • VBAR_ELx,Vector Base Address Register,存放异常向量表的基地址
  • FAR_ELx,存放出故障的虚拟地址
  • HCR_ELx,Hypervisor Configuration Register,最显著的作用就是可以配置此寄存器使得某些异常被 trap 到 hypervisor
Register Name Description
Exception Link Register ELR_ELx Holds the address of the instruction which caused the exception
Exception Syndrome Register ESR_ELx Includes information about the reasons for the exception
Fault Address Register FAR_ELx Holds the virtual faulting address
Hypervisor Configuration Register HCR_ELx Controls virtualization settings and trapping of exceptions to EL2
Secure Configuration Register SCR_ELx Controls Secure state and trapping of exceptions to EL3
System Control Register SCTLR_ELx Controls standard memory, system facilities, and provides status information for implemented functions
Saved Program Status Register SPSR_ELx Holds the saved processor state when an exception is taken to this ELx
Vector Base Address Register VBAR_ELx Holds the exception base address for any exception that is taken to ELx

异常处理

异常处理是硬件和软件一起完成的,对于硬件 CPU 需要完成的部分是保存最基本的现场,它会做以下的事情:

  1. 将 PSTATE 的内容保存到 SPSR_ELx(ELx 指的是在异常级别 ELx 处理异常)
  2. 将 PC 保存到 ELR_ELx
  3. 对于同步异常和 SError,将异常原因写入 ESR_ELx
  4. 对于与地址相关的异常,将出错的地址写入 FAR_ELx

异常处理完成后,基本就是上述的逆操作,总体如下图所示:

上述描述有一个小问题,异常处理通常都是在更高级别或者同级别处理(EL0 不能处理异常),那这个更高级别指的是哪个级别,有多高?

如果是只实现了 EL0 和 EL1 两个级别的机器,那么就只能在 EL1 级别处理中断。如果 4 个级别都实现了,那么也有一些寄存器来配置异常的路由情况。比如说配置 HCR 寄存器可以将一些异常路由到 EL2 的 hypervisor,执行 EL2 的异常处理程序来处理异常,配置 SCR 寄存器可以将异常路由到 secure monitor。所以这个具体在哪一个级别处理异常都是可以配置的,通常有 hypervisor,就会配置 HCR,让异常路由到 EL2,EL2 可以自己处理,也可以再注入到虚拟机,让虚拟机的内核处理。

异常向量表

还记得初次见这张异常向量表的时候,当时是在奔叔的 Linux 书籍里面,那是一脸的懵逼,这里来详细解释一下。

首先回顾一下,异常入口确定方式

中断向量(Interrupt Vector)是计算机系统中用于处理中断的一种技术。它是一个包含各种中断处理程序入口地址的表格。当发生中断时,中断控制器通过查找中断向量表找到对应的中断处理程序地址,然后跳转到该地址执行相应的处理。

  • 向量方式:CPU 根据硬件中断号直接获取相应的中断服务程序的地址,然后跳去执行
  • 查询方式:CPU 跳去一个特定的地址,此地址一般是一个通用的中断处理程序,由它来查询中断源(一般是中断控制器中的某个寄存器),然后根据不同的中断源执行不同的中断处理程序

EL1~EL3 都有一个 VBAR_ELx 寄存器,里面存放着异常向量表的基地址,都有一个像上面的异常向量表。向量可以分为两大类,四种:

  • Exception from Lower EL,从低特权级来的异常
    • 低特权级的执行状态为 AArch64
    • 低特权级的执行状态为 AArch32
  • Exception from the current EL,异常就来自当前特权级
    • 当前选择了使用 SP_EL0 处理异常,就是处理异常使用 EL0 的栈空间
    • 当前选择了使用 SP_ELx 处理异常,异常处理使用当前特权级的栈空间
SP_EL0

大部分应该还是挺好理解的,就是为啥有个 SP_EL0,正常情况下,就是处于哪个异常等级,就是用哪个等级的 SP。但是 ARM 提供了一种机制可以在 ELx 使用 SP_EL0:

  • PSTATE.SPSel = 0,那么使用 SP_EL0
  • PSTATE.SPSel = 1,使用 SP_ELx

什么情况下会在 ELx 上使用 SP_EL0 呢?

一般来说 EL0 用户态的栈比较大,在内核栈可能溢出的情况下,那么我们就可以使用 EL0 栈。对于栈溢出的情况,通常都要使用另外一个栈来处理。在信号处理中,对于栈溢出通常需要使用 sigaltstack 系统调用来设置另外一个栈来处理信号(原来的栈满了,当然不能执行函数处理信号了)

但是 Linux 内核现在都没使用这个特性,不过既然 ARM 提供了这个特性,那么可以用它来存放一些内核重要数据,比如说 Linux 内核常见的 get_current() 获取当前线程结构体指针:

C 复制代码
static __always_inline struct task_struct *get_current(void)
{
    unsigned long sp_el0;

    asm ("mrs %0, sp_el0" : "=r" (sp_el0));

    return (struct task_struct *)sp_el0;
}

#define current get_current()

可以看出,在内核态(EL1) 的时候,SP_EL0 里面存放的是当前线程结构体指针。这个 SP_EL0 在用户态的时候指向的是用户栈,那什么时候变成 task_struct 指针的,又是什么时候恢复的呢?可以猜到,多半是进入内核态以及返回用户态的时候做的这些操作,查找代码验证下:

C 复制代码
// linux-6.6.23
// 进入内核态时:
    .if \el == 0
    clear_gp_regs
    mrs x21, sp_el0
    ldr_this_cpu    tsk, __entry_task, x20
    msr sp_el0, tsk

原先的 SP_EL0 和其他的通用寄存器都会保存到 SP_EL1,等到异常返回时,都会恢复

异常处理流程

现代的异常处理基本都包括上述两个阶段:

  • 首先跳转到异常向量记录的地址,进行第一级的处理,这个阶段主要保存现场,就是将一系列寄存器保存到高特权级栈中
  • 然后跳转到实际的异常处理程序处理异常,比如说键盘中断就跳转到键盘中断 handler。但一般来说这个后续的处理也不是一个函数,一个 handler 就搞定了,比如在 Linux 里面还分了中断上下半部来处理。但总的来说这个第二阶段就是执行 handler 来处理中断

异常返回

处理完成之后,执行上述的逆操作,将保存在高特权级栈里面的信息恢复,之后通常紧接着一条 eret 指令,实现异常返回,异常返回就是将保存在 SPSR_ELx 寄存器中的状态字恢复到 PSTATE,将保存在 ELR 的返回地址恢复到 PC

首发公号:Rand_cs

相关推荐
虾..2 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙2 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
hkhkhkhkh1234 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen5 小时前
Linux字符串处理
linux·string
张童瑶5 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功1235 小时前
什么是SELinux
linux
石小千5 小时前
Linux安装OpenProject
linux·运维
柏木乃一5 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-30905 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu
百年渔翁_肯肯6 小时前
Linux 与 Unix 的核心区别(清晰对比版)
linux·运维·unix