4. U-Boot 编译系统:Makefile体系、链接脚本、编译流程、镜像输出产物
好,咱们进入第四章。这一章我打算聊聊U-Boot的编译系统。说实话,很多工程师把U-Boot当成一个黑盒——配一下板子,make一下,烧进去,完事。但一旦遇到链接错误、镜像体积超了、或者启动卡在某个地址,你就得回头啃编译系统了。
我个人习惯,拿到一个新板子,第一件事不是改代码,而是先跑一遍完整的编译流程,把中间产物都翻一遍。这样心里才有底。咱们今天就把它拆开看看。
4.1 Makefile体系:顶层与子目录的协作
U-Boot的Makefile体系,说白了就是一套递归构建系统。顶层Makefile是总控,它负责解析配置、决定编译哪些子目录、最后链接成镜像。
我刚开始看的时候,觉得这玩意儿太绕了。后来发现,其实核心就三个层次:
- 顶层Makefile:入口,定义目标、工具链、全局变量。
- config.mk:架构和板级配置,比如ARM还是RISC-V,DDR地址多少。
- 子目录Makefile:每个模块自己的编译规则,比如drivers/、common/、lib/。
举个例子,你执行make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-,顶层Makefile会先检查环境变量,然后递归进入各个子目录。每个子目录的Makefile通过obj-y、obj-$(CONFIG_XXX)这样的变量,告诉顶层“我要编译哪些文件”。
关键点:U-Boot的Makefile体系依赖Kconfig生成的.config文件。你运行make menuconfig时,实际上是在配置这个.config,它最终决定了哪些obj-y被激活。
嗯,这里要注意:如果你改了.config,一定要重新make,否则旧的.depend文件会让你抓狂。我曾经有一次忘了重新编译,结果链接出来的镜像还是老版本,排查了半天。
4.2 链接脚本:U-Boot的“内存地图”
链接脚本(.lds文件)是U-Boot编译中最容易被忽视、但最关键的部分。它决定了代码段、数据段、BSS段在内存里的布局。
不同架构的链接脚本位置不一样:
- ARM:
arch/arm/cpu/u-boot.lds - RISC-V:
arch/riscv/cpu/u-boot.lds - x86:
arch/x86/cpu/u-boot.lds
咱们拿ARM的链接脚本举个例子。它大概长这样:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
.text : {
*(.__image_copy_start)
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
*(.text*)
}
.rodata : { *(.rodata*) }
.data : { *(.data*) }
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(4);
__image_copy_end = .;
.bss : {
__bss_start = .;
*(.bss*)
. = ALIGN(4);
__bss_end = .;
}
}
看到__image_copy_start和__image_copy_end了吗?这两个符号在U-Boot的SPL(Secondary Program Loader)阶段特别重要。SPL会把U-Boot从Flash拷贝到DDR,拷贝的范围就是这两个符号之间的区域。
避坑指南:我曾经在移植一个新板子时,发现U-Boot启动到一半就挂了。查了半天,原来是链接脚本里.bss段的起始地址没对齐,导致BSS清零时覆盖了代码段。记住:.bss段一定要ALIGN(4),最好是ALIGN(16)。
4.3 编译流程:从源码到镜像的完整链路
U-Boot的编译流程,我把它分成四个阶段。咱们用一张图来展示:
这张图把流程说清楚了。我补充几个细节:
- 配置阶段:
make menuconfig生成.config,然后make会自动调用scripts/basic/fixdep生成依赖文件。这一步如果出错,后面全白搭。 - 编译阶段:每个
.c文件先被预处理成.i,再编译成.s汇编,最后汇编成.o目标文件。U-Boot用了很多宏和条件编译,所以.i文件有时候比.c大好几倍。 - 链接阶段:这是最容易出问题的地方。链接脚本里
.(当前地址)的初始值、段的对齐方式、KEEP关键字的使用,都会影响最终镜像。 - 格式转换:
objcopy把ELF转成裸二进制,mkimage加上U-Boot头部。你烧到Flash里的,通常是u-boot.bin或u-boot.img。
注意:u-boot.bin和u-boot.img的区别在于头部。.bin是纯粹的二进制,没有头部信息;.img包含了U-Boot的头部(比如加载地址、入口地址、校验和)。如果你的板子用SPL加载U-Boot,通常需要.img格式。
4.4 镜像输出产物:你该关注哪些文件
编译完成后,make会在顶层目录生成一堆文件。很多新手看到几十个文件就懵了。我列个表,告诉你哪些是核心产物:
| 文件名 | 格式 | 用途 | 是否必须 |
|---|---|---|---|
| u-boot | ELF | 调试用,包含符号表 | 调试时必需 |
| u-boot.bin | 裸二进制 | 烧录到Flash的原始镜像 | 是 |
| u-boot.img | 带头部二进制 | SPL加载的镜像 | 取决于启动方式 |
| u-boot.srec | SREC | 通过串口下载时使用 | 可选 |
| u-boot.map | 文本 | 符号地址映射表 | 调试时强烈推荐 |
| System.map | 文本 | 全局符号地址表 | 调试时推荐 |
| spl/u-boot-spl.bin | 裸二进制 | SPL镜像(如果启用SPL) | 使用SPL时必需 |
我个人最常用的两个文件是u-boot.map和System.map。为什么?因为当U-Boot跑飞了,或者某个函数地址不对,我直接查.map文件就能定位。比如,你看到board_init_f的地址是0x87800000,但实际运行地址是0x87800100,那肯定是链接脚本或者重定位出了问题。
小技巧:编译时加上V=1参数(比如make V=1),可以看到完整的编译命令。我曾经用这个参数排查过一个优化选项导致的问题——-Os把某个循环优化掉了,结果函数行为异常。看到编译命令后,我改成-O2就解决了。
4.5 避坑指南:编译系统常见问题
最后,我分享几个我在项目中踩过的坑:
- 链接顺序问题:U-Boot的链接脚本里,
start.o必须放在最前面,因为它是入口点。如果你改了链接脚本,记得检查start.o的位置。 - BSS段溢出:BSS段的大小受限于DDR地址空间。如果你加了太多全局变量,BSS段可能会溢出到其他区域。用
size u-boot命令可以查看各段大小。 - 交叉编译工具链版本:不同版本的GCC生成的代码可能有差异。我建议固定一个版本,比如GCC 9.2或11.3,不要频繁升级。
- 缓存问题:有时候你改了代码,但
make没有重新编译。执行make clean后再make,或者直接make mrproper清理所有配置。