[ARM Linux系统移植] U-Boot 顶层 Makefile 分析 - 基础知识

编译 u-boot 的时候,是从顶层 makefile 开始建立依赖关系的,所以在阅读 u-boot 源码的时候需要首先阅读一下顶层的 makefile。然而即使是单独的顶层 makefile,内容也比较繁杂,在不甚了解的情况下不好组织文章结构。因此这篇笔记跟随开发板配套文档里面的目录结构,以 u-boot 顶层 makefile 中零散的功能点为契机,学习一下 makefile 的基础知识。学习到的 makefile 基础知识,我会单独在绿色提示框里给出。

1. 版本号

在 makefile 的开始定义了各种版本号。

  1. VERSION = 2016
  2. PATCHLEVEL = 03
  3. SUBLEVEL =
  4. EXTRAVERSION =
  5. NAME =

当变量用来表示用户在命令行或环境中所自定义的常数时,习惯上会全部以大写来编写其名称。至于只在 makefile 文件中出现的变量,则会全部以小写来编写其名称。

要取得某个变量的值,请使用 $() 括住该变量的名称。

2. MAKEFLAGS 变量

MAKEFLAGS 如其名,是指定 make 程序选项的。相关选项的含义可以通过 make -help 查看。如下代码中的 -r 选项指禁用内置的隐含规则;-R 选项指禁用内置的变量;--include-dir 选项指定了搜索的目录。CURDIR 是标准内置变量,表示 make 进程的当前工作目录。

  1. MAKEFLAGS += -rR --include-dir=$(CURDIR)
CURDIR :

正在执行 make 进程的当前工作目录。此变量的值将会是 shell 变量 PWD 的值(表明你是从哪个目录运行 make 程序的),除非 make 在运行时用到了 --directory (或 -C) 选项。--directory 选项会使得 make 在搜索任何 makefile 之前变更到不同的目录。如果你使用的是 --directory 的形式,CURDIR 将会包含 --include-dir 的目录参数。

3. 命令输出

如图 1 所示,uboot 默认的编译是只显示短指令的,并且还有很多编译指令不显示出来。这样的好处是界面清爽,不利的地方是不便于分析 u-boot 的编译过程。而用户可以指定变量 V 来控制指令是否显示以及是显示短指令还是完整指令。

图1 默认的编译显示

V 变量的逻辑为如下片段:首先 V 变量要来自命令行。V = 1 时,不隐藏指令显示,并且显示完整指令;V = 0 时,隐藏指令显示,并且显示短指令。

  1. ifeq ("$(origin V)", "command line")
  2.   KBUILD_VERBOSE = $(V)
  3. endif
  4. ifndef KBUILD_VERBOSE
  5.   KBUILD_VERBOSE = 0
  6. endif
  7.  
  8. ifeq ($(KBUILD_VERBOSE),1)
  9.   quiet =
  10.   Q =
  11. else
  12.   quiet=quiet_
  13.   Q = @
  14. endif
$(origin variable) :

origin 函数将会返回描述变量来自何处的字符串。这个变量可以协助你决定如何使用一个变量的值。举例来说,如果一个变量来自环境,或许你想要忽略该变量的值;如果该变量来自命令行,你就不会这么做。

origin 的可能返回值包括:undefined、default、environment、environment override、file、command line、override、automatic。

关于隐藏指令显示是使用了 make 中的 @ 符号,@ 符号加在指令前面就不会显示这条指令。而 Q 变量就是控制是否有@ 符号的。挑选代表的 make 语句如下:

  1. scripts_basic:
  2.     $(Q)$(MAKE) $(build)=scripts/basic
@ 命令修饰符:

不要输出命令。当你想将某个工作目标的所有命令全都隐藏起来的时候,如果考虑到旧版兼容性,你可以把该工作目标设为特殊工作目标 .SILENT 的一个必要条件。不过,最好能够使用 @,因为它可以应用在脚本中个别的命令上。如果你将这个修饰符应用在所有的工作目标上,你可以使用 -silent (或 -s)选项。

关于短指令显示的实现,是 makefile 中让各种指令对应了短指令版本,比如下面是 rm 指令对应的短版本和完整版本,可以看到对应使用了 CLEAN 作为短命令。

  1. quiet_cmd_rmdirs = $(if $(wildcard $(rm-dirs)),CLEAN   $(wildcard $(rm-dirs)))
  2.       cmd_rmdirs = rm -rf $(rm-dirs)

最终使用的情况为如下例子,会扩展 cmd 这个变量来回显,而其中就会根据 quiet 变量决定是否使用短命令。

  1. clean: $(clean-dirs)
  2.     $(call cmd,rmdirs)

cmd 变量可以使用如下指令方便的定位到在何处定义。cmd 变量的具体实现目前不再展开叙述。

make -p | grep -B1 -E '^cmd '

4. 静默输出

正常情况下编译的时候还是会有短命令显示,使用 make -s 就可以使所有的指令不再显示,即静默输出。

makefile 中会根据 make 程序的版本号,使用不同的模板来匹配是否包含 -s 选项,并且更新 quiet 变量。quiet 变量针对 silent_ 的值是如何使用的还不得而知。

  1. ifneq ($(filter 4.%,$(MAKE_VERSION)),)  # make-4
  2. ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)
  3.   quiet=silent_
  4. endif
  5. else                    # make-3.8x
  6. ifneq ($(filter s% -s%,$(MAKEFLAGS)),)
  7.   quiet=silent_
  8. endif
  9. endif
$(filter pattern ...,text) :

filter 函数会将 text 视为一系列被空格隔开的单词,与 pattern 比较之后,接着会返回相符者。

filter 还可以接受多个(被空格隔开的)格式。正如之前所说,格式必须比较整个单词,才可以将相符的单词放在输出列表中。

一个模式中只可以包含一个 % 字符。如果模式包含了额外的 % 字符,那么第一个 % 字符除外,其余字符都会被视为文字字符。

5. 设置输出目录

变量 O 可以设置编译结果所存放的目录。不指定此变量的话,默认将编译产生的文件放在和源文件相同的目录,一般也不指定此变量。变量 O的设置逻辑和变量 V 一样,并基于此初始化其余的变量。

  1. ifeq ("$(origin O)", "command line")
  2.   KBUILD_OUTPUT := $(O)
  3. endif
  1. ifneq ($(KBUILD_OUTPUT),)
  2. # Invoke a second make in the output directory, passing relevant variables
  3. # check that the output directory actually exists
  4. saved-output := $(KBUILD_OUTPUT)
  5. KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \
  6.                                 && /bin/pwd)

6. 模块编译

可以通过 make M=dir 选项单独编译某个模块,也支持使用旧语法 make SUBDIRS=dir 指定。

  1. ifdef SUBDIRS
  2.   KBUILD_EXTMOD ?= $(SUBDIRS)
  3. endif
  4.  
  5. ifeq ("$(origin M)", "command line")
  6.   KBUILD_EXTMOD := $(M)
  7. endif

如果选择编译整个工程,那么之后会依赖于 all 条件;如果选择编译模块,那么之后会依赖于 modules 条件。

  1. PHONY += all
  2. ifeq ($(KBUILD_EXTMOD),)
  3. _all: all
  4. else
  5. _all: modules
  6. endif

之后会根据是否编译模块来设置相应的变量。因为一般编译整个工程,且 KBUILD_SRC 变量为空,所以最后 objtreesrcobjVPATH 变量都为当前目录。

  1. ifeq ($(KBUILD_SRC),)
  2.         # building in the source tree
  3.         srctree := .
  4. else
  5.         ifeq ($(KBUILD_SRC)/,$(dir $(CURDIR)))
  6.                 # building in a subdirectory of the source tree
  7.                 srctree := ..
  8.         else
  9.                 srctree := $(KBUILD_SRC)
  10.         endif
  11. endif
  12. objtree     := .
  13. src     := $(srctree)
  14. obj     := $(objtree)
  15.  
  16. VPATH       := $(srctree)$(if $(KBUILD_EXTMOD),:$(KBUILD_EXTMOD))
$(if condition,then-part,else-part) :

if 函数(不要跟条件指令 ifeq、ifne、ifdef 和 ifndef 搞混了)会根据条件表达式的求值结果,从两个宏中选一个出来,进行扩展的动作。如果 condition 扩展之后包含任何字符(即使是空格),那么它的求值结果为“真”,于是会对 then-par 进行扩展的动作;否则,如果 condition 扩展之后空无一物,那么它的求值结果为“假”,于是会对 else-part 进行扩展的动作。

7. 获取主机架构和系统

主机架构名称和系统名称都是通过 uname 命令获取的,-s (--kernel-name) 选项打印内核的名字,-m (--machine) 选项打印机器硬件的名字。同时还需要对名称做统一处理,比如将获取的架构名称中的 i.86 替换成 x86,将系统名称中的大小转化成小写等等。

  1. HOSTARCH := $(shell uname -m | \
  2.     sed -e s/i.86/x86/ \
  3.         -e s/sun4u/sparc64/ \
  4.         -e s/arm.*/arm/ \
  5.         -e s/sa110/arm/ \
  6.         -e s/ppc64/powerpc/ \
  7.         -e s/ppc/powerpc/ \
  8.         -e s/macppc/powerpc/\
  9.         -e s/sh.*/sh/)
  10.  
  11. HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \
  12.         sed -e 's/\(cygwin\).*/cygwin/')

8. 交叉编译器和配置文件

如果要编译的架构和主机架构一样,那么就不用配置交叉编译器了。然而需要编译 ARM 架构的 u-boot,主机架构是 x86_64,所以需要在命令行里设置 CROSS_COMPILE 变量为 arm-linux-gnueabihf- 。从 CC 变量的定义可以看到,CROSS_COMPILE 是交叉编译工具的前缀,所以最后的“横杠”不要忘记了。

  1. ifeq ($(HOSTARCH),$(ARCH))
  2. CROSS_COMPILE ?=
  3. endif
  1. AS      = $(CROSS_COMPILE)as
  2. # Always use GNU ld
  3. ifneq ($(shell $(CROSS_COMPILE)ld.bfd -v 2> /dev/null),)
  4. LD      = $(CROSS_COMPILE)ld.bfd
  5. else
  6. LD      = $(CROSS_COMPILE)ld
  7. endif
  8. CC      = $(CROSS_COMPILE)gcc
  9. CPP     = $(CC) -E
  10. AR      = $(CROSS_COMPILE)ar
  11. NM      = $(CROSS_COMPILE)nm
  12. LDR     = $(CROSS_COMPILE)ldr
  13. STRIP       = $(CROSS_COMPILE)strip
  14. OBJCOPY     = $(CROSS_COMPILE)objcopy
  15. OBJDUMP     = $(CROSS_COMPILE)objdump

KCONFIG_CONFIG 变量指明了 .config 文件。 .config 文件默认情况下是没有的,需要使用 make ???_defconfig 命令生成。

  1. KCONFIG_CONFIG  ?= .config

9. 调用 scripts/Kbuild.include

scripts/Kbuild.include 里定义了各种通用的变量,通过 include 指令读取。

  1. # We need some generic definitions (do not try to remake the file).
  2. scripts/Kbuild.include: ;
  3. include scripts/Kbuild.include
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 等这些变量。

  1. export VERSION PATCHLEVEL SUBLEVEL UBOOTRELEASE UBOOTVERSION
  2. export ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR
  3. export CONFIG_SHELL HOSTCC HOSTCFLAGS HOSTLDFLAGS CROSS_COMPILE AS LD CC
  4. export CPP AR NM LDR STRIP OBJCOPY OBJDUMP
  5. export MAKE AWK PERL PYTHON
  6. export HOSTCXX HOSTCXXFLAGS DTC CHECK CHECKFLAGS
  7.  
  8. export KBUILD_CPPFLAGS NOSTDINC_FLAGS UBOOTINCLUDE OBJCOPYFLAGS LDFLAGS
  9. export KBUILD_CFLAGS KBUILD_AFLAGS
export 指令

makefile 中的 export、unexport 的作用和 sh 命令中的 export、unset 一样。

ARCH 等变量在 config.mk 文件中定义,"%"=% 代表提取变量中双引号中的部分。会依据这些变量 include 指定目录下的 config.mk 文件,比如 ARCH 变量为 arm,CPU 变量为 armv7,则会读取 arch/arm/cpu/armv7/config.mk 文件。

config.mk 文件
  1. ARCH := $(CONFIG_SYS_ARCH:"%"=%)
  2. CPU := $(CONFIG_SYS_CPU:"%"=%)
  3. ifdef CONFIG_SPL_BUILD
  4. ifdef CONFIG_TEGRA
  5. CPU := arm720t
  6. endif
  7. endif
  8. BOARD := $(CONFIG_SYS_BOARD:"%"=%)
  9. ifneq ($(CONFIG_SYS_VENDOR),)
  10. VENDOR := $(CONFIG_SYS_VENDOR:"%"=%)
  11. endif
  12. ifneq ($(CONFIG_SYS_SOC),)
  13. SOC := $(CONFIG_SYS_SOC:"%"=%)
  14. endif
  1. CPUDIR=arch/$(ARCH)/cpu$(if $(CPU),/$(CPU),)
  2.  
  3. sinclude $(srctree)/arch/$(ARCH)/config.mk  # include architecture dependend rules
  4. sinclude $(srctree)/$(CPUDIR)/config.mk     # include  CPU  specific rules

而这些源头的 CONFIG_SYS_ 打头的变量都定义在第 8 节中提及的 .config 文件里。

.config 文件
  1. CONFIG_SYS_ARCH="arm"
  2. CONFIG_SYS_CPU="armv7"
  3. CONFIG_SYS_SOC="mx6"
  4. CONFIG_SYS_VENDOR="freescale"
  5. CONFIG_SYS_BOARD="mx6ullevk"
  6. CONFIG_SYS_CONFIG_NAME="mx6ullevk"