3. 安全监控器(Monitor Mode)基础
安全监控器(Monitor Mode)是 TrustZone 架构中连接安全世界(Secure World)与普通世界(Normal World)的桥梁。在 ARMv7‑A 中称为 Monitor Mode,在 ARMv8‑A 中对应 EL3(Exception Level 3)。所有跨世界切换必须通过监控器完成,而监控器的入口由异常向量表和 SMC(Secure Monitor Call)指令共同定义。本章将深入讲解监控器异常向量表的布局、SMC 指令的执行流程,以及如何编写监控器入口代码。
3.1 监控器异常向量表
监控器模式拥有独立的异常向量表,用于处理来自两个世界的异常、SMC 调用以及物理中断。ARMv7‑A 使用 MVBAR(Monitor Vector Base Address Register)存放向量表基址;ARMv8‑A 使用 VBAR_EL3。向量表按异常类型偏移排列,每个条目占用 4/8/16 字节(视架构设定)。
| 偏移(Bytes) | 异常类型 | 触发条件(EL3 视角) |
|---|---|---|
| 0x000 | 同步异常(SMC 等) | SMC 指令、未定义指令、数据中止等 |
| 0x080 | IRQ | 物理中断配置为路由到 EL3 |
| 0x100 | FIQ | 快速中断路由到 EL3 |
| 0x180 | SError | 系统错误 |
向量表对齐要求:
MVBAR / VBAR_EL3 必须按 32 字节(ARMv7‑A)或 2K 字节(ARMv8‑A)对齐。错误对齐会导致异常无法正确分发。
3.2 SMC 指令工作原理
SMC 是触发监控器入口的唯一用户指令。普通世界和安全世界均可执行,但只有在特权级足够时才会进入异常。指令格式:SMC #imm,其中 imm 作为立即数(通常被上层忽略,真正的功能 ID 通过寄存器传递)。
- 执行
SMC #0后,CPU 保存当前 ELR/SPSR,切换栈指针到监控器专用栈,跳转到同步异常向量表入口。 - 功能 ID 放入
x0(ARMv8‑A)或r0(ARMv7‑A),后续参数放入x1-x7(或r1-r7)。 - 监控器通过读取 ID 分发到不同处理函数。
3.3 监控器入口代码编写
入口代码需完成:保存通用寄存器、保存返回地址/状态、构造安全调用上下文、依据功能 ID 分发、处理完成后恢复寄存器并执行 ERET(或 MOVS PC, LR)返回。
; ARMv8‑A 简化入口汇编(AArch64)
.macro vector_entry label
.align 7
\label:
stp x0, x1, [sp, #-16]!
stp x2, x3, [sp, #-16]!
; ... 保存 x4-x29
mrs x30, ELR_EL3
str x30, [sp, #-8]!
mrs x30, SPSR_EL3
str x30, [sp, #-8]!
; 切换 C 栈
mov x30, sp
ldr x1, =monitor_c_stack_top
mov sp, x1
; 调用 C 处理函数(x0 作为参数指针)
mov x0, x30
bl monitor_smc_handler
; 恢复与返回
ldp x30, x1, [sp], #16
msr SPSR_EL3, x1
ldr x30, [sp], #8
msr ELR_EL3, x30
ldp x28, x29, [sp], #16
; ... 恢复 x0-x27
ldp x0, x1, [sp], #16
eret
.endm
; 同步异常向量表入口
.globl sync_exception_entry
sync_exception_entry:
vector_entry .
栈规划陷阱:入口代码中必须使用独立的监控器栈,不可复用普通世界或安全世界栈指针。栈大小至少 4KB,并确保 16 字节对齐。同时注意
SPSR_EL3 的保存与恢复顺序——错误顺序会导致返回时异常状态丢失。
3.4 流程概述(SVG 图)
下图展示了从普通世界调用 SMC 到监控器返回的完整数据流:
3.5 关键寄存器配置
初始化监控器必须正确配置以下寄存器:
| 寄存器 | 用途 | 初始化值示例 |
|---|---|---|
MVBAR/VBAR_EL3 | 设置监控器异常向量表基址 | 对齐后的地址(如 0x30000000) |
SCR_EL3 | 系统控制寄存器(安全配置) | 0x13 (NS=0, IRQ/FIQ 路由等) |
SPSR_EL3 | 保存监控器返回状态 | 由入口代码自动设置 |
SP_EL3 | 监控器栈指针 | 独立栈顶地址 |
3.6 常见错误与调试方法
- 向量表未对齐:触发异常时 CPU 跳转到错误地址,导致双异常复位。使用
.align伪指令强制对齐。 - 栈溢出:递归调用或功能 ID 过多消耗栈空间。使用 Canary 或手动检查 SP 边界。
- 返回地址错误:未正确保存
ELR_EL3和SPSR_EL3,返回后崩溃。可在入口处插入断点验证。 - SMC 调用约定不一致:普通世界使用
r0传功能 ID,入口函数却按x0解析。统一使用 AArch64 的x0标准。 - 调试手段:用
ERET前打印SPSR_EL3和ELR_EL3到 UART;或使用 JTAG 在sync_exception_entry处设断点。
调试技巧:在入口汇编保存链接寄存器
LR 后立即写入一个已知模式(如 0xDEADBEEF),若返回时模式被改变则说明栈污染。
3.7 C 层处理函数示例
// monitor_smc_handler.c
#include <stdint.h>
typedef struct {
uint64_t x[31]; // x0-x30
uint64_t elr_el3;
uint64_t spsr_el3;
} smc_regs_t;
uint64_t monitor_smc_handler(smc_regs_t *regs) {
uint64_t fid = regs->x[0]; // 功能 ID
switch (fid) {
case 0xC4000001: // SMC_FC_GET_VERSION
regs->x[0] = 0x00010000; // 返回值放入 x0
break;
case 0xC4000002: // SMC_FC_SECURE_ENTRY
// 进入安全世界,切换 SCR_EL3.NS 等
break;
default:
regs->x[0] = -1; // 未知 ID 返回错误
}
return 0; // 正常返回
}
上述函数在入口汇编中被调用,通过修改 regs 中的 x0 实现返回值传递。注意:处理函数需以异常返回方式结束,不可直接退出。任何对 SPSR_EL3 的修改必须谨慎,否则会破坏安全世界状态。
3.8 性能与稳定性优化要点
- 最小化寄存器保存范围:只保存可能被 C 函数破坏的寄存器(调用者保存寄存器),减少上下文切换开销。
- 使用影子栈:为每个世界维护独立的监控器栈(通过
SCR_EL3.NS切换),避免栈数据泄露。 - 快速路径:对常见 SMC 调用(如时间戳获取)直接在内联汇编中处理,避免进入 C 函数。
- 异常嵌套:监控器内应禁用中断(设置
SPSR_EL3的 A/I/F 位)或使用嵌套中断保护机制,否则复杂的嵌套调用会导致栈溢出。
推荐扩展:在完成基础入口代码后,可加入 SMC 调用 ID 的日志记录(通过 UART),用于调试普通世界与安全世界的通信协议。