三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

手撕ARM64启动栈(八):QEMU + TF-A(ATF) OP-TEE 初始化详解

手撕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.Score_init_mmu_mapNSEC_SHMreserved-memorythread_excp_vectel0_svcthread_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的启动动作可以压成这几步:

  1. 保存 BL31 传进来的 handoff 参数,重点是外部 DTB 地址。
  2. 安装临时reset_vect_table。这张表只兜底,不是真正的optee 运行时异常向量入口。
  3. .bss、切临时栈。
  4. console_init()boot_save_args()boot_mem_init()
  5. 建页表、开 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_RWOP-TEE core 的代码/数据,Secure World 私有
SEC_RAM_OVERALLOP-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/opteecompatible = "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_corereg0xe1000000xf00000(15MB);optee_shmreg精确对应第 4 节的NSEC_SHM0x42000000起 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 boot

release_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

含义:

  1. thread_clr_boot_thread()清掉 0 号 boot 线程,线程池回到空闲状态。
  2. x1 = thread_vector_table,把 OP-TEE 的 SMC 入口向量表交给 BL31/opteed。
  3. x0 = TEESMC_OPTEED_RETURN_ENTRY_DONE,告诉 BL31 初始化完成。
  4. 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 固定共享缓冲区。
  • 外部 DTreserved-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_*对应的物理地址,会发生什么?

← 返回列表