一、底层根基:ARMv8 异常模型是线程的硬件土壤
1.1 两条铁则
- 特权级上升只能靠异常:从低特权级到高特权级(比如 S-EL0→S-EL1、S-EL1→EL3),只能通过异常触发(SVC系统调用、SMC、中断、中止),硬件自动保存现场、切换栈、跳转到异常向量表。
- 特权级下降只能靠 ERET:从高特权级到低特权级,只能通过 ERET 异常返回指令,硬件自动从 ELR_ELx / SPSR_ELx 恢复程序指针和状态,完成跳转。
1.2 对线程的意义
- 线程切出:异常发生时,硬件自动保存当前线程的 PC/PSR/栈,软件再补全保存所有寄存器,形成完整快照。
- 线程切入:手动把目标线程的快照写入异常上下文,执行 ERET,硬件会“误以为”是从异常中返回,自动跳转到目标线程的代码。
二、核心数据结构:双维度分层设计
2.1 维度一:Per-CPU 核心状态thread_core_local
- curr_thread:当前 CPU 上正在运行的线程 ID,无效时为 THREAD_ID_INVALID。
- flags:运行模式标志,区分临时栈模式、异常模式、正常线程模式,决定当前使用哪套栈。
- tmp_stack_va_end / abt_stack_va_end:CPU 私有临时栈、异常栈的栈底(高地址)。
- 扩展字段:PAUTH 密钥、栈检查递归标志、漏洞缓解参数等。
2.2 维度二:全局线程池threads[]
- 寄存器上下文regs:完整的硬件状态快照(x0~x30、PC、CPSR、PAUTH 密钥等),是线程切换的核心载体。
- 私有栈stack_va_end:每个线程独立的运行栈,线程间完全隔离。
- 状态机state:标记线程当前处于空闲/活跃/挂起状态。
- 地址空间user_map:用户态 TA 线程的独立页表,实现 TA 间内存隔离。
- 浮点状态vfp_state:VFP/NEON 寄存器状态,采用懒加载策略。
- 附属资源:会话栈、共享内存缓存、线程私有数据(TSD)等。
2.3 设计意义
- 动静分离:CPU 私有数据高频访问、无锁操作;全局线程池低频修改、加锁保护,兼顾性能与多核安全。
- 静态池化:启动时一次性创建所有线程,运行时只做状态流转,不动态申请销毁,避免堆内存漏洞,内存布局完全可审计,符合可信系统的安全要求。
三、栈体系:三级栈架构 + 多层溢出防护
3.1 三级栈分工
栈类型 | 归属 | 硬件栈指针 | 作用 | 切换时机 |
异常栈 abt_stack | 每个 CPU 1 份 | SP_EL1 | 异常/中断发生时,硬件自动切换到该栈,保存异常上下文、执行异常底半部逻辑 | 异常进入硬件自动切换,异常退出恢复 |
临时栈 tmp_stack | 每个 CPU 1 份 | SP_EL0 | 无线程绑定场景的内核运行栈:RPC 返回初期、线程释放后、CPU 刚上电时使用 | 线程释放、RPC 返回时切换 |
线程栈 thread_stack | 每个线程 1 份 | SP_EL0 | 业务线程正常运行时的私有栈,承载内核逻辑、TA 调用栈 | 线程恢复时切换,线程挂起时释放 |
核心设计价值
- 异常与业务隔离:异常栈独立于业务线程栈,即使线程栈溢出被攻破,也不会破坏异常处理流程,保证安全兜底能力始终可用。
- 无线程场景兜底:RPC 返回、CPU 热启动时还没有绑定业务线程,临时栈保证内核代码能正常运行,完成线程恢复前的准备工作。
- 线程间硬隔离:每个线程拥有独立栈,切换线程同步切换栈,从硬件层面保证线程间栈数据不可见、不可篡改。
3.2 栈安全防护体系
- 守护页(Guard Page):栈的低地址侧设置一页不可访问内存,栈溢出时首先触发访问中止异常,在破坏有效数据前就被捕获。
- 首尾金丝雀(Stack Canary):栈的首尾两端写入魔法值,关键路径(线程切换、异常入口、系统调用)调用 thread_check_canaries() 校验,值被改写则直接 panic。支持运行时用硬件真随机数更新金丝雀,防止预测绕过。
- 软边界检查:栈内部预留一段检查区,编译器函数插桩(-finstrument-functions)可在每个函数入口校验栈指针是否越界,提前发现溢出风险。
- 栈深度审计:支持栈使用量统计与打印,用于调试与安全审计。
3.3 栈的内存布局
低地址 <----------------------------------- 高地址 [ 守护页 | 前金丝雀 | 检查预留区 | 栈主体 | 后金丝雀 ] 硬栈顶 软栈顶 栈底四、生命周期:线程状态机与流转逻辑
4.1 状态流转全景
空闲(FREE) —— 分配启动 ——> 活跃(ACTIVE) ^ | | | | 释放回收 | 主动挂起(RPC/中断) | | +———— 恢复执行 <———— 挂起(SUSPENDED)4.2 场景1:线程创建与首次启动
执行流程
1.分配空闲线程:加锁遍历全局线程池,找到第一个空闲线程,标记为活跃态,解锁。
2.构造初始上下文:调用 init_regs() 手动填充线程的寄存器快照,这是典型的“伪造异常现场”:
- pc 设为线程入口函数(对应 ELR_EL1)
- cpsr 设为 S-EL1 + SP_EL0 + 屏蔽外部中断(对应 SPSR_EL1)
- sp 设为线程私有栈栈底
- 入参写入 x0~x7,线程启动后直接读取
- 帧指针 x29 清零,作为栈回溯终止点
3.安全初始化:浮点懒保存标记初始化,PAUTH 密钥写入上下文。
4.启动执行:调用汇编函数 thread_resume(),把上下文加载到 CPU 寄存器,执行 ERET 指令,硬件自动跳转到线程入口,线程正式进入活跃态。
4.3 场景2:线程挂起
执行流程
- 前置安全校验:调用 thread_check_canaries() 检查栈金丝雀,确保栈没有溢出后再执行上下文保存。
- 资源回收:释放线程栈未使用的物理页(页交换模式下),节省安全内存;保存用户态浮点状态。
- 保存完整现场:把当前的 PC、CPSR(异常发生时硬件自动保存的值)写入线程上下文结构体,保证恢复后能无缝续接。
- 地址空间保存:如果是用户态 TA 线程,保存当前用户页表,然后切回内核全局地址空间,防止后续内核代码访问到 TA 私有内存。
- 状态变更:加锁把线程状态改为挂起,当前 CPU 的 curr_thread 置为无效,解锁。
- 切回临时栈:后续内核代码使用 CPU 临时栈运行,线程栈彻底脱离当前执行流。
4.4 场景3:线程恢复
执行流程
1.合法性校验:加锁校验线程 ID 合法、且状态为挂起,校验通过后改为活跃态,解锁。这一步防止恶意传入非法线程号触发越界访问。
2.CPU 绑定:把线程 ID 写入当前 CPU 的 curr_thread,完成 CPU 与线程的绑定。
3.环境恢复:
- 用户态 TA 线程:恢复用户地址空间,恢复运行时间统计、函数跟踪。
- 浮点状态:执行懒保存初始化,后续用到浮点时再真实加载寄存器。
4.参数回写(条件执行):只有正常 RPC 返回才会把非安全世界的返回值写入线程上下文的 x0~x3;中断抢占导致的挂起,绝对不允许拷贝参数,防止非安全世界通过中断注入恶意数据。
5.执行恢复:清除临时栈标志,调用 thread_resume() 加载线程上下文,执行 ERET 回到线程挂起的位置继续执行。
4.5 场景4:线程销毁
- 恢复非安全世界浮点状态,释放线程栈物理内存。
- 加锁把线程状态改回空闲,标志位清零,CPU 解绑。
- 解锁后线程回到空闲态,可被下次分配复用。
五、核心机制:上下文切换的底层原理
5.1 切换的统一范式
- 切出时机:SMC 调用、SVC 系统调用、外部中断、数据中止等异常发生后,在异常处理函数中执行。
- 切入时机:异常返回前,通过修改上下文、执行 ERET 完成。
5.2 浮点寄存器的懒加载策略
- 线程切换时,只标记浮点状态,不真实保存/恢复寄存器,同时关闭浮点单元。
- 如果后续线程不使用浮点指令,全程零额外开销。
- 如果线程执行浮点指令,会触发未定义指令异常,此时在异常处理中:
- 保存上一个线程的浮点上下文
- 加载当前线程的浮点上下文
- 开启浮点单元,返回继续执行
用“罕见的异常开销”替换“每次切换都全量保存的固定开销”,是嵌入式安全 OS 的经典优化。
5.3 用户态 TA 的特权级切换
- 进入用户态:构造 S-EL0 的异常返回现场,ERET 从 S-EL1 降到 S-EL0,同时切换到用户栈。进入前会清零所有未使用的寄存器,防止内核敏感信息泄露到 TA。
- 陷回内核:TA 执行 SVC 指令触发系统调用,硬件自动升到 S-EL1,切换到异常栈,保存用户态上下文,内核处理完再 ERET 返回。
六、多核同步:极简粗粒度锁设计
- 一把全局自旋锁:仅用一把 thread_global_lock 保护整个线程池的状态修改。虽然粒度偏粗,但线程分配释放属于低频操作,锁持有时间极短,性能影响可忽略;同时代码简单、审计难度低,出漏洞的概率远低于精细粒度锁。
- Per-CPU 数据无锁访问:关闭外部中断后,CPU 私有数据可直接访问,无需加锁,保证异常处理等高频路径的性能。
- 自旋锁选型:EL1 特权级下无法睡眠调度,只能使用自旋锁;且锁变量放在常驻内存段,不会被页交换换出,避免加锁时触发缺页异常。
七、安全设计:TEE 线程与通用 OS 的核心区别
7.1 全链路栈溢出防护
7.2 严格的边界参数校验
- 线程 ID、状态双重校验,防止非法索引触发内存越界。
- RPC 返回参数条件拷贝:只有正常 RPC 场景才允许写入参数,中断抢占场景禁止写入,阻断非安全世界通过中断注入恶意数据的攻击路径。
- 进入用户态前清零寄存器,避免内核敏感信息泄露到用户态 TA。
7.3 线程级安全特性隔离
- 每个 TA 线程拥有独立地址空间、独立 PAUTH 密钥、独立浮点上下文,线程间完全沙箱隔离,单个 TA 被攻破不会影响其他 TA 和内核。
- 安全特性(MTE、PAUTH、BTI)在线程启动时就生效,从执行流第一条指令开始就处于防护状态。
7.4 失败即终止的防御策略
八、设计思想总结
- 协作式调度,事件驱动:没有时间片抢占,线程切换只发生在异常点,调度逻辑极简、可审计性强,符合 TEE 高可靠要求。
- 静态池化,拒绝动态:线程、栈启动时一次性分配,运行时只做状态流转,彻底杜绝堆内存漏洞风险。
- 分层隔离,纵深防御:CPU 与线程隔离、内核与用户隔离、栈与栈之间隔离,每层都有独立防护,构建纵深防御体系。
- 安全优先,性能为辅:所有优化(懒加载、共享内存缓存)都以不破坏安全为前提;当安全与性能冲突时,永远安全优先。
- 贴合硬件,最小化 TCB:所有机制都基于 ARM 硬件原语实现,尽量少做软件黑魔法,代码越少、越简单,可信计算基就越小,安全性就越高。