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

手撕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 就是 BootROM
  • 3. 动手准备:搭建 GDB 调试环境
  • 4. 链接与复位向量绑定:为什么 CPU 会从 bl1_entrypoint 开始执行
    • 4.1 链接脚本 `bl1/bl1.ld.S`
    • 4.2 QEMU 侧绑定
    • 4.3 RVBAR 是什么:复位取指地址从哪来
  • 5. 两段式结构:汇编入口 → 交接给 C
    • 5.1 汇编入口:`bl1/aarch64/bl1_entrypoint.S`
      • 5.1.1 入口全貌
      • 5.1.2 `el3_entrypoint_common`:BL1/BL31 共用的复位后初始化宏
      • 5.1.3 断点 1:`bl1_entrypoint` 入口——宏执行前的复位态
      • 5.1.4 断点 2:`plat_get_my_entrypoint`——`SCTLR_EL3` 刚写完
    • 5.2 交接给 C:`bl bl1_main`
      • 5.2.1 断点 3:`bl1_main`——宏全部走完,即将进 C 主函数
  • 6. 异常向量表:`bl1_exceptions.S`
    • 6.1 `smc_handler64`:区分 `BL1_SMC_RUN_IMAGE` 与其它 SMC
  • 7. BL1 退出前的最后状态:把控制权交给 BL2
    • 7.1 构造 entry_point_info:`bl1_prepare_next_image()`
    • 7.2 真正的跳转:`bl1_entrypoint.S` 里的 `el3_exit`
    • 7.3 用 GDB 实测:交接 BL2 时的 SPSR = 0x3c5
  • 8. 本篇小结:日志与代码的对应关系
  • 9. 附录:GDB 调试踩坑速查表

1. 引言

本篇聚焦"BL1 这段代码启动瞬间所处的硬件/软件环境":ROM 代码是怎么被跑起来的、C 环境建立前后经历了哪些步骤、异常向量表长什么样、以及 BL1 退出时把 CPU 状态交给 BL2 时的最后一刻快照。BL1 的 C 代码具体做了哪些初始化动作将在下一篇展开。

本篇不是纯讲原理,而是"手撕"——先搭好 GDB 调试环境(第 3 节),然后在 BL1 启动的每个关键节点打断点、看寄存器,用实测值一步步印证"复位→建 C 环境→交接 BL2"这条链。

调试环境:gdb-multiarch+ QEMU gdbstub + VSCodecppdbg图形化界面,符号来自bl1.elf(debug 构建)。启动方式见第 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 就是 BootROM

BL1 是 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:1234);VSCode 侧靠.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// :133

4.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 =0x0",0x0从哪来——它来自RVBAR(Reset 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 Register(Arm A-profile Architecture Registers

IMPLEMENTATION 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摆在0x0bl1.bin。换一颗真实 SoC,这里可能是某个 Boot ROM 地址,BL1 就相应烧在那里。

5. 两段式结构:汇编入口 → 交接给 C

下图是 BL1 汇编入口的整体流程——主干:复位 →bl1_entrypointel3_entrypoint_common10 步 →bl bl1_mainel3_exit/ERET 进 BL2;支路:运行期 BL2 通过smc(BL1_SMC_RUN_IMAGE)重入smc_handler64

5.1 汇编入口:bl1/aarch64/bl1_entrypoint.S

BL1 的第一段代码全在这个汇编文件里:先靠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_sctlr=1 \ // 初始化 SCTLR_EL3 _warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS \ _secondary_cold_boot=!COLD_BOOT_SINGLE_CPU \ _init_memory=1 \ // 清零 BSS 等 _init_c_runtime=1 \ // 建立 C 运行时栈 _exception_vectors=bl1_exceptions \ _pie_fixup_size=0 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_common:BL1/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_ADDRESS=1是否需要靠软件读 mailbox 变量区分冷/热启动。QEMUvirt复位地址不可编程(每次复位固定跳到同一地址),必须靠这层判断;若复位地址可编程,热启动时硬件直接把 PC 设到正确入口,就不需要这一步
_secondary_cold_boot!COLD_BOOT_SINGLE_CPU=1是否需要区分主/从 CPU。多核冷启动时所有核会一起跑到这段代码,只应有一个主核继续做平台初始化,其余核被晾在一边等待唤醒;若平台保证冷启动只有单核参与,这层判断可跳过
_init_memory1调用platform_mem_init(QEMUvirt平台该函数是空实现,仅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):

  1. 初始化SCTLR_EL3(因为_init_sctlr=1):设小端字节序,关闭WXN/栈对齐检查/内存对齐检查/DSSBS
  2. PIE fixup 整段跳过(因为_pie_fixup_size=0
  3. 设置VBAR_EL3 = bl1_exceptions:写入异常向量表地址,之后任何异常才有正确的处理入口(第 6 节展开这张向量表的具体内容)
  4. call_reset_handlerasm_macros.S:367):调用plat_reset_handler+ CPU 特定的cpu_ops复位处理(勘误表 workaround 等)
  5. el3_arch_init_common再次设置SCTLR_EL3(补充I-cache/SA/A使能位),并初始化SCR_EL3MDCR_EL3CPTR_EL3
  6. setup_el3_execution_context:配置 EL3 自身独立于其它 world 的执行环境(DAIF.AMDCR_EL3.SDDSCR_EL3.EA/SIFPMCR_EL0.DPCPTR_EL3.EZ/ESMDIT等控制位)——这一步只写控制寄存器,不涉及栈
  7. 区分主/从 CPU(因为_secondary_cold_boot=1):调用plat_is_my_cpu_primary判断,从核进入plat_secondary_cold_boot_setup(QEMU 平台实现为在poll_mailboxwfe自旋等待,见plat/qemu/common/aarch64/plat_helpers.S:64-86),只有主核继续往下走
  8. platform_mem_init(因为_init_memory=1,QEMU 平台是空操作)
  9. C 运行时初始化(因为_init_c_runtime=1):zeromem清零.bss(以及USE_COHERENT_MEM时的 coherent memory 段),必要时做 data section 的 ROM→RAM 搬运
  10. 切到SP_EL0msr spsel, #0),调用plat_set_my_stack分配并设置 C 运行时栈——这一步无条件执行,不受上述任何参数控制,且在 C 运行时初始化(第 9 步)之后才发生

做完这一整套,才会执行到bl1_entrypoint.S里的bl bl1_main——即bl1_main.c:50bl1_main()函数入口(§5.2)。

5.1.3 断点 1:bl1_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 字节
($cpsr>>2)&30x3BL1 在EL3起跑($cpsr=0x400003cd
$RVBAR_EL30x0virt不设RVBAR,复位 PC 取默认0x0
$VBAR_EL30x0异常向量表还没装(msr vbar_el3在更靠后的指令)
$sp0x0C 运行栈未建

5.1.4 断点 2:plat_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_EL30xc508380x30c50830显式初始化了SCTLR_EL3
$x00x30c50830正是刚msrSCTLR_EL3的那个立即数
$VBAR_EL3/$sp仍为0x0向量表、C 栈都还没轮到

SCTLR_EL3的值0x30c50830逐位解码(M/A/C/SA/I/WXN/EE/DSSBS 全 0,其余为架构 RES1 位)。

5.2 交接给 C:bl bl1_main

宏 10 步全部走完后,bl1_entrypoint.S执行bl bl1_main(§5.1.1 汇编清单里的那一行),把控制权交给 C 主函数。跨过这道门槛后,bl1_main()内部究竟调用了哪些平台初始化函数(plat_setup_early_consolebl1_plat_arch_setupbl1_arch_setup、加载 BL2……),将会在下篇内容中介绍;本小节只用一个断点确认"进 C 之前,宏承诺的 C 运行环境(异常向量表 + 栈)已经就绪"。

5.2.1 断点 3:bl1_main——宏全部走完,即将进 C 主函数

停在bl1_entrypoint.Sbl bl1_main(源码第 45 行)——此时宏 10 步已全部完成:

相比断点 2 的两处关键变化:

WATCH印证
$VBAR_EL30x00x5000§5.1.2 第3步已执行:VBAR_EL3现在指向bl1_exceptionsnm确认其地址正是0x5000,见第 6 节)
$sp0x00xe0ef140 <bl1_cpu_context>§5.1.2 第10步已执行:C 运行栈已建好,所以下一步才敢bl bl1_main进 C 代码
$SCTLR_EL30x30c5183a相比断点2的0x30c50830el3_arch_init_common(第5步)补写了I/SA/A等使能位
($cpsr>>2)&30x3仍在 EL3

断点 1→2→3 三张图连起来看SCTLR_EL3(默认→写入→补位)、VBAR_EL3(0→0x5000)、SP(0→栈地址)三个量的演变,正好对应 §5.1.2 宏"初始化 SCTLR → 装向量表 → 建栈"的执行顺序,是这一节叙述的完整实测证据链。

第 7 节(交接 BL2 时SPSR=0x3c5)的实测放在 §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, AArch32
SynchronousSynchronousExceptionSP0(L25,panic)SynchronousExceptionSPx(L53,panic)SynchronousExceptionA64(L81,SMC 分发)SynchronousExceptionA32(L118,panic)
IRQIrqSP0(L31,panic)IrqSPx(L59,panic)IrqA64(L96,panic)IrqA32(L124,panic)
FIQFiqSP0(L37,panic)FiqSPx(L65,panic)FiqA64(L102,panic)FiqA32(L130,panic)
SErrorSErrorSP0(L43,panic)SErrorSPx(L71,panic)SErrorA64(L108,panic)SErrorA32(L136,panic)

16 个向量里 15 个是同一套"死路":都是mov x0, #<异常编号> / bl plat_report_exception / no_ret plat_panic_handler——打印异常编号后直接 panic,没有任何恢复逻辑。这符合 BL1 的定位:一段极简的启动代码,运行期间不应该发生任何这些异常,一旦发生就是不可恢复的错误。

唯一有实际处理逻辑的是SynchronousExceptionA64bl1_exceptions.S:81-94):

vector_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_EL3(Exception Syndrome Register)的 EC(Exception Class)字段,判断是否为EC_AARCH64_SMC;不是则跳到unexpected_sync_exception(本质还是plat_report_exception+ panic),是则跳转smc_handler64

6.1smc_handler64:区分BL1_SMC_RUN_IMAGE与其它 SMC

smc_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_IMAGESCR_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)跳转到目标镜像。
  • 其它 SMC(x0 != 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 退出前的最后状态:把控制权交给 BL2

7.1 构造 entry_point_info:bl1_prepare_next_image()

bl1/aarch64/bl1_context_mgmt.c里这个函数按ENABLE_RME分成两个互斥版本。本仓库未设置ENABLE_RME(第一篇已确认),实际编译进去的是#else分支:

#elsevoidbl1_prepare_next_image(unsignedintimage_id){unsignedintsecurity_state,mode=MODE_EL1;desc=bl1_plat_get_image_desc(image_id);next_bl_ep=&desc->ep_info;security_state=GET_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)){mode=MODE_EL2;}next_bl_ep->spsr=SPSR_64(mode,MODE_SP_ELX,DISABLE_ALL_EXCEPTIONS);cm_init_my_context(next_bl_ep);cm_prepare_el3_exit(security_state);desc->state=IMAGE_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-EL1(S-EL1),不是 EL3。ENABLE_RME=1时才会走前面#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-315

BL2_RUNS_AT_EL3是另一个独立开关(仅当RESET_TO_BL2=1ENABLE_RME=1时为1),本仓库两者都未启用,BL2_RUNS_AT_EL3=0,与上面的结论一致。

7.2 真正的跳转:bl1_entrypoint.S里的el3_exit

bl1_main()返回后(见第 5.1 节汇编),执行b el3_exitlib/el3_runtime/aarch64/context.S:582),它从 EL3 状态里恢复elr_el3/spsr_el3并执行ERET,PC 跳转到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

断点 4:bl1_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。

断点 5:el3_exitexception_returnERET)之前

bl1_main()返回后执行b el3_exit,它从cpu_context_t恢复 EL3 状态到硬件寄存器,停在exception_returncontext.S:658,即ERET)那一刻:

WATCH断点4 → 断点5说明
$SPSR_EL30x00x3c5内存里的0x3c5现在已被msr装进硬件SPSR_EL3——ERET会用它设置目标 PSTATE(EL1h、DAIF 全屏蔽)
$ELR_EL30x00xe05b000同样装载完成,等于断点4 里的next_bl_ep->pc——ERET的目标地址(BL2 入口)

断点 4→5 的对比是 §7 结论的决定性证据SPSR_EL30x00x3c5ELR_EL30x00xe05b000(=next_bl_ep->pc),证明"BL1 通过ERET、以SPSR=0x3c5(S-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.Svector_entry
SMC 分发入口bl1_exceptions.S:81-94 SynchronousExceptionA64smc_handler64
NOTICE: 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 填($cpsr>>2)&3
WATCH 面板数值默认十进制,位域寄存器看不直观gdb 默认output-radix 10DEBUG CONSOLE 执行-exec set output-radix 16;改回用-exec set output-radix 10