手撕ARM64启动栈(四):QEMU + TF-A(ATF) BL2 详解
2026/7/26 8:41:00 网站建设 项目流程

系列文章目录

手撕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 详解


文章目录

  • 系列文章目录
  • 1. 引言
  • 2. BL2 运行特权级:S-EL1
  • 3. BL2 汇编入口
    • GDB 实测:入口寄存器对比
  • 4. BL2 主流程:`bl2_main()`
    • GDB 实测:SMC 重入 BL1 异常向量
  • 5. image_desc 列表的来源
  • 6. entry_point_info 传递给 BL31
    • GDB 实测:三节点入口信息链表
  • 7. 本篇小结:日志与代码的对应关系

1. 引言

前两篇(第二、三篇)走完了 BL1 的全部流程——执行上下文(复位向量、异常向量表、退出前状态)与核心职责(bl1_main()逐函数拆解、加载 BL2)。本篇进入BL2 阶段:BL2 运行在哪个特权级、它的汇编入口、bl2_main()主流程如何加载 BL31/BL32/BL33 这一串镜像,以及加载完成后如何把entry_point_info链表交还给 BL1 再跳往 BL31。

和前两篇一样,本篇同样使用 GDB 断点方式分析 BL2 的关键节点。

涉及关键词:ATF / TF-A / ARM Trusted Firmware、QEMU virt aarch64、BL2、S-EL1、bl2_entrypointbl2_mainbl2_load_images、OP-TEE 镜像头、parse_optee_headerentry_point_info

2. BL2 运行特权级:S-EL1

本篇中 BL2 运行在Secure EL1ENABLE_RME=1时才改到 EL3,本篇未启用)。

第二篇中介绍BL1交给控制权给BL2时, 使用GDB 断点抓到 BL1 构造 BL2 的entry_point_infonext_bl_ep->spsr = 0x3c5ERET用它决定 BL2 起跑时的 PSTATE。0x3c5M[4:0]=0b00101EL1h(AArch64、EL1、用 EL1 自己的 SP),所以 BL1 是以 EL1 状态把控制权ERET给 BL2 的。

3. BL2 汇编入口

BL2 也有自己的汇编入口bl2/aarch64/bl2_entrypoint.S,角色和第二篇拆解的bl1_entrypoint.S相同——都是"CPU/上一级固件跳过来的第一条指令,负责在跳进 C 函数之前把运行环境搭好"。但它比 BL1 入口简单:BL1 被硬件复位向量直接拉起,要从复位态初始化搭建 EL3 环境;而 BL2 是 BL1 用ERET跳过来的运行在 S-EL1,只初始化自己EL1运行环境即可。

完整代码(trusted-firmware-a/bl2/aarch64/bl2_entrypoint.S):

func bl2_entrypoint /* 保存 BL1 传来的参数,后面转发给 bl2_main */ mov x20, x0 mov x21, x1 mov x22, x2 mov x23, x3 /* 设EL1异常向量表 */ adr x0, early_exceptions msr vbar_el1, x0 isb msr daifclr, #DAIF_ABT_BIT /* 开指令 cache、栈指针对齐检查、数据访问对齐检查 */ mov x1, #(SCTLR_I_BIT | SCTLR_A_BIT | SCTLR_SA_BIT) mrs x0, sctlr_el1 orr x0, x0, x1 bic x0, x0, #SCTLR_DSSBS_BIT msr sctlr_el1, x0 isb /* 清理 BL2 自身 RW 段的cache */ adr x0, __RW_START__ adr x1, __RW_END__ sub x1, x1, x0 bl inv_dcache_range /* .bss 清零 */ adrp x0, __BSS_START__ ... bl zeromem /* 分配栈 */ bl plat_set_my_stack #if STACK_PROTECTOR_ENABLED bl update_stack_protector_canary #endif /* 把保存的 x0-x3 还原为 bl2_main 的四个参数,跳转 */ mov x0, x20 mov x1, x21 mov x2, x22 mov x3, x23 bl bl2_main no_ret plat_panic_handler endfunc bl2_entrypoint

GDB 实测:入口寄存器对比

bl2_entrypoint断点处:

bl2_main断点处(汇编收尾已跑完,SP/VBAR/SCTLR 都已就位):

两处关键寄存器对比:

寄存器bl2_entrypoint断点处bl2_main断点处说明
$pc0xe05b0000xe05b25c与第三篇 GDB 实测的desc->image_info.image_base = 0xe05b000对应——BL2 入口地址
$VBAR(即当前 EL 的异常向量基址)0x00xe060800EL1异常向量初始化 –adr x0, early_exceptions; msr vbar_el1, x0
$SCTLR(当前 EL 的系统控制寄存器)0x30d008000x30d0180aI(指令 cache,bit12)/SA(栈指针对齐检查,bit3)/A(数据访问对齐检查,bit1)位被置上,对应mov x1,#(SCTLR_I_BIT|SCTLR_A_BIT|SCTLR_SA_BIT)那几行
$sp0x00xe064440从空(BL1 未传递 S-EL1 栈指针)变为plat_set_my_stack分配好的 BL2 栈顶
$x1(arg1)0xe0010000xe001000BL1 传给 BL2 的meminfo_t指针(BL2 可用安全 RAM 的 信息

4. BL2 主流程:bl2_main()

bl2_main.c核心序列:

voidbl2_main(u_register_targ0,u_register_targ1,u_register_targ2,u_register_targ3){plat_setup_early_console();bl2_early_platform_setup2(arg0,arg1,arg2,arg3);bl2_plat_arch_setup();auth_mod_init();next_bl_ep_info=bl2_load_images();// ← 核心:加载 BL31/BL32/BL33/* 通过 SMC 把 next_bl_ep_info 交还 BL1 */smc(BL1_SMC_RUN_IMAGE,(unsignedlong)next_bl_ep_info,0,0,0,0,0,0);}

smc(BL1_SMC_RUN_IMAGE, ...)会触发第二篇看到的 BL1smc_handler64bl1/aarch64/bl1_exceptions.S):BL2 加载完 BL31/BL32/BL33 后并不直接跳转,而是通过Mac call把执行权交还 BL1,由 BL1 跳进 BL31。

bl2_main()的主序列,以及它调用的bl2_load_images()加载循环流程见下图:

GDB 实测:SMC 重入 BL1 异常向量

smc(...)那行(bl2_main.c)打断点,看即将交还给 BL1 的next_bl_ep_info
pc = 0xe090000(BL31_BASE)、spsr = 0x3cd(EL3h)——链表头正是 BL31 的入口信息,与 §6 的 handoff 链一致。(args.arg0 = 0xe064450 是 §6 那条入口信息链表的头指针,随 SMC 一起交给 BL31.

紧接着单步过smc,CPU 会陷入 EL3,见下图 – 执行到BL1 的smc_handler64,且$x0 = 0x4=BL1_SMC_RUN_IMAGE

5. image_desc 列表的来源

bl2_load_images()具体要加载哪些镜像,是通过平台层查询得到的:

bl2_load_info=plat_get_bl_image_load_info();// 平台层实现,qemu_image_load.cbl2_node_info=bl2_load_info->head;while(bl2_node_info!=NULL){if(attr&IMAGE_ATTRIB_PLAT_SETUP){bl2_platform_setup();}// "BL2: Doing platform setup"if(attr&IMAGE_ATTRIB_SKIP_LOADING){INFO("BL2: Skip loading image id %u\n",...);// 对应 id=22}else{INFO("BL2: Loading image id %u\n",...);// 对应 id=3/4/21/5load_auth_image(bl2_node_info->image_id,bl2_node_info->image_info);}bl2_node_info=bl2_node_info->next_load_info;}

而这份列表的静态定义在 QEMU 平台代码里:plat/qemu/common/qemu_bl2_mem_params_desc.c,是一个bl_mem_params_node_t数组,关键条目:

{.image_id=BL31_IMAGE_ID,.ep_info.pc=BL31_BASE,.ep_info.spsr=SPSR_64(MODE_EL3,MODE_SP_ELX,...),.image_info.image_base=BL31_BASE,.image_info.image_max_size=BL31_LIMIT-BL31_BASE,.next_handoff_image_id=BL32_IMAGE_ID,// 加载完 BL31 后下一个是 BL32},{.image_id=BL32_IMAGE_ID,// ← id=4,OP-TEE header.ep_info.pc=BL32_BASE,.image_info.image_max_size=BL32_LIMIT-BL32_BASE,.next_handoff_image_id=BL33_IMAGE_ID,},{.image_id=BL32_EXTRA1_IMAGE_ID,// ← id=21,OP-TEE pager.image_info={...,IMAGE_ATTRIB_SKIP_LOADING},// 初始标记跳过!.image_info.image_base=BL32_BASE,.next_handoff_image_id=INVALID_IMAGE_ID,},{.image_id=BL32_EXTRA2_IMAGE_ID,// ← id=22,OP-TEE pageable.image_info={...,IMAGE_ATTRIB_SKIP_LOADING},// 初始标记跳过!.image_info.image_base=QEMU_OPTEE_PAGEABLE_LOAD_BASE,.next_handoff_image_id=INVALID_IMAGE_ID,},

id ↔ 宏的对应关系在本系列第一篇有介绍。

bl2_load_images()加载循环(bl2_image_load_v2.c)打断点,展开bl2_node_info链表可以直接看到这份静态数组转成的运行时链表——image_id沿next_load_info依次是0x3 → 0x4 → 0x15(21) → 0x16(22) → 0x5,链尾next_load_info = 0x0`:

其中 BL31(id=3)的image_info.image_base = 0xe090000(=BL31_BASE)、BL32(id=4)的image_base = 0xe100000(=BL32_BASE)。

关键点BL32_EXTRA1/BL32_EXTRA2在这份静态表里默认都标记IMAGE_ATTRIB_SKIP_LOADING,日志里能看到 id=21 被加载,说明这个标记在运行时被动态清除了——下一篇 OP-TEE header 解析会详细介绍这部分。

这份数组通过REGISTER_BL_IMAGE_DESCS()宏注册。

需要注意的是,OP-TEE(BL32)在这份列表里并非一个镜像,而是占了id=4/21/22 三项:id=4 在本篇是一个 28 字节的 OP-TEE 专用镜像头,id=21(pager)/id=22(paged) 才是真正的镜像体。上面 提到的 id=21/22 的 skip 标记之所以能在运行时被动态清除、以及 BL32 的最终入口从哪来,都由解析 id=4 这个头决定——这部分下一篇会详细拆解,本篇不再展开。

6. entry_point_info 传递给 BL31

BL2 把 BL31/BL32/BL33 全部加载到位后,如何告诉下一级"这些镜像各自从哪进入执行"?实现是——BL2 一次性把三个镜像的入口信息(entry_point_info)串成一条链表,交给 BL1,BL1 再原样转交 BL31

bl2_load_images()结尾就是这段构造 + 交接:

bl2_to_next_bl_params=plat_get_next_bl_params();// 把静态数组构造成一条入口信息链表/* 把链表头指针塞进头节点 ep_info 的 arg0,随后传给 BL31 */if(bl2_to_next_bl_params->head->ep_info->args.arg0==0)bl2_to_next_bl_params->head->ep_info->args.arg0=(u_register_t)bl2_to_next_bl_params;plat_flush_next_bl_params();// 刷 cache,确保 BL31 能读到returnbl2_to_next_bl_params->head->ep_info;// 返回链表头(=BL31 的 ep_info)

plat_get_next_bl_params()把 §5 那份静态数组串成一条链表:每个节点 =image_id+ 该镜像的ep_infoentry_point_info,含 pc/spsr/args)+ 指向下一节点的指针,节点顺序按数组里的next_handoff_image_id连成 BL31→BL32→BL33。

控制权传递路径 + 入口信息链表结构,见下图:

GDB 实测:三节点入口信息链表

bl2_load_images()return那行(此时 已把链表构造完)打断点,展开bl2_to_next_bl_params就能把 §6 图里画的三节点链表逐字段对上——沿headnext_params_info依次是 BL31→BL32→BL33,链尾next_params_info = 0x0

节点image_idep_info->pcep_info->spsr对应宏
head(第 1 个)0x30xe0900000x3cd(EL3h)BL31_BASE
第 2 个0x40xe1000000x0BL32(OP-TEE,pager 入口)
第 3 个0x50x600000000x3c5(EL1h/EL2h)NS_IMAGE_OFFSET(U-Boot)

7. 本篇小结:日志与代码的对应关系

日志行 / 关键事实代码位置
NOTICE: BL2: v2.14.0(release)...bl2_main.c
INFO: BL2: Doing platform setupbl2_image_load_v2.c,调用bl2_platform_setup()
INFO: BL2: Loading image id 3/4/21/5bl2_image_load_v2.cload_auth_image()前打印
INFO: BL2: Skip loading image id 22bl2_image_load_v2.c
INFO: OPTEE header info: ...parse_optee_header()解析 id=4 头(字段级拆解关注下一篇)
NOTICE: BL1: Booting BL31BL1 收到 BL2 的 SMC 后打印

第五篇会从本篇第 5 节点到的 OP-TEE 专用镜像头(id=4/21/22)继续深入,用真实tee-header_v2.bin的十六进制 dump 和实测日志,逐字段拆解optee_header_tparse_optee_header()的三种模式。第六篇(BL31 详解)会讲 BL31 拿到这条entry_point_info链表之后,如何完成 GICv3/运行时服务初始化,并通过SPD=opteed把控制权切换到 S-EL1 的 OP-TEE,以及 OP-TEE 执行完毕后如何再切回 BL33(U-Boot)。

最后留个小问题:BL1 跑在 EL3,BL2 却被降到了 S-EL1。为啥不让 BL2 也在 EL3运行,这样还省掉后面那次 SMC 陷回 EL3 的往返呢?你怎么看?欢迎评论区聊聊~

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

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

立即咨询