一、前言
在学习 U-Boot 启动流程时,很多初学者会遇到一个比较典型的问题:
源码里的每个函数似乎都能看懂,但一旦把它们串成完整启动流程,就容易混乱。
尤其是在分析 ARM 平台 U-Boot 启动过程时,经常会遇到以下几个概念:
- U-Boot 代码运行地址
- SRAM/IRAM 中的临时栈
gd全局数据结构board_init_frelocaddr和reloc_offu-boot.map- 设备树地址,如
fdtcontroladdr、working_fdt、fdtaddr - 自动生成文件,如
include/generated/
这些内容单独看并不复杂,但它们在启动早期交织在一起,就会让整个流程显得比较绕。
本文主要结合 U-Boot 启动过程中的几个关键问题,对早期启动流程进行梳理,重点说明:
- 为什么代码可以在 DDR 中执行,但栈却放在 SRAM 中;
- 如何理解
u-boot.map与重定位后的实际运行地址; board_init_f在启动流程中的作用;- U-Boot 是如何确定并使用设备树的;
- 为什么分析 U-Boot 不能只看
.c和.S源码。
二、U-Boot 早期为什么使用 SRAM 作为栈?
在启动早期,一个很容易让人疑惑的问题是:
U-Boot 代码已经在 DDR 中运行,为什么 SP 栈指针还放在 SRAM 或 IRAM 中?
这其实是正常现象。
CPU 执行程序时,取指地址和栈地址并不要求位于同一块内存。
也就是说,可以出现如下状态:
PC -> 指向 DDR 中的 U-Boot 代码 SP -> 指向 SRAM/IRAM 中的临时栈空间此时 CPU 的运行方式可以理解为:
取指令:从 DDR 中读取 U-Boot 指令 压栈/出栈:访问 SRAM/IRAM这和很多 MCU 的运行方式类似:
代码存放在 Flash 栈和全局变量存放在 SRAM所以,“代码在 DDR,栈在 SRAM”并不矛盾。
U-Boot 之所以在早期使用 SRAM/IRAM 作为临时栈,是因为此时完整的 DRAM 内存规划还没有完成。虽然代码可能已经被 BootROM 或前一级加载器搬运到了 DDR,但 U-Boot 自身还没有完成对内存空间的统一管理。
因此,在早期阶段,U-Boot 通常会先使用一段确定可用的小内存作为临时栈,等board_init_f完成内存规划后,再重新设置新的栈位置。
三、u-boot.map的作用与局限
分析 U-Boot 时,u-boot.map是一个非常重要的文件。
它可以帮助我们查看:
- 各个段的链接地址;
- 函数符号的位置;
- 全局变量的位置;
- 镜像中不同目标文件的排列情况;
- 链接脚本最终如何组织 U-Boot 镜像。
简单来说,u-boot.map可以看作是链接器生成的一份“地址说明书”。
但是需要注意:
u-boot.map中的地址不一定永远等于程序运行时的实际地址。
在 U-Boot 重定位之前,u-boot.map中的地址通常可以直接用于分析当前程序布局。
但是 U-Boot 启动过程中会进行重定位。
重定位完成后,程序实际运行地址会发生变化。
此时再分析地址,就不能只看u-boot.map,还需要结合:
relocaddr reloc_off其中:
relocaddr:U-Boot 重定位后的目标地址 reloc_off :重定位前后地址之间的偏移因此,重定位后的实际地址通常需要这样理解:
运行时地址 = 链接地址 + reloc_off例如,某个函数在u-boot.map中的链接地址是:
0x43001000如果reloc_off为:
0x100000那么它在重定位后的实际运行地址大致可以理解为:
0x43101000当然,具体分析时还要结合平台实际内存布局和 U-Boot 输出信息。
这一点很重要。否则在调试 U-Boot 时,经常会出现一种情况:
map 文件里的地址和实际调试看到的地址对不上。
很多时候并不是 map 文件错了,而是因为没有考虑 U-Boot 重定位。
四、spare_head.c与镜像头部信息
在一些平台中,U-Boot 镜像前面会放置特定的头部信息,用于和前一级启动程序进行参数传递。
例如笔记中提到:
spare_head.c spare_head.o其中spare_head.o里的.data段可能会被强制放到镜像最前面的.head段中。
这类设计的目的通常是:
- 在 U-Boot 镜像头部放置平台需要的启动参数;
- 让 Boot0、BootROM 或其他前级加载器能够识别镜像;
- 在 Boot0 和 U-Boot 之间传递必要参数;
- 为后续启动阶段提供硬件、存储、启动介质等信息。
例如:
uboot_spare_head.boot_data可以理解为 Boot0 和 U-Boot 之间的一块参数传递区域。
这说明在分析 U-Boot 镜像时,不能只关注_start或_main这些入口代码,还需要关注镜像最前面的平台头部结构。
对于 SoC 厂商定制版本的 U-Boot,这一部分尤其重要。
五、_main与早期运行环境建立
U-Boot 进入_main后,会逐步完成早期运行环境的建立。
在 ARM 平台中,_main通常会做几类关键操作:
- 设置临时栈;
- 初始化或准备
gd; - 调用
board_init_f; - 设置新的栈和新的
gd; - 调用
relocate_code; - 重定位完成后进入后续阶段。
可以简单理解为:
_main | |-- 设置早期栈 | |-- 准备 gd | |-- board_init_f | |-- 重新设置 sp/gd | |-- relocate_code | |-- board_init_r其中gd是 U-Boot 中非常核心的全局数据结构,很多启动阶段的信息都会记录在里面。
比如:
gd->start_addr_sp gd->relocaddr gd->reloc_off gd->fdt_blob gd->bd所以分析 U-Boot 启动流程时,不能只看函数调用顺序,还要重点追踪gd中关键字段的变化。
六、board_init_f的作用
board_init_f是 U-Boot 启动前期非常关键的函数。
从名字上看,它像是一个普通的板级初始化函数,但它实际承担了更多准备工作。
它主要用于完成重定位前的初始化,例如:
- 初始化全局数据结构;
- 确定后续内存布局;
- 计算 U-Boot 重定位地址;
- 设置栈地址;
- 准备 malloc 区域;
- 处理设备树地址;
- 为后续
relocate_code做准备。
可以说,board_init_f是 U-Boot 从“临时运行环境”过渡到“正式运行环境”的关键准备阶段。
在阅读board_init_f时,不建议只盯着某一行代码看,而应该带着几个问题去跟:
当前 gd 中哪些字段被设置了? 当前内存布局发生了什么变化? relocaddr 是什么时候确定的? start_addr_sp 是什么时候确定的? 设备树地址是在哪里确定的?这样更容易把流程串起来。
七、设备树地址的确定
U-Boot 启动过程中,设备树是一个非常重要的信息来源。
U-Boot 需要通过设备树获取板级硬件信息,例如:
- CPU 信息;
- 内存信息;
- 串口信息;
- 存储控制器信息;
- GPIO、I2C、SPI 等外设信息;
- model、compatible 等板级描述。
但是在使用设备树之前,U-Boot 必须先确定设备树的位置。
在实际分析中,可以关注以下几个变量或环境信息:
fdtcontroladdr working_fdt fdtaddr一般可以按照下面的思路判断:
- 先查看
fdtcontroladdr是否存在; - 如果没有,再查看
working_fdt; - 继续结合
fdtaddr判断当前使用的设备树地址。
当设备树地址确定后,U-Boot 会将其记录到gd->fdt_blob等字段中。
后续代码就可以通过类似方式访问设备树:
model = fdt_getprop(gd->fdt_blob, 0, "model", NULL);这行代码的含义是:
从 gd->fdt_blob 指向的设备树中, 读取根节点的 model 属性。也就是说,U-Boot 并不是凭空知道当前板子的型号或硬件信息,而是通过设备树读取出来的。
八、为什么分析 U-Boot 不能只看源码?
学习 U-Boot 时,一个常见误区是只看.c和.S文件。
但 U-Boot 是一个高度依赖构建系统的工程,很多关键信息并不直接写在源码中,而是在编译过程中生成。
例如:
include/generated/ include/autoconf.mk u-boot.map *.cmd *.o 预处理文件 反汇编文件这些文件在分析 U-Boot 时非常有价值。
1.include/generated/
该目录中通常包含自动生成的头文件,例如结构体偏移量、配置相关信息等。
汇编代码中经常会用到一些结构体偏移量,例如:
ldr r0, [r9, #GD_RELOC_OFF]这里的GD_RELOC_OFF并不是随便写的,而是在构建过程中根据 C 结构体自动生成出来的。
因此,如果想知道这些偏移量的来源,就需要查看自动生成文件。
2..cmd文件
U-Boot 编译过程中,每个目标文件通常会生成对应的.cmd文件。
例如:
.lowlevel_init.o.cmd .built-in.o.cmd这些文件记录了对应目标文件的实际编译命令。
通过.cmd文件可以知道:
- 当前文件使用了哪些编译参数;
- 包含了哪些头文件路径;
- 定义了哪些宏;
- 这个
.o文件是如何生成的。
在分析条件编译问题时,.cmd文件非常有用。
3. 预处理文件
有时候直接看源码会很痛苦,因为 U-Boot 中有大量条件编译:
#ifdef CONFIG_xxx ... #endif如果不知道某个宏是否开启,就很难判断实际参与编译的是哪一段代码。
这时可以通过编译命令生成预处理文件。
预处理文件可以帮助我们看到:
宏展开后的真实代码 条件编译后保留下来的代码 头文件展开后的结果相比单纯 grep,预处理文件更接近编译器真正看到的内容。
九、推荐的 U-Boot 启动分析顺序
如果是刚开始学习 U-Boot 启动流程,不建议直接从头到尾硬啃源码。
更推荐按照下面的顺序分析。
1. 先建立启动主线
先搞清楚大致流程:
入口汇编 ↓ 设置临时栈 ↓ 初始化 gd ↓ board_init_f ↓ 计算重定位地址 ↓ relocate_code ↓ board_init_r先有整体框架,再补细节。
2. 再追踪关键变量
重点关注:
PC SP gd gd->relocaddr gd->reloc_off gd->start_addr_sp gd->fdt_blobU-Boot 启动流程本质上就是运行环境不断变化的过程。
只要这些关键变量的变化能跟上,整体流程就不会乱。
3. 结合 map 文件和反汇编文件
不要只看源码。
地址相关问题一定要结合:
u-boot.map 反汇编文件 符号表 链接脚本 实际打印信息尤其是重定位前后的地址变化,需要特别注意。
4. 结合.cmd和预处理文件分析宏
如果遇到宏相关问题,不要只靠猜。
可以通过.cmd找到真实编译命令,再生成预处理文件。
这样可以直接看到实际参与编译的代码。
十、总结
U-Boot 启动流程之所以难,并不是因为某一行代码特别复杂,而是因为它涉及多个层面的知识:
ARM 汇编 链接脚本 内存布局 栈设置 全局数据结构 gd 重定位 设备树 Makefile/Kconfig 构建系统这些内容在启动早期同时出现,就会让分析过程显得比较混乱。
学习 U-Boot 启动流程时,可以先抓住以下几个核心问题:
- 当前代码在哪里执行?
- 当前栈在哪里?
gd当前指向哪里?relocaddr和reloc_off是多少?- 当前设备树地址在哪里?
- map 文件中的地址是否需要加上重定位偏移?
- 当前代码是否真的参与了编译?
只要围绕这些问题去分析,U-Boot 启动流程就会逐渐清晰起来。
最后总结一句:
学 U-Boot 启动,不要只看函数调用顺序,更要看运行环境是如何一步步建立起来的。
这也是理解 U-Boot 启动流程的关键。