4. 硬件初始化阶段:CPU复位向量、MMU与Cache初始化、异常向量表配置、时钟与电源管理初始化

硬件初始化,是整个TEE启动流程的基石。说白了,就是给CPU“通电叫醒”,然后告诉它该怎么跑。这一步要是没做好,后面所有软件都是空中楼阁。我见过不少团队,在移植TEE时卡在最开始的几百行汇编里,就是因为对硬件初始化理解不够透彻。

咱们今天就把这四大块掰开揉碎了讲:CPU复位向量、MMU与Cache、异常向量表、时钟与电源管理。每一块都有坑,我会把我在项目中踩过的坑也一并告诉你。

4.1 CPU复位向量:系统启动的第一行代码

CPU上电后,PC指针会指向一个固定的地址——复位向量。ARM Cortex-A系列通常是0x00000000或0xFFFF0000,具体取决于硬件设计。RISC-V则通常是0x80000000附近。

这里放的是什么?是一条跳转指令,通常是b reset_handler或ldr pc, =reset_handler。为什么不能直接放代码?因为复位向量只有4字节空间,放不下初始化逻辑。

核心要点:复位向量处必须是一条无条件跳转指令,跳转到真正的初始化入口。不要在这里放任何数据或NOP填充。

我个人习惯,在复位向量之后紧接着放一个异常向量表(后面会细讲),这样整个启动代码结构清晰,也方便调试。

; ARM Cortex-A53 复位向量示例
.section .vectors, "ax"
.global _start

_start:
    b       reset_handler        ; 复位
    b       undef_handler        ; 未定义指令
    b       svc_handler          ; 超级用户调用
    b       pabt_handler         ; 预取中止
    b       dabt_handler         ; 数据中止
    nop                          ; 保留
    b       irq_handler          ; IRQ中断
    b       fiq_handler          ; FIQ中断

reset_handler:
    ; 关闭中断
    mrs     r0, cpsr
    bic     r0, r0, #0x1F
    orr     r0, r0, #0xD3       ; SVC模式,IRQ/FIQ关闭
    msr     cpsr, r0
    ; 后续初始化...

经验之谈:我曾经在一个项目中,复位向量处不小心放了一个16位Thumb指令,结果CPU在AArch32状态下直接跑飞。排查了整整两天才发现是字节对齐问题。记住:复位向量必须4字节对齐,且是32位ARM指令。

4.2 MMU与Cache初始化:从“裸奔”到“有组织”

刚启动时,MMU是关闭的,CPU直接访问物理地址。Cache也是关闭的,每次访存都走总线。这时候性能极差,但胜在简单、确定性强。

为什么要初始化MMU?因为TEE需要虚拟地址空间隔离。你想想看,如果没有MMU,Normal World和Secure World的地址空间完全重叠,怎么隔离?

初始化步骤大致如下:

  1. 建立页表——通常是一级页表(L1),放在SRAM或DDR的固定位置
  2. 设置TTBR0/TTBR1——告诉MMU页表基地址
  3. 配置MAIR——内存属性寄存器,定义不同区域的Cache策略
  4. 使能MMU和Cache——设置SCTLR的M位和C位

注意:使能MMU时,必须保证当前代码所在的虚拟地址与物理地址映射一致。否则使能MMU的瞬间,PC指针指向的地址会变成无效虚拟地址,直接触发异常。我见过有人在这里加了一个dsb和isb之后还是挂,最后发现是页表项里没有配置可执行权限。

Cache初始化也有讲究。我个人建议先使能指令Cache(ICache),再使能数据Cache(DCache)。因为指令Cache不需要考虑一致性问题,风险小。数据Cache则要小心——如果之前BootROM写过一些数据到DDR,使能DCache后可能会读到脏数据。

; 使能MMU和Cache示例(ARMv8-A)
mrs     x0, sctlr_el1
orr     x0, x0, #(1 << 0)    ; M位:使能MMU
orr     x0, x0, #(1 << 2)    ; C位:使能DCache
orr     x0, x0, #(1 << 12)   ; I位:使能ICache
msr     sctlr_el1, x0
dsb     sy
isb
寄存器 位域 功能 初始化值建议
SCTLR_EL1 [0] M MMU使能 最后一步置1
SCTLR_EL1 [2] C DCache使能 页表建立后置1
SCTLR_EL1 [12] I ICache使能 可提前置1
TTBR0_EL1 [47:1] 页表基地址 页表物理地址
MAIR_EL1 [7:0] 内存属性0 0xFF (Normal, WB, RA, WA)

4.3 异常向量表配置:给CPU一张“紧急联络图”

异常向量表,就是CPU发生异常时该去哪里找处理函数。TEE里异常处理尤其重要——你想想看,Secure Monitor Call(SMC)就是通过异常机制实现的。

ARMv8-A的异常向量表分四组:

  • EL3异常——最高权限,处理SMC和FIQ
  • EL2异常——虚拟化扩展
  • EL1异常——操作系统和TEE内核
  • EL0异常——用户态

配置异常向量表,核心就是设置VBAR_EL3、VBAR_EL2、VBAR_EL1这几个寄存器。每个寄存器指向一个2KB对齐的向量表基地址。

关键点:异常向量表必须2KB对齐。如果不对齐,CPU在取向量时会忽略低11位,导致跳转到错误地址。我曾经在调试时发现SMC总是进不了正确的处理函数,最后用JTAG一看,VBAR_EL3的地址低11位不是0——对齐问题。

每个异常向量条目占16字节(4条指令)。为什么是16字节?因为ARM设计者希望你在向量里直接放处理逻辑,而不是再跳转一次。但实际项目中,我们通常还是放一条跳转指令,因为16字节不够写完整处理流程。

; VBAR_EL3 指向的异常向量表示例
.align 11   ; 2KB对齐
vectors_el3:
    ; Current EL, SP0
    b       sync_cur_sp0        ; 同步异常
    b       irq_cur_sp0         ; IRQ
    b       fiq_cur_sp0         ; FIQ
    b       serr_cur_sp0        ; SError
    ; Current EL, SPx
    b       sync_cur_spx
    b       irq_cur_spx
    b       fiq_cur_spx
    b       serr_cur_spx
    ; Lower EL, AArch64
    b       sync_lower_a64
    b       irq_lower_a64
    b       fiq_lower_a64
    b       serr_lower_a64
    ; Lower EL, AArch32
    b       sync_lower_a32
    b       irq_lower_a32
    b       fiq_lower_a32
    b       serr_lower_a32

我的习惯:在向量表里,每个条目第一行放一个stp保存上下文,第二行再跳转。这样即使处理函数写错了,也能从保存的上下文里找到异常发生时的寄存器状态。调试效率高很多。

4.4 时钟与电源管理初始化:让系统“活”起来

时钟和电源管理,是硬件初始化的“后勤保障”。没有时钟,CPU不工作;电源没配好,外设不响应。

TEE启动时,时钟初始化通常包括:

  • 配置系统PLL——给CPU、总线、DDR提供稳定时钟
  • 使能外设时钟——比如UART、Timer、GIC
  • 设置时钟分频——确保各模块工作在额定频率

电源管理则更复杂一些。TEE需要确保Secure World的电源域独立可控,防止Normal World通过电源管理接口攻击安全区域。

避坑指南:我曾经在一个项目中,时钟初始化时先使能了DDR控制器时钟,但DDR PHY的时钟还没稳定。结果DDR初始化失败,整个系统卡在内存检测阶段。正确的顺序应该是:先配PLL→等锁相环稳定→使能PHY时钟→再使能控制器时钟。顺序错了,系统就挂了。

电源管理方面,TEE通常需要做这几件事:

  1. 确认Secure电源域已上电——检查电源状态寄存器
  2. 配置电源管理控制器——设置唤醒源和电源策略
  3. 关闭不必要的电源域——降低功耗,同时减少攻击面

这里我画了一张图,把整个硬件初始化的流程串起来,方便你理解各模块之间的依赖关系。

硬件初始化流程 CPU复位向量 时钟与电源管理初始化 异常向量表配置 MMU与Cache初始化 硬件初始化完成 注意:时钟必须在MMU之前初始化,因为MMU配置需要稳定的时钟源 异常向量表配置后建议立即测试一次SMC,验证向量跳转是否正确

4.5 初始化顺序的“潜规则”

上面讲的四个模块,初始化顺序是有讲究的。我总结了一个顺序原则:

顺序 模块 为什么排这里
1 复位向量 CPU上电第一件事,没得选
2 时钟与电源 没有时钟,其他模块无法工作
3 异常向量表 初始化过程中可能发生异常,需要提前准备好处理函数
4 MMU与Cache 依赖时钟稳定,且异常向量表就绪后可以安全处理缺页异常

个人建议:在MMU使能之前,最好加一段自检代码——验证异常向量表是否2KB对齐、页表是否在可访问的物理地址范围内。这些检查只需要几十条指令,但能帮你省下几天的调试时间。我在项目中吃过亏,后来就养成了这个习惯。

嗯,硬件初始化这块内容就讲到这里。每一步都有它的道理,也有它的坑。你只要按照这个顺序来,配合我提到的那些注意事项,基本上不会出大问题。记住:硬件初始化不是写业务逻辑,它需要你对芯片手册了如指掌。遇到不确定的寄存器,别猜,去翻Reference Manual。