手撕ARM64启动栈(八):QEMU + TF-A(ATF) OP-TEE 初始化详解
系列文章目录
手撕ARM64启动栈(一):QEMU + TF-A → OP-TEE → U-Boot → Linux 全链路总览
手撕ARM64启动栈(二):QEMU + TF-A(ATF) BL1 执行上下文——GDB 实测复位到 BL2
手撕ARM64启动栈(三):QEMU + TF-A(ATF) BL1 核心职责——bl1_main()逐函数拆解与 BL2 加载
手撕ARM64启动栈(四):QEMU + TF-A(ATF) BL2 详解
手撕ARM64启动栈(五):QEMU + TF-A(ATF) BL32/OP-TEE 镜像头解析——三模式判定与入口改写
手撕ARM64启动栈(六):QEMU + TF-A(ATF) BL31 运行时服务框架与 opteed 注册
手撕ARM64启动栈(七):QEMU + TF-A(ATF) BL31 世界切换
手撕ARM64启动栈(八):QEMU + TF-A(ATF) OP-TEE 初始化详解
文章目录
- 系列文章目录
- 1. 引言
- 2. 初始化总览
- 3. 入口:刚进 OP-TEE 时还缺什么
- 4. 建 Secure 地址空间
- 5. 四阶段初始化:early → late → runtime → final
- 5.1 early:装上 S-EL0 → S-EL1 异常入口
- 5.2 late:改写外部 DT,告诉 Linux 哪些内存不能碰
- 5.3 runtime:准备 TA 运行时
- 5.4 final:收尾并准备返回 Normal World
- 6. 交还 BL31:登记 SMC 入口表
- 7. 小结
1. 引言
上一篇末尾,BL31 通过el3_exit()执行ERET,CPU 从 EL3 进入 Secure World 的 S-EL1,PC指向 OP-TEE 的_start。
本篇关注OP-TEE 初始化的主干:它如何把自己从一个刚被 BL31 拉起的 Secure 镜像,初始化成一个能被 Normal World 通过 SMC 调用的 TEE 服务端。
涉及关键词:OP-TEE、TrustZone、Secure World、S-EL1、S-EL0、SMC、entry_a64.S、core_init_mmu_map、NSEC_SHM、reserved-memory、thread_excp_vect、el0_svc、thread_vector_table。
2. 初始化总览
OP-TEE 初始化主线可以压成五件事:
BL31 ERET 到 OP-TEE → OP-TEE 建自己的 Secure 地址空间 → 划清 Secure 私有内存和 Non-secure 共享内存 → 改写 Normal World DT,让 Linux 知道 OP-TEE 的内存和 SMC 入口 → 准备 S-EL0 TA 的 syscall/返回路径和 TEE runtime → 把 SMC 入口表交给 BL31,自己休眠待命3. 入口:刚进 OP-TEE 时还缺什么
入口文件:optee_os/core/arch/arm/kernel/entry_a64.S。
BL31ERET落到_start时,CPU 已经在Secure World / S-EL1,但 OP-TEE 自己的运行时环境还没建好:S-EL1 MMU 和正式 VA 映射还没启用,线程池不存在,VBAR_EL1也还不是运行时向量。_start会先把VBAR_EL1设为reset_vect_table临时表,到early 阶段才换成thread_excp_vect。
_start设置临时VBAR_EL1的代码可以参考这张图:
_start的启动动作可以压成这几步:
- 保存 BL31 传进来的 handoff 参数,重点是外部 DTB 地址。
- 安装临时
reset_vect_table。这张表只兜底,不是真正的optee 运行时异常向量入口。 - 清
.bss、切临时栈。 - 调
console_init()、boot_save_args()、boot_mem_init()。 - 建页表、开 MMU,进入 C 侧
early → late → runtime → final四阶段。
这里关键的外部 DTB:后面 Linux 看到的reserved-memory和/firmware/optee,都是 OP-TEE 在初始化期间改写这份 DTB 得来的。
4. 建 Secure 地址空间
core_init_mmu_map()建 OP-TEE 自己的 VA/PA 映射,日志里三类内存值得关注:
| 区域 | 含义 |
|---|---|
TEE_RAM_RX/TEE_RAM_RW | OP-TEE core 的代码/数据,Secure World 私有 |
SEC_RAM_OVERALL | OP-TEE 可用的 Secure RAM 总范围 |
NSEC_SHM | 静态 Non-secure 共享内存,两边通过它交换消息和参数 |
关键日志:
D/TC:0 add_phys_mem:798 VCORE_UNPG_RX_PA type TEE_RAM_RX 0x0e100000 size 0x00105000 D/TC:0 add_phys_mem:798 VCORE_UNPG_RW_PA type TEE_RAM_RW 0x0e205000 size 0x00049000 D/TC:0 add_phys_mem:798 VCORE_UNPG_RW_PA type SEC_RAM_OVERALL 0x0e205000 size 0x00049000 D/TC:0 add_phys_mem:798 TEE_SHMEM_START type NSEC_SHM 0x42000000 size 0x00200000 D/TC:0 init_mem_map:1546 Mapping core at 0xb2fd3000 offs 0xa4ed3000 D/TC:0 dump_mmap_table:968 type NSEC_SHM va 0xad800000..0xad9fffff pa 0x42000000..0x421fffff size 0x00200000 D/TC:0 dump_mmap_table:968 type SEC_RAM_OVERALL va 0xada05000..0xae7fffff pa 0x0e205000..0x0effffff size 0x00dfb000 D/TC:0 dump_mmap_table:968 type TEE_RAM_RX va 0xb2fd3000..0xb30d7fff pa 0x0e100000..0x0e204fff size 0x00105000 D/TC:0 dump_mmap_table:968 type TEE_RAM_RW va 0xb30d8000..0xb31d2fff pa 0x0e205000..0x0e2fffff size 0x000fb000在 MMU 初始化阶段,用 GDB 查看mem_map,可以看到如下内存布局:
5. 四阶段初始化:early → late → runtime → final
MMU 开启后,OP-TEE 按固定顺序跑完四阶段 C 初始化。
5.1 early:装上 S-EL0 → S-EL1 异常入口
入口:boot_init_primary_early()。
关键动作:
thread_init_canaries() thread_init_per_cpu() thread_init_vbar(get_excp_vect())thread_init_vbar()把VBAR_EL1从临时reset_vect_table换成运行时向量thread_excp_vect。
这和 TA svc call直接相关:跑在S-EL0的TA 调TEE_*API 时执行svc,CPU 从 S-EL0 陷入 S-EL1,入口就是thread_excp_vect里的el0_svc路径。
所以 early 阶段结束时,S-EL0 系统调用通道已经准备好。
本阶段的日志标志:init_primary()建好 malloc 池后打一条IMSG_RAW("\n"),在日志里是一行**I/TC:**——early 阶段的分界线,前面是第 4 节的映射表输出,后面进 late 阶段:
D/TC:0 core_mmu_xlat_table_alloc:700 xlat tables used 7 I/TC: ← early 阶段的分界 D/TC:0 carve_out_core_mem:2816 0xe100000 .. 0xe300000 D/TC:0 boot_mem_release_unused:294 Allocated 41280 bytes at va 0xb3121000 pa 0xe24e000 D/TC:0 boot_mem_release_unused:298 Tempalloc 33864 bytes at va 0xb31cabb8 D/TC:0 boot_mem_release_unused:320 Carving out 0xe100000..0xe25813f D/TC:0 boot_mem_release_unused:329 Releasing 650944 bytes from va 0xb312c000这里可以思考下,为什么不一开始就使用“thread_excp_vect”?临时向量存在的意义是什么?
5.2 late:改写外部 DT,告诉 Linux 哪些内存不能碰
入口:boot_init_primary_late()。
这是本篇最重要的一段。OP-TEE 会解析 Normal World 的外部 DT,然后把安全边界写回这份 DT:
- 写
reserved-memory:标出optee_core,让 Linux 避开 OP-TEE 私有 Secure RAM。 - 写
reserved-memory:标出optee_shm,让 Linux 把静态共享内存交给optee驱动管理,而不是当普通页分配。 - 写
/firmware/optee:compatible = "linaro,optee-tz",method = "smc",让 Linux 能匹配optee驱动并使用 SMC conduit。 - 记录外部 DT 里的 Non-secure DDR 范围,后续动态共享内存注册时用来判断物理页是否合法。
在discover_nsec_memory()(改写前)和configure_console_from_dt()(改写后)两处下断点,各 dump 一次 DT 转成文本比较,能看到 OP-TEE 实际插入的内容:
+ reserved-memory { + ranges; + #size-cells = <0x02>; + #address-cells = <0x02>; + + optee_core@e100000 { + no-map; + reg = <0x00 0xe100000 0x00 0xf00000>; + }; + + optee_shm@42000000 { + no-map; + reg = <0x00 0x42000000 0x00 0x200000>; + }; + }; + + firmware { + optee { + interrupts = <0x00 0xbb 0x01>; + method = "smc"; + compatible = "linaro,optee-tz"; + }; + };optee_core的reg是0xe100000起0xf00000(15MB);optee_shm的reg精确对应第 4 节的NSEC_SHM(0x42000000起 2MB)。
对应日志:
I/TC: Non-secure external DT found D/TC:0 discover_nsec_memory:158 Non-secure memory found in extern DT D/TC:0 core_mmu_set_discovered_nsec_ddr:540 0: pa 0x40000000..0x41ffffff sz 0x2000000 D/TC:0 core_mmu_set_discovered_nsec_ddr:540 1: pa 0x42200000..0x820fffff sz 0x3ff00000 I/TC: Embedded DTB found两段 Non-secure DDR 中间缺掉的0x42000000..0x421fffff,就是前面NSEC_SHM的 2MB 静态共享内存。
late 阶段还会建线程池:
thread_init_threads(CFG_NUM_THREADS) thread_init_boot_thread()从这之后,日志前缀由D/TC:0变成D/TC:0 0,多出来的0是线程 ID,表示 0 号 boot 线程已激活。
5.3 runtime:准备 TA 运行时
入口:boot_init_primary_runtime()。
runtime 阶段有两个重点。
第一,thread_init_primary()调init_user_kcode(),把 S-EL1/S-EL0 切换需要的一小段跳板映射到用户态可见地址。它和 early 阶段的el0_svc配套:前者解决“怎么回到/恢复用户态”,后者解决“用户态怎么陷入内核态”。
这部分将在后续章节展开更多细节,可持续关注后续篇章!
第二,init_tee_runtime()跑 initcall,注册 TA store、系统调用分发表、加密服务等运行时组件:
I/TC: OP-TEE version: c02b129 ... I/TC: Primary CPU initializing D/TC:0 0 boot_init_primary_runtime:1053 Executing at offset 0xa4ed3000 with virtual load address 0xb2fd3000 D/TC:0 0 mobj_mapped_shm_init:543 Shared memory address range: afe00000, b1e00000 D/TC:0 0 check_ta_store:460 TA store: "early TA" D/TC:0 0 check_ta_store:460 TA store: "Secure Storage TA" D/TC:0 0 check_ta_store:460 TA store: "REE"5.4 final:收尾并准备返回 Normal World
入口:boot_init_primary_final()。
final 阶段跑 driver initcall/finalcall,关键日志:
D/TC:0 0 do_init_calls:19 finalcall level 8 release_external_dt() I/TC: Primary CPU switching to normal world bootrelease_external_dt()表示外部 DT 改写完成。Primary CPU switching to normal world boot是主核初始化结束的标志。
6. 交还 BL31:登记 SMC 入口表
四阶段初始化完成后,OP-TEE 要把自己变成“被动服务端”:线程池空闲,入口表交给 BL31,之后等 Normal World 的 SMC。
关键代码仍在entry_a64.S:
bl thread_clr_boot_thread adr x1, thread_vector_table mov x0, #TEESMC_OPTEED_RETURN_ENTRY_DONE smc #0含义:
thread_clr_boot_thread()清掉 0 号 boot 线程,线程池回到空闲状态。x1 = thread_vector_table,把 OP-TEE 的 SMC 入口向量表交给 BL31/opteed。x0 = TEESMC_OPTEED_RETURN_ENTRY_DONE,告诉 BL31 初始化完成。smc #0陷入 EL3,BL31 保存入口表,然后继续启动 U-Boot/Linux。
这条 SMC 之后,OP-TEE 的状态是:
- OP-TEE core 的代码和数据留在 Secure RAM。
- SMC 入口表已经挂到 BL31。
- S-EL0 syscall/返回路径已经准备好。
- 线程池空闲。
- 外部 DT 已被改写,后续会交给 Linux。
7. 小结
OP-TEE 初始化真正和 TrustZone 安全边界相关的是这几件事:
TEE_RAM_RX/RW:Secure World 私有代码/数据内存。NSEC_SHM:Secure/Normal World 固定共享缓冲区。- 外部 DT
reserved-memory:防止 Linux 误用 OP-TEE 私有内存和静态共享内存。 - 外部 DT
/firmware/optee:让 Linux 知道通过 SMC 调 OP-TEE。 thread_excp_vect/el0_svc:未来 TA 从 S-EL0 进入 S-EL1 的系统调用入口。init_user_kcode():S-EL1/S-EL0 切换所需的用户态可见跳板。thread_vector_table:BL31 后续分发 SMC 到 OP-TEE 的入口表。
一句话概括:OP-TEE 初始化的结果,是把一个刚被 BL31 拉起的 Secure 镜像,变成内存边界清楚、Linux 能识别内存保留和 SMC 调用方式、TA 用户态通道和 SMC 入口都已就绪的 TEE 服务端。
思考:OP-TEE 改写 DT,只是告诉 Linux 哪些物理内存不要纳入普通内存管理,比如TEE_RAM_*和静态NSEC_SHM。DT 本身不是访问控制机制,也不能阻止 Normal World 代码强行访问某个物理地址。
那么,真正限制 Linux 访问 OP-TEE 私有 Secure RAM 的是谁?如果在 Linux 里绕过内核内存管理,强行映射并访问TEE_RAM_*对应的物理地址,会发生什么?
