:QEMU + TF-A(ATF) BL2 详解)
系列文章目录手撕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-EL13. BL2 汇编入口GDB 实测入口寄存器对比4. BL2 主流程bl2_main()GDB 实测SMC 重入 BL1 异常向量5. image_desc 列表的来源6. entry_point_info 传递给 BL31GDB 实测三节点入口信息链表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_entrypoint、bl2_main、bl2_load_images、OP-TEE 镜像头、parse_optee_header、entry_point_info。2. BL2 运行特权级S-EL1本篇中 BL2 运行在Secure EL1ENABLE_RME1时才改到 EL3本篇未启用。第二篇中介绍BL1交给控制权给BL2时 使用GDB 断点抓到 BL1 构造 BL2 的entry_point_info时next_bl_ep-spsr 0x3c5ERET用它决定 BL2 起跑时的 PSTATE。0x3c5的M[4:0]0b00101即EL1hAArch64、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.Sfunc 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_entrypointGDB 实测入口寄存器对比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指令 cachebit12/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_infobl2_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.SBL2 加载完 BL31/BL32/BL33 后并不直接跳转而是通过Mac call把执行权交还 BL1由 BL1 跳进 BL31。bl2_main()的主序列以及它调用的bl2_load_images()加载循环流程见下图GDB 实测SMC 重入 BL1 异常向量在smc(...)那行bl2_main.c打断点看即将交还给 BL1 的next_bl_ep_infopc 0xe090000(BL31_BASE)、spsr 0x3cd(EL3h)——链表头正是 BL31 的入口信息与 §6 的 handoff 链一致。args.arg0 0xe064450 是 §6 那条入口信息链表的头指针随 SMC 一起交给 BL31.紧接着单步过smcCPU 会陷入 EL3见下图 – 执行到BL1 的smc_handler64且$x0 0x4BL1_SMC_RUN_IMAGE5. image_desc 列表的来源bl2_load_images()具体要加载哪些镜像是通过平台层查询得到的bl2_load_infoplat_get_bl_image_load_info();// 平台层实现qemu_image_load.cbl2_node_infobl2_load_info-head;while(bl2_node_info!NULL){if(attrIMAGE_ATTRIB_PLAT_SETUP){bl2_platform_setup();}// BL2: Doing platform setupif(attrIMAGE_ATTRIB_SKIP_LOADING){INFO(BL2: Skip loading image id %u\n,...);// 对应 id22}else{INFO(BL2: Loading image id %u\n,...);// 对应 id3/4/21/5load_auth_image(bl2_node_info-image_id,bl2_node_info-image_info);}bl2_node_infobl2_node_info-next_load_info;}而这份列表的静态定义在 QEMU 平台代码里plat/qemu/common/qemu_bl2_mem_params_desc.c是一个bl_mem_params_node_t数组关键条目{.image_idBL31_IMAGE_ID,.ep_info.pcBL31_BASE,.ep_info.spsrSPSR_64(MODE_EL3,MODE_SP_ELX,...),.image_info.image_baseBL31_BASE,.image_info.image_max_sizeBL31_LIMIT-BL31_BASE,.next_handoff_image_idBL32_IMAGE_ID,// 加载完 BL31 后下一个是 BL32},{.image_idBL32_IMAGE_ID,// ← id4OP-TEE header.ep_info.pcBL32_BASE,.image_info.image_max_sizeBL32_LIMIT-BL32_BASE,.next_handoff_image_idBL33_IMAGE_ID,},{.image_idBL32_EXTRA1_IMAGE_ID,// ← id21OP-TEE pager.image_info{...,IMAGE_ATTRIB_SKIP_LOADING},// 初始标记跳过.image_info.image_baseBL32_BASE,.next_handoff_image_idINVALID_IMAGE_ID,},{.image_idBL32_EXTRA2_IMAGE_ID,// ← id22OP-TEE pageable.image_info{...,IMAGE_ATTRIB_SKIP_LOADING},// 初始标记跳过.image_info.image_baseQEMU_OPTEE_PAGEABLE_LOAD_BASE,.next_handoff_image_idINVALID_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其中 BL31id3的image_info.image_base 0xe090000BL31_BASE、BL32id4的image_base 0xe100000BL32_BASE。关键点BL32_EXTRA1/BL32_EXTRA2在这份静态表里默认都标记IMAGE_ATTRIB_SKIP_LOADING日志里能看到 id21 被加载说明这个标记在运行时被动态清除了——下一篇 OP-TEE header 解析会详细介绍这部分。这份数组通过REGISTER_BL_IMAGE_DESCS()宏注册。需要注意的是OP-TEEBL32在这份列表里并非一个镜像而是占了id4/21/22 三项id4 在本篇是一个 28 字节的 OP-TEE 专用镜像头id21(pager)/id22(paged) 才是真正的镜像体。上面 提到的 id21/22 的 skip 标记之所以能在运行时被动态清除、以及 BL32 的最终入口从哪来都由解析 id4 这个头决定——这部分下一篇会详细拆解本篇不再展开。6. entry_point_info 传递给 BL31BL2 把 BL31/BL32/BL33 全部加载到位后如何告诉下一级这些镜像各自从哪进入执行实现是——BL2 一次性把三个镜像的入口信息entry_point_info串成一条链表交给 BL1BL1 再原样转交 BL31。bl2_load_images()结尾就是这段构造 交接bl2_to_next_bl_paramsplat_get_next_bl_params();// 把静态数组构造成一条入口信息链表/* 把链表头指针塞进头节点 ep_info 的 arg0随后传给 BL31 */if(bl2_to_next_bl_params-head-ep_info-args.arg00)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_infoplat_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 图里画的三节点链表逐字段对上——沿head→next_params_info依次是 BL31→BL32→BL33链尾next_params_info 0x0节点image_idep_info-pcep_info-spsr对应宏head第 1 个0x30xe0900000x3cdEL3hBL31_BASE第 2 个0x40xe1000000x0BL32OP-TEEpager 入口第 3 个0x50x600000000x3c5EL1h/EL2hNS_IMAGE_OFFSETU-Boot7. 本篇小结日志与代码的对应关系日志行 / 关键事实代码位置NOTICE: BL2: v2.14.0(release)...bl2_main.cINFO: 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.cINFO: OPTEE header info: ...parse_optee_header()解析 id4 头字段级拆解关注下一篇NOTICE: BL1: Booting BL31BL1 收到 BL2 的 SMC 后打印第五篇会从本篇第 5 节点到的 OP-TEE 专用镜像头id4/21/22继续深入用真实tee-header_v2.bin的十六进制 dump 和实测日志逐字段拆解optee_header_t与parse_optee_header()的三种模式。第六篇BL31 详解会讲 BL31 拿到这条entry_point_info链表之后如何完成 GICv3/运行时服务初始化并通过SPDopteed把控制权切换到 S-EL1 的 OP-TEE以及 OP-TEE 执行完毕后如何再切回 BL33U-Boot。最后留个小问题BL1 跑在 EL3BL2 却被降到了 S-EL1。为啥不让 BL2 也在 EL3运行这样还省掉后面那次 SMC 陷回 EL3 的往返呢你怎么看欢迎评论区聊聊