aarch64异常级别转换:EL1上下文切换的实战脉络——从svc到eret的每一步都算数
你有没有在调试一个看似简单的系统调用时,发现返回后寄存器值“莫名”错乱?或者在启用GICv3中断嵌套后,某次IRQ处理完竟卡死在eret指令上?又或者在移植一个轻量级RTOS到aarch64平台时,反复触发IllegalState异常却查不到源头?
这些都不是玄学——它们全指向同一个被许多文档轻轻带过、却被硬件严丝合缝执行的底层契约:EL1上下文切换的完整生命周期。
这不是一段可以跳过的初始化代码;它是aarch64内核世界的“呼吸节奏”。每一次svc #0,每一次外部中断到来,每一次eret落回用户空间,CPU都在按一套不容妥协的规则,完成寄存器快照、栈指针切换、状态恢复与路径校验。本文不画框图、不列概念,只带你亲手走一遍这条从EL0跌入EL1、再稳稳跃回的完整路径——用真实汇编说话,用寄存器状态验证,用错误现场反推设计逻辑。
为什么EL1上下文切换不能“大概对”?
先看一个极易踩中的坑:
// 错误示范:在EL1 handler中直接修改SPSR_EL1.M字段为0b0100(EL0) mov x0, #0b010000000000 // M=EL0, DAIF=0 msr spsr_el1, x0 eret你以为这是“优雅返回用户态”?不。ARMv8-A架构手册明确写到:
If the value written to SPSR_ELx.M specifies an Exception Level that is higher than the current Exception Level, an IllegalState exception is generated.
也就是说,当前在EL1执行msr spsr_el1, x0,而你往M[3:0]里填了0b0100(EL0),这没问题;但当你紧接着执行eret,硬件会检查SPSR_EL1.M是否合法允许返回——而EL1 → EL0是允许的。那问题出在哪?
真正致命的是:SPSR_EL1.SP = 0(即返回时要用SP_EL0),但你的SP_EL0此时可能正被用户进程疯狂压栈,甚至已被恶意覆写。更隐蔽的是:若你在进入EL1前没显式切换SP(比如忘了msr spsel, #1),那整个EL1栈操作其实还在SP_EL0上进行——等于把内核寄存器全 dump 到用户栈里。
所以,“上下文切换正确”不是指“能跑通”,而是指:
- 每个寄存器保存/恢复的位置、时机、对齐方式,都符合AAPCS64与ARM ARM双重约束;
- 每一次eret前,SPSR_EL1和ELR_EL1的组合必须通过硬件合法性校验;
- 栈指针切换、帧指针建立、参数传递,三者必须形成闭环,缺一不可。
下面我们就从一次最典型的svc #0开始,逐帧拆解。
从svc #0落地EL1:硬件做了什么?我们又该做什么?
假设用户进程正在执行:
mov x0, #123 svc #0 // ← 就是这一条,触发SVC异常 add x1, x0, #1 // ← 这条地址将被存入ELR_EL1硬件自动完成的四件事(不可绕过)
| 动作 | 寄存器/行为 | 关键细节 |
|---|---|---|
| 1. 切换栈指针 | SP ← SP_EL1(若 |