手撕ARM64启动栈(七):QEMU + TF-A(ATF) BL31 世界切换
2026/8/7 10:57:58 网站建设 项目流程

系列文章目录

手撕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_EL3cpu_context_tcm_set_next_contextel3_exit、opteed、opteed_initbl31_prepare_next_image_entry

2. 世界切换机制:两份 CPU 上下文与SCR_EL3.NS

EL3 只有一套物理寄存器(SPSR_EL3ELR_EL3SCR_EL3等),但 BL31 得同时替 Secure World(OP-TEE)和 Normal World(U-Boot/Linux)记着"下次该恢复成什么样"。办法是两份状态各存一份cpu_context_tERET前谁被选中,el3_exit()就把 CPU 带去哪个世界。

2.1cpu_context_t:每 CPU 各留一组 secure/non-secure

cpu_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_NUM=2:一组 secure/non-secure...}cpu_data_t;

per-CPU 的是cpu_data_t——四核 QEMU virt 有四份,每份各挂一组 secure/non-secure 上下文。

CPU_CONTEXT_NUM和数组下标都在context.h定义:本仓库没开ENABLE_RMECPU_CONTEXT_NUM=2,下标只有CPU_CONTEXT_SECURE=0CPU_CONTEXT_NS=1get_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_EL3NS位(SCR_NS_BITinclude/arch/aarch64/arch.h):NS=0落 Secure World,NS=1落 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*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_tel3_exit()就从哪份结构恢复现场。

2.4el3_exit()SP_EL3指哪就恢复哪

el3_exit()lib/el3_runtime/aarch64/context.S)是真正执行ERET的地方。它不关心调用者是谁、也不关心要切进 Secure 还是切回 Non-secure,只认SP_EL3当前指向的那份cpu_context_t

  1. 把当前SP_EL0存进这份上下文的RUNTIME_SP字段(留着将来切回来用),随后切到SP_ELX寄存器组。
  2. el3_state_t分区成对读出SPSR_EL3/ELR_EL3,再读SCR_EL3MDCR_EL3,逐个msr写回——即 §2.2 说的"写哪份上下文的 SCR_EL3,NS 位就是哪份"。
  3. 恢复通用寄存器、PMCR_EL0、(如开启)PAuth 密钥,最后单独恢复x30(LR)。
  4. 执行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=0xe100000spsr=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_ctxel3state_ctx已由前两行填好:ctx_regs[3]=0xe100000ELR_EL3,OP-TEE 入口)、ctx_regs[2]=0x3c5SPSR_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_EL3ERET——CPU 从 EL3 落到 S-EL1(这份上下文SCR_EL3.NS=0),PC落在 OP-TEE 的_start

停在el3_exit()末尾的exception_returnERET前最后一条指令),$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_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);// 注册电源管理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_handlerruntime_exceptions)——上一篇建好的 SMC 路由把这次陷入交到了 opteed 手上。x1=0xe1018e4是 OP-TEE 传回的向量表地址,将存入optee_vector_table供以后每次 SMC 复用;此刻optee_ctx->state=0x1OPTEE_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置位)。入参ret=0xe1018e4会经opteed_exit_sp()原样带回opteed_enter_sp()的调用点,成为opteed_synchronous_sp_entry()的返回值:

opteed_exit_sp()里的mov x0, x1ret挪进返回值寄存器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 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 = 0x3c5

0x60000000是 U-Boot 的加载地址(NS_IMAGE_OFFSET = NS_DRAM0_BASE + 0x20000000platform_def.h),SPSR=0x3c5解码:M[3:0]=0x5对应EL1h

bl31_main()返回后由bl31_entrypoint.S调用的el3_exit()就是这次的触发点:SP_EL3指向 non-secure 侧上下文,从中恢复ELR_EL3=0x60000000SPSR_EL3=0x3c5SCR_EL3.NS=1ERET后 PC 落到 U-Boot,异常级从 EL3 掉到 EL1——正是日志里 U-Boot 开始打印U-Boot 2025.07 (...)的那一刻。

同样停在exception_return,这次是$ELR_EL3=0x60000000$SPSR_EL3=0x3c5$SCR_EL3NS=1。和 §4 切入 OP-TEE 那次(ELR=0xe100000NS=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 BL32bl31_main()调用(*bl32_init)()同步陷入 S-EL1 跑 OP-TEE 初始化——世界切换从这里开始
同步切入/切出opteed_synchronous_sp_entry/exitopteed_common.c)+opteed_enter_sp/exit_spopteed_helpers.S陷入期间对应secure-world.log全部D/TC:0初始化日志
OP-TEE 报告完成SMCTEESMC_OPTEED_RETURN_ENTRY_DONEopteed_smc_handler记录optee_vector_table,注册 PM 钩子
BL31: Preparing for EL3 exit to normal worldbl31_prepare_next_image_entry()next_image_type=NON_SECURE(默认值,未被改写)
Entry point address = 0x60000000/SPSR = 0x3c5BL2 构造的 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入口开始,看它内部的初始化细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询