第3章 U-Boot源码结构分析:目录结构、关键文件与编译系统

U-Boot 的源码,说实话,第一次打开的时候确实有点懵。目录多,文件杂,感觉像个大杂烩。但你别怕,这东西有规律。我做了这么多年嵌入式,看过不下几十个版本的 U-Boot,它的骨架其实一直没怎么变。

这一章,我就带你把它拆开看看。哪里是核心,哪里是板级相关,编译系统又是怎么把这么多文件串起来的。搞懂了这些,后面移植和调试你心里就有底了。

3.1 顶层目录结构:先看骨架

你随便下一个 U-Boot 源码,解压后第一眼看到的顶层目录,大概长这样。我挑几个最重要的说说。

目录/文件 作用 我的备注
arch/ 体系结构相关代码。ARM、x86、RISC-V 都在这里 移植时改得最多的就是 arch/arm/
board/ 板级初始化代码。每个开发板一个子目录 比如 board/samsung/smdk2440/
configs/ 默认配置文件。每个板子一个 _defconfig 编译前第一步就是 make xxx_defconfig
include/ 头文件。特别是 configs/ 下的板级配置头文件 老版本 U-Boot 的配置主要在这里
drivers/ 各种驱动。网卡、串口、MMC、USB 等 设备树匹配的驱动都在这里
dts/ 设备树源文件 (.dts) 和头文件 (.dtsi) 新版本 U-Boot 设备树都放这了
common/ 通用核心代码。比如命令行、环境变量 一般不动,除非你要加新命令
net/ 网络协议栈。TFTP、NFS 等 调试时经常用到
tools/ 主机端工具。比如 mkimage 打包镜像用的
我的习惯:拿到一个新板子,我第一件事就是去 configs/ 和 board/ 下找最接近的参考板。然后复制一份,改名字。这比从零开始快得多。

3.2 关键文件:启动流程的命脉

U-Boot 的启动,说白了就是一段汇编 + 一段 C 代码。有几个文件你必须要认识。

3.2.1 入口点:arch/arm/cpu/armv7/start.S

这是 ARM 架构下的第一行代码。CPU 上电后,从复位向量跳到这里。它负责设置 CPU 模式、关闭中断、初始化栈指针,然后跳转到 C 语言的入口函数 board_init_f()。

嗯,这里要注意。不同 CPU 架构的 start.S 位置不一样。ARMv8 的在 arch/arm/cpu/armv8/ 下。别找错了。

3.2.2 板级初始化:board/<vendor>/<board>/board.c

这个文件里实现了 board_init_f() 和 board_init_r()。前者是重定位前的初始化,后者是重定位后的。我遇到过很多新手,把时钟配置写错了地方,导致串口死活没输出。其实就是没搞清楚这两个阶段的区别。

3.2.3 配置文件:include/configs/<board>.h

老版本 U-Boot 的配置中心。宏定义满天飞。比如 CONFIG_SYS_TEXT_BASE 定义了 U-Boot 的运行地址。这个值错了,板子就起不来。

我曾经踩过的坑:有一次我移植 U-Boot,忘了改 CONFIG_SYS_TEXT_BASE,结果编译出来的镜像下载到板子上,一跑就死。查了两天才发现是地址冲突了。所以,这个值一定要跟你的内存布局对上。

3.3 编译系统:Kbuild / Makefile 是怎么工作的?

U-Boot 的编译系统是从 Linux 内核学来的,叫 Kbuild。它比老式的递归 Makefile 要清晰得多。但第一次接触,可能会觉得有点绕。我把它拆成三步讲。

3.3.1 配置阶段:make xxx_defconfig

这一步会从 configs/ 目录下读取默认配置,生成 .config 文件。.config 里全是 CONFIG_XXX=y 这样的开关。比如:

CONFIG_ARM=y
CONFIG_TARGET_SMDK2440=y
CONFIG_DM=y
CONFIG_DM_SERIAL=y

这些开关决定了哪些文件被编译,哪些功能被开启。

3.3.2 编译阶段:make

顶层 Makefile 会递归进入各个子目录。但注意,它不是无脑递归。Kbuild 系统会根据 .config 和各级 Makefile 中的 obj-y 变量,决定哪些目录需要进去编译。

举个例子,在 drivers/serial/Makefile 里:

obj-$(CONFIG_DM_SERIAL) += serial-uclass.o
obj-$(CONFIG_SYS_NS16550) += ns16550.o

如果 CONFIG_DM_SERIAL=y,就编译 serial-uclass.o。否则跳过。这就是 Kbuild 的优雅之处——按需编译。

3.3.3 链接阶段:u-boot.lds

链接脚本决定了各个段(.text, .data, .bss)的排列顺序和加载地址。它通常在 arch/arm/cpu/ 下。比如:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
    . = 0x00000000;
    .text : {
        *(.__image_copy_start)
        *(.text*)
    }
    . = ALIGN(4);
    .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
    . = ALIGN(4);
    .data : { *(.data*) }
    . = ALIGN(4);
    .bss : { *(.bss*) }
}

你看,ENTRY(_start) 指定了入口点,就是 start.S 里的 _start 标号。链接脚本搞错了,程序就跑飞了。

核心逻辑总结:配置决定编译什么,编译生成目标文件,链接脚本把它们拼成可执行镜像。这三步,一步都不能错。

3.4 知识体系图:U-Boot 源码结构全景

下面这张图,我把 U-Boot 的源码结构、关键文件、编译流程画在了一起。你保存下来,以后看源码时对照着看。

U-Boot 源码结构与编译流程全景图 U-Boot 源码结构与编译流程全景图 arch/ (体系结构) board/ (板级代码) configs/ (默认配置) dts/ (设备树) start.S (入口) board.c (初始化) _defconfig (配置) .dts / .dtsi (描述) make xxx_defconfig make (编译) u-boot.bin (镜像) 关键说明: 1. 配置阶段:读取 configs/xxx_defconfig,生成 .config 文件 2. 编译阶段:Kbuild 根据 .config 和 Makefile 中的 obj-y 决定编译哪些文件 3. 链接阶段:u-boot.lds 将 .o 文件按指定地址链接成 u-boot.bin 4. 设备树:dts/ 下的 .dts 文件会被编译成 .dtb,附加到镜像中

3.5 编译系统实战:一个最小化编译示例

光说不练假把式。我们拿一个实际的板子来走一遍编译流程。假设你用的是 qemu-arm 虚拟板。

  1. 清理环境:make distclean —— 把之前编译的残留全清掉。
  2. 配置:make qemu_arm_defconfig —— 从 configs/ 下加载配置。
  3. 编译:make -j4 —— 4 线程并行编译,快一些。
  4. 查看输出:编译完成后,在顶层目录下会生成 u-boot.bin、u-boot (ELF 格式)、u-boot.map (符号映射表)。
我建议:每次编译完,都看一眼 u-boot.map 文件。它能告诉你每个函数、每个变量被链接到了哪个地址。调试时特别有用。

如果你在编译时加了 V=1,比如 make V=1,就能看到详细的编译命令。这对于排查编译错误非常有帮助。我曾经靠这个参数,发现了一个头文件路径写错的问题。

3.6 避坑指南:新手最容易犯的三个错误

  • 配置没生效:改了 configs/xxx_defconfig 后,一定要重新 make xxx_defconfig 和 make。只改文件不重新配置,等于白改。
  • 链接地址不对:CONFIG_SYS_TEXT_BASE 必须和你的硬件内存映射一致。否则 U-Boot 重定位时会把自己写死。
  • 设备树没更新:改了 .dts 文件后,编译时 U-Boot 会自动重新编译设备树。但如果你手动改了 .dtb,记得清理重编。不然用的还是旧的。
我曾经见过最离谱的:有同事把 board/ 下的文件删了,以为没用。结果编译报错,找了一下午才发现是板级目录没了。所以,不熟悉的目录别乱删。