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的地址空间完全重叠,怎么隔离?
初始化步骤大致如下:
- 建立页表——通常是一级页表(L1),放在SRAM或DDR的固定位置
- 设置TTBR0/TTBR1——告诉MMU页表基地址
- 配置MAIR——内存属性寄存器,定义不同区域的Cache策略
- 使能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通常需要做这几件事:
- 确认Secure电源域已上电——检查电源状态寄存器
- 配置电源管理控制器——设置唤醒源和电源策略
- 关闭不必要的电源域——降低功耗,同时减少攻击面
这里我画了一张图,把整个硬件初始化的流程串起来,方便你理解各模块之间的依赖关系。
4.5 初始化顺序的“潜规则”
上面讲的四个模块,初始化顺序是有讲究的。我总结了一个顺序原则:
| 顺序 | 模块 | 为什么排这里 |
|---|---|---|
| 1 | 复位向量 | CPU上电第一件事,没得选 |
| 2 | 时钟与电源 | 没有时钟,其他模块无法工作 |
| 3 | 异常向量表 | 初始化过程中可能发生异常,需要提前准备好处理函数 |
| 4 | MMU与Cache | 依赖时钟稳定,且异常向量表就绪后可以安全处理缺页异常 |
个人建议:在MMU使能之前,最好加一段自检代码——验证异常向量表是否2KB对齐、页表是否在可访问的物理地址范围内。这些检查只需要几十条指令,但能帮你省下几天的调试时间。我在项目中吃过亏,后来就养成了这个习惯。
嗯,硬件初始化这块内容就讲到这里。每一步都有它的道理,也有它的坑。你只要按照这个顺序来,配合我提到的那些注意事项,基本上不会出大问题。记住:硬件初始化不是写业务逻辑,它需要你对芯片手册了如指掌。遇到不确定的寄存器,别猜,去翻Reference Manual。