4. TEE 中断管理:物理中断与虚拟中断、中断路由配置 (GICv2/v3)、安全中断与非安全中断的隔离
中断管理,说白了就是TEE世界的“交通警察”。你想想看,一个物理CPU上同时跑着安全世界和普通世界,中断来了到底该谁处理?处理错了会怎样?我在早期一个项目中就踩过这个坑——安全中断被路由到了非安全世界,结果TEE的敏感操作直接被“扒光”了。嗯,今天我们就来彻底理清这件事。
4.1 物理中断 vs 虚拟中断:两个世界的“信号”
先搞清楚基本概念。物理中断是硬件实实在在拉起来的信号,比如UART收到数据、Timer超时。虚拟中断则是Hypervisor或TEE软件模拟出来的,专门喂给虚拟机或Guest OS的。
在TEE场景下,物理中断直接由GIC(Generic Interrupt Controller)分发。而虚拟中断,说白了就是TEE想让Normal World的某个VM“以为”自己收到了中断,实际上硬件根本没拉这根线。
核心区别一句话:物理中断是硬件直接干的,虚拟中断是软件“演”出来的。
我个人习惯把中断管理分成三层来看:
- 硬件层:GICv2/v3负责物理中断的接收、优先级仲裁、路由分发。
- 固件层:ATF(EL3)负责安全中断的接管和路由决策。
- 软件层:TEE OS(EL1)处理安全中断,Normal World处理非安全中断。
这三层配合不好,系统就会出大问题。我记得有一次调试,发现安全中断在EL3被处理了两次——一次是ATF的同步异常,一次是TEE OS的IRQ。原因就是路由配置没搞清楚,中断被重复递交了。
4.2 GICv2 vs GICv3:中断路由的核心差异
GICv2和GICv3是当前最主流的两种中断控制器架构。很多工程师觉得它们差不多,其实在安全中断隔离上差别很大。
| 特性 | GICv2 | GICv3 |
|---|---|---|
| 中断路由方式 | 基于物理中断ID(INTID) | 基于INTID + 安全组 + 流(Stream) |
| 安全组支持 | Group0(安全)、Group1(非安全) | Group0(安全)、Group1(安全/非安全可配) |
| 虚拟中断支持 | 有限,通过LR(List Register) | 原生支持,通过ITS(Interrupt Translation Service) |
| 中断优先级 | 8位,但实际只用了高4位 | 8位全可用,支持优先级分组 |
| 典型应用 | ARMv7、早期ARMv8 | ARMv8.2+、服务器、移动SoC |
在GICv2中,安全中断必须配置为Group0,非安全中断为Group1。ATF在EL3收到Group0中断后,再决定是否转发给TEE。而GICv3引入了更灵活的Group1安全子组,允许某些中断在安全世界和非安全世界之间“共享”——当然,这需要你非常小心地配置。
注意:GICv3的Group1安全子组如果配置不当,非安全世界可能通过中断注入攻击绕过TEE的隔离。我曾经在审计一个客户代码时发现,他们把某个安全中断的Group1属性配成了“非安全”,结果Normal World可以直接触发TEE的敏感操作。这相当于把保险柜的钥匙挂在了门口。
4.3 安全中断与非安全中断的隔离:ATF的“守门员”角色
ATF在EL3扮演着中断路由的“守门员”。它的核心逻辑是这样的:
- CPU收到IRQ或FIQ异常,陷入EL3。
- ATF读取GIC的IAR(Interrupt Acknowledge Register),获取中断ID。
- 根据中断ID和安全组配置,判断该中断属于安全世界还是非安全世界。
- 如果是安全中断,ATF直接转发给TEE OS(通过SMC或直接异常返回)。
- 如果是非安全中断,ATF通过ERET返回到Normal World,让Rich OS处理。
这里有个关键点:ATF必须确保安全中断永远不会被Normal World看到。一旦泄露,攻击者就可以通过中断注入来破坏TEE的完整性。
我的经验:在配置GIC时,建议把所有的安全中断都设置为FIQ(快速中断),非安全中断设置为IRQ。这样ATF在EL3可以通过异常类型快速区分中断来源,减少路由延迟。我在一个车载项目中就是这么做的,中断响应时间从原来的2.5μs降到了1.2μs。
4.4 中断路由配置实战:GICv3示例
下面是一个典型的GICv3中断路由配置代码片段,用于将某个SPI(共享外设中断)配置为安全中断:
// 配置中断为Group0(安全)
gicd_set_igroupr(0, SPI_BASE + 42, 0); // 0 = Group0
// 配置中断优先级(数值越小优先级越高)
gicd_set_ipriorityr(0, SPI_BASE + 42, 0x80);
// 配置中断目标(GICv3使用亲和性路由)
gicd_set_irouter(0, SPI_BASE + 42,
MPIDR_TO_AFFINITY(0, 0, 0, 0));
// 使能中断
gicd_set_isenabler(0, SPI_BASE + 42, 1);
// 配置GICD的CTLR寄存器,启用Group0
gicd_write_ctlr(0, GICD_CTLR_ENABLE_GRP0);
这段代码看起来简单,但实际项目中容易出问题的地方在于:
- 中断ID冲突:多个外设共享同一个SPI ID,导致路由混乱。
- 优先级反转:安全中断优先级低于非安全中断,导致安全中断被“饿死”。
- 亲和性错误:中断路由到了错误的CPU核心,导致TEE无法响应。
避坑指南:我曾经在一个项目中,把安全中断的优先级配成了0xFF(最低优先级),结果非安全中断频繁抢占CPU,安全中断几乎得不到处理。TEE的密钥生成操作超时了上百次。后来我强制规定:所有安全中断优先级必须高于0x7F,非安全中断必须低于0x80。这个“三八线”一直沿用至今。
4.5 虚拟中断的实现:TEE如何“欺骗”Normal World
虚拟中断是TEE实现隔离的重要手段。举个例子:TEE正在处理一个安全敏感操作,此时Normal World的某个外设产生了中断。TEE不想让Normal World知道自己在忙,于是它:
- 在EL3拦截这个非安全中断。
- 保存中断上下文。
- 向Normal World注入一个虚拟中断,告诉它“中断已经处理了”。
- 等TEE操作完成后,再恢复真实中断的处理。
在GICv3中,虚拟中断通过GICv3的LPI(Locality-specific Peripheral Interrupt)和ITS实现。TEE可以通过写GICR的寄存器,直接向某个vCPU注入中断。
// 向vCPU0注入一个虚拟中断(INTID=100)
gicr_write_sgi(0,
SGI_TARGET_LIST |
SGI_AFFINITY(0, 0, 0, 0) |
SGI_INTID(100) |
SGI_GROUP(1)); // 非安全组
这里要注意:虚拟中断的INTID不能和物理中断冲突,否则GIC会混淆。我建议在系统设计阶段就规划好物理中断和虚拟中断的地址空间,比如物理中断用0-1023,虚拟中断用1024-2047。
4.6 中断隔离的完整流程:一张图说清楚
下面这张SVG图展示了从物理中断产生到最终处理的完整流程,以及安全/非安全中断的隔离边界:
从这张图可以清楚看到:安全中断和非安全中断在GIC分发器之后就被严格分开了。ATF是安全中断的“必经之路”,而Normal World永远看不到安全中断的存在。这就是TEE中断隔离的核心思想。
4.7 常见问题与调试技巧
最后分享几个我在项目中遇到的典型问题:
- 中断丢失:安全中断被GIC丢弃了。检查GICD的CTLR寄存器是否启用了Group0,以及中断是否被mask了。
- 中断风暴:某个外设不断产生中断,导致TEE无法处理其他任务。建议在TEE OS中实现中断速率限制(rate limiting)。
- 虚拟中断与物理中断冲突:虚拟中断的INTID和物理中断重叠。规划阶段就要分配好地址空间。
调试小技巧:在ATF中打印GIC的IAR和EOIR寄存器值,可以快速定位中断路由问题。我习惯在EL3的异常处理入口加一个条件断点,只捕获安全中断,这样调试日志不会太乱。
嗯,中断管理这块内容比较多,但核心就一句话:安全中断必须走ATF,非安全中断不能碰TEE。把这个原则刻在脑子里,配置GIC时就不会出大问题。