U-Boot分析【学习笔记】(10)
10. _main分析
在U-Boot分析【学习笔记】(9)末尾,我们知道 lowlevel_init 函数执行完后会回到 start.S 继续往下执行 bl _main。
在 U-Boot 这种支持多架构(ARM, MIPS, x86等)的系统中,我们要在 arch/arm/lib/crt0.S 中寻找 _main,原因如下:
- 架构关联性:它是 ARM 架构的“出厂设置”
文件名含义:crt0 是 C-Runtime Startup Code 的缩写,意为“C 语言运行环境启动代码”。
平台特性:虽然 start.S 处理的是特定 CPU(如 Cortex-A7)的寄存器级初始化,但搭建 C 语言舞台(如对齐栈指针、设置全局变量指针 r 9 r9 r9)对于所有 ARM 架构的芯片来说都是通用的。
目录逻辑:arch/arm/cpu/armv7/:存放的是跟“这颗处理器”相关的代码。arch/arm/lib/:存放的是跟“整个 ARM 家族”通用的工具和启动逻辑。结论:_main 属于 ARM 家族通用的启动规范,所以它被放在了架构级的公共库(lib)中。- 职责分离:让“芯片开发”与“系统启动”解耦
U-Boot 的设计极其注重复用。
start.S (位于 cpu 目录):、他负责针对特定的芯片(i.MX6ULL)把看门狗关了、把基础时钟开了。
crt0.S (位于 lib 目录):他不关心你是 NXP 的芯片还是三星的芯片,只关心你是不是 ARM。他负责按照 ARM 的标准去分配内存、执行重定位。
这种设计的好处:如果 NXP 以后出一颗 ARMv8 架构的新芯片,开发者只需要重写 start.S 里的硬件逻辑,而 crt0.S 里的 _main 启动框架几乎可以原封不动地直接套用。- 编译系统的“指路牌”:链接脚本
即便代码分成了两个文件,它们最终是如何连在一起的呢?
在 U-Boot 编译生成的链接脚本(通常是 u-boot.lds)中,明确规定了代码的排布顺序。
通常顺序是:start.o (包含中断向量表和基础初始化) → \rightarrow → crt0.o (包含 _main)。当你在 start.S 里写下 bl _main 时,链接器会自动去 arch/arm/lib/crt0.S 编译生成的二进制文件里寻找这个入口地址。
U-Boot 启动序列(_main)执行流程
本文件处理 U-Boot 启动过程中与目标平台无关的阶段(即 ARM 通用阶段),这些阶段需要 C 语言运行环境的支持。其入口点为 _main,由目标芯片的 start.S 文件跳转而来。
- 为调用 board_init_f() 搭建初始环境
环境描述:此环境仅提供一个栈(Stack)和一块存储 GD(全局数据)结构体的空间。这两个部分都位于当前可用的内存中(如 SRAM、锁定的 L1 Cache 等)。
重要限制:在此阶段,变量(VARIABLE)形式的全局数据(无论是否初始化,即 BSS 段)是不可用的。只有常量(CONSTANT)类型的初始化数据可用。
动作:在调用 board_init_f() 之前,必须将 GD 空间清零。- 调用 board_init_f()(前台初始化)
核心职责:此函数负责准备硬件,以便程序能从系统内存(DRAM、DDR 等)中运行。
逻辑衔接:由于此时系统内存(DRAM)可能尚未就绪,board_init_f() 必须使用当前的 GD 来存储需要传递给后续阶段的数据。
关键数据:这些传递的数据包括:重定位目标地址(代码搬家到哪)、未来的栈地址以及未来的 GD 存储位置。- 设置中间环境
环境描述:此时的栈和 GD 已经切换到了 board_init_f() 在系统内存(DRAM)中分配的新位置。
状态:但此时 BSS 段和已初始化的非静态变量(非 const 数据)依然不可用。- 执行代码重定位(仅限 U-Boot Proper,非 SPL)
动作:调用 relocate_code()。
职责:将 U-Boot 从当前位置(如 Flash 或 SRAM)重定位(搬家)到由 board_init_f() 计算出的目标 DRAM 地址。
SPL 流程(二级引导程序)
动作:board_init_f() 直接返回到 crt0。SPL 中通常不涉及代码重定位。- 为调用 board_init_r() 搭建最终环境
环境描述:
BSS 段:已初始化为 0。
已初始化非 const 数据:已加载为预定值。
栈:已完全位于系统内存(DRAM)中。
数据继承:此时的 GD 保留了之前 board_init_f() 设置的所有关键数值。- 处理器最后微调(仅限 U-Boot Proper)
动作:某些 CPU 在此时仍需对内存进行最后的处理工作,因此调用 c_runtime_cpu_setup(例如刷新 Cache)。- 跳转至 board_init_r()(后台初始化)
意义:正式进入 C 语言的大本营,开始初始化各种外设(网卡、USB、闪存等)并启动内核。
10.1 搭建 C 运行环境 并 调用borad_init_f(0)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
mov r0, sp
bl board_init_f_alloc_reserve
mov sp, r0
/* set up gd here, outside any C code */
mov r9, r0
bl board_init_f_init_reserve
mov r0, #0
bl board_init_f
#if ! defined(CONFIG_SPL_BUILD)
这段代码实现了 _main 的第一个阶段:在 SRAM 中搭建最基础的 C 运行环境,并调用 board_init_f
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
如果当前正在编译的是 SPL(二级引导程序),且定义了专用的 SPL 栈地址,那么把SPL栈地址放入 sp (栈指针寄存器)中,否则从板级配置文件(通常是 include/configs/xxx.h)中读取预设的初始栈顶地址。
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
如果当前处理器架构是 ARMv7-M(如 Cortex-M 系列):
由于 v7M 架构的指令集限制,禁止直接将 sp 寄存器作为 bic 指令的操作目标。因此,代码先将 sp 的值暂存到通用寄存器 r3 中,通过 bic 指令对 r3 进行位清除操作(低 3 位清零),最后再将处理后的地址写回 sp。
否则(对于 i.MX6ULL 等 ARMv7-A 架构):
直接对 sp 寄存器执行 bic 操作。
什么是“清空低三位”?
从二进制角度看,数字 7 的二进制是 0…0111。
BIC(Bit Clear)指令的作用是:
将第一个操作数中,对应第二个操作数二进制为 1 的位全部清零。结果:无论 sp 原来的地址是多少,执行完后,其二进制地址的最后三位一定变成了 000。
数学意义:一个二进制数如果低三位是 000,那么这个数一定能被 2 3 2^3 23(即 8)整除,对齐指的是地址必须落在能被 n 整除的那个边界上,所以清空低三位就可以实现8字节对启。
10.1.1 board_init_f_alloc_reserve
mov r0, sp
bl board_init_f_alloc_reserve
mov sp, r0
函数的参数传递/返回值关系
- 参数传递:并不是 0 到 16在 ARM 32 位架构(如 i.MX6ULL)中,函数调用时的参数传递规则如下:
前 4 个参数:依次使用 r0, r1, r2, r3 这四个寄存器。
超过 4 个的部分:必须通过 栈(Stack) 来传递。
例如:如果你写一个 C 函数 void test(int a, int b, int c, int d, int e),前四个 a, b, c, d 分别占领 r0-r3,而第 5 个参数 e 会被压入内存中的栈里。
寄存器 r 4 r4 r4 - r 11 r11 r11:这些被称为“被调用者保存”寄存器。C 函数内部如果要用到它们,必须先备份(Push),用完后再恢复(Pop),它们不用于直接传递参数。- 返回值:通常只看 r 0 r0 r0
函数执行完后,返回值并不是撒在所有寄存器里的:
32 位返回值(如 int, ulong, 指针):存储在 r0 中。
64 位返回值(如 long long):存储在 r0(低 32 位)和 r1(高 32 位)中。
更大或更复杂的结构体:通常通过指针在内存中传递。
函数目的:为全局数据(GD)预留空间0
动作:将当前的栈指针(sp)值传递给r0,作为参数调用 C 函数 board_init_f_alloc_reserve。该函数在当前栈顶下方的 SRAM 区域中,计算并扣除掉 struct global_data(全局数据结构体)以及早期分配所需的大小。
物理意义:函数返回一个新的地址(在 r0 中),这个地址是扣除 GD 空间后的新栈顶。随后 mov sp, r0 更新栈指针,实现了在内存中为 GD “割地”的操作。
按照 U-Boot 的源码组织架构,board_init_f_alloc_reserve 函数通常位于以下路径:
common/init/board_init.c
为什么在这个位置?
平台无关性:
board_init_f_alloc_reserve 的逻辑是纯 C 语言编写的,它只是在做简单的地址运算(从当前的栈顶减去 gd 和早期堆的空间)。这种逻辑对于 ARM、MIPS 或 x86 架构都是通用的。
职责划分:
common/ 目录存放的是 U-Boot 的核心公共代码。由于它是启动流程中“全架构通用”的预留空间操作,因此被放在了 common/init/ 下。
board_init_f_alloc_reserve 核心注释翻译
功能描述:
从给定的“顶端(top)”地址开始,向下分配用于存放“全局变量(globals)”的预留空间,并返回分配区域的“底端(bottom)”地址。
注意事项:
真正的“空间预留”动作(即修改 SP 指针)不能在此函数内完成,因为这涉及修改 C 语言的栈指针,会导致函数返回时崩溃。因此,空间的物理预留由调用者(即 _main 汇编代码)在函数返回后完成。
对齐约束:
GD(全局数据):必须向下按 16 字节对齐。
Early Malloc(早期堆):不单独设置对齐,因此它遵循当前架构的栈对齐约束。
分配顺序:GD 必须最后分配。这样函数的返回值既是整个预留区域的底部,同时也是 GD 结构体的起始地址,方便调用者直接将此地址存入 r 9 r9 r9。
ulong board_init_f_alloc_reserve(ulong top)
{
/* 1. 预留早期 malloc 内存池空间 */
#if defined(CONFIG_SYS_MALLOC_F)
/* top 指针继续向下移动,扣除早期堆的大小 */
top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
/* 2. 最后一步:预留 GD 空间(并向下进行 16 字节对齐) */
/* sizeof(struct global_data) 是账本的大小 */
top = rounddown(top - sizeof(struct global_data), 16);
return top;
}
为什么 GD 必须最后分配?
看下面这个内存分布图:
传入的 top:
是 _main 传进来的原始栈顶(经过 8 字节对齐的 SP)。
向下减去 Malloc 空间:
此时 top 指向了早期堆的底部。
向下减去 GD 空间:
此时 top 指向了 global_data 的底部。
这个 top 地址现在具有双重身份:
它是新的栈顶位置(用于 mov sp, r0)。
它也是 GD 结构体的基地址(用于 mov r9, r0)。
由于 GD 就在最底部,所以汇编代码只需要执行一次 mov r9, r0,就能让 r 9 r9 r9 完美指向 GD 的起始位置。
10.1.2 board_init_f_init_reserve
这个函数紧跟在空间分配之后,通常也位于:
common/init/board_init.c
board_init_f_alloc_reserve 和 board_init_f_init_reserve 在 U-Boot 启动流程中,它们紧密配合,共同完成了从“原始汇编环境”向“结构化 C 环境”的物理转型。
- 逻辑上的“因果”关系
这两个函数通常在 _main 中按顺序被调用,它们共同服务于一个核心目标:确立全局数据区(GD)和早期堆(Early Malloc)。
先圈地 (alloc_reserve):
输入:当前的栈顶地址(SRAM 的顶端)。
计算:根据配置,算出 GD 结构体需要多少空间、Early Malloc 需要多少空间。
输出:返回一个新的地址。这个地址既是新栈顶,也是预留空间的起始基地址。
汇编配合:汇编收到返回值后,立刻修改 sp(物理搬家)和 r9(确立管理员)。
后装修 (init_reserve):
输入:刚刚圈出来的地皮基地址(即 r9 指向的位置)。
计算:把这块内存全部抹零(memset),并设置内部的各个“房间”(如把早期堆的地址填入 GD 账本)。
输出:无返回值,但此时 $r9 指向的内存已经从“荒地”变成了“精装房”。- 空间上的“共生”关系
它们共同切分了同一块物理内存。在内存空间中,它们的产物是紧挨在一起的:
alloc_reserve 决定了这块区域的大小和边界。
init_reserve 决定了这块区域内部的组织结构(GD 放在最下面,Early Malloc 堆放在 GD 上面)。- 代码级的协同逻辑
我们可以看它们在 _main 汇编里的“接力”过程:/* 第一步:圈地 */ mov r0, sp /* 把旧栈顶传给参数 */ bl board_init_f_alloc_reserve /* 计算空间 */ mov sp, r0 /* 【物理动作】修改 SP,正式腾出空间 */ /* 第二步:确立身份 */ mov r9, r0 /* 将地皮首地址交给管理员 r9 */ /* 第三步:装修 */ bl board_init_f_init_reserve /* 传入 r0 里的基地址,清零并设置>内部成员 */
r9 寄存器在 U-Boot 运行期间被锁定,专门用来存放 gd 结构体的首地址,就像是一个永远握在手里的索引,无论代码运行到汇编还是 C 语言,只要想读写系统配置、频率、内存大小等核心数据,CPU 就会直接去读取 r9 寄存器里的地址。
void board_init_f_init_reserve(ulong base)
{
struct global_data *gd_ptr; // 定义一个局部指针,用于初始操作
/*
* 1. 彻底清空 GD 空间并进行初始化设置。
* 使用 gd_ptr 进行操作,因为此时全局指针 gd 可能还未生效。
*/
gd_ptr = (struct global_data *)base; // 将传入的基地址强转为 GD 结构体指针
/* 将这块内存区域全部填充为 '\0',确保账本初始状态是干净的 */
memset(gd_ptr, '\0', sizeof(*gd));
/* 2. 设置全局 GD 指针。如果当前架构是 ARM,由于汇编中已经 mov r9, r0,
所以不需要在此重复调用 arch_setup_gd。*/
#if !defined(CONFIG_ARM)
arch_setup_gd(gd_ptr);
#endif
/* 3. 地址指针步进:base 现在指向 GD 之后的空间。
roundup(..., 16) 确保了地址按照 16 字节对齐。 */
base += roundup(sizeof(struct global_data), 16);
/*
* 4. 记录早期 malloc 内存池的起始地址。
* 此时可以使用 gd 指针了(对于 ARM 来说就是 r9 指针)。
*/
#if defined(CONFIG_SYS_MALLOC_F)
/* 将早期 malloc 的基地址记录在 gd 账本中,方便后续调用 malloc 使用 */
gd->malloc_base = base;
/* 地址继续步进,跳过整个早期 malloc 内存池的大小,
base 最终指向这块预留区域的最顶端。 */
base += CONFIG_SYS_MALLOC_F_LEN;
#endif
}
由于 board_init_f_init_reserve 只接收一个参数,所以这个参数 base 实际上就是 r0 寄存器的值,也就是 board_init_f_alloc_reserve 返回的GD 结构体的基地址。
函数核心逻辑总结:
- 强转指针:
将传入的 base(SRAM 预留区的首地址)强转为 struct global_data 指针。因为 base 只是个无意义的数字,强转后它才有了结构。- 初始化 GD:
使用 memset 将这块空间全部抹零。这保证了账本里没有之前的随机乱码,所有成员初始值都为 0。- 架设 Early Malloc:
在 GD 的空间之后(跳过对齐间隙),将剩余的预留空间交给 Early Malloc 管理。
关键动作:将跳过 GD 后的新地址写入 gd->malloc_base。
结果:从此 U-Boot 拥有了“动态存储”的能力。
-
为什么要存放 struct global_data (GD)?
——解决无处存储的全局变量问题。
背景:在 C 语言中,普通全局变量存放在 .data(已初始化)或 .bss(未初始化)段。这些段通常位于 DDR 中。但在 i.MX6ULL 刚上电时,DDR 还没初始化,直接写全局变量会导致系统崩溃。
解决方案:我们需要一个“中央账本”。
临时存储:GD 结构体被强行放在内部 SRAM 里,这是一上电就能用的内存。
信息传承:U-Boot 在搬家(Relocation)前,会将探测到的硬件信息(如:CPU 频率、DDR 大小、串口波特率、搬家后的目标地址)记录在 GD 里。
唯一性:通过锁定 r9 寄存器指向 GD,确保了无论代码运行到哪,都能通过同一个入口找到这些核心数据。 -
为什么要存放 Early Malloc?
背景:传统的 malloc 函数需要管理大片内存(堆池),这通常是在 DDR 里划出一块几百 MB 的空间。但在 DDR 亮起之前,有些驱动程序(比如设备树 FDT、串口驱动、电源管理 I2C)在初始化时,就需要动态申请一丁点儿内存(比如几百字节)来存放临时数据。
解决方案:建立“早期堆”。
微型内存池:在 SRAM 里、紧接着 GD 之后划出一块很小的区域(通常只有几 KB)。
动态支持:有了它,U-Boot 的早期 C 代码就可以调用 malloc 来申请空间,而不需要硬编码内存地址。
使命完成:一旦 DDR 初始化完成,U-Boot 就会建立真正的堆空间,这个“早期堆”的使命就完成了。
假设没有 Early Malloc:
如果 U-Boot 的驱动程序想在 DDR 还没亮时存一点数据(比如串口驱动要存一下波特率计算结果),程序员必须手动指定一个地址。/* 程序员必须自己算:SRAM 哪块地儿没被用? */ /* 万一算错了,就可能把栈或者代码段给覆盖了,系统直接挂掉 */ int *temp_data = (int *)0x0091F000; *temp_data = 115200;缺点:非常危险,不同的芯片、不同的配置,SRAM 大小都不一样。手动指定的地址 0x0091F000 在 A 芯片上能跑,在 B 芯片上可能就冲突了。这就是“硬编码”的局限性——死板、易错、不可移植。
10.1.3 board_init_f
mov r0, #0
bl board_init_f
mov r0, #0:根据 ARM 的函数调用约定,r0 存放的是函数的第一个参数。
在 C 语言中,board_init_f 的定义通常是 void board_init_f(ulong boot_flags)。
这里把 r0 设为 0,意味着传给 boot_flags 的值为 0,表示这是一个正常的冷启动。
bl board_init_f:执行带链接的跳转。
此时,程序正式离开汇编文件的掌控,进入位于 common/board_f.c 的 C 语言世界。
board_init_f 是一个通用核心函数,它的位置非常固定,源码绝对路径:
common/board_f.c
void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
/*
*对于某些 CPU 架构,全局数据(gd)在调用本函数之前就已经被初始化并投入使用了
*此时应当完好地保留它。而对于另外一些架构,则需要定义 CONFIG_SYS_GENERIC_GLOBAL_DATA 宏
*直接在当前的 C 语言栈中临时创建一块空间来托管全局数据,直到系统完成代码重定位。
*/
gd_t data;
gd = &data;
/*
*在 initcall_run_list 轮询器内部触发调试打印(Debug Print)访问全局数据之前,
*必须先对其进行清零(抹平)。否则,后续的调试打印可能会因为读到残留的随机乱码,
*而对 gd->have_console(控制台是否可用标志)做出错误的判断。
*/
zero_global_data();
#endif
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
}
1.条件编译块
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA gd_t data; gd = &data; zero_global_data(); #endif对于 ARM 架构,这个条件通常不成立。
因为在前几步汇编里,已经用 mov r9, r0 把 r9 寄存器牢牢绑定在 SRAM 的 gd 账本上了。ARM 早就有了合法的 gd 内存,不需要在这个函数的 C 语言栈里临时抠一块 gd_t data 出来。
如果是其他架构(比如某些 MIPS),上电后寄存器不够用,就会借用这几行代码,在栈里临时用一下。
2.参数交接与状态初始化
gd->flags = boot_flags; gd->have_console = 0;前一步汇编里的 mov r0, #0 中 r0 清零,并且 r0 作为第一个参数传入进来,也就是 boot_flags = 0。这行代码把 0(代表冷启动、正常启动)写进标志位里。
gd->have_console = 0;此时系统还没有初始化串口,先把控制台可用标志置为 0。
3.核心:硬件初始化
if (initcall_run_list(init_sequence_f)) hang();init_sequence_f 是一个函数指针数组
initcall_run_list 会用一个 while 循环,把数组里的 serial_init(开串口)、dram_init(点亮内存)、reserve_uboot(规划新家)等函数每个都调用一遍。
返回值检查:在 C 语言中,返回 0 代表成功。如果其中任何一个硬件驱动出了问题,initcall_run_list 就会返回非 0 值。
hang(); 一旦前面报错,立马执行 hang()。这个函数的底层实现就是一个死循环(while(1);),在早期的裸机上,这表现为直接卡死,防止带病运行。
initcall_run_list
物理本质:它是一个通用的函数指针轮询器,通过接收一个以 NULL 结尾的函数指针数组,实现硬件初始化行为的集中化管理。
三大核心职能:
架构解耦:将“初始化策略(board_init_f)”与“具体动作(各驱动函数)”分离。
安全熔断:采用“非零即死”的返回机制,一旦某个硬件初始化失败,触发 hang() 卡死系统。
状态诊断:为系统提供了调试信息,方便捕获上电阶段的硬件故障。
4.尾部保护:防代码跑飞
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \ !defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) /* NOTREACHED - jump_to_copy() does not return */ hang(); #endif注意前面的取反符号 !。因为我们正是 CONFIG_ARM,所以这个 if 条件对于 ARM 来说不成立。
既然不成立,里面的 hang() 就会被编译器直接抹掉。
这导致的最终结果是:对于 ARM 架构,board_init_f 执行完上面的大数组后,正常结束函数调用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)