ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手撕ARM64启动栈(七):QEMU + TF-A(ATF) BL31 世界切换

2026/8/7 10:58:02 拓冰建站 浏览量
手撕ARM64启动栈(七):QEMU + TF-A(ATF) BL31 世界切换 系列文章目录手撕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 世界切换文章目录系列文章目录1. 引言2. 世界切换机制两份 CPU 上下文与 SCR_EL3.NS2.1 cpu_context_t每 CPU 各留一组 secure/non-secure2.2 决定落到哪个世界SCR_EL3.NS2.3 从 C 到寄存器选中上下文的调用链2.4 el3_exit()SP_EL3 指哪就恢复哪3. 两次切换全景4. 同步切入 OP-TEE5. 切到 Normal World6. 小结1. 引言上一篇中运行时服务表已建好、但CPU 还停在 EL3。本篇接着BL31: Initializing BL32这行日志往下走先同步陷入 S-EL1 跑 OP-TEE 初始化再切向 Normal World 的 U-Boot。两步对应的主要两个函数opteed_init()、bl31_prepare_next_image_entry()寄存器层面是同一件事把SP_EL3指向目标世界那份cpu_context_t再由el3_exit()执行ERET。所以 “EL3→S-EL1” 和 “EL3→EL1” 切换逻辑一样只是参数不同。下面先讲机制再看两次切换各自的落点。本篇主要关注两次切换流程optee os内部初始化流程将在后续章节展开涉及关键词ATF / TF-A / ARM Trusted Firmware、QEMU virt aarch64、BL31、TrustZone、世界切换、SCR_EL3、cpu_context_t、cm_set_next_context、el3_exit、opteed、opteed_init、bl31_prepare_next_image_entry。2. 世界切换机制两份 CPU 上下文与SCR_EL3.NSEL3 只有一套物理寄存器SPSR_EL3、ELR_EL3、SCR_EL3等但 BL31 得同时替 Secure WorldOP-TEE和 Normal WorldU-Boot/Linux记着下次该恢复成什么样。办法是两份状态各存一份cpu_context_tERET前谁被选中el3_exit()就把 CPU 带去哪个世界。2.1cpu_context_t每 CPU 各留一组 secure/non-securecpu_context_tinclude/lib/el3_runtime/aarch64/context.h是 TF-A 给每个安全状态维护的寄存器快照结构// context.htypedefstructcpu_context{gp_regs_tgpregs_ctx;// 通用寄存器 x0-x30el3_state_tel3state_ctx;// EL3 状态寄存器SCR_EL3/SPSR_EL3/ELR_EL3...§2.4 恢复用cve_2018_3639_tcve_2018_3639_ctx;...el1_sysregs_tel1_sysregs_ctx;// EL1 系统寄存器}__aligned(CACHE_WRITEBACK_GRANULE)cpu_context_t;其中el3state_ctx保存的SCR_EL3/SPSR_EL3/ELR_EL3正是el3_exit()在ERET前要写回硬件的三个寄存器§2.4。一份cpu_context_t只描述一个安全状态secure/non-secure 两份并排挂在cpu_data_tinclude/lib/el3_runtime/cpu_data.h的cpu_context[CPU_CONTEXT_NUM]数组里// cpu_data.htypedefstructcpu_data{...void*cpu_context[CPU_CONTEXT_NUM];// 本仓库 CPU_CONTEXT_NUM2一组 secure/non-secure...}cpu_data_t;per-CPU 的是cpu_data_t——四核 QEMU virt 有四份每份各挂一组 secure/non-secure 上下文。CPU_CONTEXT_NUM和数组下标都在context.h定义本仓库没开ENABLE_RMECPU_CONTEXT_NUM2下标只有CPU_CONTEXT_SECURE0和CPU_CONTEXT_NS1get_cpu_context_index(security_state)负责SECURE/NON_SECURE到下标的映射。于是每个 CPU 手里就是cpu_context[0]OP-TEE 的 S-EL1 上下文和cpu_context[1]U-Boot/Linux 的 EL1 上下文。2.2 决定落到哪个世界SCR_EL3.NS两份cpu_context_t都只是内存里的数据真正决定ERET后 CPU 处于哪个安全状态的是硬件寄存器SCR_EL3的NS位SCR_NS_BITinclude/arch/aarch64/arch.hNS0落 Secure WorldNS1落 Non-secure World。每份上下文的el3_state_t分区里都存着各自世界那份SCR_EL3快照el3_exit()在ERET前原样写回硬件——写的是哪份上下文的SCR_EL3NS就是哪份原本设定的值。这条就是选中一份上下文到CPU 落到哪个世界的因果链。2.3 从 C 到寄存器选中上下文的调用链选中一份上下文落到代码上是一条很短的链。最底层的读写由 BL31 专属的cm_get_context()/cm_set_context()bl31/bl31_context_mgmt.c完成按security_state算下标读/写当前 CPU 的cpu_context[]// bl31_context_mgmt.cvoid*cm_get_context(size_tsecurity_state){returnget_cpu_data(cpu_context[get_cpu_context_index(security_state)]);}voidcm_set_context(void*context,uint32_tsecurity_state){set_cpu_data(cpu_context[get_cpu_context_index(security_state)],context);}真正告诉 CPU 下一次ERET用哪份上下文的是cm_set_next_eret_context()lib/el3_runtime/aarch64/context_mgmt.c它把取到的指针交给cm_set_next_context()// context_mgmt.cvoidcm_set_next_eret_context(uint32_tsecurity_state){cpu_context_t*ctxcm_get_context(security_state);cm_set_next_context(ctx);}落到寄存器的是cm_set_next_context()include/lib/el3_runtime/context_mgmt.hstatic inline——把SP_EL3直接指向选中的那份cpu_context_t// context_mgmt.h伪代码staticinlinevoidcm_set_next_context(void*context){/* msr spsel, #1 切到 SP_ELx 组 mov sp, context msr spsel, #0 切回 SP_EL0 组但 SP_EL3 已经改写完毕 */}于是选中哪个世界就是一次指针赋值SP_EL3指向哪份cpu_context_tel3_exit()就从哪份结构恢复现场。2.4el3_exit()SP_EL3指哪就恢复哪el3_exit()lib/el3_runtime/aarch64/context.S是真正执行ERET的地方。它不关心调用者是谁、也不关心要切进 Secure 还是切回 Non-secure只认SP_EL3当前指向的那份cpu_context_t把当前SP_EL0存进这份上下文的RUNTIME_SP字段留着将来切回来用随后切到SP_ELX寄存器组。从el3_state_t分区成对读出SPSR_EL3/ELR_EL3再读SCR_EL3、MDCR_EL3逐个msr写回——即 §2.2 说的写哪份上下文的 SCR_EL3NS 位就是哪份。恢复通用寄存器、PMCR_EL0、如开启PAuth 密钥最后单独恢复x30LR。执行ERET。一句话TF-A 的世界切换就是换一次SP_EL3指向再调用同一份el3_exit()。opteed_init()把SP_EL3指向 secure 侧上下文后触发它bl31_prepare_next_image_entry()把SP_EL3指向 non-secure 侧上下文后触发它使用同样的机制。3. 两次切换全景机制讲清了把剩下两步放进一张往返时序图。三条泳道分别是 EL3opteed dispatcher、S-EL1OP-TEE、EL1Normal World箭头标出ERET/SMC两种跳转的落点图中三次跳转对应本篇两节opteed_init()一次ERET切入 S-EL1§4 去程OP-TEE 初始化完用SMC陷回 EL3§4 回程最后bl31_prepare_next_image_entry()一次ERET切向 EL1 的 U-Boot§5。三次都走 §2 那套SP_EL3el3_exit()。4. 同步切入 OP-TEEopteed_init()把去程ERET切入 S-EL1、OP-TEE 初始化完再用SMC切回这一整趟往返包成了一次看似普通的函数调用bl32_init()被调用时即BL31: Initializing BL32打印之后走到的就是opteed_init()// opteed_main.cstaticint32_topteed_init(void){entry_point_info_t*optee_entry_pointbl31_plat_get_next_image_ep_info(SECURE);returnopteed_init_with_entry_point(optee_entry_point);}opteed_init_with_entry_point()opteed_main.c用cm_init_my_context()应用上一篇构造好的上下文然后同步切入// opteed_main.ccm_init_my_context(optee_entry_point);rcopteed_synchronous_sp_entry(optee_ctx);// 陷入直到 OP-TEE 用 SMC 报告初始化完成才返回在opteed_init_with_entry_point()断下调用栈opteed_init_with_entry_point ← opteed_init ← bl31_main印证了 §3 的调用关系。optee_entry_point此刻是pc0xe100000、spsr0x3c5——spsr相比上一篇opteed_setup()入口时的0x0已经填好正是opteed_init_optee_ep_state()写入的 S-EL1EL1h状态opteed_synchronous_sp_entry()opteed_common.c先恢复上一篇构造的 S-EL1 系统寄存器再调 §2.3 的cm_set_next_eret_context(SECURE)把SP_EL3指向cpu_context[CPU_CONTEXT_SECURE]最后进汇编opteed_enter_sp()opteed_helpers.S// opteed_common.cC 侧uint64_topteed_synchronous_sp_entry(optee_context_t*optee_ctx){cm_el1_sysregs_context_restore(SECURE);// 恢复 S-EL1 系统寄存器cm_set_next_eret_context(SECURE);// SP_EL3 指向 secure 侧上下文rcopteed_enter_sp(optee_ctx-c_rt_ctx);returnrc;}// opteed_helpers.S汇编侧func opteed_enter_sp mov x3,sp str x3,[x0,#0]// 保存当前 SP供 opteed_exit_sp 恢复用sub sp,sp,#OPTEED_C_RT_CTX_SIZE stp x19,x20,[sp,#OPTEED_C_RT_CTX_X19]// 保存 callee-saved 寄存器 x19-x30...stp x29,x30,[sp,#OPTEED_C_RT_CTX_X29]b el3_exit// ERETPC 跳到 OP-TEE 入口进入S-EL1endfunc opteed_enter_sp在opteed_enter_sp()调用点断下optee_ctx-cpu_ctx的el3state_ctx已由前两行填好ctx_regs[3]0xe100000ELR_EL3OP-TEE 入口、ctx_regs[2]0x3c5SPSR_EL3S-EL1h 全屏蔽。这就是cm_set_next_eret_context(SECURE)把SP_EL3指过来之后的现场el3_exit()待会要恢复的就是这两个槽位b el3_exit就是 §2.4 的那次触发SP_EL3已指向 secure 侧上下文el3_exit()从中恢复通用寄存器和ELR_EL3/SPSR_EL3/SCR_EL3后ERET——CPU 从 EL3 落到 S-EL1这份上下文SCR_EL3.NS0PC落在 OP-TEE 的_start。停在el3_exit()末尾的exception_returnERET前最后一条指令$ELR_EL30xe100000、$SPSR_EL30x3c5——和上一张快照里的ctx_regs[3]/[2]完全一致说明这两个值确实是从选中的那份cpu_context_t恢复到硬件寄存器的这是一次同步调用从 C 视角看opteed_init()仿佛阻塞了很久实际上 CPU 正在 S-EL1 跑 OP-TEE 自己的初始化。OP-TEE 初始化完成后用 SMC 号TEESMC_OPTEED_RETURN_ENTRY_DONE陷回 EL3opteed_smc_handleropteed_main.c收到后// opteed_main.ccaseTEESMC_OPTEED_RETURN_ENTRY_DONE:optee_vector_table(optee_vectors_t*)x1;// 记下 OP-TEE 的 SMC 向量表地址if(optee_vector_table){set_optee_pstate(optee_ctx-state,OPTEE_PSTATE_ON);psci_register_spd_pm_hook(opteed_pm);// 注册电源管理hookcpu_on/suspend 等用}opteed_synchronous_sp_exit(optee_ctx,x1);// 跳回 opteed_enter_sp() 的调用点break;在case TEESMC_OPTEED_RETURN_ENTRY_DONE断下调用栈是opteed_smc_handler ← sync_exception_handlerruntime_exceptions——上一篇建好的 SMC 路由把这次陷入交到了 opteed 手上。x10xe1018e4是 OP-TEE 传回的向量表地址将存入optee_vector_table供以后每次 SMC 复用此刻optee_ctx-state0x1OPTEE_PSTATE_ON还没置位opteed_synchronous_sp_exit()opteed_common.c先保存 OP-TEE 侧系统寄存器再调opteed_exit_sp()opteed_helpers.S用先前存下的c_rt_ctx恢复 EL3 侧 callee-saved 寄存器并ret。从汇编角度这就是opteed_enter_sp()里b el3_exit那一跳的返回点C 代码里看到的则是opteed_synchronous_sp_entry()返回、opteed_init()返回、bl32_init()返回bl31_main()接着往下走。在opteed_synchronous_sp_exit()断下optee_ctx-state已从上一张的0x1变成0x2OPTEE_PSTATE_ON置位。入参ret0xe1018e4会经opteed_exit_sp()原样带回opteed_enter_sp()的调用点成为opteed_synchronous_sp_entry()的返回值opteed_exit_sp()里的mov x0, x1把ret挪进返回值寄存器x0bl31_main()拿它判断 OP-TEE 初始化是否成功非 0 成功0 失败。OP-TEE 回传的是自己的 SMC 向量表地址非 0。注意同一个0xe1018e4在这里扮演两个互不相干的角色作向量表地址被opteed_smc_handler存进optee_vector_table供后续 SMC 复用作返回值则只被bl31_main()拿去判空。时间点和 secure world 日志也吻合secure-world.log里那整段 OP-TEE 初始化全部发生在这次同步调用期间最后以I/TC: Primary CPU switching to normal world boot收尾紧接着就是TEESMC_OPTEED_RETURN_ENTRY_DONE这次 SMC 返回。5. 切到 Normal Worldopteed 初始化返回后bl31_main()继续执行到bl31_prepare_next_image_entry()// bl31_main.c删减 CTX_INCLUDE_AARCH32_REGS 分支本仓库未开voidbl31_prepare_next_image_entry(void){image_typebl31_get_next_image_type();// next_image_type 默认初值 NON_SECUREnext_image_infobl31_plat_get_next_image_ep_info(image_type);// 取出 BL33 的 entry_point_infoINFO(BL31: Preparing for EL3 exit to %s world\n,(image_typeSECURE)?secure:normal);// normal worldprint_entry_point_info(next_image_info);// 即日志里的 Entry point address/SPSR 两行cm_init_my_context(next_image_info);if(image_typeNON_SECURE)cm_prepare_el3_exit_ns();// 配置 EL2 状态、恢复 NS 侧系统寄存器}next_image_type是文件静态变量初值就是NON_SECURE——本仓库没有任何代码调用bl31_set_next_image_type()改它所以 opteed 初始化走完下一跳始终是 BL33。next_image_info同样来自bl31_early_platform_setup2()早就解析好的静态变量上一篇提过这里直接取出 BL33 那一项。cm_prepare_el3_exit_ns()内部最终也走到 §2.3 的cm_set_next_eret_context(NON_SECURE)SP_EL3这次改指cpu_context[CPU_CONTEXT_NS]。print_entry_point_info()common/bl_common.c打印的就是日志里这两行INFO: Entry point address 0x60000000 INFO: SPSR 0x3c50x60000000是 U-Boot 的加载地址NS_IMAGE_OFFSET NS_DRAM0_BASE 0x20000000platform_def.hSPSR0x3c5解码M[3:0]0x5对应EL1h。bl31_main()返回后由bl31_entrypoint.S调用的el3_exit()就是这次的触发点SP_EL3指向 non-secure 侧上下文从中恢复ELR_EL30x60000000、SPSR_EL30x3c5、SCR_EL3.NS1ERET后 PC 落到 U-Boot异常级从 EL3 掉到 EL1——正是日志里 U-Boot 开始打印U-Boot 2025.07 (...)的那一刻。同样停在exception_return这次是$ELR_EL30x60000000、$SPSR_EL30x3c5、$SCR_EL3的NS1。和 §4 切入 OP-TEE 那次ELR0xe100000、NS0是同一条指令的两次不同现场印证了 §2.4 那句同一份el3_exit()只看SP_EL3指向哪份上下文6. 小结本节介绍的两次切换流程总结如下图中 ①②③ 都封装在opteed_init()内部是一次同步往返④⑤ 切向 U-Boot 是单向ERET①opteed_synchronous_sp_entry()用cm_init_my_context填好 Secure 上下文cm_set_next_eret_context(SECURE)把SP_EL3指向cpu_context[SECURE]。②opteed_enter_sp()存好 EL3 现场后跳el3_exit()ERET落到 S-EL1 的 OP-TEE 入口0xe100000。③OP-TEE 初始化完用SMC(ENTRY_DONE)陷回opteed_smc_handler()→opteed_synchronous_sp_exit()保存 Secure 系统寄存器并ret同步返回bl31_main()——opteed_init()只是看似阻塞。④bl31_prepare_next_image_entry()用cm_init_my_contextcm_prepare_el3_exit_ns()填好 Non-secure 上下文SP_EL3改指cpu_context[NS]。⑤bl31_main()返回后由el3_exit()执行ERET落到 EL1 的 U-Boot0x60000000。⑥这一跳不返回EL3 转为常驻运行时服务Normal World 此后只以新的SMC陷回不再回到bl31_main()。日志行 / 关键事实代码位置本例取值BL31: Initializing BL32bl31_main()调用(*bl32_init)()同步陷入 S-EL1 跑 OP-TEE 初始化——世界切换从这里开始同步切入/切出opteed_synchronous_sp_entry/exitopteed_common.copteed_enter_sp/exit_spopteed_helpers.S陷入期间对应secure-world.log全部D/TC:0初始化日志OP-TEE 报告完成SMCTEESMC_OPTEED_RETURN_ENTRY_DONE→opteed_smc_handler记录optee_vector_table注册 PM 钩子BL31: Preparing for EL3 exit to normal worldbl31_prepare_next_image_entry()next_image_typeNON_SECURE默认值未被改写Entry point address 0x60000000/SPSR 0x3c5BL2 构造的 BL33 entryel_implemented(2)探测NS_IMAGE_OFFSETEL1h因virtualizationfalseEL2 未实现U-Boot 开始打印el3_exit()执行ERET异常级别 EL3→EL1至此两次切换完成具体源码两份cpu_context_t各存一份完整的 EL3 现场快照cm_set_next_context()把SP_EL3指向其中一份el3_exit()不管调用者是谁只从SP_EL3当前指向的结构恢复现场并ERET。opteed 用它同步切入 OP-TEE 又切回来bl31_prepare_next_image_entry()用它切向 U-Boot两次共享同一套底层机制。下一篇进入 OP-TEE 自身从optee os的_start入口开始看它内部的初始化细节。