系列文章目录
手撕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.NS`
- 2.1 `cpu_context_t`:每 CPU 各留一组 secure/non-secure
- 2.2 决定落到哪个世界:`SCR_EL3.NS`
- 2.3 从 C 到寄存器:选中上下文的调用链
- 2.4 `el3_exit()`:`SP_EL3` 指哪就恢复哪
- 3. 两次切换全景
- 4. 同步切入 OP-TEE
- 5. 切到 Normal World
- 6. 小结
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.NS
EL3 只有一套物理寄存器(SPSR_EL3、ELR_EL3、SCR_EL3等),但 BL31 得同时替 Secure World(OP-TEE)和 Normal World(U-Boot/Linux)记着"下次该恢复成什么样"。办法是两份状态各存一份cpu_context_t,ERET前谁被选中,el3_exit()就把 CPU 带去哪个世界。
2.1cpu_context_t:每 CPU 各留一组 secure/non-secure
cpu_context_t(include/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_t(include/lib/el3_runtime/cpu_data.h)的cpu_context[CPU_CONTEXT_NUM]数组里:
// cpu_data.htypedefstructcpu_data{...void*cpu_context[CPU_CONTEXT_NUM];// 本仓库 CPU_CONTEXT_NUM=2:一组 secure/non-secure...}cpu_data_t;per-CPU 的是cpu_data_t——四核 QEMU virt 有四份,每份各挂一组 secure/non-secure 上下文。
CPU_CONTEXT_NUM和数组下标都在context.h定义:本仓库没开ENABLE_RME,CPU_CONTEXT_NUM=2,下标只有CPU_CONTEXT_SECURE=0和CPU_CONTEXT_NS=1,get_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_BIT,include/arch/aarch64/arch.h):NS=0落 Secure World,NS=1落 Non-secure World。每份上下文的el3_state_t分区里都存着各自世界那份SCR_EL3快照,el3_exit()在ERET前原样写回硬件——写的是哪份上下文的SCR_EL3,NS就是哪份原本设定的值。这条就是"选中一份上下文"到"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*ctx=cm_get_context(security_state);cm_set_next_context(ctx);}落到寄存器的是cm_set_next_context()(include/lib/el3_runtime/context_mgmt.h,static 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_t,el3_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_EL3,NS 位就是哪份"。 - 恢复通用寄存器、
PMCR_EL0、(如开启)PAuth 密钥,最后单独恢复x30(LR)。 - 执行
ERET。
一句话:TF-A 的"世界切换",就是换一次SP_EL3指向,再调用同一份el3_exit()。opteed_init()把SP_EL3指向 secure 侧上下文后触发它,bl31_prepare_next_image_entry()把SP_EL3指向 non-secure 侧上下文后触发它,使用同样的机制。
3. 两次切换全景
机制讲清了,把剩下两步放进一张往返时序图。三条泳道分别是 EL3(opteed dispatcher)、S-EL1(OP-TEE)、EL1(Normal 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_EL3+el3_exit()。
4. 同步切入 OP-TEE
opteed_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_point=bl31_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);rc=opteed_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此刻是pc=0xe100000、spsr=0x3c5——spsr相比上一篇opteed_setup()入口时的0x0已经填好,正是opteed_init_optee_ep_state()写入的 S-EL1(EL1h)状态: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.c(C 侧)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 侧上下文rc=opteed_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// ERET,PC 跳到 OP-TEE 入口,进入S-EL1endfunc opteed_enter_sp在opteed_enter_sp()调用点断下,optee_ctx->cpu_ctx的el3state_ctx已由前两行填好:ctx_regs[3]=0xe100000(ELR_EL3,OP-TEE 入口)、ctx_regs[2]=0x3c5(SPSR_EL3,S-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.NS=0),PC落在 OP-TEE 的_start。
停在el3_exit()末尾的exception_return(ERET前最后一条指令),$ELR_EL3=0xe100000、$SPSR_EL3=0x3c5——和上一张快照里的ctx_regs[3]/[2]完全一致,说明这两个值确实是从选中的那份cpu_context_t恢复到硬件寄存器的:
这是一次同步调用:从 C 视角看opteed_init()仿佛"阻塞"了很久,实际上 CPU 正在 S-EL1 跑 OP-TEE 自己的初始化。OP-TEE 初始化完成后用 SMC 号TEESMC_OPTEED_RETURN_ENTRY_DONE陷回 EL3,opteed_smc_handler(opteed_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);// 注册电源管理hook(cpu_on/suspend 等用)}opteed_synchronous_sp_exit(optee_ctx,x1);// 跳回 opteed_enter_sp() 的调用点break;在case TEESMC_OPTEED_RETURN_ENTRY_DONE断下,调用栈是opteed_smc_handler ← sync_exception_handler(runtime_exceptions)——上一篇建好的 SMC 路由把这次陷入交到了 opteed 手上。x1=0xe1018e4是 OP-TEE 传回的向量表地址,将存入optee_vector_table供以后每次 SMC 复用;此刻optee_ctx->state=0x1(OPTEE_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变成0x2(OPTEE_PSTATE_ON置位)。入参ret=0xe1018e4会经opteed_exit_sp()原样带回opteed_enter_sp()的调用点,成为opteed_synchronous_sp_entry()的返回值:
opteed_exit_sp()里的mov x0, x1把ret挪进返回值寄存器x0,bl31_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 World
opteed 初始化返回后,bl31_main()继续执行到bl31_prepare_next_image_entry():
// bl31_main.c(删减 CTX_INCLUDE_AARCH32_REGS 分支,本仓库未开)voidbl31_prepare_next_image_entry(void){image_type=bl31_get_next_image_type();// next_image_type 默认初值 NON_SECUREnext_image_info=bl31_plat_get_next_image_ep_info(image_type);// 取出 BL33 的 entry_point_infoINFO("BL31: Preparing for EL3 exit to %s world\n",(image_type==SECURE)?"secure":"normal");// "normal world"print_entry_point_info(next_image_info);// 即日志里的 Entry point address/SPSR 两行cm_init_my_context(next_image_info);if(image_type==NON_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 + 0x20000000,platform_def.h),SPSR=0x3c5解码:M[3:0]=0x5对应EL1h。
bl31_main()返回后由bl31_entrypoint.S调用的el3_exit()就是这次的触发点:SP_EL3指向 non-secure 侧上下文,从中恢复ELR_EL3=0x60000000、SPSR_EL3=0x3c5、SCR_EL3.NS=1,ERET后 PC 落到 U-Boot,异常级从 EL3 掉到 EL1——正是日志里 U-Boot 开始打印U-Boot 2025.07 (...)的那一刻。
同样停在exception_return,这次是$ELR_EL3=0x60000000、$SPSR_EL3=0x3c5、$SCR_EL3的NS=1。和 §4 切入 OP-TEE 那次(ELR=0xe100000、NS=0)是同一条指令的两次不同现场,印证了 §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_context+cm_prepare_el3_exit_ns()填好 Non-secure 上下文,SP_EL3改指cpu_context[NS]。 - ⑤
bl31_main()返回后由el3_exit()执行ERET,落到 EL1 的 U-Boot(0x60000000)。 - ⑥这一跳不返回:EL3 转为常驻运行时服务,Normal World 此后只以新的
SMC陷回,不再回到bl31_main()。
| 日志行 / 关键事实 | 代码位置 | 本例取值 |
|---|---|---|
BL31: Initializing BL32 | bl31_main()调用(*bl32_init)() | 同步陷入 S-EL1 跑 OP-TEE 初始化——世界切换从这里开始 |
| 同步切入/切出 | opteed_synchronous_sp_entry/exit(opteed_common.c)+opteed_enter_sp/exit_sp(opteed_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 world | bl31_prepare_next_image_entry() | next_image_type=NON_SECURE(默认值,未被改写) |
Entry point address = 0x60000000/SPSR = 0x3c5 | BL2 构造的 BL33 entry;el_implemented(2)探测 | NS_IMAGE_OFFSET,EL1h(因virtualization=false,EL2 未实现) |
| 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入口开始,看它内部的初始化细节。