第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 的运行地址。这个值错了,板子就起不来。
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 的源码结构、关键文件、编译流程画在了一起。你保存下来,以后看源码时对照着看。
3.5 编译系统实战:一个最小化编译示例
光说不练假把式。我们拿一个实际的板子来走一遍编译流程。假设你用的是 qemu-arm 虚拟板。
- 清理环境:
make distclean—— 把之前编译的残留全清掉。 - 配置:
make qemu_arm_defconfig—— 从configs/下加载配置。 - 编译:
make -j4—— 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/ 下的文件删了,以为没用。结果编译报错,找了一下午才发现是板级目录没了。所以,不熟悉的目录别乱删。