Lazy loaded image
Words 0Read Time 1 min
Invalid Date
u-boot加载启动内核过程可以大致分为两个阶段上,接下来我们将详细分析u-boot源代码(版本号为2019.04)。
notion image
u-boot启动第一阶段流程图如下所示:
notion image
u-boot在第一阶段进行重定位的时候会将其第二阶段的整个 u-boot 重定位到内存中,内核也一样,因为内核是要运行在 DDR 中的,因此就要将内核重定位到 DDR 中。
对于imx6ull而言,其第一阶段对应的文件时arch/arm/cpu/armv7/start.S和arch/arm/cpu/armv7/lowlevel_init.S

1. 从链接脚本说起

U-Boot 链接段布局(u-boot.lds

以下图表展示 u-boot.lds 各段在内存中的布局,重点标注 __image_copy_start__image_copy_end 的拷贝范围:
段名
基地址 (源)
近似大小
属于 image_copy?
内容说明
.text
0x87800000
~384KB
✅ 是
vectors.S (异常向量表) + start.o + 其他代码
.rodata
0x87860000
~128KB
✅ 是
只读常量、字符串
.data
0x87880000
~256KB
✅ 是
已初始化全局/静态变量
.u_boot_list
✅ 是
u-boot command 等自有函数表
.__image_copy_end
标记
⬛ 边界
relocate_code 复制终点
.rel.dyn
❌ 否
动态重定位表(fixloop 遍历修正)
.mmutable
❌ 否
MMU 页表
.bss
0x878C0000
~256KB
❌ 否 (OVERLAY)
未初始化数据,重定位后清零
💡
绿色行 = __image_copy_start__image_copy_end 范围,即 relocate_code 物理拷贝的全部内容。

U-Boot 拷贝源与目标位置

项目
源地址 (Flash/DDR 低地址)
目标地址 (DDR 高地址)
说明
image_copy 整体
0x87800000 (__image_copy_start)
0x9FE70000 (gd->relocaddr)
relocate_code 逐 8 字节 copy_loop 拷贝
├ .text
0x87800000 – 0x8785FFFF
0x9FE70000 – 0x9FECFFFF
异常向量表 + 启动代码 + 功能代码
├ .rodata
0x87860000 – 0x8787FFFF
0x9FED0000 – 0x9FEEFFFF
只读数据随代码一起搬运
├ .data
0x87880000 – 0x878BFFFF
0x9FEF0000 – 0x9FF2FFFF
已初始化变量,搬运后 fixloop 修正指针
└ .u_boot_list
紧随 .data 之后
对应偏移
命令表、驱动表等
.rel.dyn
不拷贝
仅遍历修正目标地址中的绝对引用
.bss
不拷贝
紧随 image_copy 之后
符号地址通过重定位表自动映射,随后 memset 清零
为什么 U-Boot 要重定位到 DDR 高地址?
根本目的:为 Linux 内核腾出低地址的连续大块内存。 Linux 内核通常被加载到 DDR 低地址区域(i.MX6ULL 上 zImage 典型加载地址为 0x80008000)。如果 U-Boot 留在低地址(0x87800000),内核解压/加载时极可能覆盖正在运行的 U-Boot 代码,导致系统崩溃。
board_init_f 中的 reserve_* 系列函数从 ram_top(0xA0000000)向下依次预留页表、U-Boot 代码、malloc 堆、GD、FDT、栈等,U-Boot 自身 + 运行时数据仅占顶部约 18MB,底部 ~494MB 完整保留给内核使用
其他收益:
  • 统一启动介质差异:无论从 NOR Flash、NAND、SD 卡还是 eMMC 加载到哪个初始地址,重定位后始终运行在 DDR 高地址的固定位置,简化后续逻辑。
  • 位置无关性验证:重定位过程本身验证了 U-Boot 的 PIC 机制(.rel.dyn 修正逻辑)正确工作。
  • 访问局部性:代码、堆、栈都集中在 DDR 高地址区域,cache 命中率更高。
上面是总的链接脚本u-boot.lds,将_start作为入口点,_start arch/arm/lib/vectors.S 中定义异常向量表, 当 cpu 产生异常时,便会将对应的异常入口地址加载到 pc 中,进而处理相应的异常处理程序。其中复位异常向量指令“b resets”决定了 u-boot 启动或者复位后将自动跳转到 resets 标志处执行。

2. 跳转到复位处理程序

接着分析一下 resets 做了哪些工作,全局搜索我们发现 resets 其实就定义在arch/arm/cpu/armv7/start.S文件中

2.1 分析 reset

分析下面代码 reset 中只有一条跳转指令 “b save_boot_params”

2.2 分析save_boot_params

save_boot_params 中只有一条跳转指令 b save_boot_params_ret

2.3 分析 save_boot_params_ret

2.3 寄存器操作汇总(缩写 + 功能)

寄存器
全称 / 缩写含义
关键字段(常见位)
本段代码做了什么
目的 / 效果
CPSR
Current Program Status Register(当前程序状态寄存器)
M[4:0]:CPU 模式位(如 SVC、IRQ、FIQ、HYP) • I:IRQ 屏蔽位(1=禁止 IRQ) • F:FIQ 屏蔽位(1=禁止 FIQ)
mrs r0, cpsr 读取 CPSR • 清除并设置 M[4:0]0x13(SVC32)(若非 HYP) • orr r0, r0, #0xc0 置位 I/F(屏蔽 IRQ/FIQ) • msr cpsr, r0 写回
• 进入可控的管理模式(SVC)执行启动流程 • 禁止中断,避免早期初始化阶段被异步打断
SCTLR
System Control Register(系统控制寄存器,CP15 c1)
V(bit13):异常向量表基址选择(0=低向量 0x00000000,1=高向量 0xFFFF0000) • 其他位常用于 MMU/Cache/对齐等全局控制(本段重点是 V)
mrc p15,0,r0,c1,c0,0 读 SCTLR • bic r0, #CR_V 清除 V 位(选择低向量) • mcr p15,0,r0,c1,c0,0 写回
• 选择低向量地址空间(便于后续通过 VBAR 指向实际向量代码) • 为异常向量重映射做准备
VBAR
Vector Base Address Register(向量表基地址寄存器,CP15 c12)
• 保存异常向量表的基地址(ARMv7-A/R 支持)
ldr r0, =_start 取向量表入口 _start 地址 • mcr p15,0,r0,c12,c0,0 写 VBAR
• 将异常向量表基址指向 U-Boot 镜像中的 _start(无需把向量表物理拷贝到 0x0) • 确保异常发生时跳转到正确的向量入口
CP15
ARM 系统控制协处理器(Coprocessor 15),提供 MMU/Cache/TLB/向量表等控制接口
• 通过 mrc/mcr 访问 CP15 寄存器组(如 SCTLR、VBAR、TLB/Cache 维护寄存器)
• 本段通过 mrc/mcr 访问 SCTLRVBAR
• 在早期启动阶段,用最小代价完成系统关键控制寄存器配置
CP15
ARM 系统控制协处理器(Coprocessor 15),提供 MMU/Cache/TLB/向量表等控制接口
• 通过 mrc/mcr 访问 CP15 寄存器组(如 SCTLR、VBAR、TLB/Cache 维护寄存器)
• 本段通过 mrc/mcr 访问 SCTLRVBAR
• 在早期启动阶段,用最小代价完成系统关键控制寄存器配置
⛓️
三步操作的执行顺序与因果链
这三个寄存器的配置顺序不可调换,存在严格的因果依赖:
① CPSR → ② SCTLR → ③ VBAR
  1. 先 CPSR:将 CPU 切换到 SVC32(管理模式) 并屏蔽中断。只有在特权模式下才有权限通过 mcr/mrc 访问 CP15 协处理器寄存器(SCTLR、VBAR 均属于 CP15)。若仍处于 User 模式则写 CP15 会触发未定义指令异常。
  1. 再 SCTLR:清除 V 位(bit13),选择低向量地址空间(0x00000000)。这一步是 VBAR 生效的前提条件——当 V=1 时,CPU 固定从 0xFFFF0000 取异常向量,VBAR 寄存器的值被忽略;只有 V=0 时,CPU 才从 VBAR 指定的基地址取向量。
  1. 最后 VBAR:将 _start(0x87800000)写入 VBAR。此后异常发生时,CPU 直接跳转到 U-Boot 镜像中的向量表,而非物理地址 0x0。

2.3.1 通过操作CPSR寄存器,执行下列操作:

  1. 将cpu的工作模式设置为SVC32模式(即管理模式);
  1. 屏蔽IRQ和FIQ的中断。

2.3.2 通过操作SCTLR寄存器,执行如下操作:

  1. 设置异常向量表的及地址为0x00000000,且支持重映射;
  1. 向量表重定位到 _start 地址(uboot的运行介质 - norflash nandflash sram等,映射地址可能不在0x0 起始的地址)。
继续分析接下来的汇编代码

2.3.3 配置协处理器cpu_init_cp15 ,执行如下操作:

  1. 使整个数据和指令TLB、指令缓冲、分支预测无效,清空写缓冲区和预取缓冲区。
  1. 禁止内存管理单元mmu、地址对齐检查、数据缓冲。打开ARM系统的跳转预测(分支预测)功能,不打断流水线,提高指令执行效率。(数据cache一定要关闭,避免缓存不一致,指令cache可保留,以提高操作效率。)

2.3.4 配置寄存器 cpu_init_crit,执行如下操作:

lowlevel_init:与特定开发板相关的初始化函数,会做 pll 初始化, 如果不是从内存启动,则会做内存初始化,方便后续拷贝到内存中运行。 设置栈指针指向CONFIG_SYS_INIT_SP_ADDR;

2.3.5 _main,执行如下操作:

⚠️
_main 包括 1. 执行uboot第一阶段,为重定位做准备; 2. 执行重定位代码 3. 执行uboot第二阶段,板级初始化、emmc初始化、控制台初始化、中断初始化及网络初始化
其中第2部分另起一个小节 2.4 分析 relocate_code 讲解
其中第3部分在
Uboot 启动第二阶段
讲解
_main 说明文档
  1. board_init_f_alloc_reserve:从初始 SP 向下预留 early malloc + GD 空间
  1. board_init_f_init_reserve:将 GD 区域清零,返回 early malloc 基地址
📐
CONFIG_SYS_INIT_SP_ADDR = 0x0091FF00
该值定义在板级头文件 include/configs/mx6ullevk.h 中,计算方式:
CONFIG_SYS_INIT_RAM_ADDR (0x00900000) + CONFIG_SYS_INIT_RAM_SIZE (0x20000) - GENERATED_GBL_DATA_SIZE (0x100) = 0x0091FF00
即 OCRAM 顶部(0x0091FFFF)向下偏移 256 字节处,预留了一个 GD 大小的安全边界。
为什么 GD/栈放在 OCRAM 而非 DDR? 更准确地说,不是"DDR 不可用"——硬件级 DDR 初始化已由 Boot ROM 通过 DCD 完成,U-Boot 代码实际上已经在 DDR 中执行(加载地址 0x87800000 就在 DDR 范围内)。真正的原因是:board_init_f 需要 GD 来记录 DDR 大小和布局规划结果gd->ram_sizegd->relocaddr 等),而这些信息要等到 dram_init 探测/确认后才可用。在 dram_init 之前,U-Boot 不知道 DDR 有多大、该如何规划内存,因此 GD/栈必须先放在 OCRAM 这个无需任何软件初始化即可使用的存储器中。
💾
GD(Global Data)区域含义
struct global_data(别名 gd_t,大小 GD_SIZE = 0x100 = 256 字节)是 U-Boot 启动全过程的核心状态结构体,通过寄存器 r9 全局访问。关键字段包括:
  • gd->ram_size — DDR 总容量(512MB)
  • gd->relocaddr — 重定位目标地址
  • gd->start_addr_sp — 重定位后新栈顶
  • gd->mon_len — U-Boot 镜像总长度
  • gd->fdt_blob — 设备树地址
  • gd->new_gd — 重定位后 GD 新地址(DDR 高地址区)
board_init_f 阶段,GD 存放在 OCRAM;完成内存规划后,setup_reloc 将其搬运到 DDR 高地址的 new_gd 区域。

OCRAM 早期分配布局(board_init_f 之前)

起始地址
结束地址
容量
内容
说明
0x00900000
0x0091F9FF
~126KB
SPL / BootROM 工作区
启动早期代码和数据(参见
0x00900000
0x0091F9FF
~126KB
SPL / BootROM 工作区
启动早期代码和数据(参见
0x0091FA00
0x0091FAFF
256B
GD(global_data)
r9 指向此处,board_init_f_init_reserve 将其清零
0x0091FB00
0x0091FEFF
1KB
Early malloc arena
CONFIG_SYS_MALLOC_F_LEN = 0x400,DDR 初始化前的临时堆
0x0091FF00
0x0091FFFF
256B
初始栈空间
CONFIG_SYS_INIT_SP_ADDR,向下增长
以上总结:初始化c语言环境,以便调用board_init_f函数。
board_init_f - 初始化 boot_flags 和 have_console 标志位,运行 initcall_run_list (遍历执行 init_sequence[ ] 中函数,初始化uboot前半段)。
下面init_sequence_f包括哪些函数:
  • setup_mon_len - 根据.lds 文件中 __bss_end 与 __bss_end 计算出 u-boot 本身的大,赋给gd->mon_len变量;
  • fdtdec_setup - 检查 gd->fdt_blob 处是否存在 dtb;
  • env_init - 使用 default_environment[] 数组初始化 env_addr 和 env_valid 状态;
  • init_baud_rate - 设置默认串口波特率;
  • serial_init - 设置 flags 串口准备好标志,返回当前指向的串口设备指针;
  • console_init_f - 设置 have_console 标志,初始化 console;
  • display_options - 打印横幅信息;
  • display_text_info - 打印代码段基地址、BSS 段起始地址以及 BSS 段末尾地址
  • show_board_info - 从 fdt 中获取 model 信息并打印;
  • announce_dram_init - 打印 "DRAM: ";
  • dram_init - 获取 dram 大小,并设置 dram_size;
  • setup_dest_addr - 初始化 ram_size,ram_base,ram_top,relocaddr,DRAM 的基地址为0x80000000,大小为 0x20000000(512M),RAM顶端地址为 0xA0000000 = 0x80000000 + 0x20000000(512M)。 重定位后地址为 0x9FE70000;
  • reserve_round_4k - 内存指针指向下一个4kB处,也就是4kB对齐;
  • reserve_mmu - 为 MMU 一级页表(L1 Translation Table)预留 16KB 空间,做 64KB 对齐后赋值给 gd->relocaddr;
⚠️
易混淆概念辨析:TLB ≠ 页表
  • 页表(Page Table):存放在 DDR 中的数据结构,记录完整的虚拟地址→物理地址映射。由软件(U-Boot / 内核)创建和维护。MMU 硬件通过 TTBR 寄存器读取页表基地址,再从 DDR 中查表完成地址翻译。
  • TLB(Translation Lookaside Buffer):CPU 片上硬件缓存,缓存最近使用的页表查询结果。由硬件自动管理,软件仅能执行 invalidate 操作,无需也无法为其分配内存
  • MMU(Memory Management Unit):CPU 内部的地址翻译单元。工作流:虚拟地址 → TLB 命中则直接返回 → 未命中则 MMU 执行 table walk(从 DDR 读页表)→ 结果回填 TLB。
  • reserve_mmu 预留的是 DDR 中的页表空间,不是 TLB。 U-Boot 源码注释中的 "TLB" 是历史遗留用词,实际指 translation table。
为什么是 16KB?
ARMv7 一级页表采用段式映射(Section mapping,每段 1MB),覆盖 4GB 地址空间需要 4096 个条目,每个条目 4 字节:4096 × 4B = 16KB。ARM 架构要求一级页表基地址 16KB 对齐,U-Boot 实际做 64KB 对齐以满足更严格的兼容性要求。
为什么页表放在 DDR 而非片上 SRAM?
i.MX6ULL 的 OCRAM 仅 128KB,而页表 + 后续内核页表扩展会持续占用内存。DDR 容量充足(512MB),是唯一合理的长期存放位置。
为什么 U-Boot 只建立一级页表?
U-Boot 作为 bootloader,仅需粗粒度的内存映射(1MB 段映射)来开启 cache 加速启动过程,不需要 4KB 粒度的二级页表。精细的二级页表由 Linux 内核接管后自行建立。
  • reserve_video - 空;
  • reserve_trace - 空;
  • reserve_uboot - 为 uboot 分配空间,同时做4kB对齐;
  • reserve_malloc - 为 malloc 腾出一段空间, malloc 空间16MB, Env空间 64KB;
  • reserve_board - 给bd预留空间,存放板子信息,如 DRAM 起始地址、DRAM大小、SRAM 起始地址、SRAM 大小、boot 参数等,共预留了80字节,gd->start_addr_sp = 0x9eef1fb0;
  • setup_machine - 空;
  • reserve_global_data - 预留256字节给 new_gd (新的全局数据);
  • reserve_fdt - 预留 40032 字节存放设备树信息;
以上内存分配完成。
  • reloc_fdt - 负责将设备树数据搬运到新分配的 new_fdt 地址中去;
  • setup_reloc - 将gd重定位到 new_gd 中
💡
init_sequence_f 执行情况
  • 执行前: DDR未初始化,使用OCRAM临时环境
  • 执行中: DDR初始化,完成内存布局规划
  • 执行后: 为重定位做好完整准备,但代码仍在原位置
以上board_init_f 中的内容就已经分析完了。
执行完成后,内存布局从高地址到低地址规划如下(以 i.MX6ULL 为例):
起始地址
结束地址
容量
内容
说明
0xA0000000
0xA0000000
-
RAM顶部
gd->ram_top (512MB DDR终点)
0x9FFF0000
0x9FFFFFFF
64KB
MMU 一级页表区域
L1 Translation Table(实际 16KB,64KB 对齐)
0x9FE70000
0x9FEEFFFF
~512KB
U-Boot代码区域
gd->relocaddr,重定位目标
0x9EE70000
0x9FE6FFFF
16MB
malloc堆区域
TOTAL_MALLOC_LEN=16MB
0x9EE6FF00
0x9EE6FFFF
~256B
板信息结构
sizeof(bd_t)
0x9EE6FE00
0x9EE6FEFF
~256B
新全局数据
sizeof(gd_t)
0x9EE6E000
0x9EE6FDFF
~8KB
设备树区域
如果不是嵌入式DT
0x9EE6D000
0x9EE6DFFF
4KB
启动阶段数据
如果启用bootstage
0x9EE6BFF0
0x9EE6CFFF
~4KB
栈对齐区域
16字节对齐
...
0x9EE6BFF0
-
新栈顶
gd->start_addr_sp
0x80000000
0x9EE6BFEF
~494MB
可用内存
Linux内核使用区域
以下 uboot 自身的重定位和 bss 段的初始化。

2.4 分析 relocate_code

🔄
relocate_code 主要作用
将 U-Boot 镜像从 DDR 低地址(0x87800000)物理拷贝到 DDR 高地址(gd->relocaddr),并修正所有绝对地址引用。具体分三步:
  1. copy_loop — 逐 8 字节将 __image_copy_start__image_copy_end 范围的 .text/.rodata/.data 段复制到目标地址
  1. copy_loop — 逐 8 字节将 __image_copy_start__image_copy_end 范围的 .text/.rodata/.data 段复制到目标地址
  1. fixloop — 遍历 .rel.dyn 重定位表,将目标代码中所有 R_ARM_RELATIVE 类型的绝对地址加上偏移量 r4,使指针指向新位置
  1. 缓存维护 — 刷新指令缓存,确保 CPU 从新地址取到正确的指令
执行完成后,DDR 高地址区域拥有一份地址修正后可直接运行的 U-Boot 副本,bx lr 返回时 PC 已跳转到新地址继续执行。

2.4.1 函数原型和参数

  • 输入参数: r0 = gd->relocaddr (目标地址,如 0x9FE70000)
  • 全局状态: r9 = 新的全局数据指针

2.4.2 执行前的系统状态

组件
位置
地址示例
状态
代码执行
Flash/DDR低地址
0x87800000
当前执行位置
DDR高地址
0x9EE6BFF0
已切换到新栈
全局数据
DDR高地址
0x9EE6FE00
已切换到新gd
目标区域
DDR高地址
0x9FE70000
空白区域,待复制

2.4.3 核心重定位逻辑

2.4.4 .rel.dyn 重定位表详解

📋
.rel.dyn 是什么?
U-Boot 以 -fpie(位置无关可执行)方式编译,代码中所有绝对地址引用(全局变量指针、函数指针、间接向量表的 .word addr 等)在链接时基于 __image_copy_start(0x87800000)。copy_loop 将镜像搬运到新地址后,这些绝对地址仍指向旧位置。
.rel.dyn 段是链接器生成的一张"哪些位置存放了绝对地址"的清单fixloop 遍历该清单,将每个绝对地址加上偏移量 r4,使其指向新位置。
条目结构
每个 .rel.dyn 条目占 8 字节(两个 32-bit word):
  • Word 0r_offset:需要修正的地址(基于原链接地址)
  • Word 1r_info:低 8 位 = 重定位类型,高 24 位 = 符号索引
U-Boot 只关心类型 R_ARM_RELATIVE(值 = 0x17 = 23),其余类型直接跳过。
具体示例
假设 C 代码中有一个全局函数指针:
编译链接后,init_func 位于 .data 段,存储的值是 board_early_init_f绝对地址,例如 0x87812340。链接器同时在 .rel.dyn 中生成一条记录:
fixloop 处理过程(偏移量 r4 = 0x18670000):
哪些代码模式会产生条目?
代码模式
示例
产生原因
全局函数指针
void (*fp)(void) = func;
.data 存绝对地址
间接向量表
_irq: .word irq
.word 存处理程序地址
全局变量指针
int *p = &global_var;
指针初始值是绝对地址
u_boot_list 命令表
U_BOOT_CMD(...)
函数指针数组
switch-case 跳转表
大型 switch 语句
编译器生成绝对跳转表
不需要修正的情况:
  • PC 相对跳转b/bl 指令)— 编码为相对偏移,搬运后天然有效
  • 立即数 / 常量 — 不包含地址信息
  • 栈上局部变量 — 运行时动态分配,不涉及链接时地址
💡
核心要点.rel.dyn 是编译器/链接器的产物,它让 U-Boot 无需在运行时解析 ELF 符号表,仅通过一次线性遍历 + 加法即可完成所有绝对地址的修正,实现高效的运行时重定位。

2.4.5 内存段的复制

复制的内容包括
  • .text 段:可执行代码
  • .rodata 段:只读数据
  • .data 段:已初始化数据
  • .got 段:全局偏移表
不复制的内容
  • .bss 段:未初始化数据(稍后清零)
  • 栈和堆:已经在目标位置

2.5 分析 relocate_vectors

🎯
relocate_vectors 主要作用
relocate_code 只负责搬运代码,不更新 CPU 的异常向量表入口。如果此时发生异常(如中断、数据访问错误),CPU 仍会跳转到旧地址的向量表,而旧地址的代码可能已被覆盖或失效。
relocate_vectors 解决这个问题:将异常向量表的基地址指向重定位后的新位置,确保异常发生时 CPU 跳转到 DDR 高地址区的正确处理程序。
三种实现方式(取决于架构):
  • ARMv7-A/R(i.MX6ULL):直接写 VBAR 寄存器 = gd->relocaddr,零拷贝
  • 传统 ARM:将 64 字节向量表物理拷贝到 0x00000000 或 0xFFFF0000
  • ARMv7-M(Cortex-M):写 VTOR 寄存器
基于 arch/arm/lib/vectors.S 的定义,ARM 异常向量表有以下结构:
偏移地址
异常类型
指令内容
处理程序
0x00
Reset
b resetldr pc, _reset
系统复位
0x04
Undefined Instruction
ldr pc, _undefined_instruction
未定义指令异常
0x08
Software Interrupt (SWI/SVC)
ldr pc, _software_interrupt
软件中断
0x0C
Prefetch Abort
ldr pc, _prefetch_abort
指令预取终止
0x10
Data Abort
ldr pc, _data_abort
数据访问终止
0x14
Reserved
ldr pc, _not_used
保留 (未使用)
0x18
IRQ
ldr pc, _irq
普通中断
0x1C
FIQ
ldr pc, _fiq
快速中断
其在uboot镜像中向量表布局
relocate_vectors 负责重定位 ARM 异常向量表,确保异常处理程序指向正确的地址。

2.5.1 ARM 支持三种向量表配置

1. 标准向量表 (无 VBAR 支持,传统方式) - 拷贝到 0x00000000 或 0xFFFF0000
2. VBAR 支持 (ARMv7-A/R) - 设置 VBAR 寄存器,无需物理拷贝
3. ARMv7-M (Cortex-M) - 设置 VTOR 寄存器

2.5.2 i.MX6ULL 的向量表重定位过程

2.5.3 小节

阶段
向量表存储位置
CPU访问地址
说明
重定位前
0x87800000
0x00000000/0xFFFF0000
原始位置,可能已拷贝到系统向量地址
relocate_code
0x87800000→0x9FE70000
0x00000000/0xFFFF0000
代码拷贝,但向量表未更新
relocate_vectors
0x9FE70000
0x9FE70000 (VBAR) 或 0x00000000/0xFFFF0000
向量表重定位完成
  1. 源地址: 重定位后的 U-Boot 基地址 (如 0x9FE70000)
  1. 目标地址:
      • VBAR 支持: 设置 VBAR 寄存器,无需物理拷贝
      • 传统方式: 拷贝到 0x00000000 或 0xFFFF0000
      • ARMv7-M: 设置 VTOR 寄存器
  1. 拷贝内容: 64字节的向量表 (直接表32字节 + 间接表32字节)
  1. 架构适配: 不同 ARM 架构和 SoC 有不同的处理方式

2.6 分析清除BSS段

🧹
清除 BSS 段主要作用
重定位完成后,BSS 段的符号地址已通过 .rel.dyn fixloop 映射到 DDR 高地址,但该区域的内存内容是随机脏数据。C 语言规范要求未初始化的全局/静态变量必须为零,因此必须对 __bss_start__bss_end 范围执行 memset(0)
关键点:BSS 段不参与物理拷贝(不在 __image_copy_start ~ __image_copy_end 范围内),它的"重定位"仅是符号地址的重新计算。清零操作是 BSS 段在新地址上唯一的初始化动作,完成后 C 运行时环境才真正就绪,可以安全调用 board_init_r 进入第二阶段。
BSS 段什么时候重定位到 DDR 高地址?
答案:BSS 段在 relocate_code 执行时就"重定位"了,但它不是通过物理复制,而是通过符号地址重新计算实现的。

2.6.1 BSS 段重定位的特殊性

1. BSS 段不需要物理复制
2. 链接脚本中的布局
3. 重定位的实际过程

2.6.2 内存布局示例

2.6.3 为什么这样设计?

  1. BSS 段在 ELF 文件中不占存储空间 - 只是内存布局信息
  1. 只需要清零的内存空间 - 不需要复制实际数据
  1. 通过重定位表自动调整符号地址 - 无需额外的复制操作
  1. 提高效率 - 避免无意义的数据复制
核心理解:BSS 段的"重定位"是地址计算的重新映射,发生在 relocate_code 的重定位表处理阶段,而不是传统意义上的数据复制重定位!

3. 第一阶段总结

U-Boot 启动第一阶段从复位向量 _start 开始,到 board_init_r 入口前结束,核心目标是将自身从加载地址搬运到 DDR 高地址并建立完整的 C 运行时环境。整个过程可归纳为以下六步:
阶段
执行函数
关键动作
执行环境
① 异常向量表
_start (vectors.S)
定义 8 个异常入口,复位向量跳转到 reset
Flash/DDR 低地址
② CPU 早期初始化
save_boot_params_ret
设置 SVC32 模式、屏蔽中断、配置 VBAR/SCTLR、失效 TLB/ICache、关闭 MMU
Flash/DDR 低地址
③ 板级底层初始化
lowlevel_inits_init
设置临时栈(OCRAM)、PLL/时钟/DDR 控制器初始化
OCRAM 栈
④ 内存规划
board_init_finit_sequence_f[]
初始化 GD、DDR、串口;从 ram_top 向下规划页表/代码/malloc/GD/FDT/栈
OCRAM 栈 + GD
⑤ 重定位
relocate_coderelocate_vectors
copy_loop 搬运镜像、fixloop 遍历 .rel.dyn 修正绝对地址、更新向量表
DDR 新栈 + 新 GD
⑥ BSS 清零
memset(__bss_start, 0, len)
将 BSS 段归零,C 运行时环境就绪
DDR 高地址(重定位后)

执行流程总览

关键地址速查(i.MX6ULL, 512MB DDR)

符号 / 寄存器
含义
__image_copy_start
0x87800000
U-Boot 加载基地址(源)
gd->ram_top
0xA0000000
DDR 顶端
MMU 页表
0x9FFF0000
64KB 对齐,实际 16KB
gd->relocaddr
0x9FE70000
重定位目标地址
r4(重定位偏移)
0x18670000
0x9FE70000 − 0x87800000
gd->start_addr_sp
~0x9EE6BFF0
重定位后新栈顶
Linux 可用区域
0x80000000 – ~0x9EE6BFEF
~494MB 连续内存
➡️
下一步:BSS 清零完成后,_main 调用 board_init_r 进入
Uboot 启动第二阶段
,执行板级外设初始化(eMMC、网络、USB)、环境变量加载、控制台初始化,最终进入命令行循环或自动引导 Linux 内核。
上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...