当我们 git clone U-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 文件逻辑上再核心,如果它所在的目录缺少 MakefileKconfig 条目,Kbuild 也不会编译它。

层级 必需文件/目录
根构建系统 MakefileKbuild.includescripts/basic/scripts/kconfig/
配置生成 .configinclude/config/include/generated/autoconf.h
链接脚本 arch/sandbox/cpu/u-boot.lds(定义 .u_boot_list_* 等链接段)
各级 Makefile arch/sandbox/Makefileboard/sandbox/Makefilecommon/Makefiledrivers/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 memcpymemsetstrlen
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 gdglobal_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.ccommon/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.cos.c
启动序列 2 board_f.c(Stage F)、board_r.c(Stage R)
主循环 2 main.ccli.c
Driver Model 5-6 root.clists.cdevice.cuclass.c
串口/控制台 3-4 console.cserial-*.c
内存管理 2 dlmalloc.c
环境变量 2 env.cnowhere.c
基础库 10+ string.cvsprintf.cfdtdec.c、libfdt

其余 2000+ 个文件(驱动、网络、文件系统、测试框架等)都是按需选配。理解这一点,你就掌握了阅读 U-Boot 源码的"导航图"——遇到陌生文件时,先问自己:它属于最小骨架,还是可选扩展?


延伸阅读建议

  1. Linker List 机制:阅读 include/linker_lists.h,理解 U_BOOT_CMDU_BOOT_DRIVER 等宏如何利用链接段自动收集数据
  2. Driver Model 文档doc/driver-model/README.txt
  3. FIT Imagedoc/uImage.FIT/howto.txt,了解现代 U-Boot 如何加载多组件镜像
  4. 真实硬件对比:将 Sandbox 的最小文件集合与 TI K3 平台(arch/arm/mach-k3/)对比,观察 tiboot3.bintispl.binu-boot.img 的启动链如何映射到这些核心文件

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐