imx6ull-系统移植篇5——U-Boot 顶层 Makefile 简析
目录
前言
Makefile分析
版本号
MAKEFLAGS 变量
命令输出
静默输出
设置编译结果输出目录
代码检查
模块编译
获取主机架构和系统
设置目标架构、交叉编译器和配置文件
调用 scripts/Kbuild.include
交叉编译工具变量设置
导出其他变量
make xxx_defconfig 过程
1. 命令解析阶段
2. Makefile 关键逻辑
分解说明:
3. 配置生成流程
步骤 1:复制默认配置
步骤 2:运行配置工具
步骤 3:生成头文件
4. Kconfig 系统的作用
5. 输出文件解析
Makefile.build 脚本分析
scripts_basic目标分析
%config目标分析
make流程
前言
在分析大型开源项目(如 U-Boot 或 Linux 内核)的源码时,必须从顶层 Makefile 入手,这是理解工程组织结构的核心入口。通过逐层分析顶层 Makefile 和子目录的 Makefile,可以清晰地掌握以下关键点:
-
工程构建流程:了解编译目标、依赖关系以及工具链的调用方式。
-
代码模块划分:通过子目录的 Makefile 定位功能模块的代码分布。
-
配置系统:分析
Kconfig
与 Makefile 的联动逻辑,明确功能模块的编译条件。
本讲实验就让我们来分析一下U-Boot 的顶层 Makefile。
Makefile分析
版本号
VERSION 是主版本号, PATCHLEVEL 是补丁版本号, SUBLEVEL 是次版本号,这三个一起构成了 uboot 的版本号。比如当前的 uboot 版本号就是“2016.03”。
EXTRAVERSION 是附加版本信息, NAME 是和名字有关的,一般不使用这两个。
VERSION = 2016
PATCHLEVEL = 03
SUBLEVEL =
EXTRAVERSION =
NAME =
MAKEFLAGS 变量
递归调用:在 Makefile 中,可以使用 make
命令调用其他 Makefile(通常是子目录中的 Makefile)。
基本格式:
$(MAKE) -C subdir
-
$(MAKE)
:调用make
命令(推荐使用变量MAKE
而非直接写make
,确保兼容性)。 -
-C <子目录路径>
:切换到指定目录后执行其 Makefile。
导出变量(传递给子 Makefile):使变量在子 make
过程中可见。
export VARIABLE_NAME
禁止导出变量(不传递给子 Makefile):阻止变量传递到子 make
。
unexport VARIABLE_NAME
有两个特殊的变量:“SHELL”和“MAKEFLAGS”,这两个变量除非使用“unexport”声明,否则的话在整个make的执行过程中,它们的值始终自动的传递给子make。
在uboot的主Makefile中有如下代码:
MAKEFLAGS += -rR --include-dir=$(CURDIR)
上述代码使用“+=”来给变量 MAKEFLAGS 追加了一些值,“-rR”表示禁止使用内置的隐含规则和变量定义,“--include-dir”指明搜索路径, ”$(CURDIR)”表示当前目录。
命令输出
uboot 默认编译是不会在终端中显示完整的命令,都是短命令,如图:
调试 uboot 的时候,可以通过设置变量“V=1“来实现完整的命令输出。
顶层 Makefile 中控制命令输出的代码如下:
ifeq ("$(origin V)", "command line")KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSEKBUILD_VERBOSE = 0
endififeq ($(KBUILD_VERBOSE),1)quiet =Q =
elsequiet=quiet_Q = @
endif
函数 $(origin V)
-
作用:origin是 Makefile 内置函数,用于判断变量的来源。
-
返回值:
-
"command line":表示变量 V是通过命令行传入的(如 make V=1)。
-
其他可能值:"file"(来自 Makefile)、"environment"(环境变量)、"undefined"(未定义)。
-
检查 V
是否来自命令行:
ifeq ("$(origin V)", "command line")KBUILD_VERBOSE = $(V)
endif
如果用户通过命令行指定了 V(如 make V=1),则将 V的值赋给 KBUILD_VERBOSE。
- 输入 make V=1→ KBUILD_VERBOSE = 1(显示完整命令)
- 输入 make V=0→ KBUILD_VERBOSE = 0(静默模式)。
如果 KBUILD_VERBOSE未被定义(用户未传入 V),则默认为 0(静默模式):
ifndef KBUILD_VERBOSEKBUILD_VERBOSE = 0
endif
控制命令回显:
- 分支1(KBUILD_VERBOSE=1):quiet为空,Q为空 → 显示完整编译命令(如 gcc -o file.c)。
- 分支2(KBUILD_VERBOSE=0):quiet = quiet_:用于生成简化的日志(如 CC file.o而非完整命令)。Q = @:禁止命令回显,实现静默编译。
ifeq ($(KBUILD_VERBOSE),1)quiet =Q =
elsequiet=quiet_Q = @
endif
- 如果变量 quiet 为空的话,整个命令都会输出。
- 如果变量 quiet 为“quiet_”的话,仅输出短版本。
- 如果变量 quiet 为“silent_”的话,整个命令都不会输出。
静默输出
使用 uboot 的静默输出功能,编译的时候使用“make -s”即可。
顶层 Makefile中相应的代码如下:
# If the user is running make -s (silent mode), suppress echoing of# commandsifneq ($(filter 4.%,$(MAKE_VERSION)),) # make-4ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)quiet=silent_endifelse # make-3.8xifneq ($(filter s% -s%,$(MAKEFLAGS)),)quiet=silent_endif
endifexport quiet Q KBUILD_VERBOSE
| 检测 |
| 检测 |
| 检测 |
| 设置静默模式标志 |
| 确保变量传递给子 |
函数$(filter pattern, text):
作用:从 text中筛选出符合 pattern的单词。
示例:
$(filter s%, -s --silent) # 返回 `-s`(匹配 `s%` 模式)
函数 $(firstword text)
作用:取 text中的第一个单词。
示例:
$(firstword a b c) # 返回 `a`
变量$(MAKE_VERSION)
作用:获取当前 make工具的版本号(如 4.2.1)。
变量(MAKEFLAGS)
作用:获取 make命令的选项(如 -s、-j8),通常用于判断是否启用了静默模式。
代码逻辑首先是检查 make
版本是否为 4.x:
ifneq ($(filter 4.%,$(MAKE_VERSION)),) # make-4ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)quiet=silent_endif
else # make-3.8xifneq ($(filter s% -s%,$(MAKEFLAGS)),)quiet=silent_endif
endif
不同版本的 make对 MAKEFLAGS的处理方式不同,因此需要区分:
- make 4.x:
MAKEFLAGS可能包含 s或 --silent,但格式可能不同(如 --jobserver-fds=3,4 -s)。
$(firstword x$(MAKEFLAGS))确保正确解析第一个选项。
- make 3.8x:
直接检查 MAKEFLAGS是否包含 -s或 s%(旧版 make可能使用短选项 -s)。
然后是设置 quiet
变量
quiet=silent_
如果检测到 make -s,则设置 quiet=silent_,表示进入静默模式。
后续的编译命令会使用 $(quiet)来决定是否打印详细信息(如 quiet_cmd_CC)。
最后是导出变量:
export quiet Q KBUILD_VERBOSE
export确保 quiet、Q、KBUILD_VERBOSE变量传递给子 make(递归调用时仍然有效)。
Q=@(可能在其他地方定义)用于禁止命令回显。
设置编译结果输出目录
uboot 可以将编译出来的目标文件输出到单独的目录中,在 make 的时候使用“O”来指定输出目录,比如“make O=out”就是设置目标文件输出到 out 目录中。
这么做是为了将源文件和编译产生的文件分开,当然也可以不指定 O 参数,不指定的话源文件和编译产生的文件都在同一个目录内,一般我们不指定 O 参数。
顶层 Makefile 中相关的代码如下:
# kbuild supports saving output files in a separate directory.
# To locate output files in a separate directory two syntaxes are supported.
# In both cases the working directory must be the root of the kernel src.
# 1) O=
# Use "make O=dir/to/store/output/files/"
#
# 2) Set KBUILD_OUTPUT
# Set the environment variable KBUILD_OUTPUT to point to the directory
# where the output files shall be placed.
# export KBUILD_OUTPUT=dir/to/store/output/files/
# make
#
# The O= assignment takes precedence over the KBUILD_OUTPUT environment variable.# KBUILD_SRC is set on invocation of make in OBJ directory
# KBUILD_SRC is not intended to be used by the regular user (for now)
ifeq ($(KBUILD_SRC),)# OK, Make called in directory where kernel src resides
# Do we want to locate output files in a separate directory?
ifeq ("$(origin O)", "command line")KBUILD_OUTPUT := $(O)
endif# That's our default target when none is given on the command line
PHONY := _all
_all:# Cancel implicit rules on top Makefile
$(CURDIR)/Makefile Makefile: ;ifneq ($(KBUILD_OUTPUT),)# Invoke a second make in the output directory, passing relevant variables# check that the output directory actually existssaved-output := $(KBUILD_OUTPUT)KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) && /bin/pwd)...
endif # ifneq ($(KBUILD_OUTPUT),)
endif # ifeq ($(KBUILD_SRC),)
判断“O”是否来自于命令行,如果来自命令行的话条件成立, KBUILD_OUTPUT就为$(O),因此变量 KBUILD_OUTPUT 就是输出目录。
ifeq ("$(origin O)", "command line")
判断 KBUILD_OUTPUT 是否为空。
ifneq ($(KBUILD_OUTPUT),)
调用 mkdir 命令,创建 KBUILD_OUTPUT 目录,并且将创建成功以后的绝对路径赋值给 KBUILD_OUTPUT。至此,通过 O 指定的输出目录就存在了。
KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd
关键点 | 实现方式 |
---|---|
输出目录指定 |
|
优先级控制 |
|
目录切换与递归编译 |
|
源码目录判断 |
|
代码检查
uboot 支持代码检查:
- 使用命令“make C=1”,使能代码检查,检查那些需要重新编译的文件。
- “make C=2”用于检查所有的源码文件。
顶层 Makefile 中的代码如下:
# Call a source code checker (by default, "sparse") as part of the C compilation.
#
# Use 'make C=1' to enable checking of only re-compiled files.
# Use 'make C=2' to enable checking of *all* source files, regardless of whether they are re-compiled or not.
#
# See the file "Documentation/sparse.txt" for more details, including where to get the "sparse" utility.ifeq ("$(origin C)", "command line")KBUILD_CHECKSRC = $(C)
endif
ifndef KBUILD_CHECKSRCKBUILD_CHECKSRC = 0
endif
主要判断 C 是否来源于命令行,如果 C 来源于命令行,那就将 C 赋值给变量KBUILD_CHECKSRC,如果命令行没有 C 的话 KBUILD_CHECKSRC 就为 0。
模块编译
在 uboot 中允许单独编译某个模块,使用命令“ make M=dir”即可,旧语法“ make SUBDIRS=dir”也是支持的。
顶层 Makefile 中的代码如下:
# Use make M=dir to specify directory of external module to build
# Old syntax make ... SUBDIRS=$PWD is still supported
# Setting the environment variable KBUILD_EXTMOD take precedence
ifdef SUBDIRSKBUILD_EXTMOD ?= $(SUBDIRS)
endififeq ("$(origin M)", "command line")KBUILD_EXTMOD := $(M)
endif# If building an external module we do not care about the all: rule
# but instead _all depend on modules
PHONY += all
ifeq ($(KBUILD_EXTMOD),)_all: all
else_all: modules
endififeq ($(KBUILD_SRC),)# building in the source treesrctree := .
elseifeq ($(KBUILD_SRC)/,$(dir $(CURDIR)))# building in a subdirectory of the source treesrctree := ..elsesrctree := $(KBUILD_SRC)endif
endif
objtree := .
src := $(srctree)
obj := $(objtree)VPATH := $(srctree)$(if $(KBUILD_EXTMOD),:$(KBUILD_EXTMOD))export srctree objtree VPATH
代码解析:
外部模块构建控制,支持两种指定外部模块目录的方式:
- 旧语法:SUBDIRS=$PWD
- 新语法:M=dir(优先级更高)
构建目标选择:
- 普通构建:_all依赖 all目标
- 外部模块构建:_all依赖 modules目标
源码/构建目录处理:
- 检测是否在源码树构建(KBUILD_SRC为空)
- 设置 srctree和 objtree路径
- 处理 VPATH(虚拟路径)以便查找源码
变量导出:将关键目录变量导出供子 make 使用
该段代码主要实现外部模块构建支持和灵活的目录路径处理,是 Linux 内核构建系统的核心功能之一。
获取主机架构和系统
下来顶层 Makefile 会获取主机架构和系统,也就是我们电脑的架构和系统。
代码如下:
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
HOSTARCH(主机架构)的确定:
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/)
-
uname -m
:获取机器的硬件名称(如x86_64
,armv7l
,ppc64le
等)。 -
sed
命令:将不同的架构名称统一为构建系统识别的标准名称:-
i.86
→x86
(匹配 i386, i486, i586, i686) -
sun4u
→sparc64
-
arm.*
→arm
(所有ARM架构统一为arm) -
sa110
→arm
(StrongARM处理器归类为ARM) -
ppc64
/ppc
/macppc
→powerpc
(所有PowerPC架构统一) -
sh.*
→sh
(SuperH架构)
-
HOSTOS(主机操作系统)的确定
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \sed -e 's/\(cygwin\).*/cygwin/')
-
uname -s
:获取操作系统名称(如Linux
,Darwin
,CYGWIN_NT-10.0
)。 -
tr '[:upper:]' '[:lower:]'
:将结果转换为小写(如Linux
→linux
)。 -
sed
命令:特殊处理Cygwin环境: -
将
cygwin.*
统一为cygwin
(避免版本号差异)。
将 HOSTARCH
和 HOSTOS
导出为环境变量,供后续Makefile或子进程使用。
export HOSTARCH HOSTOS
最后导出的目标是: HOSTARCH=x86_64, HOSTOS=linux。
设置目标架构、交叉编译器和配置文件
编 译 uboot 的 时 候 需 要 设 置 目 标 板 架 构 和 交 叉 编 译 器 ,“ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-”就是用于设置 ARCH 和 CROSS_COMPILE。
在顶层Makefile 中代码如下:
# set default to nothing for native builds
ifeq ($(HOSTARCH),$(ARCH))CROSS_COMPILE ?=
endifKCONFIG_CONFIG ?= .config
export KCONFIG_CONFIG
原生构建的交叉编译工具链设置:
当检测到主机架构(HOSTARCH)和目标架构(ARCH)相同时,自动清空交叉编译前缀(CROSS_COMPILE)。
ifeq ($(HOSTARCH),$(ARCH))CROSS_COMPILE ?=
endif
- HOSTARCH:主机CPU架构(由之前代码通过uname -m获取并标准化)。
- ARCH:目标系统架构(通常在Makefile或配置中指定,如arm、x86)。
如果 HOSTARCH == ARCH,说明是原生编译(如x86主机编译x86程序),无需交叉编译工具链。
CROSS_COMPILE ?=:清空变量,避免使用类似arm-linux-gnueabi-的前缀。
?=表示仅当变量未定义时才赋值,允许用户在命令行覆盖(如强制指定make CROSS_COMPILE=arm-linux-)。
内核配置文件路径设置
定义内核配置文件的默认路径,并导出为环境变量。
KCONFIG_CONFIG ?= .config
export KCONFIG_CONFIG
- KCONFIG_CONFIG:存储配置文件路径(默认为当前目录下的.config)。
- ?=:如果未通过环境变量或命令行指定,则默认使用.config。
- export:确保子Makefile或脚本能访问该路径(如scripts/kconfig读取配置)。
调用 scripts/Kbuild.include
主 Makefile 会调用文件 scripts/Kbuild.include 这个文件。
顶层 Makefile 中代码如下:
# We need some generic definitions (do not try to remake the file).
scripts/Kbuild.include: ;
include scripts/Kbuild.include
使用“include”包含了文件 scripts/Kbuild.include,此文件里面定义了很多变量,在 uboot 的编译过程中会用到这些变量,后面有机会再分析。
交叉编译工具变量设置
# Make variables (CC, etc...)AS = $(CROSS_COMPILE)as
# Always use GNU ld
ifneq ($(shell $(CROSS_COMPILE)ld.bfd -v 2> /dev/null),)LD = $(CROSS_COMPILE)ld.bfd
elseLD = $(CROSS_COMPILE)ld
endif
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
LDR = $(CROSS_COMPILE)ldr
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
所有工具均以$(CROSS_COMPILE)为前缀,支持交叉编译(如arm-linux-gnueabi-gcc)
基础工具:
- AS:汇编器(as)
- CC:C编译器(gcc)
- CPP:C预处理器(gcc -E)
- AR:静态库打包工具(ar)
- NM:符号表查看工具(nm)
检测是否存在ld.bfd
(Binutils的默认链接器),若存在则优先使用:
ifneq ($(shell $(CROSS_COMPILE)ld.bfd -v 2> /dev/null),)LD = $(CROSS_COMPILE)ld.bfd # 优先尝试GNU bfd版本的ld
elseLD = $(CROSS_COMPILE)ld # 回退到标准ld
endif
导出其他变量
接下来在顶层 Makefile 会导出很多变量,代码如下:
export VERSION PATCHLEVEL SUBLEVEL UBOOTRELEASE UBOOTVERSION
export ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR
export CONFIG_SHELL HOSTCC HOSTCFLAGS HOSTLDFLAGS CROSS_COMPILE AS
LD CC
export CPP AR NM LDR STRIP OBJCOPY OBJDUMP
export MAKE AWK PERL PYTHON
export HOSTCXX HOSTCXXFLAGS DTC CHECK CHECKFLAGSexport KBUILD_CPPFLAGS NOSTDINC_FLAGS UBOOTINCLUDE OBJCOPYFLAGS
LDFLAGS
export KBUILD_CFLAGS KBUILD_AFLAGS
目标系统配置变量如下:
变量 | 典型值示例 | 作用 |
---|---|---|
|
| 目标CPU架构 |
|
| 具体CPU型号 |
|
| 开发板名称 |
|
| 芯片厂商 |
|
| 芯片系列 |
|
| CPU相关代码路径 |
|
| 开发板相关代码路径 |
在 uboot 根目录下有个文件叫做 config.mk,这 7 个变量就是在 config.mk 里面定义的。
make xxx_defconfig 过程
在编译 uboot 之前要使用“make xxx_defconfig”命令来配置 uboot,那么这个配置过程是如何运行的呢?
1. 命令解析阶段
当执行 make xxx_defconfig
时:
-
xxx_defconfig
:对应
configs/
目录下的预置配置文件(如mx6ull_alientek_emmc_defconfig
)。 -
Makefile 目标匹配:
顶层 Makefile 中定义了
%config
规则,匹配所有以_defconfig
结尾的目标。
2. Makefile 关键逻辑
顶层 Makefile 中的相关规则如下:
%config: scripts_basic outputmakefile FORCE $(Q)$(MAKE) $(build)=scripts/kconfig $@
分解说明:
-
依赖项:
-
scripts_basic
:确保scripts/kconfig
目录下的工具(如conf
、mconf
)已编译。 -
outputmakefile
:生成输出目录的 Makefile(用于O=dir
构建)。 -
FORCE
:强制目标每次执行(即使文件已存在)。
-
-
实际执行:
-
$(build)=scripts/kconfig
:调用scripts/Kbuild.include
中的build
变量,切换到scripts/kconfig
目录。 -
$@
:展开为xxx_defconfig
(用户输入的目标)。
-
3. 配置生成流程
步骤 1:复制默认配置
-
从
configs/xxx_defconfig
复制到.config
(根目录下):cp configs/xxx_defconfig .config
步骤 2:运行配置工具
调用 scripts/kconfig/conf
工具处理配置:
scripts/kconfig/conf --defconfig=.config Kconfig
-
--defconfig
:指定输入配置文件。 -
Kconfig
:顶层 Kconfig 文件,定义所有配置选项的层级关系。
步骤 3:生成头文件
-
根据
.config
生成include/config.h
和include/config/auto.conf
: -
auto.conf
:包含所有配置项的CONFIG_*
变量,供 Makefile 使用。 -
config.h
:C 代码可包含的宏定义(如#define CONFIG_SYS_TEXT_BASE 0x87800000
)。
4. Kconfig 系统的作用
-
层级化配置:
每个子目录的
Kconfig
文件定义该模块的配置选项(如arch/arm/Kconfig
定义 ARM 架构相关配置)。 -
依赖关系:
通过
depends on
、select
等关键字确保配置的合理性(例如启用某驱动时自动依赖其总线配置)。 -
交互界面:
支持
menuconfig
、xconfig
等图形化配置工具(但defconfig
是非交互模式)。
5. 输出文件解析
配置完成后生成的关键文件:
文件路径 | 作用 |
---|---|
| 最终生效的配置文件(可直接手动修改后运行 |
| 供 C 代码使用的宏定义(如 |
| 供 Makefile 使用的变量(如 |
| 三态配置( |
Makefile.build 脚本分析
scripts_basic
目标分析
编译生成 scripts/basic/fixdep工具,用于处理依赖文件(如 .d文件):
@make -f ./scripts/Makefile.build obj=scripts/basic
scripts_basic 目标对应的命令为: @make -f ./scripts/Makefile.build obj=scripts/basic。打开文件 scripts/Makefile.build,有如下代码:
# Modified for U-Boot
prefix := tpl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
prefix := spl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
prefix := .
endif
endif
路径解析:
- prefix初始为 tpl,尝试匹配 tpl/scripts/basic→ 失败 → 回退到 spl→ 最终 prefix=.。
- src := scripts/basic(源码目录),obj := scripts/basic(输出目录)。
Makefile 包含:
- 通过 include ./scripts/basic/Makefile加载子目录的构建规则。
- scripts/basic/Makefile中定义 always := fixdep,强制编译 fixdep。
默认目标 __build:
依赖 scripts/basic/fixdep,触发 fixdep的编译:
__build: scripts/basic/fixdep@:
%config
目标分析
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
各个变量值如下:
src= scripts/kconfig
kbuild-dir = ./scripts/kconfig
kbuild-file = ./scripts/kconfig/Makefile
include ./scripts/kconfig/Makefile
可以看出, Makefilke.build 会读取 scripts/kconfig/Makefile 中的内容.
总而言之,一切的一切都是为了最后生成.config文件。
make流程
make xxx_defconfig: 用于配置 uboot,这个命令最主要的目的就是生成.config 文件。
make:用于编译 uboot,这个命令的主要工作就是生成二进制的 u-boot.bin 文件和其他的一些与 uboot 有关的文件,比如 u-boot.imx 等等。