手撕ARM64启动栈(二):QEMU + TF-A(ATF) BL1 执行上下文——GDB 实测复位到 BL2

发布时间:2026/7/11 3:23:30
手撕ARM64启动栈(二):QEMU + TF-A(ATF) BL1 执行上下文——GDB 实测复位到 BL2 系列文章目录手撕ARM64启动栈一QEMU TF-A → OP-TEE → U-Boot → Linux 全链路总览手撕ARM64启动栈二QEMU TF-A(ATF) BL1 执行上下文——GDB 实测复位到 BL2文章目录系列文章目录1. 引言2. BL1 就是 BootROM3. 动手准备搭建 GDB 调试环境4. 链接与复位向量绑定为什么 CPU 会从 bl1_entrypoint 开始执行4.1 链接脚本 bl1/bl1.ld.S4.2 QEMU 侧绑定4.3 RVBAR 是什么复位取指地址从哪来5. 两段式结构汇编入口 → 交接给 C5.1 汇编入口bl1/aarch64/bl1_entrypoint.S5.1.1 入口全貌5.1.2 el3_entrypoint_commonBL1/BL31 共用的复位后初始化宏5.1.3 断点 1bl1_entrypoint 入口——宏执行前的复位态5.1.4 断点 2plat_get_my_entrypoint——SCTLR_EL3 刚写完5.2 交接给 Cbl bl1_main5.2.1 断点 3bl1_main——宏全部走完即将进 C 主函数6. 异常向量表bl1_exceptions.S6.1 smc_handler64区分 BL1_SMC_RUN_IMAGE 与其它 SMC7. BL1 退出前的最后状态把控制权交给 BL27.1 构造 entry_point_infobl1_prepare_next_image()7.2 真正的跳转bl1_entrypoint.S 里的 el3_exit7.3 用 GDB 实测交接 BL2 时的 SPSR 0x3c58. 本篇小结日志与代码的对应关系9. 附录GDB 调试踩坑速查表1. 引言本篇聚焦BL1 这段代码启动瞬间所处的硬件/软件环境ROM 代码是怎么被跑起来的、C 环境建立前后经历了哪些步骤、异常向量表长什么样、以及 BL1 退出时把 CPU 状态交给 BL2 时的最后一刻快照。BL1 的 C 代码具体做了哪些初始化动作将在下一篇展开。本篇不是纯讲原理而是手撕——先搭好 GDB 调试环境第 3 节然后在 BL1 启动的每个关键节点打断点、看寄存器用实测值一步步印证复位→建 C 环境→交接 BL2这条链。调试环境gdb-multiarch QEMU gdbstub VSCodecppdbg图形化界面符号来自bl1.elfdebug 构建。启动方式见第 3 节。涉及关键词ATF / TF-A / ARM Trusted Firmware、QEMU virt aarch64、BL1、EL3 复位、RVBAR、异常向量表、SCTLR_EL3 / SCR_EL3 / VBAR_EL3 / SPSR_EL3、GDB / gdb-multiarch 调试、bl1_entrypoint、SMC、交接 BL2。2. BL1 就是 BootROMBL1 是 TF-A 定义的第一阶段 Bootloader源码在trusted-firmware-a/bl1/。TF-A 官方设计文档明确说明 BL1 从平台复位向量开始执行且驻留在 Trusted ROM 中This stage begins execution from the platform’s reset vector at EL3. The reset address is platform dependent but it is usually located in a Trusted ROM area.——trusted-firmware-a/docs/design/firmware-design.rst:146-147即 BL1 本身相当于 BootROM 里的代码复位后直接执行。本仓库里-bios bl1.binbuild/qemu_v8.mk:676让 QEMU 把 BL1 当固件放到复位地址 0x0.3. 动手准备搭建 GDB 调试环境本篇用 VSCode 的图形化界面做 gdb 调试、用 WATCH 面板看寄存器。QEMU 侧用make run-only启动其QEMU_RUN_ARGS带-s -Sbuild/qemu_v8.mk:711开机即停在复位态、开 gdbstub 于localhost:1234VSCode 侧靠.vscode/launch.json连上去核心配置{ name: ATF: Attach to QEMU (BL1 start), type: cppdbg, request: launch, preLaunchTask: Restart QEMU for debug, // 连接前先把 QEMU 复位到挂起态等价于跑 make run-only MIMode: gdb, miDebuggerPath: /usr/bin/gdb-multiarch, // aarch64 目标必须用 multiarch 版 program: ${workspaceFolder}/trusted-firmware-a/build/qemu/debug/bl1/bl1.elf, // 符号来源 miDebuggerServerAddress: localhost:1234, // QEMU gdbstub cwd: ${workspaceFolder}, stopAtEntry: false, setupCommands: [ { text: set architecture aarch64, ignoreFailures: true }, // 关键把容器里编译时的路径映射回主机源码路径否则单步时看不到源码 { text: set substitute-path /atf/trusted-firmware-a ${workspaceFolder}/trusted-firmware-a, ignoreFailures: true } ] }连上后WATCH 面板挂上这组表达式本篇所有截图全程复用$x0 $pc $sp $x30 $cpsr ($cpsr 2) 3 $SCTLR_EL3 $SCR_EL3 $RVBAR_EL3 $VBAR_EL3 $SPSR_EL3 $ELR_EL3环境就绪下面从CPU 复位后为什么落在bl1_entrypoint开始逐段打断点手撕。4. 链接与复位向量绑定为什么 CPU 会从 bl1_entrypoint 开始执行4.1 链接脚本bl1/bl1.ld.S. BL1_RO_BASE; .text . : { *bl1_entrypoint.o(.text*) /* 显式排在最前 */ *(SORT_BY_ALIGNMENT(.text*)) ... } ROM*bl1_entrypoint.o(.text*)写在其它.text*之前链接器按脚本里的出现顺序摆放 section于是bl1_entrypoint的第一条指令落在整个镜像的第 0 字节。起始地址BL1_RO_BASE等于SEC_ROM_BASE 0x0即镜像第 0 字节对应物理地址0x0。两者都在plat/qemu/qemu/include/platform_def.h#defineSEC_ROM_BASE0x00000000// :79#defineSEC_ROM_SIZE0x00020000// :80...#defineBL1_RO_BASESEC_ROM_BASE// :1334.2 QEMU 侧绑定build/qemu_v8.mk里用-bios bl1.bin启动。-bios指定的镜像被映射到virt内存图的VIRT_FLASH基址0x0而 QEMU 复位后 CPU 的 PC 就是0x0为什么是0x0下一节讲RVBAR时展开。于是复位取指0x0正好落在bl1.bin第 0 字节。结论链bl1.bin第0字节 bl1_entrypoint第一条指令 QEMU 复位后 PC 指向的地址。4.3 RVBAR 是什么复位取指地址从哪来上一节说QEMU 复位后 PC 0x00x0从哪来——它来自RVBARReset Vector Base Address Register存CPU 复位后从哪个物理地址取第一条指令复位时硬件直接把它装进 PC。Arm 官方对它的定义是If EL3 is the highest Exception level implemented, contains theIMPLEMENTATION DEFINEDaddress that execution starts from after reset when executing in AArch64 state.—— RVBAR_EL3, Reset Vector Base Address RegisterArm A-profile Architecture RegistersIMPLEMENTATION DEFINED复位地址由 SoC 厂商定义真实芯片上通常指向片内 Boot ROM 基址并不一定都是0x0。本仓库之所以是0x0只是因为 QEMUvirt从不设置rvbar属性cpu-rvbar_prop取默认0x0见qemu/target/arm/cpu.c:330-331的复位钩子env-pc env-cp15.rvbar正好命中-bios摆在0x0的bl1.bin。换一颗真实 SoC这里可能是某个 Boot ROM 地址BL1 就相应烧在那里。5. 两段式结构汇编入口 → 交接给 C下图是 BL1 汇编入口的整体流程——主干复位 →bl1_entrypoint→el3_entrypoint_common10 步 →bl bl1_main→el3_exit/ERET 进 BL2支路运行期 BL2 通过smc(BL1_SMC_RUN_IMAGE)重入smc_handler645.1 汇编入口bl1/aarch64/bl1_entrypoint.SBL1 的第一段代码全在这个汇编文件里先靠el3_entrypoint_common宏把 EL3 复位后的基础环境SCTLR_EL3、异常向量表、栈、C 运行时铺好再bl bl1_main交接给 C。本小节先看汇编清单§5.1.1和宏展开§5.1.2再用两个断点坐实宏执行前§5.1.3、执行中§5.1.4的寄存器状态。5.1.1 入口全貌func bl1_entrypoint el3_entrypoint_common \ _init_sctlr1 \ // 初始化 SCTLR_EL3 _warm_boot_mailbox!PROGRAMMABLE_RESET_ADDRESS \ _secondary_cold_boot!COLD_BOOT_SINGLE_CPU \ _init_memory1 \ // 清零 BSS 等 _init_c_runtime1 \ // 建立 C 运行时栈 _exception_vectorsbl1_exceptions \ _pie_fixup_size0 bl bl1_main // 跳转到 C 主函数 b el3_exit // main 返回后跳转到下一镜像无 RME 时 endfunc bl1_entrypoint这段汇编在 C 环境尚不存在时完成最基础的工作设置 EL3 相关系统寄存器、安装异常向量表bl1_exceptions见第 6 节、建立栈指针然后bl bl1_main进入 C 代码。5.1.2el3_entrypoint_commonBL1/BL31 共用的复位后初始化宏el3_entrypoint_common定义在include/arch/aarch64/el3_common_macros.S:157是 BL1 和 BL31 共用的一段EL3 复位后基础初始化逻辑——CPU 上电复位、以及从低功耗状态恢复都会先落到 EL3这段初始化设置异常向量、EL3 架构寄存器、栈、C 运行时环境对这两级是一样的所以抽成公共宏靠参数控制每一级各自需要哪些步骤。BL1 传入的各参数取值如下表。其中_warm_boot_mailbox/_secondary_cold_boot取决于PROGRAMMABLE_RESET_ADDRESS/COLD_BOOT_SINGLE_CPU——QEMUvirt未在自己的platform.mk里覆盖两者沿用make_helpers/defaults.mk默认值0故两个参数都取1参数值含义_init_sctlr1初始化SCTLR_EL3。复位后该寄存器很多字段是 UNKNOWN必须显式设置字节序小端、关闭栈/内存对齐检查等不能依赖硬件默认值_warm_boot_mailbox!PROGRAMMABLE_RESET_ADDRESS1是否需要靠软件读 mailbox 变量区分冷/热启动。QEMUvirt复位地址不可编程每次复位固定跳到同一地址必须靠这层判断若复位地址可编程热启动时硬件直接把 PC 设到正确入口就不需要这一步_secondary_cold_boot!COLD_BOOT_SINGLE_CPU1是否需要区分主/从 CPU。多核冷启动时所有核会一起跑到这段代码只应有一个主核继续做平台初始化其余核被晾在一边等待唤醒若平台保证冷启动只有单核参与这层判断可跳过_init_memory1调用platform_mem_initQEMUvirt平台该函数是空实现仅ret详见下一篇做平台相关的内存控制器初始化。BL1 是最早跑的一环必须执行这一步_init_c_runtime1初始化 C 运行时环境——清零.bss、必要时把 data section 从 ROM 搬到 RAM因为紧接着bl bl1_main就要跳进 C 代码全局变量必须先被正确初始化_exception_vectorsbl1_exceptions写入VBAR_EL3的异常向量表地址宏第 235-236 行adr x0, \_exception_vectors; msr vbar_el3, x0。BL1 用自己的bl1_exceptionsbl1/aarch64/bl1_exceptions.S见第 6 节_pie_fixup_size0PIE位置无关可执行文件重定位表修复所需的内存区域大小。传0表示跳过 PIE fixup宏第 211 行.if \_pie_fixup_size为假整段跳过——本仓库未设置ENABLE_PIE默认值0宏展开后按顺序执行对照el3_common_macros.S:157-427初始化SCTLR_EL3因为_init_sctlr1设小端字节序关闭WXN/栈对齐检查/内存对齐检查/DSSBSPIE fixup 整段跳过因为_pie_fixup_size0设置VBAR_EL3 bl1_exceptions写入异常向量表地址之后任何异常才有正确的处理入口第 6 节展开这张向量表的具体内容call_reset_handlerasm_macros.S:367调用plat_reset_handler CPU 特定的cpu_ops复位处理勘误表 workaround 等el3_arch_init_common再次设置SCTLR_EL3补充I-cache/SA/A使能位并初始化SCR_EL3、MDCR_EL3、CPTR_EL3setup_el3_execution_context配置 EL3 自身独立于其它 world 的执行环境DAIF.A、MDCR_EL3.SDD、SCR_EL3.EA/SIF、PMCR_EL0.DP、CPTR_EL3.EZ/ESM、DIT等控制位——这一步只写控制寄存器不涉及栈区分主/从 CPU因为_secondary_cold_boot1调用plat_is_my_cpu_primary判断从核进入plat_secondary_cold_boot_setupQEMU 平台实现为在poll_mailbox里wfe自旋等待见plat/qemu/common/aarch64/plat_helpers.S:64-86只有主核继续往下走platform_mem_init因为_init_memory1QEMU 平台是空操作C 运行时初始化因为_init_c_runtime1zeromem清零.bss以及USE_COHERENT_MEM时的 coherent memory 段必要时做 data section 的 ROM→RAM 搬运切到SP_EL0msr spsel, #0调用plat_set_my_stack分配并设置 C 运行时栈——这一步无条件执行不受上述任何参数控制且在 C 运行时初始化第 9 步之后才发生做完这一整套才会执行到bl1_entrypoint.S里的bl bl1_main——即bl1_main.c:50的bl1_main()函数入口§5.2。5.1.3 断点 1bl1_entrypoint入口——宏执行前的复位态前面 §4.3、§5.1.2 的结论复位从0x0取指、EL3 起跑、SCTLR_EL3被显式初始化、VBAR_EL3随后才指向bl1_exceptions现在用 GDB 断点逐条坐实。断点打在bl1_entrypoint入口*0x0宏第一条指令尚未执行停在el3_entrypoint_common展开的第一行源码第 31 行。关键读数WATCH值印证$pc0x0 bl1_entrypoint复位取指落在0x0正是bl1.bin第 0 字节($cpsr2)30x3BL1 在EL3起跑$cpsr0x400003cd$RVBAR_EL30x0virt不设RVBAR复位 PC 取默认0x0$VBAR_EL30x0异常向量表还没装msr vbar_el3在更靠后的指令$sp0x0C 运行栈未建5.1.4 断点 2plat_get_my_entrypoint——SCTLR_EL3刚写完宏中段停下SCTLR_EL3刚写完bl1_entrypoint的前几条指令是mov x0,#0x830; movk x0,#0x30c5,lsl#16; msr sctlr_el3,x0把0x30c50830写进SCTLR_EL3后才bl plat_get_my_entrypoint做冷/热启动判断。所以停在这里时WATCH断点1 → 断点2印证$SCTLR_EL30xc50838→0x30c50830宏显式初始化了SCTLR_EL3$x0→0x30c50830正是刚msr进SCTLR_EL3的那个立即数$VBAR_EL3/$sp仍为0x0向量表、C 栈都还没轮到SCTLR_EL3的值0x30c50830逐位解码M/A/C/SA/I/WXN/EE/DSSBS 全 0其余为架构 RES1 位。5.2 交接给 Cbl bl1_main宏 10 步全部走完后bl1_entrypoint.S执行bl bl1_main§5.1.1 汇编清单里的那一行把控制权交给 C 主函数。跨过这道门槛后bl1_main()内部究竟调用了哪些平台初始化函数plat_setup_early_console、bl1_plat_arch_setup、bl1_arch_setup、加载 BL2……将会在下篇内容中介绍本小节只用一个断点确认进 C 之前宏承诺的 C 运行环境异常向量表 栈已经就绪。5.2.1 断点 3bl1_main——宏全部走完即将进 C 主函数停在bl1_entrypoint.S的bl bl1_main源码第 45 行——此时宏 10 步已全部完成相比断点 2 的两处关键变化WATCH值印证$VBAR_EL30x0→0x5000§5.1.2 第3步已执行VBAR_EL3现在指向bl1_exceptionsnm确认其地址正是0x5000见第 6 节$sp0x0→0xe0ef140 bl1_cpu_context§5.1.2 第10步已执行C 运行栈已建好所以下一步才敢bl bl1_main进 C 代码$SCTLR_EL30x30c5183a相比断点2的0x30c50830el3_arch_init_common第5步补写了I/SA/A等使能位($cpsr2)30x3仍在 EL3断点 1→2→3 三张图连起来看SCTLR_EL3默认→写入→补位、VBAR_EL30→0x5000、SP0→栈地址三个量的演变正好对应 §5.1.2 宏初始化 SCTLR → 装向量表 → 建栈的执行顺序是这一节叙述的完整实测证据链。第 7 节交接 BL2 时SPSR0x3c5的实测放在 §7.3因为它发生在bl1_main()返回之后。6. 异常向量表bl1_exceptions.S第 5.1.2 节第 3 步提到VBAR_EL3被设为bl1_exceptions——这是 BL1 专属的异常向量表定义在bl1/aarch64/bl1_exceptions.S。ARMv8 EL3 异常向量表按异常源分 4 组、每组 4 种异常类型共 16 个向量异常类型Current EL, SP0Current EL, SPxLower EL, AArch64Lower EL, AArch32SynchronousSynchronousExceptionSP0L25panicSynchronousExceptionSPxL53panicSynchronousExceptionA64L81SMC 分发SynchronousExceptionA32L118panicIRQIrqSP0L31panicIrqSPxL59panicIrqA64L96panicIrqA32L124panicFIQFiqSP0L37panicFiqSPxL65panicFiqA64L102panicFiqA32L130panicSErrorSErrorSP0L43panicSErrorSPxL71panicSErrorA64L108panicSErrorA32L136panic16 个向量里 15 个是同一套死路都是mov x0, #异常编号 / bl plat_report_exception / no_ret plat_panic_handler——打印异常编号后直接 panic没有任何恢复逻辑。这符合 BL1 的定位一段极简的启动代码运行期间不应该发生任何这些异常一旦发生就是不可恢复的错误。唯一有实际处理逻辑的是SynchronousExceptionA64bl1_exceptions.S:81-94vector_entry SynchronousExceptionA64 msr daifclr, #DAIF_ABT_BIT str x30, [sp, #CTX_GPREGS_OFFSET CTX_GPREG_LR] /* Expect only SMC exceptions */ mrs x30, esr_el3 ubfx x30, x30, #ESR_EC_SHIFT, #ESR_EC_LENGTH cmp x30, #EC_AARCH64_SMC b.ne unexpected_sync_exception b smc_handler64 end_vector_entry SynchronousExceptionA64读取ESR_EL3Exception Syndrome Register的 ECException Class字段判断是否为EC_AARCH64_SMC不是则跳到unexpected_sync_exception本质还是plat_report_exception panic是则跳转smc_handler64。6.1smc_handler64区分BL1_SMC_RUN_IMAGE与其它 SMCsmc_handler64bl1_exceptions.S:143-206第一步就是判断这次 SMC 的用途func smc_handler64 /* Detect if this is a RUN_IMAGE or other SMC. */ mov x30, #BL1_SMC_RUN_IMAGE cmp x30, x0 b.ne smc_handler /* Make sure only Secure world reaches here. */ mrs x30, scr_el3 tst x30, #SCR_NS_BIT b.ne unexpected_sync_exception ...x0 BL1_SMC_RUN_IMAGE且SCR_EL3.NS 0发起方在 Secure World时走BL1_SMC_RUN_IMAGE专属路径切回SP_EL0的 C 运行时栈从x1指向的entry_point_info_t里取出目标镜像的pc/spsr写入elr_el3/spsr_el3禁用 MMU/I-cache 并做 TLB 全清tlbi alle3最后exception_return即ERET跳转到目标镜像。其它 SMCx0 ! BL1_SMC_RUN_IMAGE落到smc_handler标签保存完整的x0-x29上下文prepare_el3_entry调用 C 函数bl1_smc_handler处理处理完后统一走el3_exit返回。这个机制正是第一篇总体流程里BL2 加载完 BL31/BL32/BL33 后通过smc(BL1_SMC_RUN_IMAGE, ...)把控制权交还 BL1再由 BL1 跳转到 BL31的底层实现BL2 运行在 S-EL1没有权限直接切换到 EL3 去跳转 BL31只能通过 SMC 陷入 EL3——而 EL3 此刻还驻留着 BL1 的代码bl1_exceptions.S这张向量表本身就没被覆盖所以这次陷入由 BL1 的smc_handler64接住识别出是BL1_SMC_RUN_IMAGE后完成到 BL31 的最终跳转。这也解释了实测日志里NOTICE: BL1: Booting BL31这一行为什么是 BL1 打印的不是 BL2。7. BL1 退出前的最后状态把控制权交给 BL27.1 构造 entry_point_infobl1_prepare_next_image()bl1/aarch64/bl1_context_mgmt.c里这个函数按ENABLE_RME分成两个互斥版本。本仓库未设置ENABLE_RME第一篇已确认实际编译进去的是#else分支#elsevoidbl1_prepare_next_image(unsignedintimage_id){unsignedintsecurity_state,modeMODE_EL1;descbl1_plat_get_image_desc(image_id);next_bl_epdesc-ep_info;security_stateGET_SECURITY_STATE(next_bl_ep-h.attr);/* Prepare the SPSR for the next BL image. */if((security_state!SECURE)(el_implemented(2)!EL_IMPL_NONE)){modeMODE_EL2;}next_bl_ep-spsrSPSR_64(mode,MODE_SP_ELX,DISABLE_ALL_EXCEPTIONS);cm_init_my_context(next_bl_ep);cm_prepare_el3_exit(security_state);desc-stateIMAGE_STATE_EXECUTED;print_entry_point_info(next_bl_ep);// 对应日志里的 Entry point address / SPSR}#endif/* ENABLE_RME */BL2 的image_descBL2_IMAGE_DESCinclude/plat/common/common_def.h:105标记为SECURE所以mode值为MODE_EL1最终bl2_ep_info-spsr SPSR_64(MODE_EL1, MODE_SP_ELX, DISABLE_ALL_EXCEPTIONS)。即 BL2 在本配置下实际运行在 Secure-EL1S-EL1不是 EL3。ENABLE_RME1时才会走前面#if分支、把spsr设为MODE_EL3注释明确写to run BL2 in Root world, i.e. EL3, for the case when FEAT_RME is enabled——这是 RME/CCA 场景专属的优化路径本仓库没有走到。这与 TF-A 官方设计文档的描述一致BL1 passes control to the BL2 image at Secure EL1 (for AArch64) or at Secure SVC mode (for AArch32), starting from its load address.——trusted-firmware-a/docs/design/firmware-design.rst:314-315BL2_RUNS_AT_EL3是另一个独立开关仅当RESET_TO_BL21或ENABLE_RME1时为1本仓库两者都未启用BL2_RUNS_AT_EL30与上面的结论一致。7.2 真正的跳转bl1_entrypoint.S里的el3_exitbl1_main()返回后见第 5.1 节汇编执行b el3_exitlib/el3_runtime/aarch64/context.S:582它从 EL3 状态里恢复elr_el3/spsr_el3并执行ERETPC 跳转到bl2_ep_info记录的入口地址即bl2.bin被加载到的地址。这与日志中NOTICE: BL1: Booting BL2 NOTICE: BL2: v2.14.0(release):v2.14.0之间的跳转一致——Booting BL2是 BL1 最后的打印紧接着执行流已经切换到 BL2 的代码。7.3 用 GDB 实测交接 BL2 时的 SPSR 0x3c5断点 4bl1_prepare_next_image()末尾print_entry_point_info(next_bl_ep)构造完成尚未 ERET把局部变量next_bl_ep加进 WATCH 展开可以看到它指向的entry_point_info_t已被填好WATCH值说明next_bl_ep-spsr0x3c5构造的 BL2 SPSR此刻只存在于内存结构体里next_bl_ep-pc0xe05b000BL2 入口地址bl2.bin被加载到的位置$SPSR_EL3硬件寄存器0x0硬件SPSR_EL3还没被写——因为这个函数只填内存不碰msr spsr_el3$ELR_EL3硬件寄存器0x0同理ELR_EL3也还是 0所以停在这里$SPSR_EL3/$ELR_EL3仍为 0 是正常的把内存上下文搬进硬件寄存器是后面el3_exit的活。注next_bl_ep是局部变量continue 出bl1_prepare_next_image后 WATCH 里这行会变成-var-create: unable to create variable下面断点 5 截图底部可见属正常现象——详见附录 §9。断点 5el3_exit里exception_returnERET之前bl1_main()返回后执行b el3_exit它从cpu_context_t恢复 EL3 状态到硬件寄存器停在exception_returncontext.S:658即ERET那一刻WATCH断点4 → 断点5说明$SPSR_EL30x0→0x3c5内存里的0x3c5现在已被msr装进硬件SPSR_EL3——ERET会用它设置目标 PSTATEEL1h、DAIF 全屏蔽$ELR_EL30x0→0xe05b000同样装载完成等于断点4 里的next_bl_ep-pc——ERET的目标地址BL2 入口断点 4→5 的对比是 §7 结论的决定性证据SPSR_EL3从0x0变0x3c5、ELR_EL3从0x0变0xe05b000next_bl_ep-pc证明BL1 通过ERET、以SPSR0x3c5S-EL1把控制权交给 BL2 入口地址这一整条链。0x3c5解码为 S-EL1h 的推导见 §7.1。8. 本篇小结日志与代码的对应关系本篇完整走完了 BL1 从复位取指到交接 BL2 的执行上下文复位后从bl1_entrypoint起跑靠el3_entrypoint_common建立 EL3 基础环境异常向量表、C 运行时栈后交给 C 主函数退出前通过bl1_prepare_next_image()构造 BL2 的entry_point_info再由el3_exit把 SPSR/ELR 写入硬件寄存器、ERET跳转到 BL2。日志行 / 关键事实代码位置bl1.bin第0字节是bl1_entrypointbl1/bl1.ld.S*bl1_entrypoint.o(.text*)排在最前VBAR_EL3指向bl1_exceptionsel3_common_macros.S:235-236el3_entrypoint_common第3步15/16 个异常向量直接 panicbl1_exceptions.S各vector_entrySMC 分发入口bl1_exceptions.S:81-94 SynchronousExceptionA64→smc_handler64NOTICE: BL1: Booting BL2bl1_load_bl2()末尾紧接el3_exit跳转GDB 实测EL3 / SCTLR_EL3 / VBAR_EL3 / SP 的建立时序§5.1.3→§5.1.4→§5.2.1 断点 1→2→3§7.3 断点 4→5下一篇将具体展开bl1_main()中的详细流程详细分析从从串口初始化到加载 BL2 之间调用的每一个平台初始化函数。9. 附录GDB 调试踩坑速查表现象原因解决/结论汇编文件.S无法打断点VSCode 默认不允许在非已知语言文件里设断点.vscode/settings.json加debug.allowBreakpointsEverywhere: true$CurrentEL求值为void改用 WATCH 填($cpsr2)3WATCH 面板数值默认十进制位域寄存器看不直观gdb 默认output-radix 10DEBUG CONSOLE 执行-exec set output-radix 16改回用-exec set output-radix 10