*OS运行态切换

Published: 2024年09月08日

In Kernel.

ARMv8的异常机制

运行态

这里的运行态指特权级别,非执行状态(AArch32/AArch64),ARMv8-A 架构使用异常级别(Exception levels)表特权级别,从 EL0 到 EL3权限依次提升,其中:

  • EL0 级别执行称为非特权执行(unprivileged execution),通常为用户态。
  • EL1 通常执行操作系统内核及其相关功能,通常称为内核态。
  • EL2 提供了对非安全操作虚拟化的支持,通常跑虚拟机监控程序(Hypervisor)。
  • EL3 提供了在两种安全状态(Secure state 和 Non-secure state)之间切换的支持,即跑安全监控程序(Secure monitor)。

一个实现必须包含 EL0 和 EL1,而EL2 和 EL3 是可选的。 当在 AArch64 状态下执行时,执行只能在发生异常或从异常返回时在异常级别之间移动: - 在发生异常时,异常级别只能增加或保持不变。 - 在从异常返回时,异常级别只能减少或保持不变。

在发生异常时,执行切换到的或保持的异常级别称为该异常的目标异常级别(target Exception level)。 每种异常类型都有一个目标异常级别,该级别要么是:

  • 由异常的性质隐含决定。

  • 由系统寄存器中的配置位定义。

异常不能以 EL0 为目标。 异常级别存在于特定的安全状态(Security state)中。当在某个异常级别执行时,处理单元(PE)可以访问以下两者:

  • 当前异常级别和当前安全状态组合下可用的资源。

  • 所有较低异常级别可用的资源,前提是这些资源对当前安全状态可用。

这意味着,如果实现包含 EL3,则当执行在 EL3 时,PE 可以访问所有异常级别下可用的资源,适用于两种安全状态。

除了 EL0 之外,每个异常级别都有自己的转换机制(translation regime)和相关的控制寄存器。

image-20250121145427239

注:具体跳到哪一级,由控制寄存器来决定,比如默认EL0是跳到EL1,但设置了HCR就可以直接跳到EL2,具体可见异常相关的伪代码。

异常类型

主要分同步和异步,判断标准很复杂,粗略来说就是异常是否由于直接执行或尝试执行指令而生成的。

中断(异步异常)

异步异常的类型
类型 名称 描述
物理中断 SError 系统错误中断,通常由内存系统或其他硬件错误引发。
IRQ 普通中断请求,优先级低于 FIQ,用于处理一般外设中断。
FIQ 快速中断请求,优先级高于 IRQ,用于处理需要快速响应的外设中断。
虚拟中断 vSError 虚拟系统错误中断,由 EL2 软件启用并挂起。
vIRQ 虚拟普通中断请求,由 EL2 软件启用并挂起。
vFIQ 虚拟快速中断请求,由 EL2 软件启用并挂起。

异常(同步异常)

它存在如下的同步异常:

同步异常的种类
异常类型 描述
未定义指令异常 尝试执行未定义的指令时触发,如在不适当的异常级别(Exception Level)执行指令、在指令被禁用时尝试执行、尝试执行未分配的指令位模式。
非法执行状态异常 PSTATE.IL 为 1 时,尝试执行指令会触发此异常,通常是由于非法返回事件,可以见异常返回那儿。
栈指针(SP)未对齐异常 使用未对齐的栈指针(SP)时触发。
程序计数器(PC)未对齐异常 尝试执行未对齐的程序计数器(PC)指向的指令时触发。
异常生成指令异常 执行以下异常生成指令时触发:
  • SVC(Supervisor Call): 用于从 EL0 切换到 EL1。
  • HVC(Hypervisor Call): 用于从 EL1 切换到 EL2。
  • SMC(Secure Monitor Call): 用于从 Non-secure 状态切换到 Secure 状态。
指令陷阱异常 尝试执行被系统寄存器配置为陷阱的指令时触发,某些指令被配置为触发陷阱,以便在更高异常级别处理。
指令中止异常(Instruction Abort) 内存地址转换系统在尝试执行指令时发生故障时触发,如访问的内存区域生成故障(如页表无效或权限不足)。
数据中止异常(Data Abort) 内存地址转换系统在尝试读取或写入内存时发生故障时触发,如访问的内存区域生成故障(如页表无效或权限不足)。
未对齐地址数据中止异常 尝试访问未对齐的内存地址时触发。
调试异常 与调试相关的异常,包括:
  • 断点指令异常(Breakpoint Instruction exceptions)是执行到断点指令时触发的异常。
  • 断点异常(Breakpoint exceptions)是访问到硬件中断地址时触发的异常。
  • 观察点异常(Watchpoint exceptions)
  • 向量捕获异常(Vector Catch exceptions)是触发了特定的异常时产生的异常。
  • 软件步进异常(Software Step exceptions)
浮点异常 在支持浮点异常捕获的实现中,IEEE 浮点异常会触发此异常,如浮点运算中发生异常(如除以零或溢出)。
外部中止异常(External Abort) 内存访问失败时触发,包括地址转换期间的内存系统部分访问失败。ARMv8 架构允许但不要求实现将此类异常视为同步异常。

复位

略略略~

寄存器

异常链接寄存器用于保存异常返回地址,每个目标异常级别会有一个ELR,即有ELR_EL1~ELR_EL3,在异常处理完成后返回到该地址继续执行。

SPSR(Saved Program Status Register)

保存的程序状态寄存器用于在发生异常时保存处理单元(PE)的状态,在 AArch64 状态下,每个可以目标异常级别都有一个 SPSR,即存在SPSR_EL1~SPSR_EL3三个,在异常返回时用来恢复PSTATE,根据异常发生时两种执行态它们的域有差别:

image-20250121162400509
image-20250121162518032

下面是详细解释:

SPSR各位作用
位域 描述 详细作用
N, Z, C, V, bits[31:28] 异常发生前 PSTATE.{N, Z, C, V} 条件标志的值。 这些是条件标志位,用于记录上一条指令的执行结果:
  • N(Negative):结果为负时置位。
  • Z(Zero):结果为零时置位。
  • C(Carry):发生进位或借位时置位。
  • V(Overflow):发生溢出时置位。
Bits[27:24], 从 AArch64 状态发生异常 保留,RES0。 这些位保留未使用,通常设置为 0(RES0)。
Q, bit[27], 从 AArch32 状态发生异常 异常发生前 PSTATE.Q 的值。 Q 位用于指示 SIMD 或浮点指令是否发生了溢出或饱和。在 AArch32 状态下,它记录异常发生前的 SIMD 溢出状态。
IT[1:0], bits[26:25], 从 AArch32 状态发生异常 IT 位用于 AArch32 状态下的条件执行指令(如 IT 块)。IT[1:0] 与 IT[7:2] 一起表示条件执行的状态。
J, bit[24], 从 AArch32 状态发生异常 显示异常发生前 PSTATE.J 的值。此位为 RES0。 J 位在 AArch32 状态下用于指示 Jazelle 执行状态。在 AArch64 状态下未使用,通常为 0(RES0)。
UAO, bit[23], 从 ARMv8.2 开始 异常发生前 PSTATE.UAO 的值。 UAO(User Access Override)位用于控制用户模式下的内存访问权限。当置位时,允许用户模式访问某些特权内存区域。
PAN, bit[22], 从 ARMv8.1 开始 异常发生前 PSTATE.PAN 的值。 PAN(Privileged Access Never)位用于防止特权模式(如内核)访问用户空间内存。当置位时,特权模式访问用户内存会触发异常。
SS, bit[21] 软件步进位。 SS(Software Step)位用于调试。当置位时,PE 会在执行下一条指令后触发调试异常,用于单步调试。
IL, bit[20] 非法执行状态位。 IL(Illegal Execution)位指示是否发生了非法执行状态。例如,尝试从 AArch64 状态返回到 AArch32 状态时,此位置位。
Bits[19:10], 从 AArch64 状态发生异常 保留,RES0。 这些位保留未使用,通常设置为 0(RES0)。
GE[3:0], bits[19:16], 从 AArch32 状态发生异常 显示异常发生前 PSTATE.GE 的值。 GE(Greater than or Equal)位用于 AArch32 状态下的 SIMD 指令,表示每个字节或半字的比较结果。
IT[7:2], bits[15:10], 从 AArch32 状态发生异常 与 IT[1:0] 一起,显示异常发生前 PSTATE.IT 的值。 IT 位用于 AArch32 状态下的条件执行指令(如 IT 块)。IT[7:2] 与 IT[1:0] 一起表示条件执行的状态。
D, bit[9], 从 AArch64 状态发生异常 调试异常屏蔽位。 D 位用于屏蔽调试异常。当置位时,调试异常被禁用。
E, bit[9], 从 AArch32 状态发生异常 数据访问的字节序。 E 位用于指示数据访问的字节序(大端或小端)。当置位时,表示大端模式;清零时,表示小端模式。
A, bit[8] SError 中断屏蔽位。 A 位用于屏蔽 SError(系统错误)中断。当置位时,SError 中断被禁用。
I, bit[7] IRQ 屏蔽位。 I 位用于屏蔽 IRQ(普通中断)。当置位时,IRQ 中断被禁用。
F, bit[6] FIQ 屏蔽位。 F 位用于屏蔽 FIQ(快速中断)。当置位时,FIQ 中断被禁用。
Bit[5], 从 AArch64 状态发生异常 保留,RES0。 此位保留未使用,通常设置为 0(RES0)。
T, bit[5], 从 AArch32 状态发生异常 异常发生前 PSTATE.T 的值。 T 位用于指示 AArch32 状态下的 Thumb 执行状态。当置位时,表示当前执行 Thumb 指令集。
M[4:0], bits[4:0] 此字段的名称继承自 ARMv7,其中 M 字段指定了 PE 模式。 M M[4]表示异常发生时的执行状态,如1为AArch32,0为AArch64,剩下的根据它含义有所不同:
  • M[4]=0,M[3:2]为异常级别,M[1]为RES0,M[0]为异常SP选择器,0为t,1为h。
  • M[4]=1,M[3:0]为工作模式

SP(Stack Pointer)

每个实现的异常级别都有一个专用的栈指针寄存器,除了各级别专有的SP,EL1~EL3还能用EL0的SP,这里用t表示使用SP_EL0h表示使用SP_ELx

异常级别 AArch64 栈指针选项
EL0 EL0t
EL1 EL1t, EL1h
EL2 EL2t, EL2h
EL3 EL3t, EL3h

在每次异常切换时,硬件会自动使用对应目标异常级别的SP,而后异常处理程序可通过设置PSTATE.SP来手动切换。

ESR(Exception Syndrome Register)

异常综合信息寄存器保存同步异常和系统错误的综合信息,每个目标级别有一个,即ESR_EL1~ESR_EL3:

image-20250121185732378

它的含义如下:

  • EC:Exception Class表示异常的类别。
  • IL:Instruction Length用于同步异常,表示造成异常的指令长度。
  • ISS:Instruction Specific Syndrome保存EC类下更详细的信息,根据EC不同而编码不同内容。

这一块内容很多,可以直接见手册的D1.10.4 Exception classes and the ESR_ELx syndrome registers部分,不再额外列了~

FAR(Fault Address Register)

它是64位的,存储导致故障或异常的虚拟地址,在处理页面故障(以便在地址有效时调入页面)和检测内核内存操作(内核文本中的故障)时非常重要。

VBAR(Vector Base Address Register)

向量基址寄存器存储异常向量表的基址,每个目标及异常级别有一个,即VBAR_EL1~VBAR_EL3,该表是12位对齐的,由于地址只有48/52位宽,因此需注意若未使用指针认证则高位未使用部分必须相同(全0/1):

image-20250121192004193

注:有个类似的RVBAR_ELn,即Reset Vector Base Address Register,存储复位后从哪里开始执行,它是硬件设置的软件无法修改的物理地址,并且只有最高异常级别有一个。

机制

先说异常向量表,每个目标异常级别一张,每张表根据异常来源可分为4类,而每类又分为4部分,每部分0x80字节:

异常向量表偏移量
异常来源 同步异常 IRQ 或 vIRQ FIQ 或 vFIQ SError 或 vSError
当前异常级别,使用 SP_EL0 0x000 0x080 0x100 0x180
当前异常级别,使用 SP_ELx(x > 0) 0x200 0x280 0x300 0x380
较低异常级别,且目标级别的下一级实现为 AArch64 0x400 0x480 0x500 0x580
较低异常级别,且目标级别的下一级实现为 AArch32 0x600 0x680 0x700 0x780

值得注意的是它里面存储的并不是异常处理例程的地址,而是指令,即每部分能存32条指令,当异常发生时,根据如下伪代码跳到异常向量表对应位置去执行:

aarch64/exceptions/takeexception/AArch64.TakeException
// AArch64.TakeException()
// =======================
// Take an exception to an Exception Level using AArch64.
AArch64.TakeException(bits(2) target_el, ExceptionRecord exception, bits(64) preferred_exception_return, integer vect_offset)
    // 检查目标异常级别是否有效,且当前状态为 AArch64,且目标异常级别不低于当前异常级别(合法)
    assert HaveEL(target_el) && !ELUsingAArch32(target_el) && UInt(target_el) >= UInt(PSTATE.EL);

    from_32 = UsingAArch32(); // 检查当前是否处于 AArch32 状态
    // 如果是 AArch32 状态,清零 X[] 寄存器的高位,确保 AArch64 寄存器的正确性
    if from_32 then AArch64.MaybeZeroRegisterUppers(); 

    if UInt(target_el) > UInt(PSTATE.EL) then // 判断目标异常级别是否高于当前异常级别
        boolean lower_32;
        if target_el == EL3 then // 处理 EL3 的特殊情况
            // 如果当前是非安全状态且支持 EL2,则检查 EL2 是否使用 AArch32
            if !IsSecure() && HaveEL(EL2) then // 非安全状态下,检查 EL2 的状态
                lower_32 = ELUsingAArch32(EL2); // 如果 EL2 使用 AArch32,则标记为 true
            else
                lower_32 = ELUsingAArch32(EL1); // 如果 EL1 使用 AArch32,则标记为 true
        // 如果当前在 Host 模式,且当前异常级别是 EL0,目标异常级别是 EL2 (处理 Host 模式下 EL0 到 EL2 的异常)
        elsif IsInHost() && PSTATE.EL == EL0 && target_el == EL2 then
            lower_32 = ELUsingAArch32(EL0); // 如果 EL0 使用 AArch32,则标记为 true
        else
            lower_32 = ELUsingAArch32(target_el - 1); // 检查目标异常级别的下一级是否使用 AArch32
        // 如果下一级是 AArch32,偏移量增加 0x600,否则增加 0x400
        vect_offset = vect_offset + (if lower_32 then 0x600 else 0x400); // 
    // 如果当前使用 SP_ELx(x > 0)作为栈指针,调整异常向量表的偏移量,增加 0x200
    elsif PSTATE.SP == '1' then 
        vect_offset = vect_offset + 0x200; 

    spsr = GetPSRFromPSTATE(); // 保存当前处理器状态到 SPSR,用于异常返回时恢复状态

    // 如果支持 UAO 扩展,则清零 PSTATE.UAO,禁用用户访问覆盖功能
    if HaveUAOExt() then PSTATE.UAO = '0';

    // 如果异常类型不是 IRQ 或 FIQ,即为同步异常则报告异常
    if !(exception.type IN {Exception_IRQ, Exception_FIQ}) then
        AArch64.ReportException(exception, target_el);

    PSTATE.EL = target_el; PSTATE.nRW = '0'; PSTATE.SP = '1'; // 更新 PSTATE 的值,设置目标异常级别、状态为 AArch64、使用 SP_ELx 作为栈指针
    SPSR[] = spsr; // 将当前状态保存到 SPSR,用于异常返回时恢复
    ELR[] = preferred_exception_return; // 设置异常返回地址,通常是异常发生时的下一条指令地址
    PSTATE.SS = '0'; // 禁用单步调试功能
    PSTATE.<D,A,I,F> = '1111'; // 禁用调试、异步异常、IRQ 和 FIQ,确保异常处理期间不会被中断
    PSTATE.IL = '0'; // 清零 PSTATE.IL(指令长度),确保指令长度为正常模式

    // 如果当前是从 AArch32 状态进入异常,则清零 PSTATE.IT 和 PSTATE.T(Thumb 状态),确保 AArch64 状态正确
    if from_32 then
        PSTATE.IT = '00000000'; PSTATE.T = '0'; 

    // 如果支持 PAN 扩展,并且当前异常级别是 EL1 或 EL2(Host 模式),且 SCTLR.SPAN 为 0,则启用 PAN,限制特权模式对用户内存的访问
    if HavePANExt() && (PSTATE.EL == EL1 || (PSTATE.EL == EL2 && ELIsInHost(EL0))) && SCTLR[].SPAN == '0' then
        PSTATE.PAN = '1'; 

    // 跳转到异常向量表的指定偏移量处执行异常处理程序
    BranchTo(VBAR[]<63:11>:vect_offset<10:0>, BranchType_EXCEPTION); 

    // 如果支持 RAS 扩展,并且 SCTLR.IESB 为 1,则执行错误同步屏障,确保错误处理的正确性
    if HaveRASExt() && SCTLR[].IESB == '1' then 
        ErrorSynchronizationBarrier(MBReqDomain_FullSystem, MBReqTypes_All);
        iesb_req = TRUE;

    TakeUnmaskedPhysicalSErrorInterrupts(iesb_req); // 处理未屏蔽的物理 SError 中断
    EndOfInstruction(); // 结束当前指令的执行,准备进入异常处理程序

*OS的运行态切换

总览

xnu支持4类系统调用:

  1. unix syscall
  2. mach syscall
  3. mdep syscall
  4. diag syscall:这是x86独有的,开发调试用,忽略~
image-20250124092917723
image-20250124091912697

学习中....

参考

  1. ARM® Architecture Reference Manual ARMv8, for ARMv8-A architecture profile:D1 The AArch64 System Level Programmers’ Model
  2. 《*OS Internals Volume II:Kernel Internals》chapter 4 -- Jonathan Levin
  3. 《*OS Internals Volume I:User Space》chapter 2 -- Jonathan Levin