U-Boot分析【学习笔记】(11)
2026/5/17 4:30:20 网站建设 项目流程

在U-Boot分析【学习笔记】(10)中我们分析完了 board_init_f ,接下来继续往下进行分析:

// 预处理阶段:判断是不是编译完整版 U-Boot#if!defined(CONFIG_SPL_BUILD)/* * “搭建中间过渡环境(建立新的栈指针 sp 和全局数据指针 gd) * 并调用 relocate_code(addr_moni) 函数(实施物理搬家)。 * 这里的绝妙绝招(Trick)在于:虽然等一下程序会跳过去执行代码拷贝, * 但当它返回时,我们将跳转到‘重定位(搬家)后’的全新 here 标签位置继续执行。” */ldr sp,[r9,#GD_START_ADDR_SP]/* sp = gd->start_addr_sp */#ifdefined(CONFIG_CPU_V7M)/* v7M forbids using SP as BIC destination */mov r3,sp bic r3,r3,#7mov sp,r3#elsebic sp,sp,#7/* 8-byte alignment for ABI compliance */#endif
ldr sp,[r9,#GD_START_ADDR_SP]/* sp = gd->start_addr_sp */

ldr(Load Register):从内存中加载数据到寄存器。
sp:ARM 架构中的栈指针寄存器(Stack Pointer,即 r13)。
[r9, #GD_START_ADDR_SP]变址寻址(寄存器相对寻址)
r9:我们在前面锁定的大管家,它此时仍然指向位于内部 SRAM 中的旧 global_data(旧账本)。
#GD_START_ADDR_SP:这是 struct global_data 结构体中 start_addr_sp 成员相对于结构体首地址的偏移量。
物理动作:
通过旧账本指针 r9 加上偏移量,找到里面记录的 gd->start_addr_sp 的值(这个值是前一步 board_init_f 在大容量的 DDR 顶端通过数学减法精确计算出来的、预留给新 C 语言运行环境的栈顶物理地址),然后将这个大内存地址直接轰入 CPU 的 sp 寄存器中。

#ifdefined(CONFIG_CPU_V7M)/* v7M forbids using SP as BIC destination */mov r3,sp bic r3,r3,#7mov sp,r3#elsebic sp,sp,#7/* 8-byte alignment for ABI compliance */

上一篇文章 10.1 章节 分析过,这是保证8字节对启。

10.2 修改 r9 指向新的GD结构体

ldr r9,[r9,#GD_BD]/* r9 = gd->bd */sub r9,r9,#GD_SIZE/* new GD is below bd */
  1. 第一行:利用旧的 r9 账本指针,读取里面记录的 gd->bd 的值,并将其直接覆盖写入 r9 寄存器中。
    背景
    在上一阶段的 board_init_f 数组中,执行了 reserve_global_data 和 reserve_board。U-Boot 在大容量的 DDR 内存最顶端圈出了两块地,分别用来存放克隆后的 新 GD(global_data) 和bd(bd_t 板级信息)。并且,当时的 SRAM 旧账本里已经登记了 gd->bd 指向 DDR 中 bd 的绝对物理地址。
    动作:
    此时的 r9 依然指着 SRAM 里的老家。此行指令利用变址寻址,以老 r9 为基基准,加上 bd 成员的偏移量 #GD_BD,把记录在里面的 DDR 版 bd 结构体的首地址 取出来,然后直接写入 r9 寄存器本身。
    结果:
    此时 r9 里的老 SRAM 地址被直接覆盖抹去,r9 指向外部 DDR 内存的顶端(指向了 bd 结构体)。
  2. 第二行:将当前的 r9 地址向下减去一个 GD_SIZE(即整个 global_data 结构体的大小)。因为根据内存规划,DDR 里的新 global_data 刚好紧挨在 bd(开发板信息结构体)的下方。
    内存规划如图:
    ——————————————————————————————————————
    【高地址】 ──── DDR 物理顶端 (RAM_TOP) ────

    ├─► bd_t 结构体 (开发板信息) ◄── [ 此时 r9 指向这里 ]

    ├─► gd_t 结构体 (DDR中的GD) ◄── [ r9 减去 GD_SIZE 后,精准定位到这里]

    ▼ 往下是重定位后的 U-Boot 代码段…
    【低地址】
    ——————————————————————————————————————
    gd_t(全称 Global Data Type)是整个引导程序中最核心、最特殊的全局数据结构体。
    总结:
    上电时,由于外部 DDR 还没亮,U-Boot 只能把 gd 结构体地放在片内 SRAM 里,这时候的 r9 寄存器里存的是 SRAM 的低地址。在 board_init_f 运行期间,DDR 被点亮了。U-Boot 在 DDR 的最顶端规划了新的空间,并且在核心子函数(setup_reloc)里,用memcpy,把 SRAM 里的旧 gd 拷贝到了 DDR 的新位置。
    此时,虽然 DDR 里有了一个全新的、一模一样的账本拷贝,但r9 寄存器由于还没被修改,它还是指向 SRAM 的老地址,所以这两行代码的作用就是令 r9 指向存放 gd 的 DDR 的新位置。

10.3 U-Boot 运行环境的切换

adr lr,here ldr r0,[r9,#GD_RELOC_OFF]/* r0 = gd->reloc_off */add lr,lr,r0

adr (Address):这是一条 ARM 汇编中的位置无关(Position-Independent)的相对寻址伪指令。它专门用来读取基于当前程序计数器 pc 相对偏移的某个标签的当前实际物理地址。
lr (Link Register):链接寄存器(即 r14),专门用来存放函数调用或者跳转后的返回地址。
here:紧接着后面几行代码下方的一个汇编标签。
动作:
获取当前运行状态下(此时代码还在低地址的 SRAM 里运行),here 标签的实际绝对物理地址,并把它存到 lr 寄存器中。

为什么需要这一步?
U-Boot 就要执行 b relocate_code 进行物理搬家了(把代码从 SRAM 完整拷贝到 DDR 顶端)。

#ifdefined(CONFIG_CPU_V7M)orr lr,#1/* 1. 硬件打补丁:Thumb-only 内核(如M4)强制将返回地址最低位置1,防止跳转返回时触发硬件状态异常 */#endifldr r0,[r9,#GD_RELOCADDR]/* 2. 传参:从 DDR 的 GD 结构体获取U-Boot 重定位的目标首地址,作为第一个参数打入 r0 */b relocate_code/* 3. 使用 b 指令(不破坏现有的 lr 寄存器),发起拷贝命令 */

relocate_code 是一个标准的汇编函数,它在复制后,最后一行必然会执行 mov pc, lr(即:函数执行完毕,弹回当初调用它的地方继续往下跑)。
致命问题:
如果我们在跳转前不对 lr 做任何修改,那么 lr 里面默认记录的将是 SRAM 里的 here 标签低地址。
结果:
当物理拷贝结束,函数返回时,CPU 会回到 SRAM 里去读下一行指令。这样一来,搭建并规划好的外部大内存 DDR 就失去了意义。
解决手段:
在进行拷贝前,我们现将 lr 寄存器的值改成 DDR 的顶端高地址,这样就不会回到 SRAM,而是直接跳转到我们刚准备好的 DDR 大内存中继续运行。
核心逻辑:
代码段在物理上被整体搬移了,所以任何一段代码、任何一个标签的“新地址”,都等于它在“原位置(SRAM)的地址”加上“偏移量”。

adr lr,here @1.指向物理低地址:lr=SRAM 里的 here 地址 ldr r0,[r9,#GD_RELOC_OFF]@2.得出偏移量: r0=gd->reloc_off add lr,lr,r0 @3.相加: lr=lr+r0

这样,我们就可以通过 here 标签,让 U-Boot 的运行环境从 SRAM 移动到 DDR 中了。

10.4 向量表重映射与 CPU 高速缓存清洗

here:/* * now relocate vectors */bl relocate_vectors/* Set up final (full) environment */bl c_runtime_cpu_setup/* we still call old routine here */

relocate_vectors:
背景:
虽然控制流(PC)已经成功跳转到 DDR 的 here: 标签,但 CPU 核心控制异常中断的 VBAR(Vector Base Address Register,向量基地址寄存器) 依然死板地指向刚上电时的 0x00000000(或 SRAM 低地址)。
措施:
必须立刻调用 bl relocate_vectors,将 DDR 中全新拷贝的向量表首地址强行写入 VBAR 寄存器,迫使 CPU 的硬件中断触角实时同步过渡到新大陆,重建系统的安全防御网。
为什么要切换到 DDR 的新地址?
在计算机底层硬件中,SRAM(静态随机存储器)是一种掉电才会丢失数据的存储介质。
当 U-Boot 执行 relocate_code 把数据复制到 DDR 时,它采用的是 “复制(Copy)” 而不是 “剪切(Cut)”。
物理现状:
芯片内部 SRAM 里的那份旧代码、旧 gd 账本、旧中断向量表,在此时依然在原来的物理地址上(例如 0x00918000 附近)。硬件上并没有任何机制去主动清空或者抹零这块区域。
软件现状:
虽然内容还在,但随着 sp(栈)、r9(账本)、pc(程序指针)和 VBAR(向量表)这四大核心硬件指针全面向 DDR 重定向,SRAM 里的这份旧代码没有地方会用到了,因此要把重新写入 VBAR 寄存器,指向 DDR 中的新地址。

核心硬件指针重定向前的物理状态(SRAM 运行期)重定向后的物理状态(DDR 稳定期)环境转场带来的物理与逻辑本质
PC(程序计数器)指向 SRAM 低地址空间,执行位置无关的初级引导代码。成功降落在 DDR 高地址的here:标签物理坐标。控制流执行环境的跨介质转场。标志着 U-Boot 正式脱离临时宿主,全面进入大内存的高速执行态。
SP(运行时栈指针)驻留在 SRAM 临时划分的栈区,容量极度受限。切换至 DDR 物理最顶端规整划分的正式栈顶区域。运行时后勤保障环境的安全着陆。为后续复杂的、多层嵌套的 C 语言业务函数调用提供了稳固、海量的堆栈空间。
R9(平台大管家)锁定在片内 SRAM 存储空间中的临时gd_t结构体首地址。刷新为外部 DDR 顶端克隆出的全新gd_t结构体绝对物理地址。核心配置上下文(Global Data)的物理交接。实现了生命周期中全局硬件探测账目、环境参数的全时空、跨阶段无缝传承。
VBAR(向量基地址寄存器)固化在芯片上电默认的底层基地址0x00000000(或只读 ROM 映射区)。通过当前指令,强行改写为 DDR 内存中全新异常向量表的首地址。异常与安全防御网络(中断捕获机制)的硬件级重映射。修正了 CPU 发生硬件突发中断时的寻址探针,确保在外部大内存运行期间,系统应急救援机制的绝对安全。
/* Set up final (full) environment */bl c_runtime_cpu_setup/* we still call old routine here */
  1. 为什么要强调 “we still call old routine here”?
    官方注释特意写了一句:“我们在这里依然调用旧的例程”
    历史背景:
    在老版本的 U-Boot 中,CPU 刚上电、代码还在 SRAM 跑的时候,会调用一次 cpu_init_cp15 或者是早期的 CPU 核心初始化。
    重定位后的悖论
    现在,代码已经物理克隆并重定向到了外部 DDR 的高地址运行(全新环境)。按理说,CPU 的体系架构(CP15 协处理器、Cache、MMU 状态)应该随着运行介质的改变而进行一次重新审视。
    架构师的妥协与严谨:
    为了保证跨平台的兼容性,U-Boot 开发者决定维持原样,在 DDR 里重新调用一次在低地址时跑过的 CPU 运行时配置例程。通过重用这段成熟的底层逻辑,确保 CPU 的核心硬件状态在转场后是绝对干净、稳定且符合预期的。
  2. c_runtime_cpu_setup 是完全重跑 cpu_init_cp15 吗?
    核心结论:
    不是,它只执行了一些核心的操作。
    物理本质:
    cpu_init_cp15 属于上电初期的“大动干戈”,包含了关闭 MMU 等底层硬件全局配置,这些状态在转场 DDR 后依然合法,无需重置。
    复用动作:
    c_runtime_cpu_setup 则是针对运行介质发生物理跨越(SRAM 到 DDR)这一特殊节点进行的微调。它精准复用了 cpu_init_cp15 中针对 ARM CP15 协处理器 的两大核心清场动作:
    Invalidate I-Cache(使指令缓存失效):
    清除 CPU 读写低地址 SRAM 时残留的 Cache 伪命中,确保后续取指探针百分百直达外部 DDR 新地址。
    Invalidate Branch Predictor(使分支预测失效):
    清除脑硬件预测器的旧跳转记忆,防止 CPU 按照低地址的历史经验对高地址 DDR 的指令流水线进行错误预判。

10.5 SPL 堆栈二次重定向与 BSS 清洗准备

#if!defined(CONFIG_SPL_BUILD)||defined(CONFIG_SPL_FRAMEWORK)//意味着本段全局环境的构建,只对完整版 U-Boot(Proper)//或者启用了现代通用框架的 SPL 开放#ifdefCONFIG_SPL_BUILD// 判断是否处于编译 SPL 阶段/* Use a DRAM stack for the rest of SPL, if requested */bl spl_relocate_stack_gd cmp r0,#0movne sp,r0 movne r9,r0#endifldr r0,=__bss_start/* this is auto-relocated! */

bl spl_relocate_stack_gd:
调用位于 common/spl/spl.c 中的 C 语言核心功能函数。该函数会检查用户是否在板级配置中启用了 CONFIG_SPL_STACK_R(要求 SPL 运行时重定向栈)。若满足,它会在 DDR 中重新规划一块安全空间,将 SRAM 的 旧 GD 结构体 memcpy 过去,并返回这个全新的 DDR 栈顶物理地址。
cmp r0, #0:
根据 ARM 的 C 语言函数调用规范(AAPCS),上述函数的返回值会通过 r0 寄存器 带回。此步用于校验 DDR 堆栈是否分配成功(即 r0 是否不为 0)。
movne sp, r0 与 movne r9, r0:
ne(Not Equal)条件执行。只有在分配成功,即 r0 != 0时,才会将当前全局运行时栈指针(SP)与GD结构体指针(R9)重定向至 DDR 的全新高地址空间。

为什么要在此时“二次重定向”堆栈?
对于完整版 U-Boot,我们在重定位阶段已经把 SP(运行时栈)和 R9(GD结构体地址)搬到了 DDR 的最顶端。但SPL(第二阶段引导程序)的生命周期却完全不同:
背景:
SPL 刚上电时,外部大容量 DDR 内存根本还未初始化。它是在芯片片内极其受限的 SRAM(通常仅有几十到几百 KB) 里艰难成长起来的,其初期的堆栈和 gd_t 只能扣扣搜搜地挤在 SRAM 的角落里。
物理转折点:
后来,SPL 在低地址的 C 语言世界里,终于把外部巨大的 DDR 内存给成功点亮了。
转移:
既然现在大容量的 DRAM(DDR)已经亮起,SPL 随后的任务是去加载、校验庞大的完整版 U-Boot 镜像,这极度消耗堆栈空间。为了不再受片内 SRAM 容量的物理限制,开发者迫切希望在进入 BSS 清洗前,把 SPL 随后的运行时堆栈和全局账本(GD)也一并升级迁移到宽敞的 DDR 内存中去。

ldr r0,=__bss_start/* 符号地址自动重定向! */
  1. 什么是 __bss_start?
    链接脚本的产物:__bss_start 是 U-Boot 链接脚本(.lds)里固化定义的一个全局符号。它不代表任何实际的机器指令,而是未初始化全局变量区(BSS 段)在内存中的起始物理地址
    原始属性:在编译器最初进行链接时,它被分配了一个基于低地址空间(如静态编译时的初始基地址)的绝对物理值。

  2. 怎么做到地址自动重定向的?
    既然链接时分配的是低地址(SRAM),而现在 U-Boot 已经物理搬家到了外部 DDR 的最顶端,为什么这一行简单的 ldr r0, =__bss_start 就能准确抓到 DDR 新家里 BSS 段的高地址,而不会跑回 SRAM 呢?
    这得益于前面 relocate_code 拷贝时,顺便处理的内容:.rel.dyn(动态链接重定位表)。
    修正:
    U-Boot 在物理拷贝镜像时,不仅搬运了代码,还通过一段循环,专门去扫描了 .rel.dyn 表。这个表里记录了代码中所有绝对地址符号的账目。
    自动重定向:
    重定位代码在当时就已经把 __bss_start 符号在全局符号表(GOT)里的数值,加上了整个 SRAM 到 DDR 的偏移量(gd->reloc_off)。
    逻辑闭环:
    所以,虽然现在看到的汇编源码和最初上电时一模一样,但经历过动态链接表的物理修正后,硬件执行这行指令时,r0 被加载进来的,已经是被自动修正(Auto-relocated)之后的、外部 DDR 内存中的全新 BSS 段物理起始首地址。

  3. 作用
    把这个高地址的 __bss_start 塞进 r0 寄存器,实际上是在为接下来的 C 语言全面接管运行环境做最后的准备:
    C 语言的规定:根据标准 C 语言规范,所有未初始化的全局变量(比如 int global_value;)在系统启动后,其默认值必须严格为 0。
    埋下伏笔:这里把 BSS 的起始首地址放入 r0,就是为了给紧随其后的清场循环(clbss_l: 循环)提供第一个核心边界参数。

10.6 BSS清零

#ifdefCONFIG_USE_ARCH_MEMSET/* 1. 自动重定向:加载 DDR 新家里 BSS 段的物理结束尾地址 */ldr r3,=__bss_end/* 2. 备弹:将抹零核心数值 0x00000000 放入 r1 寄存器 */mov r1,#0x00000000/* 3. 算力开销:r2 = r3 (__bss_end) - r0 (__bss_start),算出 BSS 段的总物理字节长度 */subs r2,r3,r0/* 4. 直接调用极致优化的 memset 汇编例程,瞬间完成全段抹零 */bl memset#else/* 1. 自动重定向:将 DDR 新家里 BSS 段的物理结束尾地址放入 r1 */ldr r1,=__bss_end/* 2. 将抹零核心数值 0x00000000 打入 r2 */mov r2,#0x00000000clbss_l:cmp r0,r1/* 1. 边界比对:判定当前游标(r0)是否触碰BSS尾边界(r1) */#ifdefined(CONFIG_CPU_V7M)itt lo/* 2. 架构微调:针对 Cortex-M 核心注入 IT (If-Then-Then) 指令块通报 */#endifstrlo r2,[r0]/* 3. 条件物理灌零:若 r0 < r1,将 0 写入当前内存(单次擦除4字节) */addlo r0,r0,#4/* 4. 条件游标自增:若 r0 < r1,游标自增 4 字节(Word跨度) */blo clbss_l/* 5. 循环复苏:若游标未到终点,则回到起始点继续循环处理 */
  1. 为什么 BSS 段必须在此时清零?
    在标准 C 语言规范中,所有未初始化的全局变量(如 int global_var;)或静态变量,其默认初始值必须严格为 0。
    这些变量在编译时都会被打包归类到 BSS 段 中。由于链接器在生成二进制镜像时,为了精简体积,并不会为 BSS 段真正分配物理的 0(只记录了它的起始和结束符号),因此在 U-Boot 准备完全跳转到 C 语言执行复杂业务之前,必须由汇编代码在内存中手动把这块区域全部刷成 0。
  2. 分支一:
    如果系统配置了 CONFIG_USE_ARCH_MEMSET,说明当前平台拥有经过硬件特性极致优化过的 memset 汇编函数。
    在执行 bl memset 前,寄存器的传参完全严格遵守了 ARM 官方的 C 语言调用法则:
    r0:已经在前一小节被写入了 __bss_start(抹零的起始目的地址)。
    r1:此时被写入了 0x00000000(要填充的目标数值)。
    r2:通过 subs 算出来的差值(要擦除的内存物理长度)。
    三大参数传入 memset 函数,高效率完成 BSS 清场。
  3. 分支二:
    如果平台未定义该宏,控制流则滑向 #else 分支。这里是在为后续的手工汇编循环抹零进行最后的参数准备。
    本段循环摒弃了传统的高频分支跳转,转而利用 ARM 架构特有的条件执行后缀(lo)。通过 strlo 与 addlo 在流水线内的平滑咬合,以单次 4 字节(Word)的物理步长,对外部内存中的 BSS 区间执行了全抹零,彻底满足了标准 C 语言运行时(Runtime)对未初始化全局变量的合规要求。
#if!defined(CONFIG_SPL_BUILD)//编译完整版 U-Boot(Proper)时才会生效bl coloured_LED_init/* 1. 硬件初始化:初始化板载多彩 LED 的 GPIO 控制引脚 */bl red_led_on/* 2. 状态机物理宣告:点亮红灯,标志着汇编阶段顺利结束 */#endif

10.7 接口对启

/* call board_init_r(gd_t *id, ulong dest_addr) */mov r0,r9/* gd_t */ldr r1,[r9,#GD_RELOCADDR]/* dest_addr */

在纯汇编的世界里,寄存器怎么用完全是自由的。但如果汇编想要成功调用一个 C 语言函数,就必须无条件遵守 AAPCS(ARM Architecture Procedure Call Standard) 规范。
该规范严格规定:
C 语言函数的前四个参数,必须依次通过 CPU 的硬件寄存器 r0、r1、r2、r3 进行传递。

C 语言端 board_init_r 的函数声明原型:

voidboard_init_r(gd_t*id,ulong dest_addr);

这就形成了一次完美的软硬件跨界对齐:
第一个参数 gd_t *id→ \rightarrow必须由硬件寄存器 r0 承载。
第二个参数 ulong dest_addr→ \rightarrow必须由硬件寄存器 r1 承载。

  1. mov r0, r9
    物理动作:
    将一直死死抓着 global_data 中央账本首地址的硬件寄存器 r9,将其中的数值(指针地址)全量复制一份,轰入寄存器 r0。
    目的:
    完成了 C 语言第一个入参 gd_t *id 的打包。这一步的物理意义在于,让 C 语言能立刻通过 r0 接管在第一阶段汇编拷贝并重定位到 DDR 高地址后的全新全局数据结构体(GD 结构体),实现了配置上下文的生命周期全跨越。
  2. ldr r1, [r9, #GD_RELOCADDR]
    物理动作:
    变址寻址加载指令。以 r9(GD 结构体基地址)为基准,加上一个在编译期就固定好的偏移量 #GD_RELOCADDR(对应 gd_t 结构体中的 relocaddr 成员变量的物理偏移量),将该内存里的数值(即 U-Boot 在 DDR 中重定位后的绝对目标首地址,例如 0x9FF00000)精准加载到寄存器 r1 中。
    目的:
    通过 r1 传递该重定位基地址,确立了 C 语言运行时的基地址感知,支撑其在初始化初期建立绝对精确的内存映射视图,这里的“基地址”指的是U-Boot自身代码段在 DDR 中的起始物理地址,而不是 gd 的地址。

10.8 架构兼容性适配

/* call board_init_r *//* call board_init_r */#ifdefined(CONFIG_SYS_THUMB_BUILD)ldr lr,=board_init_r/* 1. Thumb分支:加载自动重定位后的 C 函数绝对物理地址至 lr */bx lr/* 2. 状态切换:利用 bx 指令强行跨状态跳转 */#elseldr pc,=board_init_r/* 1. ARM分支:直接将自动重定位后的 C 函数绝对物理地址存入 PC! */#endif

分支一:高代码密度下的 CONFIG_SYS_THUMB_BUILD(Thumb-2 状态切换)
如果全局配置启用了 CONFIG_SYS_THUMB_BUILD,说明当前 U-Boot 镜像被编译为了 Thumb/Thumb-2 指令集 模式(通常为了极致压缩镜像体积,常见于 Cortex-M 核心或部分对尺寸极为敏感的资源受限平台)。

ldr lr,=board_init_r bx lr

ldr lr, =board_init_r:
由于 .rel.dyn 动态重定位表在前期已经对文字池执行了符号修正,此时加载进 lr(链接寄存器)中的地址,已经是 U-Boot 代码段搬移到外部 DDR 高地址后,board_init_r 函数的全新绝对物理坐标。
bx lr(Branch and Exchange):
在 ARM 体系架构中,如果涉及从 ARM 状态(32位指令)向 Thumb 状态(16/32位指令)的转场,绝不能使用普通的跳转指令。
bx 指令会强制检查目标地址 lr 的最低法定有效位(LSB,即 Bit[0]):
若 lr 的 Bit[0] 为 1,CPU 硬件会自动将当前状态寄存器(CPSR)的 T 标志位置位,瞬间将处理器切换为 Thumb 状态,并精准剥离掉 Bit[0] 开启指令对齐。
这种设计确保了即便前期的汇编代码运行在标准的 ARM 状态下,后续的 C 语言也能以高密度的 Thumb 模式全速、合规地运转。

分支二:常规清场 ldr pc, =board_init_r(ARM 状态)
如果平台运行在标准的 32 位 ARM 指令集下,控制流则滑向 #else。
① 为什么跨阶段跳转不用 bl 指令?
在正常的汇编调用中,我们习惯使用 bl board_init_r(带链接跳转)。
但 bl 指令有两个致命的底层硬伤,使其无法在这里使用:
跳转范围受限:
bl 指令的物理本质是相对寻址。它的机器码内部只有 24 位的偏移量,极限跳转范围只有± 32 MB \pm32\text{ MB}±32MB。而在重定位后,低地址的 SRAM 老家与高地址的 DDR 新家之间的物理跨度往往长达数 GB,bl 指令在硬件上根本无法跳转过去。
航向保护机制:
bl 会强制把返回地址写进 lr。但在此时,汇编时代已经进入了末尾,系统没有任何理由、也没有任何环境支持它回到汇编语言。
CPU 在执行 bl 指令时,锁存到 lr 寄存器里的,准确来说不是“低地址”,而是“紧随其后的下一行指令的绝对物理地址”。
在 U-Boot 启动的这个特殊节点上,它之所以表现为你所看到的“低地址”,是因为此时 U-Boot 还没有完成控制流的变轨,CPU 的物理探针仍然在低地址的 SRAM里取指运行。
② 存入 PC 指针的物理本质
ldr pc, =board_init_r 是一条绝对寻址加载指令:

  • 汇编器会将 board_init_r 函数在 DDR 新家里的绝对 32 位物理坐标(如 0x9FF00040)放入文字池中。
  • 硬件执行该指令的瞬间,直接将这个 32 位的绝对物理高地址存入 CPU 的中央指挥官——PC(程序计数器)寄存器。
  • 流水线彻底清洗(Pipeline Flush):这一动作将导致 CPU 内部现有的译码、执行等所有多级流水线彻底发生破坏性清空,强制硬件探针从外部大容量 DDR 内存的全新坐标点开始重新抓取机器码。这次变轨彻底斩断了控制流与原低地址空间的全部联系。

为何分支一使用 lr + bx,而分支二采用 pc ?
这是一个受制于 ARM 处理器状态机(CPSR)硬件解码切换机制的必然设计。其本质并非对寄存器有特定偏好,而是两种编译生态在硬件执行层面的天然鸿沟:

  1. 常规 ARM 分支的单向平铺(ldr pc)在未启用 Thumb 编译的 #else 分支下,汇编期与即将进入的 C 语言大本营均处于标准的 32 位 ARM 状态下。此时控制流仅面临 “跨空间寻址切换(SRAM→ \rightarrowDDR)” 的单一任务
    通过 ldr pc, =board_init_r 绝对寻址指令,直接向程序计数器强制灌入 32 位全权重物理坐标,即可在维持原有机器码解码器状态不变的前提下,平滑完成控制流跨 GB 级阻隔的跃迁。
  2. Thumb 高密度分支的双重蜕变(ldr lr + bx lr)当全局确立 CONFIG_SYS_THUMB_BUILD 宏时,控制流面临着 “跨空间寻址(SRAM→ \rightarrowDDR)” 与 “跨指令集状态变轨(32位 ARM→ \rightarrow16/32位混合 Thumb)” 的双重任务
    ldr pc 的硬件局限性:在 ARM 物理架构中,直接对 pc 寄存器实施赋值加载,绝不会触发处理器状态寄存器(CPSR)中 T 标志位的改变。
    若强行使用 ldr pc,CPU 将带着 ARM 的 32 位解码器去读取 DDR 里的 Thumb 高密度机器码,导致译码器逻辑瞬间崩溃,引发未定义指令异常(Undefined Instruction Abort)。
    bx 指令的晶体管嗅探本质:为了实现合规的状态跃迁,代码首先利用 ldr lr 将自带 LSB(最低有效位)为 1 的函数绝对目标符号地址锁存。
    随后执行 bx lr(Branch and Exchange)。
    bx 指令的内部硬线逻辑在运转时,会自动嗅探源寄存器的 Bit[0]:一旦确认为 1,硬件电路将原子级强制改写 CPSR 的 T 位,使处理器译码模块瞬间物理切轨至 Thumb 状态,随后抹除 Bit[0] 的标记位并赋给 pc。
    总结:
    分支二用 pc,是因为只需要搬移空间(Change Space);分支一用 lr 中转加 bx,是因为它必须同时完成搬移空间(Change Space)与切换状态(Exchange State)。

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

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

立即咨询