4. U-Boot编译系统
说实话,U-Boot的编译系统,是很多初学者第一个碰壁的地方。
我第一次接触U-Boot时,直接敲了个make,结果报了一堆莫名其妙的错误。后来才明白——这东西不是随便就能编译的。你得先配好工具链,选对板子,然后它才会乖乖工作。
这一章,我就带你把它彻底搞明白。
4.1 从顶层Makefile说起
U-Boot的顶层Makefile,是整个编译系统的入口。你执行make时,它负责调度一切。
但你别指望它像普通Linux应用那样简单。U-Boot的Makefile结构,其实借鉴了Linux内核的Kbuild系统。说白了,就是一套递归编译 + 配置分离的框架。
核心思想:U-Boot把编译过程拆成两步——先配置,再编译。配置生成.config文件,编译则根据.config决定哪些文件参与构建。
顶层Makefile主要干这几件事:
- 定义目标架构、交叉编译工具链前缀
- 包含
config.mk和.config文件 - 递归进入各子目录执行编译
- 链接生成最终的
u-boot.bin、u-boot.img等镜像
嗯,这里要注意:U-Boot的Makefile不是手写的,而是通过scripts/Makefile.build这套模板自动生成的。你想想看,如果每个子目录都手写Makefile,那维护量得多大?
4.2 Kbuild系统:U-Boot的编译骨架
Kbuild系统,全称是Kernel Build System。U-Boot直接把它从Linux内核搬了过来。
它的核心逻辑很简单:
- 每个子目录里放一个
Makefile,只描述本目录要编译什么 - 顶层Makefile通过
scripts/Makefile.build统一调用 - 所有编译选项、链接脚本,都通过变量传递
举个例子,你在drivers/net/目录下看到的Makefile,可能就几行:
obj-$(CONFIG_DRIVER_ETH_DM9000) += dm9000.o
obj-$(CONFIG_DRIVER_ETH_RTL8139) += rtl8139.o
看到了吗?obj-y表示总是编译,obj-$(CONFIG_XXX)表示条件编译。这个CONFIG_XXX的值,就来自你配置时生成的.config文件。
我的经验:如果你要添加一个新的驱动,千万别去改顶层Makefile。只需要在对应子目录的Makefile里加一行obj-y += my_driver.o,然后在Kconfig里加上配置项就行了。U-Boot会自动处理剩下的。
4.3 交叉编译工具链配置
这是最容易踩坑的地方。我见过太多人卡在这一步。
交叉编译工具链,说白了就是一套在PC上运行、但生成目标板代码的编译器。比如你给ARM板编译U-Boot,就得用arm-linux-gnueabihf-gcc,而不是PC自带的gcc。
配置方式有两种:
| 方式 | 命令示例 | 说明 |
|---|---|---|
| 环境变量 | export CROSS_COMPILE=arm-linux-gnueabihf- |
全局生效,适合长期使用 |
| 命令行参数 | make CROSS_COMPILE=arm-linux-gnueabihf- |
临时指定,适合测试 |
我个人习惯用环境变量。因为这样不用每次编译都敲一遍。但要注意:工具链的路径必须加到PATH里,否则系统找不到。
我曾经踩过的坑:有一次我配好了CROSS_COMPILE,但编译时一直报arm-linux-gnueabihf-gcc: command not found。查了半天才发现,工具链装在了/opt/toolchains/下,但PATH里没加。记住:export PATH=$PATH:/opt/toolchains/bin,这一步不能省。
4.4 编译过程详解:从源码到镜像
好了,现在我们来走一遍完整的编译流程。你执行make后,U-Boot内部到底发生了什么?
我用一张流程图来展示:
流程其实不复杂。我拆开来讲:
- 配置阶段:你执行
make xxx_defconfig,U-Boot会根据configs/目录下的默认配置,生成.config文件。 - 读取配置:顶层Makefile包含
.config,所有CONFIG_XXX变量被赋值。 - 生成autoconf.mk:
.config被转换成include/autoconf.mk,供Makefile使用。同时也会生成include/config.h,供C代码使用。 - 递归编译:Kbuild系统遍历所有子目录,根据
obj-y和obj-$(CONFIG_XXX)决定编译哪些文件。每个.c文件被编译成.o文件。 - 链接:所有
.o文件按照u-boot.lds链接脚本,生成ELF格式的u-boot文件。 - 生成镜像:通过
objcopy去掉ELF头部,生成纯二进制的u-boot.bin。如果需要,还会用mkimage工具加上U-Boot头部,生成u-boot.img。
一个小技巧:如果你只想编译某个模块,比如只编译网卡驱动,可以执行make drivers/net/。U-Boot的Kbuild系统支持单目录编译,能省不少时间。
4.5 编译产物说明
编译完成后,你会在顶层目录看到一堆文件。我列个表,告诉你哪些是真正有用的:
| 文件 | 格式 | 用途 |
|---|---|---|
| u-boot | ELF | 调试用,包含符号表 |
| u-boot.bin | 纯二进制 | 直接烧写到Flash,从0地址启动 |
| u-boot.img | 带头部 | 包含校验和、加载地址,适合网络加载 |
| u-boot.srec | S-Record | 用于串口下载,每行包含地址和数据 |
| .config | 文本 | 当前配置,可以备份用于后续编译 |
我个人最常用的是u-boot.bin。直接烧到SPI Flash里,上电就能跑。但如果你用的是SD卡启动,可能需要u-boot.img,因为SPL会解析头部信息。
注意:不同板子的启动方式不同,需要的镜像格式也不同。比如有些TI的AM335x板子,需要MLO(SPL)和u-boot.img两个文件。别搞混了。
4.6 常见编译问题与解决
编译过程中,你可能会遇到这些问题。我把自己踩过的坑列出来:
- 找不到头文件:检查
include/configs/下有没有你的板级头文件。如果没有,说明配置没选对。 - 链接报错:通常是某个驱动没编译进去,或者链接脚本里地址不对。检查
.config里对应的CONFIG_XXX有没有打开。 - 工具链版本不兼容:U-Boot对GCC版本有要求。太新的GCC可能会报一些奇怪的警告,甚至错误。我建议用Linaro提供的工具链,比较稳定。
- 编译到一半卡住:可能是某个子目录的Makefile写错了。试试
make V=1,打印详细日志,定位问题。
嗯,其实编译系统这块,说难也不难。你只要记住:配置靠Kconfig,编译靠Kbuild,工具链靠环境变量。这三条线理清了,U-Boot的编译对你来说就是小菜一碟。
好了,这一章就到这里。下一章我们聊聊U-Boot的启动流程——从ROM代码到SPL,再到U-Boot proper,看看芯片上电后到底发生了什么。
可可精品资料库