[ARM Linux系统移植] U-Boot 顶层 Makefile 分析 - 基础知识
编译 u-boot 的时候,是从顶层 makefile 开始建立依赖关系的,所以在阅读 u-boot 源码的时候需要首先阅读一下顶层的 makefile。然而即使是单独的顶层 makefile,内容也比较繁杂,在不甚了解的情况下不好组织文章结构。因此这篇笔记跟随开发板配套文档里面的目录结构,以 u-boot 顶层 makefile 中零散的功能点为契机,学习一下 makefile 的基础知识。学习到的 makefile 基础知识,我会单独在绿色提示框里给出。
1. 版本号
在 makefile 的开始定义了各种版本号。
- VERSION = 2016
- PATCHLEVEL = 03
- SUBLEVEL =
- EXTRAVERSION =
- NAME =
当变量用来表示用户在命令行或环境中所自定义的常数时,习惯上会全部以大写来编写其名称。至于只在 makefile 文件中出现的变量,则会全部以小写来编写其名称。
要取得某个变量的值,请使用 $() 括住该变量的名称。
2. MAKEFLAGS 变量
MAKEFLAGS 如其名,是指定 make 程序选项的。相关选项的含义可以通过 make -help 查看。如下代码中的 -r 选项指禁用内置的隐含规则;-R 选项指禁用内置的变量;--include-dir 选项指定了搜索的目录。CURDIR 是标准内置变量,表示 make 进程的当前工作目录。
- MAKEFLAGS += -rR --include-dir=$(CURDIR)
正在执行 make 进程的当前工作目录。此变量的值将会是 shell 变量 PWD 的值(表明你是从哪个目录运行 make 程序的),除非 make 在运行时用到了 --directory (或 -C) 选项。--directory 选项会使得 make 在搜索任何 makefile 之前变更到不同的目录。如果你使用的是 --directory 的形式,CURDIR 将会包含 --include-dir 的目录参数。
3. 命令输出
如图 1 所示,uboot 默认的编译是只显示短指令的,并且还有很多编译指令不显示出来。这样的好处是界面清爽,不利的地方是不便于分析 u-boot 的编译过程。而用户可以指定变量 V 来控制指令是否显示以及是显示短指令还是完整指令。

V 变量的逻辑为如下片段:首先 V 变量要来自命令行。V = 1 时,不隐藏指令显示,并且显示完整指令;V = 0 时,隐藏指令显示,并且显示短指令。
- 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 函数将会返回描述变量来自何处的字符串。这个变量可以协助你决定如何使用一个变量的值。举例来说,如果一个变量来自环境,或许你想要忽略该变量的值;如果该变量来自命令行,你就不会这么做。
origin 的可能返回值包括:undefined、default、environment、environment override、file、command line、override、automatic。
关于隐藏指令显示是使用了 make 中的 @ 符号,@ 符号加在指令前面就不会显示这条指令。而 Q 变量就是控制是否有@ 符号的。挑选代表的 make 语句如下:
- scripts_basic:
- $(Q)$(MAKE) $(build)=scripts/basic
不要输出命令。当你想将某个工作目标的所有命令全都隐藏起来的时候,如果考虑到旧版兼容性,你可以把该工作目标设为特殊工作目标 .SILENT 的一个必要条件。不过,最好能够使用 @,因为它可以应用在脚本中个别的命令上。如果你将这个修饰符应用在所有的工作目标上,你可以使用 -silent (或 -s)选项。
关于短指令显示的实现,是 makefile 中让各种指令对应了短指令版本,比如下面是 rm 指令对应的短版本和完整版本,可以看到对应使用了 CLEAN 作为短命令。
- quiet_cmd_rmdirs = $(if $(wildcard $(rm-dirs)),CLEAN $(wildcard $(rm-dirs)))
- cmd_rmdirs = rm -rf $(rm-dirs)
最终使用的情况为如下例子,会扩展 cmd 这个变量来回显,而其中就会根据 quiet 变量决定是否使用短命令。
- clean: $(clean-dirs)
- $(call cmd,rmdirs)
cmd 变量可以使用如下指令方便的定位到在何处定义。cmd 变量的具体实现目前不再展开叙述。
make -p | grep -B1 -E '^cmd '
4. 静默输出
正常情况下编译的时候还是会有短命令显示,使用 make -s 就可以使所有的指令不再显示,即静默输出。
makefile 中会根据 make 程序的版本号,使用不同的模板来匹配是否包含 -s 选项,并且更新 quiet 变量。quiet 变量针对 silent_ 的值是如何使用的还不得而知。
- 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
filter 函数会将 text 视为一系列被空格隔开的单词,与 pattern 比较之后,接着会返回相符者。
filter 还可以接受多个(被空格隔开的)格式。正如之前所说,格式必须比较整个单词,才可以将相符的单词放在输出列表中。
一个模式中只可以包含一个 % 字符。如果模式包含了额外的 % 字符,那么第一个 % 字符除外,其余字符都会被视为文字字符。
5. 设置输出目录
变量 O 可以设置编译结果所存放的目录。不指定此变量的话,默认将编译产生的文件放在和源文件相同的目录,一般也不指定此变量。变量 O的设置逻辑和变量 V 一样,并基于此初始化其余的变量。
- ifeq ("$(origin O)", "command line")
- KBUILD_OUTPUT := $(O)
- endif
- 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)
6. 模块编译
可以通过 make M=dir 选项单独编译某个模块,也支持使用旧语法 make SUBDIRS=dir 指定。
- ifdef SUBDIRS
- KBUILD_EXTMOD ?= $(SUBDIRS)
- endif
- ifeq ("$(origin M)", "command line")
- KBUILD_EXTMOD := $(M)
- endif
如果选择编译整个工程,那么之后会依赖于 all 条件;如果选择编译模块,那么之后会依赖于 modules 条件。
- PHONY += all
- ifeq ($(KBUILD_EXTMOD),)
- _all: all
- else
- _all: modules
- endif
之后会根据是否编译模块来设置相应的变量。因为一般编译整个工程,且 KBUILD_SRC 变量为空,所以最后 objtree 、src 、 obj 、 VPATH 变量都为当前目录。
- 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))
if 函数(不要跟条件指令 ifeq、ifne、ifdef 和 ifndef 搞混了)会根据条件表达式的求值结果,从两个宏中选一个出来,进行扩展的动作。如果 condition 扩展之后包含任何字符(即使是空格),那么它的求值结果为“真”,于是会对 then-par 进行扩展的动作;否则,如果 condition 扩展之后空无一物,那么它的求值结果为“假”,于是会对 else-part 进行扩展的动作。
7. 获取主机架构和系统
主机架构名称和系统名称都是通过 uname 命令获取的,-s (--kernel-name) 选项打印内核的名字,-m (--machine) 选项打印机器硬件的名字。同时还需要对名称做统一处理,比如将获取的架构名称中的 i.86 替换成 x86,将系统名称中的大小转化成小写等等。
- 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/')
8. 交叉编译器和配置文件
如果要编译的架构和主机架构一样,那么就不用配置交叉编译器了。然而需要编译 ARM 架构的 u-boot,主机架构是 x86_64,所以需要在命令行里设置 CROSS_COMPILE 变量为 arm-linux-gnueabihf- 。从 CC 变量的定义可以看到,CROSS_COMPILE 是交叉编译工具的前缀,所以最后的“横杠”不要忘记了。
- ifeq ($(HOSTARCH),$(ARCH))
- CROSS_COMPILE ?=
- endif
- 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
KCONFIG_CONFIG 变量指明了 .config 文件。 .config 文件默认情况下是没有的,需要使用 make ???_defconfig 命令生成。
- KCONFIG_CONFIG ?= .config
9. 调用 scripts/Kbuild.include
scripts/Kbuild.include 里定义了各种通用的变量,通过 include 指令读取。
- # We need some generic definitions (do not try to remake the file).
- scripts/Kbuild.include: ;
- include scripts/Kbuild.include
当 make 看到 include 指令时,会事先对通配符以及变量引用进行扩展的动作,然后试着读进引用文件。如果这个文件存在,则整个处理过程会继续下去;然而,如果这个文件不存在,则 make 会汇报此问题并且继续读取其余的 makefile。当所有的读取动作皆已完成之后,make 会从规则数据库中找出任何可用来更新引入文件的规则。如果找到了一个相符的规则,make 就会按照正常的步骤更新工作目标。如果任何一个引入文件被规则更新,make 接着会清除它的内部数据库并且重新读进整个 makefile。如果完成读取、更新和重新读取的过程之后,仍有 include 指令因为文件不存在而执行失败,那么 make 就会显示错误状态并终止执行。
单独一个分号代表执行的空指令:
The commands of a rule consist of shell command lines to be executed one by one. Each command line must start with a tab, except that the first command line may be attached to the target-and-prerequisites line with a semicolon in between.
10. 导出变量
顶层 makefile 中会导出很多变量供子 makefile 使用。这边重点看第 369 行导出的诸如 ARCH 等这些变量。
- 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
makefile 中的 export、unexport 的作用和 sh 命令中的 export、unset 一样。
ARCH 等变量在 config.mk 文件中定义,"%"=% 代表提取变量中双引号中的部分。会依据这些变量 include 指定目录下的 config.mk 文件,比如 ARCH 变量为 arm,CPU 变量为 armv7,则会读取 arch/arm/cpu/armv7/config.mk 文件。
- ARCH := $(CONFIG_SYS_ARCH:"%"=%)
- CPU := $(CONFIG_SYS_CPU:"%"=%)
- ifdef CONFIG_SPL_BUILD
- ifdef CONFIG_TEGRA
- CPU := arm720t
- endif
- endif
- BOARD := $(CONFIG_SYS_BOARD:"%"=%)
- ifneq ($(CONFIG_SYS_VENDOR),)
- VENDOR := $(CONFIG_SYS_VENDOR:"%"=%)
- endif
- ifneq ($(CONFIG_SYS_SOC),)
- SOC := $(CONFIG_SYS_SOC:"%"=%)
- endif
- 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
而这些源头的 CONFIG_SYS_ 打头的变量都定义在第 8 节中提及的 .config 文件里。
- CONFIG_SYS_ARCH="arm"
- CONFIG_SYS_CPU="armv7"
- CONFIG_SYS_SOC="mx6"
- CONFIG_SYS_VENDOR="freescale"
- CONFIG_SYS_BOARD="mx6ullevk"
- CONFIG_SYS_CONFIG_NAME="mx6ullevk"