4、U-Boot源码结构:目录、编译框架与配置系统

U-Boot 这玩意儿,说白了就是嵌入式世界的「万能启动器」。你拿到一块新板子,第一件事就是把它跑起来。我这些年经手的项目,从 ARM9 到 Cortex-A72,几乎都在跟它打交道。

这一章,咱们就把它扒开看看。目录怎么组织的?编译怎么串联的?配置系统又是什么鬼?搞懂了这些,你才算真正入了 Bootloader 的门。

4.1 顶层目录结构:一眼看穿 U-Boot

先说说顶层目录。你下载一份 U-Boot 源码,解压后第一眼看到的就是这些文件夹。别慌,我帮你捋一捋。

目录/文件 作用 我的经验
arch/ 架构相关代码(ARM、RISC-V、x86 等) 改板级支持,90% 的时间泡在这里
board/ 具体板级初始化、外设配置 每个项目都要新建一个目录
configs/ 默认配置文件(defconfig) 我习惯从这里开始改配置
include/ 全局头文件、配置宏定义 configs/ 下的头文件是核心
drivers/ 各种驱动(串口、网卡、MMC、NAND 等) 移植新硬件,先看这里有没有现成的
common/ 通用命令、核心逻辑 比如 cmd/ 下的命令实现
lib/ 通用库函数(CRC、加密、字符串处理) 基本不动,除非要加新算法
tools/ 主机端工具(mkimage、fdt 处理等) 打包镜像时经常用到
doc/ 文档、README 遇到坑,先翻翻这里

嗯,这里要注意:arch/ 和 board/ 是移植工作的主战场。我刚开始做 U-Boot 移植时,总喜欢在 common/ 里乱改,后来发现那是给自己挖坑——架构相关的代码,一定要放在对应的 arch/ 目录下。

4.2 编译框架:Kbuild 与 Makefile 的协作

U-Boot 的编译框架,其实是从 Linux 内核学来的。它用了一套叫 Kbuild 的系统。说白了,就是通过 Makefile 把成千上万个 .c 文件组织起来,最终生成一个 u-boot.bin。

4.2.1 顶层 Makefile 做了什么?

你执行 make 时,顶层 Makefile 会做三件事:

  1. 解析配置:读取 .config 文件,决定哪些代码被编译。
  2. 递归进入子目录:根据配置,逐层进入 arch/、board/、drivers/ 等目录。
  3. 链接生成镜像:把所有目标文件链接成 u-boot 可执行文件,再转换成 u-boot.bin。

举个例子,你想编译一个 ARM 板子的 U-Boot:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard_defconfig
make -j4

第一行是配置,第二行是编译。我个人习惯把 CROSS_COMPILE 写进环境变量,省得每次敲。

4.2.2 Kbuild 的核心:obj-y 与 obj-$(CONFIG_xxx)

每个子目录下都有一个 Makefile,里面写着类似这样的内容:

obj-y += my_driver.o
obj-$(CONFIG_MY_FEATURE) += my_feature.o

这里 obj-y 表示「总是编译」,obj-$(CONFIG_xxx) 表示「如果配置了 xxx 就编译」。你想想看,这其实就是个条件编译的开关。我在项目中遇到过一个问题:明明加了驱动文件,但编译出来就是没有。后来发现是 CONFIG_xxx 没打开,驱动根本没被编译进去。

小技巧:调试时,可以用 make V=1 查看详细的编译命令。这样能清楚看到每个文件有没有被编译,链接顺序对不对。

4.3 配置系统:从 defconfig 到 .config

U-Boot 的配置系统,经历过一次大改。早期版本用的是 include/configs/ 下的头文件,现在主流版本(2016 年以后)用的是 Kconfig 加 defconfig 的方式。

4.3.1 配置流程

整个配置过程,其实就三步:

  1. 选择默认配置:make xxx_defconfig,从 configs/ 目录下读取默认配置。
  2. 生成 .config:把默认配置展开,生成一个完整的 .config 文件。
  3. 生成头文件:编译时,.config 会被转换成 include/autoconf.mk 和 include/generated/autoconf.h,供 Makefile 和 C 代码使用。

我曾经踩过一个坑:手动改了 .config 后,直接 make,结果配置没生效。后来才发现,改完 .config 后必须执行 make olddefconfig 来同步依赖关系。

4.3.2 Kconfig 的层级结构

Kconfig 文件分布在各个目录下,形成了一个树状结构。顶层 Kconfig 会 source 子目录的 Kconfig,子目录再 source 更细的模块。

举个例子,你想给板子加一个网络功能,配置路径可能是:

Device Drivers → Network device support → Ethernet driver → My Ethernet driver

对应的 Kconfig 写法:

config MY_ETHERNET
    bool "My Ethernet driver"
    depends on DM_ETH
    help
      Say Y here to enable My Ethernet driver.
注意:depends on 和 select 的区别要搞清楚。depends on 是「依赖」,如果父选项没开,子选项不会显示;select 是「自动选中」,常用于强制开启某个依赖。我见过有人用 select 导致循环依赖,编译直接挂掉。

4.4 知识体系总览

说了这么多,咱们用一张图把 U-Boot 源码结构、编译框架和配置系统的关系串起来。这张图我画了好几次,才把逻辑理清楚。

U-Boot 源码结构与编译配置流程 U-Boot 源码结构与编译配置流程 源码目录结构 编译框架 (Kbuild) 配置系统 (Kconfig) 生成产物 arch/ board/ drivers/ common/ lib/ include/ configs/ tools/ doc/ 每个目录有独立 Makefile 顶层 Makefile 递归进入子目录 obj-y / obj-$(CONFIG) 条件编译控制 defconfig → .config Kconfig 树状结构 depends on / select make menuconfig 可视化 u-boot.bin u-boot.img SPL / TPL mkimage 打包 核心逻辑:配置驱动编译,编译生成镜像 配置系统决定「编译什么」→ 编译框架决定「怎么编译」→ 目录结构决定「代码放哪」 三者缺一不可,任何一个环节出错,U-Boot 都跑不起来 最终目标:启动内核,加载系统 Bootloader 的使命,就是把控制权交给操作系统

4.5 避坑指南:我踩过的几个坑

最后,分享几个实战中容易踩的坑。这些可都是真金白银换来的教训。

  • 配置没生效:改完 .config 后,一定要执行 make olddefconfig 或 make savedefconfig。我有一回改了个驱动开关,直接 make,结果编译出来还是老样子,排查了半天才发现是配置没同步。
  • 链接顺序问题:U-Boot 对链接顺序有要求,特别是初始化函数。如果你在 board/ 下加了新文件,记得检查 Makefile 中的 obj-y 顺序。我曾经因为顺序不对,导致板子启动到一半就挂了。
  • 头文件路径:U-Boot 的头文件搜索路径很复杂,有 include/、arch/arm/include/、board/xxx/include/ 等。如果你遇到「找不到头文件」的错误,先看看 -I 参数有没有包含你的路径。
  • SPL 与 U-Boot 的配置冲突:现在很多板子用 SPL(Secondary Program Loader)来加载 U-Boot。SPL 和 U-Boot 共用一部分配置,但有些配置只能用于 SPL。我见过有人把 CONFIG_SPL_BUILD 用错了地方,导致 SPL 编译出来的镜像比 U-Boot 还大。

一句话总结:U-Boot 的源码结构、编译框架和配置系统,是一个三位一体的整体。搞懂了目录怎么组织、Makefile 怎么串联、Kconfig 怎么控制开关,你就能像庖丁解牛一样,轻松驾驭任何板子的 U-Boot 移植工作。