在前三篇文章中,我们完成了最小Bootloader的基础框架:reset向量启动、LED控制、早期调试串口。这些代码能够运行,但从架构角度审视,它们仍是“散装”的裸机程序——缺乏统一的状态管理,没有规范的初始化流程,各功能模块之间也没有清晰的数据传递机制。
当我们深入研读U-Boot源码时,会发现一个无处不在的数据结构——GD(Global Data,全局数据)。从最早期的串口初始化,到DDR探测、环境变量加载、设备树解析、驱动模型构建,几乎每一个模块都要访问GD。可以毫不夸张地说,GD是U-Boot的“神经中枢”,它承载着整个引导过程所需的全部运行时状态。
与GD紧密配合的是 board_init_f 函数,这是U-Boot在代码重定位之前的核心初始化入口。“f”代表“first”或“before relocation”,意味着这是重定位前的第一阶段初始化。在此阶段,代码运行在初始加载地址,外部DDR可能尚未就绪,可用资源极为有限。board_init_f 的使命是在这些严苛约束下完成必要的早期初始化,并为后续的代码重定位和系统完整启动奠定基础。
本篇将深入剖析GD结构体的存在意义与内部构成,并系统性地介绍 board_init_f 框架。这是从“能运行的代码”走向“正规Bootloader”的关键一步,也是理解U-Boot内存管理与启动流程精髓的必经之路。
理解U-Boot的启动阶段划分
在深入GD之前,我们需要先理解U-Boot的启动流程为何要划分为多个阶段。这种划分不是人为的复杂化,而是嵌入式系统启动过程中的客观需求所决定的。
为什么需要分阶段启动?
当SoC上电复位时,面临以下现实约束:
- DDR未初始化:外部DRAM需要复杂的时序配置才能工作,此时只有片内SRAM(通常几十KB到几百KB)可用。
- 存储设备未初始化:eMMC、SD卡、SPI Flash等都需要相应控制器的初始化和协议握手。
- 时钟未配置:PLL、分频器等可能处于默认的低频状态。
- 代码位置受限:Bootloader最初可能被加载到片内SRAM的固定地址,空间有限。
因此,U-Boot采用分阶段的启动策略:
Stage 0: Boot ROM
└─ SoC固化代码,加载SPL到SRAM
Stage 1: SPL (Secondary Program Loader) - 可选
├─ 运行在片内SRAM
├─ 初始化DDR控制器
└─ 加载完整U-Boot到DDR
Stage 2: board_init_f (Before Relocation)
├─ 运行在初始加载地址
├─ GD结构体初始化
├─ 早期硬件初始化(串口、定时器等)
├─ 计算重定位参数
└─ 准备将自身复制到DDR高端地址
Stage 3: Relocation
├─ 将代码/数据复制到新地址
├─ 修正GOT/重定位表
└─ 跳转到新地址继续执行
Stage 4: board_init_r (After Relocation)
├─ 完整的硬件初始化
├─ 驱动模型(DM)初始化
├─ 环境变量加载
├─ 网络/USB/文件系统初始化
└─ 进入命令行或自动启动内核
每个阶段只承担与其资源条件相匹配的任务,阶段间通过明确的接口传递控制权和必要信息。GD结构体正是这种信息传递的核心载体。
board_init_f的特殊性
board_init_f 运行在重定位之前,这意味着:
- 不能使用全局变量:因为 .data 和 .bss 段可能尚未正确初始化,或者即将被重定位覆盖。
- 栈空间有限:初始栈通常设置在片内SRAM,通常只有几KB。这意味着不能使用深层递归,不能在栈上分配大型数组,每一字节都需要精打细算。
- 代码必须位置无关或在固定地址运行:不能依赖链接时确定的绝对地址(除非是XIP执行)。动态内存分配不可用。
正是这些约束,催生了GD结构体的设计——它是一块预先分配的、位置已知的内存区域,用于在无法使用全局变量的阶段存储全局状态。
GD结构体的设计哲学
GD的本质:全局变量的替代方案
在常规的C程序中,全局状态自然地存储在全局变量中。但如前所述,在U-Boot的早期启动阶段,全局变量是不可靠的。那么,如何在没有全局变量的情况下维护全局状态?
U-Boot的解决方案是:将所有需要跨函数共享的状态集中到一个结构体中,并通过一个专用的CPU寄存器保存指向该结构体的指针。
在ARM架构上,这个专用寄存器是 r9。GD结构体的首地址被存入r9,此后任何函数只需读取r9就能获得GD指针,进而访问任意全局状态。这种设计巧妙地绕过了对.data/.bss段的依赖。
U-Boot通过以下宏声明GD指针:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm("r9")
这个声明告诉编译器:变量 gd 是一个“寄存器变量”,它始终存储在r9寄存器中。编译器会:
- 在访问
gd 时直接使用r9,而非内存地址
- 在函数调用时保持r9不变(r9在ARM调用约定中是callee-saved寄存器)
- 不将r9分配给其他临时变量
GD的存储位置
GD结构体本身需要占用一块内存。在不同的启动阶段,这块内存的位置不同:
重定位前:GD通常被放置在初始栈的下方。栈向下生长,GD就在栈底(最低地址)。以i.MX6ULL为例,片内OCRAM的顶部(如0x00920000)被设为初始栈指针,GD结构体则紧贴其下方分配。
重定位后:GD会被复制到DDR的预规划位置。新的GD地址在 board_init_f 中计算并存储在 gd->new_gd 字段中。重定位代码负责将GD内容复制到新位置,然后更新r9寄存器。
GD的初始化
GD结构体的初始化发生在非常早期——在 _main(C运行时启动)函数中,甚至在 board_init_f 被调用之前。初始化过程包括:
- 计算GD地址:通常是
初始SP - sizeof(gd_t),然后对齐到16字节边界
- 清零GD内存:确保所有字段从0开始
- 设置r9寄存器:将计算出的GD地址存入r9
此阶段两个关键函数:board_init_f_alloc_reserve和board_init_f_init_reserve。
GD的生命周期
GD的生命周期贯穿整个U-Boot的执行过程:
- 创建:在
_main 中分配和初始化
- 填充(board_init_f):逐步填入硬件信息、内存配置、时钟频率等
- 复制(relocation):连同U-Boot代码一起复制到DDR新位置
- 继续使用(board_init_r):在重定位后的代码中继续访问和更新
- 传递给内核:某些信息(如内存大小)会被转换为内核启动参数
GD结构体的内部构成
U-Boot源码中的GD定义
在U-Boot源码中,GD结构体定义在 include/asm-generic/global_data.h,并由各架构通过 arch/<arch>/include/asm/global_data.h 进行扩展。核心字段包括:
/* include/asm-generic/global_data.h */
typedef struct global_data gd_t;
typedef struct global_data {
bd_t *bd; /* 板级信息指针 */
unsigned long new_gd; /* 重定位后的GD地址 */
const void *fdt_blob; /* 设备树指针 */
struct udevice *cur_serial_dev; /* 当前使用的串口设备 */
struct jt_funcs *jt; /* 跳转表(导出函数表) */
struct board_f *boardf; /* 仅在重定位前使用的板级信息 */
unsigned long ram_base; /* U-Boot 使用的 RAM 起始地址 */
unsigned long ram_top; /* U-Boot 可用 RAM 的顶部地址 */
unsigned long ram_size; /* RAM总大小 */
unsigned long relocaddr; /* 重定位后的U-Boot地址 */
unsigned long reloc_off; /* 重定位偏移量 */
unsigned long irq_sp; /* IRQ 模式栈指针 */
unsigned long start_addr_sp; /* 重定位后的栈指针(SVC 模式主栈) */
unsigned long flags; /* 全局标志位 */
unsigned int baudrate; /* 串口波特率 */
unsigned long cpu_clk; /* CPU时钟频率 */
unsigned long bus_clk; /* 总线时钟频率 */
unsigned long pci_clk; /* PCI时钟(如适用)*/
unsigned long mem_clk; /* 内存时钟频率 */
unsigned int mon_len; /* U-Boot 镜像长度 */
unsigned long fb_base; /* 帧缓冲基地址 */
struct udevice *dm_root; /* 驱动模型根设备 */
/* ... 更多字段 ... */
} gd_t;
GD的flags字段
gd->flags 是一个位图,每一位代表一个布尔状态,用于记录系统状态:
/* include/asm-generic/global_data.h */
#define GD_FLG_RELOC 0x00001 /* 代码已重定位 */
#define GD_FLG_DEVINIT 0x00002 /* 设备已初始化 */
#define GD_FLG_SILENT 0x00004 /* 静默模式 */
#define GD_FLG_POSTFAIL 0x00008 /* POST测试失败 */
#define GD_FLG_POSTSTOP 0x00010 /* POST停止 */
#define GD_FLG_LOGINIT 0x00020 /* Log已初始化 */
#define GD_FLG_DISABLE_CONSOLE 0x00040 /* 禁用控制台 */
#define GD_FLG_ENV_READY 0x00080 /* 环境变量已导入 */
#define GD_FLG_SERIAL_READY 0x00100 /* 串口已就绪 */
#define GD_FLG_FULL_MALLOC_INIT 0x00200 /* malloc完全初始化 */
#define GD_FLG_SPL_INIT 0x00400 /* SPL已初始化 */
#define GD_FLG_SKIP_RELOC 0x00800 /* 跳过重定位 */
#define GD_FLG_RECORD 0x01000 /* 记录控制台 */
#define GD_FLG_ENV_DEFAULT 0x02000 /* 使用默认环境变量 */
通过检查这些标志,代码可以知道当前处于启动的哪个阶段,哪些子系统已经就绪。
bd_t结构体
bd_t结构体定义在 include/asm-generic/u-boot.h。
gd->bd 指向的 bd_t(Board Data)结构体包含传递给内核的板级信息:
typedef struct bd_info {
unsigned long bi_memstart; /* 内存起始地址 */
unsigned long bi_memsize; /* 内存大小 */
unsigned long bi_flashstart; /* Flash起始地址 */
unsigned long bi_flashsize; /* Flash大小 */
unsigned long bi_flashoffset; /* Flash保留区偏移 */
unsigned long bi_sramstart; /* SRAM起始地址 */
unsigned long bi_sramsize; /* SRAM大小 */
unsigned long bi_bootflags; /* 启动标志 */
unsigned long bi_ip_addr; /* IP地址 */
unsigned char bi_enetaddr[6]; /* MAC地址 */
unsigned long bi_arch_number; /* ARM机器类型号 */
unsigned long bi_boot_params; /* 内核启动参数地址 */
...
} bd_t;
bd_t在 board_init_f 的内存预留阶段分配空间,在 board_init_r 中填充具体值。
早期Malloc机制
在board_init_f阶段,完整的malloc/free尚不可用(那需要DRAM),但某些初始化函数(特别是设备模型DM的初始化)需要动态分配内存。U-Boot为此实现了一个极简的“早期malloc”。位置在 common/malloc_simple.c。
这个早期malloc的特点是:
- 只分配不释放:没有free功能,分配指针
malloc_ptr只会单调递增。
- 极简高效:没有空闲链表管理、没有内存碎片处理。
- 容量有限:总大小由
CONFIG_SYS_MALLOC_F_LEN决定,通常只有几KB。
- 过渡性质:在DRAM初始化完成、
board_init_r阶段调用mem_malloc_init后,会切换到功能完整的dlmalloc。
board_init_f框架解析
board_init_f 承担着U-Boot启动第一阶段的所有C语言初始化工作,其核心使命可以归纳为三点:
- 初始化DRAM控制器,使外部DDR内存可用。
- 规划DRAM内存布局,确定U-Boot重定位目标地址、栈地址、malloc区域、设备树存放位置等。
- 计算重定位参数,为后续的代码搬移做好准备。
board_init_f运行结束后,系统将具备以下条件:
- DRAM已初始化并可访问
- GD中记录了完整的内存布局信息
- 重定位地址、栈地址等关键参数已计算就绪
但此时代码尚未被搬移到DRAM,U-Boot仍在原始加载地址运行。真正的搬移工作由后续的relocate_code汇编函数完成。
init_sequence_f初始化序列
U-Boot采用了一种优雅的设计模式来组织 board_init_f 中的初始化步骤——初始化序列(init sequence)。将复杂的初始化过程分解为一系列独立的小函数,然后按顺序依次调用。这是一个函数指针数组,每个元素指向一个初始化函数:
/* common/board_f.c */
static const init_fnc_t init_sequence_f[] = {
setup_mon_len,
#ifdef CONFIG_OF_CONTROL
fdtdec_setup,
#endif
initf_malloc,
initf_bootstage,
arch_cpu_init, /* 架构级CPU初始化 */
mach_cpu_init, /* 机器级CPU初始化 */
initf_dm, /* 早期驱动模型初始化 */
board_early_init_f, /* 板级早期初始化 */
timer_init, /* 定时器初始化 */
env_init, /* 环境变量初始化 */
init_baud_rate, /* 波特率初始化 */
serial_init, /* 串口初始化 */
console_init_f, /* 控制台初始化 */
display_options, /* 显示启动信息 */
display_text_info,
announce_dram_init,
dram_init, /* DDR初始化 */
setup_dest_addr, /* 计算目标地址 */
reserve_round_4k, /* 预留空间并对齐 */
reserve_mmu, /* 预留MMU表空间 */
reserve_video, /* 预留显存 */
reserve_malloc, /* 预留malloc堆 */
reserve_board_buf, /* 预留板级缓冲 */
reserve_global_data, /* 预留新GD空间 */
reserve_fdt, /* 预留设备树空间 */
reserve_stacks, /* 预留栈空间 */
setup_dram_config,
reloc_fdt,
setup_reloc, /* 设置重定位参数 */
NULL,
};
这种设计的优点:
- 清晰的执行顺序:初始化步骤以数组形式线性排列,一目了然。
- 易于维护:添加或删除初始化步骤只需修改数组。
- 统一的错误处理:每个函数返回0表示成功,非0表示失败,由框架统一检查。
- 可配置性:通过条件编译,不同配置可以包含不同的初始化步骤。
board_init_f执行入口
/* common/board_f.c */
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
}
这个函数的结构极为简洁:设置启动标志,然后通过initcall_run_list机制依次执行init_sequence_f数组中注册的所有初始化函数。如果任何一个初始化函数返回非零值(表示错误),系统将调用hang()` 永久挂起。
initcall机制详解
U-Boot借鉴了Linux内核的initcall设计思想,将启动过程分解为一系列独立的初始化函数,按顺序组织在一个函数指针数组中依次调用,任何一个失败则立即终止并报错。
int initcall_run_list(const init_fnc_t init_sequence[])
{
ulong reloc_ofs;
const init_fnc_t *ptr;
init_fnc_t func;
int ret = 0;
for (ptr = init_sequence; func = *ptr, func; ptr++) {
reloc_ofs = calc_reloc_ofs();
if (reloc_ofs) {
debug("initcall: %p (relocated to %p)\n",
(char *)func - reloc_ofs, (char *)func);
} else {
debug("initcall: %p\n", (char *)func - reloc_ofs);
}
ret = func();
if (ret)
break;
}
if (ret) {
printf("initcall failed at call %p (err=%d)\n",
(char *)func - reloc_ofs, ret);
return ret;
}
return 0;
}
calc_reloc_ofs()函数用于获取当前的重定位偏移量,其返回值决定了 debug 日志中如何打印函数地址——是打印重定位后的地址,还是打印编译时的原始链接地址。开发者通过 func - reloc_ofs 运算,可以反推出编译时的原始地址,从而在 map 文件中精确定位是哪个函数。
U-Boot 2022年引入了 Event(事件)机制,这是对传统 initcall 的一种补充。传统方式下,所有初始化函数都以函数指针的形式硬编码在 init_sequence_f[] 或 init_sequence_r[] 数组中。Event 机制允许在初始化序列中插入“事件触发点”,当执行到该点时,会触发所有注册了对应事件类型的回调函数。
内存布局规划
board_init_f 的一个核心任务是规划重定位后的内存布局。这个规划过程由一系列 reserve_* 函数完成,它们从RAM顶部开始,依次向下分配各功能区域。
这个布局由一系列 reserve_* 函数计算:
static int setup_dest_addr(void)
{
gd->ram_top = gd->ram_base + gd->ram_size;
gd->relocaddr = gd->ram_top; /* 从顶部开始分配 */
return 0;
}
static int reserve_malloc(void)
{
gd->relocaddr -= CONFIG_SYS_MALLOC_LEN;
gd->start_addr_sp = gd->relocaddr;
return 0;
}
static int reserve_global_data(void)
{
gd->relocaddr -= sizeof(gd_t);
gd->relocaddr &= ~15; /* 16字节对齐 */
gd->new_gd = gd->relocaddr;
return 0;
}
这种布局设计的精妙之处在于:
- U-Boot放在最高地址:为内核和ramdisk留出连续的低地址空间
- 栈在堆的下方:即使栈溢出,也不会覆盖U-Boot代码
- 16字节对齐:满足ARM ABI对栈指针的要求
- 4KB对齐:满足MMU页表的要求
board_init_f执行完init_sequence_f中所有函数后返回。此时控制流回到_main汇编代码(arch/arm/lib/crt0.S)的下半部分,系统已具备执行代码重定位的所有条件。
下一篇文章将详细分析 init_sequence_f 初始化序列中的各个函数,并在云栈社区继续我们的U-Boot探索之旅。