3、SMC 调用机制:从非安全世界到安全世界的桥梁

SMC 指令,说白了就是 ARM 架构里从普通世界(Non-Secure World)跳进安全世界(Secure World)的那扇门。我刚开始接触 TEE 时,总觉得这不过是一条汇编指令而已,后来踩了不少坑才明白——这条指令背后藏着整个安全生态的通信契约。

今天我们就把它彻底拆开,看看 SMC 到底怎么用,调用约定长什么样,以及切换流程里有哪些容易翻车的地方。

3.1 SMC 指令:不只是“软中断”

SMC(Secure Monitor Call)是一条汇编指令,编码是 0xE1600070(ARM 32位)或 SMC #0(AArch64)。它触发一个同步异常,让 CPU 陷入 EL3(异常等级 3),也就是 Monitor 模式。

你可能会问:这和普通的 svc 指令有啥区别?

区别大了。SVC 是给操作系统用的系统调用,而 SMC 是专门给 Secure Monitor 用的。我在项目中见过有人把 SMC 当 SVC 用,结果直接挂死——因为 EL3 的异常向量表根本没注册 SVC 处理函数。

关键点:SMC 指令只能在 EL1 或 EL2 执行,不能在 EL0(用户态)直接调用。如果非要在用户态发 SMC,系统会触发一个未定义指令异常。

SMC 指令的格式很简单:

// AArch64 语法
SMC #0x0       // 标准 SMC 调用
SMC #0x1       // 带 fast call 标识(某些实现用)

// ARM 32位语法
SMC #0         // 立即数通常为 0

嗯,这里要注意:SMC 的立即数在标准规范里其实没被广泛使用,大部分实现都直接传 #0。真正的功能区分靠的是寄存器里的参数,而不是指令本身的立即数。

3.2 SMC 调用约定(ABI):谁传参,谁收尾

SMC 调用约定,说白了就是一套“谁负责传什么、谁负责清理”的规则。ARM 官方定义了 SMCCC(SMC Calling Convention),目前主流版本是 SMCCC v1.2。

我习惯把 SMCCC 的核心规则总结成一张表:

寄存器 方向 用途
X0 - X7 输入/输出 参数传递(最多 8 个 64 位参数)
X0 输出 函数返回值(错误码或结果)
X1 - X7 输出 返回值扩展(可选)
X8 - X17 暂存 被调用者可自由使用,调用者需保存
X18 - X30 保留 平台特定或调用者保存

你想想看,为什么最多只传 8 个参数?因为 ARM 的异常处理机制在陷入 EL3 时,硬件只保证保存 X0-X7 到 CPU context 里。超过 8 个参数就得走内存栈,那性能就大打折扣了。

我的经验:在实际项目中,我建议把 SMC 参数控制在 4 个以内。超过 4 个时,ATF 的上下文保存/恢复开销会明显增加,尤其是在高频调用的场景下(比如每秒几千次的安全中断处理)。

SMCCC 还定义了两种调用类型:

  • Fast Call(快速调用):X0 的 bit31 = 1。调用者会自旋等待,不产生调度。适合短操作。
  • Yield Call(让出调用):X0 的 bit31 = 0。调用者可能被挂起,安全世界可以调度其他任务。适合长操作。

我记得有一次调试一个 TEE 驱动,发现 SMC 调用总是超时。查了半天,原来是驱动把 Yield Call 当 Fast Call 用了——安全世界那边做了个 5 秒的加解密操作,普通世界这边却自旋等待,直接把 CPU 占死了。

3.3 切换流程:从 Non-Secure 到 Secure 的完整路径

好,现在我们把 SMC 指令、调用约定都搞清楚了,接下来看看整个切换流程到底怎么走。

我画了一张流程图,把关键步骤都标出来了:

SMC 调用完整流程 SMC 调用:从 Non-Secure 到 Secure 的切换流程 Non-Secure World EL1/EL2 执行 SMC 指令 异常陷入 EL3 异常向量表 查找 SMC 处理入口 路由 SMC 分发器 解析 Function ID 上下文保存 保存 X0-X7, SP, ELR, SPSR Secure World 处理 TEE/SP 执行具体功能 上下文恢复 恢复 X0-X7, SP, ELR, SPSR ERET 返回 回到 Non-Secure 下一条指令 完成返回 ⚠ 注意: 上下文保存/恢复由 ATF 的 runtime_exceptions.S 自动完成,开发者只需关注 X0-X7 的参数传递。

整个流程可以拆成 7 个关键步骤:

  1. 发起调用:Non-Secure 世界的代码(比如 Linux 内核驱动)把参数塞进 X0-X7,然后执行 SMC #0。
  2. 异常陷入:CPU 硬件自动切换到 EL3,从异常向量表里找到 SMC 对应的入口。ATF 里这个入口叫 sync_exception_sp_elx。
  3. 分发处理:ATF 的 smc_handler 解析 X0 里的 Function ID,判断是标准服务(如 PSCI)、TEE 服务还是自定义服务。
  4. 上下文保存:ATF 把调用者的寄存器上下文保存到当前 CPU 的 cpu_context 结构体中。这一步是自动的,但你要知道它存在。
  5. 安全世界执行:ATF 切换到 Secure World 的上下文,跳转到对应的 SP(Secure Partition)或 TEE 处理函数。
  6. 上下文恢复:安全世界处理完后,ATF 把之前保存的上下文恢复回来,把返回值写进 X0。
  7. ERET 返回:执行 ERET 指令,CPU 切回 Non-Secure 世界,继续执行 SMC 的下一条指令。

我曾经踩过的坑:有一次我在 ATF 里加了一个自定义 SMC 服务,结果每次调用都死机。查了两天才发现——我在安全世界处理函数里修改了 X1 寄存器,但 ATF 的上下文恢复机制默认只恢复 X0。后来我手动在 smc_handler 里加了 X1 的保存/恢复逻辑才搞定。

所以记住:如果你在安全世界修改了 X0 以外的寄存器,并且希望调用者能看到变化,必须在 ATF 的上下文保存/恢复代码里显式处理。

3.4 实战:一个简单的 SMC 调用示例

光说不练假把式。我们来看一个实际的 SMC 调用代码片段,感受一下整个流程:

// Non-Secure 侧(Linux 内核驱动)
static int tee_smc_call(uint64_t arg0, uint64_t arg1, uint64_t *result)
{
    register uint64_t x0 asm("x0") = arg0;
    register uint64_t x1 asm("x1") = arg1;
    register uint64_t x2 asm("x2") = 0;
    register uint64_t x3 asm("x3") = 0;

    // 执行 SMC 调用
    asm volatile(
        "SMC #0"
        : "+r" (x0), "+r" (x1)
        : "r" (x2), "r" (x3)
        : "memory"
    );

    *result = x0;
    return (int)x1;  // 返回状态码
}

// Secure 侧(ATF 或 TEE)
uint64_t tee_smc_handler(uint64_t x0, uint64_t x1, 
                         uint64_t x2, uint64_t x3)
{
    // 解析 Function ID
    uint32_t fid = (uint32_t)(x0 >> 32);
    
    switch (fid) {
    case 0x82000001:  // 自定义服务
        return tee_do_work(x1, x2);
    default:
        return SMC_UNKNOWN;  // 未知服务
    }
}

这段代码看起来简单,但有几个细节值得注意:

  • asm volatile 里的 "+r" 表示寄存器既输入又输出,这样编译器不会优化掉返回值。
  • Function ID 的高 16 位是服务标识,低 16 位是具体功能号。0x82000000 系列是标准 TEE 服务。
  • 安全世界的处理函数必须保证不破坏 X0-X7 以外的寄存器,否则返回后 Non-Secure 世界会崩溃。

我的建议:在开发阶段,可以在 ATF 的 SMC 分发器里加一段日志,打印每次调用的 Function ID 和参数。这样调试起来会轻松很多。ATF 的 VERBOSE_PRINT 宏就是干这个的。

3.5 避坑指南:SMC 调用的常见问题

最后,我把自己这些年遇到的 SMC 相关坑整理一下,希望能帮你少走弯路:

问题 现象 原因 解决方案
SMC 调用后系统重启 执行 SMC 后直接复位 EL3 异常向量表未正确注册 检查 ATF 的 runtime_svc_init 是否注册了服务
返回值不对 X0 返回了奇怪的值 安全世界修改了 X0 但 ATF 未正确恢复 检查上下文保存/恢复逻辑
调用卡死 CPU 在 SMC 后不返回 Yield Call 被当作 Fast Call 处理 确认 X0 的 bit31 设置正确
性能骤降 SMC 调用耗时异常高 上下文保存/恢复开销过大 减少参数数量,使用 Fast Call

嗯,SMC 调用机制这块内容就讲到这里。记住一句话:SMC 是门,ABI 是规矩,切换流程是路。门对了、规矩懂了、路熟了,你就能在 Non-Secure 和 Secure 世界之间自由穿梭。