LV05-02-U-Boot-06-01-顶层Makefile基础解析

本文主要是uboot——uboot源码顶层Makefile基础解析的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
其他网站 kernel - Linux source code (v4.15) - Bootlin linux内核源码在线查看
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、U-Boot顶层Makefile基础

1. 版本号

版本号定义在这里:Makefile

1
2
3
4
5
VERSION = 2019
PATCHLEVEL = 04
SUBLEVEL =
EXTRAVERSION =
NAME =

VERSION 是主版本号,PATCHLEVEL 是补丁版本号,SUBLEVEL 是次版本号,这三个一 起构成了 uboot 的版本号,比如当前的 uboot 版本号就是“2019.04”。EXTRAVERSION 是附加 版本信息,NAME 是和名字有关的,一般不使用这两个。

2. MAKEFLAGS 变量

make 是支持递归调用的,也就是在 Makefile 中使用“make”命令来执行其他的 Makefile 文件,一般都是子目录中的 Makefile 文件。假如在当前目录下存在一个“subdir”子目录,这个 子目录中又有其对应的 Makefile 文件,那么这个工程在编译的时候其主目录中的 Makefile 就可 以调用子目录中的 Makefile,以此来完成所有子目录的编译。主目录的 Makefile 可以使用如下代码来编译这个子目录:

1
$(MAKE) -C subdir

$(MAKE)就是调用“make”命令,-C 指定子目录。有时候我们需要向子 make 传递变量, 这个时候使用“export”来导出要传递给子 make 的变量即可,如果不希望哪个变量传递给子 make 的话就使用“unexport”来声明不导出:

1
2
export VARIABLE   # 导出变量给子 make 
unexport VARIABLE # 不导出变量给子 make

有两个特殊的变量:“SHELL”和“MAKEFLAGS”,这两个变量除非使用“unexport”声明, 否则的话在整个make的执行过程中,它们的值始终自动的传递给子make。在uboot的主 Makefile 中有如下代码:

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

上述代码使用“+=”来给变量 MAKEFLAGS 追加了一些值,“-rR”表示禁止使用内置的隐 含规则和变量定义,“–include-dir”指明搜索路径,”$(CURDIR)”表示当前目录。

3. 命令输出

3.1 默认的输出情况

uboot 默认编译是不会在终端中显示完整的命令,都是短命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$  make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
scripts/kconfig/conf --syncconfig Kconfig
CHK include/config.h
UPD include/config.h
GEN include/autoconf.mk.dep
CFG u-boot.cfg
GEN include/autoconf.mk
# ......
*** Your GCC is older than 6.0 and will not be supported
HOSTCC scripts/dtc/dtc.o
# ......
HOSTCC tools/zynqimage.o
# ......
LD fs/fat/built-in.o
CC lib/efi_loader/efi_unicode_collation.o
# ......
OBJCOPY u-boot.srec
SYM u-boot.sym
DTC arch/arm/dts/imx6ull-14x14-ddr3-arm2.dtb
# ......
COPY u-boot.dtb
COPY u-boot.bin
CFGS u-boot-dtb.cfgout
MKIMAGE u-boot-dtb.imx
CFGCHK u-boot.cfg

在终端中输出短命令虽然看起来很清爽,但是不利于分析 uboot 的编译过程。

3.2 完整命令输出

可以通过设 置变量“V=1“来实现完整的命令输出,这个在调试 uboot 的时候很有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$  make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
make -f ./Makefile syncconfig
#......
make -f ./scripts/Makefile.build obj=scripts/kconfig syncconfig
mkdir -p include/config include/generated
scripts/kconfig/conf --syncconfig Kconfig
make -f ./scripts/Makefile.autoconf || \
{ rm -f include/config/auto.conf; false; }
if [ -d arch/arm/mach-mx6/include/mach ]; then \
dest=../../mach-mx6/include/mach; \
else \
dest=arch-mx6; \
fi; \
# ......
*** Your GCC is older than 6.0 and will not be supported
make -f ./scripts/Makefile.build obj=scripts
make -f ./scripts/Makefile.build obj=scripts/dtc
cc -Wp,-MD,scripts/dtc/.dtc.o.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu11 -Iscripts/dtc -Iscripts/dtc/libfdt -c -o scripts/dtc/dtc.o scripts/dtc/dtc.c
# ......
make -f ./scripts/Makefile.build obj=arch/arm/mach-imx u-boot-dtb.imx
cp u-boot-dtb.bin u-boot.bin
#......
./tools/mkimage -n u-boot-dtb.cfgout -T imximage -e 0x87800000 -d u-boot-dtb.bin u-boot-dtb.imx >u-boot-dtb.imx.log && cat u-boot-dtb.imx.log
Image Type: Freescale IMX Boot Image
Image Ver: 2 (i.MX53/6/7 compatible)
Mode: DCD
Data Size: 675840 Bytes = 660.00 KiB = 0.64 MiB
Load Address: 877ff420
Entry Point: 87800000
./scripts/check-config.sh u-boot.cfg ./scripts/config_whitelist.txt .

3.3 命令输出控制

3.3.1 命令输出控制命令

顶层 Makefile 中控制命令输出的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
3.3.2 V的来源

V的来源在Makefile中是这一部分:

1
2
3
4
5
6
ifeq ("$(origin V)", "command line")
KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
KBUILD_VERBOSE = 0
endif

上述代码中先使用 ifeq 来判断"$(origin V)""command line"是否相等。这里用到了 Makefile 中的函数 origin,origin 和其他的函数不一样,它不操作变量的值,origin 用于告诉你变量是哪来的,语法为:

1
$(origin <variable>)

variable 是变量名,origin 函数的返回值就是变量来源,因此$(origin V)就是变量 V 的来源。 如果变量 V 是在命令行定义的那么它的来源就是"command line",这样"$(origin V)""command line"就相等了。当这两个相等的时候变量 KBUILD_VERBOSE 就等于 V 的值,比如在命令行中 输入“ V=1 “ 的 话 那 么 KBUILD_VERBOSE=1 。如果没有在命令行输入 V 的话 KBUILD_VERBOSE=0。

3.3.2 Q与quiet

这两个变量的控制在Makefile中是这一部分:

1
2
3
4
5
6
7
ifeq ($(KBUILD_VERBOSE),1)
quiet =
Q =
else
quiet=quiet_
Q = @
endif

判断 KBUILD_VERBOSE 是否为 1,如果 KBUILD_VERBOSE 为 1 的话变量 quiet和 Q 都为空,如果 KBUILD_VERBOSE=0 的话变量 quiet 为“quiet_“,变量 Q 为“@”,所以V的作用是:

  • V = 1
1
2
3
KBUILD_VERBOSE=1
quiet=空
Q=空
  • V = 0 或者命令行不定义 V
1
2
3
KBUILD_VERBOSE=0
quiet= quiet_
Q=@

3.4 怎么控制输出信息?

Makefile 中会用到变量 quiet 和 Q 来控制编译的时候是否在终端输出完整的命令,在顶层 Makefile 中有很多如下所示的命令:

1
2
tools-only: scripts_basic $(version_h) $(timestamp_h)
$(Q)$(MAKE) $(build)=tools

如果 V=0 的话上述命令展开就是:

1
@ make $(build)=tools

make 在执行的时候默认会在终 端输出命令,但是在命令前面加上“@”就不会在终端输出命令了。

当 V=1 的时候 Q 就为空, 上述命令就是:

1
make $(build)=tools

此时在 make 执行的过程,命令会被完整的输出在终端上。

有些命令会有两个版本,比如 Makefile 中的:

1
2
quiet_cmd_sym ?= SYM     $@
cmd_sym ?= $(OBJDUMP) -t $< > $@

sym 命令分为“quiet_cmd_sym”和“cmd_sym”两个版本,这两个命令的功能都是一样的, 区别在于 make 执行的时候输出的命令不同。quiet_cmd_xxx 命令输出信息少,也就是短命令, 而 cmd_xxx 命令输出信息多,也就是完整的命令。 如果变量 quiet 为空的话,整个命令都会输出。 如果变量 quiet 为“quiet_”的话,仅输出短版本。 如果变量 quiet 为“silent_”的话,整个命令都不会输出。这部分在Makefile - Beautify output 中有说明。

3.5 静默输出

3.5.1 静默输出命令

设置 V=0 或者在命令行中不定义 V 的话,编译 uboot 的时候终端中显示的短命令,但是还是会有命令输出,有时候我们在编译 uboot 的时候不需要输出命令,这个时候 就可以使用 uboot 的静默输出功能。编译的时候使用这个命令就可以实现:

1
make -s

即可实现静默输出,顶层 Makefile 中相应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 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

3.5.2 make版本号判断

先看Makefile 这一部分:

1
2
3
4
5
ifneq ($(filter 4.%,$(MAKE_VERSION)),)	# make-4
# (1)......
else # make-3.8x
# (2)......
endif

这里在判断当前正在使用的make命令版本号是否为 4.x,首先进行如下判断:

1
判断 $(filter 4.%,$(MAKE_VERSION))" "(空)是否相等?

如果不相等的话就成立,执行里面的(1)语句。这里用到了 Makefile 中的 filter 函数,这是 个过滤函数,函数格式如下:

1
$(filter <pattern...>,<text>)

filter 函数表示以 pattern 模式过滤 text 字符串中的单词,仅保留符合模式 pattern 的单词, 可以有多个模式。函数返回值就是符合 pattern 的字符串。

因此$(filter 4.%,$(MAKE_VERSION)) 的含义就是在字符串 “MAKE_VERSION” 中找出符合“4.%”的字符(%为通配符), MAKE_VERSION 是make工具的版本号,ubuntu16.04里面默认自带的make工具版本号为4.1,之后的版本应该都是4.1及以上了, 我们可以在ubuntu中输入“make -v”查看。所以:

1
$(filter 4.%,$(MAKE_VERSION)) != " "

这里条件成立。后续走(1)处的语句。

3.5.3 确定quiet的值

上面我们已经确认条件成立,所以接下来走的是 Makefile 这一部分的命令:

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

这里也是一个判断语句

1
$(filter %s ,$(firstword x$(MAKEFLAGS))) != " " (空)

如果上面的条件成立,变量 quiet 等于“silent_”。这里也用到了函数 filter:

1
$(firstword x$(MAKEFLAGS)))

在上面这个函数中过滤出符合“%s”的单词。到了函数 firstword,函数 firstword 是获取首单词,函数格式如下:

1
$(firstword <text>)

firstword 函数用于取出 text 字符串中的第一个单词,函数的返回值就是获取到的单词。当 使用“make -s”编译的时候,“-s”会作为 MAKEFLAGS 变量的一部分传递给 Makefile。在顶层 Makefile 的这段命令之后添加以下代码:

1
2
3
4
5
6
7
ifneq ($(filter 4.%,$(MAKE_VERSION)),)	# make-4
#......
else # make-3.8x
# ......
endif
my_test:
@echo "first word="$(firstword x$(MAKEFLAGS))

然后我们在源码目录执行以下命令,看一下这个到底输出的是啥:

1
2
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$  make -s ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- my_test -j16
first word=xrRs

从输出可以看出第一个单词是“xrRs”,把下面的命令展开

1
2
3
$(filter %s ,$(firstword x$(MAKEFLAGS))) 
# 展开就是
$(filter %s, xrRs)

而$(filter %s, xrRs)的返回值肯定不为空,条件成立,所以quiet=silent_。

3.6 导出变量

上面关于命令输出的变量的值都已经确定完毕了,最后使用export命令导出,导出命令在这里:Makefile

1
export quiet Q KBUILD_VERBOSE

使用 export 导出变量 quiet、Q 和 KBUILD_VERBOSE。

4. 设置编译结果输出目录

4.1 输出目录控制命令 

uboot 可以将编译出来的目标文件输出到单独的目录中,在 make 的时候使用“O”来指定 输出目录。比如:

1
make O=out

就是设置目标文件输出到 out 目录中。这么做是为了将源文件和编译产生的文件分开,当然也可以不指定 O 参数,不指定的话源文件和编译产生的文件都在 同一个目录内,一般在进行uboot编译的时候我们不指定 O 参数。在 Makefile 中实现设置输出目录的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# ......
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)
$(if $(KBUILD_OUTPUT),, \
$(error failed to create output directory "$(saved-output)"))

PHONY += $(MAKECMDGOALS) sub-make

$(filter-out _all sub-make $(CURDIR)/Makefile, $(MAKECMDGOALS)) _all: sub-make
@:

sub-make: FORCE
$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))

# Leave processing to above invocation of make
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)
endif # ifeq ($(KBUILD_SRC),)

4.2 O参数来源判断

Makefile文件判断O参数来源的代码如下:

1
2
3
ifeq ("$(origin O)", "command line")
KBUILD_OUTPUT := $(O)
endif

行判断“O”是否来自于命令行,如果来自命令行的话条件成立,KBUILD_OUTPUT 就为$(O),因此变量 KBUILD_OUTPUT 就是输出目录。

4.3 创建KBUILD_OUTPUT目录

还需要判断一下这个KBUILD_OUTPUT目录是否存在,不存在的话,会进行创建,Makefile 中对应的代码如下:

1
2
3
4
5
saved-output := $(KBUILD_OUTPUT)
KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \
&& /bin/pwd)
$(if $(KBUILD_OUTPUT),, \
$(error failed to create output directory "$(saved-output)"))

这里相当于将KBUILD_OUTPUT重新赋值了一下,从相对路径变为绝对路径。经过这里之后,一个不存在的输出目录就被创建好了。

5. 代码检查

5.1 代码检查控制命令

uboot 支持代码检查,命令如下:

1
2
make C=1 # 使能代码检查,检查那些需要重新编译的文件。
make C=2 # 用于检查所有的源码文件

5.2 KBUILD_CHECKSRC

顶层 Makefile 中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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 "doc/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。

6. 模块编译

6.1 模块编译命令

在 uboot 中允许单独编译某个模块,使用以下命令:

1
2
make M=dir
make SUBDIRS=dir # 旧语法是这样的,也是支持的。

6.2 KBUILD_EXTMOD

我们看一下 Makefile 中对这个参数的定义:

1
2
3
4
5
6
7
8
9
10
# 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

首先判断是否定义了 SUBDIRS , 如果定义了 SUBDIRS , 变量 KBUILD_EXTMOD=SUBDIRS,这里是为了支持老语法“make SUBIDRS=dir” 。

接着后面是判断是否在命令行定义了 M,如果定义了的话 KBUILD_EXTMOD=$(M)。

6.3 编译目标

接着是根据上面的KBUILD_EXTMOD是否为空来确定编译目标,我们看一下 Makefile 文件:

1
2
3
4
5
6
7
8
# 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

判断 KBUILD_EXTMOD 时为空,如果为空的话目标_all 依赖 all,因此要先编译出 all。否则的话默认目标_all 依赖 modules,要先编译出 modules,也就是编译模块。一般情况 下我们不会在 uboot 中编译模块,所以此处会编译 all 这个目标。

7. VPATH的配置

看一下 Makefile 这一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

第 1~11 行判断 KBUILD_SRC 是否为空,如果为空的话就设置变量 srctree 为当前目录,即 srctree 为“.”,一般不设置 KBUILD_SRC。

第 12 行设置变量 objtree 为当前目录。

第 13~14 行分别设置变量 src 和 obj,根据前面的变量的值,这里都为当前目录。

第 16 行设置 VPATH。 这里会KBUILD_EXTMOD是否为空,要是非空,这里就会加入KBUILD_EXTMOD路径。也就是要编译的模块的路径。

第 18 行导出变量 scrtree、objtree 和 VPATH。

8. 编译平台

8.1 主机架构 HOSTARCH

HOSTARCH在Makefile中定义如下:

1
2
3
4
5
6
7
8
9
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/)

变量 HOSTARCH,用于保存主机架构,这里调用 shell 命令“uname -m”获取架构名称:

1
2
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$  uname -m
x86_64

可以看出当前ubuntu主机架构为“x86_64”,shell 中的“|”表示管道,意思是将 左边的输出作为右边的输入,sed -e 是替换命令,“sed -e s/i.86/x86/”表示将管道输入的字符串 中的“i.86”替换为“x86”,其他的“sed -e s”命令同理。对于我现在运行的ubuntu系统而言,HOSTARCH=x86_64。

8.2 主机操作系统 HOSTOS

HOSTOS在 Makefile 中定义如下:

1
2
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \
sed -e 's/\(cygwin\).*/cygwin/')

变量 HOSTOS,此变量用于保存主机 OS 的值,先使用 shell 命令“uname -s”来获取主机 OS,结果下所示:

1
2
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$  uname -s
Linux

可以看出此时的主机 OS 为“Linux”,使用管道将“Linux”作为后面“ tr ‘[:upper:]‘ ‘[:lower:]‘ ”的输入,“ tr ‘[:upper:]‘ ‘[:lower:]‘ ”表示将所有的大写字母替换为小写字母,因此得到 “ linux ”。最后同样使用管道,将“ linux ”作为“ sed -e ‘s/(cygwin).*/cygwin/‘ ”的输入,用于将 cygwin.*替换为 cygwin。因此,现在在这里HOSTOS=linux。

8.3 导出HOSTARCH和HOSTOS

接着导出这两个变量,在Makefile中定义如下:

1
export	HOSTARCH HOSTOS

8.4 目标架构和编译器

8.4.1 设置命令

编译 uboot 的时候需要设置目标板架构和交叉编译器,我们一般在使用make命令的时候传入:

1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

这个命令就是用于设置 ARCH 和CROSS_COMPILE。

8.4.2 ARCH与CROSS_COMPILE

在顶层 Makefile 中代码如下:

1
2
3
ifeq ($(HOSTARCH),$(ARCH))
CROSS_COMPILE ?=
endif

这里判断 HOSTARCH 和 ARCH 这两个变量是否相等,前面我们知道主机架构(变量 HOSTARCH)是 x86_64,ARCH就是通过make命令传入,要是没有传入,那ARCH就为空,我们编译的是 ARM 版本 uboot,肯定不相等,所以 CROSS_COMPILE= arm-linux-gnueabihf-。当然,要是我们在ARM开发板装了GCC,这里就不需要前缀了,因为开发板上装的GCC一定就是ARM版本的,可以直接本地编译,不需要交叉编译。

可以看到,每次编译 uboot 的时候都要在 make 命令后面设置 ARCH 和 CROSS_COMPILE,使用起来很麻烦(当然,写shell脚本调用Makefile的除外),我们完全可以直接修改顶层 Makefile,在里面加入 ARCH 和 CROSS_COMPILE 的定义,如下所示:

1
2
ARCH          ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-

这样直接在顶层 Makefile 里面定义 ARCH 和 CROSS_COMPILE,这样就不用每次编译的时候都要在 make 命令后面定义 ARCH 和 CROSS_COMPILE,直接一个make就可以了。

9. 配置文件

1
2
KCONFIG_CONFIG	?= .config
export KCONFIG_CONFIG

uboot 是可以配置的,这里设置配置文件为 .config。.config 文件默认是没有的,需要使用命令:

1
make xxx_defconfig

对 uboot 进行配置,配置完成以后就会在 uboot 根目录下生成 .config。默认情况下.config 和 xxx_defconfig 内容是一样的,因为 .config 就是从 xxx_defconfig 复制过来的。如果后续自行调整 了 uboot 的一些配置参数,那么这些新的配置参数就添加到了.config 中,而不是 xxx_defconfig。 相当于 xxx_defconfig 只是一些初始配置,而 .config 里面的才是实时有效的配置。我们在图形配置界面进行的各种配置也会被写入到这个 .config 文件中。

10. 通用定义

10.1 Kbuild.include

这里还会有一些通用的定义在 scripts/Kbuild.include 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
####
# kbuild: Generic definitions

# Convenient variables
comma := ,
quote := "
squote := '
empty :=
space := $(empty) $(empty)
pound := \#

###
# Name of target with a '.' as filename prefix. foo/bar.o => foo/.bar.o
dot-target = $(dir $@).$(notdir $@)

###
# The temporary file to save gcc -MD generated dependencies must not
# contain a comma
depfile = $(subst $(comma),_,$(dot-target).d)

10.2 怎么被包含到Makefile中?

Makefile中调用的代码:

1
2
3
# We need some generic definitions (do not try to remake the file).
scripts/Kbuild.include: ;
include scripts/Kbuild.include

在 uboot 的编译过程中会用到 scripts/Kbuild.include 中的这些变量,后面用到的时候再分析。

11. 相关变量

11.1 交叉编译工具变量

上面我们只是设置了 CROSS_COMPILE 的名字,但是交叉编译器其他的工具还没有设置,顶层 Makefile 中相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
LEX = flex
YACC = bison
AWK = awk
PERL = perl
PYTHON ?= python
PYTHON2 = python2
PYTHON3 = python3
DTC ?= $(objtree)/scripts/dtc/dtc

11.2 编译选项

我们编译的时候还有一些选项的定义,这些在顶层的 Makefile中也有定义,例如这里就有一些宏和标志:

1
2
3
4
5
6
7
8
9
10
CHECKFLAGS     := -D__linux__ -Dlinux -D__STDC__ -Dunix -D__unix__ \
-Wbitwise -Wno-return-void -D__CHECK_ENDIAN__ $(CF)

KBUILD_CPPFLAGS := -D__KERNEL__ -D__UBOOT__

KBUILD_CFLAGS := -Wall -Wstrict-prototypes \
-Wno-format-security \
-fno-builtin -ffreestanding $(CSTD_FLAG)
KBUILD_CFLAGS += -fshort-wchar -fno-strict-aliasing
KBUILD_AFLAGS := -D__ASSEMBLY__

11.3 变量导出

接下来在顶层 Makefile 会导出很多变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 LEX YACC AWK PERL PYTHON PYTHON2 PYTHON3
export HOSTCXX HOSTCXXFLAGS CHECK CHECKFLAGS DTC DTC_FLAGS

export KBUILD_CPPFLAGS NOSTDINC_FLAGS UBOOTINCLUDE OBJCOPYFLAGS LDFLAGS
export KBUILD_CFLAGS KBUILD_AFLAGS

# When compiling out-of-tree modules, put MODVERDIR in the module
# tree rather than in the kernel tree. The kernel tree might
# even be read-only.
export MODVERDIR := $(if $(KBUILD_EXTMOD),$(firstword $(KBUILD_EXTMOD))/).tmp_versions

# Files to ignore in find ... statements

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
export RCS_TAR_IGNORE := --exclude SCCS --exclude BitKeeper --exclude .svn \
--exclude CVS --exclude .pc --exclude .hg --exclude .git

这些变量中大部分都已经在前面定义了,我们重点来看一下下面这几个变量:

1
ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR

这 7 个变量在顶层 Makefile 是找不到的,说明这 7 个变量是在其他文件里面定义的。先来看一下这 7 个变量都是什么内容,在顶层 Makefile 中导出变量结束的位置输入如图所示的内容:

1
2
3
4
5
6
7
8
9
10
11
12
# ......
export RCS_TAR_IGNORE := --exclude SCCS --exclude BitKeeper --exclude .svn \
--exclude CVS --exclude .pc --exclude .hg --exclude .git

my_test:
@echo "ARCH"=$(ARCH)
@echo "CPU"=$(CPU)
@echo "BOARD"=$(BOARD)
@echo "VENDOR"=$(VENDOR)
@echo "SOC"=$(SOC)
@echo "CPUDIR"=$(CPUDIR)
@echo "BOARDDIR"=$(BOARDDIR)

然后执行以下命令看一下打印信息:

1
2
3
4
5
6
7
8
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$  make -s ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- my_test -j16
ARCH=arm
CPU=armv7
BOARD=mx6ullevk
VENDOR=freescale
SOC=mx6
CPUDIR=arch/arm/cpu/armv7
BOARDDIR=freescale/mx6ullevk

可以看到这 7 个变量的值,这 7 个变量是从哪里来的呢?在 uboot 根目录下有个文件叫做 config.mk,这 7 个变量就是在 config.mk 里面定义的,打开 config.mk 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# ......
# clear VENDOR for tcsh
VENDOR :=
#########################################################################

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),)
# ......
ifneq ($(BOARD),)
ifdef VENDOR
BOARDDIR = $(VENDOR)/$(BOARD)
else
BOARDDIR = $(BOARD)
endif
endif
# ......

接下来需要找到 CONFIG_SYS_ARCH、 CONFIG_SYS_CPU、 CONFIG_SYS_BOARD、CONFIG_SYS_VENDOR 和 CONFIG_SYS_SOC 这 5 个变量的值。这 5 个变量在 uboot 根目录下的 .config 文件(这个文件需要执行配置命令后才会生成)中有定义,,可以打开看一下,搜索一下这个几个变量,会发现他们在 .config 文件中定义如下:

1
2
3
4
5
6
CONFIG_SYS_ARCH="arm"
CONFIG_SYS_CPU="armv7"
CONFIG_SYS_SOC="mx6"
CONFIG_SYS_VENDOR="freescale"
CONFIG_SYS_BOARD="mx6ull_alpha_emmc"
CONFIG_SYS_CONFIG_NAME="mx6ull_alpha_emmc"

主要是这里我是用的自己移植的板子,所以这些名字什么都不太一样,不过用nxp evk评估板的默认配置文件的话会是对应的。所以就有:

1
2
3
4
5
6
7
ARCH = arm
CPU = armv7
BOARD = mx6ullevk
VENDOR = freescale
SOC = mx6
CPUDIR = arch/arm/cpu/armv7
BOARDDIR = freescale/mx6ullevk

会发现和上面的打印信息是一样的。再来分析 config.mk ,就会发现在 config.mk 中读取的文件有 :

1
2
3
4
arch/arm/config.mk
arch/arm/cpu/armv7/config.mk
arch/arm/cpu/armv7/mx6/config.mk (此文件不存在)
board/ freescale/mx6ullevk/config.mk (此文件不存在)

二、Makefile过程分析

1.  make xxx_defconfig

在编译 uboot 之前要使用“make xxx_defconfig”命令来配置 uboot,那么这个配置过程是如何运行的呢?接下来就来了解一下吧。这个我们从这里开始:Makefile · 苏木/u-boot - 码云 - 开源中国

1.1 版本与时间

1.1.1 version_h

先来看一些这个变量,它在Makefile中定义如下:

1
version_h := include/generated/version_autogenerated.h

这个变量保存版本号文件,此文件是自动生成的。文件 include/generated/version_autogenerated.h (编译后才会生成)内容如下:

1
2
3
4
#define PLAIN_VERSION "2019.04-gf32bcf5b"
#define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
#define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 8.3.0"
#define LD_VERSION_STRING "GNU ld (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 2.32.0.20190321"

1.1.2 timestamp_h

变量 timestamp_h在 Makefile 中定义如下:

1
timestamp_h := include/generated/timestamp_autogenerated.h

这个变量保存时间戳文件,此文件也是编译自动生成的。文件 include/generated/timestamp_autogenerated.h 内容如下:

1
2
3
4
5
#define U_BOOT_DATE "Nov 19 2024"
#define U_BOOT_TIME "21:45:10"
#define U_BOOT_TZ "+0800"
#define U_BOOT_DMI_DATE "11/19/2024"
#define U_BOOT_BUILD_DATE 0x20241119

1.2 三个变量

接下来看几个变量,定义在Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
no-dot-config-targets := clean clobber mrproper distclean \
help %docs check% coccicheck \
ubootversion backup tests check qcheck

config-targets := 0
mixed-targets := 0
dot-config := 1

ifneq ($(filter $(no-dot-config-targets), $(MAKECMDGOALS)),)
ifeq ($(filter-out $(no-dot-config-targets), $(MAKECMDGOALS)),)
dot-config := 0
endif
endif

ifeq ($(KBUILD_EXTMOD),)
ifneq ($(filter config %config,$(MAKECMDGOALS)),)
config-targets := 1
ifneq ($(words $(MAKECMDGOALS)),1)
mixed-targets := 1
endif
endif
endif

第 1 行定义了变量 no-dot-config-targets。

第 5 行定义了变量 config-targets,初始值为 0。

第 6 行定义了变量 mixed-targets,初始值为 0。

第 7 行定义了变量 dot-config,初始值为 1。

第 9 行将 MAKECMDGOALS 中不符合 no-dot-config-targets 的部分过滤掉,剩下的如果不为空的话条件就成立。MAKECMDGOALS 是 make 的一个环境变量,这个变量会保存我们所指 定的终极目标列表,比如执行“make mx6ull_14x14_evk_defconfig”,那么 MAKECMDGOALS 就为 mx6ull_14x14_evk_defconfig。很明显过滤后为空,所以条件不成立,变量 dot-config 依 旧为 1。

第 15 行判断KBUILD_EXTMOD是否为空,如果KBUILD_EXTMOD为空的话条件成立, 经过前面的分析,我们知道 KBUILD_EXTMOD 为空,所以条件成立。

第 16 行将 MAKECMDGOALS 中不符合“config”和“%config”的部分过滤掉,如果剩 下的部分不为空条件就成立,MAKECMDGOALS = mx6ull_14x14_evk_defconfig,所以此处条件成立,变量 config-targets=1。

第 18 行统计 MAKECMDGOALS 中的单词个数,如果不为 1 的话条件成立。此处调用 Makefile 中的 words 函数来统计单词个数,words 函数格式如下:

1
$(words <text>)

MAKECMDGOALS 的单词个数是 1 个,所以条件不成立,mixed-targets 继续为 0。

综上所述,这些变量值如下:

1
2
3
config-targets = 1
mixed-targets = 0
dot-config = 1

1.3 三个变量的使用

上面确定了那三个变量的值,我们继续往下看 Makefile

1
2
3
4
5
ifeq ($(mixed-targets),1)
# (1)......
else
# (2)......
endif

如果变量 mixed-targets 为 1 的话条件成立,这里显然不成立了,所以会走2)这里,(2)这里在 Makefile 中是:

1
2
3
4
5
ifeq ($(config-targets),1)
# (2.1)......
else
# (2.2)......
endif

如果变量 config-targets 为 1 的话条件成立,很明显,条件成立,执行这个(2.1)分支。这个(2.1)在 Makefile 中的代码,我们下面接着分析。

1.4 %config

我们来看一下上面(2.1)在Makefile 中的代码:

1
2
3
4
5
6
7
8
9
10
11
# *config targets only - make sure prerequisites are updated, and descend
# in scripts/kconfig to make the *config target

KBUILD_DEFCONFIG := sandbox_defconfig
export KBUILD_DEFCONFIG KBUILD_KCONFIG

config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

第 7 行,没有目标与之匹配,所以不执行。

第 10 行,有目标与之匹配,当输入“make xxx_defconfig”的时候就会匹配到%config 目标,目标“%config”依赖于 scripts_basic、 outputmakefile 和 FORCE。

1.4.1 FORCE

FORCE 在顶层 Makefile中定义如下:

1
2
3
4
5
6
PHONY += FORCE
FORCE:

# Declare the contents of the .PHONY variable as phony. We keep that
# information in a variable so we can use it in if_changed and friends.
.PHONY: $(PHONY)

可以看出 FORCE 是没有规则和依赖的,所以每次都会重新生成 FORCE。当 FORCE 作为 其他目标的依赖时,由于 FORCE 总是被更新过的,因此依赖所在的规则总是会执行的。

1.4.2 scripts_basic

依赖 scripts_basic在Makefile中定义如下:

1
2
3
4
5
6
7
8
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
$(Q)rm -f .tmp_quiet_recordmcount

# To avoid any implicit rule to kick in, define an empty command.
scripts/basic/%: scripts_basic ;

这是scripts_basic 的规则,其对应的命令用到了变量 Q、MAKE 和 build,其中:

1
2
Q=@或为空
MAKE=make

变量 build 是在 scripts/Kbuild.include文件中有定义,定义如下:

1
2
3
4
5
###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

经过前面的分析 可知,变量 srctree(Makefile · srctree) 为”.”,所以有:

1
build=-f ./scripts/Makefile.build obj

scripts_basic 展开以后如下:

1
2
3
scripts_basic:
@make -f ./scripts/Makefile.build obj=scripts/basic # 也可以没有@,视配置而定
@rm -f . tmp_quiet_recordmcount # 也可以没有@

scripts_basic 会调用文件./scripts/Makefile.build,这个我们后面在分析。

1.4.3 %config展开

上面的几个依赖我们都找到了,接下来吧%config展开:

1
2
%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

把build的值替换展开后是:

1
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig

发现同样也跟文件./scripts/Makefile.build 有关,我们后面再分析此文件。

1.5 编译信息

我们在uboot源码目录下执行以下命令:

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig

主要是看第二条命令的打印信息:

image-20241119220727685

可以看到里面有两条 make -f 开头的命令:

  • scripts_basic 目标对应的命令
1
make -f ./scripts/Makefile.build obj=scripts/basic
  • %config 目标对应的命令
1
make -f ./scripts/Makefile.build obj=scripts/kconfig mx6ull_14x14_evk_defconfig

1.6 Makefile.build 脚本

接下来我们就来看一下Makefile.build 脚本里面都有什么。从前边知道“ make xxx_defconfig“配置 uboot 的时候如下两行命令会执行脚本scripts/Makefile.build:

1
2
@make -f ./scripts/Makefile.build obj=scripts/basic
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig

1.6.1 scripts_basic 目标对应的命令

scripts_basic 目标对应的命令为:

1
@make -f ./scripts/Makefile.build obj=scripts/basic
  • src与prefix变量

先看一下scripts/Makefile.build最开始的两个变量:

1
2
3
4
5
6
7
8
9
10
# 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

第 2 行,变量 prefix 值为 tpl。

第 3 行定义了变量 src,这里用到了函数 patsubst,此行代码展开后为:

1
$(patsubst tpl/%,%, scripts/basic)

patsubst 是替换函数,格式如下:

1
$(patsubst <pattern>,<replacement>,<text>)

此函数用于在 text 中查找符合 pattern 的部分,如果匹配的话就用 replacement 替换掉。 pattern 是可以包含通配符“%”,如果 replacement 中也包含通配符“%”,那么 replacement 中的 这个“%”将是 pattern 中的那个“%”所代表的字符串。函数的返回值为替换后的字符串。因 此,第 3 行就是在“scripts/basic”中查找符合“tpl/%”的部分,然后将“tpl/”取消掉,但是 “scripts/basic”没有“tpl/”,所以 src= scripts/basic。

第 4 行判断变量 obj 和 src 是否相等,相等的话条件成立,很明显,此处条件成立。

第 6 行和第 3 行一样,只是这里处理的是“spl”,“scripts/basic”里面也没有“spl/”,所以 src 继续为 scripts/basic。

第 8 行因为变量 obj 和 src 相等,所以 prefix=. 。

  • kbuild-dir与kbuild-file

我们继续往下看scripts/Makefile.build,这里有两个变量:

1
2
3
4
# The filename Kbuild has precedence over Makefile
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)

我们前面分析过srctree=.,src=scripts/basic, 将 kbuild-dir 展开后为:

1
kbuild-dir := $(if $(filter /%, scripts/basic), scripts/basic, ./scripts/basic)

因为没有以“/”为开头的单词,所以$(filter /%, scripts/basic)的结果为空,kbuild-dir=./scripts/basic。得到这个变量后,我们将 kbuild-file 展开后为:

1
kbuild-file := $(if $(wildcard ./scripts/basic/Kbuild), ./scripts/basic/Kbuild, ./scripts/basic/Makefile)

因为 scrpts/basic 目录中没有 Kbuild 这个文件,所以 kbuild-file = ./scripts/basic/Makefile。最后将第 4 行展开,即:

1
include ./scripts/basic/Makefile

也就是读取 scripts/basic 下面的 Makefile 文件。

  • __build

我们继续向下看scripts/Makefile.build,会看到这个__build:

1
2
3
4
5
6
# We keep a list of all modules in $(MODVERDIR)

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
$(subdir-ym) $(always)
@:

__build 是默认目标,因为下面这条命令

1
@make -f ./scripts/Makefile.build obj=scripts/basic

没有指定目标,所以会使用到默认目标:__build。在顶层 Makefile 中,KBUILD_BUILTIN 为 1,KBUILD_MODULES 为 0,因此展开后目标__build 为:

1
2
__build:$(builtin-target) $(lib-target) $(extra-y) $(subdir-ym) $(always)
@:

可以看出目标__build 有 5 个依赖:builtin-target、lib-target、extra-y、subdir-ym 和 always。 这 5 个依赖的具体内容我们就不通过源码来分析了,直接在 scripts/Makefile.build 中输入以下代码:

1
2
3
4
5
6
7
8
9
10
11
# We keep a list of all modules in $(MODVERDIR)

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
$(subdir-ym) $(always)
@:
@echo "builtin-target="$(builtin-target)
@echo "lib-target="$(lib-target)
@echo "extra-y="$(extra-y)
@echo "subdir-ym="$(subdir-ym)
@echo "always="$(always)

然后我重新执行以下命令:

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig

会看到有如下打印信息出现:

1
2
3
4
5
builtin-target=
lib-target=
extra-y=
subdir-ym=
always=scripts/basic/fixdep

可以看出,只有 always 有效,因此__build 最终为:

1
2
__build:scripts/basic/fixdep
@:

__build 依赖于 scripts/basic/fixdep,所以要先编译 scripts/basic/fixdep.c,生成 fixdep,前面已经读取了 scripts/basic/Makefile 文件。

总的来说scripts_basic 目标的作用就是编译出 scripts/basic/fixdep 这个软件。

1.6.2 %config 目标对应的命令

%config 目标对应的命令为:

1
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig  

按照前面的分析,相关的变量值如下所示:

1
2
3
4
src= scripts/kconfig
kbuild-dir = ./scripts/kconfig
kbuild-file = ./scripts/kconfig/Makefile
include ./scripts/kconfig/Makefile

可以看出,Makefilke.build 会读取 scripts/kconfig/Makefile 中的内容,此文件中有如下所示内容:

1
2
3
4
5
6
%_defconfig: $(obj)/conf
$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)

# Added for U-Boot (backward compatibility)
%_config: %_defconfig
@:

这几行在 scripts/kconfig/Makefile · %_defconfig 附近。目标%_defconfig 刚好和我们输入的 xxx_defconfig 匹配,所以会执行这条规则。依赖为 $(obj)/conf,展开后就是 scripts/kconfig/conf。

接下来就是检查并生成依赖 scripts/kconfig/conf。 conf 是主机软件,到这里我们就停下来,不要纠结 conf 是怎么编译出来的,否则就越陷越深,太绕了,像 conf 这种主机所使用的工具类软件我们一般不关心它是如何编译产生的。如果一定要看是 conf 是怎么生成的,可以输入如下命令重新配置 uboot,在重新配置 uboot 的过程中就会 输出 conf 编译信息。

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig

会有下面的打印信息:

1
2
3
4
5
6
7
# ......
cc -Wp,-MD,scripts/kconfig/.zconf.tab.o.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu11 -D_GNU_SOURCE -D_DEFAULT_SOURCE -I/usr/include/ncursesw -DCURSES_LOC="<ncurses.h>" -DNCURSES_WIDECHAR=1 -DLOCALE -Iscripts/kconfig -c -o scripts/kconfig/zconf.tab.o scripts/kconfig/zconf.tab.c
cc -o scripts/kconfig/conf scripts/kconfig/conf.o scripts/kconfig/zconf.tab.o
scripts/kconfig/conf --defconfig=arch/../configs/mx6ull_14x14_evk_defconfig Kconfig
#
# configuration written to .config
#

这里的cc -o scripts/kconfig/conf这行就是在编译生成conf软件。得到 scripts/kconfig/conf 以后就要执行目标%_defconfig 的命令:

1
$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)

相关的变量值如下:

1
2
3
silent=-s 或为空
SRCARCH=..
Kconfig=Kconfig

将其展开就是:

1
@ scripts/kconfig/conf --defconfig=arch/../configs/xxx_defconfig Kconfig

上述命令用到了 xxx_defconfig 文件,比如 mx6ull_14x14_evk_defconfig 。这里会将mx6ull_14x14_evk_defconfig 中的配置输出到.config 文件中,最终生成 uboot 根目录下的.config 文件。

1.7 总结一下

image-20241119221119307

2. u-boot.bin的生成过程

2.1 目标文件

配置好 uboot 以后就可以直接 make 编译了,因为没有指明目标,所以会使用默认目标,主 Makefile 中的默认目标如下:

1
2
3
# That's our default target when none is given on the command line
PHONY := _all
_all:

目标_all 又依赖于 all,在 Makefile 中如下所示 :

1
2
3
4
5
6
7
8
# 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

如果 KBUILD_EXTMOD 为空的话 _all 依赖于 all 。这里不是单独编译模块,所以KBUILD_EXTMOD 为空, _all 的依赖就是 all。

2.2 all目标规则

在主 Makefile 中 all 目标规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
all:		$(ALL-y)
ifeq ($(CONFIG_DM_I2C_COMPAT)$(CONFIG_SANDBOX),y)
@echo >&2 "===================== WARNING ======================"
@echo >&2 "This board uses CONFIG_DM_I2C_COMPAT. Please remove"
@echo >&2 "(possibly in a subsequent patch in your series)"
@echo >&2 "before sending patches to the mailing list."
@echo >&2 "===================================================="
endif
#......
endif
@# Check that this build does not use CONFIG options that we do not
@# know about unless they are in Kconfig. All the existing CONFIG
@# options are whitelisted, so new ones should not be added.
$(call cmd,cfgcheck,u-boot.cfg)

all里面其实是大量的打印信息,主要还是要看它的依赖$(ALL-y)。

2.3 ALL-y

在顶层 Makefile 中, ALL-y 如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Always append ALL so that arch config.mk's can add custom ones
ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map binary_size_check

ALL-$(CONFIG_ONENAND_U_BOOT) += u-boot-onenand.bin
#......
ALL-$(CONFIG_OF_HOSTFILE) += u-boot.dtb
ifneq ($(CONFIG_SPL_TARGET),)
ALL-$(CONFIG_SPL) += $(CONFIG_SPL_TARGET:"%"=%)
endif
#......
# Add optional build target if defined in board/cpu/soc headers
ifneq ($(CONFIG_BUILD_TARGET),)
ALL-y += $(CONFIG_BUILD_TARGET:"%"=%)
endif
#......
ifeq ($(CONFIG_MPC85xx)$(CONFIG_OF_SEPARATE),yy)
ALL-y += u-boot-with-dtb.bin
endif

可以看出, ALL-y一定包含 u-boot.srec、 u-boot.bin、 u-boot.sym、System.map、 u-boot.cfg 和 binary_size_check 这几个文件。根据 uboot 的配置情况也可能包含其他的文件,比如:

1
ALL-$(CONFIG_ONENAND_U_BOOT) += u-boot-onenand.bin

CONFIG_ONENAND_U_BOOT 就是 uboot 中跟 ONENAND 配置有关的,如果我们使能了ONENAND,那么在.config 配置文件中就会有“CONFIG_ONENAND_U_BOOT=y”这一句。相当于 CONFIG_ONENAND_U_BOOT 是个变量,这个变量的值为“y”,所以展开以后就是:

1
ALL-y += u-boot-onenand.bin

这个就是.config 里面的配置参数的含义,这些参数其实都是变量,后面跟着变量值,会在顶层 Makefile 或者其他 Makefile 中调用这些变量。

2.4 u-boot.bin

ALL-y 里面有个 u-boot.bin,这个就是我们最终需要的 uboot 二进制可执行文件,所作的所有工作就是为了它。在顶层 Makefile 中找到 u-boot.bin 目标对应的规则,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ifeq ($(CONFIG_MULTI_DTB_FIT),y)

fit-dtb.blob: dts/dt.dtb FORCE
$(call if_changed,mkimage)

MKIMAGEFLAGS_fit-dtb.blob = -f auto -A $(ARCH) -T firmware -C none -O u-boot \
-a 0 -e 0 -E \
$(patsubst %,-b arch/$(ARCH)/dts/%.dtb,$(subst ",,$(CONFIG_OF_LIST))) -d /dev/null

u-boot-fit-dtb.bin: u-boot-nodtb.bin fit-dtb.blob
$(call if_changed,cat)

u-boot.bin: u-boot-fit-dtb.bin FORCE
$(call if_changed,copy)
else ifeq ($(CONFIG_OF_SEPARATE),y)
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
$(call if_changed,cat)

u-boot.bin: u-boot-dtb.bin FORCE
$(call if_changed,copy)
else
u-boot.bin: u-boot-nodtb.bin FORCE
$(call if_changed,copy)
endif

可以搜索一下这个CONFIG_MULTI_DTB_FIT,会发现是没有这个配置项的:

1
2
3
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$ grep -nRw "CONFIG_MULTI_DTB_FIT" ./
#......
./.config:654:# CONFIG_MULTI_DTB_FIT is not set

所以走的是else这部分,我们再搜一下CONFIG_OF_SEPARATE:

1
2
3
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$ grep -nRw "CONFIG_OF_SEPARATE" ./
# ......
./.config:649:CONFIG_OF_SEPARATE=y

判断 CONFIG_OF_SEPARATE 是否等于 y,如果相等,那条件就成立。所以在这里是成立的。所以这一大段最后就是:

1
2
3
4
5
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
$(call if_changed,cat)

u-boot.bin: u-boot-dtb.bin FORCE
$(call if_changed,copy)

这部分就是制作u-boot-dtb.bin和u-boot.bin的规则,目标u-boot.bin依赖于u-boot-dtb.bin,命令为$(call if_changed,copy) , 这里调用了 if_changed ,if_changed 是一个函数 ,这个函数在 scripts/Kbuild.include 中有定义。而顶层 Makefile 中会包含 scripts/Kbuild.include 文件,这个前面已经说过了。 if_changed 在 scripts/Kbuild.include 中的定义如下:

1
2
3
4
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的描述在scripts/Kbuild.include

1
2
3
4
5
6
7
###
# if_changed - execute command if any prerequisite is newer than
# target, or command line has changed
# if_changed_dep - as if_changed, but uses fixdep to reveal dependencies
# including used config symbols
# if_changed_rule - as if_changed but execute rule instead
# See Documentation/kbuild/makefiles.txt for more info

根据描述,在一些先决条件比目标新的时候,或者命令行有改变的时候, if_changed 就会执行一些命令。继续往后看就可以找到刚才的定义:

1
2
3
4
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, if_changed 函数引用的变量比较多,也比较绕,我们只需要知道它可以从 u-boot-dtb.bin 生成 u-boot.bin 就行了。

2.5 u-boot-dtb.bin

2.5.1 依赖目标

既然 u-boot.bin 依赖于 u-boot-dtb.bin,那么肯定要先生成 u-boot-dtb.bin 文件,顶层 Makefile 中相关代码如下:

1
2
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
$(call if_changed,cat)

目标 u-boot-dtb.bin 又依赖于 u-boot-nodtb.bin, u-boot-nodtb.bin的规则在 Makefile中如下所示:

1
2
3
4
u-boot-nodtb.bin: u-boot FORCE
$(call if_changed,objcopy)
$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
$(BOARD_SIZE_CHECK)

可以看到u-boot-nodtb.bin又依赖于u-boot。

2.5.2 u-boot

2.5.2.1 u-boot的依赖

u-boot的规则在顶层 Makefile 中相关规则如下:

1
2
3
4
5
6
7
8
9
10
u-boot:	$(u-boot-init) $(u-boot-main) u-boot.lds FORCE
+$(call if_changed,u-boot__)
ifeq ($(CONFIG_KALLSYMS),y)
$(call cmd,smap)
$(call cmd,u-boot__) common/system_map.o
endif

ifeq ($(CONFIG_RISCV),y)
@tools/prelink-riscv $@ 0
endif

可以看到目标 u-boot 依赖于 u-boot-init、 u-boot-main 和 u-boot.lds, u-boot-init 和 u-boot-main 是两个变量,在顶层 Makefile 中有定义,值如下:

1
2
u-boot-init := $(head-y)
u-boot-main := $(libs-y)
2.5.2.2 head-y

$(head-y)跟 CPU 架构有关,我们使用的是 ARM 芯片,所以 head-y 在 arch/arm/Makefile 中被指定为:

1
head-y := arch/arm/cpu/$(CPU)/start.o

根据本篇笔记 “第一部分 第11.3 变量导出” 小节,我们知道 CPU=armv7,因此 head-y 展开以后就是:

1
head-y := arch/arm/cpu/armv7/start.o

所以就有:

1
u-boot-init= arch/arm/cpu/armv7/start.o
2.5.2.3 libs-y

$(libs-y)在顶层 Makefile 中被定义为 uboot 所有子目录下 build-in.o 的集合,代码如下:

1
2
3
4
5
6
7
8
9
10
11
libs-y += lib/
libs-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/
libs-$(CONFIG_OF_EMBED) += dts/
libs-y += fs/
libs-y += net/
# ......
u-boot-dirs := $(patsubst %/,%,$(filter %/, $(libs-y))) tools examples

u-boot-alldirs := $(sort $(u-boot-dirs) $(patsubst %/,%,$(filter %/, $(libs-))))

libs-y := $(patsubst %/, %/built-in.o, $(libs-y))

从上面的代码可以看出, libs-y 都是 uboot 各子目录的集合,最后:

1
libs-y := $(patsubst %/, %/built-in.o, $(libs-y))

这里调用了函数 patsubst,将 libs-y 中的“/”替换为”/built-in.o”,比如“drivers/dma/”就变为了“drivers/dma/built-in.o”,相当于将 libs-y 改为所有子目录中 built-in.o 文件的集合。那么 uboot-main 就等于所有子目录中 built-in.o 的集合。

2.5.2.4 u-boot.lds规则

上面的这个 u-boot 规则就相当于将以 u-boot.lds 为链接脚本,将 arch/arm/cpu/armv7/start.o 和各个子目录下的 built-in.o 链接在一起生成 u-boot。u-boot.lds 的规则在 Makefile 中是这样的:

1
2
u-boot.lds: $(LDSCRIPT) prepare FORCE
$(call if_changed_dep,cpp_lds)
2.5.2.5  built-in.o怎么生成?

接下来的重点就是各子目录下的 built-in.o 是怎么生成的,以 drivers/gpio/built-in.o 为例,在drivers/gpio/目录下会有个名为.built-in.o.cmd 的文件:

image-20241119231818010

此文件内容如下:

1
cmd_drivers/gpio/built-in.o :=  arm-linux-gnueabihf-ld.bfd     -r -o drivers/gpio/built-in.o drivers/gpio/gpio-uclass.o drivers/gpio/74x164_gpio.o drivers/gpio/mxc_gpio.o 

从命令“cmd_drivers/gpio/built-in.o”可以看出, drivers/gpio/built-in.o 这个文件是使用 ld 命令由文件 drivers/gpio/mxc_gpio.o 还有另外两个.o生成而来的,其中 mxc_gpio.o 是 mxc_gpio.c 编译生成的.o 文件,其他两个也类似,这个是 NXP 的 I.MX 系列的 GPIO 驱动文件。这里用到了 ld 的“-r”参数,参数含义如下:-r –relocateable: 产生可重定向的输出,比如,产生一个输出文件它可再次作为‘ld’ 的输入,这经常被叫做“部分链接”,当我们需要将几个小的.o 文件链接成为一个.o 文件的时候,需要使用此选项。

2.5.2.6 u-boot的生成

最终将各个子目录中的 built-in.o 文件链接在一起就形成了 u-boot,使用如下命令编译 uboot就可以看到链接的过程:

1
2
3
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
image-20241119232614054

将其整理一下,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
arm-linux-gnueabihf-ld.bfd   -pie  --gc-sections -Bstatic  --no-dynamic-linker -Ttext 0x87800000 \
-o u-boot -T u-boot.lds \
arch/arm/cpu/armv7/start.o --start-group \
arch/arm/cpu/built-in.o \
arch/arm/cpu/armv7/built-in.o \
#......
lib/built-in.o \
net/built-in.o --end-group \
arch/arm/lib/eabi_compat.o \
arch/arm/lib/lib.a \
-Map u-boot.map; \
true

可以看出最终是用 arm-linux-gnueabihf-ld.bfd 命令将 arch/arm/cpu/armv7/start.o 和其他众多的 built_in.o 链接在一起,形成 u-boot。目标 all 除了 u-boot.bin 以外还有其他的依赖,比如 u-boot.srec 、 u-boot.sym 、 System.map、u-boot.cfg 和 binary_size_check 等等,这些依赖的生成方法和 u-boot.bin 很类似,这里就不再详细说明了。

2.6 总结一下

这个图是按照uboot 2016.03画的,2019.04有所不同,2019.04使用设备树了,但是流程大概都是一样的。

image-20230730143850408

make xxx_defconfig: 用于配置 uboot,这个命令最主要的目的就是生成.config 文件。
make:用于编译 uboot,这个命令的主要工作就是生成二进制的 u-boot.bin 文件和其他的一些与 uboot 有关的文件,比如 u-boot.imx 等等。