2. ARM64 异常向量表:基址、布局与入口区分

ARM64 架构的异常处理机制是内核异常现场恢复的基石。当 CPU 检测到异常(如中断、缺页、系统调用)时,硬件会自动跳转到预定义的异常向量入口执行。理解异常向量表的基址寄存器 VBAR_EL1、向量表的物理布局以及同步/异步异常的入口区分,是分析异常现场的第一步。

2.1 异常向量表基址:VBAR_EL1

VBAR_EL1(Vector Base Address Register, EL1)是 ARM64 架构中用于存放异常向量表基地址的系统寄存器。该寄存器保存了异常向量表在物理内存或虚拟内存中的起始地址,且要求地址必须 2KB 对齐(即低 11 位为 0)。

  • 作用:CPU 在发生异常时,根据异常类型和来源,从 VBAR_EL1 指向的基址加上固定偏移量,计算出异常处理入口地址。
  • 初始化时机:Linux 内核在启动早期(head.S)中设置 VBAR_EL1,指向内核预定义的异常向量表 __entry_tramp_text_start 或 vectors 符号。
  • 多级异常级别:EL2(虚拟化)、EL3(安全监控)也有各自的 VBAR_EL2、VBAR_EL3,但内核最常用的是 VBAR_EL1。
调试要点:在 kdump/crash 工具中,通过 rdmsr VBAR_EL1 或读取 sys_regs 结构体,可以确认异常向量表是否被篡改或损坏,这是排查异常入口异常的关键步骤。

2.2 异常向量表布局:2KB 对齐的 16 个入口

ARM64 异常向量表固定大小为 2KB,共包含 16 个异常向量入口,每个入口占用 128 字节(32 条指令)。这 16 个入口按照异常类型(同步/异步)和异常来源(当前 EL 使用 SP0/SPx、低异常级别 AArch64/AArch32)进行排列。

偏移量(十六进制) 异常类型 异常来源描述 典型触发场景
0x000 同步异常 当前 EL,使用 SP_EL0 EL0 下的系统调用(SVC)
0x080 IRQ(异步) 当前 EL,使用 SP_EL0 EL0 下的普通中断
0x100 FIQ(异步) 当前 EL,使用 SP_EL0 快速中断(通常未使用)
0x180 SError(异步) 当前 EL,使用 SP_EL0 内存访问错误、总线错误
0x200 同步异常 当前 EL,使用 SP_ELx EL1 内核态下的缺页、数据中止
0x280 IRQ(异步) 当前 EL,使用 SP_ELx EL1 内核态下的中断
0x300 FIQ(异步) 当前 EL,使用 SP_ELx EL1 快速中断
0x380 SError(异步) 当前 EL,使用 SP_ELx EL1 系统错误
0x400 同步异常 低异常级别 AArch64 EL0 64 位应用触发同步异常
0x480 IRQ(异步) 低异常级别 AArch64 EL0 64 位应用被中断
0x500 FIQ(异步) 低异常级别 AArch64 EL0 64 位快速中断
0x580 SError(异步) 低异常级别 AArch64 EL0 64 位系统错误
0x600 同步异常 低异常级别 AArch32 EL0 32 位应用触发同步异常
0x680 IRQ(异步) 低异常级别 AArch32 EL0 32 位应用被中断
0x700 FIQ(异步) 低异常级别 AArch32 EL0 32 位快速中断
0x780 SError(异步) 低异常级别 AArch32 EL0 32 位系统错误
记忆技巧:偏移量每 0x80(128 字节)一个入口,前 4 个(0x000-0x180)对应“当前 EL + SP_EL0”,接着 4 个(0x200-0x380)对应“当前 EL + SP_ELx”,再 4 个(0x400-0x580)对应“低 EL AArch64”,最后 4 个(0x600-0x780)对应“低 EL AArch32”。

2.3 同步异常与异步异常的入口区分

在 ARM64 异常向量表中,同步异常(Synchronous)和异步异常(Asynchronous)的入口通过 偏移量最低 7 位 进行区分:

  • 同步异常:偏移量低 7 位为 0x00(即偏移量 0x000、0x200、0x400、0x600)。
  • IRQ(异步):偏移量低 7 位为 0x80(即偏移量 0x080、0x280、0x480、0x680)。
  • FIQ(异步):偏移量低 7 位为 0x100(即偏移量 0x100、0x300、0x500、0x700)。
  • SError(异步):偏移量低 7 位为 0x180(即偏移量 0x180、0x380、0x580、0x780)。

这种设计使得 CPU 硬件可以通过异常类型和当前异常级别快速计算入口地址,无需复杂的查表操作。内核在 entry.S 中为每个入口编写了对应的处理存根(stub),这些存根负责保存现场、识别具体异常原因并分发到高层处理函数。

常见误区:同步异常并不等同于“错误”,系统调用(SVC)也是一种同步异常。异步异常(IRQ/FIQ/SError)则完全由外部事件或硬件错误触发,与当前指令流无关。在寄存器现场恢复时,同步异常需要更谨慎地处理 PC 和 PSTATE,因为异常指令可能未完成执行。

2.4 异常向量表结构 SVG 图

下图展示了 ARM64 异常向量表的完整布局,包括 16 个入口的偏移量、异常类型以及来源分组关系。

ARM64 异常向量表布局 ARM64 异常向量表布局 (VBAR_EL1 + 偏移) 分组 A: 当前 EL, SP_EL0 分组 B: 当前 EL, SP_ELx 分组 C: 低 EL AArch64 分组 D: 低 EL AArch32 0x000 同步异常 SVC, 数据中止等 0x080 IRQ 普通中断 0x100 FIQ 快速中断 0x180 SError 系统错误 0x200 同步异常 内核态缺页 0x280 IRQ 内核态中断 0x300 FIQ 内核态快速中断 0x380 SError 内核态系统错误 0x400 同步异常 EL0 64位同步 0x480 IRQ EL0 64位中断 0x500 FIQ EL0 64位快速中断 0x580 SError EL0 64位系统错误 0x600 同步异常 EL0 32位同步 0x680 IRQ EL0 32位中断 0x700 FIQ EL0 32位快速中断 0x780 SError EL0 32位系统错误 同步异常 IRQ FIQ SError 每个入口 128 字节(0x80 偏移),共 16 个入口,总大小 2KB。VBAR_EL1 必须 2KB 对齐。 同步异常入口偏移低 7 位为 0x00,IRQ 为 0x80,FIQ 为 0x100,SError 为 0x180。

2.5 内核代码中的向量表定义

在 Linux 内核源码 arch/arm64/kernel/entry.S 中,异常向量表通过 .align 11(2KB 对齐)和 __entry_vectors 标签定义。以下是简化后的向量表结构:

    .align 11
ENTRY(__entry_vectors)
    // 当前 EL, SP_EL0
    ventry  el1_sync_invalid        // 0x000 同步异常
    ventry  el1_irq_invalid         // 0x080 IRQ
    ventry  el1_fiq_invalid         // 0x100 FIQ
    ventry  el1_error_invalid       // 0x180 SError

    // 当前 EL, SP_ELx
    ventry  el1_sync                // 0x200 同步异常
    ventry  el1_irq                 // 0x280 IRQ
    ventry  el1_fiq_invalid         // 0x300 FIQ
    ventry  el1_error_invalid       // 0x380 SError

    // 低异常级别 AArch64
    ventry  el0_sync                // 0x400 同步异常
    ventry  el0_irq                 // 0x480 IRQ
    ventry  el0_fiq_invalid         // 0x500 FIQ
    ventry  el0_error_invalid       // 0x580 SError

    // 低异常级别 AArch32
    ventry  el0_sync_compat         // 0x600 同步异常
    ventry  el0_irq_compat          // 0x680 IRQ
    ventry  el0_fiq_invalid         // 0x700 FIQ
    ventry  el0_error_invalid       // 0x780 SError
END(__entry_vectors)

其中 ventry 是一个宏,用于生成跳转指令(b 或 br),跳转到对应的异常处理存根。每个存根会执行以下操作:

  1. 分配栈帧(sub sp, sp, #S_FRAME_SIZE)。
  2. 保存通用寄存器(stp x0, x1, [sp, #S_X0] 等)。
  3. 保存异常原因寄存器 ESR_EL1 和返回地址 ELR_EL1。
  4. 调用 C 语言异常处理函数(如 do_mem_abort、el0_svc_handler)。
现场恢复关键点:在异常处理返回前,内核会从栈帧中恢复所有寄存器,包括 ELR_EL1(返回地址)和 SPSR_EL1(保存的 PSTATE)。如果这些值被破坏,将导致无法正确返回到异常发生前的执行流,从而引发内核崩溃。

2.6 同步与异步异常的处理差异

特性 同步异常 异步异常
触发源 当前指令执行直接导致(如 SVC、数据中止、未定义指令) 外部事件或硬件错误(如外设中断、总线错误)
返回地址 (ELR_EL1) 指向触发异常的指令(需重试或修正) 指向下一条未执行的指令(可继续执行)
可重入性 通常不可重入(同一异常级别下) 可嵌套(通过中断优先级和掩码控制)
现场保存要求 必须保存完整现场,包括 ESR_EL1 异常原因 需保存现场,但异常原因寄存器可能不关键
典型处理流程 识别异常原因 → 修复或终止 → 返回到异常指令 保存现场 → 处理中断 → 返回到下一条指令

在寄存器现场恢复时,同步异常需要特别关注 ELR_EL1 是否指向正确的指令地址,以及是否需要修改 ESR_EL1 中的异常原因。异步异常则更关注中断控制器状态和嵌套中断的栈管理。

实战案例:在分析内核 panic 时,如果 ESR_EL1 值为 0x96000021(数据中止,异常级别 EL1,写操作),且 ELR_EL1 指向内核代码段,则说明是内核态访问非法地址导致的同步异常。此时需要检查 FAR_EL1(错误地址寄存器)来定位具体访问了哪个地址。