目录
前言
在分析大型开源项目(如 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_VERBOSE
KBUILD_VERBOSE = 0
endif
ifeq ($(KBUILD_VERBOSE),1)
quiet =
Q =
else
quiet=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_VERBOSE
KBUILD_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 =
else
quiet=quiet_
Q = @
endif
- 如果变量 quiet 为空的话,整个命令都会输出。
- 如果变量 quiet 为“quiet_”的话,仅输出短版本。
- 如果变量 quiet 为“silent_”的话,整个命令都不会输出。
静默输出
使用 uboot 的静默输出功能,编译的时候使用“make -s”即可。
顶层 Makefile中相应的代码如下:
# If the user is running make -s (silent mode), suppress echoing of
# commands
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
export 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-4
ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)
quiet=silent_
endif
else # make-3.8x
ifneq ($(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 exists
saved-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_CHECKSRC
KBUILD_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 SUBDIRS
KBUILD_EXTMOD ?= $(SUBDIRS)
endif
ifeq ("$(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
endif
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)
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 ?=
endif
KCONFIG_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
else
LD = $(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
else
LD = $(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 CHECKFLAGS
export 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 等等。
1833

被折叠的 条评论
为什么被折叠?



