
1. 项目概述这不是一份“抄命令就能跑”的笔记而是一次对 U-Boot 构建系统内核逻辑的解剖你打开终端敲下make distclean、make xxx_defconfig、make -j$(nproc)U-Boot 就编译出来了。但你有没有想过那一行行make命令背后到底发生了什么为什么改一个CONFIG_XXX宏整个链接脚本就变了为什么include/autoconf.mk这个文件像幽灵一样自动出现又在每次make clean后消失为什么board/xxx/xxx/目录下的Kconfig文件能决定menuconfig里是否显示某个选项这些不是魔法是 U-Boot 构建系统精密运转的齿轮咬合声。这篇笔记就是带你把 U-Boot 的配置和编译过程从“黑盒”变成“透明玻璃盒”。它不教你如何烧写镜像也不讲如何移植到新板子——那是后续章节的事。它聚焦在最基础、也最容易被忽视的一环从你输入第一个make命令开始到最终生成u-boot.bin这个二进制文件为止中间每一步的触发条件、数据流向、文件生成逻辑和关键决策点。核心关键词是U-Boot 配置系统、Kconfig 语法、Makefile 递归机制、autoconf.mk 生成原理、链接脚本动态生成、交叉编译工具链集成。无论你是刚接触嵌入式开发的新手还是已经能熟练烧写固件、却对底层构建流程一知半解的工程师只要你希望真正掌控自己的编译环境而不是依赖别人打包好的 SDK这篇内容就是为你写的。它不提供速成捷径但能帮你建立一套稳固的、可追溯、可调试、可定制的构建认知框架。2. 整体设计与思路拆解为什么 U-Boot 要用 Kconfig Makefile 这套“双引擎”架构U-Boot 的配置和编译绝非简单的“源码编译器可执行文件”线性流程。它是一个典型的“元构建系统”Meta-Build System其设计哲学根植于 Linux 内核并针对嵌入式场景做了深度裁剪。理解它的整体思路是避免后续陷入“改了配置但没生效”、“编译报错找不到头文件”这类问题的前提。2.1 核心矛盾驱动的设计选择嵌入式世界最大的现实是“碎片化”。一块 ARM Cortex-A72 的服务器级 SoC 和一块 RISC-V 架构的 MCU它们的启动流程、内存布局、外设初始化需求天差地别。如果为每种芯片都写一套独立的、硬编码的 Makefile维护成本将是灾难性的。U-Boot 的解决方案是将“硬件差异”抽象为“配置项”将“编译规则”抽象为“模板”再用一个通用的“引擎”来驱动这两者结合。这个引擎就是 Kconfig Makefile 的组合。Kconfig 是“需求描述语言”它不关心怎么编译只负责定义“有哪些功能可以开/关”、“哪些参数可以设置”、“开启 A 功能是否强制要求 B 功能也开启”。比如CONFIG_SYS_TEXT_BASE这个宏它在arch/arm/Kconfig里被定义为一个hex类型的配置项其默认值、依赖关系、用户提示文本全部由 Kconfig 语法描述。它本身不产生任何代码只是一个“蓝图”。Makefile 是“执行调度器”它不关心硬件细节只负责读取 Kconfig 生成的蓝图即.config文件然后根据蓝图里的每一个CONFIG_XXXy/m/n去决定哪些 C 源文件需要被gcc编译obj-y xxx.o哪些头文件需要被包含通过-I参数最终链接时哪些目标文件要被塞进u-boot这个 ELF 文件里甚至它还会调用scripts/kconfig/conf这个工具来解析Kconfig文件并生成.config。提示你可以把 Kconfig 想象成一份“装修需求清单”上面写着“客厅要装吊灯y、厨房要装洗碗机m、卫生间不要智能马桶n”。而 Makefile 就是那个拿着清单去建材市场采购、联系水电工、安排施工队的项目经理。没有清单项目经理不知道该买什么没有项目经理清单只是一张废纸。2.2 为什么不用 CMake 或 Meson历史与现实的权衡现在主流的跨平台构建系统是 CMake它语法清晰、IDE 支持好、跨平台能力强。那为什么 U-Boot 还死守着这套看起来“古老”的 KconfigMakefile答案是三个字向后兼容。U-Boot 的历史比绝大多数现代构建系统都要长。它的构建系统已经沉淀了超过二十年全球数以万计的板级支持包BSP都是基于这套系统开发的。任何一个重大的构建系统重构都意味着所有已有的 BSP 都要重写CMakeLists.txt所有开发者都要重新学习一套全新的规则。这在开源社区里无异于一场“革命”代价远大于收益。更重要的是Kconfig 对于“配置项爆炸”的嵌入式系统有其独特优势。menuconfig提供的树状菜单、搜索、依赖高亮、反向依赖查询等功能在处理上千个配置项时其交互效率和可维护性至今仍是 CMake 的ccmake难以比拟的。U-Boot 的开发者们不是不懂新技术而是清醒地认识到对于一个以稳定性和生态广度为生命线的 Bootloader 来说渐进式演进远比颠覆式创新更明智。所以我们看到的不是技术落后而是一种深思熟虑的工程选择。2.3 整个流程的宏观视图从命令行到二进制的七步跃迁为了建立全局观我们先勾勒出整个流程的七个关键阶段后续章节将逐一深入环境准备设置CROSS_COMPILE、ARCH等环境变量这是所有后续步骤的基石。配置生成运行make xxx_defconfig触发conf工具读取Kconfig生成初始.config。交互式配置运行make menuconfig在图形界面中修改.config其本质仍是调用conf工具。配置解析与头文件生成make进入顶层Makefile读取.config生成include/generated/autoconf.h和include/config/auto.conf。依赖扫描与编译规则生成make扫描所有Makefile根据auto.conf中的配置动态生成每个目录下的built-in.o规则。源码编译gcc根据上一步生成的规则编译所有.c和.S文件生成.o目标文件。链接与格式转换ld将所有.o文件按u-boot.lds链接脚本链接成u-boot再用objcopy转换为u-boot.bin。这七个阶段环环相扣任何一个环节出错都会导致后续失败。而其中第 4 步头文件生成和第 5 步规则生成是最容易被忽略也是问题高发区。接下来我们就从最源头的“配置生成”开始一层层剥开它的外壳。3. 核心细节解析与实操要点Kconfig 语法、配置项类型与defconfig的秘密U-Boot 的配置能力全部来源于其庞大的Kconfig文件树。理解Kconfig是理解整个配置系统的起点。它不是一门编程语言而是一种声明式配置描述语言其语法简洁但语义精妙。3.1 Kconfig 文件的组织结构一棵自顶向下的“配置树”U-Boot 的Kconfig文件并非散落在各处而是遵循严格的层级结构构成了一棵逻辑清晰的树顶层Kconfig位于 U-Boot 源码根目录。它是整棵树的根节点其唯一职责是source引入其他子目录的Kconfig文件。例如它会包含source arch/Kconfig和source board/Kconfig。架构层Kconfig位于arch/xxx/Kconfig如arch/arm/Kconfig。它定义了该 CPU 架构特有的配置项比如CONFIG_ARM64、CONFIG_SYS_TEXT_BASE代码起始地址、CONFIG_SYS_INIT_SP_ADDR栈指针初始地址。这是所有板级配置的基础。板级Kconfig位于board/vendor/boardname/Kconfig。它定义了该具体开发板的特性比如CONFIG_TARGET_MYBOARD板子的主配置开关、CONFIG_SYS_MALLOC_LENmalloc 区域大小、CONFIG_DM_GPIO是否启用设备模型 GPIO 子系统。一个板子的Kconfig通常会select强制选择其依赖的架构和驱动配置。驱动/功能层Kconfig位于drivers/xxx/Kconfig、fs/Kconfig、net/Kconfig等。它们定义了具体功能模块的开关比如CONFIG_CMD_NET网络命令、CONFIG_FAT_WRITEFAT 文件系统写支持。这些配置项通常会被板级或架构层的Kconfig所depends on依赖。这种分层结构带来了巨大的灵活性。当你为一块新板子添加支持时你不需要动arch/arm/Kconfig只需要在board/mycompany/myboard/Kconfig里source进来arch/arm/Kconfig然后select CONFIG_ARM再select你需要的所有驱动配置即可。所有的“定制化”工作都被约束在了板级目录下保证了上游代码的纯净。3.2 配置项的四种核心类型bool、tristate、string、int/hexKconfig 中的每一个配置项都必须声明其类型。类型决定了它在menuconfig中的呈现方式和.config文件中的存储格式。bool布尔型最常用。它只有两个状态yyes编译进内核或nno不编译。在.config文件中它表现为CONFIG_XXXy或# CONFIG_XXX is not set。例如CONFIG_CMD_BOOTZbool表示是否启用bootz命令。tristate三态型继承自 Linux 内核但在 U-Boot 中几乎不使用。它有y编译进、m编译为模块、n不编译三种状态。U-Boot 作为一个静态链接的 Bootloader不存在“运行时加载模块”的概念所以m状态是无效的。如果你在 U-Boot 的Kconfig中看到tristate那基本可以认为是历史遗留或误用。string字符串型用于需要输入一段文本的配置。在.config文件中它表现为CONFIG_XXXyour_string。例如CONFIG_SYS_BOARDmyboard用于在运行时打印板子信息。int/hex整数/十六进制型用于需要输入数字的配置。int以十进制存储hex以十六进制存储。在.config文件中int表现为CONFIG_XXX12345hex表现为CONFIG_XXX0x12345。例如CONFIG_SYS_TEXT_BASE0x80000000这是一个典型的hex类型。注意int和hex类型的配置项必须配合range关键字来限定其有效取值范围否则用户可能输入一个非法的地址导致链接失败。例如range 0x00000000 0xffffffff。3.3defconfig文件的本质一个被“冻结”的.config快照当你执行make myboard_defconfig时你可能会以为它是在运行某个神秘的脚本。其实它非常简单myboard_defconfig文件本身就是一个精简版的.config文件。这个文件通常位于configs/myboard_defconfig。它里面的内容就是一系列CONFIG_XXXy或CONFIG_XXXn的行。例如CONFIG_ARMy CONFIG_TARGET_MYBOARDy CONFIG_SYS_TEXT_BASE0x80000000 CONFIG_CMD_NETy CONFIG_NET_RANDOM_ETHADDRymake myboard_defconfig命令的执行逻辑是make在顶层Makefile中找到myboard_defconfig这个目标。它会执行一条 shell 命令$(Q)$(MAKE) $(build)scripts/kconfig conf并传入Kconfig文件路径和configs/myboard_defconfig作为输入。scripts/kconfig/conf工具读取Kconfig文件构建出完整的配置项依赖图。然后它再读取configs/myboard_defconfig这个“快照”将快照中的值应用到依赖图的对应节点上。最后它根据依赖规则自动推导出所有未在快照中显式指定的配置项的值通常是n并生成一个完整的.config文件。因此defconfig文件的核心价值在于它是一个可版本控制、可复现、可审查的“最小配置集”。它不包含任何逻辑只包含事实。你可以把它想象成一个 Dockerfile 中的FROM指令——它为你提供了一个确定的、可重复的起点。3.4menuconfig的工作原理不只是一个图形界面make menuconfig是最常用的交互式配置方式。它的背后是scripts/kconfig/mconf这个基于 ncurses 的程序。但它的强大之处远不止于漂亮的菜单。依赖关系可视化当你在一个配置项上按?键它会显示该项目的详细帮助信息包括其depends on的所有条件。例如CONFIG_CMD_NET的帮助里会写明depends on NET CMD_NET这意味着你必须先开启CONFIG_NETCONFIG_CMD_NET才会出现在菜单里。反向依赖查询按/键可以进行全局搜索。搜索SYS_TEXT_BASE它会列出所有depends on SYS_TEXT_BASE的配置项。这在调试“为什么我开启了 A 功能B 功能却没出现”时是救命稻草。配置项状态标识菜单中[*]表示y[ ]表示nM表示m在 U-Boot 中应避免。而---表示这是一个菜单标题不可选----表示这是一个注释行。实操心得我曾经遇到过一个诡异的问题在menuconfig里明明把CONFIG_DM设备模型设为了y但编译时却报错说struct udevice未定义。排查了半小时最后发现是CONFIG_DM依赖于CONFIG_OF_CONTROL设备树控制而后者被我无意中设为了n。menuconfig的依赖高亮功能当前项依赖的项会变灰救了我。所以永远不要只看你想改的那一项要看它周围变灰的那些项。4. 实操过程与核心环节实现从make命令到autoconf.h的完整旅程现在我们进入最核心、也最“魔法”的环节当你敲下makeU-Boot 的构建系统是如何一步步将.config文件变成 C 编译器能识别的#define宏的这个过程是理解整个构建流程的“奇点”。4.1 第一步make的入口与顶层Makefile的调度U-Boot 的顶层Makefile是一个超过 2000 行的庞然大物但它有一个非常清晰的主线。当你在根目录执行make时make默认会寻找第一个目标target也就是all。在顶层Makefile的开头你会看到这样一行PHONY all all: $(ALL-y)$(ALL-y)是一个变量它的值是通过一系列操作动态拼接出来的。它的核心来源是ALL-y u-boot.srec u-boot.bin u-boot.imx u-boot.kwb ...也就是说make的默认行为就是尝试构建u-boot.bin这个目标。而u-boot.bin的构建规则定义在Makefile的末尾u-boot.bin: u-boot FORCE $(call if_changed,objcopy)这条规则的意思是u-boot.bin依赖于u-boot这个目标。只有当u-boot发生了变化或者FORCE这个伪目标被触发时才会执行objcopy命令。那么u-boot这个目标又是怎么来的呢它是由链接规则生成的u-boot: $(u-boot-init) $(u-boot-main) FORCE $(call if_changed,ld.u-boot)这里出现了两个关键变量$(u-boot-init)和$(u-boot-main)。它们分别代表了 U-Boot 启动代码arch/arm/cpu/armv7/start.o等和主程序代码lib/built-in.o,drivers/built-in.o等的集合。而这些.o文件的集合正是由Makefile的“递归遍历”机制根据auto.conf动态生成的。4.2 第二步include/generated/autoconf.h的诞生——配置宏的“翻译官”这是整个流程中最关键的一步。.config文件是给make看的而 C 编译器gcc只认识#define。autoconf.h就是这座桥梁。这个文件的生成由顶层Makefile中的一个隐式规则触发include/generated/autoconf.h: $(wildcard $(srctree)/include/config/auto.conf) $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf这段代码的意思是如果include/config/auto.conf这个文件存在它是由conf工具生成的内容与.config几乎一致那么就执行scripts/Makefile.autoconf这个子Makefile。scripts/Makefile.autoconf的核心任务就是读取include/config/auto.conf然后生成include/generated/autoconf.h。它的实现非常巧妙利用了make的内置函数# scripts/Makefile.autoconf $(obj)/autoconf.h: $(obj)/auto.conf $(Q)$(CPP) $(CFLAGS) -DDO_DEPS_ONLY -dM $(srctree)/include/common.h | \ sed -n s/^#define CONFIG_\([^[:space:]]*\).*/CONFIG_\1/p | \ sort -u $这段命令的执行过程是用cppC 预处理器对include/common.h进行一次“假编译”只输出所有#define宏-dM参数。用sed过滤出所有以CONFIG_开头的宏。用sort -u去重并排序输出到autoconf.h。但等等common.h里并没有CONFIG_XXX宏啊没错这就是精髓所在。cpp在预处理时会读取所有#include的头文件而common.h的开头就有一行#include config.h而config.h这个文件又是一个由Makefile自动生成的符号链接它指向include/generated/autoconf.h。这就形成了一个“鸡生蛋蛋生鸡”的循环不make的聪明之处在于它会先检查autoconf.h是否存在。如果不存在它会先去构建autoconf.h然后再去构建common.h。这个过程就是make的“隐式规则”和“依赖图”在起作用。最终生成的autoconf.h内容大致如下#ifndef __AUTOCONF_H__ #define __AUTOCONF_H__ #define CONFIG_ARM 1 #define CONFIG_TARGET_MYBOARD 1 #define CONFIG_SYS_TEXT_BASE 0x80000000 #define CONFIG_CMD_NET 1 ... #endif /* __AUTOCONF_H__ */从此所有#include common.h的 C 文件都能通过CONFIG_XXX宏来控制编译分支了。4.3 第三步built-in.o的动态生成——编译规则的“活地图”有了autoconf.h下一步就是告诉make“如果CONFIG_CMD_NETy请把net/net.o加入编译列表如果n请跳过它。”这个任务由每个子目录下的Makefile完成。以net/Makefile为例obj-$(CONFIG_CMD_NET) net.o obj-$(CONFIG_CMD_PING) ping.o obj-$(CONFIG_CMD_DHCP) dhcp.o这里的obj-$(CONFIG_CMD_NET)是一个make变量。当CONFIG_CMD_NETy时obj-y net.o当CONFIG_CMD_NETn时obj-变量为空net.o就不会被加入。那么obj-y这个变量最终是如何变成一个具体的.o文件列表并被传递给ld链接器的呢答案是每个目录下的Makefile都会被make递归地include一次。顶层Makefile会执行subdir-ym : $(sort $(subdir-ym)) $(foreach dir,$(subdir-ym),$(eval $(dir)/built-in.o: $(obj)$(dir)/built-in.o))它会遍历所有子目录drivers/,net/,fs/...并为每个目录生成一个built-in.o目标。而built-in.o的构建规则定义在scripts/Makefile.build中$(obj)/built-in.o: $(obj)/built-in.a FORCE $(call if_changed,ld.builtin)ld.builtin规则就是调用ld -rrelocatable linking命令将该目录下所有obj-y列表中的.o文件合并成一个built-in.o。最终u-boot的链接命令会是这样的arm-linux-gnueabihf-ld -T u-boot.lds -o u-boot \ arch/arm/cpu/armv7/start.o \ arch/arm/lib/built-in.o \ board/mycompany/myboard/built-in.o \ drivers/built-in.o \ net/built-in.o \ ...这个长长的列表就是由成百上千个obj-y xxx.o语句经过make的递归解析和变量展开后动态生成的。4.4 第四步链接脚本u-boot.lds的动态注入——内存布局的“指挥棒”U-Boot 的链接脚本u-boot.lds是决定最终二进制文件内存布局的灵魂。它不是一个静态文件而是一个由Makefile动态生成的模板。在arch/arm/cpu/armv7/Makefile中你会看到LDFLAGS_u-boot -T$(obj)/u-boot.lds而u-boot.lds的生成由arch/arm/cpu/armv7/u-boot.lds这个模板文件驱动。这个模板里充满了#ifdef CONFIG_XXX这样的预处理指令。例如它可能包含SECTIONS { . $(CONFIG_SYS_TEXT_BASE); .text : { *(.text) ... } . ALIGN(4); .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } . ALIGN(4); .data : { *(.data) ... } . ALIGN(4); .u_boot_list : { KEEP(*(SORT(.u_boot_list*))); } }当make构建u-boot.lds时它会先用cpp预处理这个模板文件将$(CONFIG_SYS_TEXT_BASE)替换为0x80000000并将所有#ifdef CONFIG_XXX分支根据.config中的值进行裁剪。最终生成的u-boot.lds是一个完全“实例化”的、只包含当前配置所需段的链接脚本。实操心得有一次我需要为一块新板子调整CONFIG_SYS_TEXT_BASE但我只改了Kconfig里的默认值忘了在defconfig里显式设置。结果编译出来的u-boot.lds里. $(CONFIG_SYS_TEXT_BASE);这一行被预处理器展开成了. ;导致链接器报错syntax error。这个错误非常隐蔽因为u-boot.lds是自动生成的你很难一眼看出问题。解决方法是make V1查看完整的ld命令然后手动执行cpp预处理u-boot.lds模板检查输出。5. 常见问题与排查技巧实录那些让你抓狂的“玄学”错误与真实解决方案在 U-Boot 的配置和编译过程中90% 的问题都源于对上述流程的误解或疏忽。下面是我踩过的、以及在社区里高频出现的几个“经典坑”附带了真实的排查思路和解决方案。5.1 问题一“undefined reference to xxx” —— 符号未定义但源码明明存在现象编译时ld报错说某个函数或变量未定义比如undefined reference to board_init_f。你打开arch/arm/lib/board.c发现这个函数定义得清清楚楚。排查思路首先确认这个函数所在的 C 文件是否被成功编译进了built-in.o。执行make V1找到board/目录的编译命令看board.c是否在gcc的输入列表里。如果不在说明obj-y board.o这条规则没有被触发。检查board/Makefile看它是否依赖于某个CONFIG_XXX宏。然后检查.config文件看这个CONFIG_XXX是否为y。如果不是问题就在这里。如果是y那就要检查CONFIG_XXX的依赖链。运行make menuconfig搜索CONFIG_XXX按?看它的depends on。很可能它的某个依赖项是n导致CONFIG_XXX被自动设为了n而menuconfig的界面里这个项是灰色的你根本看不到它。解决方案在menuconfig中按/搜索CONFIG_XXX查看其所有依赖项并逐一确认它们的状态。或者直接编辑.config手动添加CONFIG_XXXy然后make olddefconfig让make自动修复所有依赖。5.2 问题二“fatal error: xxx.h: No such file or directory” —— 头文件找不到现象gcc报错说找不到某个头文件比如asm/arch/gpio.h。排查思路这个错误几乎总是由-Iinclude path参数缺失引起的。make V1输出的gcc命令里会列出所有-I参数。检查这些路径中是否包含了arch/arm/include/asm/和arch/arm/include/asm/arch-myboard/。asm/arch/gpio.h的实际路径通常是arch/arm/include/asm/arch-myboard/gpio.h。如果路径缺失说明Makefile没有为这个目录添加extra-y或ccflags-y。检查arch/arm/cpu/armv7/Makefile或board/mycompany/myboard/Makefile看是否有类似ccflags-y -I$(srctree)/arch/arm/include/asm/arch-myboard的行。解决方案在对应的Makefile中添加正确的ccflags-y。但更规范的做法是在arch/arm/Kconfig中为你的板子select CONFIG_ARCH_MYBOARD然后在arch/arm/mach-myboard/Kconfig中定义CONFIG_ARCH_MYBOARD并让它select CONFIG_ARCH_ARM。这样Makefile就会自动添加相应的-I路径。5.3 问题三“u-boot.bin大小异常或启动后卡死” —— 链接脚本与内存布局错配现象u-boot.bin编译成功但烧写到板子上后串口没有任何输出或者输出乱码后卡死。排查思路这是典型的内存布局问题。首先用arm-linux-gnueabihf-objdump -h u-boot查看u-boot的各个段section的虚拟地址VMA和大小。重点检查.text段的 VMA是否等于你在menuconfig中设置的CONFIG_SYS_TEXT_BASE。然后用arm-linux-gnueabihf-objdump -x u-boot | grep load address查看LOAD地址确保它和 VMA 一致。最后检查你的板子 SDRAM 初始化代码是否真的将内存初始化到了CONFIG_SYS_TEXT_BASE所在的地址空间。如果 SDRAM 只初始化了 128MB而CONFIG_SYS_TEXT_BASE设为了0x900000002.5GB那肯定启动不了。解决方案回到menuconfig仔细核对CONFIG_SYS_TEXT_BASE、CONFIG_SYS_SDRAM_BASE、CONFIG_SYS_MONITOR_LEN这三个关键内存地址。它们必须构成一个逻辑自洽的体系TEXT_BASE必须在SDRAM_BASE之上且两者之间的空间要足够容纳 U-Boot 的代码和数据。5.4 问题四“make menuconfig里找不到我的新配置项” —— Kconfig 未被正确source现象你在board/mycompany/myboard/Kconfig里添加了一个新的config MY_NEW_FEATURE但运行make menuconfig后在菜单里完全找不到它。排查思路menuconfig的菜单结构完全由Kconfig文件的source关系决定。你的Kconfig文件必须被上层的Kconfigsource进来。检查board/Kconfig看它是否source mycompany/myboard/Kconfig。如果没有添加它。如果已经有了检查路径是否拼写错误Linux 是大小写敏感的。还有一个常见错误Kconfig文件的首字母必须大写。如果你创建的是kconfigmake是不会识别的。解决方案在board/Kconfig中添加source mycompany/myboard/Kconfig然后执行make distclean清理所有缓存再make menuconfig。记住Kconfig的变更必须make distclean才能完全生效。5.5 终极排查技巧make V1与make -n这两个make参数是所有构建问题的“X 光机”。make V1让make输出完整的、未经简化的命令行。你将看到每一个gcc、ld、cpp命令的每一个参数。这是定位“为什么找不到头文件”、“为什么链接器用了错误的脚本”的唯一途径。make -n让make只打印它将要执行的命令但不真正执行。这对于理解make的依赖图、验证你的Makefile修改是否生效极其有用。例如make -n u-boot.bin它会打印出从u-boot到u-boot.bin的所有中间步骤。我个人在实际操作中的体会是每当遇到一个无法解释的构建错误我的第一反应永远不是 Google而是先make V1 -j1 21 | tee build.log然后打开build.log从最后一行报错开始向上翻找到那个失败的命令再手动执行一遍。90% 的时候错误信息会比make的摘要更详细直接告诉你问题出在哪。这个习惯帮我节省了无数个通宵调试的时间。