从零开始逐步实现U-Boot(一)——Kconfig/Kbuild配置构建系统搭建与主机程序编译
系列:从零开始逐步实现U-Boot
目标平台:NXP i.MX6ULL (ARM Cortex-A7)
QEMU mcimx6ul-evk模拟板 或 正点原子IMX6ULL-MINI板
U-Boot(Universal Bootloader)作为嵌入式系统的核心组件,其构建系统的演进经历了从早期的纯 Makefile 到引入 Linux 内核式 Kconfig/Kbuild 系统的转变。这一转变极大地增强了代码的可配置性和模块化。
本篇将聚焦于 Kconfig 与 Kbuild 基础架构 的简化版本搭建,这是实现可视化配置(make menuconfig)和自动化编译的基础。
项目源码地址:uscxi/imx6ull-custom-uboot: 从零开始逐步实现U-Boot
1. 实验环境准备
- 宿主机: Ubuntu 24.04 LTS
- 工具链: gcc, make, flex, bison等
- 交叉编译工具链:gcc-arm-none-eabi(10.3.1 20210824)
- 目标框架: 仿照 U-Boot v2025.x+ 结构
- 目标机器:(可在下面两个目标上运行,推荐虚拟机器)
- QEMU 上的虚拟机器 mcimx6ul-evk(Freescale i.MX6UL Evaluation Kit (Cortex-A7))
- 正点原子IMX6ULL-Mini开发板
2. 核心目录结构
首先,我们需要建立最基础的目录树,剥离 U-Boot 的繁杂源码,只保留构建系统核心。
u-boot/
├── Kconfig # 顶层配置入口
├── Makefile # 顶层 Makefile,处理全局逻辑
├── config.mk/ # Makefile 相关变量
├── arch/ # 架构相关代码(存放 Kconfig)
│ └── arm/
├── scripts/ # Kconfig/Kbuild 核心脚本
│ ├── basic/ # 构建工具 fixdep 源码
│ ├── kconfig/ # Kconfig 配置工具源码
│ ├── dtc/ # 设备树工具 DTC 源码
│ ├── gcc-version.sh # 相关脚本
│ ├── setlocalversion # 相关脚本
│ ├── mkmakefile # 外部构建目录中生成辅助 Makefile
│ ├── Makefile # 主机程序编译 Makefile
│ ├── Kbuild.include # 定义通用 Makefile 函数
│ ├── Kbuild.lib # Kbuild 文件中的对象列表处理
│ ├── Kbuild.host # 用于编译主机程序(如mkimage)的规则
│ ├── Kbuild.autoconf # 处理自动生成的配置头文件
│ ├── Kbuild.clean # 清理规则
│ ├── Kconfig.include # Kconfig 配置工具变量
│ └── Makefile.build # 递归编译的核心脚本
├── include/ # 头文件
└── configs/ # 存放默认配置文件(defconfig)
3. 顶层 Makefile 实现
顶层 Makefile 是整个构建系统的入口,负责环境变量初始化、Kconfig 工具链调用以及编译流程控制。
基本配置与输出美化控制
# 最终版本号格式: VERSION.PATCHLEVEL.SUBLEVEL-EXTRAVERSION
# 例如: 2025.04-rc3
VERSION = 2025 # 主版本号,通常与发布年份对应
PATCHLEVEL = 04 # 补丁级别,通常与发布月份对应(例如04代表四月)
SUBLEVEL = # 子版本号,表示当前版本的第几次修正
EXTRAVERSION = # 额外版本标识,用于标记候选版本(如 -rc1、-rc2)或自定义后缀
NAME = # 版本代号,可填写该版本的昵称,通常用于开发者内部标识,通常留空
# 禁用内置规则和内置变量
# 确保所有工具链变量都由本 Makefile 显式定义,避免环境污染,便于理解和维护。
MAKEFLAGS += -rR
# 确定宿主机的架构
# include/host_arch.h 定义了架构常量,形如:
# #define HOST_ARCH_X86 0x0386
# #define HOST_ARCH_X86_64 0x8664
# #define HOST_ARCH_ARM 0x00a7
# #define HOST_ARCH_AARCH64 0xaa64
# 等等...
include include/host_arch.h
ifeq ("", "$(CROSS_COMPILE)") # 如果没有设置交叉编译工具链(本地编译)
MK_ARCH="${shell uname -m}" # 使用 uname -m 获取本机架构,例如 x86_64
else # 否则,已设置交叉编译工具链
MK_ARCH="${shell echo $(CROSS_COMPILE) | sed -n 's/^\(.*ccache\)\{0,1\}[[:space:]]*\([^\/]*\/\)*\([^-]*\)-[^[:space:]]*/\3/p'}" # 从工具链前缀中提取目标架构(如 aarch64-linux-gnu- → aarch64),并处理可能的 ccache 前缀
endif
unexport HOST_ARCH
ifeq ("x86_64", $(MK_ARCH))
export HOST_ARCH=$(HOST_ARCH_X86_64)
else ifneq (,$(findstring $(MK_ARCH), "aarch64" "armv8l"))
export HOST_ARCH=$(HOST_ARCH_AARCH64)
else ifneq (,$(findstring $(MK_ARCH), "arm" "armv7" "armv7a" "armv7l"))
export HOST_ARCH=$(HOST_ARCH_ARM)
endif
undefine MK_ARCH # 清理临时变量 MK_ARCH
# Locale 环境隔离
# 构建系统必须在确定性的语言环境下运行。
unexport LC_ALL # 取消 LC_ALL 环境变量,避免覆盖具体分类设置
LC_COLLATE=C # 设置字符串排序规则为 C(简单 ASCII 顺序),保证构建输出一致性
LC_NUMERIC=C # 设置数字格式为 C,避免因区域不同导致小数点等问题
export LC_COLLATE LC_NUMERIC # 导出这两个设置,使其在子进程中生效
# Avoid interference with shell env settings
unexport GREP_OPTIONS
# 输出美化控制
# V=0(默认): 精简输出
# quiet = "quiet_"
# Q = "@"
# 效果: 只显示 "CC xxx.o" 等摘要信息
#
# V=1(详细): 完整命令输出
# quiet = ""
# Q = ""
# 效果: 显示完整的编译命令行
#
# make -s(静默): 几乎无输出
# quiet = "silent_"
# 效果: 抑制所有非错误输出
ifeq ("$(origin V)", "command line")
KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
KBUILD_VERBOSE = 0
endif
ifeq ($(KBUILD_VERBOSE),1)
quiet =
Q =
else
quiet=quiet_
Q = @
endif
# 检测 make -s(静默模式)的兼容性处理
# make 4.x 和 3.8x 在 MAKEFLAGS 中表示 -s 的方式不同:
# make 4.x: MAKEFLAGS 第一个词中包含 's',且前面可能有其他单字符选项
# make 3.8x: MAKEFLAGS 以 "s" 开头或包含 "-s"
ifneq ($(filter 4.%,$(MAKE_VERSION)),) # make-4
ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)
quiet=silent_
endif
else # make-3.8x
ifneq ($(filter s% -s%,$(MAKEFLAGS)),)
quiet=silent_
endif
endif
# 导出到所有子 make 进程,确保递归构建中输出行为一致
export quiet Q KBUILD_VERBOSE
外部构建(Out-of-tree Build)支持
U-Boot/Linux 支持将构建产物放到独立目录(O=
make O=./build
第一次 make 在源码目录启动,设置 KBUILD_OUTPUT=./build,在./build 中启动第二次 make,KBUILD_SRC 指向源码目录,第二次 make 完成实际构建。
判断流程:
KBUILD_SRC 为空?第一次调用在源码目录中,检查 O 变量, 如果有,启动 sub-make;
KBUILD_SRC 非空?第二次调用在输出目录中,跳过此段,进入实际构建逻辑。
ifeq ($(KBUILD_SRC),)
# 检查用户是否通过命令行指定了输出目录
# origin 函数确保只接受命令行参数,不接受环境变量
ifeq ("$(origin O)", "command line")
KBUILD_OUTPUT := $(O)
endif
# _all 是未指定任何目标时的默认目标
# PHONY 变量收集所有伪目标,最后统一声明 .PHONY
PHONY := _all
_all:
# 防止 make 尝试用隐式规则重新生成 Makefile 自身
$(CURDIR)/Makefile Makefile: ;
ifneq ($(KBUILD_OUTPUT),)
# 创建输出目录并解析为绝对路径
saved-output := $(KBUILD_OUTPUT)
KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \
# 如果 KBUILD_OUTPUT 为空,说明目录创建失败&& /bin/pwd)
$(if $(KBUILD_OUTPUT),, \
$(error failed to create output directory "$(saved-output)"))
# 让 make 在查找 include 文件时也搜索源码根目录
# 这使得 "include scripts/Kbuild.include" 等语句在输出目录中也能工作
MAKEFLAGS += --include-dir=$(CURDIR)
PHONY += $(MAKECMDGOALS) sub-make
# 将所有目标(除了 _all, sub-make, 和 Makefile 本身)重定向到 sub-make
# @: 是空命令(冒号是 shell 的 no-op),表示"什么都不做"
# 实际工作全部由 sub-make 完成
$(filter-out _all sub-make $(CURDIR)/Makefile, $(MAKECMDGOALS)) _all: sub-make
@:
# sub-make:在输出目录中启动第二次 make
# -C $(KBUILD_OUTPUT):切换到输出目录
# KBUILD_SRC=$(CURDIR):告诉子 make 源码在哪里
# -f $(CURDIR)/Makefile:使用源码目录的 Makefile
# filter-out:过滤掉内部目标,只传递用户指定的目标
sub-make: FORCE
$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))
# 设置跳过标志,后续所有构建逻辑都不再执行
# (因为实际构建已委托给 sub-make)
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)
endif # ifeq ($(KBUILD_SRC),)
# 只有当 skip-makefile 未设置时,才继续处理后续内容。
# 此时我们要么在源码目录中直接构建(无 O=),
# 要么在输出目录中作为 sub-make 运行。
ifeq ($(skip-makefile),)
# 抑制 "Entering directory ..." 消息
# 进入输出目录时的消息由 sub-make 机制自行处理
# 这里抑制的是后续递归进入子目录时的消息
MAKEFLAGS += --no-print-directory
# 确保 _all(默认目标)最终指向 all
PHONY += all
_all: all
源码树定位
根据是否为外部构建,确定 srctree 的值:
原地构建(KBUILD_SRC 为空): srctree = .
在源码子目录构建: srctree = ..
完全外部构建: srctree = $(KBUILD_SRC) (绝对路径)
ifeq ($(KBUILD_SRC),)
# building in the source tree
srctree := .
else
ifeq ($(KBUILD_SRC)/,$(dir $(CURDIR)))
# building in a subdirectory of the source tree
srctree := ..
else
srctree := $(KBUILD_SRC)
endif
endif
objtree := . # 构建产物总是在当前目录
src := $(srctree)
obj := $(objtree)
# 当 make 在当前目录找不到依赖文件时,会自动到 VPATH 指定的目录中查找
VPATH := $(srctree)
export srctree objtree VPATH
# CDPATH 是 bash 的环境变量,会影响 cd 命令的行为
# 在 Makefile 中执行 shell 命令时,CDPATH 可能导致 cd 跳转到意外目录
unexport CDPATH
宿主机环境探测
检测宿主机操作系统,用于后续条件判断的特殊编译选项,编译主机程序。
# 探测宿主机 CPU 架构,统一名称。
HOSTARCH := $(shell uname -m | \
sed -e s/i.86/x86/ \
-e s/sun4u/sparc64/ \
-e s/arm.*/arm/ \
-e s/sa110/arm/ \
-e s/ppc64/powerpc/ \
-e s/ppc/powerpc/ \
-e s/macppc/powerpc/\
-e s/sh.*/sh/)
# 探测宿主机操作系统
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \
sed -e 's/\(cygwin\).*/cygwin/')
export HOSTARCH HOSTOS
交叉编译与基础配置
如果宿主机架构与目标架构相同,表示这是原生编译,此时无需交叉编译器前缀。
# ?= 表示仅在变量未定义时才赋值,允许用户覆盖
ifeq ($(HOSTARCH),$(ARCH))
CROSS_COMPILE ?=
endif
# .config 文件路径,Kconfig 系统的核心输出
# 用户可以通过 KCONFIG_CONFIG 环境变量指定替代路径
# 默认为源码根目录下的 .config
KCONFIG_CONFIG ?= .config
export KCONFIG_CONFIG
# 宿主机工具链配置
# CONFIG_SHELL:Kbuild 使用的 shell 解释器
# 优先使用 bash(因为部分 Kbuild 脚本依赖 bash 特性),
# 降级为 /bin/bash,最终降级为 sh
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
else if [ -x /bin/bash ]; then echo /bin/bash; \
else echo sh; fi ; fi)
# 宿主机编译器(用于编译构建过程中在宿主机上运行的工具)
# 注意区分:
# HOSTCC — 编译宿主机工具(如 scripts/kconfig/mconf)
# CC — 编译目标平台代码(U-Boot 本身)
HOSTCC = cc # 系统默认 C 编译器
HOSTCXX = c++ # 系统默认 C++ 编译器
# 宿主机编译标志
# -Wall: 开启所有常见警告
# -Wstrict-prototypes: 要求函数原型声明中必须有参数列表
# -O2: 开启优化(宿主机工具不需要调试,追求速度)
# -fomit-frame-pointer: 省略帧指针,释放一个寄存器
KBUILD_HOSTCFLAGS := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer \
$(HOSTCFLAGS)
KBUILD_HOSTCXXFLAGS := -O2 $(HOSTCXXFLAGS)
KBUILD_HOSTLDFLAGS := $(HOSTLDFLAGS)
KBUILD_HOSTLDLIBS := $(HOSTLDLIBS)
# C 语言标准:使用 GNU C11(C11 + GNU 扩展)
# GNU 扩展包括:内联汇编、语句表达式、typeof、__attribute__ 等
CSTD_FLAG := -std=gnu11
KBUILD_HOSTCFLAGS += $(CSTD_FLAG)
export KBUILD_SRC
Kbuild 核心框架
Kbuild.include 是整个构建系统的”标准库”,它定义了大量核心函数和变量。
# 空规则 "scripts/Kbuild.include: ;" 防止 make 尝试重新生成此文件
scripts/Kbuild.include: ;
include scripts/Kbuild.include
# 目标平台工具链定义
# $(CROSS_COMPILE) 是交叉编译器前缀,例如:
# arm-linux-gnueabihf- → AS = arm-linux-gnueabihf-as
# aarch64-none-elf- → CC = aarch64-none-elf-gcc
AS = $(CROSS_COMPILE)as # 汇编器
# 链接器:优先使用 ld.bfd
# 原因:
# 1) BFD (Binary File Descriptor) 链接器是 GNU ld 的传统实现
# 2) gold 链接器虽然更快,但对某些嵌入式链接脚本的支持不完整
# 3) LLVM 的 lld 在裸机(bare-metal)场景下可能有兼容性问题
# 4) U-Boot 的链接脚本依赖 BFD ld 的某些特定行为
ifneq ($(shell $(CROSS_COMPILE)ld.bfd -v 2> /dev/null),)
LD = $(CROSS_COMPILE)ld.bfd
else
LD = $(CROSS_COMPILE)ld
endif
CC = $(CROSS_COMPILE)gcc # C 编译器
CPP = $(CC) -E # C 预处理器(仅预处理,不编译)
AR = $(CROSS_COMPILE)ar # 静态库打包工具
NM = $(CROSS_COMPILE)nm # 符号表查看工具
LDR = $(CROSS_COMPILE)ldr # U-Boot 特有的镜像加载工具
STRIP = $(CROSS_COMPILE)strip # 符号剥离工具
OBJCOPY = $(CROSS_COMPILE)objcopy # 目标文件格式转换(ELF→BIN 等)
OBJDUMP = $(CROSS_COMPILE)objdump # 反汇编工具
LEX = flex # 词法分析器生成器(用于 Kconfig 解析)
YACC = bison # 语法分析器生成器(用于 Kconfig 解析)
# 设备树编译器(DTC)配置
# DTC (Device Tree Compiler) 用于编译 .dts → .dtb
# U-Boot 自带 DTC 源码(scripts/dtc/),默认使用内建版本
# 用户可通过 DTC=/usr/bin/dtc 指定外部版本
# 如果使用外部 DTC,假设 pylibfdt(Python 绑定)也可用
DTC_INTREE := $(objtree)/scripts/dtc/dtc # 内建 DTC 路径
DTC ?= $(DTC_INTREE) # 默认使用内建版本
DTC_MIN_VERSION := 010406 # 最低版本要求:1.4.6
编译标志定义
# C 预处理器标志
# -D__KERNEL__: 标识正在编译内核态代码(U-Boot 复用了大量 Linux 内核头文件)
# -D__UBOOT__: 标识正在编译 U-Boot(区分于 Linux 内核和其他项目)
KBUILD_CPPFLAGS := -D__KERNEL__ -D__UBOOT__
# C 编译器标志
# -Wall: 开启大部分编译警告
# -Wstrict-prototypes: 要求严格的函数原型声明
# -Wno-format-security: 抑制 printf 格式字符串安全警告
# (U-Boot 中大量使用运行时动态格式字符串)
# -fno-builtin: 禁用 GCC 内建函数(如 memcpy, printf 的优化替换)
# U-Boot 有自己的实现,不能被编译器替换
# -ffreestanding: 告知编译器这是独立环境(非宿主环境)
# 不假设标准库和启动代码的存在
# -fshort-wchar: wchar_t 使用 2 字节(与 UEFI 规范一致)
# -fno-strict-aliasing: 禁用严格别名规则优化
# 嵌入式代码中经常需要通过不同类型指针访问同一内存
KBUILD_CFLAGS := -Wall -Wstrict-prototypes \
-Wno-format-security \
-fno-builtin -ffreestanding $(CSTD_FLAG)
KBUILD_CFLAGS += -fshort-wchar -fno-strict-aliasing
# 汇编器标志
# -D__ASSEMBLY__: 在汇编文件中定义此宏
# 使得 C 头文件在被汇编文件 include 时可以条件编译掉 C 专有内容
KBUILD_AFLAGS := -D__ASSEMBLY__
# 链接器标志(此处为空,由架构 Makefile 和 board 配置追加)
KBUILD_LDFLAGS :=
# 禁止生成位置无关代码(PIE - Position Independent Executable)
# 原因:
# 1) U-Boot 有自己的重定位机制,不依赖 PIE
# 2) PIE 会引入 GOT/PLT 等开销,不适合 bootloader
# 3) 某些现代 GCC 默认开启 PIE,需要显式关闭
# cc-option 是 Kbuild.include 中定义的探测函数:
# 如果编译器支持 -fno-PIE 则使用之,否则忽略
KBUILD_CFLAGS += $(call cc-option,-fno-PIE)
KBUILD_AFLAGS += $(call cc-option,-fno-PIE)
# 版本字符串生成
# UBOOTRELEASE: 完整版本字符串,从 include/config/uboot.release 读取
# 该文件由 scripts/setlocalversion 生成
# 内容示例: "2025.04-rc3-00001-gabcdef1234-dirty"
# 包含: 版本号 + git 信息 + 脏标记
# UBOOTVERSION: 基础版本字符串(不含 git 信息)
# 内容示例: "2025.04-rc3"
UBOOTRELEASE = $(shell cat include/config/uboot.release 2> /dev/null)
UBOOTVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
# 将所有关键变量导出到子 make 进程和 shell 命令
# 按功能分组:
# 版本信息 → 子 Makefile 和链接脚本中使用
# 架构信息 → 决定编译哪些目录和文件
# 工具链 → 所有编译命令都需要
# 编译标志 → 统一的编译选项
export VERSION PATCHLEVEL SUBLEVEL UBOOTRELEASE UBOOTVERSION
export ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR
export CONFIG_SHELL HOSTCC KBUILD_HOSTCFLAGS CROSS_COMPILE AS LD CC
export CPP AR NM LDR STRIP OBJCOPY OBJDUMP KBUILD_HOSTLDFLAGS KBUILD_HOSTLDLIBS
export LEX YACC DTC
export KBUILD_CPPFLAGS KBUILD_CFLAGS KBUILD_AFLAGS
export UBOOTINCLUDE KBUILD_LDFLAGS
export CC_VERSION_TEXT := $(shell $(CC) --version | head -n 1)
export RCS_FIND_IGNORE := \( -name SCCS -o -name BitKeeper -o -name .svn -o \
-name CVS -o -name .pc -o -name .hg -o -name .git \) \
-prune -o
配置目标与构建目标共享的规则
scripts_basic:编译最基础的宿主机工具。
scripts/basic/ 下通常包含 fixdep 工具,这个工具是 Kconfig 等后续步骤的前置依赖:
fixdep:修正 GCC 生成的依赖文件(.d),使其与 Kbuild 的依赖跟踪系统兼容。它将 #include 的头文件依赖转化为CONFIG_xxx 的依赖,这样只有相关配置改变时才重编译。
# $(build) 在 Kbuild.include 中定义为:
# build := -f $(srctree)/scripts/Makefile.build obj
# 因此 $(Q)$(MAKE) $(build)=scripts/basic 展开为:
# @make -f scripts/Makefile.build obj=scripts/basic
PHONY += scripts_basic
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
# 防止 scripts/basic/ 下的文件触发隐式规则
# 空规则确保 make 不会尝试"构建"这些文件
scripts/basic/%: scripts_basic ;
外部构建目录中生成辅助 Makefile
# outputmakefile: 在外部构建目录中生成辅助 Makefile
# 这样用户可以在输出目录中直接运行 make,无需指定 -f
PHONY += outputmakefile
outputmakefile:
ifneq ($(KBUILD_SRC),)
$(Q)ln -fsn $(srctree) source # 创建 source → 源码目录的符号链接
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile $(srctree)
endif
目标分类
Kbuild 将 make 目标分为三类:
- 配置目标(*config): menuconfig,defconfig,oldconfig 等
- 非配置目标: all,u-boot.bin,clean 等
- 混合目标:同时指定配置和非配置目标(如 make defconfig all)
分类的原因:
配置目标不需要读取 .config(它们的目的就是生成 .config),构建目标需要读取 .config(否则不知道编译什么),混合目标需要逐个执行(先配置,再构建)。
# 版本和时间戳头文件路径
# 这些文件在 prepare 阶段生成,供 C 代码引用
version_h := include/generated/version_autogenerated.h
timestamp_h := include/generated/timestamp_autogenerated.h
# 不需要 .config 的目标列表
# clean/mrproper/distclean: 清理目标
# ubootversion: 只打印版本号
no-dot-config-targets := clean mrproper distclean \
ubootversion
# 三个控制标志的初始值
config-targets := 0 # 是否包含配置目标
mixed-targets := 0 # 是否同时包含配置和非配置目标
dot-config := 1 # 是否需要读取 .config
# 检查当前目标是否全部为"不需要 .config"的目标
# 逻辑:
# 如果 MAKECMDGOALS 中有 no-dot-config-targets 的成员,则继续检查
# 如果过滤掉这些目标后为空,全部是不需要 .config 的,则dot-config := 0
# 否则,还有其他目标需要 .config,保持 dot-config := 1
ifneq ($(filter $(no-dot-config-targets), $(MAKECMDGOALS)),)
ifeq ($(filter-out $(no-dot-config-targets), $(MAKECMDGOALS)),)
dot-config := 0
endif
endif
# 检查是否包含配置目标
# "config" 匹配纯 "config" 目标
# "%config" 匹配 menuconfig, defconfig, oldconfig 等
ifneq ($(filter config %config,$(MAKECMDGOALS)),)
config-targets := 1
# 如果目标多于 1 个(即除了 *config 还有其他),则为混合目标
ifneq ($(words $(MAKECMDGOALS)),1)
mixed-targets := 1
endif
endif
# 混合目标处理
ifeq ($(mixed-targets),1)
# 当用户执行类似 "make defconfig all" 时:
# 不能同时处理配置和构建,因为 .config 必须先生成
# 解决方案:逐个目标递归调用 make
# 1. make -f Makefile defconfig
# 2. make -f Makefile all
PHONY += $(MAKECMDGOALS) __build_one_by_one
# 除 __build_one_by_one 外的所有目标都依赖它
# @: 空命令——这些目标本身不做任何事,实际工作在 __build_one_by_one 中
$(filter-out __build_one_by_one, $(MAKECMDGOALS)): __build_one_by_one
@:
# 核心逻辑:用 set -e 确保任一步骤失败立即停止
# 然后 for 循环逐个调用 make
__build_one_by_one:
$(Q)set -e; \
for i in $(MAKECMDGOALS); do \
$(MAKE) -f $(srctree)/Makefile $$i; \
done
else # mixed-targets=0
# 配置目标处理
ifeq ($(config-targets),1)
# KBUILD_DEFCONFIG: 默认的 defconfig 文件
KBUILD_DEFCONFIG :=
export KBUILD_DEFCONFIG KBUILD_KCONFIG
# config 和 %config 规则:
# 依赖:
# scripts_basic — 确保 fixdep 等基础工具已编译
# outputmakefile — 确保外部构建的 Makefile 已生成
# FORCE — 强制每次都执行(即使目标文件存在)
#
# 动作:
# 进入 scripts/kconfig 目录执行对应目标
# scripts/kconfig/Makefile 会编译并运行:
# conf — 命令行配置工具(处理 defconfig, oldconfig, syncconfig)
# mconf — ncurses 图形界面(处理 menuconfig)
# nconf — 更现代的 ncurses 界面(处理 nconfig)
#
# 例如 make menuconfig 展开为:
# @make -f scripts/Makefile.build obj=scripts/kconfig menuconfig
config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@
%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@
else # config-targets=0
# 构建目标处理
# 处理所有非配置目标:u-boot.bin、all、clean 等
# include/config.mk 由 Kconfig 的 syncconfig 生成
# -include: 文件不存在时不报错(首次构建时该文件尚未生成)
-include include/config.mk
# scripts 目标:编译辅助脚本工具
# 仅在 auto.conf 存在后才编译,确保配置已完成
PHONY += scripts
scripts: scripts_basic scripts_dtc include/config/auto.conf
$(Q)$(MAKE) $(build)=$(@)
# .config 与 auto.conf 同步机制
ifeq ($(dot-config),1)
# 读取 auto.conf
# 这些变量在后续 Makefile 逻辑中作为条件判断使用
-include include/config/auto.conf
# auto.conf.cmd 记录了生成 auto.conf 时的 Kconfig 依赖
# 这样当任何 Kconfig 文件改变时,auto.conf 会自动重新生成
-include include/config/auto.conf.cmd
# 防止 make 尝试用隐式规则重建这些文件
$(KCONFIG_CONFIG) include/config/auto.conf.cmd: ;
自动配置同步规则
触发条件(任一):
- .config 比 auto.conf 新(用户手动编辑了 .config)
- auto.conf.cmd 不存在(首次构建或 clean 后)
- 某个 Kconfig 文件被修改(通过 auto.conf.cmd 的依赖跟踪)
执行流程:
Step 1: make syncconfig
运行 scripts/kconfig/conf –syncconfig Kconfig
读取 .config,与 Kconfig 树校验,生成:
include/config/auto.conf (Makefile 变量)
include/config/auto.conf.cmd (依赖追踪)
include/generated/autoconf.h (C 宏定义)
include/config/ 下的空文件 (精细依赖追踪)
Step 2: make -f scripts/Makefile.autoconf
生成 include/autoconf.mk 等兼容性文件
如果失败,删除 auto.conf 强制下次重新生成
Step 3: touch auto.conf
更新时间戳,确保 auto.conf 比 autoconf.h 新
避免 syncconfig 被重复触发
include/config/%.conf: $(KCONFIG_CONFIG) include/config/auto.conf.cmd
$(Q)$(MAKE) -f $(srctree)/Makefile syncconfig
@# 如果 Makefile.autoconf 执行失败,删除 auto.conf
@# 这样下次 make 时会重新触发 syncconfig
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf || \
{ rm -f include/config/auto.conf; false; }
@# 更新 auto.conf 的时间戳
@# 防止 auto.conf 比 autoconf.h 旧导致循环触发
$(Q)touch include/config/auto.conf
# u-boot.cfg: 生成完整的预处理配置文件
# 用于调试——可以看到所有 CONFIG_xxx 的最终值
u-boot.cfg:
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf $(@)
# 旧式兼容性配置文件
# autoconf.mk: 类似 auto.conf 但格式略有不同
# autoconf.mk.dep: autoconf.mk 的依赖文件
-include include/autoconf.mk
-include include/autoconf.mk.dep
# 三重条件守卫确保只在配置完全就绪时才加载架构 Makefile:
# 1) .config 文件存在
# 2) auto.conf 文件存在
# 3) auto.conf 不比 .config 旧(用户没有在生成后修改 .config)
#
# config.mk: 传统的板级配置(ARCH, CPU, BOARD 等变量)
# arch/$(ARCH)/Makefile: 架构级编译规则
# 定义 head-y(启动代码), PLATFORM_CPPFLAGS 等关键变量
ifneq ($(wildcard $(KCONFIG_CONFIG)),)
ifneq ($(wildcard include/config/auto.conf),)
autoconf_is_old := $(shell find . -path ./$(KCONFIG_CONFIG) -newer \
include/config/auto.conf)
ifeq ($(autoconf_is_old),)
include config.mk
include arch/$(ARCH)/Makefile
endif
endif
endif
链接脚本
链接脚本(Linker Script,.lds)决定了二进制文件的内存布局。
U-Boot 的链接脚本通常定义:
代码段、数据段的起始地址
特殊段(如 .u_boot_list 用于命令/驱动注册)
堆栈位置
搜索优先级:
- CONFIG_SYS_LDSCRIPT 显式指定(Kconfig 中配置)
- board/$(BOARDDIR)/u-boot.lds(板级定制)
- $(CPUDIR)/u-boot.lds(CPU 级别)
- arch/$(ARCH)/cpu/u-boot.lds(架构级别通用)
ifndef LDSCRIPT
ifdef CONFIG_SYS_LDSCRIPT
# 去除 Kconfig 字符串值的双引号
# Kconfig 中 string 类型的值带引号: CONFIG_SYS_LDSCRIPT="arch/arm/cpu/u-boot.lds"
LDSCRIPT := $(srctree)/$(CONFIG_SYS_LDSCRIPT:"%"=%)
endif
endif
ifndef LDSCRIPT
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds
endif
endif
else #dot-config=0
# dot-config=0 时(clean 等目标),auto.conf 不需要存在
# 提供空规则防止 make 报错
include/config/auto.conf: ;
endif # $(dot-config)
优化级别控制
优化级别通过 Kconfig 配置,而非在 Makefile 中硬编码,这样用户可以通过 menuconfig 灵活切换。
ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
# -Os: 优化代码体积(bootloader 通常受 ROM/Flash 大小限制)
# 这是 U-Boot 最常用的优化级别
KBUILD_CFLAGS += -Os
endif
ifdef CONFIG_CC_OPTIMIZE_FOR_SPEED
# -O2: 优化执行速度(某些性能敏感场景)
KBUILD_CFLAGS += -O2
endif
ifdef CONFIG_CC_OPTIMIZE_FOR_DEBUG
# -Og: 优化调试体验
# 在不严重影响调试的前提下进行最小优化
# 比 -O0 生成更好的代码,同时保持大部分变量可观察
KBUILD_CFLAGS += -Og
# GCC 在 -Og 下可能产生误报的 "maybe-uninitialized" 警告
# 参见 GCC Bug #78394
KBUILD_CFLAGS += -Wno-maybe-uninitialized
endif
# LTO (Link-Time Optimization) 链接时优化标志
# 允许编译器在链接阶段跨编译单元进行优化
# 此处初始化为空,由架构 Makefile 按需设置
LTO_CFLAGS :=
LTO_FINAL_LDFLAGS :=
export LTO_CFLAGS LTO_FINAL_LDFLAGS
头文件包含路径
UBOOTINCLUDE: 统一的头文件搜索路径
-Iinclude: U-Boot 通用头文件目录
-I$(srctree)/include: 外部构建时,源码目录的 include
-I$(srctree)/arch/$(ARCH)/include: 架构特定头文件
-include $(srctree)/include/linux/kconfig.h: 强制所有编译单元包含此文件(-include 而非 -I)
kconfig.h 定义了 IS_ENABLED(),IS_BUILTIN() 等宏,这些宏将 CONFIG_xxx 转换为编译时布尔判断,使得 C 代码可以写: if (IS_ENABLED(CONFIG_FOO)) { … },编译器会在优化阶段消除不可达代码,替代 #ifdef。
UBOOTINCLUDE := \
-Iinclude \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-I$(srctree)/arch/$(ARCH)/include \
-include $(srctree)/include/linux/kconfig.h
# -nostdinc: 不搜索标准系统头文件目录
# U-Boot 是独立环境,不能使用宿主机的 stdio.h 等
# -isystem: 但仍需要编译器内建头文件(如 stdarg.h, stddef.h, stdint.h),这些是编译器提供的,与操作系统无关
# $(CC) -print-file-name=include 输出编译器内建头文件路径
# 例如: /usr/lib/gcc/arm-linux-gnueabihf/11/include
NOSTDINC_FLAGS += -nostdinc -isystem $(shell $(CC) -print-file-name=include)
# 组合完整的预处理和编译标志
cpp_flags := $(KBUILD_CPPFLAGS) $(PLATFORM_CPPFLAGS) $(UBOOTINCLUDE) \
$(NOSTDINC_FLAGS)
c_flags := $(KBUILD_CFLAGS) $(cpp_flags)
编译目标
libs-y 变量收集所有需要编译的子目录,每个目录后面的 ‘/‘ 是 Kbuild 约定:表示需要递归进入该目录。在实际 U-Boot 中,这个列表会很长,包含:arch/$(ARCH)/lib/, cpu/, board/, common/, lib/, drivers/ 等
libs-y += lib/
# sort 去重并按字母排序
# 排序虽然改变了链接顺序,但 U-Boot 的启动入口由链接脚本的 ENTRY() 指令决定,而非链接顺序
libs-y := $(sort $(libs-y))
# u-boot-dirs: 去掉尾部 '/' 得到纯目录名列表
# filter %/: 过滤出以 '/' 结尾的项(即需要递归的目录)
# patsubst %/,%: 去掉尾部的 '/'
# 例如: "common/ lib/ drivers/" → "common lib drivers"
u-boot-dirs := $(patsubst %/,%,$(filter %/, $(libs-y)))
# u-boot-alldirs: 包含所有目录(含 libs- 即被禁用的目录)
# 用于 clean 等操作——即使被禁用的目录也需要清理
u-boot-alldirs := $(sort $(u-boot-dirs) $(patsubst %/,%,$(filter %/, $(libs-))))
# 将目录名转换为 built-in.o 路径
# 例如: "common/ lib/" → "common/built-in.o lib/built-in.o"
# built-in.o 是每个目录的编译产物集合(由 Makefile.build 生成)
# 它是该目录下所有 obj-y 目标的归档文件
libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
# u-boot-init: 启动代码(必须最先链接)
# head-y 由架构 Makefile 定义,通常指向:arch/arm/cpu/armv7/start.o (ARM 示例)
# 这是 U-Boot 的入口点,包含复位向量和初始化代码
u-boot-init := $(head-y)
# u-boot-main: 其余所有编译产物
u-boot-main := $(libs-y)
# 链接脚本 (.lds) 通常需要预处理以支持条件编译
# LDPPFLAGS 是传给 CPP 的标志:
# -include u-boot.lds.h: 强制包含链接脚本头文件
# -DCPUDIR: 传递 CPU 目录路径
# -DLD_MAJOR/-DLD_MINOR: 链接器版本号,用于兼容性处理
LDPPFLAGS += \
-include $(srctree)/include/u-boot/u-boot.lds.h \
-DCPUDIR=$(CPUDIR) \
$(shell $(LD) --version | \
sed -ne 's/GNU ld version \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/-DLD_MAJOR=\1 -DLD_MINOR=\2/p')
# INPUTS-y: 最终要生成的二进制文件列表
INPUTS-y += u-boot.bin
# 链接标志
# LDFLAGS_FINAL: 由架构 Makefile 定义的最终链接选项
# -Ttext: 指定 .text 段的加载地址
# CONFIG_TEXT_BASE 在 Kconfig 中配置,例如 0x87800000
LDFLAGS_u-boot += $(LDFLAGS_FINAL)
LDFLAGS_u-boot += -Ttext $(CONFIG_TEXT_BASE)
# objcopy 标志:将 ELF 转换为纯二进制(raw binary)
# -O binary: 输出格式为原始二进制
# 结果文件可以直接烧写到 Flash 或通过 JTAG 加载
OBJCOPYFLAGS_u-boot.bin := -O binary
# 命令定义
# Kbuild 的命令定义约定:
# quiet_cmd_xxx: V=0 时显示的简短信息
# cmd_xxx: 实际执行的命令
# $(call if_changed,xxx) 会:
# 1) 检查命令是否与上次相同(通过 .cmd 文件)
# 2) 检查依赖是否有变化
# 3) 如果需要重新执行,根据 $(quiet) 选择输出格式
quiet_cmd_objcopy = OBJCOPY $@
cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) \
$(OBJCOPYFLAGS_$(@F)) $< $@
quiet_cmd_objcopy_uboot = OBJCOPY $@
cmd_objcopy_uboot = $(cmd_objcopy)
# cfg 目标:生成 u-boot.cfg 配置摘要文件
cfg: u-boot.cfg
# binman 集成
# binman 是 U-Boot 的二进制打包工具
# 它负责将 u-boot.bin、SPL、ATF、设备树等组合成最终的烧写镜像
# .binman_stamp 是时间戳文件,确保每次构建都运行 binman
.binman_stamp: $(INPUTS-y) FORCE
ifeq ($(CONFIG_BINMAN),y)
$(call if_changed,binman)
endif
@touch $@
# all 是默认构建目标,依赖 .binman_stamp
all: .binman_stamp
# u-boot.bin 生成规则
# u-boot (ELF) → u-boot.bin (raw binary)
# FORCE 确保每次 make 都检查是否需要重新生成
u-boot.bin: u-boot FORCE
$(call if_changed,objcopy_uboot)
# u-boot (ELF) 链接规则
# 这是整个构建系统最核心的规则——将所有目标文件链接为 ELF
#
# cmd_u-boot__ 的执行步骤:
# 1) touch $(u-boot-main): 确保 built-in.o 文件存在(防止空目录报错)
# 2) $(LD) 链接:
# $(KBUILD_LDFLAGS): 全局链接选项
# $(LDFLAGS_u-boot): u-boot 特定选项(-Ttext 等)
# -o $@: 输出文件 u-boot
# -T u-boot.lds: 使用链接脚本
# $(u-boot-init): 启动代码(head-y,如 start.o)
# $(u-boot-main): 主体代码(各目录的 built-in.o)
# -Map u-boot.map: 生成内存映射文件,用于调试
quiet_cmd_u-boot__ ?= LD $@
cmd_u-boot__ ?= \
touch $(u-boot-main) ; \
$(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_u-boot) -o $@ \
-T u-boot.lds $(u-boot-init) \
$(u-boot-main) \
-Map u-boot.map
# u-boot 目标规则
# 依赖:
# $(u-boot-init): 启动代码 .o 文件
# $(u-boot-main): 各目录的 built-in.o
# u-boot.lds: 链接脚本(已预处理)
# FORCE: 强制检查
#
# +$(call if_changed,...): 前缀 '+' 的作用:
# 即使 make 以 -n (dry-run) 或 -t (touch) 模式运行,
# 也强制执行此命令。这是因为链接步骤对于验证构建至关重要
u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE
+$(call if_changed,u-boot__)
递归构建机制
Makefile.build 是 Kbuild 的核心递归脚本,它会:
- 进入目标目录
- 读取该目录的 Makefile(或 Kbuild 文件)
- 获取 obj-y += xxx.o 列表
- 编译所有 .c/.S → .o
- 将 obj-y 归档为 built-in.o
- 如果 obj-y 中有目录(如 obj-y += subdir/),递归处理
# built-in.o 文件本身不需要显式规则
# 它们在递归进入子目录时由 Makefile.build 自动生成
# 空规则防止隐式规则干扰
# sort 去重,避免同一目标被执行多次
$(sort $(u-boot-init) $(u-boot-main)): $(u-boot-dirs) ;
# 核心递归规则:
# 对 u-boot-dirs 中的每个目录,执行:
# make -f scripts/Makefile.build obj=<dir>
#
# 依赖 prepare 和 scripts:确保头文件、配置、工具都已就绪
#
# Locale 设置注释说明:
# 这里不再修改 locale,因为全局已设置为 C locale
# 错误信息仍使用用户的原始语言(未 export LC_MESSAGES)
PHONY += $(u-boot-dirs)
$(u-boot-dirs): prepare scripts
$(Q)$(MAKE) $(build)=$@
构建准备阶段(prepare 链)
# 版本信息生成
# setlocalversion 脚本来自 Linux 内核
# 它生成类似 "-00001-gabcdef1234-dirty" 的版本后缀
# KERNELVERSION 变量是脚本所需的环境变量(沿用 Linux 的命名)
define filechk_uboot.release
KERNELVERSION=$(UBOOTVERSION) $(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree)
endef
# filechk 宏(定义在 Kbuild.include 中)的工作方式:
# 1) 执行 filechk_xxx 生成新内容到临时文件
# 2) 与现有文件比较
# 3) 仅在内容改变时才替换文件
# 4) 这避免了不必要的重编译(文件时间戳不变,依赖不触发)
include/config/uboot.release: include/config/auto.conf FORCE
$(call filechk,uboot.release)
# 构建准备阶段
# prepare 是一个多阶段的准备链,从 prepare3 到 prepare0 逐级执行:
# prepare3: 外部构建环境检查
# prepare2: 生成输出目录的 Makefile
# prepare1: 生成版本头文件 + 检查链接脚本
# archprepare: 架构特定准备 + 编译工具
# prepare0: 处理根目录的 Kbuild 文件
# prepare: 最终就绪(其他规则依赖此目标)
PHONY += prepare archprepare prepare0 prepare1 prepare2 prepare3
# prepare3: 外部构建时的完整性检查
# 确保源码目录是"干净"的——不包含之前构建的残留物
# 如果源码目录中有 .config 或 include/config/,说明有人在源码目录中执行过构建,这可能导致冲突
prepare3: include/config/uboot.release
ifneq ($(KBUILD_SRC),)
@$(kecho) ' Using $(srctree) as source for U-Boot'
$(Q)if [ -f $(srctree)/.config -o -d $(srctree)/include/config ]; then \
echo >&2 " $(srctree) is not clean, please run 'make mrproper'"; \
echo >&2 " in the '$(srctree)' directory.";\
/bin/false; \
fi;
endif
# prepare2: 确保输出目录有可用的 Makefile
prepare2: prepare3 outputmakefile cfg
# prepare1: 生成核心头文件 + 配置验证
# 依赖:
# version_h: include/generated/version_autogenerated.h
# timestamp_h: include/generated/timestamp_autogenerated.h
# auto.conf: 确保配置已同步
# 检查链接脚本是否存在(LDSCRIPT 为空或指向不存在的文件时报错)
prepare1: prepare2 $(version_h) $(timestamp_h) \
include/config/auto.conf
ifeq ($(wildcard $(LDSCRIPT)),)
@echo >&2 " Could not find linker script."
@/bin/false
endif
# archprepare: 编译架构相关的准备工具
# tools 目标会编译如 mkimage、imxdownload 等镜像打包工具
archprepare: prepare1 scripts_basic tools
# prepare0: 处理根目录的 Kbuild 文件
# $(Q)$(MAKE) $(build)=. 会读取根目录的 Kbuild 文件
# 用于生成根目录级的头文件或配置
prepare0: archprepare FORCE
$(Q)$(MAKE) $(build)=.
# 最终的 prepare 目标——所有子目录编译前的"总集结点"
prepare: prepare0
# filechk_version.h: 生成版本信息头文件
# 输出示例:
# #define PLAIN_VERSION "2025.04-rc3-00001-gabcdef1234"
# #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
# #define U_BOOT_VERSION_NUM 2025
# #define U_BOOT_VERSION_NUM_PATCH 4 ← 注意去除前导零!
# #define HOST_ARCH 2 ← HOST_ARCH_X86_64 的数值
# #define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (Ubuntu 11.3.0) 11.3.0"
# #define LD_VERSION_STRING "GNU ld (GNU Binutils) 2.38"
#
# sed "s/^0*//": 去除 PATCHLEVEL 的前导零
# "04" → "4"
# 这是必要的,因为 C 预处理器会将 "04" 视为八进制数
define filechk_version.h
(echo \#define PLAIN_VERSION \"$(UBOOTRELEASE)\"; \
echo \#define U_BOOT_VERSION \"U-Boot \" PLAIN_VERSION; \
echo \#define U_BOOT_VERSION_NUM $(VERSION); \
echo \#define U_BOOT_VERSION_NUM_PATCH $$(echo $(PATCHLEVEL) | \
sed -e "s/^0*//"); \
echo \#define HOST_ARCH $(HOST_ARCH); \
echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef
# filechk_timestamp.h: 生成构建时间戳头文件
# 支持 SOURCE_DATE_EPOCH 环境变量实现可复现构建(Reproducible Build):
# 如果设置了 SOURCE_DATE_EPOCH(Unix 时间戳),使用该固定时间
# 否则使用当前时间
#
# 可复现构建的意义:
# 相同的源码 + 相同的工具链 → 二进制级别相同的输出
# 对安全审计和供应链验证至关重要
#
# GNU date vs BSD date 兼容性:
# GNU date 支持 -d "@timestamp" 格式
# BSD date (macOS) 使用不同的语法
# 脚本尝试 gdate、date.gnu、date 三种命令名
#
# 输出示例:
# #define U_BOOT_DATE "Jan 15 2025"
# #define U_BOOT_TIME "10:30:45"
# #define U_BOOT_TZ "+0800"
# #define U_BOOT_EPOCH 1736930445
define filechk_timestamp.h
(if test -n "$${SOURCE_DATE_EPOCH}"; then \
SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
DATE=""; \
for date in gdate date.gnu date; do \
$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
done; \
if test -n "$${DATE}"; then \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TIME "%T"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_EPOCH %s'; \
else \
return 42; \
fi; \
else \
LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; \
LC_ALL=C date +'#define U_BOOT_TIME "%T"'; \
LC_ALL=C date +'#define U_BOOT_TZ "%z"'; \
LC_ALL=C date +'#define U_BOOT_EPOCH %s'; \
fi)
endef
# version_h 和 timestamp_h 的生成规则
# filechk 宏确保只在内容真正改变时才更新文件
# 这避免了因时间戳变化导致的不必要重编译链
$(version_h): include/config/uboot.release FORCE
$(call filechk,version.h)
# timestamp_h 依赖顶层 Makefile(因为 VERSION 等变量在此定义)
$(timestamp_h): $(srctree)/Makefile FORCE
$(call filechk,timestamp.h)
# 工具编译
# tools 目标:编译构建过程中使用的宿主机工具
# tools_imxdownload: NXP i.MX 系列处理器的镜像下载工具
# 在实际 U-Boot 中还包括 mkimage、mkenvimage 等
PHONY += tools
tools: tools_imxdownload
# imxdownload 工具编译
PHONY += tools_imxdownload
tools_imxdownload: scripts_basic
$(Q)$(MAKE) $(build)=tools
# DTC(设备树编译器)构建
# 场景 1: 使用内建 DTC,编译 scripts/dtc/ 下的源码
#
# 场景 2: 使用外部 DTC,检查版本是否满足最低要求,
# 如果启用了 CONFIG_PYLIBFDT,检查 Python 的 libfdt 绑定
#
# pylibfdt 是设备树的 Python 绑定库,U-Boot 的 binman 工具使用它来操作设备树
PHONY += scripts_dtc
scripts_dtc: scripts_basic
$(Q)if test "$(DTC)" = "$(DTC_INTREE)"; then \
$(MAKE) $(build)=scripts/dtc; \
else \
if ! $(DTC) -v > /dev/null; then \
echo '*** Failed to check dtc version: $(DTC)'; \
false; \
else \
if test "$(call dtc-version)" -lt $(DTC_MIN_VERSION); then \
echo '*** Your dtc is too old, please upgrade to dtc $(DTC_MIN_VERSION) or newer'; \
false; \
else \
if [ -n "$(CONFIG_PYLIBFDT)" ]; then \
if ! echo "import libfdt" | $(PYTHON3) 2>/dev/null; then \
echo '*** pylibfdt does not seem to be available with $(PYTHON3)'; \
false; \
fi; \
fi; \
fi; \
fi; \
fi
# 链接脚本预处理
# U-Boot 的链接脚本(.lds)需要 C 预处理器处理
# 原因:链接脚本中使用了 #include、#ifdef、CONFIG_xxx 等
# 标志说明:
# -Wp,-MD,$(depfile): 生成依赖文件(用于增量编译)
# $(cpp_flags): C 预处理标志(包含路径、宏定义等)
# $(LDPPFLAGS): 链接脚本专用标志(CPUDIR、LD 版本等)
# -D__ASSEMBLY__: 告知头文件这不是 C 编译
# -x assembler-with-cpp: 告知 GCC 输入是需要预处理的汇编/脚本
# -std=c99: 使用 C99 预处理器标准
# -P: 不生成行号标记(#line),链接器不识别它们
quiet_cmd_cpp_lds = LDS $@
cmd_cpp_lds = $(CPP) -Wp,-MD,$(depfile) $(cpp_flags) $(LDPPFLAGS) \
-D__ASSEMBLY__ -x assembler-with-cpp -std=c99 -P -o $@ $<
# u-boot.lds 生成规则
# if_changed_dep: 同 if_changed,但额外处理依赖文件(.d to .cmd 转换)
u-boot.lds: $(LDSCRIPT) prepare FORCE
$(call if_changed_dep,cpp_lds)
清理目标
清理系统实现三级渐进式清理策略,每一级都在前一级的基础上删除更多文件:
- make clean
目的:删除大部分编译产物,保留足以支持外部模块编译的最小文件集
保留:配置文件 (.config)、Kconfig 生成的头文件、部分工具
删除:目标文件 (.o)、可执行文件 (u-boot*)、依赖文件 (.d/.cmd)
使用场景:快速重新编译,无需重新配置
- make mrproper
目的:删除所有生成的文件,包括配置,恢复到刚解压源码的状态
删除:clean 删除的所有内容 + .config + auto.conf + tags 文件
使用场景:切换板卡配置、提交代码到版本控制前
- make distclean (distribution clean)
目的:删除 mrproper 的内容 + 开发工具残留文件
删除:mrproper 删除的内容 + 编辑器备份文件 + patch 残留
使用场景:打包发布前的最终清理、清理 Git 未跟踪的垃圾文件
# Directories & files removed with 'make clean'
CLEAN_DIRS +=
CLEAN_FILES += include/autoconf.mk* include/config.h u-boot* System.map \
defconfig
# Directories & files removed with 'make mrproper'
MRPROPER_DIRS += include/config include/generated include/asm
# 需要精确删除的文件(包含配置文件)
MRPROPER_FILES += .config .config.old include/autoconf.mk* include/config.h
# GNU Make 的"目标特定变量"特性:
# clean: rm-dirs := $(CLEAN_DIRS)
# 等价于:
# 在执行 clean 目标时,临时设置 rm-dirs = $(CLEAN_DIRS)
# 执行完毕后,rm-dirs 恢复原值(如果有)rm-dirs 只在 clean 目标及其依赖中有效
clean: rm-dirs := $(CLEAN_DIRS)
clean: rm-files := $(CLEAN_FILES)
# 收集需要递归清理的子目录
# u-boot-alldirs包含所有 libs-y 和 libs- 的目录
# 逻辑:
# 1) 遍历 u-boot-alldirs(所有源码目录,包括被禁用的)
# 2) 检查每个目录是否存在 Makefile($(wildcard $(srctree)/$f/Makefile))
# 3) 只有存在 Makefile 的目录才需要递归清理
clean-dirs := $(foreach f,$(u-boot-alldirs),$(if $(wildcard $(srctree)/$f/Makefile),$f))
# 为每个目录添加 _clean_ 前缀,构造伪目标名称
clean-dirs := $(addprefix _clean_, $(clean-dirs))
PHONY += $(clean-dirs) clean archclean
# 递归清理子目录的规则(模式规则)
# -----------------------------------------------------------------------
# $(clean-dirs) 包含 _clean_common, _clean_drivers 等
# 对每个 _clean_xxx 目标:
# 1) $(patsubst _clean_%,%,$@) 提取目录名(_clean_common → common)
# 2) $(clean) 在 Kbuild.include 中定义为:
# clean := -f $(srctree)/scripts/Makefile.clean obj
# 3) 展开后的命令:
# $(Q)make -f scripts/Makefile.clean obj=common
# 4) Makefile.clean 会:
# - 读取 common/Makefile
# - 删除 obj-y 中所有编译产物(.o, .cmd, .d 等)
# - 删除 clean-files 指定的文件
# - 递归进入 subdir-y 子目录
$(clean-dirs):
$(Q)$(MAKE) $(clean)=$(patsubst _clean_%,%,$@)
clean: $(clean-dirs)
$(call cmd,rmdirs)
$(call cmd,rmfiles)
@find . $(RCS_FIND_IGNORE) \
\( -name '*.[oas]' -o -name '.*.cmd' \
-o -name '.*.d' -o -name '.*.tmp' \
-o -name '*.lex.c' -o -name '*.tab.[ch]' \
-o -name 'generated_defconfig' \
-o -name '*.efi' -o -name '*.gcno' -o -name '*.so' \) \
-type f -print | xargs rm -f
# mrproper - Delete all generated files, including .config
#
mrproper: rm-dirs := $(wildcard $(MRPROPER_DIRS))
mrproper: rm-files := $(wildcard $(MRPROPER_FILES))
mrproper-dirs := $(addprefix _mrproper_,scripts)
PHONY += $(mrproper-dirs) mrproper archmrproper
$(mrproper-dirs):
$(Q)$(MAKE) $(clean)=$(patsubst _mrproper_%,%,$@)
mrproper: clean $(mrproper-dirs)
$(call cmd,rmdirs)
$(call cmd,rmfiles)
@rm -f arch/*/include/asm/arch
# distclean
#
PHONY += distclean
distclean: mrproper
@find $(srctree) $(RCS_FIND_IGNORE) \
\( -name '*.orig' -o -name '*.rej' -o -name '*~' \
-o -name '*.bak' -o -name '#*#' -o -name '.*.orig' \
-o -name '.*.rej' -o -name '*%' -o -name 'core' \
-o -name '*.pyc' \) \
-type f -print | xargs rm -f
quiet_cmd_rmdirs = $(if $(wildcard $(rm-dirs)),CLEAN $(wildcard $(rm-dirs)))
cmd_rmdirs = rm -rf $(rm-dirs)
quiet_cmd_rmfiles = $(if $(wildcard $(rm-files)),CLEAN $(wildcard $(rm-files)))
cmd_rmfiles = rm -f $(rm-files)
# .cmd 文件是 Kbuild 增量编译系统的核心
# 每次编译命令执行后,if_changed 宏会生成对应的 .cmd 文件
# 下次 make 时:
# 1) include .main.o.cmd,加载上次的命令和依赖
# 2) if_changed 比较当前命令与 cmd_main.o
# 3) 如果命令改变(如 CFLAGS 变了),重新编译
# 4) 如果依赖文件改变,重新编译
# 5) 都没变,跳过编译
# 找到所有 .cmd 文件
cmd_files := $(wildcard .*.cmd)
ifneq ($(cmd_files),)
# 空规则防止 make 尝试重建 .cmd 文件
$(cmd_files): ;
# 包含所有 .cmd 文件,加载历史命令和依赖信息
include $(cmd_files)
endif
endif #ifeq ($(config-targets),1)
endif #ifeq ($(mixed-targets),1)
endif # skip-makefile
# FORCE 目标:Kbuild 的"万能依赖"
# 它永远被视为"已更新"(因为没有规则来创建它,也不是文件)
# 任何依赖 FORCE 的目标都会被重新评估(但不一定重新执行)
#
# 与 .PHONY 的区别:
# .PHONY 目标总是执行其命令
# 依赖 FORCE 的目标只在命令或依赖改变时执行(通过 if_changed 检查)
#
# 这是一个关键的优化:
# 例如 u-boot.bin 依赖 FORCE,但如果 u-boot 没有改变,if_changed 会跳过 objcopy,避免不必要的工作
PHONY += FORCE
FORCE:
# 统一声明所有伪目标
# 将 PHONY 变量中收集的所有目标名声明为 .PHONY
# 这比在每个规则处单独声明更清晰、更不易遗漏
.PHONY: $(PHONY)
4. Kbuild递归构建Makefile.build
Makefile.build — Kbuild 递归构建系统的核心引擎。
调用入口(来自顶层 Makefile 或上级 Makefile.build):
$(Q)$(MAKE) $(build)=<dir>
展开为:
@make -f scripts/Makefile.build obj=<dir>
其中 obj=
例如:obj=lib, obj=drivers/gpio, obj=arch/arm/cpu
# 检查是否有 vpl/ 前缀
# patsubst 尝试从 obj 中去掉 vpl/ 前缀
# 如果 obj=vpl/lib,则 src=lib(后续条件不相等,则有vpl前缀)
# 如果 obj=lib,则 src=lib(无变化,说明没有 vpl/ 前缀,继续比较tpl,spl)
prefix := vpl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
# obj 与 src 相同,说明没有 vpl/ 前缀,继续尝试 tpl/
prefix := tpl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
# 也没有 tpl/ 前缀,继续尝试 spl/
prefix := spl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
# 三个前缀都不匹配,这是主 U-Boot 构建
# prefix 设为 ".",表示当前目录(无前缀)
prefix := .
endif
endif
endif
# __build 是 Makefile.build 的默认目标
# 当执行 make -f Makefile.build obj=xxx 时,如果不指定目标,
# 就会执行 __build
#
# 此处先声明为空,后面会用完整依赖覆盖
# PHONY 声明确保每次都重新评估
PHONY := __build
__build:
# --- 编译目标变量 ---
obj-y := # 需要编译并链接到最终二进制的目标列表
# 子目录 Makefile 中填充:
# obj-y += foo.o → 编译 foo.c 为 foo.o
# obj-$(CONFIG_BAR) += bar.o → 条件编译
# obj-y += subdir/ → 递归进入 subdir/
lib-y := # 编译为库文件的目标(与 obj-y 类似但语义不同)
# 通常用于 lib/ 目录下的通用库函数
targets := # 所有编译目标的完整列表
# 用于 .cmd 文件的查找和加载
# 包括 obj-y + extra-y + always + lib-y
subdir-y := # 需要递归进入的子目录列表
# 由 Makefile.lib 从 obj-y 中提取
# obj-y += subdir/ → subdir-y += subdir
# --- 编译标志变量 ---
EXTRA_AFLAGS := # 额外的汇编器标志(旧式接口,建议用 asflags-y)
EXTRA_CFLAGS := # 额外的 C 编译器标志(旧式接口,建议用 ccflags-y)
EXTRA_LDFLAGS := # 额外的链接器标志
asflags-y := # 当前目录的汇编标志(新式接口)
ccflags-y := # 当前目录的 C 编译标志(新式接口)
ldflags-y := # 当前目录的链接标志
# --- 子目录继承标志 ---
subdir-asflags-y := # 传递给子目录的汇编标志
subdir-ccflags-y := # 传递给子目录的 C 编译标志
# 这些标志会向下传递到所有子目录
# 用于在父目录统一设置编译选项
# 加载 Kconfig 生成的配置,使得子目录 Makefile 中的,obj-$(CONFIG_FOO) 条件编译生效
# include/config/auto.conf 包含所有 CONFIG_xxx=y 的定义
# -include: 文件不存在时不报错(首次构建时尚未生成)
-include include/config/auto.conf
-include $(prefix)/include/autoconf.mk
# Kbuild.include 定义了核心宏和函数
include scripts/Kbuild.include
# $(src) 可能是相对路径(如 lib)或绝对路径(如 /opt/u-boot/lib)
# 如果 src 以 / 开头,绝对路径,直接使用
# 否则 → 相对于 $(srctree) 的路径,需要拼接
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
# 选择 Kbuild 文件或 Makefile,查找优先级:
# 1. $(kbuild-dir)/Kbuild ← 优先(Kbuild 专用文件)
# 2. $(kbuild-dir)/Makefile ← 降级(传统 Makefile)
# U-Boot 中,大部分目录仍使用 Makefile
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)
PLATFORM_CPPFLAGS 是架构 Makefile(如 arch/arm/Makefile)定义的平台特定预处理器标志
asflags-y += $(PLATFORM_CPPFLAGS)
ccflags-y += $(PLATFORM_CPPFLAGS)
cppflags-y += $(PLATFORM_CPPFLAGS)
# Makefile.lib 是 Kbuild 的"中间层",负责:
# 1. 从 obj-y 中分离出子目录和文件
# 2. 为目标添加路径前缀
# 3. 计算最终编译标志
# 4. 处理条件编译
include scripts/Makefile.lib
# 宿主机程序编译支持。条件包含:只有定义了 hostprogs-y 才加载
# 避免不必要的规则加载和潜在冲突
ifneq ($(hostprogs-y),)
include scripts/Makefile.host
endif
# 确保编译产物的输出目录存在
# shell 命令在 Makefile 解析阶段执行(不是在规则执行阶段)
# 使用 _dummy 变量承接 shell 的返回值(实际不使用该值)
_dummy := $(shell [ -d $(obj) ] || mkdir -p $(obj))
# 当 Makefile 中使用 obj-y := dir/file.o 语法时
# 目标文件 dir/file.o 的目录 dir/ 可能不存在
# $(obj-dirs) 由 Makefile.lib 计算,包含所有需要创建的子目录
_dummy := $(foreach d,$(obj-dirs), $(shell [ -d $(d) ] || mkdir -p $(d)))
# 确保 obj 变量已正确传入
# 如果有人直接执行 make -f Makefile.build 而不传 obj=,会报警
# 这是一个防御性检查,帮助开发者发现错误用法
ifndef obj
$(warning kbuild: Makefile.build is included improperly)
endif
# built-in.o 是整个目录的编译产物集合:
# lib/built-in.o 包含 lib/ 下所有 obj-y 的 .o 文件,它最终参与 u-boot 的链接
ifneq ($(strip $(obj-y) $(obj-)),)
builtin-target := $(obj)/built-in.o
endif
# __build 本身不执行任何操作,所有工作由其依赖的规则完成
__build: $(builtin-target) $(extra-y) $(subdir-ym) $(always)
@:
# quiet_modtag:模块标签(U-Boot 不支持模块,此处为空)
# 在 Linux 内核中:quiet_modtag = [M](标识模块编译)
# 在 U-Boot 中:空字符串 + 两个空格的间距对齐
quiet_modtag := $(empty) $(empty)
# quiet_cmd_cc_s_c:V=0 时的简短输出
# 示例输出:" CC lib/string.s"
# cmd_cc_s_c:实际命令
# $(CC):交叉编译器(如 arm-linux-gnueabihf-gcc)
# $(c_flags):完整的 C 编译标志(由 Makefile.lib 计算)
# $(DISABLE_LTO):禁用 LTO(生成汇编时不应做链接时优化)
# -fverbose-asm:在汇编输出中添加 C 源码注释
# -S:只编译到汇编阶段,不生成 .o
quiet_cmd_cc_s_c = CC $(quiet_modtag) $@
cmd_cc_s_c = $(CC) $(c_flags) $(DISABLE_LTO) -fverbose-asm -S -o $@ $<
# $(obj)/%.s:目标文件(如 lib/string.s)
# $(src)/%.c:源文件(如 lib/string.c)
# FORCE:强制检查是否需要重新生成
# if_changed_dep:增量编译 + 依赖文件处理(通过 fixdep)
$(obj)/%.s: $(src)/%.c FORCE
$(call if_changed_dep,cc_s_c)
# .c → .i(生成预处理后的 C 代码,用于调试宏展开)
quiet_cmd_cc_i_c = CPP $(quiet_modtag) $@
cmd_cc_i_c = $(CPP) $(c_flags) -o $@ $<
$(obj)/%.i: $(src)/%.c FORCE
$(call if_changed_dep,cc_i_c)
# quiet_cmd_cc_o_c:V=0 时输出 " CC lib/string.o"
# cmd_cc_o_c:实际编译命令
# $(CC):交叉编译器
# $(c_flags):完整编译标志(来自 Makefile.lib)
# = KBUILD_CFLAGS (全局标志:-Wall -Os 等)
# + ccflags-y (目录级标志:子目录 Makefile 定义)
# + CFLAGS_$(@F) (文件级标志:如 CFLAGS_string.o = -O2)
# + NOSTDINC_FLAGS (-nostdinc -isystem ...)
# + UBOOTINCLUDE (-Iinclude -Iarch/arm/include ...)
# -c:只编译不链接
# -o $@:输出文件
# $<:输入文件(第一个依赖,即 .c 文件)
quiet_cmd_cc_o_c = CC $(quiet_modtag) $@
cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<
define rule_cc_o_c
$(call echo-cmd,cc_o_c) $(cmd_cc_o_c); \
scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' > \
$(dot-target).tmp; \
rm -f $(depfile); \
mv -f $(dot-target).tmp $(dot-target).cmd
endef
# .c → .o 的模式规则
# 每个 .c 文件都通过此规则编译为 .o
# $(call if_changed_rule,cc_o_c):
# 与 if_changed 类似,但执行 rule_cc_o_c 而非 cmd_cc_o_c
# 检查命令和依赖是否改变,仅在需要时重新编译
$(obj)/%.o: $(src)/%.c FORCE
$(call if_changed_rule,cc_o_c)
# --- .S → .s(汇编文件预处理,用于调试)---
quiet_cmd_as_s_S = CPP $(quiet_modtag) $@
cmd_as_s_S = $(CPP) $(a_flags) -o $@ $<
$(obj)/%.s: $(src)/%.S FORCE
$(call if_changed_dep,as_s_S)
# --- .S → .o(汇编编译,核心规则)---
# .S 文件包含 C 预处理指令(#include, #ifdef 等)
# $(CC)(GCC)会先调用预处理器展开这些指令,再调用汇编器
# 直接使用 $(AS) 无法处理预处理指令
quiet_cmd_as_o_S = AS $(quiet_modtag) $@
cmd_as_o_S = $(CC) $(a_flags) -c -o $@ $<
$(obj)/%.o: $(src)/%.S FORCE
$(call if_changed_dep,as_o_S)
# targets 变量记录本目录中所有可能生成的目标文件
# 用途:
# 1. 查找对应的 .cmd 文件(增量编译支持)
# 2. 确保 .cmd 文件覆盖所有编译产物
# 3. V=2 调试输出中检查目标是否在列表中
#
# real-objs-y:真实的 .o 文件(排除子目录项)
# lib-y:库文件
# extra-y:额外编译目标
# MAKECMDGOALS:用户在命令行指定的目标
# always, always-y:无条件构建的目标
targets += $(real-objs-y) $(lib-y)
targets += $(extra-y) $(MAKECMDGOALS) $(always)
targets += $(lib-y) $(always-y)
# 链接脚本中使用 C 预处理指令来实现条件编译
quiet_cmd_cpp_lds_S = LDS $@
cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -C -U$(ARCH) \
-D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<
$(obj)/%.lds: $(src)/%.lds.S FORCE
$(call if_changed_dep,cpp_lds_S)
# sort 去重,避免同一子目录被处理多次
# 空规则 ";" 表示目标由依赖自动满足,无需额外命令
$(sort $(subdir-obj-y)): $(subdir-ym) ;
# built-in.o 归档规则
ifdef builtin-target
quiet_cmd_link_o_target = AR $@
cmd_link_o_target = $(if $(strip $(obj-y)),\
rm -f $@; $(AR) cDPrsT $@ $(filter $(obj-y), $^), \
rm -f $@; $(AR) cDPrsT$(KBUILD_ARFLAGS) $@)
$(builtin-target): $(obj-y) FORCE
$(call if_changed,link_o_target)
# 将 built-in.o 加入 targets 列表,确保其 .cmd 文件被加载
targets += $(builtin-target)
endif # builtin-target
# 中间目标处理
# 某些目标文件是通过多步生成的:
# .l 文件(Lex 源码)→ .lex.c(C 代码)→ .lex.o(目标文件)
# .y 文件(Yacc 源码)→ .tab.c + .tab.h(C 代码 + 头文件)→ .tab.o
#
# intermediate_targets 函数:
# 从 targets 列表中找出最终目标(如 .lex.o),反向推导出中间目标(如 .lex.c)
define intermediate_targets
$(foreach sfx, $(2), \
$(patsubst %$(strip $(1)),%$(sfx), \
$(filter %$(strip $(1)), $(targets))))
endef
targets += $(call intermediate_targets, .lex.o, .lex.c) \
$(call intermediate_targets, .tab.o, .tab.c .tab.h)
# 对 subdir-ym 中的每个子目录,递归调用 Makefile.build
# subdir-ym 由 Makefile.lib 从 obj-y 中提取
# 顶层 Makefile → Makefile.build(lib) → Makefile.build(lib/gpio) → ...
# 每一层产生 built-in.o,上层将其归档到自己的 built-in.o 中
PHONY += $(subdir-ym)
$(subdir-ym):
$(Q)$(MAKE) $(build)=$@
PHONY += FORCE
FORCE:
cmd_files := $(wildcard $(foreach f,$(sort $(targets)),$(dir $(f)).$(notdir $(f)).cmd))
ifneq ($(cmd_files),)
include $(cmd_files)
endif
# 确保所有对象目录存在
obj-dirs := $(sort $(obj) $(patsubst %/,%, $(dir $(targets))))
$(shell mkdir -p $(obj-dirs))
.PHONY: $(PHONY)
5. Makefile.lib
Makefile.lib 负责将 Kbuild 文件中的对象列表(obj-y、lib-y 等)转换为实际的构建目标和编译标志。
# scripts/Makefile.lib
# Kbuild 核心库文件:将 Kbuild 变量转换为构建目标和编译标志
# subdir-asflags-y 和 subdir-ccflags-y 用于向下传递到子目录
# 例如:在 drivers/Makefile 中设置 subdir-ccflags-y += -DDEBUG
# 则 drivers/ 下所有子目录都会使用这个标志
#
# export 确保这些变量在递归 make 时可见
# 使用 += 追加而非覆盖,实现累积效果
export KBUILD_SUBDIR_ASFLAGS := $(KBUILD_SUBDIR_ASFLAGS) $(subdir-asflags-y)
export KBUILD_SUBDIR_CCFLAGS := $(KBUILD_SUBDIR_CCFLAGS) $(subdir-ccflags-y)
# __subdir-y: 去掉尾部 '/',得到纯目录名
# 例如:obj-y = "foo.o drivers/ bar.o gpio/"
# __subdir-y = "drivers gpio"
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
# 追加到显式声明的 subdir-y 中
# 这样 Kbuild 文件可以同时使用两种方式声明子目录:
# subdir-y += subdir1 # 方式1:显式声明
# obj-y += subdir2/ # 方式2:在 obj-y 中带 '/' 后缀
subdir-y += $(__subdir-y)
# 将 obj-y 中的子目录项替换为 built-in.o 路径
# 例如:obj-y = "foo.o drivers/ bar.o"
# 变为:obj-y = "foo.o drivers/built-in.o bar.o"
# 这样链接时直接包含子目录的归档文件
obj-y := $(patsubst %/, %/built-in.o, $(obj-y))
# 合并需要递归进入的子目录(去重并排序)
subdir-ym := $(sort $(subdir-y))
# Kbuild 支持"复合对象":一个 .o 文件由多个源文件组成
# suffix-search: 搜索带指定后缀的变量
# 参数:$1 = 目标名(如 foo.o)
# $2 = 要替换的后缀(如 .o)
# $3 = 要搜索的后缀列表(如 "-objs -y")
# 示例:$(call suffix-search, foo.o, .o, -objs -y)
# 展开为:$(foo-objs) $(foo-y)
suffix-search = $(strip $(foreach s, $3, $($(1:%$(strip $2)=%$s))))
# multi-search: 找出所有复合对象
# 返回 obj-y 中那些有 -objs 或 -y 定义的目标
# 例如:obj-y = "foo.o bar.o",foo-objs 有定义,bar-objs 无定义
# 返回:foo.o
multi-search = $(sort $(foreach m, $1, $(if $(call suffix-search, $m, $2, $3 -), $m)))
# real-search: 展开复合对象为其组成部分
# 如果目标有 -objs/-y 定义,返回其内容;否则返回目标本身
# 例如:obj-y = "foo.o bar.o",foo-objs = "a.o b.o"
# 返回:a.o b.o bar.o
real-search = $(foreach m, $1, $(if $(call suffix-search, $m, $2, $3 -), $(call suffix-search, $m, $2, $3), $m))
# subdir-obj-y: 从 obj-y 中提取子目录的 built-in.o
# 这些不是真正要编译的文件,而是要链接的归档文件
subdir-obj-y := $(filter %/built-in.o, $(obj-y))
# real-objs-y: 真正需要编译的对象文件列表
# 处理逻辑:
# 1) 从 obj-y 中排除子目录项(built-in.o)
# 2) 对每个目标,检查是否有 -objs 或 -y 定义
# 3) 如果有,展开为组成部分;否则保留原样
# 4) 追加 extra-y 中的目标
real-objs-y := $(foreach m, $(filter-out $(subdir-obj-y), $(obj-y)), \
$(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))),$($(m:.o=-objs)) \
$($(m:.o=-y)),$(m))) $(extra-y)
# 将所有目标名前加上 $(obj)/ 目录前缀
# 这是实现外部构建(O=builddir)的关键:
# 源文件在 $(src)/,对象文件生成到 $(obj)/
extra-y := $(addprefix $(obj)/,$(extra-y))
always-y := $(addprefix $(obj)/,$(always-y))
always := $(addprefix $(obj)/,$(always))
targets := $(addprefix $(obj)/,$(targets))
obj-y := $(addprefix $(obj)/,$(obj-y))
lib-y := $(addprefix $(obj)/,$(lib-y))
subdir-obj-y := $(addprefix $(obj)/,$(subdir-obj-y))
real-objs-y := $(addprefix $(obj)/,$(real-objs-y))
subdir-ym := $(addprefix $(obj)/,$(subdir-ym))
# name-fix: 将文件名转换为合法的 C 标识符
# 替换 '-' 和 ',' 为 '_',并用引号包裹
# squote/quote 在 Kbuild.include 中定义:squote=' quote="
name-fix = $(squote)$(quote)$(subst $(comma),_,$(subst -,_,$1))$(quote)$(squote)
# basename_flags: 定义 KBUILD_BASENAME 宏
# 每个源文件编译时都定义这个宏,值为文件的基本名
# 用于调试输出、日志等场景
# 例如:编译 drivers/gpio/mxc_gpio.c 时-DKBUILD_BASENAME='"mxc_gpio"'
basename_flags = -DKBUILD_BASENAME=$(call name-fix,$(basetarget))
# C 编译标志组合
orig_c_flags = $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) $(KBUILD_SUBDIR_CCFLAGS) \
$(ccflags-y) $(CFLAGS_$(basetarget).o)
# _c_flags: 过滤掉需要移除的标志
# CFLAGS_REMOVE_xxx.o 允许针对特定文件移除某些标志
# 例如:CFLAGS_REMOVE_broken.o += -Werror
_c_flags = $(filter-out $(CFLAGS_REMOVE_$(basetarget).o), $(orig_c_flags))
# 汇编标志组合
orig_a_flags = $(KBUILD_CPPFLAGS) $(KBUILD_AFLAGS) $(KBUILD_SUBDIR_ASFLAGS) \
$(asflags-y) $(AFLAGS_$(basetarget).o)
_a_flags = $(filter-out $(AFLAGS_REMOVE_$(basetarget).o), $(orig_a_flags))
# 预处理标志
_cpp_flags = $(KBUILD_CPPFLAGS) $(cppflags-y) $(CPPFLAGS_$(@F))
# 外部构建支持
# 当使用 O=builddir 外部构建时,需要调整 -I 路径
# 源文件中的 #include "foo.h" 需要找到 $(srctree)/dir/foo.h
ifeq ($(KBUILD_SRC),)
# 源码内构建:直接使用标志
__c_flags = $(_c_flags)
__a_flags = $(_a_flags)
__cpp_flags = $(_cpp_flags)
else
# 外部构建:添加源码树路径
# -I$(obj): 定位生成的 .h 文件(在输出目录)
# $(call addtree,-I$(src)): 将相对路径转换为 $(srctree)/... 形式定位源码中的 .h 文件
# $(call flags,_c_flags): 处理 _c_flags 中的所有 -I 选项
__c_flags = $(if $(obj),$(call addtree,-I$(src)) -I$(obj)) \
$(call flags,_c_flags)
__a_flags = $(call flags,_a_flags)
__cpp_flags = $(call flags,_cpp_flags)
endif
# 最终编译标志(传给编译器的完整命令行)
c_flags = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(UBOOTINCLUDE) \
$(__c_flags) $(modkern_cflags) \
$(basename_flags)
a_flags = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(UBOOTINCLUDE) \
$(__a_flags)
cpp_flags = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(UBOOTINCLUDE) \
$(__cpp_flags)
ld_flags = $(KBUILD_LDFLAGS) $(ldflags-y) $(LDFLAGS_$(@F))
# multi_depend: 为复合对象生成依赖规则
define multi_depend
$(foreach m, $(notdir $1), \
$(eval $(obj)/$m: \
$(addprefix $(obj)/, $(foreach s, $3, $($(m:%$(strip $2)=%$(s)))))))
endef
# LEX 词法分析器生成
# .l 文件 → .lex.c 文件
# 用于 Kconfig 解析器(scripts/kconfig/zconf.l)
quiet_cmd_flex = LEX $@
cmd_flex = $(LEX) -o$@ -L $<
# -o$@: 输出到目标文件
# -L: 不生成 #line 指令(简化输出)
$(obj)/%.lex.c: $(src)/%.l FORCE
$(call if_changed,flex)
# YACC 语法分析器生成
# .y 文件 → .tab.c 和 .tab.h 文件
# 用于 Kconfig 解析器(scripts/kconfig/zconf.y)
# 生成 .tab.c(解析器实现)
quiet_cmd_bison = YACC $@
cmd_bison = $(YACC) -o$@ -t -l $<
# -o$@: 输出到目标文件
# -t: 启用调试(定义 YYDEBUG)
# -l: 不生成 #line 指令
$(obj)/%.tab.c: $(src)/%.y FORCE
$(call if_changed,bison)
# 生成 .tab.h(解析器头文件,供词法分析器使用)
quiet_cmd_bison_h = YACC $@
cmd_bison_h = $(YACC) -o/dev/null --defines=$@ -t -l $<
# -o/dev/null: 丢弃 .c 输出(只要头文件)
# --defines=$@: 生成头文件到 $@
$(obj)/%.tab.h: $(src)/%.y FORCE
$(call if_changed,bison_h)
# ASM 偏移量生成(用于汇编访问 C 结构体)
# 用途:让汇编代码可以使用 C 结构体的偏移量
# 例如:offsetof(struct pt_regs, ARM_pc) → #define ARM_pc 60
# sed 正则表达式:提取 ->xxx 格式的行并转换为 #define
define sed-offsets
"s:[[:space:]]*\.ascii[[:space:]]*\"\(.*\)\":\1:; \
/^->/{s:->#\(.*\):/* \1 */:; \
s:^->\([^ ]*\) [\$$#]*\([-0-9]*\) \(.*\):#define \1 \2 /* \3 */:; \
s:^->\([^ ]*\) [\$$#]*\([^ ]*\) \(.*\):#define \1 \2 /* \3 */:; \
s:->::; p;}"
endef
# filechk_offsets: 生成完整的头文件
# $2 是头文件保护宏名称
define filechk_offsets
(set -e; \
echo "#ifndef $2"; \
echo "#define $2"; \
echo "/*"; \
echo " * DO NOT MODIFY."; \
echo " *"; \
echo " * This file was generated by Kbuild"; \
echo " */"; \
echo ""; \
sed -ne $(sed-offsets); \
echo ""; \
echo "#endif" )
endef
6. Kbuild.include
Kbuild.include 是Kbuild 的通用定义文件。
# 基础字符常量
comma := ,
quote := "
squote := '
empty :=
space := $(empty) $(empty)
space_escape := _-_SPACE_-_
pound := \#
# 路径和文件名处理
# dot-target: 目标文件的隐藏版本路径
# foo/bar.o => foo/.bar.o
# 用于生成 .cmd 文件路径
dot-target = $(dir $@).$(notdir $@)
# depfile: 依赖文件路径(替换逗号避免问题)
# 用于 -Wp,-MD,$(depfile)
depfile = $(subst $(comma),_,$(dot-target).d)
# basetarget: 不带目录和扩展名的目标名
# 用于 CFLAGS_$(basetarget).o 等
basetarget = $(basename $(notdir $@))
# escsq: 转义单引号,用于 echo 语句
escsq = $(subst $(squote),'\$(squote)',$1)
# 状态消息输出
kecho := :
quiet_kecho := echo
silent_kecho := :
kecho := $($(quiet)kecho)
# filechk 函数 【保留 - 用于生成 version.h、timestamp.h、uboot.release】
# =========================================================================
# 检查生成文件内容是否需要更新
# 只有内容真正改变时才更新文件,避免不必要的重编译
define filechk
$(Q)set -e; \
mkdir -p $(dir $@); \
$(filechk_$(1)) < $< > $@.tmp; \
if [ -r $@ ] && cmp -s $@ $@.tmp; then \
rm -f $@.tmp; \
else \
$(kecho) ' UPD $@'; \
mv -f $@.tmp $@; \
fi
endef
# 编译器选项探测函数
# 输出目录(用于临时文件)
TMPOUT :=
# try-run: 尝试运行命令,根据成功/失败返回不同值
# 用于探测编译器是否支持某选项
try-run = $(shell set -e; \
TMP="$(TMPOUT).$$$$.tmp"; \
TMPO="$(TMPOUT).$$$$.o"; \
TMPSU="$(TMPOUT).$$$$.su"; \
if ($(1)) >/dev/null 2>&1; \
then echo "$(2)"; \
else echo "$(3)"; \
fi; \
rm -f "$$TMP" "$$TMPO" "$$TMPSU")
# as-option: 检测汇编器选项
as-option = $(call try-run,\
$(CC) $(KBUILD_CFLAGS) $(1) -c -x assembler /dev/null -o "$$TMP",$(1),$(2))
# __cc-option: 通用编译器选项检测
__cc-option = $(call try-run,\
$(1) -Werror $(2) $(3) -c -x c /dev/null -o "$$TMP",$(3),$(4))
# cc-option: 检测 C 编译器选项
cc-option = $(call __cc-option, $(CC),\
$(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS),$(1),$(2))
# cc-name: 获取编译器名称(gcc 或 clang)
cc-name = $(shell $(CC) -v 2>&1 | grep -q "clang version" && echo clang || echo gcc)
# cc-version: 获取编译器版本
cc-version = $(shell $(CONFIG_SHELL) $(srctree)/scripts/gcc-version.sh $(CC))
# added for U-Boot
binutils-version = $(shell $(CONFIG_SHELL) $(srctree)/scripts/binutils-version.sh $(AS))
dtc-version = $(shell $(CONFIG_SHELL) $(srctree)/scripts/dtc-version.sh $(DTC))
# build: 递归构建简写
# 用法: $(Q)$(MAKE) $(build)=dir
# 展开: @make -f scripts/Makefile.build obj=dir
build := -f $(srctree)/scripts/Makefile.build obj
###
# clean: 递归清理简写
# 用法: $(Q)$(MAKE) $(clean)=dir
# 展开: @make -f scripts/Makefile.clean obj=dir
clean := -f $(srctree)/scripts/Makefile.clean obj
# addtree: 将相对路径的 -I 选项加上 $(srctree) 前缀
# 用于外部构建时定位源码树中的头文件
# 例如: -Iinclude -> -I$(srctree)/include
# 但 -I/usr/include 保持不变(绝对路径)
addtree = $(if $(patsubst -I%,%,$(1)), \
$(if $(filter-out -I/% -I./% -I../%,$(1)),$(patsubst -I%,-I$(srctree)/%,$(1)),$(1)),$(1))
# flags: 处理变量中所有 -I 选项
# 对每个 -I 选项调用 addtree,其他选项保持不变
flags = $(foreach o,$($(1)),$(if $(filter -I%,$(o)),$(call addtree,$(o)),$(o)))
# echo-cmd: 根据 $(quiet) 变量选择输出格式
# quiet_ 前缀: 简短输出(如 "CC foo.o")
# 无前缀: 完整命令输出
# silent_ 前缀: 无输出
echo-cmd = $(if $($(quiet)cmd_$(1)),\
echo ' $(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)
# cmd: 执行命令的简写
# @$(echo-cmd): 先输出命令描述(根据 V= 设置)
# $(cmd_$(1)): 再执行实际命令
cmd = @$(echo-cmd) $(cmd_$(1))
# arg-check: 检查命令是否改变
# 比较上次保存的命令 (cmd_$@) 与当前命令 (cmd_$1)
# 空字符串表示相同,非空表示有变化
ifneq ($(KBUILD_NOCMDDEP),1)
# 正常模式:完整比较命令(包括参数顺序)
arg-check = $(filter-out $(subst $(space),$(space_escape),$(strip $(cmd_$@))), \
$(subst $(space),$(space_escape),$(strip $(cmd_$1))))
else
# KBUILD_NOCMDDEP=1: 只检查命令是否存在
arg-check = $(if $(strip $(cmd_$@)),,1)
endif
# make-cmd: 准备保存到 .cmd 文件的命令字符串
# 转义 $, #, ' 等特殊字符
make-cmd = $(call escsq,$(subst $(pound),$$(pound),$(subst $$,$$$$,$(cmd_$(1)))))
# any-prereq: 检查是否有比目标新的依赖,或依赖不存在
# $?: 比目标新的依赖列表
# $^: 所有依赖列表
# 过滤掉 PHONY 目标
any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)
# if_changed: 条件执行命令
# 当命令改变或依赖更新时才执行
# 执行后保存命令到 .cmd 文件
if_changed = $(if $(strip $(any-prereq) $(arg-check)), \
@set -e; \
$(echo-cmd) $(cmd_$(1)); \
printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)
# if_changed_dep: 条件执行 + 处理依赖文件
# 执行命令后用 fixdep 处理 .d 文件
# 用于编译 C/汇编文件
if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ), \
@set -e; \
$(echo-cmd) $(cmd_$(1)); \
scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\
rm -f $(depfile); \
mv -f $(dot-target).tmp $(dot-target).cmd, @:)
# if_changed_rule: 条件执行规则(而非单个命令)
# 用于需要多步骤的复杂规则
if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ), \
@set -e; \
$(rule_$(1)), @:)
# 调试输出(V=2 时启用)
ifeq ($(KBUILD_VERBOSE),2)
why = \
$(if $(filter $@, $(PHONY)),- due to target is PHONY, \
$(if $(wildcard $@), \
$(if $(strip $(any-prereq)),- due to: $(any-prereq), \
$(if $(arg-check), \
$(if $(cmd_$@),- due to command line change, \
$(if $(filter $@, $(targets)), \
- due to missing .cmd file, \
- due to $(notdir $@) not in $$(targets) \
) \
) \
) \
), \
- due to target missing \
) \
)
echo-why = $(call escsq, $(strip $(why)))
endif
7. Makefile.host
在宿主机系统上构建二进制工具
这些工具在编译 U-Boot/内核 过程中使用,例如:
- fixdep: 处理依赖文件
- conf/mconf: Kconfig 配置工具
- mkimage: 生成 U-Boot 镜像
- bin2hex: 数据格式转换
# host-csingle: 单文件 C 程序
# 判断逻辑:如果程序没有定义 -objs、-cxxobjs、-sharedobjs,则是单文件程序
# foreach 遍历每个程序 m:
# $($(m)-objs)$($(m)-cxxobjs)$($(m)-sharedobjs):展开该程序的所有组件定义
# $(if ...):如果都为空,则返回 $(m),否则返回空
host-csingle := $(foreach m,$(__hostprogs), \
$(if $($(m)-objs)$($(m)-cxxobjs)$($(m)-sharedobjs),,$(m)))
# host-cmulti: 多文件 C 程序(纯 C,不含 C++)
# 判断逻辑:有 -objs 定义,但没有 -cxxobjs 定义
host-cmulti := $(foreach m,$(__hostprogs),\
$(if $($(m)-cxxobjs),,$(if $($(m)-objs),$(m))))
# host-cobjs: 所有需要编译的 C 目标文件
# 收集所有程序的 -objs 定义中的 .o 文件
# sort 去重(同一个 .o 可能被多个程序共享)
host-cobjs := $(sort $(foreach m,$(__hostprogs),$($(m)-objs)))
# 添加目录前缀
# =========================================================================
# 将所有目标添加 $(obj)/ 前缀,支持外部构建
# 例如:obj=scripts/kconfig
# host-csingle = scripts/kconfig/conf
# host-cobjs = scripts/kconfig/menu.o scripts/kconfig/zconf.tab.o
host-csingle := $(addprefix $(obj)/,$(host-csingle))
host-cmulti := $(addprefix $(obj)/,$(host-cmulti))
host-cobjs := $(addprefix $(obj)/,$(host-cobjs))
# 编译标志处理
# _hostc_flags: 基础宿主机 C 编译标志
# 组成部分:
# KBUILD_HOSTCFLAGS : 全局宿主机编译标志(在顶层 Makefile 定义)
# 通常包含 -Wall -O2 等
# HOST_EXTRACFLAGS : 额外的宿主机标志(可在 Kbuild 文件中设置)
# HOSTCFLAGS_xxx.o : 特定文件的编译标志
# 例如:HOSTCFLAGS_zconf.tab.o := -I$(src)
_hostc_flags = $(KBUILD_HOSTCFLAGS) $(HOST_EXTRACFLAGS) \
$(HOSTCFLAGS_$(basetarget).o)
# 外部构建支持
ifeq ($(KBUILD_SRC),)
# 源码内构建:直接使用标志
__hostc_flags = $(_hostc_flags)
else
# 外部构建:添加 -I$(obj) 以找到生成的头文件
# flags 函数(定义在 Kbuild.include)处理 -I 路径转换
__hostc_flags = -I$(obj) $(call flags,_hostc_flags)
endif
# hostc_flags: 最终编译标志
# -Wp,-MD,$(depfile): 生成依赖文件
# -Wp: 将后续选项传给预处理器
# -MD: 生成 .d 依赖文件
# $(__hostc_flags): 上面处理过的编译标志
hostc_flags = -Wp,-MD,$(depfile) $(__hostc_flags)
# 编译规则
# 规则 1: 单文件 C 程序 (host-csingle)
# 示例展开(编译 scripts/basic/fixdep):
# cc -Wp,-MD,.fixdep.d -Wall -O2 -o scripts/basic/fixdep \
# scripts/basic/fixdep.c
quiet_cmd_host-csingle = HOSTCC $@
cmd_host-csingle = $(HOSTCC) $(hostc_flags) $(KBUILD_HOSTLDFLAGS) -o $@ $< \
$(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(@F))
# 模式规则:
# $(obj)/%: $(src)/%.c 表示:
# 目标 scripts/basic/fixdep 依赖于 scripts/basic/fixdep.c
# if_changed_dep:
# 1) 检查命令或依赖是否改变
# 2) 如果改变,执行命令并处理 .d 文件
$(host-csingle): $(obj)/%: $(src)/%.c FORCE
$(call if_changed_dep,host-csingle)
# 规则 2: 多文件 C 程序链接 (host-cmulti)
# 输入:多个 .o 文件(由 host-cobjs 规则生成)
# 输出:可执行文件
quiet_cmd_host-cmulti = HOSTLD $@
cmd_host-cmulti = $(HOSTCC) $(KBUILD_HOSTLDFLAGS) -o $@ \
$(addprefix $(obj)/,$($(@F)-objs)) \
$(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(@F))
# 因为依赖由 multi_depend 动态生成
$(host-cmulti): FORCE
$(call if_changed,host-cmulti)
$(call multi_depend, $(host-cmulti), , -objs)
# 规则 3: C 目标文件编译 (host-cobjs)
# 这是 host-cmulti 的前置步骤,先编译所有 .o 文件
quiet_cmd_host-cobjs = HOSTCC $@
cmd_host-cobjs = $(HOSTCC) $(hostc_flags) -c -o $@ $<
$(host-cobjs): $(obj)/%.o: $(src)/%.c FORCE
$(call if_changed_dep,host-cobjs)
# 将所有宿主机目标加入 targets 列表
# 用于:
# 1) clean 时清理这些文件
# 2) if_changed 的目标验证(V=2 调试信息)
targets += $(host-csingle) $(host-cmulti) $(host-cobjs)
8. Makefile.autoconf
此辅助 Makefile 用于创建:
- 符号链接 (arch/$ARCH/include/asm/arch -> arch-$(SOC))
- include/autoconf.mk 及其依赖文件
- include/config.h
在 Kconfig 引入之前,这些文件由 mkconfig 脚本生成,现在由此 Makefile 处理。这个 Makefile 是连接 Kconfig 系统和实际编译的桥梁,确保配置信息以正确的格式提供给 Makefile 和 C 代码。
# 默认目标:生成 autoconf.mk 和其依赖文件
__all: include/autoconf.mk include/autoconf.mk.dep
# 读取 Kconfig 生成的配置(CONFIG_xxx=y 等)
# 这是 syncconfig 的输出,包含所有启用的配置选项
include include/config/auto.conf
# 包含 Kbuild 通用函数(filechk、cmd 等)
include scripts/Kbuild.include
# 需要在这里重新定义 CC 和 CPP,因为:
# 1) 此 Makefile 可能被独立调用(不经过顶层 Makefile)
# 2) 某些架构在 arch/$(ARCH)/config.mk 中定义 CROSS_COMPILE
#
# CC: C 编译器
# CPP: C 预处理器(仅预处理,不编译)
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
# 包含板级配置(定义 ARCH, CPU, BOARD, VENDOR, SOC 等变量)
# 这些变量用于确定符号链接的目标路径
include config.mk
# 头文件搜索路径
# 用于编译和预处理时的 -I 选项
UBOOTINCLUDE := \
-Iinclude \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-I$(srctree)/arch/$(ARCH)/include
# 用于预处理 include/config.h 以提取 CONFIG_xxx 宏
c_flags := $(KBUILD_CFLAGS) $(KBUILD_CPPFLAGS) $(PLATFORM_CPPFLAGS) \
$(UBOOTINCLUDE) $(NOSTDINC_FLAGS)
# autoconf.mk.dep - 依赖文件生成
# 作用:跟踪 include/config.h 的所有依赖关系
# 当任何被包含的头文件改变时,触发 autoconf.mk 重新生成
# 命令解析:
# -x c : 强制作为 C 语言处理
# -DDO_DEPS_ONLY : 定义此宏(某些头文件据此跳过实际内容)
# -M : 生成 make 依赖规则(只输出依赖,不编译)
# -MP : 为每个依赖生成空规则(防止头文件删除后报错)
# -MQ target : 指定依赖规则的目标名(这里是 auto.conf)
quiet_cmd_autoconf_dep = GEN $@
cmd_autoconf_dep = $(CC) -x c -DDO_DEPS_ONLY -M -MP $(c_flags) \
-MQ include/config/auto.conf include/config.h > $@ || { \
rm $@; false; \
}
# 依赖规则:当 config.h 改变时重新生成
# FORCE 确保每次 make 都检查是否需要更新
include/autoconf.mk.dep: include/config.h FORCE
$(call cmd,autoconf_dep)
# autoconf.mk - 传统格式的配置文件
# 作用:将 C 预处理器宏格式转换为 Makefile 变量格式
# 输入(u-boot.cfg 中的内容):
# #define CONFIG_SYS_TEXT_BASE 0x87800000
# #define CONFIG_ARM y
# 输出(autoconf.mk):
# CONFIG_SYS_TEXT_BASE=0x87800000
# CONFIG_ARM=y
# 处理逻辑:
# 1) sed 使用 define2mk.sed 脚本转换格式
# 2) while 循环检查重复定义(可通过 KCONFIG_IGNORE_DUPLICATES 跳过)
# 3) 只输出在 auto.conf 中未定义的选项(避免冲突)
# 这种双重检查的原因:
# - Kconfig 生成的选项在 auto.conf 中(权威来源)
# - 头文件中的传统 #define 在 autoconf.mk 中(兼容旧代码)
# - 避免同一选项被定义两次导致的警告或错误
quiet_cmd_autoconf = GEN $@
cmd_autoconf = \
sed -n -f $(srctree)/tools/scripts/define2mk.sed $< | \
while read line; do \
if [ -n "${KCONFIG_IGNORE_DUPLICATES}" ] || \
! grep -q "$${line%=*}=" include/config/auto.conf; then \
echo "$$line"; \
fi; \
done > $@
# u-boot.cfg - 预处理后的配置宏集合
# 作用:提取 include/config.h 中所有 CONFIG_xxx 宏定义
# 命令解析:
# $(CPP) ... -dM : 预处理并输出所有宏定义(-dM = dump macros)
# grep 'define CONFIG_' : 只保留 CONFIG_ 开头的宏
# sed 过滤掉:
# - CONFIG_IS_ENABLED() : 这是宏函数,不是配置值
# - CONFIG_IF_ENABLED_INT() : 同上
# - CONFIG_VAL() : 同上
# 输出示例:
# #define CONFIG_ARM 1
# #define CONFIG_SYS_TEXT_BASE 0x87800000
# #define CONFIG_BAUDRATE 115200
quiet_cmd_u_boot_cfg = CFG $@
cmd_u_boot_cfg = \
$(CPP) $(c_flags) $2 -DDO_DEPS_ONLY -dM include/config.h > $@.tmp && { \
grep 'define CONFIG_' $@.tmp | \
sed '/define CONFIG_IS_ENABLED(/d;/define CONFIG_IF_ENABLED_INT(/d;/define CONFIG_VAL(/d;' > $@; \
rm $@.tmp; \
} || { \
rm $@.tmp; false; \
}
# u-boot.cfg 依赖于 config.h
u-boot.cfg: include/config.h FORCE
$(call cmd,u_boot_cfg)
# autoconf.mk 依赖于 u-boot.cfg(转换格式)
include/autoconf.mk: u-boot.cfg
$(call cmd,autoconf)
# include/config.h - 核心配置头文件
# 作用:C 代码的配置入口点,所有 .c/.h 文件通过它访问配置
# 生成内容:
# /* Automatically generated - do not edit */
# #define CFG_BOARDDIR board/freescale/mx6ullevk
# #include <configs/mx6ullevk.h> // 板级配置头文件
# #include <asm/config.h> // 架构配置
# #include <linux/kconfig.h> // Kconfig 生成的配置
#
# 设计说明:
# - CFG_BOARDDIR: 板级目录路径,用于定位板级特定文件
# - configs/xxx.h: 传统的板级配置(正在逐步迁移到 Kconfig)
# - asm/config.h: 架构级默认配置
# - linux/kconfig.h: 包含 auto.conf 的 C 头文件版本
define filechk_config_h
(echo "/* Automatically generated - do not edit */"; \
echo \#define CFG_BOARDDIR board/$(if $(VENDOR),$(VENDOR)/)$(BOARD);\
$(if $(CONFIG_SYS_CONFIG_NAME),echo \#include \<configs/$(CONFIG_SYS_CONFIG_NAME).h\> ;) \
echo \#include \<asm/config.h\>; \
echo \#include \<linux/kconfig.h\>;)
endef
# config.h 的依赖:
# - 此 Makefile 本身(规则改变时重新生成)
# - create_symlink(确保 asm/arch 链接存在)
# - FORCE(总是检查是否需要更新)
#
# filechk 宏(定义在 Kbuild.include)会:
# 1) 生成新内容到临时文件
# 2) 与现有文件比较
# 3) 仅在内容改变时才更新(避免不必要的重编译)
include/config.h: scripts/Makefile.autoconf create_symlink FORCE
$(call filechk,config_h)
# 架构符号链接创建
# 作用:创建 arch/$(ARCH)/include/asm/arch 符号链接
# 为什么需要这个链接?
# - 代码中使用 #include <asm/arch/xxx.h> 的统一写法
# - 实际指向 arch/arm/include/asm/arch-mx6/ 等 SoC 特定目录
# - 这样切换 SoC 时只需改变符号链接,无需修改代码
#
# 链接目标选择逻辑:
# 1) 优先检查 arch/$(ARCH)/mach-$(SOC)/include/mach/ 是否存在
# 某些架构(如 ARM 的 mach-xxx)使用这种布局
# 2) 否则使用 arch/$(ARCH)/include/asm/arch-$(SOC)/
# 这是更常见的布局(如 arch-mx6, arch-sunxi)
# 3) 如果 SOC 未定义,退化使用 CPU(如 arch-armv7)
#
# 外部构建(KBUILD_SRC)与源码内构建的区别:
# - 外部构建:链接需要指向 $(KBUILD_SRC)/arch/... 的绝对路径
# - 源码内构建:使用相对路径 ../../arch-xxx
PHONY += create_symlink
create_symlink:
# CONFIG_CREATE_ARCH_SYMLINK 控制是否创建链接
# 某些架构(如 sandbox)不需要此链接
ifdef CONFIG_CREATE_ARCH_SYMLINK
ifneq ($(KBUILD_SRC),)
# ----- 外部构建模式 -----
# 首先确保 include/asm 目录存在
$(Q)mkdir -p include/asm
# 确定链接目标并创建链接
$(Q)if [ -d $(KBUILD_SRC)/arch/$(ARCH)/mach-$(SOC)/include/mach ]; then \
dest=arch/$(ARCH)/mach-$(SOC)/include/mach; \
else \
dest=arch/$(ARCH)/include/asm/arch-$(if $(SOC),$(SOC),$(CPU)); \
fi; \
ln -fsn $(KBUILD_SRC)/$$dest include/asm/arch
# -fsn 选项:
# -f: 强制覆盖已存在的链接
# -s: 创建符号链接(而非硬链接)
# -n: 如果目标是目录的链接,不要进入它
else
# ----- 源码内构建模式 -----
# 使用相对路径创建链接
$(Q)if [ -d arch/$(ARCH)/mach-$(SOC)/include/mach ]; then \
dest=../../mach-$(SOC)/include/mach; \
else \
dest=arch-$(if $(SOC),$(SOC),$(CPU)); \
fi; \
ln -fsn $$dest arch/$(ARCH)/include/asm/arch
# 相对路径说明:
# 从 arch/arm/include/asm/arch 出发:
# ../../mach-imx/include/mach → arch/arm/mach-imx/include/mach
# arch-mx6 → arch/arm/include/asm/arch-mx6 (同级目录)
endif
endif
# 标准伪目标声明
PHONY += FORCE
FORCE:
.PHONY: $(PHONY)
9. Makefile.clean
这是清理系统,用于清理构建产物。自动发现并清理所有子目录,包括禁用配置的目录(可能之前编译过),只删除已知的构建产物,不会误删源文件。
# 功能:
# 1) 删除当前目录下的构建产物(.o, .cmd, 生成的文件等)
# 2) 递归进入子目录执行清理
# src 与 obj 相同(清理时源码目录 = 输出目录的对应关系)
src := $(obj)
# 默认目标
PHONY := __clean
__clean:
# 包含通用函数(cmd, clean 简写等)
include scripts/Kbuild.include
# 读取目标目录的 Kbuild/Makefile 文件
# 需要知道该目录定义了哪些目标,才能知道要清理什么
#
# kbuild-dir: 确定 Kbuild 文件的完整路径
# 如果 src 是绝对路径(以 / 开头),直接使用,否则加上 $(srctree)/ 前缀
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
# 优先使用 Kbuild 文件,其次使用 Makefile
include $(if $(wildcard $(kbuild-dir)/Kbuild), $(kbuild-dir)/Kbuild, $(kbuild-dir)/Makefile)
# 从 Kbuild 变量中提取需要处理的内容
# 提取子目录列表
# __subdir-y: 从 obj-y 中提取子目录(以 '/' 结尾的项)
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# __subdir-: 从 obj- 中提取子目录
# obj- 是被禁用的目标(CONFIG_xxx=n 时),这些目录也需要清理(可能之前编译过,现在禁用了)
__subdir- := $(patsubst %/,%,$(filter %/, $(obj-)))
subdir- += $(__subdir-)
# 合并需要递归清理的子目录
# subdir-ym: 启用的子目录(去重排序)
subdir-ym := $(sort $(subdir-y))
# subdir-ymn: 所有子目录(启用的 + 禁用的)
# 清理时需要处理所有目录,不管当前是否启用
subdir-ymn := $(sort $(subdir-ym) $(subdir-))
# 添加目录前缀
subdir-ymn := $(addprefix $(obj)/,$(subdir-ymn))
# U-Boot 临时解决方案:过滤不存在的目录
# 某些目录可能在 Kbuild 文件中声明,但实际并不存在(可能是条件编译导致的,或者是从其他项目复制的配置)
# 这里检查每个目录是否真的有 Makefile,没有则跳过
#
# 注意:使用 $(srctree)/$f/Makefile 检查源码树中的文件
subdir-ymn := $(foreach f, $(subdir-ymn), \
$(if $(wildcard $(srctree)/$f/Makefile),$f))
# 构建要清理的文件列表
# __clean-files: 收集所有需要删除的文件
# 来源包括:
# extra-y, extra- : 额外的构建目标
# always : 始终构建的目标
# targets : 在 Makefile.build 中注册的目标
# clean-files : 显式声明需要清理的文件
# hostprogs-y/hostprogs- : 宿主机程序
# hostlibs-y/hostlibs- : 宿主机静态库
# hostcxxlibs-y : 宿主机 C++ 库
__clean-files := $(extra-y) $(extra-) \
$(always) $(targets) $(clean-files) \
$(hostprogs-y) $(hostprogs-) \
$(hostlibs-y) $(hostlibs-) \
$(hostcxxlibs-y)
# 过滤掉不需要清理的文件
# no-clean-files: 在 Kbuild 文件中可以定义此变量,列出虽然是生成文件但不应被 clean 删除的文件
# 例如:某些需要手动维护的生成文件
__clean-files := $(filter-out $(no-clean-files), $(__clean-files))
# 处理文件路径
# 文件路径有两种情况:
# 1) 相对路径:相对于当前目录($(obj)/)
# 2) 绝对路径:以 $(objtree)/ 开头(指向输出目录根)
#
# 处理逻辑:
# - filter-out $(objtree)/%: 提取相对路径,加上 $(obj)/ 前缀
# - filter $(objtree)/%: 提取绝对路径,保持不变
# - wildcard: 只保留实际存在的文件(避免 rm 报错)
# 示例:
# obj = scripts/kconfig
# __clean-files = conf mconf zconf.tab.c $(objtree)/include/config.h
#
# 处理后:
# - scripts/kconfig/conf(如果存在)
# - scripts/kconfig/mconf(如果存在)
# - scripts/kconfig/zconf.tab.c(如果存在)
# - ./include/config.h(如果存在)
__clean-files := $(wildcard \
$(addprefix $(obj)/, $(filter-out $(objtree)/%, $(__clean-files))) \
$(filter $(objtree)/%, $(__clean-files)))
# clean-dirs: 在 Kbuild 文件中定义需要删除的整个目录
# 例如:clean-dirs := generated/
__clean-dirs := $(wildcard \
$(addprefix $(obj)/, $(filter-out $(objtree)/%, $(clean-dirs))) \
$(filter $(objtree)/%, $(clean-dirs)))
# cmd_clean: 删除文件
# 使用 rm -f(-f 忽略不存在的文件,不报错)
quiet_cmd_clean = CLEAN $(obj)
cmd_clean = rm -f $(__clean-files)
# cmd_cleandir: 删除目录
# 使用 rm -rf(递归删除整个目录)
quiet_cmd_cleandir = CLEAN $(__clean-dirs)
cmd_cleandir = rm -rf $(__clean-dirs)
# 主清理规则
# 执行顺序:
# 1) 递归进入所有子目录($(subdir-ymn))
# 2) 删除当前目录的文件(如果有)
# 3) 删除当前目录的子目录(如果有)
#
# 依赖 $(subdir-ymn) 确保:先清理子目录,再清理当前目录
# 这样如果当前目录要删除某个子目录,它已经是空的了
__clean: $(subdir-ymn)
# 如果有文件需要删除
ifneq ($(strip $(__clean-files)),)
# '+' 前缀:即使 make -n (dry-run) 也执行此命令
# 原因:清理操作通常是安全的,用户期望看到效果
+$(call cmd,clean)
endif
# 如果有目录需要删除
ifneq ($(strip $(__clean-dirs)),)
+$(call cmd,cleandir)
endif
# @: 是空命令(no-op)
# 确保规则总是有命令,即使上面两个 ifneq 都不满足
# 没有命令的规则可能导致 make 行为异常
@:
# 子目录递归
PHONY += $(subdir-ymn)
# 对每个子目录,递归调用 Makefile.clean
# $(clean) 在 Kbuild.include 中定义:
# clean := -f $(srctree)/scripts/Makefile.clean obj
#
# 展开示例:
# make -f scripts/Makefile.clean obj=drivers/gpio
$(subdir-ymn):
$(Q)$(MAKE) $(clean)=$@
.PHONY: $(PHONY)
10. include/host_arch.h
它是一个 Makefile 和 C 语言共用的头文件。
#开头的行对 Makefile 来说是注释。- C 预处理器看到
#if 0,直接跳到#endif,忽略中间内容。
export HOST_ARCH_AARCH64=0xaa64是有效的 Makefile 语句。
#include 和 #define 是 C 预处理指令,Makefile 看到 # 开头,全部当作注释忽略。
#if 0
# SPDX SPDX-License-Identifier: GPL-2.0+
#
# Constants defining the host architecture in assembler, C, and make files.
# The values are arbitrary.
#
# Copyright 2019 Heinrich Schuchardt <xypron.glpk@gmx.de>
#endif
#if 0
export HOST_ARCH_AARCH64=0xaa64
export HOST_ARCH_ARM=0x00a7
export HOST_ARCH_RISCV32=0x5032
export HOST_ARCH_RISCV64=0x5064
export HOST_ARCH_X86=0x0386
export HOST_ARCH_X86_64=0x8664
#endif
#include <version.h>
#define HOST_ARCH_AARCH64 0xaa64
#define HOST_ARCH_ARM 0x00a7
#define HOST_ARCH_RISCV32 0x5032
#define HOST_ARCH_RISCV64 0x5064
#define HOST_ARCH_X86 0x0386
#define HOST_ARCH_X86_64 0x8664
11. config.mk
# SPDX-License-Identifier: GPL-2.0+
#
# (C) Copyright 2000-2013
# Wolfgang Denk, DENX Software Engineering, wd@denx.de.
#########################################################################
# This file is included from ./Makefile and spl/Makefile.
# Clean the state to avoid the same flags added twice.
#
# (Tegra needs different flags for SPL.
# That's the reason why this file must be included from spl/Makefile too.
# If we did not have Tegra SoCs, build system would be much simpler...)
PLATFORM_RELFLAGS :=
PLATFORM_CPPFLAGS :=
LDFLAGS_FINAL :=
LDFLAGS_STANDALONE :=
OBJCOPYFLAGS :=
# clear VENDOR for tcsh
VENDOR :=
#########################################################################
ARCH := $(CONFIG_SYS_ARCH:"%"=%)
CPU := $(CONFIG_SYS_CPU:"%"=%)
BOARD := $(CONFIG_SYS_BOARD:"%"=%)
ifneq ($(CONFIG_SYS_VENDOR),)
VENDOR := $(CONFIG_SYS_VENDOR:"%"=%)
endif
ifneq ($(CONFIG_SYS_SOC),)
SOC := $(CONFIG_SYS_SOC:"%"=%)
endif
# Some architecture config.mk files need to know what CPUDIR is set to,
# so calculate CPUDIR before including ARCH/SOC/CPU config.mk files.
# Check if arch/$ARCH/cpu/$CPU exists, otherwise assume arch/$ARCH/cpu contains
# CPU-specific code.
CPUDIR=arch/$(ARCH)/cpu$(if $(CPU),/$(CPU),)
sinclude $(srctree)/arch/$(ARCH)/config.mk # include architecture dependend rules
sinclude $(srctree)/$(CPUDIR)/config.mk # include CPU specific rules
ifdef SOC
sinclude $(srctree)/$(CPUDIR)/$(SOC)/config.mk # include SoC specific rules
endif
ifneq ($(BOARD),)
ifdef VENDOR
BOARDDIR = $(VENDOR)/$(BOARD)
ENVDIR=${vendor}/env
else
BOARDDIR = $(BOARD)
ENVDIR=${board}/env
endif
endif
ifdef BOARD
sinclude $(srctree)/board/$(BOARDDIR)/config.mk # include board specific rules
endif
ifdef FTRACE
PLATFORM_CPPFLAGS += -finstrument-functions -DFTRACE
endif
#########################################################################
RELFLAGS := $(PLATFORM_RELFLAGS)
PLATFORM_CPPFLAGS += $(RELFLAGS)
PLATFORM_CPPFLAGS += -pipe
LDFLAGS_FINAL += -Bstatic
export PLATFORM_CPPFLAGS
export RELFLAGS
export LDFLAGS_FINAL
export LDFLAGS_STANDALONE
export CONFIG_STANDALONE_LOAD_ADDR
12.编译辅助程序移植
(1)fixdep
直接将fixdep.c源文件复制到scripts/basic/下
scripts/basic/Makefile
# SPDX-License-Identifier: GPL-2.0
###
# Makefile.basic 列出了构建过程中使用的基本程序。
# 此处列出的程序用于完成基础操作,例如修复文件依赖关系。
# 此初始步骤旨在避免内核配置变更时(当主 Makefile 包含 .config 文件时即会发生此情况)文件被重新编译。
# ---------------------------------------------------------------------------
# fixdep: 用于在构建过程中生成依赖关系信息
hostprogs-y := fixdep
always := $(hostprogs-y)
# fixdep 是编译其他主机程序所必需的
$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep
(2)kconfig
将U-Boot源码中的kconfig目录下的文件全部复制到scripts/kconfig/目录下。
也可以按照我的方式仅保留核心部分:
/kconfig.png)
lxdialog/目录内容全部复制。
kconfig/Makefile内容调整:
# SPDX-License-Identifier: GPL-2.0
# ===========================================================================
# Kernel configuration targets
# These targets are used from top-level makefile
PHONY += menuconfig config localyesconfig build_menuconfig
# Added for U-Boot
# Linux has defconfig files in arch/$(SRCARCH)/configs/,
# on the other hand, U-Boot does in configs/.
# Set SRCARCH to .. fake this Makefile.
SRCARCH := ..
ifdef KBUILD_KCONFIG
Kconfig := $(KBUILD_KCONFIG)
else
Kconfig := Kconfig
endif
ifeq ($(quiet),silent_)
silent := -s
endif
# We need this, in case the user has it in its environment
unexport CONFIG_
menuconfig: $(obj)/mconf
$< $(silent) $(Kconfig)
config: $(obj)/conf
$< $(silent) --oldaskconfig $(Kconfig)
build_menuconfig: $(obj)/mconf
simple-targets := oldconfig syncconfig
PHONY += $(simple-targets)
$(simple-targets): $(obj)/conf
$< $(silent) --$@ $(Kconfig)
PHONY += savedefconfig
savedefconfig: $(obj)/conf
$< $(silent) --$@=defconfig $(Kconfig)
%_defconfig: $(obj)/conf
$(Q)$(CPP) -nostdinc -P -I $(srctree) -undef -x assembler-with-cpp $(srctree)/arch/$(SRCARCH)/configs/$@ -o generated_defconfig
$(Q)$< $(silent) --defconfig=generated_defconfig $(Kconfig)
# ===========================================================================
# Shared Makefile for the various kconfig executables:
# conf: Used for defconfig, oldconfig and related targets
# object files used by all kconfig flavours
conf-objs := conf.o zconf.tab.o
hostprogs-y := conf
targets += zconf.lex.c
# generated files seem to need this to find local include files
HOSTCFLAGS_zconf.lex.o := -I$(src)
HOSTCFLAGS_zconf.tab.o := -I$(src)
# mconf: Used for the menuconfig target based on lxdialog
hostprogs-y += mconf
lxdialog := checklist.o inputbox.o menubox.o textbox.o util.o yesno.o
mconf-objs := mconf.o zconf.tab.o $(addprefix lxdialog/, $(lxdialog))
HOSTLDLIBS_mconf = $(shell . $(obj)/.mconf-cfg && echo $$libs)
$(foreach f, mconf.o $(lxdialog), \
$(eval HOSTCFLAGS_$f = $$(shell . $(obj)/.mconf-cfg && echo $$$$cflags)))
$(obj)/mconf.o: $(obj)/.mconf-cfg
$(addprefix $(obj)/lxdialog/, $(lxdialog)): $(obj)/.mconf-cfg
$(obj)/zconf.tab.o: $(obj)/zconf.lex.c
# check if necessary packages are available, and configure build flags
define filechk_conf_cfg
$(CONFIG_SHELL) $<
endef
$(obj)/.%conf-cfg: $(src)/%conf-cfg.sh FORCE
$(call filechk,conf_cfg)
clean-files += .*conf-cfg
scripts/Makefile
always := $(hostprogs-y)
# Let clean descend into subdirs
subdir- += basic kconfig dtc
(3)Kconfig.include
# Kconfig helper macros
# Convenient variables
comma := ,
quote := "
squote := '
empty :=
space := $(empty) $(empty)
dollar := $
right_paren := )
left_paren := (
# $(if-success,<command>,<then>,<else>)
# Return <then> if <command> exits with 0, <else> otherwise.
if-success = $(shell,{ $(1); } >/dev/null 2>&1 && echo "$(2)" || echo "$(3)")
# $(success,<command>)
# Return y if <command> exits with 0, n otherwise
success = $(if-success,$(1),y,n)
# $(cc-option,<flag>)
# Return y if the compiler supports <flag>, n otherwise
cc-option = $(success,$(CC) -Werror $(1) -E -x c /dev/null -o /dev/null)
# $(cc-define,<macro>)
# Return y if the compiler defines <macro>, n otherwise
cc-define = $(success,$(CC) -dM -E -x c /dev/null | grep -q '^#define \<$(1)\>')
# $(ld-option,<flag>)
# Return y if the linker supports <flag>, n otherwise
ld-option = $(success,$(LD) -v $(1))
# gcc version including patch level
gcc-version := $(shell,$(srctree)/scripts/gcc-version.sh -p $(CC) | sed 's/^0*//')
将下列各种相关文件直接复制到对应目录下:
scripts/gcc-version.sh
scripts/setlocalversion
scripts/mkmakefile
顶级目录下创建一个Kconfig文件
mainmenu "U-Boot $(UBOOTVERSION) Configuration"
comment "Compiler: $(CC_VERSION_TEXT)"
source "scripts/Kconfig.include"
menu "General setup"
endmenu # General setup
运行 make O=out menuconfig,可以看到配置图像画面出来。
13. 总结
至此Kconfig/Kbuild配置构建系统基本框架搭建完成。当前实现的版本是一个精简的核心框架,相比 U‑Boot 和 Linux 内核源码,去除了大量暂时用不到的功能(如多架构支持、复杂设备树编译、特定外设驱动等),但完整保留了 Kconfig 的配置机制、递归 Makefile 构建逻辑、主机工具编译支持以及自动依赖处理等核心模块。
所有后续开发都将以此框架为基础,随着项目功能的逐步增加,可以按需向系统中添加新的配置选项、子目录、编译规则以及主机工具,实现渐进式的功能扩展。
这样既大幅降低了初学者的理解门槛,又为后续的功能扩展提供了坚实的基石。
往期相关文章:
U-Boot 构建工具 fixdep 的工作原理以及编译分析