U-Boot 最小骨架剖析:从 2000+ 源文件到 50 个核心文件,以SandBox为例
摘要: U-Boot源码虽庞大,但其核心启动流程仅由不到50个关键文件支撑。本文通过Sandbox平台分析最小骨架,揭示U-Boot的核心机制:1)构建系统依赖Kbuild和链接脚本(如u-boot.lds);2)启动分为前置重定位(init_sequence_f)和后置重定位(init_sequence_r)两个阶段;3)最终进入main_loop实现命令交互。最小文件集涵盖架构层(sandbo
当我们
git cloneU-Boot 源码时,面对 2000 多个源文件往往无从下手。本文从编译构建、链接产物、运行时执行路径三个维度,拆解出让 U-Boot 在 Sandbox 平台跑起来的最小文件集合,帮你建立对 U-Boot 架构的"第一性原理"认知。
一、为什么需要理解"最小骨架"?
U-Boot 的源码树极其庞大,涵盖 ARM、RISC-V、x86 等数十种架构,以及网络、USB、MMC、SPI 等数百个驱动。但无论功能如何膨胀,其核心启动流程始终由不到 50 个文件支撑。理解这个"最小骨架",能让你:
- 快速定位启动失败的根因(是构建系统缺失?链接脚本错误?还是初始化序列中断?)
- 在裁剪 U-Boot 时做出理性决策,而非盲目关闭配置项
- 为移植到新硬件建立清晰的"必需 vs 可选"判断标准
本文以 Sandbox 平台(arch/sandbox)为分析对象,因为它允许 U-Boot 作为 Linux 用户态程序运行,剥离了硬件汇编的复杂性,最利于观察纯 C 层面的启动逻辑。
二、构建系统层面:没有这些,编译无法进行
U-Boot 使用 Kbuild 构建系统。即使某个 .c 文件逻辑上再核心,如果它所在的目录缺少 Makefile 和 Kconfig 条目,Kbuild 也不会编译它。
| 层级 | 必需文件/目录 |
|---|---|
| 根构建系统 | Makefile、Kbuild.include、scripts/basic/、scripts/kconfig/ |
| 配置生成 | .config → include/config/、include/generated/autoconf.h |
| 链接脚本 | arch/sandbox/cpu/u-boot.lds(定义 .u_boot_list_* 等链接段) |
| 各级 Makefile | arch/sandbox/Makefile、board/sandbox/Makefile、common/Makefile、drivers/core/Makefile 等 |
关键洞察:u-boot.lds 链接脚本中定义的 .u_boot_list_* 段是 U-Boot Linker List 机制的基石。命令注册(U_BOOT_CMD)、驱动注册(U_BOOT_DRIVER)、初始化函数注册(INIT_LIST)都依赖这些段自动收集符号,避免手动维护列表。
三、执行路径层面:从 main() 到命令提示符
U-Boot 的启动分为两个阶段:前置重定位(Stage F)和后置重定位(Stage R)。Sandbox 平台虽然不需要真实重定位,但完整保留了这两个初始化序列。
3.1 Stage F:前置重定位(init_sequence_f[])
// common/board_f.c
static const init_fnc_t init_sequence_f[] = {
setup_mon_len, // 计算 U-Boot 镜像长度
initf_malloc, // 初始化早期堆内存
log_init, // 日志子系统
initf_bootstage, // 启动阶段计时
setup_spl_handoff, // SPL 交接信息
initf_console_record, // 控制台记录
arch_cpu_init, // 架构级 CPU 初始化
mach_cpu_init, // 机器级 CPU 初始化
initf_dm, // 早期 Driver Model 初始化
arch_cpu_init_dm, // 架构 DM 初始化
timer_init, // 定时器初始化
env_init, // 环境变量初始化
init_baud_rate, // 波特率初始化
serial_init, // 串口初始化
console_init_f, // 控制台前置初始化
display_options, // 打印版本信息
display_text_info, // 打印文本信息
checkcpu, // CPU 检测
announce_dram_init, // 宣告 DRAM 初始化
dram_init, // DRAM 参数设置(Sandbox 中为模拟值)
...
};
3.2 Stage R:后置重定位(init_sequence_r[])
// common/board_r.c
static const init_fnc_t init_sequence_r[] = {
initr_reloc, // 重定位(Sandbox 中为模拟)
initr_reloc_global_data,// 重定位全局数据 gd
initr_barrier, // 内存屏障
initr_malloc, // 完整堆内存初始化
log_init, // 日志初始化
initr_bootstage, // 启动阶段计时
initr_console_record, // 控制台记录
initr_dm, // 完整 Driver Model 初始化
board_init, // 板级初始化
initr_dm_devices, // DM 设备扫描与绑定
stdio_init_tables, // 标准 IO 表初始化
initr_serial, // 串口后置初始化
initr_announce, // 宣告启动
power_init_board, // 板级电源初始化
initr_env, // 环境变量后置初始化
stdio_add_devices, // 添加 stdio 设备
console_init_r, // 控制台后置初始化
board_late_init, // 板级晚期初始化
eth_initialize, // 网络初始化(如启用)
main_loop, // 进入主循环
};
关键洞察:main_loop() 位于 common/main.c,它是 U-Boot 的"宇宙中心"——要么执行自动启动命令 bootcmd,要么进入交互式命令行。所有初始化序列都是为这一刻做准备。
四、最小源码文件集合(Sandbox 平台)
基于上述执行路径,我们可以反推出理论最小可运行的源文件集合:
4.1 架构层(arch/sandbox/)
| 文件 | 作用 |
|---|---|
arch/sandbox/cpu/start.c |
main() 入口,连接宿主系统 |
arch/sandbox/cpu/os.c |
宿主机系统调用封装(文件、网络、时间) |
arch/sandbox/lib/sections.c |
段信息获取 |
4.2 板级层(board/sandbox/)
| 文件 | 作用 |
|---|---|
board/sandbox/sandbox.c |
dram_init()、board_init()、gd 全局数据定义 |
4.3 启动序列与主循环(common/)
| 文件 | 作用 |
|---|---|
common/board_f.c |
init_sequence_f[],Stage F 启动序列 |
common/board_r.c |
init_sequence_r[],Stage R 启动序列 |
common/main.c |
main_loop(),主循环入口 |
common/cli.c |
命令行主循环 |
common/cli_simple.c |
简单命令解析器 |
common/cli_readline.c |
行编辑(历史记录、退格处理) |
common/command.c |
命令注册与查找(依赖 Linker List) |
common/console.c |
控制台输入输出 |
common/stdio.c |
stdio 设备管理 |
common/dlmalloc.c |
堆内存管理 |
common/bootstage.c |
启动阶段计时 |
common/log.c |
日志子系统 |
4.4 环境变量(env/)
| 文件 | 作用 |
|---|---|
env/env.c |
环境变量核心 API |
env/nowhere.c |
无持久存储后端(纯内存环境变量) |
4.5 Driver Model 核心(drivers/core/)
| 文件 | 作用 |
|---|---|
drivers/core/root.c |
dm_init()、dm_scan_fdt(),DM 根节点 |
drivers/core/lists.c |
驱动查找与设备绑定 |
drivers/core/device.c |
udevice 创建与销毁 |
drivers/core/uclass.c |
uclass 管理(设备分类) |
drivers/core/ofnode.c |
设备树节点操作 |
drivers/core/of_access.c |
设备树属性访问 |
4.6 串口(drivers/serial/)
| 文件 | 作用 |
|---|---|
drivers/serial/serial-uclass.c |
串口 uclass 层 |
drivers/serial/sandbox.c |
Sandbox 虚拟串口实现 |
4.7 基础库(lib/)
| 文件 | 作用 |
|---|---|
lib/string.c |
memcpy、memset、strlen 等 |
lib/vsprintf.c |
printf 家族格式化输出 |
lib/ctype.c |
字符分类函数 |
lib/errno.c |
错误码定义 |
lib/display_options.c |
版本号打印 |
lib/hang.c |
死循环挂起 |
lib/fdtdec.c |
设备树解析封装 |
lib/libfdt/fdt*.c |
官方 libfdt(约 10 个文件) |
4.8 最小命令集(cmd/)
| 文件 | 作用 |
|---|---|
cmd/help.c |
help 命令 |
cmd/echo.c |
echo 命令 |
cmd/version.c |
version 命令 |
cmd/exit.c |
exit 命令 |
cmd/nvedit.c |
env 命令族(setenv/saveenv/printenv) |
4.9 关键头文件(include/)
| 文件 | 作用 |
|---|---|
include/common.h |
万能头文件,几乎所有 .c 都会包含 |
include/configs/sandbox.h |
Sandbox 板级配置宏 |
include/asm-generic/global_data.h |
gd(global_data)结构体定义 |
include/dm/device.h |
udevice 结构体 |
include/dm/uclass.h |
uclass 结构体 |
include/dm/root.h |
DM 根设备声明 |
include/dm/lists.h |
Linker List 宏定义 |
include/fdtdec.h |
设备树解析接口 |
五、链接层面的真相:编译多 ≠ 链接多
观察编译日志 .u-boot.cmd,链接器接收的是各层级的 built-in.o:
arch/sandbox/cpu/built-in.o
arch/sandbox/lib/built-in.o
board/sandbox/built-in.o
cmd/built-in.o
common/built-in.o
drivers/core/built-in.o
drivers/serial/built-in.o
lib/built-in.o
env/built-in.o
但 Linker 只会从中提取包含未定义符号的目标文件。
这意味着:即使你把 drivers/net/built-in.o 传进去,如果没有任何代码调用网络函数,里面的 .o 不会进入最终 ELF。所以编译时生成的 .o 很多,但链接进 u-boot 的实际代码要少得多。这也是 U-Boot 能通过 Kconfig 大幅裁剪体积的根本原因——未被引用的代码会被链接器自动丢弃。
六、实验:如何验证"最小集合"?
如果你想亲手验证这个理论,可以按以下步骤操作:
步骤 1:关闭所有可选功能
make sandbox_defconfig
make menuconfig
# 关闭以下选项:
# CONFIG_NET=n
# CONFIG_USB=n, CONFIG_PCI=n, CONFIG_MMC=n, CONFIG_SPI=n, CONFIG_I2C=n
# 所有文件系统
# CONFIG_EFI=n, CONFIG_ANDROID=n, CONFIG_TPM=n, CONFIG_WDT=n
# CONFIG_UT_TEST=n
步骤 2:观察体积变化
记录 u-boot 文件大小和编译时间的变化。
步骤 3:极限裁剪初始化序列
手工修改 common/board_f.c 和 common/board_r.c,将 init_sequence_f[] / init_sequence_r[] 中大部分函数指针删掉,只保留:
// Stage F 最小集
setup_mon_len, initf_malloc, serial_init, console_init_f, dram_init
// Stage R 最小集
initr_malloc, initr_dm, stdio_init_tables, initr_serial, console_init_r, main_loop
这样编译出来的 U-Boot 虽然功能极少,但仍然能启动并进入命令行。
七、总结:四层核心架构
| 模块 | 关键文件数 | 核心作用 |
|---|---|---|
| 构建系统 | ~5 | Makefile、Kconfig、链接脚本 |
| 架构入口 | 2-3 | start.c、os.c |
| 启动序列 | 2 | board_f.c(Stage F)、board_r.c(Stage R) |
| 主循环 | 2 | main.c、cli.c |
| Driver Model | 5-6 | root.c、lists.c、device.c、uclass.c |
| 串口/控制台 | 3-4 | console.c、serial-*.c |
| 内存管理 | 2 | dlmalloc.c |
| 环境变量 | 2 | env.c、nowhere.c |
| 基础库 | 10+ | string.c、vsprintf.c、fdtdec.c、libfdt |
其余 2000+ 个文件(驱动、网络、文件系统、测试框架等)都是按需选配。理解这一点,你就掌握了阅读 U-Boot 源码的"导航图"——遇到陌生文件时,先问自己:它属于最小骨架,还是可选扩展?
延伸阅读建议
- Linker List 机制:阅读
include/linker_lists.h,理解U_BOOT_CMD、U_BOOT_DRIVER等宏如何利用链接段自动收集数据 - Driver Model 文档:
doc/driver-model/README.txt - FIT Image:
doc/uImage.FIT/howto.txt,了解现代 U-Boot 如何加载多组件镜像 - 真实硬件对比:将 Sandbox 的最小文件集合与 TI K3 平台(
arch/arm/mach-k3/)对比,观察tiboot3.bin→tispl.bin→u-boot.img的启动链如何映射到这些核心文件
更多推荐


所有评论(0)