异常处理基本概念
在 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引用宏参数 labelel\()\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 信号给进程,进程崩溃。