☰
ARMv8-A异常处理机制详解:异常级别、向量表与ESR调试实战
2026/10/13 5:51:44 网站建设 项目流程

1. 异常模型与ARMv8-A的“分工协议”

1.1 异常级别(EL):CPU世界里最基础的分层

做ARMv8-A底层开发的人,第一个绕不开的概念就是Exception Level(异常级别)。ARMv8-A定义了四个异常级别:EL0(用户态)、EL1(操作系统内核态)、EL2(虚拟机监控器Hypervisor)、EL3(安全监控器Secure Monitor)。这个体系不是拍脑袋设计出来的,它解决的是“谁授权谁运行”的问题——EL0最低,权限最小;EL3最高,掌握全局控制权。

我在实际项目中接触过不少从单片机转过来的开发者,他们往往会问:以前Cortex-M系列也有异常优先级,为什么到了A系列就变成了EL?其实这两套模型的逻辑完全不同。Cortex-M的NVIC中断控制器是以“中断优先级”为核心的调度系统;而ARMv8-A的异常模型更强调“异常资源的归属权”——EL决定了异常向量表存在哪、异常处理代码在哪个特权级跑、哪些寄存器能访问。一个直观的理解方式:EL级别就是CPU世界里的“房间权限”,你在哪一级,就只能开哪一级的门。

  • EL0:跑应用程序,访问受限的虚拟地址空间,无法操作系统寄存器
  • EL1:跑操作系统内核,能操作大部分系统寄存器,具备MMU和中断控制能力
  • EL2:跑Hypervisor,负责虚拟机的隔离管理,只有虚拟化方案里才需要
  • EL3:跑Secure Monitor,切换安全世界和普通世界,同时控制系统级安全配置

正常跑Linux的嵌入式设备,主要活跃在EL0和EL1;如果你在调试TrustZone或Secure Boot相关的内容,就会碰EL3;做虚拟化方案(比如把多个系统跑在同一块板子上)则会用EL2。理解了这些级别定义,后面所有的异常路由、屏蔽和现场保存机制都会顺理成章。

1.2 异常类型:同步与异步的本质区别

ARMv8-A把异常分成两大类:同步异常(Synchronous Exception)和异步异常(Asynchronous Exception)。很多资料只列一堆名词,但真正理解两者的区别,对后续排查问题非常重要。

同步异常是指令执行本身触发的错误,包括未定义指令、SVC/HVC/SMC指令调用、MMU访问违例(Data Abort、Instruction Abort)、PC对齐错误等。这类异常的共同特点是:异常的来源跟当前正在执行的指令直接相关,并且异常处理完成后,返回地址(ELR_ELx)指向的是一条明确的指令。因此排查同步异常时,你几乎总能通过ELR精确锁定出错位置。

异步异常就是外部触发的,主要指IRQ(普通中断)、FIQ(快速中断)和SError(系统错误,比如总线错误或外部设备的错误响应)。这类异常跟当前指令流水线没有精确关联,可能在任意执行点上被触发。异步异常处理过程中的“现场恢复”必须特别仔细,因为你不知道CPU被打断时正在执行什么,所有通用寄存器都需要保存和恢复,否则任务现场就丢了。

顺带说一个冷门但常见的坑:很多开发者以为SError就是“外部中止”的专属异常,但其实SError的类型相当杂,包括异步外部中止、来自某些系统寄存器的错误报告、甚至边界上的奇偶校验错误都可能走SError。调试的时候需要结合ESR和系统寄存器综合判断。

1.3 异常进入与返回:硬件固定的协作流程

异常处理不是纯软件逻辑,ARMv8-A在异常进入时由硬件完成一组固定的操作,把这些操作记牢比背寄存器名字更重要:

异常进入时自动完成的操作包括:

  1. 把当前PSTATE(处理器状态)保存到目标异常等级的SPSR_ELx
  2. 把当前PC值保存到目标异常等级的ELR_ELx
  3. 根据目标异常等级重新设置PSTATE,包括屏蔽异常(DAIF置位)、切换异常等级、切换栈指针选择位
  4. 跳转到目标异常等级向量表中的对应入口

异常返回则通过ERET指令完成:ERET会从SPSR_ELx恢复PSTATE,从ELR_ELx恢复PC。整个过程不需要软件手动读写这些寄存器,这也是ARMv8-A相对老ARM架构的一个大改进。老架构里返回异常时要手动读LR、手动恢复PSR,而新架构把这些统一成了硬件状态机。

关键点在于:异常进入和返回到达的“目标异常等级”是由路由控制寄存器决定的。如果异常路由到EL1,硬件就把当前状态保存进SPSR_EL1和ELR_EL1;如果路由到EL2或EL3,则对应保存到SPSR_EL2/SPSR_EL3和ELR_EL2/ELR_EL3。这就是我常讲的“异常资源归属权”——谁处理异常,硬件状态就保存在谁的寄存器里。

2. 异常向量表:16个入口背后的布局哲理

2.1 为什么是16个入口而不是一个入口

ARMv8-A规定每一个异常等级都有一张独立的向量表,向量表的基地址由VBAR_ELx寄存器指定(例如VBAR_EL1是EL1的向量表基地址)。向量表里一共有16个入口,每个入口间隔0x80(128字节)对齐,16个入口总共占2KB空间。

有些开发者第一次看ARM文档时会被16个入口吓到,觉得入口太多。其实16个入口是由四组异常的“二维矩阵”组合出来的。

四组分别是:

  • 异常发生在当前EL,且SP使用的是SP_EL0
  • 异常发生在当前EL,且SP使用的是SP_ELx(x为当前EL)
  • 异常来自低EL,且低EL运行在AArch64状态
  • 异常来自低EL,且低EL运行在AArch32状态

每一组之下再按同步异常、IRQ、FIQ、SError的顺序排列四个入口,就得到4×4=16个入口。

这个设计让每个入口天然携带了异常发生的上下文信息——CPU 一跳到对应入口,你立刻就能知道“异常是来自高一级还是低一级”“当前栈指针用的是哪一套”,不用再去读取额外状态分析。做过内核调试的人都知道,异常处理里最磨人的就是上下文判断,ARM把这一步固化在硬件里,省掉了大量软件开销。

向量表布局示意:

偏移当前EL + SP_EL0当前EL + SP_ELx低EL(AArch64)低EL(AArch32)
0x000同步异常同步异常同步异常同步异常
0x080IRQIRQIRQIRQ
0x100FIQFIQFIQFIQ
0x180SErrorSErrorSErrorSError

2.2 向量表汇编实现:布局与对齐的实战细节

实际写向量表的时候,最容易出问题的是对齐和空间预留。每个入口虽然是128字节,但如果你在入口处直接嵌入大段处理逻辑,会被后续入口的边界约束卡死。我建议每个入口只放一条跳转分支,跳到真正用于保存现场和处理的代码区。

这是一个我在项目X里反复验证过的简化模板:

.macro vector_entry label .align 7 \label: b \label\()_handler .endm .section .vectors, "ax" .align 12 .globl exception_vector_table exception_vector_table: // 当前EL,SP_EL0 vector_entry el0_sp0_sync vector_entry el0_sp0_irq vector_entry el0_sp0_fiq vector_entry el0_sp0_serror // 当前EL,SP_ELx vector_entry elx_spx_sync vector_entry elx_spx_irq vector_entry elx_spx_fiq vector_entry elx_spx_serror // 低EL,AArch64 vector_entry el_lower_a64_sync vector_entry el_lower_a64_irq vector_entry el_lower_a64_fiq vector_entry el_lower_a64_serror // 低EL,AArch32 vector_entry el_lower_a32_sync vector_entry el_lower_a32_irq vector_entry el_lower_a32_fiq vector_entry el_lower_a32_serror

几个细节值得格外强调:

  1. .align 7是因为每个入口0x80即128字节对齐,128=2^7
  2. .align 12是最关键的,整个向量表要求2KB对齐(2KB=2^11,但这条指令写12是因为部分汇编器把.align解释为字节幂的兼容性问题,实际以你工具链的手册为准),否则CPU取向量表时低位地址不匹配,直接跳错入口
  3. 每个入口的处理器必须是独立标签,因为异常处理代码需要知道自己是从哪个入口跳进来的,这是排查问题的第一条线索

我见过某位开发者把向量表定义到了普通的.text段,结果链接器把它跟其他代码混排在非2KB边界上,导致中断一进来就跳到莫名其妙的位置。这个问题极难排查,因为它不是逻辑错,是物理布局错。最终靠读取VBAR_EL1和手工对照反汇编才定位到。

2.3 VBAR_ELx 设置:什么时候该设置,什么时候不该设置

这里有个新手容易懵的地方:向量表基地址寄存器VBAR_ELx,在EL1、EL2、EL3各级都有独立的版本。你需要处理哪一个异常级别的异常,就得在那一级把VBAR设好。实际项目中,我们通常在启动早期(关闭MMU或刚打开MMU时)就完成设置。

设置VBAR_EL1的典型代码:

adrp x0, exception_vector_table add x0, x0, :lo12:exception_vector_table msr vbar_el1, x0 isb

注意那个ISB指令。MSR写完系统寄存器后,要立刻执行ISB(指令同步屏障),保证后续指令能使用新的向量表设置。如果漏了ISB,极端情况下CPU可能还在按旧的VBAR取值,一旦此刻来中断,直接跳飞。我在几个项目里都吃过ISB的亏,后来养成了写任何系统控制寄存器后都顺手加ISB的习惯。

3. 异常路由控制:谁来接管这个异常

3.1 SCR_EL3、HCR_EL2 与路由决策链

不是操作系统说“这个中断归我管”,异常就会乖乖去EL1。ARMv8-A规定了非常明确的路由决策链:高一级的软件可以决定哪些异常留在本级处理,哪些路由给低一级处理。这个决策由两个寄存器核心控制:

  • SCR_EL3(Secure Configuration Register)控制EL2及以下的异常路由:IRQ、FIQ、SError分别有对应的控制位,例如SCR_EL3.IRO位为1时,IRQ路由到EL3;为0时,IRQ停留在低级别
  • HCR_EL2(Hypervisor Configuration Register)控制EL1及以下的异常路由:例如HCR_EL2.IMO(IRQ路由)、HCR_EL2.FMO(FIQ路由)、HCR_EL2.AMO(SError路由)

这些路由控制位不是靠“中断号”来区分,而是靠异常类别来区分。也就是说,你可以让所有IRQ都去EL2处理,也可以让一部分IRQ去EL1处理——但路由位本身是全局的,做不到按中断号细分路由。如果真有按中断号细分路由的需求,通常得靠软件在代理层做二次分发。

举个例子:如果你在EL3跑一个可信固件,EL1跑一个普通操作系统,你希望操作系统产生的SError由EL3接管用于错误记录,同时IRQ仍然由EL1直接处理。那你需要把SCR_EL3的SError控制位置1,IRQ控制位置0。这样硬件就会自动把SError带到EL3,IRQ还是留在EL1。整个过程是硬件完成的,不需要软件在EL1和EL3之间来回切换上下文。

3.2 中断屏蔽位 DAIF:掩码与使能的微妙平衡

PSTATE里的DAIF位组是异常屏蔽的核心开关。每位屏蔽一类异步异常:

  • D:Debug exception
  • A:SError
  • I:IRQ
  • F:FIQ

异常进入时硬件会自动把这些屏蔽位置位,防止处理过程中再次被打断。不过这带来一个常见的操作问题:很多内核在恢复上下文时会先恢复寄存器,再恢复DAIF,假如中断在恢复寄存器后被打开,而整个异常现场尚未完全恢复,就会引发嵌套异常处理崩溃。

我踩过这类坑:在某次实现调度器切换时,我恢复完x0到x30后直接执行了ERET,没有显式管理DAIF。结果在退出中断处理的过程中,高优先级中断插入,现场瞬间错乱。后来我改成先恢复DAIF(使能中断)前,确保现场保存完整,并在ERET前设置好栈指针,问题才稳定下来。这中间的经验是:DAIF的恢复时机不是简单的“先恢复后ERET”,而是“保证嵌套安全性之后再使能嵌套”。

3.3 往低级别路由的例外情况:为什么虚拟化会依赖EL2

上面的路由体系可以理解为“上层有权决定下层事务”。但实际系统里还有一类常见的需求:EL1希望某些异常被EL2处理。比如虚拟化环境里,Guest OS(EL1)触发一个SMC(Secure Monitor Call),EL1没有处理SMC的能力,这时异常必须路由到EL3;如果没有EL2介入做转发,EL1和EL3之间的接口风险会大很多。

不过这套机制在非虚拟化系统里用不上,普通嵌入式Linux的异常路径通常就是两条:

  1. EL0 → EL1:应用程序触发系统调用(SVC)或MMU异常
  2. EL1自身异常:内核自身访问了错误地址或触发BUG

只有在涉及安全世界(EL3)或者多系统隔离(EL2)的时候,路由寄存器才成为调试的重点。刚开始接触ARMv8-A的人,建议先把EL0/EL1这条链路跑通,再扩展去研究高级路由。

4. 从零搭建异常处理框架:上下文保存、ESR解码与系统调用集成

4.1 上下文保存:哪些寄存器必须入栈

任何异常处理的第一步都是保存现场。这里的现场包括通用寄存器x0-x30、SP、ELR、SPSR、ESR等。但实际操作中有个取舍问题:保存哪些寄存器决定了处理程序的复杂度和中断延迟。

在ARMv8-A里,异常处理的上下文保存没有“调用者保存/被调用者保存”的说法,因为异常是异步事件,CPU可能在任意指令处被打断。理论上通用寄存器x0-x29都必须保存,x30(LR)其实也表示特定函数的返回地址,同样需要保存。

不过现实中很多轻量中断服务程序(例如简单的定时器节拍、串口收发)并不需要完整保存浮点/向量寄存器。如果你确认处理程序里不会用到FP/SIMD,就可以不保存v0-v31。但如果处理程序可能被调度器切走,而任务上下文里有浮点状态,那就必须完整保存NEON寄存器。这个判断不是靠猜测,而是靠调度器设计:你的任务切换代码保存了哪些寄存器,中断入口就得同步保存哪些。

一个经典的EL1入口保存序列:

el1_irq_handler: sub sp, sp, #272 stp x0, x1, [sp, #16 * 0] stp x2, x3, [sp, #16 * 1] stp x4, x5, [sp, #16 * 2] // ... 保存 x0-x29 mrs x0, esr_el1 mrs x1, elr_el1 mrs x2, spsr_el1 stp x0, x1, [sp, #16 * 14] stp x2, x30, [sp, #16 * 15]

保存顺序尽量跟你的上下文结构体定义一致,这样恢复时能对应上。我在设计自己的框架时,还顺手把ESR、ELR、SPSR也存进了栈帧,因为排查问题时这三位是“事故现场的第一手证据”。

4.2 ESR 详解:从异常原因寄存器反推错误现场

ESR(Exception Syndrome Register)是ARMv8-A里最重要的调试寄存器之一。它分两个主要字段:EC(Exception Class,位[31:26])和ISS(Instruction Specific Syndrome,位[24:0])。EC告诉你异常类别,ISS给出更细的错误类型,比如是翻译错误还是权限错误。

常见EC值速查:

EC值异常类别说明
0b000000Unknown reason未知原因,通常需要进一步查系统寄存器
0b000001Trap WF*执行WFE/WFI被陷入高EL
0b010101SVC指令发生在低EL的SVC调用
0b100000Instruction Abort(lower EL)取指时发生MMU错误
0b100001Instruction Abort(same EL)同上
0b100100Data Abort(lower EL)数据访问触发MMU错误
0b100101Data Abort(same EL)同上
0b100010PC alignment faultPC未对齐

针对Data Abort,ISS里还有更细的字段:DFSC(Data Fault Status Code,位[5:0])能告诉你具体是translation fault、permission fault还是alignment fault。曾经我排查一个“随机崩溃”问题,系统运行一段时间后偶尔死机。依靠ESR的DFSC字段发现是translation fault,但地址却属于合法映射区域。后来定位到是TLB缓存未失效导致的陈旧映射,手动执行TLB维护指令后问题消失。可以说,没有ESR,这类问题排查周期会翻倍。

SSR(ESR ISS)里还有一个值得关注的位:WnR(Write not Read,位[6]),它标记这次Data Abort是读还是写触发。在做写保护或者CoW(写时复制)机制时,这个位是必须解析的。

4.3 实例:把SVC处理集成到异常框架

SVC(Supervisor Call)是用户态(EL0)主动触发EL1的最常见方式,也就是我们常说的系统调用。ARMv8-A里,EL0通过SVC指令触发异常,异常进入EL1后,ESR的EC字段会标记为SVC,而SVC立即数在ISS的低16位里。

处理流程非常简单清晰:

void handle_svc(uint64_t esr, uint64_t *regs) { uint32_t imm = esr & 0xffff; // SVC immediate uint64_t syscall_id = regs[0]; // 通常 x0 存调用号 uint64_t arg0 = regs[1]; uint64_t arg1 = regs[2]; // 按 syscall_id 分发 regs[0] = dispatch_syscall(syscall_id, arg0, arg1); }

SVC入口在恢复阶段有一个特别容易忽略的点:ELR保存的是SVC指令本身的地址,ERET时会重新执行SVC指令一次。为了避免死循环,通常在SVC处理完成后必须把ELR加4(跳过SVC指令),返回到下一条指令。这个操作在系统调用实现里是基础中的基础,但我确实见过有人忘了加4,导致刚进用户态立刻再次触发SVC,系统调用无限抢占CPU。

正确返回前调整:

mrs x0, elr_el1 add x0, x0, #4 msr elr_el1, x0

4.4 栈指针选择:SP_EL0 与 SP_ELx 的切换内幕

ARMv8-A有一个很让人困惑的设计:在同一个异常级别里,物理上存在两个栈指针寄存器,SP_EL0和SP_ELx。当PSTATE的SPSel位为0时,SP操作的就是SP_EL0;为1时,SP对应SP_ELx。

这个设计的用途是“内核态平时用SP_EL1作为核心栈,但想访问用户栈时可以把SPSel切到0,用SP_EL0做临时的用户栈访问”。实际Linux内核就是靠这招实现copy_from_user/copy_to_user的——切换SPSel,用用户栈指针,不信任不检查直接访问,再由异常处理机制兜底。

但对于异常处理本身,最需要关心的是:异常向量表第一组(当前EL + SP_EL0)意味着异常发生时CPU仍在使用SP_EL0,此时处理程序要非常小心,因为SP_EL0可能指向用户栈(比如你在EL1想访问用户内存时被异常打断)。很多新手在EL1里使用SP_EL0时忘了切换回SP_EL1,结果异常处理程序自己的栈帧写进了用户栈区域,轻则崩溃,重则权限混乱。

通用建议:异常入口第一时间确认SPSel,如果是SP_EL0场景且后续处理需要大量栈空间,立刻把SP切到SP_EL1。

mrs x0, spsel cbnz x0, 1f // 如果已经在用 SP_ELx 则跳过 msr spsel, #1 // 切到 SP_ELx ldr x0, =kernel_stack_top mov sp, x0 1:

5. 调试实录:Fault 排查、嵌套中断与持久稳定的技巧

5.1 常见问题速查表

现象可能原因排查方向
异常后PC跳到0地址或随机地址向量表未2KB对齐 / VBAR未设置 / 链接脚本把向量表排错检查VBAR_ELx值、链接器MAP文件、向量表对齐属性
系统调用无法返回,始终触发SVC返回时未将ELR加4检查ERET前ELR是否指向SVC下一条指令
中断处理完成后任务现场错乱DAIF恢复时机过早 / 上下文保存不完整核对保存寄存器列表、恢复顺序
Data Abort发生但地址看似合法TLB缓存了陈旧映射执行TLB失效(如TLBI)后再测试
高EL级别无法触发FIQSCR/HCR路由控制位配置错误分别检查SCR_EL3、HCR_EL2对应位
使用SP_EL0时异常处理崩溃SPSel切换逻辑遗漏 / 栈指针指错进入异常后立即检查SPSel并切到SP_ELx

5.2 场景复盘:一个典型的“死机”问题定位全程

有一次在某开发板上调试一个裸机调度器,系统运行到多个任务同时就绪时,突然在切换上下文过程中死机。初看代码逻辑没有问题,上下文保存恢复的代码我都检查过,甚至加了调试打印都正常,但一旦跑到某个特定任务组合就死。

后来我同时抓取了ELR、ESR、SPSR三个值:

  • ELR指向一个看似正常的指令地址
  • ESR的EC显示是Data Abort
  • SPSR的M[4:0]显示异常发生前CPU在EL1

进一步查DFSC,发现是Permission Fault。我马上检查那个地址的页表项,发现确实没有设置AP[2:1]位的可写权限,但我的代码认为它可写。为什么?因为我当时用的映射表是基于“只读代码段+数据段”划分的,调度器某个栈帧恰好落在了只读段的保护页内,触发了权限错误。

这类问题如果没有ESR这个“证据链”的帮助,几乎只能靠暴力调试。所以我的建议是,在开发初期的异常处理程序里,把ELR、ESR、SPSR、FAR(Fault Address Register)全部保存并想办法输出,你会在后续调试过程中省掉大量时间。

5.3 嵌套中断的门槛:什么时候你才需要它

嵌套中断是指处理一个中断的过程中,又允许更高优先级中断进入。ARMv8-A异常进入时自动屏蔽所有异常,所以默认不支持嵌套。如果你需要嵌套,必须在处理程序中手动清除I位或F位。

我的经验是:前期开发阶段不要开嵌套,先把单层中断的稳定性跑扎实。嵌套中断涉及栈深度、优先级反转、锁重入等问题,复杂度陡增。等到系统框架稳定后,再根据实时性需求决定哪些中断需要嵌套。

如果必须开嵌套,有一点必须遵守:在你清除I位使能嵌套前,必须保证当前异常现场已经完全保存并且栈指针已切换到独立的中断栈,否则任何进入的嵌套异常都会覆盖当前未保存的上下文。在某项目里我因为早开了几百微秒,导致高优先级中断压栈时覆盖了低优先级中断的寄存器,故障非常诡异。

5.4 让异常处理变得可观测:裸机下的日志输出策略

裸机环境没有标准输出,异常日志怎么给出来?我从实践中归纳出一套三层方案:

  1. 寄存器快照写入固定内存区:在异常入口把ELR、ESR、SPSR、FAR按固定偏移写入一段预设内存,这段内存不被复用
  2. 关键信息通过串口/调试口输出:如果板子有串口,可在进入异常后用最简单的轮询方式打印,注意打印代码本身不能使用复杂函数,否则可能再次触发异常
  3. 保留标志位用于系统重启后分析:把寄存器快照写到不掉电内存或Flash区域,重启后由启动代码检查这个标志位,决定是否进入恢复或上报模式

这个三层方案帮我解决过不少“只在现场复现”的疑难杂症,特别是涉及外部总线错误和电源噪声触发的SError,这类问题不常在开发台上复现。

在NRF环境下,我通常还会配合JTAG调试器,用脚本自动读取ELR和ESR,结合反汇编比对出错位置。这种“双通道验证”(硬件快照+调试器实读)能大幅缩小排查范围。

5.5 优雅稳定:避免异常处理程序本身成为新的故障源

说到最后,异常处理程序本身也是代码,也可能引入新问题。这是我多年实践下来的一些稳定化建议:

处理程序保持短小精悍。别在异常上下文里做复杂业务逻辑,只负责保存现场、识别异常、记录信息,然后跳转到C语言层的统一处理函数。C层可以做调度决策、错误分类、进程终止等重逻辑。

栈空间要留足。异常处理可能发生在任务栈几乎用满的时候,所以要么给每个任务预留异常帧空间,要么使用独立中断栈。我在项目里统一使用中断栈,每个CPU核一个,8KB起步,宁可多给也不要省。

不要依赖锁。异常处理路径里尽量别用自旋锁或其他复杂的互斥机制,因为中断可能打断持有锁的代码路径,造成死锁。如果必须用锁,需要确保锁的实现是中断安全的,并且清楚锁在异常上下文中的语义。

定期审查向量表与处理入口。硬件不会提醒你向量表某个入口跳错了,只有线上故障才会。养成每次改汇编入口后都对照反汇编核对一遍的好习惯,成本很低,收益很高。

6. 写在最后的实操体会

ARMv8-A的异常处理是一个“硬件协议栈”——硬件帮你完成了大部分流程,但软件必须精准匹配它的设计约定。向量表布局、路由控制、上下文保存、ESR解析,每块都环环相扣。我见过太多人卡在“向量表不对齐”“DAIF恢复时机不对”这些细节上,这些坑用文档查不出来,只能靠实打实的调试经验积累。

如果你刚开始接触,我建议先抛开TrustZone和虚拟化这些高阶话题,用最朴素的裸机环境把EL0到EL1的SVC路径跑通,再依次加入IRQ、FIQ、SError的处理。每一步都确认ELR、ESR、SPSR的对应关系,用调试器打印这些寄存器的值,直到你对“异常进入时硬件做了什么”有了肌肉记忆,再回头处理复杂架构就会轻松很多。

最后再分享一个小技巧:写异常处理程序之前,先把芯片参考手册里ESR的EC字段表格打印出来贴在显示器边上。刚开始你会频繁翻它,但几个月后你会发现大部分异常类型你已经烂熟于心,那时候你才算真正跟ARMv8-A的异常模型交了朋友。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询