ARM异常处理

异常处理基本概念

在 ARMv8 体系里,中断和所有异常都属于"异常处理"。简单说:凡是打断 CPU 当前正常执行流的事件,都被统一归为"异常",需要一套专门的流程来处理。

异常的类型

ARMv8 把异常分成几大类,对应不同的触发场景和处理方式。

① 中断(异步异常的典型)

中断是最常见的异常,特点是异步触发:它的发生和 CPU 当前执行的指令无关,是外设主动"打断" CPU。分为两种:

  • IRQ(普通中断请求):优先级较低,常见外设的通用中断都走这条线。
  • FIQ(快速中断请求):优先级更高,专门给对延迟要求极高的场景(比如实时控制)使用。

工作流程:外设的中断信号先发给中断控制器(比如 GIC),由它做优先级调度,再通过 nIRQ/nFIQ 两根线发给 ARM 处理器。

② 中止(同步异常)

中止通常是访问内存出错导致的,由 MMU 捕获,分为两种:

  • 指令中止:CPU 尝试执行指令时出错(比如指令地址非法、权限不够、缺页)。
  • 数据中止:执行加载 / 存储指令访问内存时出错(比如数据地址非法、缺页、权限错误)。

③ 复位

优先级最高的异常,直接让 CPU 复位引脚收到信号,强制回到初始状态重启系统。

④ 系统调用(软件触发的同步异常)

ARMv8 提供 3 种特殊指令,让软件主动触发异常,请求更高权限的服务:

  • SVC:用户态应用请求操作系统内核的服务(比如系统调用)。
  • HVC:客户操作系统请求虚拟机监控器(Hypervisor)的服务(虚拟化场景)。
  • SMC:普通世界(Normal World)程序请求安全监控器(Secure Monitor)的服务(TrustZone 安全场景)。

异常等级

ARM64 用 4 个异常等级来区分权限,等级越高,权限越大:

等级 用途 权限说明
EL0 运行用户态应用 非特权模式,不能直接访问系统关键资源
EL1 运行操作系统内核 特权模式,管理内存、调度进程等核心功能
EL2 运行虚拟化管理程序(Hypervisor) 控制虚拟机的特权模式
EL3 运行安全世界的监控程序 管理 TrustZone 安全 / 非安全世界切换

同步异常 vs 异步异常

这是 ARMv8 里非常关键的分类,决定了异常的处理时机和方式。

① 同步异常

特点:由 CPU 当前执行的指令直接导致,和指令强绑定。

处理要求:必须处理完异常,CPU 才能继续执行后续指令。

例子:

  • 访问非法地址的指令 / 数据中止
  • 执行 SVC/HVC/SMC 系统调用指令
  • 未定义指令、权限错误、断点 / 观察点调试异常

② 异步异常

特点:触发原因和当前执行的指令无关,是外部事件(比如外设中断)主动打断 CPU。

处理要求:CPU 暂停当前指令流,先处理中断,再恢复执行。

例子:

  • 物理中断:SError(系统错误)、IRQ、FIQ
  • 虚拟中断:vSError、vIRQ、vFIQ(虚拟化场景下的虚拟中断)

异常处理与返回

异常入口

当异常触发时,CPU 会自动完成一套固定的"上下文保存 + 准备工作",这部分是硬件级的,操作系统不用写代码实现:

步骤 操作内容 作用说明
1 保存当前状态到 SPSR_ELx 把当前 PSTATE 寄存器的值,存到目标异常等级对应的 SPSR_ELx(Saved Program Status Register),异常返回时要恢复它
2 保存返回地址到 ELR_ELx 把异常发生时的 PC 值,存到目标异常等级对应的 ELR_ELx(Exception Link Register),ERET 指令会用它恢复 PC
3 关闭中断与调试异常 把 PSTATE 里的 D/A/I/F 标志位全部置 1,相当于关闭调试异常、SError、IRQ、FIQ,防止异常嵌套干扰当前处理
4 同步异常写入原因到 ESR_ELx 同步异常(如缺页、未定义指令)会把具体错误原因写入 ESR_ELx(Exception Syndrome Register),操作系统用它判断处理方式
5 切换栈指针 自动把 SP 切换到目标异常等级的专用栈 SP_ELx(比如 EL1 异常就用 SP_EL1),避免破坏被打断的上下文栈
6 跳转到异常向量表 切换到目标异常等级后,自动跳转到对应等级的异常向量表,操作系统在这里写跳转指令,转到具体的异常处理函数

简单说:CPU 帮你把"现场保护好、栈切好、路引到向量表",剩下的处理逻辑由操作系统接管。

异常返回

当操作系统处理完异常,只需要执行一条 ERET(Exception Return)指令,CPU 就会自动恢复现场:

  • 从 ELR_ELx 恢复 PC:回到被异常打断的指令地址,继续执行原来的程序。
  • 从 SPSR_ELx 恢复 PSTATE:恢复之前的状态,包括权限模式、中断标志位(I/F 位会被自动恢复,中断也随之重新打开)。

补充:中断什么时候重新打开?

CPU 在异常入口时会把 PSTATE 的 I/F 位置 1(关闭中断),而 ERET 指令会把 SPSR_ELx 里保存的旧 PSTATE 恢复回去,中断标志位会被还原成异常发生前的状态,中断就自动打开了。

异常返回地址的指向

ELR_ELx 里保存的返回地址,不是所有情况都指向"异常发生时的指令",要分情况:

异常类型 返回地址指向 例子说明
异步异常(中断) 指向第一条还没执行的指令 比如 CPU 正在执行指令 A,刚执行到一半被中断打断,ELR_ELx 会保存指令 A 的地址,返回时继续执行指令 A
非系统调用的同步异常 指向触发异常的那条指令 比如 LDR 指令访问非法地址触发缺页,异常处理里建立映射后,返回时会重新执行这条 LDR 指令,这次就能正常访问了
系统调用(SVC/HVC/SMC) 指向系统调用指令的下一条指令 比如 SVC #0 触发系统调用,异常返回时会执行 SVC 后面的指令,不会重复执行系统调用

异常处理路由

异常发生后,CPU 要决定"去哪个异常等级处理",这就是异常路由,由 SCR_EL3(EL3 配置)和 HCR_EL2(EL2 配置)寄存器控制。

① 路由的核心规则

  • 异常可以在当前等级处理,也可以路由到更高等级(比如 EL0 的中断路由到 EL1/EL2/EL3)。
  • EL0 不能处理异常,只能由更高等级接管。
  • 同步异常(如缺页)通常在当前等级处理(比如 EL1 的内核缺页,就在 EL1 处理);但虚拟化场景下,客户物理地址(GPA)缺页会路由到 EL2 由 Hypervisor 处理。

② 关键控制位解析

SCR_EL3(安全世界配置)

字段名 位号 取值 含义说明
NS Bit0 0 EL0 和 EL1 都处于安全状态(secure state)
NS Bit0 1 低于 EL3 的异常级别处于非安全状态,这些级别的内存访问指令不能访问安全内存
IRQ Bit1 0 来自低于 EL3 的异常级别的 IRQ 不会路由到 EL3
IRQ Bit1 1 来自低于 EL3 的异常级别的 IRQ 会路由到 EL3
FIQ Bit2 0 来自低于 EL3 的异常级别的 FIQ 不会路由到 EL3
FIQ Bit2 1 来自低于 EL3 的异常级别的 FIQ 会路由到 EL3
EA Bit3 0 来自低于 EL3 的异常级别的外部中止和 SError 中断不会路由到 EL3;来自 EL3 的外部中止也不会路由到 EL3,来自 EL3 的 SError 中断会路由到 EL3
EA Bit3 1 来自低于 EL3 的异常级别的外部中止和 SError 中断会路由到 EL3
RW Bit10 0 表示低于 EL3 的异常级别都在 AArch32 执行状态下
RW Bit10 1 表示低于 EL3 的异常级别都在 AArch64 执行状态下

HCR_EL2(虚拟化配置)

字段名 位号 取值 含义说明
RW Bit31 0 表示低于 EL2 的异常级别都在 AArch32 执行状态下
RW Bit31 1 表示 EL1 在 AArch64 执行状态下,而 EL0 则需要根据 PSTATE.nRW 字段来判断
TGE Bit27 0 表示对 EL0 的执行没有影响
TGE Bit27 1 如果系统实现了 EL2,那么所有原本要路由到 EL1 的异常都将路由到 EL2;如果系统没有实现 EL2,那么对 EL0 的异常没有影响。TGE 字段主要用在虚拟化主机扩展(VHE)中
AMO Bit5 0 来自低于 EL2 的异常级别的 SError 中断不会路由到 EL2
AMO Bit5 1 来自低于 EL2 的异常级别的 SError 中断会路由到 EL2;当 TGE 字段为 1 并且实现了 EL2 时,不管 AMO 字段的值是多少,都会路由到 EL2
IMO Bit4 0 来自低于 EL2 的异常级别的 IRQ 不会路由到 EL2
IMO Bit4 1 来自低于 EL2 的异常级别的 IRQ 会路由到 EL2;当 TGE 字段为 1 并且实现了 EL2 时,不管 IMO 字段的值是多少,都会路由到 EL2
FMO Bit3 0 来自低于 EL2 的异常级别的 FIQ 不会路由到 EL2
FMO Bit3 1 来自低于 EL2 的异常级别的 FIQ 会路由到 EL2;当 TGE 字段为 1 并且实现了 EL2 时,不管 FMO 字段的值是多少,都会路由到 EL2

栈的选择(SP 寄存器)

① 两个独立的栈指针

在 ARMv8 里,每个异常等级(EL0~EL3)都有两个独立的栈指针寄存器:

  • SP_EL0:用户态 / 通用栈指针
  • SP_ELx:当前异常等级的专用栈指针(比如 EL1 对应 SP_EL1,EL2 对应 SP_EL2)

② CPU 怎么决定当前用哪一个?

靠的是 SPSel 寄存器,它只有 1 个关键位:

  • SPSel = 0:所有 EL 都用 SP_EL0 作为栈指针
  • SPSel = 1:当前 EL 用自己的 SP_ELx 作为栈指针

③ t 和 h 后缀是什么意思?

  • t = Thread(线程模式栈):表示当前 EL 使用 SP_EL0 作为栈指针
  • h = Handler(处理模式栈):表示当前 EL 使用自己的 SP_ELx 作为栈指针

举个例子,对应表 11.1 来看:

异常等级 栈指针 含义说明
EL1 SP_EL1t EL1 运行时,SPSel=0,使用 SP_EL0 作为栈指针
EL1 SP_EL1h EL1 运行时,SPSel=1,使用 SP_EL1 作为栈指针
EL2 SP_EL2t EL2 运行时,SPSel=0,使用 SP_EL0 作为栈指针
EL2 SP_EL2h EL2 运行时,SPSel=1,使用 SP_EL2 作为栈指针

这是为了实现用户栈和内核栈分离,核心好处有两个:

  • 安全隔离:用户进程无法访问内核栈(SP_EL1),防止用户态程序通过栈溢出攻击内核
  • 上下文保护:中断 / 异常处理时,直接用内核专用栈,不会污染用户进程的栈数据,异常返回时用户栈还是原来的样子

重要要求:栈必须 16 字节对齐

如果栈指针不是 16 字节对齐,会触发 SP alignment fault(栈对齐错误),这是 ARMv8 的强制要求,因为 AArch64 的函数调用依赖栈的 16 字节对齐。

执行状态切换

ARMv8 支持 32 位(AArch32)和 64 位(AArch64)两种执行状态,异常处理和返回时都可以切换状态:

  • 异常入口时:HCR_EL2.RW 字段控制 EL1 的执行状态,比如设置为 1 表示 EL1 运行在 AArch64。
  • 异常返回时:SPSR_ELx.M4 字段决定返回后的执行状态:0 表示返回 AArch64 状态,1 表示返回 AArch32 状态。

比如一个 32 位的用户程序(EL0 AArch32)触发中断,内核(EL1 AArch64)处理完后,ERET 会根据 SPSR_EL0.M4 恢复到 AArch32 状态继续执行。

异常向量表

当异常(中断、同步异常、SError 等)发生时,CPU 必须立刻跳转到一段固定的地址去执行处理代码,这个跳转目标的集合,就是异常向量表。

  • ARMv7:向量表每个表项只有 4 字节,只能放一条跳转指令。
  • ARMv8:向量表每个表项扩展到 128 字节,可以放 32 条 32 位宽的指令,处理更灵活。

ARMv8 异常向量表

① 按异常等级(EL)独立

ARMv8 有 EL0/EL1/EL2/EL3,除了 EL0 之外,每个 EL 都有自己的向量表,通过 VBAR_ELx 寄存器来指定基地址:

  • VBAR_EL1:内核态 EL1 的向量表基地址
  • VBAR_EL2:虚拟化 EL2 的向量表基地址
  • VBAR_EL3:安全世界 EL3 的向量表基地址

② 表项的分类与偏移

向量表的每个表项偏移是固定的,按**异常类型 + SP 选择 + 执行状态(AArch32/AArch64)**划分:

偏移(基地址 +) 异常类型 使用的栈指针 描述
+0x000 同步 SP_EL0 使用 SP_EL0 的当前异常等级同步异常
+0x080 IRQ/vIRQ SP_EL0 使用 SP_EL0 的当前异常等级中断
+0x100 FIQ/vFIQ SP_EL0 使用 SP_EL0 的当前异常等级快速中断
+0x180 SError/vSError SP_EL0 使用 SP_EL0 的当前异常等级系统错误
+0x200 同步 SP_ELx 使用 SP_ELx 的当前异常等级同步异常
+0x280 IRQ/vIRQ SP_ELx 使用 SP_ELx 的当前异常等级中断
+0x300 FIQ/vFIQ SP_ELx 使用 SP_ELx 的当前异常等级快速中断
+0x380 SError/vSError SP_ELx 使用 SP_ELx 的当前异常等级系统错误
+0x400 同步 - AArch64 低异常等级(如 EL0)同步异常
+0x480 IRQ/vIRQ - AArch64 低异常等级中断
+0x500 FIQ/vFIQ - AArch64 低异常等级快速中断
+0x580 SError/vSError - AArch64 低异常等级系统错误
+0x600 同步 - AArch32 低异常等级同步异常
+0x680 IRQ/vIRQ - AArch32 低异常等级中断
+0x700 FIQ/vFIQ - AArch32 低异常等级快速中断
+0x780 SError/vSError - AArch32 低异常等级系统错误

每个表项固定 128 字节,对应偏移差 0x80。向量表整体大小:16 个表项 × 128 字节 = 2048 字节(2KB),所以基地址必须 2KB 对齐。

CPU 跳转对应的向量

CPU 会根据三个关键信息,自动计算跳转地址:

  • 当前异常等级(ELx),对应 VBAR_ELx 作为基地址
  • 异常的类型(同步 / IRQ/FIQ/SError)
  • 异常发生时使用的栈指针(SP_EL0 还是 SP_ELx),以及被中断代码的执行状态(AArch32/AArch64)

举个例子:

  • 内核态(EL1)触发 IRQ,且使用 SP_EL1:跳转地址是 VBAR_EL1 + 0x280
  • 用户态(EL0,AArch64)触发 IRQ,陷入 EL1:跳转地址是 VBAR_EL1 + 0x480
  • 用户态(EL0,AArch32)触发 IRQ,陷入 EL1:跳转地址是 VBAR_EL1 + 0x680

VBAR_ELx 寄存器

VBAR_ELx(Vector Base Address Register)是设置向量表基地址的核心寄存器:

  • 寄存器结构 :
    • Bit63:11:向量表基地址(要求 2KB 对齐,所以低 11 位必须为 0)
    • Bit10:0:保留位,必须为 0
  • 作用:替代了早期 ARM 固定在 0x0 的限制,允许操作系统把向量表放在任意内存位置,提高灵活性。

Linux 5.0 内核的异常向量表

Linux 内核在 arch/arm64/kernel/entry.S 中定义了向量表,和 ARMv8 架构完全对应。

① 向量表的整体定义

asm 复制代码
ENTRY(vectors)
    // EL1t(使用 SP_EL0 的 EL1)模式的异常向量
    kernel_ventry    1, sync_invalid      // 同步异常
    kernel_ventry    1, irq_invalid       // IRQ
    kernel_ventry    1, fiq_invalid       // FIQ
    kernel_ventry    1, error_invalid     // SError

    // EL1h(使用 SP_EL1 的 EL1)模式的异常向量
    kernel_ventry    1, sync              // 同步异常
    kernel_ventry    1, irq               // IRQ
    kernel_ventry    1, fiq_invalid       // FIQ
    kernel_ventry    1, error             // SError

    // AArch64 EL0 模式的异常向量
    kernel_ventry    0, sync              // 同步异常
    kernel_ventry    0, irq               // IRQ
    kernel_ventry    0, fiq_invalid       // FIQ
    kernel_ventry    0, error             // SError

    // AArch32 EL0 模式的异常向量
    kernel_ventry    0, sync_compat, 32   // 同步异常
    kernel_ventry    0, irq_compat, 32    // IRQ
    kernel_ventry    0, fiq_invalid_compat, 32 // FIQ
    kernel_ventry    0, error_compat, 32  // SError
END(vectors)

.align 11:保证向量表整体 2KB(2^11 字节)对齐,满足 VBAR_ELx 的要求。kernel_ventry 是一个宏,用来生成每个表项的代码。

② kernel_ventry 宏的实现

asm 复制代码
.macro kernel_ventry, el, label, regsize = 64
    .align 7          // 每个表项 128 字节(2^7 字节)对齐
    sub sp, sp, #S_FRAME_SIZE  // 分配栈帧,保存现场
    b    el\()\el()_\label      // 跳转到具体的异常处理函数
.endm
  • .align 7:保证每个表项 128 字节对齐,符合 ARMv8 要求。
  • sub sp, sp, #S_FRAME_SIZE:在栈上分配 pt_regs 结构体大小的空间,保存异常发生时的寄存器现场。
  • b el\()\el()_\label:跳转到具体的异常处理函数,例如 el1_irq、el0_sync 等。

这里的 () 和 \ 是 GNU 汇编宏的语法,用来处理参数拼接:

  • \el 引用宏参数 el,\label 引用宏参数 label
  • el\()\el()_\label 会展开成 el1_irq 这样的符号名

异常现场

异常发生时,CPU 会打断当前正在执行的程序,跳转到异常处理函数。为了让异常处理完之后,程序能完全回到被打断的那一刻继续运行,必须把当时所有关键的状态信息保存下来,这就叫保存异常现场。

如果不保存,异常处理过程中修改了寄存器的值,等程序回去的时候,原来的变量、地址、标志位全乱了,程序就崩了。

ARM64 保存现场

① PSTATE 寄存器的值

  • 是什么 :ARM64 的程序状态寄存器,里面存着 CPU 的各种状态标志:
    • 条件标志位(N/Z/C/V):比如加减运算的正负、是否溢出
    • 异常屏蔽位(DAIF):中断是否被屏蔽
    • 当前异常等级、执行状态(AArch64/AArch32)
  • 为什么要保存:异常处理函数可能会修改这些标志位(比如执行新的加减指令),如果不保存恢复,回到原程序时,条件判断、中断屏蔽状态就都错了。

② PC 值(程序计数器)

  • 是什么:PC 指向下一条要执行的指令地址,异常发生时,PC 的值就是"异常返回后要接着执行的那条指令的地址"。
  • 为什么要保存:不保存的话,异常处理完 CPU 不知道该跳回哪里,程序就跑飞了。ARMv8 里,异常发生时 CPU 会自动把返回地址放到 ELR_ELx 寄存器里,我们要做的就是把 ELR_ELx 的值存到栈里。

③ SP 值(栈指针)

  • 是什么:SP 指向当前栈的栈顶,记录了程序自己的栈使用情况。
  • 为什么要保存:异常处理会用内核栈,会修改 SP。如果不保存原程序的 SP,回去之后栈就乱了,局部变量、函数调用栈全错。

④ X0 ~ X30 通用寄存器的值

  • 是什么:ARM64 有 31 个通用寄存器 X0~X30,程序里的变量、函数参数、返回值、临时计算结果都存在这里。
  • 为什么要保存:异常处理函数里会用到这些寄存器,如果不保存,原程序里存在寄存器里的数据就会被覆盖,等程序回去的时候,变量值全变了,逻辑直接出错。

现场保存位置

保存在进程的内核态栈空间里。每个进程都有自己的内核栈,大小一般几 KB 到十几 KB。异常发生时,CPU 会自动切换到内核栈(比如用户态 EL0 触发异常,会切到 EL1 的内核栈)。

我们就在这个内核栈上,分配一块固定大小的空间,把上面说的所有寄存器都存进去,这块空间在 Linux 里叫 struct pt_regs。

Linux 里的示例

你之前看到的 S_FRAME_SIZE,就是这个结构体的大小,也就是我们说的"寄存器框架大小"。简化版的结构大概长这样:

c 复制代码
struct pt_regs {
    // 通用寄存器
    u64 regs[31];  // X0 ~ X30
    u64 sp;        // SP 值
    u64 pc;        // PC(实际来自 ELR_EL1)
    u64 pstate;    // PSTATE(实际来自 SPSR_EL1)
    // 还有其他一些异常相关的寄存器,比如 FAR/ESR 等
};

异常处理的入口 kernel_ventry 宏里的这句:

asm 复制代码
sub sp, sp, #S_FRAME_SIZE

就是在内核栈上分配 pt_regs 结构体的空间,为后面保存所有寄存器做准备。

同步异常的解析

CPU 怎么告诉我们"出了什么问题"?当同步异常(比如访问非法内存、未定义指令、系统调用)发生时,CPU 会自动把这次异常的"详细情况"记录在**异常综合信息寄存器(ESR_ELx)**里。操作系统处理异常时,首先要读这个寄存器,才能知道:

  • 到底是哪种异常?
  • 是指令访问出了问题,还是数据访问?
  • 是页表翻译失败,还是权限不够?
  • 出问题的指令是 32 位还是 64 位?

ESR_ELx 寄存器结构

寄存器是 64 位,分成 4 个关键部分:

字段 位宽 位置 作用
保留位 32 位 Bit63:32 保留给未来扩展,软件处理时直接忽略
EC(Exception Class,异常类) 6 位 Bit31:26 核心!标记异常的大类型,决定怎么解析 ISS
IL(Instruction Length,指令长度) 1 位 Bit25 标记触发异常的指令是 16 位 / 32 位(Thumb/A32)还是 32 位(AArch64)
ISS(Instruction Specific Syndrome,指令特定信息) 25 位 Bit24:0 具体的异常细节,格式完全由 EC 字段决定

① EC 字段:异常的"大类"

EC 是 6 位,所以最多支持 64 种异常类型,这里列出一部分关键的:

  • 0x15:AArch64 下 SVC 指令触发的系统调用
  • 0x24/0x25:数据访问异常(低等级 / 当前等级),比如用户态访问内核地址、页表没命中
  • 0x20/0x21:指令访问异常(低等级 / 当前等级),比如执行非法地址的指令
  • 0x3C:AArch64 下 BRK 断点指令触发的调试异常
  • 0x0E:非法执行状态(比如从 EL0 执行特权指令)

EC 字段的核心作用:告诉软件"这是哪一类异常",然后软件才能知道怎么解析后面的 ISS 字段。比如:

  • EC=0x15 → 这是系统调用,ISS 里存着系统调用号
  • EC=0x24 → 这是用户态数据访问错误,ISS 里存着错误原因(权限 / 页表 / 对齐等)

② IL 字段:指令长度标记

  • IL=0:触发异常的指令是 16 位或 32 位(AArch32/Thumb 模式)
  • IL=1:触发异常的指令是 32 位(AArch64 模式)

这个信息会影响 PC 的计算,比如异常返回地址 ELR_ELx 的调整。

③ ISS 字段:异常的"细节"

ISS 是 25 位,格式完全由 EC 字段决定,不同异常类型的 ISS 格式完全不同。

数据访问异常的 ISS 字段详解

当 EC=0x24/0x25(数据访问异常)时,ISS 格式如下,关键字段:

字段 位位置 作用
DFSC Bit5:0 数据错误状态码,核心标记具体错误原因
WnR Bit6 0 = 读操作出错,1 = 写操作出错
S1PTW Bit7 0 = 正常访问出错,1 = 页表转换过程中出错
CM Bit8 0 = 普通访问,1 = 缓存维护 / 地址转换指令出错
EA Bit9 标记 FAR_ELx 寄存器里的地址是否有效
FnV Bit10 0=FAR 有效,1=FAR 无效
SF Bit15 标记访问的寄存器宽度:0=32 位,1=64 位
SAS Bit23:22 访问大小:字节 / 半字 / 字 / 双字

核心:DFSC 字段(数据错误状态码)

DFSC 是 6 位,能表示 64 种错误,教材里列了常见的几类:

  • 页表翻译错误 :比如 0b0000100(L1 页表翻译错误)、0b0000101(L2 页表翻译错误),说明 MMU 在查页表时没找到对应映射,也就是我们常说的"缺页异常"。
  • 访问权限错误 :比如 0b0001100(L1 页表访问权限错误),说明访问了没有权限的内存(比如用户态访问内核地址、写只读内存)。
  • 地址对齐错误 :比如 0b1000001,说明访问的地址不符合对齐要求(比如 4 字节访问地址不是 4 的倍数)。
  • 地址大小错误 :比如 0b0000000,说明访问的地址超出了当前地址空间的范围。

OS 处理同步异常的完整流程

  • 异常发生 :CPU 触发同步异常,跳转到向量表对应入口(比如 VBAR_EL1 + 0x400)。
  • 保存现场 :把所有寄存器保存到内核栈的 pt_regs 结构里。
  • 读取关键寄存器 :
    • ESR_EL1:获取异常类型和 ISS 信息
    • FAR_EL1:获取出错的虚拟地址
    • ELR_EL1:获取异常发生时的 PC
  • 解析异常 :读 ESR_EL1 的 EC 字段,判断是哪种异常,再根据 EC 解析 ISS 字段,得到具体错误原因。
  • 处理异常 :
    • 如果是系统调用(EC=0x15):从 ISS 里取系统调用号,执行对应的内核函数
    • 如果是缺页异常(EC=0x24,DFSC = 页表错误):调用页错误处理函数,分配物理内存并建立映射
    • 如果是权限错误 / 非法地址:向进程发送 SIGSEGV 信号,终止进程
  • 恢复现场 :处理完后恢复寄存器,跳回 ELR_EL1 继续执行原程序。

实战例子:用户态访问非法内存

用户程序执行 ldr x0, [x1],x1 指向内核地址 0xffff0000。

  • CPU 触发数据访问异常,陷入 EL1,跳转到 VBAR_EL1 + 0x400。
  • 内核读取 ESR_EL1:
    • EC=0x24(低等级数据访问异常)
    • WnR=0(读操作)
    • DFSC=0b0001100(L1 页表访问权限错误)
  • 内核再读 FAR_EL1,得到出错地址 0xffff0000。
  • 内核判断:用户态进程试图访问内核地址,权限不足,发送 SIGSEGV 信号给进程,进程崩溃。
相关推荐
武汉万象奥科2 小时前
T153芯片解析:ARM+RISC-V异构架构,集成三路千兆网
arm开发·嵌入式硬件·物联网·arm核心板
黑马水牛13 小时前
RK3588系列:01-系统烧录与开发环境搭建
arm开发·嵌入式硬件
像枫一样1 天前
STM32串口轮询收发实战教程
arm开发·c++·stm32·单片机·嵌入式硬件·fpga开发·51单片机
晚枫歌F1 天前
dpdk实现kni
arm开发
大叔学Linux2 天前
FreeRTOS任务切换笔记
arm开发·笔记
Chery11402 天前
DPE5500 SPI以太网协议栈芯片详解:10/100M以太网、硬件TCP/IP与国产替代方案
arm开发·网络协议·tcp/ip
a5986522 天前
原生ARM云手机实测|ARM服务器底座,主流手游兼容性、闪退、帧率专项测试
服务器·arm开发·智能手机·云手机
a5986522 天前
工作室批量多开云手机|ARM 服务器方案成本解析、架构选型与踩坑总结
服务器·arm开发·智能手机·云手机
a5986523 天前
无公网IP,ARM服务器搭建云手机远程访问方案
服务器·arm开发·tcp/ip·云手机