10. _main分析

  在U-Boot分析【学习笔记】(9)末尾,我们知道 lowlevel_init 函数执行完后会回到 start.S 继续往下执行 bl _main。

在 U-Boot 这种支持多架构(ARM, MIPS, x86等)的系统中,我们要在 arch/arm/lib/crt0.S 中寻找 _main,原因如下:

  1. 架构关联性:它是 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)中。
  2. 职责分离:让“芯片开发”与“系统启动”解耦
    U-Boot 的设计极其注重复用。
    start.S (位于 cpu 目录):、他负责针对特定的芯片(i.MX6ULL)把看门狗关了、把基础时钟开了。
    crt0.S (位于 lib 目录):他不关心你是 NXP 的芯片还是三星的芯片,只关心你是不是 ARM。他负责按照 ARM 的标准去分配内存、执行重定位。
    这种设计的好处:如果 NXP 以后出一颗 ARMv8 架构的新芯片,开发者只需要重写 start.S 里的硬件逻辑,而 crt0.S 里的 _main 启动框架几乎可以原封不动地直接套用。
  3. 编译系统的“指路牌”:链接脚本
    即便代码分成了两个文件,它们最终是如何连在一起的呢?
    在 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 文件跳转而来。

  1. 为调用 board_init_f() 搭建初始环境
    环境描述:此环境仅提供一个栈(Stack)和一块存储 GD(全局数据)结构体的空间。这两个部分都位于当前可用的内存中(如 SRAM、锁定的 L1 Cache 等)。
    重要限制:在此阶段,变量(VARIABLE)形式的全局数据(无论是否初始化,即 BSS 段)是不可用的。只有常量(CONSTANT)类型的初始化数据可用。
    动作:在调用 board_init_f() 之前,必须将 GD 空间清零。
  2. 调用 board_init_f()(前台初始化)
    核心职责:此函数负责准备硬件,以便程序能从系统内存(DRAM、DDR 等)中运行。
    逻辑衔接:由于此时系统内存(DRAM)可能尚未就绪,board_init_f() 必须使用当前的 GD 来存储需要传递给后续阶段的数据。
    关键数据:这些传递的数据包括:重定位目标地址(代码搬家到哪)、未来的栈地址以及未来的 GD 存储位置。
  3. 设置中间环境
    环境描述:此时的栈和 GD 已经切换到了 board_init_f() 在系统内存(DRAM)中分配的新位置。
    状态:但此时 BSS 段和已初始化的非静态变量(非 const 数据)依然不可用。
  4. 执行代码重定位(仅限 U-Boot Proper,非 SPL)
    动作:调用 relocate_code()。
    职责:将 U-Boot 从当前位置(如 Flash 或 SRAM)重定位(搬家)到由 board_init_f() 计算出的目标 DRAM 地址。
    SPL 流程(二级引导程序)
    动作:board_init_f() 直接返回到 crt0。SPL 中通常不涉及代码重定位。
  5. 为调用 board_init_r() 搭建最终环境
    环境描述:
    BSS 段:已初始化为 0。
    已初始化非 const 数据:已加载为预定值。
    栈:已完全位于系统内存(DRAM)中。
    数据继承:此时的 GD 保留了之前 board_init_f() 设置的所有关键数值。
  6. 处理器最后微调(仅限 U-Boot Proper)
    动作:某些 CPU 在此时仍需对内存进行最后的处理工作,因此调用 c_runtime_cpu_setup(例如刷新 Cache)。
  7. 跳转至 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

函数的参数传递/返回值关系

  1. 参数传递:并不是 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),它们不用于直接传递参数。
  2. 返回值:通常只看 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_reserveboard_init_f_init_reserve 在 U-Boot 启动流程中,它们紧密配合,共同完成了从“原始汇编环境”向“结构化 C 环境”的物理转型。

  1. 逻辑上的“因果”关系
    这两个函数通常在 _main 中按顺序被调用,它们共同服务于一个核心目标:确立全局数据区(GD)和早期堆(Early Malloc)。
    先圈地 (alloc_reserve):
    输入:当前的栈顶地址(SRAM 的顶端)。
    计算:根据配置,算出 GD 结构体需要多少空间、Early Malloc 需要多少空间。
    输出:返回一个新的地址。这个地址既是新栈顶,也是预留空间的起始基地址。
    汇编配合:汇编收到返回值后,立刻修改 sp(物理搬家)和 r9(确立管理员)。
    后装修 (init_reserve):
    输入:刚刚圈出来的地皮基地址(即 r9 指向的位置)。
    计算:把这块内存全部抹零(memset),并设置内部的各个“房间”(如把早期堆的地址填入 GD 账本)。
    输出:无返回值,但此时 $r9 指向的内存已经从“荒地”变成了“精装房”。
  2. 空间上的“共生”关系
    它们共同切分了同一块物理内存。在内存空间中,它们的产物是紧挨在一起的:
    alloc_reserve 决定了这块区域的大小和边界。
    init_reserve 决定了这块区域内部的组织结构(GD 放在最下面,Early Malloc 堆放在 GD 上面)。
  3. 代码级的协同逻辑
    我们可以看它们在 _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 结构体的基地址。

函数核心逻辑总结:

  1. 强转指针:
    将传入的 base(SRAM 预留区的首地址)强转为 struct global_data 指针。因为 base 只是个无意义的数字,强转后它才有了结构。
  2. 初始化 GD:
    使用 memset 将这块空间全部抹零。这保证了账本里没有之前的随机乱码,所有成员初始值都为 0。
  3. 架设 Early Malloc:
    在 GD 的空间之后(跳过对齐间隙),将剩余的预留空间交给 Early Malloc 管理。
    关键动作:将跳过 GD 后的新地址写入 gd->malloc_base。
    结果:从此 U-Boot 拥有了“动态存储”的能力。
  1. 为什么要存放 struct global_data (GD)?
    ——解决无处存储的全局变量问题。
    背景:在 C 语言中,普通全局变量存放在 .data(已初始化)或 .bss(未初始化)段。这些段通常位于 DDR 中。但在 i.MX6ULL 刚上电时,DDR 还没初始化,直接写全局变量会导致系统崩溃。
    解决方案:我们需要一个“中央账本”。
    临时存储:GD 结构体被强行放在内部 SRAM 里,这是一上电就能用的内存
    信息传承:U-Boot 在搬家(Relocation)前,会将探测到的硬件信息(如:CPU 频率、DDR 大小、串口波特率、搬家后的目标地址)记录在 GD 里。
    唯一性:通过锁定 r9 寄存器指向 GD,确保了无论代码运行到哪,都能通过同一个入口找到这些核心数据。

  2. 为什么要存放 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 执行完上面的大数组后,正常结束函数调用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐