3. 硬件基础:Monitor Mode 与 SMC 指令,如何完成世界切换?

好,我们进入第三章。前两章我们聊了 TrustZone 的基本概念和内存布局,说白了就是知道了「有什么」和「在哪」。但真正让 TrustZone 跑起来的核心机制,是 世界切换——从安全世界切到非安全世界,或者反过来。这个动作,全靠 Monitor Mode 和 SMC 指令 来完成。

我记得刚接触 TrustZone 时,总觉得这玩意儿很玄乎。后来亲手调过几次切换流程,才明白它其实就是一个精心设计的「门禁系统」。今天我就带你把这个门禁拆开看看。

3.1 什么是 Monitor Mode?

ARM 架构里,CPU 有好几种模式,比如 User Mode、Supervisor Mode、IRQ Mode 等等。但 TrustZone 引入了一个特殊的模式——Monitor Mode(监控模式)。

你可以把它想象成一个安全岛。它不属于安全世界,也不属于非安全世界。它独立于两者之外,专门负责处理世界切换的决策和执行。

核心要点:Monitor Mode 是 TrustZone 世界切换的「交通警察」。所有跨世界的异常、中断、指令,最终都要经过它来裁决。

为什么需要这么个中间层?你想想看,如果非安全世界的代码能直接跳到安全世界,那安全还有什么意义?所以 ARM 设计了一个强制隔离点——任何世界切换都必须先进入 Monitor Mode,由它检查合法性,再决定是否放行。

3.2 SMC 指令:发起切换的「敲门砖」

那么,怎么触发世界切换呢?靠一条指令——SMC(Secure Monitor Call)。

SMC 指令的作用很简单:触发一个异常,让 CPU 进入 Monitor Mode。就像你按门铃,门铃响了,保安(Monitor)过来开门。

// 在非安全世界发起 SMC 调用
// 请求切换到安全世界执行某个服务
asm volatile("SMC #0" : : : "memory");

这条指令执行后,CPU 会:

  1. 保存当前世界的上下文(寄存器、状态等)
  2. 切换到 Monitor Mode
  3. 跳转到 Monitor 向量表中对应的入口

我在项目中遇到过一个问题:有人把 SMC 指令放在中断处理函数里,结果导致嵌套异常,系统直接挂掉。嗯,这里要注意——SMC 指令不能在 IRQ/FIQ 处理函数中随意调用,除非你做好了完整的栈保护和重入处理。

3.3 世界切换的完整流程

光说理论不够,我们画个图看看。下面这张 SVG 展示了从非安全世界发起 SMC 到返回的完整路径。

TrustZone 世界切换流程图 SMC 世界切换完整流程 非安全世界 (Normal World) 安全世界 (Secure World) Monitor Mode(安全岛) ① 非安全应用执行 SMC asm volatile("SMC #0") ② Monitor 向量表入口 检查 SMC 功能号 / 参数 ③ 安全世界执行服务 访问安全内存 / 外设 ④ Monitor 恢复上下文 ERET 返回非安全世界 关键点:所有世界切换必须经过 Monitor Mode,不允许直接跳转

流程其实就四步:

  • 第一步:非安全世界执行 SMC 指令,CPU 硬件自动保存状态,进入 Monitor Mode。
  • 第二步:Monitor 向量表里的 SMC handler 被调用。它检查 SMC 的功能号(Function ID),判断要调用哪个安全服务。
  • 第三步:Monitor 切换到安全世界,执行对应的安全服务代码。
  • 第四步:安全服务执行完毕,再次触发 SMC 或直接 ERET,Monitor 恢复非安全世界的上下文,返回。

个人经验:我建议你在写 Monitor 代码时,把 SMC 功能号设计成类似 Linux 系统调用的方式——用一个统一的入口,通过参数分发。这样后期扩展安全服务时,不用改向量表,只加 handler 就行。

3.4 Monitor 向量表与异常处理

Monitor Mode 有自己的异常向量表,和普通世界的向量表是分开的。这个向量表通常放在安全内存里,非安全世界无法篡改。

向量偏移 异常类型 触发条件 典型用途
0x00 Reset 系统复位 初始化 Monitor
0x04 Undefined Instruction 未定义指令 捕获非法指令
0x08 SMC 执行 SMC 指令 世界切换入口
0x0C Prefetch Abort 指令预取失败 内存访问错误
0x10 Data Abort 数据访问失败 内存保护违规
0x14 IRQ 普通中断 中断路由决策
0x18 FIQ 快速中断 安全中断处理

这里有个关键点:IRQ 和 FIQ 在 Monitor Mode 下也需要处理。为什么?因为中断可能发生在任何世界。如果非安全世界正在执行,来了一个安全中断(比如安全世界的加密引擎完成),Monitor 需要决定:是立即切换到安全世界处理,还是等当前任务完成?

我曾经踩过一个坑:把所有的中断都直接路由到安全世界,结果非安全世界的网络驱动超时,整个系统卡死。后来我改成了中断分类路由——安全中断走 FIQ 直接进安全世界,普通中断走 IRQ 留在当前世界。这样两边互不干扰。

3.5 SMC 调用约定:参数怎么传?

SMC 指令本身不带参数,那怎么告诉 Monitor 你要干什么?ARM 定义了一套调用约定(SMC Calling Convention)。

简单说,就是通过寄存器传参:

  • R0:SMC 功能号(Function ID),告诉 Monitor 要调用哪个服务。
  • R1-R6:参数,最多 6 个。
  • R0(返回时):返回值。

举个例子:

// 非安全世界发起 SMC 调用
// 功能号 0x82000001:请求安全世界生成随机数
unsigned int func_id = 0x82000001;
unsigned int arg1 = 0;      // 预留
unsigned int arg2 = 0;      // 预留
unsigned int result;

asm volatile(
    "MOV R0, %[func]\n"
    "MOV R1, %[arg1]\n"
    "MOV R2, %[arg2]\n"
    "SMC #0\n"
    "MOV %[res], R0\n"
    : [res] "=r" (result)
    : [func] "r" (func_id), [arg1] "r" (arg1), [arg2] "r" (arg2)
    : "r0", "r1", "r2", "memory"
);

// result 里就是安全世界返回的随机数

警告:SMC 功能号有严格的范围划分。0x80000000-0x8FFFFFFF 是厂商自定义范围,0xC0000000-0xFFFFFFFF 是标准服务范围。不要乱用,否则可能和其他安全服务冲突。

3.6 世界切换的性能开销

你可能会问:每次切换都要保存恢复上下文,会不会很慢?

确实有开销。我实测过,一次完整的 SMC 切换(包括进入 Monitor、切换世界、执行服务、返回)大约需要 几百纳秒到几微秒,具体取决于 CPU 频率和 Monitor 代码的复杂度。

但别担心,ARM 做了优化:

  • 硬件自动保存:部分上下文(如 LR、SPSR)由硬件自动保存,不需要软件干预。
  • Lazy 上下文切换:有些寄存器(比如浮点寄存器)可以延迟保存,只在真正用到时才保存。
  • 世界本地中断:如果安全服务很短,可以关中断直接执行,避免额外的切换。

我个人习惯是:把频繁调用的安全服务设计成「批处理」模式。比如一次性传入多个请求,安全世界批量处理完再统一返回。这样能大幅减少切换次数。

3.7 避坑指南:我见过的 SMC 翻车现场

做了这么多年 TrustZone,我见过不少 SMC 相关的 bug。挑几个典型的说说:

  • 栈溢出:Monitor Mode 的栈通常很小(几 KB)。有人递归调用 SMC,直接把栈撑爆了。我建议 Monitor 栈至少分配 4KB,并且加上栈溢出检测。
  • 参数校验缺失:非安全世界传进来的指针,Monitor 直接用了。结果指向非法地址,Data Abort。记住:永远不要信任非安全世界传来的指针,必须校验地址范围。
  • 中断丢失:在 Monitor 里长时间关中断,导致外部中断丢失。我建议 Monitor 代码尽量短,如果必须长时间处理,考虑开中断并做好重入保护。
  • 功能号冲突:两个团队各自定义了相同的 SMC 功能号,结果系统行为诡异。解决方案:统一管理功能号分配,用文档或头文件约束。