3. 安全监控器(Monitor)详解:入口、出口与切换流程

安全监控器,很多人叫它 Monitor,也有人叫它安全监视器。说白了,它就是安全世界和普通世界之间的那个「看门人」。你想想看,两个世界要切换,总得有个东西来管着吧?Monitor 就是干这个的。

我个人习惯把 Monitor 理解成一个「特权中转站」。它运行在比安全世界还要高的权限级别上,两个世界之间的所有切换,都必须经过它点头才行。嗯,这里要注意,Monitor 本身不是一个操作系统,它更像是一个固件级别的安全管家。

3.1 Monitor 的入口与出口

先说说入口。什么时候会进入 Monitor?我遇到过不少开发者,一开始以为只有 SMC 指令才能进 Monitor。其实不止,还有好几种情况:

  • SMC 指令:最常用的入口,后面会细讲
  • 中断/异常:安全世界或普通世界触发的异常,如果配置了路由到 Monitor
  • 系统复位:上电后默认进入 Monitor 初始化流程

出口呢?出口只有一条路——ERET(Exception Return)。从 Monitor 返回时,硬件会根据 Monitor 中保存的上下文,决定回到安全世界还是普通世界。

关键点:Monitor 的入口是「多对一」,出口是「一对多」。所有入口最终都汇聚到 Monitor,然后 Monitor 根据情况选择出口方向。

我曾经踩过一个坑:在 Monitor 里修改了返回地址,但忘了同步更新 SPSR(保存的程序状态寄存器)。结果 ERET 之后,CPU 直接跑飞了。调试了整整两天才找到原因。所以,改返回地址的时候,一定要连 SPSR 一起改。

3.2 SMC 指令详解

SMC(Secure Monitor Call)是触发安全监控器调用的指令。它的用法其实很简单:

// 触发 SMC 调用
SMC #0

// 带功能号的 SMC 调用
// x0 中存放功能号,x1-x6 存放参数
MOV x0, #0xC4000001   // 功能号:切换到安全世界
SMC #0

为什么要有功能号?你想想看,两个世界之间要传递的信息可不止「切换」这么简单。比如:

功能号范围 用途 说明
0xC4000000 - 0xC400FFFF 标准服务 ARM 定义的通用服务,比如获取版本号
0xC5000000 - 0xC500FFFF 安全服务 安全世界提供的专有服务,比如密钥管理
0xC6000000 - 0xC600FFFF 可信应用调用 普通世界请求可信应用执行特定操作

我个人习惯把功能号分成三类:查询类、操作类、控制类。查询类只读不写,操作类会修改安全状态,控制类则涉及 Monitor 自身的配置。这样分类,写代码的时候不容易搞混。

小技巧:SMC 指令执行时,CPU 会自动保存当前世界的上下文到 Monitor 的寄存器中。所以你在 Monitor 里可以直接读取 x0-x6 获取参数,不需要额外保存。

3.3 安全监控器切换流程

完整的切换流程,我画了一张图,你看完就明白了:

安全监控器切换流程图 安全监控器切换流程 普通世界 运行中 SMC #0 SMC 触发 保存上下文 安全监控器 (Monitor) 解析功能号 → 执行服务 → 准备返回 更新 SPSR 和返回地址 判断返回目标 返回普通世界 普通世界 恢复执行 返回安全世界 安全世界 恢复执行 ERET 指令根据 SPSR 中的返回模式,自动选择返回普通世界或安全世界

流程其实就三步:

  1. 触发:普通世界或安全世界执行 SMC 指令,CPU 自动保存当前上下文
  2. 处理:Monitor 解析功能号,执行对应的服务逻辑
  3. 返回:Monitor 设置好返回地址和 SPSR,执行 ERET

我记得第一次看这个流程的时候,觉得很简单。后来自己写 Monitor 代码才发现,坑全在细节里。比如保存上下文的时候,哪些寄存器要保存?哪些不用?ARM 规范里其实有明确说明,但很多人不看。

注意:Monitor 中不能嵌套调用 SMC。如果你在 Monitor 里又执行了一次 SMC,CPU 会触发一个未定义指令异常。我曾经见过有人这么写,结果系统直接死锁。

3.4 异常处理机制

Monitor 的异常处理,说白了就是「谁惹的事,谁负责擦屁股」。但具体怎么擦,有讲究。

异常分两类:

  • 同步异常:比如 SMC 指令本身、未定义指令、数据中止。这些是 CPU 执行指令时当场触发的。
  • 异步异常:比如中断、FIQ。这些是外部设备触发的,和当前执行的指令无关。

处理流程是这样的:

// Monitor 异常向量表(简化版)
// 每个入口 16 字节,存放跳转指令
Monitor_vectors:
    .word Monitor_Sync_Handler    // 同步异常
    .word Monitor_IRQ_Handler     // IRQ 中断
    .word Monitor_FIQ_Handler     // FIQ 快速中断
    .word Monitor_Error_Handler   // 系统错误

// 同步异常处理示例
Monitor_Sync_Handler:
    // 读取 ESR(异常综合寄存器)判断异常类型
    MRS x1, ESR_EL3
    // 如果是 SMC 调用,走正常切换流程
    // 如果是其他异常,记录错误并返回
    // ... 处理逻辑 ...
    ERET

我遇到过最头疼的问题是什么?是中断路由。普通世界的中断,要不要路由到 Monitor?安全世界的中断呢?这取决于你的系统设计。我的建议是:

中断类型 路由策略 原因
普通世界 IRQ 直接路由到普通世界 减少 Monitor 的负担,提高响应速度
安全世界 IRQ 直接路由到安全世界 安全世界的中断通常更紧急
FIQ 路由到 Monitor FIQ 用于安全关键场景,需要 Monitor 统一调度
跨世界中断 路由到 Monitor 需要 Monitor 判断当前世界状态,决定是否转发

经验之谈:FIQ 路由到 Monitor 后,处理逻辑一定要短。FIQ 的设计初衷就是「快进快出」。我一般只在 FIQ 里做两件事:记录事件源,然后触发一个软中断交给安全世界去慢慢处理。

异常处理还有一个容易忽略的点:异常返回时的世界切换。如果异常发生在普通世界,但 Monitor 决定把异常交给安全世界处理,那返回的时候就要回到安全世界。这时候 SPSR 里的返回模式必须设置正确,否则 ERET 会把你送回错误的世界。

嗯,说到这个我想起来一个案例。有个项目,普通世界的定时器中断总是莫名其妙地丢失。查了很久才发现,Monitor 在处理中断时,错误地把返回地址设成了安全世界的地址。结果每次中断处理完,CPU 都跑到了安全世界,普通世界的定时器服务函数永远执行不到。修复方法很简单:在 Monitor 的中断处理函数里,先保存当前世界的上下文,再根据中断来源决定返回哪个世界。

核心原则:Monitor 的异常处理,本质上是「上下文切换」+「权限仲裁」。谁触发的异常不重要,重要的是异常该由谁处理,以及处理完后该回到哪里。

最后说一句,Monitor 的代码一定要精简。我见过有人把文件系统驱动都塞进 Monitor 里,结果 Monitor 的代码量比安全世界还大。这完全违背了 Monitor 的设计初衷——它只是一个「看门人」,不是「管家」。