U-Boot分析【学习笔记】(6)

7. Make 工具的文件调用溯源

在前面章节的我们介绍了 U-Boot 的许多文件,如 .configautoconf.hconfig.huboot-cfg

但是我们并不知道 Makefile 是如何在执行过程中调用这些文件的,故本文通过 make 执行的输出内容按图索骥寻找这些文件是如何被调用的。

7.1 remake 介绍

在此之前我们需要先介绍 make 工具中的 remake机制,对后续分析文件调用有帮助。

shell 复制代码
-include A_value

是指包含 A_value 这个文件,如果不存在这个文件也不会报错

但是我们发现后面有一个同名的 A_value 目标,那么 makefile 会自动执行这个选项

shell 复制代码
@echo "A=5" > A_value

是指创建一个 A_value 文件,并把A=5写入进去。

在创建完后,makefile 会重新执行

shell 复制代码
-include A_value

此时存在 A_value 文件,所以 A 的值可以被正常打印

从打印顺序我们也可以看出,先打印 "A_value file is created",再打印后面两句。由此我们可以知道 makefile 是执行了A_value 目标,再回头执行 all 目标

7.2 makefile 基本语法说明

1. -include <文件路径>

语法规则:

如果文件存在,就地展开;如果不存在,不报错,而是去下方寻找有没有"生产这个文件"的规则。

U-Boot 实战举例:

shell 复制代码
-include include/config/auto.conf

情景:这是存放 .config 转化后的变量文件。

逻辑:

Make 读到这一行,发现 auto.conf 还没生成。

因为有 -,它不报错,继续往下走。

它在后面找到了生成 auto.conf 的规则,于是去执行那个规则。

生成文件后,Make 原地复活,重新包含这个文件。
2. (Q) (MAKE) -f <Makefile路径> <目标>

语法规则:

主 Makefile 不处理具体的操作,让另一个 Makefile 带着特定的任务去执行。

U-Boot 实战举例:

shell 复制代码
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=drivers/gpio

解释:

$(Q):如果定义了 V=1,就显示完整命令,否则保持安静(quiet)。

-f scripts/Makefile.build:这是关键,它告诉 Make:"不要用当前的规则了,去读 scripts/Makefile.build 这个文件"。

obj=drivers/gpio:传个参数过去,告诉它:"去把 GPIO 驱动目录给我编了"。
3. $(obj)/%.o: %.c

语法规则:

这是一种通配符规则。% 相当于通配符 *。它定义了"原材料"到"产物"的通用转换公式。

U-Boot 实战举例

shell 复制代码
$(obj)/%.o: %.c
   $(CC) $(c_flags) -c -o $@ $<

通俗翻译:

产物 ( $@ ):冒号左边的 %.o(比如 main.o)。

原料 ( $< ):冒号右边的第一个 %.c(比如 main.c)。

逻辑:只要你想造一个 .o 文件,只要目录下有个同名的 .c,就按下面的 $(CC)(编译器)命令去加工。

寻找线索:如果你发现某个 .h 也是这么生成的,你就能在冒号右边看到它是通过哪个脚本生成出来的。
4.ifeq / else / endif

语法规则:

根据变量的值,决定哪部分代码块生效。

U-Boot 实战举例:

shell 复制代码
ifeq ($(dot-config),1)
   # 只要你配置过(即 .config 存在),就执行这里的逻辑
   -include include/config/auto.conf
else
   # 如果你还没配置,就默认执行这个
   _all: config-error
endif

逻辑:ifeq 即 if equal,也就是相等,同理 ifneq 就是不相等

shell 复制代码
ifneq (参数1, 参数2)
   # 如果 参数1 和 参数2 不一样,执行这里的代码
else
  # 如果 参数1 和 参数2 一样,执行这里的代码(可选)
endif

检查变量是否为空

ifneq 后面跟着一个变量和一个"空值"。如果变量里有内容,条件就成立。

shell 复制代码
ifneq ($(KBUILD_EXTMOD),)
   # 如果 KBUILD_EXTMOD 不是空的,说明我们在编译外部模块
   ...
endif

7.3 默认目标 _all 跟踪

不需要逐句阅读代码,前面 ifeq 类似的命令基本都是设置环境变量和处理分支逻辑。

直接看下面的代码:

在 Makefile 语法中,如果在终端只输入 make 而不带任何参数,它会自动执行文件中出现的第一个目标。在 U-Boot 的 Makefile 里,这个第一个目标就是 _all。

_all 是入口 ,分析 Makefile 的核心逻辑就不再是向下读,而是 追踪依赖。

在 Makefile 中,一个目标的格式是 目标: 依赖。
分析思路:看 _all 后面是什么依赖,然后一样一级一级查下去。


即目标 _all 依赖于 all

即目标 all 依赖于 $(ALL-y)
在 U-Boot 这种大型工程中,ALL-y 并不是一次性定义完的,它采用了追加赋值(+=)

在 Linux 内核和 U-Boot 的 Makefile 体系中,变量命名遵循一个固定模式:变量名-$(配置状态)。

ALL-:表示这是一个汇总所有产物的清单。

y:代表 Yes。

$(ALL-y):最终展开后,它只包含那些在配置阶段(.config)被设置为 y(编译进镜像)的文件。

在 Makefile 的各个角落发现 ALL-y += ... 这样的语句。

Makefile 会根据 .config 中的配置,动态地往这个清单里加东西。

随着 Makefile 往下运行,会看到大量的 ALL-$(CONFIG_XXX) += ...。

如果某个功能被勾选(变为 ALL-y),它就会被追加到这个清单里。

不只有ALL-y,还会存在大量的变量值,举例:

$(CONFIG_OF_SEPARATE) 是一个变量。它的值通常有两种情况:

如果在 .config 中勾选了该功能,它的值就是 y

那么

shell 复制代码
ALL-$(CONFIG_OF_SEPARATE) += u-boot-dtb-tegra.bin

也可以展开成

shell 复制代码
ALL-y += u-boot-dtb-tegra.bin

现在从第一个出现的 ALL-XXX 入手分析

shell 复制代码
ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map binary_size_check

ALL-y 是一个变量名

它是 Makefile 中的一个集合

Makefile 的执行逻辑:

第一阶段:初始化

shell 复制代码
ALL-y := u-boot.cfg  # 假设初始只有这一个

第二阶段:你看到的这一行(追加)

shell 复制代码
ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map binary_size_check

此时 ALL-y 的内容变为:
u-boot.cfg u-boot.srec u-boot.bin u-boot.sym System.map binary_size_check

第三阶段:根据你的 i.MX6ULL 配置继续追加

shell 复制代码
# 如果你在 .config 里开启了设备树
ALL-$(CONFIG_OF_SEPARATE) += u-boot-dtb.bin 

如果 CONFIG_OF_SEPARATE=y,那么变量再次变长,最终汇聚到 all: $(ALL-y)。
这一行里的每一个成员,都代表了一个独立的目标(Target),Make 会为它们分别寻找"生产规则":
①u-boot.srec (S-Record 格式)

这是一种 Motorola 开发的 ASCII 文本格式,包含了地址信息和数据。

用途:主要用于通过串口(如使用 loady 命令)下载代码到开发板。虽然现在用得少了,但依然是 U-Boot 的标准产物。
②u-boot.bin (核心产物)

这是你最关心的二进制文件。它是经过裁剪、链接后,可以直接烧录到 i.MX6ULL 的 SD 卡或 NAND Flash 中的镜像。

后续追踪:在 Makefile 中搜索 u-boot.bin:,你会发现它通常是由 u-boot-nodtb.bin 和设备树 dt.dtb 合并而来的。
③ u-boot.sym (符号表)

它记录了 U-Boot 中所有函数和全局变量的内存地址。

用途:它是给开发者看的。当你遇到崩溃(Panic)并打印出一串 16 进制地址时,你需要查阅这个文件来确定到底是哪个函数出错了。
④ System.map (地址映射表)

与 .sym 类似,它是对整个编译出的二进制文件的"地图"描述。

区别:.sym 通常由 nm 工具生成,而 System.map 是更直观的地址与符号对照表。
⑤ binary_size_check (动作目标)

注意,这不是一个文件,而是一个校验动作。

逻辑:它会触发一个脚本,检查生成的 u-boot.bin 是否超过了你定义的 Flash 分区大小(比如 i.MX6ULL 的 SPL 阶段对大小限制非常严格)。如果超了,编译会直接报错。

这里我们选取其中一个产物 u-boot.bin 进行分析说明:

shell 复制代码
u-boot.bin: u-boot-dtb.bin FORCE
	$(call if_changed,copy)

代码遵循标准的 Makefile 规则格式:

**目标(Target): 依赖项(Prerequisites)

Tab\] 命令(Recipe)** 1. 目标 (u-boot.bin):最终生成的、准备烧录到存储介质(如 SD 卡)的镜像文件。 2. 依赖项 (u-boot-dtb.bin): 这个文件通常已经在之前的步骤中生成了。 从名字可以看出,它是 u-boot (代码) + dtb (设备树) 的结合体。 依赖项 (FORCE):一个伪目标,确保每次执行 make 时都会检查此规则的命令是否需要运行。 3. 命令 ($(call if_changed,copy)): if_changed:U-Boot 的智能包装函数,只有当 u-boot-dtb.bin 更新了,或者编译命令改变了,才会执行。 copy:对应 Makefile 中的 cp(拷贝)指令。

接下来分析:u-boot-dtb.bin

shell 复制代码
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
	$(call if_changed,cat)

在现代嵌入式系统中,为了让一份代码(U-Boot)能跑在不同的板子上,采用了代码与硬件描述分离的设计:

代码:负责逻辑控制(怎么读磁盘、怎么初始化网络)。

设备树 (DTB):负责描述硬件(串口寄存器地址是多少、网卡接到哪个引脚)。

这一行 Makefile 规则的作用,就是把编译好的"逻辑代码"和"硬件地图"缝合在一起。

依赖项:原材料拆解

  1. u-boot-nodtb.bin (代码主体):
    nodtb 的意思是 "no Device Tree Blob"。
    它是从 u-boot ELF 文件中提取出来的纯机器码,它包含了所有的函数逻辑,但它不知道具体的硬件寄存器基地址。
  2. dts/dt.dtb (硬件地图):
    ( "/" 代表 dt.dtb 这个文件位于一个名为 dts 的文件夹内)
    这是由 .dts 源文件经过 DTC (Device Tree Compiler) 编译生成的二进制文件。
    dts: Device Tree Source(设备树源文件目录) 。这是存放 .dts(文本格式,人能读懂)文件的地方。
    dt: Device Tree(设备树)
    dtb: Device Tree Blob(设备树二进制大对象) 。它是 .dts 经过编译器(DTC)处理后生成的二进制文件,机器才能读懂。
    它记录了 i.MX6ULL 的外设信息。
  3. FORCE:
    这是一个伪目标,确保 Makefile 每次运行到这里时都会重新检查依赖关系和命令。
    命令:$(call if_changed,cat)

这一行看起来是调用了一个复杂的函数,其实核心动作非常"暴力":

if_changed:这是 U-Boot 构建系统的一个智能宏。

它会对比当前生成的命令和上次编译时的命令,或者检查依赖文件是否有更新,只有在必要时才会执行。

cat:这就是 Linux 系统中的 cat 命令。

实际发生的动作:

shell 复制代码
cat u-boot-nodtb.bin dts/dt.dtb > u-boot-dtb.bin

它直接将设备树二进制文件接在了代码镜像的末尾。


接下来是分析 u-boot-nodtb.bin 和 dts/dt.dtb 这两个依赖
先分析 dts/dt.dtb

shell 复制代码
dts/dt.dtb: checkdtc u-boot
	$(Q)$(MAKE) $(build)=dts dtbs

目标与依赖
目标 (dts/dt.dtb):

主 Makefile 最终需要的硬件地图文件。
依赖项 (checkdtc):

这是一个检查动作。在编译设备树之前,系统必须先检查你的电脑上是否安装了 dtc(设备树编译器)以及版本是否符合要求。
依赖项 (u-boot):

依赖于 ELF 格式的 u-boot。这确保了在处理设备树之前,主要的链接工作已经完成(或者至少同步进行)。
命令部分

shell 复制代码
$(Q)$(MAKE) $(build)=dts dtbs

$(Q):控制是否回显命令。

如果你 make V=1,你能看到完整的命令;默认情况下它是安静模式。

$(MAKE):启动一个新的 make 进程。

$(build)=dts:这是 Kbuild 系统的精髓。

它告诉 Make:"去子目录 dts/ 寻找 Makefile,并以那里的规则为准"。

dtbs:这是传给子 Makefile 的目标参数,意思是"生成所有的设备树二进制文件"。

再分析 u-boot-nodtb.bin

shell 复制代码
u-boot-nodtb.bin: u-boot FORCE
	$(call if_changed,objcopy)
	$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
	$(BOARD_SIZE_CHECK)
shell 复制代码
$(call if_changed,objcopy)

这是最核心的一步。

背景:u-boot (ELF) 文件虽然包含机器码,但它还带着大量的调试信息、符号表、段描述符等。如果你直接把它烧进 SD 卡,CPU 是读不懂这些元数据的。

动作:objcopy 工具只把 .text(代码段)、.data(数据段)等真正运行需要的二进制机器码提取出来。

结果:生成一个纯净的机器码文件。

shell 复制代码
$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE)) 

参数解释:

$<:代表依赖项 u-boot。

$@:代表目标项 u-boot-nodtb.bin。

$(CONFIG_SYS_TEXT_BASE):这就是你在 autoconf.h 或 Kconfig 里定义的 U-Boot 在内存(DDR)中的起始地址(通常是 0x87800000)。
为什么需要它?:

有些指令使用了绝对地址。如果你编译时指定代码在 0x87800000 运行,但实际由于某些原因需要挪动位置,这个宏(如果开启了 CONFIG_STATIC_RELA)会处理静态重定位表,确保代码在指定的内存基地址上能正确跑起来。
DO_STATIC_RELA 的具体工作:

它运行一个脚本(通常是 tools/relocate-rela),读取 u-boot (ELF) 里的重定位表,然后直接修改 u-boot-nodtb.bin 里的机器码,把里面所有相对于基地址的偏移量,根据你设定的 $(CONFIG_SYS_TEXT_BASE) 算好并填进去。

shell 复制代码
$(BOARD_SIZE_CHECK) 

作用:检查生成的 u-boot-nodtb.bin 大小是否超过了硬件限制。

场景:i.MX6ULL 的内部寄存器和缓存大小是有限的。如果你的 U-Boot 编得太臃肿,超过了设定的最大尺寸,这一行会直接报错中止编译,防止你烧录一个注定无法启动的镜像。
u-boot (ELF):包含所有函数名和行号,文件可能很大(几十MB)。

u-boot.bin:是精简版,只有机器指令,文件很小(几百KB)。

联系:正式 objcopy 进行了删减

7.4 u-boot 跟踪

shell 复制代码
u-boot:	$(u-boot-init) $(u-boot-main) u-boot.lds FORCE
	$(call if_changed,u-boot__)
ifeq ($(CONFIG_KALLSYMS),y)
	$(call cmd,smap)
	$(call cmd,u-boot__) common/system_map.o
endif

这一行规则的目的是让所有源代码(.c 和 .S)从零散的状态,最终组合成成一个可执行整体(ELF 格式)。

依赖项:

shell 复制代码
u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE

$(u-boot-init)

通常包含的是板子启动时最先运行的那一小段代码(比如 arch/arm/cpu/armv7/start.S 编译出的目标)。

它必须放在内存的最前面,否则 CPU 上电后找不到第一条指令。
$(u-boot-main)

这是 U-Boot 的核心,包含了配置的所有驱动(USB、网络、串口)、文件系统和命令处理逻辑。

它实际上是把各个子目录下的 built-in.o 文件打包在了一起。
u-boot.lds

这是链接脚本。

它不提供代码,但它告诉链接器(LD):把 u-boot-init 放哪,把 u-boot-main 放哪,内存起始地址设为多少。
命令部分:

shell 复制代码
$(call if_changed,u-boot__)

本质:这行宏调用最终会转换成一条长长的链接命令。

实际动作:arm-linux-ld [参数] -T u-boot.lds (u-boot-init) --start-group (u-boot-main) --end-group -o u-boot。

意义:它把所有的二进制碎片按照链接脚本的指示,严丝合缝地缝合在一起,生成带调试信息的 ELF 文件。

shell 复制代码
ifeq ($(CONFIG_KALLSYMS),y)
	$(call cmd,smap)
	$(call cmd,u-boot__) common/system_map.o
endif

如果配置里开启了 CONFIG_KALLSYMS:

cmd,smap:系统会先扫描一遍刚才生成的 u-boot,提取出所有函数的地址和名字,存入 common/system_map.o。

二次链接:再次执行 u-boot__,把这个带有"地址簿"的 .o 文件也链接进去。

好处:U-Boot 在 i.MX6ULL 上崩溃时,它能直接打印出出错的函数名字(如 do_bootm),而不是给你一串冰冷的 16 进制地址。这对于研究生阶段做科研调试非常有帮助。

我们已经看完了文件是如何组装的,下一步应该去看零件是从哪来的,所以接下来要分析 u-boot-main

shell 复制代码
u-boot-main := $(libs-y)

(注意:n 和 : 之间有一个空格,不加空格无法搜索出来)

在这一行之前,Makefile 已经执行了成百上千行 libs-y += ...。

里面装满了 drivers/、common/、fs/、net/ 等目录名。
u-boot-main:

这一行把购物车里的内容一次性倒进"总装清单"里。
:= (立即赋值):

它告诉 Make,"现在立刻就把 libs-y 里的目录列表确定下来,不要等后面变了再改"。
为什么不直接在链接命令里用 $(libs-y),非要倒手给 u-boot-main?

这体现了 U-Boot 构建系统的分层设计思维:
1. 解耦:

libs-y 负责收集(不同架构、不同配置下的文件夹)。
2. 管理:

u-boot-main 负责定义结构。在复杂的版本中,u-boot-main 可能还要包含 (libs-m) 或其他特殊模块,通过 u-boot-main 统一管理,可以让最后的链接命令 (call if_changed,u-boot__) 保持简洁。

shell 复制代码
libs-y += drivers/spi/
libs-$(CONFIG_FMAN_ENET) += drivers/net/fm/
libs-$(CONFIG_SYS_FSL_DDR) += drivers/ddr/fsl/
libs-$(CONFIG_SYS_FSL_MMDC) += drivers/ddr/fsl/
libs-$(CONFIG_ALTERA_SDRAM) += drivers/ddr/altera/
libs-y += drivers/serial/
libs-y += drivers/usb/cdns3/
libs-y += drivers/usb/dwc3/
libs-y += drivers/usb/common/
libs-y += drivers/usb/emul/
libs-y += drivers/usb/eth/
libs-y += drivers/usb/gadget/
libs-y += drivers/usb/gadget/udc/
libs-y += drivers/usb/host/
libs-y += drivers/usb/musb/
libs-y += drivers/usb/musb-new/
libs-y += drivers/usb/phy/
libs-y += drivers/usb/ulpi/
libs-y += cmd/
libs-y += common/
libs-$(CONFIG_API) += api/
libs-$(CONFIG_HAS_POST) += post/
libs-y += test/
libs-y += test/dm/
libs-$(CONFIG_UT_ENV) += test/env/
libs-$(CONFIG_UT_OVERLAY) += test/overlay/

libs-y += $(if $(BOARDDIR),board/$(BOARDDIR)/)

libs-XXXALL-XXX 一个原理

它们都遵循 Kbuild 系统的核心公式:变量-(宏定义) += 目标。 **libs-(CONFIG_XXX) += ...**

角色:零件收集器。

原理:如果 CONFIG_XXX 是 y,就把对应的文件夹(车间)加入到 libs-y 托盘里。

最终归宿:这些文件夹里的代码会被编成 .o 文件,最后放入 u-boot 里。
ALL-$(CONFIG_XXX) += ...

角色:产物决定者。

原理:如果 CONFIG_XXX 是 y,就把对应的最终文件(如 u-boot.img、u-boot.kwb)加入到 ALL-y 清单里。

最终归宿:决定了运行 make 结束后,目录下会多出哪些可以直接拿去烧录的文件。

相关推荐
armwind1 小时前
这6年的小感悟-重新记录自己
笔记
Tingjct1 小时前
Linux开发工具
linux·运维·服务器
DragonnAi1 小时前
论文解读:SFINet 空间-频率统一学习框架用于多模态图像融合
深度学习·学习·计算机视觉
晓梦林1 小时前
Commit靶场学习笔记
笔记·学习·安全·web安全
星夜夏空991 小时前
STM32单片机学习(4)——嵌入式概述
stm32·单片机·学习
sheeta19981 小时前
LeetCode 每日一题笔记 日期:2026.05.10 题目:2770. 达到末尾下标所需的最大跳跃次数
笔记·算法·leetcode
LeeeX!2 小时前
OpenClaw CLI 完整实操笔记
笔记·openclaw
van久2 小时前
Day28 第四周总结 & 项目整体收官笔记
笔记
nashane2 小时前
HarmonyOS 6学习:HWAsan监测开启后应用崩溃的终极解决方案
学习·华为·harmonyos·harmonyos 5