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的编译流程,我把它分成四个阶段。咱们用一张图来展示:

U-Boot编译流程四阶段 U-Boot 编译流程四阶段 阶段1:配置 make menuconfig 阶段2:预处理+编译 .c → .i → .s → .o 阶段3:链接 .o → u-boot.elf 阶段4:格式转换 .elf → .bin / .img 输出产物 u-boot.elf ELF调试文件 u-boot.bin 裸二进制镜像 u-boot.img 带头部镜像 u-boot.srec SREC格式 说明 • 配置阶段生成 .config 和 autoconf.mk • 编译阶段每个 .c 文件生成对应的 .o 文件 • 链接阶段使用 .lds 脚本合并所有 .o 文件

这张图把流程说清楚了。我补充几个细节:

  1. 配置阶段:make menuconfig生成.config,然后make会自动调用scripts/basic/fixdep生成依赖文件。这一步如果出错,后面全白搭。
  2. 编译阶段:每个.c文件先被预处理成.i,再编译成.s汇编,最后汇编成.o目标文件。U-Boot用了很多宏和条件编译,所以.i文件有时候比.c大好几倍。
  3. 链接阶段:这是最容易出问题的地方。链接脚本里.(当前地址)的初始值、段的对齐方式、KEEP关键字的使用,都会影响最终镜像。
  4. 格式转换: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清理所有配置。