[IMX][UBoot] 03.顶层 Makefile 解析
目录
1.UBoot 版本号设置
2.用户选项处理
2.1.选项保存 - MAKEFLAGS
2.2.静默输出 - -s
2.3.输出信息控制 - V
2.3.1.变量 Q 的用途
2.3.2.变量 quiet 的用途
2.4.设置输出目录 - O
2.5.代码检查 - C
2.6.编译外部模块 - M
3.配置交叉编译器
3.1.获取本机的架构和系统信息
3.2.设置目标架构和交叉编译器
3.3.配置文件
3.4.配置交叉编译器
4.配置预处理标志
5.导入变量/函数 - scripts/Kbuild.include
6.导出变量 - export
7.编译过程
7.1.生成 .config 配置文件的编译规则
7.1.1.目标列表 - MAKECMDGOALS
7.1.2.检查是否包含依赖配置文件的目标
7.1.3.检查是否包含配置文件
7.1.4.设置编译工具的编译规则 - scripts_basic
7.1.5.设置输出目录 - outputmakefile
7.1.6.设置 .config 配置文件的编译规则
7.2.生成配置文件
7.2.1.检查目标所处阶段
7.2.2.获取需要编译的目标 - obj-y
7.2.3.获取子 Makefile 或 Kbuild 文件
7.2.4.编译 scrpts/basic 中的 fixdep 等工具
7.2.5.生成 .config 配置文件
7.3.生成 u-boot.bin 的编译规则
7.3.1.检索目标 u-boot.bin
7.3.2.u-boot.bin 的编译规则
7.3.3.CPU 初始化程序 - u-boot_init
7.3.4.平台的各个模块 - u-boot-main
7.3.5.链接脚本 - u-boot.lds
7.3.6.子模块 built-in.o 编译规则
8.Makefile.build 脚本分析
8.1.检查目标所处阶段
8.2.获取需要编译的目标 - obj-y
8.3.查找子模块的 Kbuild/Makefile 文件
8.4.汇编文件的编译规则
8.4.1.目标文件分类
8.4.2.汇编文件 .s 编译成 .o 文件
8.5.C 文件的编译规则
8.5.1.生成临时目标文件
8.5.2.符号表重链接
8.5.3.执行流程
8.6.链接目标文件
8.7.链接生成 Lib 库文件
8.8.递归编译子模块的规则
8.9.默认目标 - __build
uboot 顶层 Makefile 即 uboot 根目录下的 Makefile 文件:
1.UBoot 版本号设置
uboot 的版本号在 Makefile 中直接定义:
VERSION = 2016
PATCHLEVEL = 03
SUBLEVEL =
EXTRAVERSION =
NAME =
-
VERSION 为主版本号;
-
PATCHLEVEL 为补丁版本号;
-
SUBLEVEL 为次版本号;
-
以上三个变量构成 uboot 的版本号,例如,当前 uboot 的版本号为 2016.03;
-
EXTRAVERSION 为附加版本信息,一般不使用;
-
NAME 为名称,一般不使用;
2.用户选项处理
编译 uboot 时,用户可以通过命令行选项,指定是否输出详细信息、是否进行代码检查等,例如:
make V=1 -j4 target
-
V=1:Verbose Log Output,打印更为详细的 Log 信息;
-
-j4:使用 4 个线程进行编译;
其中:
-
部分选项直接传递给 Makefile 并直接进行处理,如 V=1;
-
另一部分则会保存至默认变量 MAKEFLAGS 中,然后传递给子 Makefile,如 -j4;
2.1.选项保存 - MAKEFLAGS
MAKEFLAGS 为 GNU Make 中的特殊变量,用于存储和传递命令行选项,例如,执行 make -j4 -s target 时,GNU Make 会自动将选项 -j4 -s 存入 MAKEFLAGS,即 MAKEFLAGS = -j4 -s
注意:某些选项 (如 -C、-f、-o 等) 不会存入 MAKEFLAGS,因为这些选项是针对当前 Makefile 的路径或行为,无需传递给子 Makefile,即只有需要传递给子 Makefile 的选项才会保存在 MAKEFLAGS 中
默认变量 MAKEFLAGS 支持递归传递,即在递归调用子 Makefile 时,自动将该变量的值传递给子 Makefile,例如,子 Makefile 编译时也会使用 -j4 -s 选项
顶层 Makefile 中为 MAKEFLAGS 设置了默认值:
# o Do not use make's built-in rules and variables
# (this increases performance and avoids hard-to-debug behaviour);
# o Look for make include files relative to root of kernel src
MAKEFLAGS += -rR --include-dir=$(CURDIR)
...
# Do not print "Entering directory ...",
# but we want to display it when entering to the output directory
# so that IDEs/editors are able to understand relative filenames.
MAKEFLAGS += --no-print-directory
-
+= 表示追加值,即在 MAKEFLAGS 现有值的基础上追加新的值;
-
-r 为 --no-builtin-rules 的简写,该选项禁用所有 GNU Make 内置的隐含规则,如 .c 到 .o 的默认编译规则,使用该选项后,必须显式的定义编译规则;
-
-R 为 --no-builtin-variables 的简写,表示禁用所有 GNU Make 的内置变量,如 CC=gcc、CFLAGS=-g -O2 等,使用该选项后,必须显式的定义 CC、CFLAGS 等内置变量;
-
--include-dir=$(CURDIR) 表示在当前目录 (根目录 . ) 中搜索文件:
-
--include-dir 指定搜索目录;
-
CURDIR 为 GNU Make 的内置变量,表示当前 Makefile 所在目录的路径 (对于顶层 Makefile 为根目录 . );
-
-
--no-print-directory 表示禁止输出类似 Entering directory ... 的提示信息 (大型项目编译时,需要在各个子目录和根目录间来回切换,这会导致输出大量的目录切换提示信息);
2.2.静默输出 - -s
编译时使用 -s 选项 (make -s ...) 可以实现静默输出,即编译时不会输出任何 Log 信息,同时 -s 选项会保存在变量 MAKEFLAGS 中,并传递给子 Makefile
顶层 Makefile 会检查变量 MAKEFLAGS,判断用户是否在命令行中使用了 -s 选项,若用户使用了 -s 选项,则会设置变量 quiet 的值:
# 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
endifexport quiet Q KBUILD_VERBOSE
-
ifneq ($(filter 4.%,$(MAKE_VERSION)),) 检查当前 Make 的版本是否为 4.x:
-
MAKE_VERSION 为 GNU Make 的内置变量,表示当前 Make 的版本号;
-
filter 4.% 筛选版本号,区分出 Make 4.x 版本和 Make 3.8x 版本;
-
-
ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),) 检查用户是否在命令行中使用了 -s 或 --silent 选项:
-
x$(MAKEFLAGS):x 用于防止 firstword 函数返回空字符串,MAKEFLAGS 保存用户传入的选项;
-
firstword 函数用于提取命令行选项 MAKEFLAGS 中的第一个选项;
-
因此 $(firstword x$(MAKEFLAGS))) 从命令行选项 MAKEFLAGS 中提取第一个选项;
-
filter %s 检查选项中是否包含匹配的字符串 (% 为通配符,s 表示字符 s),即 -s 或 --silent;
-
-
之所以要区分 Make 4.x 和 Make 3.8x,是因为 Make 3.8x 只能使用简单的字符串匹配,仅能处理短选项,即只能处理 -s 选项,无法处理 --silent 选项;
-
若用户通过命令行设置静默输出 (make -s ...),则会设置 quiet=silent_,设置后变量 quiet_cmd_cc_o_c 等同于 silent_cmd_cc_o_c,但是该变量未定义,因此不会再输出任何信息;
-
export quiet Q KBUILD_VERBOSE 将控制 Log 输出的相关变量,传递给子 Makefile (设置为全局变量);
2.3.输出信息控制 - V
uboot 默认编译时不会在终端显示完整的命令,如下图所示;
可以通过选项 V=1 (即 make 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
-
ifeq ("$(origin V)", "command line") 判断用户是否在命令行中使用了 V=1 选项设置 Log 输出等级:
-
origin 函数以字符串格式输出变量的来源,例如,origin V 表示查询变量 V 的来源,若用户在命令行中使用了该选项 (make V=1 ...),则会返回字符串 "command line";
-
此时,ifeq() 判断等式成立,执行 KBUILD_VERBOSE = $(V);
-
-
KBUILD_VERBOSE = $(V) 将变量 KBUILD_VERBOSE 的值设置为变量 V 的值,若在编译时使用了 V=1 选项,则有 KBUILD_VERBOSE = V = 1,否则 KBUILD_VERBOSE = 0;
-
ifeq ($(KBUILD_VERBOSE),1) 判断变量 KBUILD_VERBOSE 的值是否为 1,即是否使用了 V=1 选项:
-
若为 1 则将 quiet 和 Q 变量均设置为空,即打印所有 Log 信息;
-
若用户未使用 V=1 设置 Log 输出等级,则将 quiet 设置为 quiet_,Q 设置为 @;
-
2.3.1.变量 Q 的用途
Makefile 中使用变量 Q 控制打印输出,例如:
env: scripts_basic$(Q)$(MAKE) $(build)=tools/$@
如果用户未使用 V=1 选项,则 Q = @,上述代码展开如下:
env: scripts_basic@$(MAKE)$(build)=tools/$@
在编译命令前加上 @ 会禁止其在终端打印 Log 信息
2.3.2.变量 quiet 的用途
有些命令有两个版本,例如:
quiet_cmd_sym ?= SYM $@cmd_sym ?= $(OBJDUMP) -t $< > $@
sym 命令有 quiet_cmd_sym 和 cmd_sym 两个版本,这两个命令功能一样,区别在于命令执行时输出的信息不同,quiet_cmd_xxx 命令输出的信息少 (短命令),cmd_xxx 命令输出的信息多 (完整命令):
-
变量 quiet 为空时输出完整的 Log 信息,如 arm-linux-gnueabihf-gcc -Wp ...;
-
变量 quiet 为 quiet_ 时仅输出短命令 Log 信息,如 CC ...、CFLAGS ...;
-
变量 quiet 为 silent_ 时命令不会输出任何 Log 信息 (因为没有定义 silent_cmd_xxx 命令);
2.4.设置输出目录 - O
uboot 可以将编译生成的目标文件存放到指定的目录中
执行 make 命令时使用 O 选项指定输出目录,例如 make O=out 表示将目标文件输出到 /out 目录中
若不使用 O 选项,则源文件和编译产生的文件位于同一个目录中,一般不使用 O 选项
顶层 Makefile 中处理 O 选项的代码如下:
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)
...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))
-
ifeq ("$(origin O)", "command line") 判断 O 选项是否由用户通过命令行设置;
-
KBUILD_OUTPUT := $(O) 表示如果用户在编译时使用 make O=/src/.. 指定了输出目录,则将指令目录 /src/.. 的路径赋给 KBUILD_OUTPUT 变量;
-
ifneq ($(KBUILD_OUTPUT),) 判断 KBUILD_OUTPUT 指定的输出目录是否为空,若不为空则调用 mkdir 创建指定的输出目录,并将绝对路径赋给 KBUILD_OUTPUT;
-
执行 make sub-make 命令或编译依赖 sub-make 的目标时,在 KBUILD_OUTPUT 目录下重新运行 Makefile,源码路径通过 KBUILD_SRC 传递源码路径;
-
综上,通过这种方式将编译产生的文件存放到 O 选项指定的目录中;
2.5.代码检查 - C
uboot 支持代码检查:
-
编译时使用 C=1 选项 (make C=1) 检查需要重新编译的文件;
-
编译时使用 C=2 选项 (make C=2) 检查所有的源码文件;
顶层 Makefile 中的相关代码如下:
ifeq ("$(origin C)", "command line")KBUILD_CHECKSRC = $(C)
endif
ifndef KBUILD_CHECKSRCKBUILD_CHECKSRC = 0
endif
-
ifeq ("$(origin C)", "command line") 检查用户是否在命令行中使用了 C 选项;
-
KBUILD_CHECKSRC = $(C):若用户使用了 C 选项,则将该选项的值赋给 KBUILD_CHECKSRC;
在 Makefile.build 脚本中,检查 KBUILD_CHECKSRC 是否有值,即用户是否使用了 C 选项,若用户使用了 C 选项,则调用 CHECK (CHECK 为静态代码分析工具 sparse) 执行代码检查:
# Linus' kernel sanity checking tool
ifneq ($(KBUILD_CHECKSRC),0)ifeq ($(KBUILD_CHECKSRC),2)quiet_cmd_force_checksrc = CHECK $<cmd_force_checksrc = $(CHECK) $(CHECKFLAGS) $(c_flags) $< ;elsequiet_cmd_checksrc = CHECK $<cmd_checksrc = $(CHECK) $(CHECKFLAGS) $(c_flags) $< ;endif
endif
2.6.编译外部模块 - M
uboot 允许编译外部模块,使用命令 make M=dir 即可,也支持旧版语法 make SUBDIRS=dir,相关代码如下:
# 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
-
ifdef SUBDIRS 判断是否定义了变量 SUBDIRS (旧版 Make 使用 SUBDIRS=$PWD 选项,指定需要编译的外部模块的源码路径),若 SUBDIRS 存在定义,则将其值赋给变量 KBUILD_EXTMOD,这里是为了支持旧版 Make 的语法 make SUBIDRS=dir;
-
ifeq ("$(origin M)", "command line") 判断是否在命令行中使用了 M 选项,若用户在命令行中使用了 M 选项,则将其值赋给 KBUILD_EXTMOD;
-
ifeq ($(KBUILD_EXTMOD),) 判断 KBUILD_EXTMOD 是否为空:
-
若用户未使用 M 选项 (未指定需要编译的外部模块的源码路径),则 KBUILD_EXTMOD 为空,_all 依赖 all,即默认在当前目录下执行编译 (直接编译 uboot);
-
若 KBUILD_EXTMOD 不为空,则 _all 依赖 modules,会优先编译外部模块,但一般不在 uboot 中编译外部模块;
-
-
ifeq ($(KBUILD_SRC),) 判断 KBUILD_SRC 是否为空 (是否指定源码目录),若未指定则将源码目录 srctree 设置为根目录 (在根目录下进行编译,即编译完整的 uboot),一般不指定源码目录的路径 KBUILD_SRC;
-
注意:这里 srctree 和 objtree 均为当前根目录 .;
-
-
VPATH := $(srctree)$(if $(KBUILD_EXTMOD),:$(KBUILD_EXTMOD)):如果在当前目录中未找到源文件,则在指定目录中 (srctree、KBUILD_EXTMOD、KBUILD_EXTMOD) 进行搜索;
3.配置交叉编译器
uboot 支持多种架构,因此在执行编译前,需要配置所使用的编译器
若本机和目标架构一致,则使用本机编译器,若两者架构不同,则需要配置交叉编译器
3.1.获取本机的架构和系统信息
顶层 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 保存当前 PC 的架构信息,如 x86_64、ARM 等:
-
通过 uname -m 命令获取 PC 的架构信息,例如,本机终端执行 uname -m 命令,输出结果为 x86_64;
-
sed -e s 为替换命令,例如 sed -e s/i.86/x86/,其将字符串 i.86 替换为 x86,因此这一行命令对于 x86 架构,等同于 HOSTARCH := x86_64;
-
-
HOSTOS 保存当前 PC 的系统信息,如 Linux、Windows 等:
-
通过 uname -s 命令获取 PC 的操作系统信息,例如,本机终端执行 uname -s 命令,输出结果为 Linux;
-
tr '[:upper:]' '[:lower:]' 将字符串中的大写字符转换为小写字母,例如,将 Linux 转换为 linux;
-
sed -e 's/\(cygwin\).*/cygwin/' 将 cygwin_* 格式的字符串简化为 cygwin 格式;
-
因此,对于本机这行代码等同于 HOSTOS := linux;
-
3.2.设置目标架构和交叉编译器
本机架构为 x86_64,目标架构为 ARM,因此需要使用交叉编译器
顶层 Makefile 中并未设置目标架构和交叉编译器,因此需要用户在编译时指定,例如:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- ...
该命令将目标架构 ARCH 设置为 ARM,并指定所使用的交叉编译器 CROSS_COMPILE 为 arm-linux-gnueabihf-
Makefile 会获取用户在命令行中设置的目标架构及所使用的交叉编译器,并将值赋给对应的变量:
# set default to nothing for native builds
ifeq ($(HOSTARCH),$(ARCH))
CROSS_COMPILE ?=
endif
-
前面已经通过 uname 命令获取了本机的架构信息 HOSTARCH 和系统信息 HOSTOS,用户在终端执行 make ARCH=arm ... 命令后,会将值 arm 赋给变量 ARCH;
-
ifeq ($(HOSTARCH),$(ARCH)) 判断本机和目标是否为同架构,若不同则设置交叉编译器 CROSS_COMPILE;
-
CROSS_COMPILE ?=:CROSS_COMPILE 由用户通过命令行指定,本机系统为 Linux,目标架构为 ARM,其使用的交叉编译器为 arm-linux-gnueabihf-;
3.3.配置文件
Makefile 中将配置文件 KCONFIG_CONFIG 指定为 .config 文件:
KCONFIG_CONFIG ?= .config
export KCONFIG_CONFIG
在终端执行 make xxx_defconfig 命令,会在根目录下生成 .config 配置文件 (.config 是 xxx_defconfig 的副本)
.config 文件中的默认值和 xxx_defconfig 中的一致,若后续调整了 uboot 中的相关配置项,则这些参数只会保存在 .config 文件中,相当于 xxx_defconfig 只进行初始配置,而 .config 才是实时有效的配置
3.4.配置交叉编译器
交叉编译器指定后需要进行配置,设置编译工具 CC、链接工具 LD 及相关标记 FLAGS 等:
# 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
AWK = awk
PERL = perl
PYTHON = python
DTC = dtc
CHECK = sparse
例如,若用户使用的编译命令为:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- ...
则变量 CROSS_COMPILE 的值为 arm-linux-gnueabihf-,则:
-
AS = arm-linux-gnueabihf-as;
-
CC = arm-linux-gnueabihf-gcc;
-
... (其余变量类似);
4.配置预处理标志
顶层 Makefile 中定义了一些宏/标志,这些宏/标志控制条件编译、代码检查等功能:
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
KBUILD_AFLAGS := -D__ASSEMBLY__
-
变量 CHECKFLAGS:
-
-Dlinux 和 -D__linux__:在编译时分别定义 linux 宏和 __linux__ 宏,声明当前系统环境为 Linux;
-
-D__STDC__:在编译时定义 __STDC__ 宏,声明当前编译环境支持标准 C 语言规范;
-
-Dunix 和 -D__unix__:在编译时分别定义 unix 宏和 __unix__ 宏,声明当前编译环境为 Unix 或类 Unix 系统 (如 Linux、FreeBSD 等);
-
-Wbitwise 控制警告选项,当代码中使用了错误的位运算时,输出警告信息;
-
-Wno-return-void 控制警告选项,当有返回值的函数没有返回值时,输出警告信息;
-
-D__CHECK_ENDIAN__:在编译时定义 __CHECK_ENDIAN__ 宏,声明编译时需要进行字节序相关的检查和处理;
-
变量 CF 为用户自定义的编译标志,允许用户在编译时添加自定义的编译选项,如警告级别、优化选项等;
-
-
变量 KBUILD_CPPFLAGS:
-
-D__KERNEL__:在编译时定义 __KERNEL__ 宏,声明当前编译的目标为内核程序,区别于用户空间程序;
-
-D__UBOOT__:在编译时定义 __UBOOT__ 宏,声明当前编译的目标为 uboot;
-
-
变量 KBUILD_CFLAGS:
-
-Wall:控制警告选项,表示启用所有常见警告;
-
-Wstrict-prototypes:控制警告选项,表示严格检查函数原型;
-
-Wno-format-security:控制警告选项,表示禁止输出格式化字符串相关的安全检查的警告信息;
-
-fno-builtin 表示禁用 GCC 所有的内置函数替换,强制调用标准库实现;
-
-ffreestanding 表示代码将在独立环境中运行,允许使用不完整的标准库实现;
-
-
变量 KBUILD_AFLAGS:
-
-D__ASSEMBLY__:在编译时定义 __ASSEMBLY__ 宏,声明当前编译的目标为汇编文件;
-
5.导入变量/函数 - scripts/Kbuild.include
顶层 Makefile 会引用 scripts/Kbuild.include 文件,导入其中定义的变量/函数:
# We need some generic definitions (do not try to remake the file).
scripts/Kbuild.include: ;
include scripts/Kbuild.include
scripts/Kbuild.include 文件中定义了许多变量和函数,uboot 编译过程中会使用这些变量和函数,例如:
# as-option
# Usage: cflags-y += $(call as-option,-Wa$(comma)-isa=foo,)as-option = $(call try-run,\$(CC) $(KBUILD_CFLAGS) $(1) -c -x assembler /dev/null -o "$$TMP",$(1),$(2))# as-instr
# Usage: cflags-y += $(call as-instr,instr,option1,option2)as-instr = $(call try-run,\printf "%b\n" "$(1)" | $(CC) $(KBUILD_AFLAGS) -c -x assembler -o "$$TMP" -,$(2),$(3))
-
函数 as-option 用于检查编译器是否支持特定的编译选项;
-
函数 as-instr 用于检测编译器是否支持特定的汇编指令或语法;
6.导出变量 - export
顶层 Makefile 会使用 export 导出一些变量供子 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
...
这些变量大部分在顶层 Makefile 中定义,部分变量在 config.mk 文件中定义:
ARCH := $(CONFIG_SYS_ARCH:"%"=%)
CPU := $(CONFIG_SYS_CPU:"%"=%)
...
BOARD := $(CONFIG_SYS_BOARD:"%"=%)
ifneq ($(CONFIG_SYS_VENDOR),)
VENDOR := $(CONFIG_SYS_VENDOR:"%"=%)
endif
ifneq ($(CONFIG_SYS_SOC),)
SOC := $(CONFIG_SYS_SOC:"%"=%)
endif
用户通过命令 make xxx_defconfig 生成 .config 配置文件时,会读取其中的配置 (如 CONFIG_xxx 等),从而为 config.mk 文件中定义的变量赋值:
-
ARCH := $(CONFIG_SYS_ARCH:"%"=%) 定义变量 ARCH,值为 $(CONFIG_SYS_ARCH:"%"=%),即提取 CONFIG_SYS_ARCH 中双引号 “” 之间的内容,例如,当 CONFIG_SYS_ARCH=“arm” 时, ARCH=arm;
-
CPU := $(CONFIG_SYS_CPU:"%"=%) 定义变量 CPU,值为 $(CONFIG_SYS_CPU:"%"=%);
-
BOARD := $(CONFIG_SYS_BOARD:"%"=%) 定义变量 BOARD,值为 $(CONFIG_SYS_BOARD:"%"=%);
-
VENDOR := $(CONFIG_SYS_VENDOR:"%"=%) 定义变量 VENDOR,值为 $(CONFIG_SYS_VENDOR:"%"=%);
-
SOC := $(CONFIG_SYS_SOC:"%"=%) 定义变量 SOC,值为 $(CONFIG_SYS_SOC:"%"=%);
CONFIG_SYS_ARCH 等变量在根目录的 .config 文件中定义 (.config 由用户执行 make xxx_defconfig 生成):
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"
因此,这几个变量的值如下:
ARCH = arm
CPU = armv7
BOARD = mx6ullevk
VENDOR = freescale
SOC = mx6
Makefile 会指定对应架构及开发板的源码路径,从而包含对应的 config.mk 文件:
# Some architecture config.mk files need to know what CPUDIR is set to,
# so calculate CPUDIR before including ARCH/SOC/CPU config.mk files.
# Check if arch/$ARCH/cpu/$CPU exists, otherwise assume arch/$ARCH/cpu contains
# CPU-specific code.
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 rulesifdef SOC
sinclude $(srctree)/$(CPUDIR)/$(SOC)/config.mk # include SoC specific rules
endif
ifneq ($(BOARD),)
ifdef VENDOR
BOARDDIR = $(VENDOR)/$(BOARD)
else
BOARDDIR = $(BOARD)
endif
endif
ifdef BOARD
sinclude $(srctree)/board/$(BOARDDIR)/config.mk # include board specific rules
endif
-
CPUDIR=arch/$(ARCH)/cpu$(if $(CPU),/$(CPU),) 定义变量 CPUDIR,值为 arch/$(ARCH)/cpu$(if$(CPU),/$(CPU),),将其逐个展开如下:
-
$(ARCH) 获取变量 ARCH 的值,这里 ARCH=arm,因此 CPUDIR=arch/arm/cpu$(if $(CPU),/$(CPU),);
-
$(if $(CPU),/$(CPU),) 检查变量 CPU 是否有值,若没有值会返回空,这里 CPU=armv7,因此 CPUDIR=arch/arm/cpu/armv7;
-
-
sinclude $(srctree)/arch/$(ARCH)/config.mk:sinclude 和 include 的功能类似,表示读取指定文件中的内容,这里读取文件 $(srctree)/arch/$(ARCH)/config.mk 中的内容,若 sinclude 读取的文件不存在,不会报错;
-
BOARDDIR = $(VENDOR)/$(BOARD) 定义变量 BOARDDIR,如果定义了 VENDOR 则 BOARDDIR=$(VENDOR)/$(BOARD),否则 BOARDDIR=$(BOARD);
-
前面将 VENDOR 设置为 freescale,BOARD 设置为 mx6ullevk,因此 BOARDDIR=freescale/mx6ullevk;
-
7.编译过程
顶层 Makefile 描述 .config 及 uboot.bin 等主要文件的编译规则,具体工作由 Makefile.build 脚本完成
Makefile.build 脚本读取子目录的 Makefile 或 Kbuild 文件,并根据其中的配置决定是否编译以及如何编译该子模块
Makefile.build 脚本具体描述 .c 文件或汇编文件生成 .o 文件的编译规则
7.1.生成 .config 配置文件的编译规则
用户在终端执行 make xxx_defconfig 时生成 .config 文件,该文件为 xxx_defconfig 的副本,其中包含开发板相关的配置信息,如架构信息、是否编译网络模块、是否编译串口模块等:
CONFIG_SYS_ARCH="arm"
...
CONFIG_HAS_VBAR=y
...
CONFIG_DISK=y
编译时可以修改这些变量的值,新的值只会保存在 .config 文件中,不会写入 xxx_defconfig 文件,即 .config 为用户生成的临时配置文件,仅供编译使用,Makefile 会读取 .config 配置文件,并依据其中的配置,编译对应的模块或者启用对应的功能
7.1.1.目标列表 - MAKECMDGOALS
MAKECMDGOALS 为 GNU Make 中的默认变量,用于存储用户在命令行中指定的目标,例如,用户在命令行中执行以下命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
则 MAKECMDGOALS 的值为 mx6ull_14x14_ddr512_emmc_defconfig (注意:仅该选项为目标)
7.1.2.检查是否包含依赖配置文件的目标
部分类型的目标不依赖 .config 配置文件,如 clean、check 等,而像 all、modules 等类型的目标则依赖配置文件,Makefile 会判断用户命令行中的选项,是否包含依赖配置文件的目标,并使用变量 dot-config 标记:
dot-config := 1
变量 dot-config 的默认值为 1,表示默认编译选项中包含依赖配置文件的目标
Makefile 将不依赖配置文件的目标 (如 clean、check 等) 定义为 no-dot-config-targets:
no-dot-config-targets := clean clobber mrproper distclean \help %docs check% coccicheck \ubootversion backup
不依赖配置文件的目标可以分为以下几类:
-
清理类:移除生成的文件,无需配置:clean、clobber、mrproper、distclean;
-
帮助/文档类:显示帮助或生成文档,无需配置:help、%docs (如 htmldocs);
-
检查类:代码检查工具,无需配置:check% (如 checkheaders)、coccicheck;
-
特殊工具:生成版本信息或备份,无需配置:ubootversion、backup;
其余目标依赖于 .config 配置文件,如 all、zImage、modules、defconfig 等
Makefile 中通过以下代码检查命令行选项中,是否包含依赖配置文件的目标:
ifneq ($(filter $(no-dot-config-targets), $(MAKECMDGOALS)),)ifeq ($(filter-out $(no-dot-config-targets), $(MAKECMDGOALS)),)dot-config := 0endif
endif
这段代码包含两层判断:
-
第一层判断,检查 MAKECMDGOALS 中是否包含任何 no-dot-config-targets (如 clean、backup 等);
-
第二层判断,检查排除 no-dot-config-targets 类型的目标后,剩余目标是否为空:
-
例如,若命令为 make clean,则排除后为空,此时将 dot-config 设置为 0,表示没有依赖配置文件的目标;
-
例如,若命令为 make backup all,则排除后还剩目标 all,即存在依赖配置文件的目标,dot-config 为 1;
-
生成 .config 文件时使用的命令如下:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
排除后仍剩余目标 mx6ull_14x14_ddr512_emmc_defconfig,因此 MAKECMDGOALS 不为空,变量 dot-config 的值为 1,表示当前编译选项中包含依赖配置文件的目标
7.1.3.检查是否包含配置文件
Makefile 中使用变量 config-targets 标记命令行选项中,是否包含配置文件,例如以下编译命令:
make defconfig
这条命令中就包含了配置文件 defconfig,因此 config-targets 为 1
Makefile 中使用变量 mixed-targets 标记命令行选项中,是否包含多个目标,例如以下命令:
make defconfig all
这条命令中既包含了配置文件 defconfig,又包含了目标 all,因此 mixed-targets 为 1
Makefile 中通过以下代码,检查命令行选项中是否包含配置文件,以及是否包含多个目标:
ifeq ($(KBUILD_EXTMOD),)ifneq ($(filter config %config,$(MAKECMDGOALS)),)config-targets := 1ifneq ($(words $(MAKECMDGOALS)),1)mixed-targets := 1endifendif
endif
这段代码包含三层判断:
-
第一层判断,检查是否正在编译外部模块,编译外部模块时 KBUILD_EXTMOD 不为空,但一般不在 uboot 中编译外部模块,因此 KBUILD_EXTMOD 为空,继续下一步判断;
-
第二层判断,检查命令行选项中是否包含配置文件,filter config %config 判断 MAKECMDGOALS 中是否包含 config 目标,或包含与 "config" 字符串匹配的目标,生成 .config 文件时使用的命令如下:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
-
该命令行选项中,可以匹配到目标 mx6ull_14x14_ddr512_emmc_defconfig,因此会设置 config-targets := 1,表示命令行选项中包含需要编译的配置文件;
-
第三层判断,检查命令行选项中是否包含多个目标,例如,执行命令 make modules all 时,其中包含 modules 和 all 两个目标,因此会将 mixed-targets 设置为 1,表示存在多个目标;这里仅编译配置文件,因此只存在一个目标 (即 mx6ull_14x14_ddr512_emmc_defconfig),因此 mixed-targets 为 0,表示仅有一个目标;
7.1.4.设置编译工具的编译规则 - scripts_basic
uboot 编译过程中会使用一些工具,如使用 fixdep 处理依赖项,使用 recoder 记录和处理编译过程中的各类信息
编译用户指定的目标前,需要先编译这些基础工具:
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:$(Q)$(MAKE) $(build)=scripts/basic$(Q)rm -f .tmp_quiet_recordmcount
-
PHONY += scripts_basic 将 scripts_basic 定义为伪目标,防止同名文件冲突;
-
$(Q) 控制 Log 输出,若用户未使用 V=1 选项设置打印更多信息,则变量 Q 的默认值为 @,即该编译命令执行时不会输出 Log 信息;
-
$(MAKE) 中的 MAKE 为 GNU Make 中的默认变量,其值为 make;
-
变量 build 在 ./scripts/Kbuild.include 中定义:
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj
变量 build 为 -f $(srctree)/scripts/Makefile.build obj 的别名,目的是减少重复输入,其中变量 srctree 的值为 . (根目录),这里可以看出,对于各个子文件夹,使用 Makefile.build 脚本中定义的规则进行编译
综上,目标 scripts_basic 的编译规则为:
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:make -f ./scripts/Makefile.build obj=scripts/basicrm -f . tmp_quiet_recordmcount
即使用 Makefile.build 中定义的规则,编译 ./scripts/basic 目录下的工具,然后删除编译过程中输出的 Log 文件:
7.1.5.设置输出目录 - outputmakefile
如果用户在命令行中使用了 O 选项,指定存放输出文件的目录,则需要在该目录中生成临时的 Makefile 文件、符号链接文件等:
PHONY += outputmakefile
# outputmakefile generates a Makefile in the output directory, if using a
# separate output directory. This allows convenient use of make in the
# output directory.
outputmakefile:
ifneq ($(KBUILD_SRC),)$(Q)ln -fsn $(srctree) source$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile \$(srctree) $(objtree) $(VERSION) $(PATCHLEVEL)
endif
-
PHONY += outputmakefile 将 outputmakefile 定义为伪目标,防止同名文件冲突;
-
ifneq ($(KBUILD_SRC),) 判断是否指定了源码目录的路径,若未指定则在当前目录 (根目录) 下进行编译;
-
$(Q)ln -fsn $(srctree) source 创建 srctree 的链接,指向用户指定的目录;
-
最后,调用 ./scripts/mkmakefile 工具,生成临时的 Makefile 文件,并将源码目录的路径 srctree 等变量,传递给新的 Makefile 文件;
-
这里未指定源码目录的路径,而是在根目录下进行编译,因此变量 KBUILD_SRC 的值为空,不执行这段代码;
7.1.6.设置 .config 配置文件的编译规则
设置完编译工具的编译规则后,设置 .config 配置文件的编译/生成规则,若用户执行的命令为:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
经过之前的分析可知,这里需要生成 .config 配置文件,目标为 mx6ull_14x14_ddr512_emmc_defconfig,该目标与 %config 类型的目标匹配,因此变量 config-targets 的值为 1,执行下面这段代码:
ifeq ($(config-targets),1)
# ===========================================================================
# *config targets only - make sure prerequisites are updated, and descend
# in scripts/kconfig to make the *config targetKBUILD_DEFCONFIG := sandbox_defconfig
export KBUILD_DEFCONFIG KBUILD_KCONFIGconfig: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@%config: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@
目标 mx6ull_14x14_ddr512_emmc_defconfig 和 %config 类型的目标匹配,因此分析下面这段代码:
%config: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@
-
依赖项 scripts_basic 为编译工具,依赖项 outputmakefile 为用户指定目标文件输出目录的相关配置;
-
变量 FORCE 并未设置值:
PHONY += FORCE
FORCE:
-
因此,当 FORCE 作为其他目标的依赖项时,由于 FORCE 总是被更新过的,所以依赖项所在的规则总会执行;
-
则这段代码展开为:
%config: scripts_basic outputmakefile FORCEmake -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
同样,这里可以看到使用 Makefile.build 脚本中定义的规则进行编译
在命令行中执行 make mx6ull_14x14_ddr512_emmc_defconfig V=1,可以看到对应的 Log 输出:
7.2.生成配置文件
Makefile.build 脚本提供统一的编译规则,解析子目录中的 Kbuild 或 Makefile 文件,生成目标文件 (如 .o、.ko 等),配置文件 .config 同样通过 Makefile.build 脚本生成,例如以下编译命令:
make mx6ull_14x14_ddr512_emmc_defconfig
根据之前的分析,其最终生成的编译命令为:
make -f ./scripts/Makefile.build obj=scripts/kconfig mx6ull_14x14_ddr512_emmc_defconfig
7.2.1.检查目标所处阶段
与大多数 BootLoader 一样,uboot 也分为多个阶段:
-
TPL:Tertiary Program Loader,底层引导程序,此时编译的程序要求尽可能小,主要用于加载 VPL 及 SPL;
-
VPL:Verifying Program Loader,验证引导程序,可以在多个 SPL 二进制文件中选择一个运行;
-
SPL:Secondary Program Loader,二级引导程序,主要用于配置内存 DDR 等核心模块;
-
UBoot:运行阶段 (uboot 主程序),在该阶段中配置部分外设,并支持命令行操作,同时负责加载操作系统;
因此,编译前会检查当前编译的目标所位于的阶段,从而读取对应的配置文件,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 阶段;
-
src := $(patsubst $(prefix)/%,%,$(obj)) 使用 patsubst 函数移除 obj 中包含的 tpl/ 前缀,例如:若 obj = tpl/start,则 src = start;
-
ifeq ($(obj),$(src)) 检查移除 tpl/ 前缀后,obj 的路径是否和原来的一致,即判断 obj 的路径中是否包含 tpl/ 前缀 (判断所编译的目标是否属于 TPL 阶段);若一致 (即目标 obj 的路径中不包含 tpl/ 前缀),则认为目标不属于 TPL 阶段,将变量 prefix 的值设置为 spl;
-
src := $(patsubst $(prefix)/%,%,$(obj)) 和 ifeq ($(obj),$(src)) 判断目标是否属于 SPL 阶段,若目标也不属于 SPL 阶段,则将变量 prefix 的值设置为 . ,表示属于主程序阶段,即属于 uboot 主程序;
编译前会根据目标所处阶段,加载对应的配置文件:
# Read auto.conf if it exists, otherwise ignore
# Modified for U-Boot
-include include/config/auto.conf
-include $(prefix)/include/autoconf.mk
include scripts/Makefile.uncmd_spl
-
-include $(prefix)/include/autoconf.mk 根据当前目标的所处阶段,加载对应的 autoconf.mk 文件;
-
当前使用的 2016.03 版本的 uboot 仅包含 SPL 的配置文件 Makefile.uncmd_spl,其余文件不存在;
7.2.2.获取需要编译的目标 - obj-y
Makefile.build 脚本中定义了一些变量,用于保存标志信息以及需要编译的目标:
# Init all relevant variables used in kbuild files so
# 1) they have correct type
# 2) they do not inherit any value from the environment
obj-y :=
obj-m :=
lib-y :=
lib-m :=
always :=
targets :=
subdir-y :=
subdir-m :=
EXTRA_AFLAGS :=
EXTRA_CFLAGS :=
EXTRA_CPPFLAGS :=
EXTRA_LDFLAGS :=
asflags-y :=
ccflags-y :=
cppflags-y :=
ldflags-y :=subdir-asflags-y :=
subdir-ccflags-y :=
可以将其分为以下三类:
-
目标文件列表:obj-y、obj-m;
-
库文件列表:lib-y、lib-m;
-
编译标志变量:asflags-y、ccflags-y 等;
每个模块都有一个子 Makefile/Kbuild 文件,在子 Makefile 中添加需要编译的目标 (为 obj-y 赋值),例如,在 I.MX6U SoC 对应的文件夹 arch/arm/cpu/armv7/mx6 中,包含一个子 Makefile 文件,其中的内容如下:
obj-y := soc.o clock.o
...
obj-$(CONFIG_MODULE_FUSE) += module_fuse.o
可以看到,该子 Makefile 中将变量 obj-y 的值设置为 soc.o clock.o,表示在 uboot 编译阶段,需要编译的目标为 SoC (soc.o) 和系统时钟 (clock.o)
7.2.3.获取子 Makefile 或 Kbuild 文件
部分模块使用 Kbuild 文件配置需要编译的目标,该文件的作用和 Makefile 一样,但其格式更为规范且支持更多高级特性,Makefile.build 脚本中使用以下代码查找 Kbuild 文件:
# 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)
这段代码分为三个部分:
-
第一行设置子目录的实际路径:
-
若 $(src) 包含路径分隔符 /,则直接使用 $(src) 作为目录的路径;
-
否则,将 $(src) 与源码树根目录 ($(srctree)) 拼接,形成完整路径;
-
例如,若 src = drivers/usb 则 kbuild-dir = drivers/usb;
-
若 src = usb 则 kbuild-dir = $(srctree)/usb;
-
-
第二行在目标所属的目录中查找对应的编译规则文件:
-
优先查找 Kbuild 文件,若不存在,则查找 Makefile文件;
-
例如,若 drivers/usb/Kbuild 存在,则 kbuild-file = drivers/usb/Kbuild;
-
若不存在,则查找 drivers/usb/Makefile;
-
-
第三行将找到的编译规则文件,包含到当前的 Makefile 中,从而获取子目录的编译规则;
编译 .config 配置文件的命令如下:
make mx6ull_14x14_ddr512_emmc_defconfig
其最终生成的编译命令为:
make -f ./scripts/Makefile.build obj=scripts/basic
rm -f . tmp_quiet_recordmcount
make -f ./scripts/Makefile.build obj=scripts/kconfig mx6ull_14x14_ddr512_emmc_defconfig
变量 obj 的值为 scripts/basic,Makefile.build 脚本通过以下代码获取变量 src 的值:
# 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
由之前的分析可知,变量 src 的值是变量 obj 的值去除了前缀 tpl/ 和 spl/,因此:
-
src 的值为 scripts/basic;
-
变量 srctree 的值为 . ;
获取变量 kbuild-dir 值的方式如下:
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
因此,变量 kbuild-dir 的值展开为:
$(if $(filter /%, scripts/basic), scripts/basic, ./scripts/basic)
-
$(if $(filter /%, scripts/basic) 提取 / 开头的单词:
-
如果有,则 kbuild-dir 的值为 scripts/basic;
-
如果没有,则 kbuild-dir 的值为 ./scripts/basic;
-
而 scripts/basic 不以 / 开头,因此 kbuild-dir 的值为 ./scripts/basic;
-
获取变量 kbuild-file 值的方式为:
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
这行代码检查在 ./scrpts/basic 目录中是否存在 Kbuild 文件:
-
若存在,则 kbuild-file 的值为 ./scrpts/basic/kbuild;
-
若不存在,则 kbuild-file 的值为 ./scrpts/basic/Makefile;
-
这里 ./scrpts/basic 目录中不存在 kbuild 文件,因此,kbuild-file 的值为 ./scrpts/basic/Makefile;
因此,Makefile.build 脚本中会通过以下代码包含 ./scrpts/basic/Makefile:
include $(kbuild-file)
# 展开结果:
include ./scrpts/basic/Makefile
./scrpts/basic/Makefile 文件中的内容如下:
hostprogs-y := fixdep
always := $(hostprogs-y)# fixdep is needed to compile other host programs
$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep
-
变量 hostprogs-y 的值为 fixdep;
-
最后一行代码为 always 变量加上前缀 (路径),因此,变量 always 的值为 scrpts/basic/fixdep;
7.2.4.编译 scrpts/basic 中的 fixdep 等工具
使用以下命令编译 scrpts/basic 中的工具 (如 fixdep 等):
make -f ./scripts/Makefile.build obj=scripts/basic
这条编译命令并未指定目标 target,因此会使用 Makefile.build 脚本中的默认目标 __build:
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \$(subdir-ym) $(always)@:
顶层 Makefile 中定义了变量 KBUILD_BUILTIN 和变量 KBUILD_MODULES 的默认值:
# Decide whether to build built-in, modular, or both.
# Normally, just do built-in.
KBUILD_MODULES :=
KBUILD_BUILTIN := 1
因此,默认目标 __build 展开为:
__build:$(builtin-target) $(lib-target) $(extra-y)) $(subdir-ym) $(always)@:
可以看到目标 __build 的依赖项为:builtin-target、lib-target、extra-y、subdir-ym 和 always
在编译工具 scripts/basic 目录的 Makefile 中,定义了变量 always 的值 (scripts/basic/fixdep),其余并未定义,因此 __build 的值为:
__build: scripts/basic/fixdep@:
目标 __build 依赖 scripts/basic/fixdep,因此会先编译 scripts/basic/fixdep.c
综上,scripts_basic 目标的作用为编译 scripts/basic/fixdep
7.2.5.生成 .config 配置文件
生成 .config 配置文件所使用的命令如下:
make mx6ull_14x14_ddr512_emmc_defconfig
其最终生成的编译命令为:
make -f ./scripts/Makefile.build obj=scripts/basic
rm -f . tmp_quiet_recordmcount
make -f ./scripts/Makefile.build obj=scripts/kconfig mx6ull_14x14_ddr512_emmc_defconfig
上一节已经分析了 scripts/basic 的编译规则
本小节分析目标 mx6ull_14x14_ddr512_emmc_defconfig 的编译规则:
make -f ./scripts/Makefile.build obj=scripts/kconfig mx6ull_14x14_ddr512_emmc_defconfig
根据 4.1.6 小节的分析可知,该命令匹配的编译规则为:
%config: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@
该子模块相关变量的值为:
src= scripts/kconfig
kbuild-dir = ./scripts/kconfig
kbuild-file = ./scripts/kconfig/Makefile
include ./scripts/kconfig/Makefile
Makefilke.build 脚本会读取 scripts/kconfig/Makefile,其中的内容如下:
%_defconfig: $(obj)/conf$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)
目标 mx6ull_14x14_ddr512_emmc_defconfig 和 _defconfig 匹配,因此会应用该编译规则
依赖项为 $(obj)/conf,展开为 scripts/kconfig/conf,conf 为主机生成的编译工具,不必过于关心
获取 scripts/kconfig/conf 后会执行目标 %_defconfig 的命令:
$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)
相关变量的值为:
Q = @ 或 空
silent=-s 或 空
SRCARCH=..
Kconfig=Kconfig
因此目标 %_defconfig 的编译命令展开为:
%_defconfig: $(obj)/confscripts/kconfig/conf --defconfig=arch/../configs/xxx_defconfig Kconfig
该命令使用 xxx_defconfig 文件 (如 mx6ull_alientek_emmc_defconfig),将 mx6ull_alientek_emmc_defconfig 中的配置输出到 .config 文件中,最终生成 uboot 根目录下的 .config 文件
7.3.生成 u-boot.bin 的编译规则
编译 uboot 使用的命令为:
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12
编译 uboot 时并未指定目标,因此与顶层 Makefile 中的默认目标 _all 匹配:
# That's our default target when none is given on the command line
PHONY := _all
_all:
7.3.1.检索目标 u-boot.bin
默认目标_all 依赖 all,而目标 all 依赖 $(ALL-y):
# 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
...all:$(ALL-y)
ifneq ($(CONFIG_SYS_GENERIC_BOARD),y)
...
目标 ALL-y 的值如下:
# 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 u-boot.cfg binary_size_checkALL-$(CONFIG_ONENAND_U_BOOT) += u-boot-onenand.bin
ifeq ($(CONFIG_SPL_FSL_PBL),y)
ALL-$(CONFIG_RAMBOOT_PBL) += u-boot-with-spl-pbl.bin
else
...
目标 ALL-y 包含 u-boot.srec、u-boot.bin 等文件,根据 uboot 的配置不同也可能包含其他文件,例如:
ALL-$(CONFIG_ONENAND_U_BOOT) += u-boot-onenand.bin
CONFIG_ONENAND_U_BOOT 配置 uboot 的 ONENAND Flash,若使能 ONENAND Flash,在 .config 配置文件中会生成 CONFIG_ONENAND_U_BOOT=y,即 CONFIG_ONENAND_U_BOOT 变量的值为 y,因此展开结果为:
ALL-y += u-boot-onenand.bin
目标 ALL-y 中的 u-boot.bin 为 uboot 的二进制可执行文件
7.3.2.u-boot.bin 的编译规则
顶层 Makefile 中,u-boot.bin 目标对应的规则为:
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
函数 if_changed 在 scripts/Kbuild.include 中定义,用于检查依赖项是否比目标文件新,或命令行是否发生变化,仅当这两种情况下才会重新编译 uboot,从而避免重复编译,其定义如下:
# Execute command if command has changed or prerequisite(s) are updated.
#
if_changed = $(if $(strip $(any-prereq) $(arg-check)), \@set -e; \$(echo-cmd) $(cmd_$(1)); \printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)
配置文件 .config 中没有定义 CONFIG_OF_SEPARATE 的值,因此 u-boot.bin 的实际编译规则为:
u-boot.bin: u-boot-nodtb.bin FORCE$(call if_changed,copy)
从中可以看到,目标 u-boot.bin 依赖 u-boot-nodtb.bin
u-boot-nodtb.bin 是 UBoot 编译过程中生成的不含设备树的 UBoot 镜像,其编译规则为:
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,顶层 Makefile 中目标 u-boot 的编译规则如下:
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
从中可以看到,目标 u-boot 依赖 u-boot_init、u-boot-main 和 u-boot.lds
7.3.3.CPU 初始化程序 - u-boot_init
变量 u-boot_init 为初始化代码,包含 arch/arm/lib/start.o 等程序,其在顶层 Makefile 中的定义如下:
u-boot-init := $(head-y)
$(head-y) 与 CPU 架构有关,这里使用的是 ARM 芯片,所以 head-y 在 arch/arm/Makefile 中指定为:
head-y := arch/arm/cpu/$(CPU)/start.o
变量 CPU 在 .config 配置文件中设置为 armv7,因此,变量 head-y 展开结果为:
head-y := arch/arm/cpu/armv7/start.o
因此,变量 u-boot-init 的值为:
u-boot-init= arch/arm/cpu/armv7/start.o
7.3.4.平台的各个模块 - u-boot-main
变量 u-boot-main 为 uboot 主程序的目标文件列表,包含所有模块的 .o 文件:
u-boot-main := $(libs-y)
$(libs-y) 为 uboot 所有子模块目录中 build-in.o 的集合:
libs-y += lib/
libs-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/
libs-$(CONFIG_OF_EMBED) += dts/
libs-y += fs/
libs-y += net/
libs-y += disk/
libs-y += drivers/
libs-y += drivers/dma/
...
libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y := $(patsubst %/, %/built-in.o, $(libs-y)) 将 libs-y 中的 / 替换为 /built-in.o,例如,drivers/dma/ 替换后为 drivers/dma/built-in.o,因此,uboot-main 为所有子目录中 built-in.o 的集合
7.3.5.链接脚本 - u-boot.lds
u-boot.lds 为链接脚本,将 arch/arm/cpu/armv7/start.o 和各个子目录下的 built-in.o 链接在一起生成 u-boot:
u-boot.lds: $(LDSCRIPT) prepare FORCE$(call if_changed_dep,cpp_lds)
顶层 Makefile 中定义了链接脚本的查找规则:
# If board code explicitly specified LDSCRIPT or CONFIG_SYS_LDSCRIPT, use
# that (or fail if absent). Otherwise, search for a linker script in a
# standard location.ifndef LDSCRIPT#LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds.debugifdef CONFIG_SYS_LDSCRIPT# need to strip off double quotesLDSCRIPT := $(srctree)/$(CONFIG_SYS_LDSCRIPT:"%"=%)endif
endif# If there is no specified link script, we look in a number of places for it
ifndef LDSCRIPTifeq ($(wildcard $(LDSCRIPT)),)LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.ldsendififeq ($(wildcard $(LDSCRIPT)),)LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.ldsendififeq ($(wildcard $(LDSCRIPT)),)LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.ldsendif
endif
其分为两个阶段:
-
第一阶段:
-
检查是否在命令行中指定了链接脚本 (make LDSCRIPT=...),若用户指定了链接脚本,则直接使用;
-
否则,检查配置项 CONFIG_SYS_LDSCRIPT,如 CONFIG_SYS_LDSCRIPT="board/arm/xxx/u-boot.lds";
-
-
第二阶段:
-
在开发板对应的目录中查找,例如,board/freescale/u-boot.lds,IMX 没有在其中定义链接脚本;
-
在 SoC 对应的目录中查找,例如,arch/arm/cpu/armv7/u-boot.lds,该链接脚本不存在;
-
在通用架构对应的目录中查找,例如,arch/arm/cpu/u-boot.lds,该链接脚本存在;
-
因此,u-boot.lds 最终对应的链接脚本为 arch/arm/cpu/u-boot.lds;
-
7.3.6.子模块 built-in.o 编译规则
Makefile.build 脚本会读取子模块的 Makefile 以确定其是否需要编译,例如 cmd/Makefile 中定义了各种类型的外设:
...
obj-$(CONFIG_CMD_FUSE) += fuse.o
obj-$(CONFIG_CMD_GETTIME) += gettime.o
obj-$(CONFIG_CMD_GPIO) += gpio.o
obj-$(CONFIG_CMD_I2C) += i2c.o
obj-$(CONFIG_CMD_IOTRACE) += iotrace.o
...
编译时会读取 .config 配置文件,若模块需要编译,则其对应 CONFIG_xxx 的值会设置为 y,从而将该模块添加到需要编译的目标 obj-y 中,以 IMX6U 的编译为例,其 .config 中对应 GPIO 模块的配置为:
CONFIG_CMD_GPIO=y
因此,cmd/Makefile 中对应的条目就会变为:
obj-y += gpio.o
Makefile.build 脚本中的 obj-y 保存所有需要编译的子模块,同样的还有 obj-m、lib-y 等
模块编译后,通过链接脚本链接平台对应的 GPIO 模块,该链接脚本为 drivers/gpio/ 目录下的 .built-in.o.cmd 文件:
cmd_drivers/gpio/built-in.o := arm-linux-gnueabihf-ld.bfd -r -o \drivers/gpio/built-in.o drivers/gpio/mxc_gpio.o
Makefile.build 脚本会检查是否有需要编译的目标,如果有则将其存入变量 builtin-target 中:
ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
builtin-target := $(obj)/built-in.o
endif
因为在编译时未指定目标,因此会匹配 Makefile.build 脚本中的默认目标 __build:
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \$(subdir-ym) $(always)@:
与生成 .config 配置文件的编译规则一样,Makefile.build 脚本会在模块对应的目录中查找 Makefile/Kbuild 文件,然后编译后缀为 y 的目标
8.Makefile.build 脚本分析
Makefile.build 脚本负责递归编译子目录中的代码
8.1.检查目标所处阶段
与大多数 BootLoader 一样,uboot 也分为多个阶段:
-
TPL:Tertiary Program Loader,底层引导程序,此时编译的程序要求尽可能小,主要用于加载 VPL 及 SPL;
-
VPL:Verifying Program Loader,验证引导程序,可以在多个 SPL 二进制文件中选择一个运行;
-
SPL:Secondary Program Loader,二级引导程序,主要用于配置内存 DDR 等核心模块;
-
UBoot:运行阶段 (uboot 主程序),在该阶段中配置部分外设,并支持命令行操作,同时负责加载操作系统;
因此,编译前会检查当前编译的目标所位于的阶段,从而读取对应的配置文件,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 阶段;
-
src := $(patsubst $(prefix)/%,%,$(obj)) 使用 patsubst 函数移除 obj 中包含的 tpl/ 前缀,例如:若 obj = tpl/start,则 src = start;
-
ifeq ($(obj),$(src)) 检查移除 tpl/ 前缀后,obj 的路径是否和原来的一致,即判断 obj 的路径中是否包含 tpl/ 前缀 (判断所编译的目标是否属于 TPL 阶段);若一致 (即目标 obj 的路径中不包含 tpl/ 前缀),则认为目标不属于 TPL 阶段,将变量 prefix 的值设置为 spl;
-
src := $(patsubst $(prefix)/%,%,$(obj)) 和 ifeq ($(obj),$(src)) 判断目标是否属于 SPL 阶段,若目标也不属于 SPL 阶段,则将变量 prefix 的值设置为 . ,表示属于主程序阶段,即属于 uboot 主程序;
编译前会根据目标所处阶段,加载对应的配置文件:
# Read auto.conf if it exists, otherwise ignore
# Modified for U-Boot
-include include/config/auto.conf
-include $(prefix)/include/autoconf.mk
include scripts/Makefile.uncmd_spl
-
-include $(prefix)/include/autoconf.mk 根据当前目标的所处阶段,加载对应的 autoconf.mk 文件;
-
当前使用的 2016.03 版本的 uboot 仅包含 SPL 的配置文件 Makefile.uncmd_spl,其余文件不存在;
8.2.获取需要编译的目标 - obj-y
Makefile.build 脚本中定义了一些变量,用于保存标志信息以及需要编译的目标:
# Init all relevant variables used in kbuild files so
# 1) they have correct type
# 2) they do not inherit any value from the environment
obj-y :=
obj-m :=
lib-y :=
lib-m :=
always :=
targets :=
subdir-y :=
subdir-m :=
EXTRA_AFLAGS :=
EXTRA_CFLAGS :=
EXTRA_CPPFLAGS :=
EXTRA_LDFLAGS :=
asflags-y :=
ccflags-y :=
cppflags-y :=
ldflags-y :=subdir-asflags-y :=
subdir-ccflags-y :=
可以将其分为以下三类:
-
目标文件列表:obj-y、obj-m;
-
库文件列表:lib-y、lib-m;
-
编译标志变量:asflags-y、ccflags-y 等;
每个模块都有一个子 Makefile,在子 Makefile 中添加需要编译的目标 (为 obj-y 赋值),例如,在 I.MX6U SoC 对应的文件夹 arch/arm/cpu/armv7/mx6 中,包含一个子 Makefile 文件,其中的内容如下:
obj-y := soc.o clock.o
...
obj-$(CONFIG_MODULE_FUSE) += module_fuse.o
可以看到,该子 Makefile 中将变量 obj-y 的值设置为 soc.o clock.o,表示在 uboot 编译阶段,需要编译的目标为 SoC (soc.o) 和系统时钟 (clock.o)
8.3.查找子模块的 Kbuild/Makefile 文件
部分模块使用 Kbuild 文件配置需要编译的目标,该文件的作用和 Makefile 一样,但其格式更为规范且支持更多高级特性,Makefile.build 脚本中使用以下代码查找 Kbuild 文件:
# 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)
这段代码分为三个部分:
-
第一行设置子目录的实际路径:
-
若 $(src) 包含路径分隔符 /,则直接使用 $(src) 作为目录的路径;
-
否则,将 $(src) 与源码树根目录 ($(srctree)) 拼接,形成完整路径;
-
例如,若 src = drivers/usb 则 kbuild-dir = drivers/usb;
-
若 src = usb 则 kbuild-dir = $(srctree)/usb;
-
-
第二行在目标所属的目录中查找对应的编译规则文件:
-
优先查找 Kbuild 文件,若不存在,则查找 Makefile文件;
-
例如,若 drivers/usb/Kbuild 存在,则 kbuild-file = drivers/usb/Kbuild;
-
若不存在,则查找 drivers/usb/Makefile;
-
-
第三行将找到的编译规则文件,包含到当前的 Makefile 中,从而获取子目录的编译规则;
8.4.汇编文件的编译规则
Makefile.build 脚本中,汇编文件的编译规则如下:
# Compile assembler sources (.S)
# ---------------------------------------------------------------------------modkern_aflags := $(KBUILD_AFLAGS_KERNEL) $(AFLAGS_KERNEL)$(real-objs-m) : modkern_aflags := $(KBUILD_AFLAGS_MODULE) $(AFLAGS_MODULE)
$(real-objs-m:.o=.s): modkern_aflags := $(KBUILD_AFLAGS_MODULE) $(AFLAGS_MODULE)quiet_cmd_as_s_S = CPP $(quiet_modtag) $@
cmd_as_s_S = $(CPP) $(a_flags) -o $@ $<$(obj)/%.s: $(src)/%.S FORCE$(call if_changed_dep,as_s_S)quiet_cmd_as_o_S = AS $(quiet_modtag) $@
cmd_as_o_S = $(CC) $(a_flags) -c -o $@ $<$(obj)/%.o: $(src)/%.S FORCE$(call if_changed_dep,as_o_S)
8.4.1.目标文件分类
变量 modkern_aflags 用于标记所编译的目标,是属于内核还是属于外设模块:
modkern_aflags := $(KBUILD_AFLAGS_KERNEL) $(AFLAGS_KERNEL)
内核模块需要特殊的标志,如 -fno-builtin、-nostdinc 等
需要编译进内核的目标定义为 real-objs-y,需要编译为模块的目标定义为 real-objs-m
Makefile.build 脚本中使用变量 real-objs-m 区分该 .o 文件是否需要编译进内核,变量 real-objs-m:.o=.s 表示该 .o 文件由汇编文件编译获得:
$(real-objs-m) : modkern_aflags := $(KBUILD_AFLAGS_MODULE) $(AFLAGS_MODULE)
$(real-objs-m:.o=.s): modkern_aflags := $(KBUILD_AFLAGS_MODULE) $(AFLAGS_MODULE)
8.4.2.汇编文件 .s 编译成 .o 文件
.S 文件中可能包含头文件 #include、宏、条件编译等,编译前需要将其展开:
quiet_cmd_as_s_S = CPP $(quiet_modtag) $@
cmd_as_s_S = $(CPP) $(a_flags) -o $@ $<
使用以下代码直接将 .S 文件编译为 .o 文件:
$(obj)/%.s: $(src)/%.S FORCE$(call if_changed_dep,as_s_S)
编译所使用的命令为 as_s_S:
quiet_cmd_as_s_S = CPP $(quiet_modtag) $@
cmd_as_s_S = $(CPP) $(a_flags) -o $@ $<
编译工具 $(CPP) 为交叉编译器,在顶层 Makefile 中定义:
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
8.5.C 文件的编译规则
C 文件的编译使用 $(CC),具体的编译规则如下:
# C (.c) files
# The C file is compiled and updated dependency information is generated.
# (See cmd_cc_o_c + relevant part of rule_cc_o_c)quiet_cmd_cc_o_c = CC $(quiet_modtag) $@ifndef CONFIG_MODVERSIONS
cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<else
# When module versioning is enabled the following steps are executed:
# o compile a .tmp_<file>.o from <file>.c
# o if .tmp_<file>.o doesn't contain a __ksymtab version, i.e. does
# not export symbols, we just rename .tmp_<file>.o to <file>.o and
# are done.
# o otherwise, we calculate symbol versions using the good old
# genksyms on the preprocessed source and postprocess them in a way
# that they are usable as a linker script
# o generate <file>.o from .tmp_<file>.o using the linker to
# replace the unresolved symbols __crc_exported_symbol with
# the actual value of the checksum generated by genksymscmd_cc_o_c = $(CC) $(c_flags) -c -o $(@D)/.tmp_$(@F) $<
cmd_modversions = \if $(OBJDUMP) -h $(@D)/.tmp_$(@F) | grep -q __ksymtab; then \$(call cmd_gensymtypes,$(KBUILD_SYMTYPES),$(@:.o=.symtypes)) \> $(@D)/.tmp_$(@F:.o=.ver); \\$(LD) $(LDFLAGS) -r -o $@ $(@D)/.tmp_$(@F) \-T $(@D)/.tmp_$(@F:.o=.ver); \rm -f $(@D)/.tmp_$(@F) $(@D)/.tmp_$(@F:.o=.ver); \else \mv -f $(@D)/.tmp_$(@F) $@; \fi;
endififdef CONFIG_FTRACE_MCOUNT_RECORD
ifdef BUILD_C_RECORDMCOUNT
ifeq ("$(origin RECORDMCOUNT_WARN)", "command line")RECORDMCOUNT_FLAGS = -w
endif
# Due to recursion, we must skip empty.o.
# The empty.o file is created in the make process in order to determine
# the target endianness and word size. It is made before all other C
# files, including recordmcount.
sub_cmd_record_mcount = \if [ $(@) != "scripts/mod/empty.o" ]; then \$(objtree)/scripts/recordmcount $(RECORDMCOUNT_FLAGS) "$(@)"; \fi;
recordmcount_source := $(srctree)/scripts/recordmcount.c \$(srctree)/scripts/recordmcount.h
else
sub_cmd_record_mcount = set -e ; perl $(srctree)/scripts/recordmcount.pl "$(ARCH)" \"$(if $(CONFIG_CPU_BIG_ENDIAN),big,little)" \"$(if $(CONFIG_64BIT),64,32)" \"$(OBJDUMP)" "$(OBJCOPY)" "$(CC) $(KBUILD_CFLAGS)" \"$(LD)" "$(NM)" "$(RM)" "$(MV)" \"$(if $(part-of-module),1,0)" "$(@)";
recordmcount_source := $(srctree)/scripts/recordmcount.pl
endif
cmd_record_mcount = \if [ "$(findstring $(CC_FLAGS_FTRACE),$(_c_flags))" = \"$(CC_FLAGS_FTRACE)" ]; then \$(sub_cmd_record_mcount) \fi;
endifdefine rule_cc_o_c$(call echo-cmd,checksrc) $(cmd_checksrc) \$(call echo-cmd,cc_o_c) $(cmd_cc_o_c); \$(cmd_modversions) \$(call echo-cmd,record_mcount) \$(cmd_record_mcount) \scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' > \$(dot-target).tmp; \rm -f $(depfile); \mv -f $(dot-target).tmp $(dot-target).cmd
endef
8.5.1.生成临时目标文件
编译输出到临时文件,如 .tmp_usb.o,避免污染最终目标文件:
cmd_cc_o_c = $(CC) $(c_flags) -c -o $(@D)/.tmp_$(@F) $<
8.5.2.符号表重链接
-
使用 objdump -h 检查目标文件是否包含 __ksymtab 段 (导出符号表);
-
生成符号版本文件;
-
使用 -r (relocatable) 选项重新链接,将版本信息嵌入目标文件;
cmd_modversions = \if $(OBJDUMP) -h $(@D)/.tmp_$(@F) | grep -q __ksymtab; then \$(call cmd_gensymtypes,$(KBUILD_SYMTYPES),$(@:.o=.symtypes)) \> $(@D)/.tmp_$(@F:.o=.ver); \\$(LD) $(LDFLAGS) -r -o $@ $(@D)/.tmp_$(@F) \-T $(@D)/.tmp_$(@F:.o=.ver); \rm -f $(@D)/.tmp_$(@F) $(@D)/.tmp_$(@F:.o=.ver); \else \mv -f $(@D)/.tmp_$(@F) $@; \fi;
8.5.3.执行流程
rule_cc_o_c 定义了从源码文件到目标文件的完整过程:
define rule_cc_o_c$(call echo-cmd,checksrc) $(cmd_checksrc) \$(call echo-cmd,cc_o_c) $(cmd_cc_o_c); \$(cmd_modversions) \$(call echo-cmd,record_mcount) \$(cmd_record_mcount) \scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' > \$(dot-target).tmp; \rm -f $(depfile); \mv -f $(dot-target).tmp $(dot-target).cmd
endef
-
$(call echo-cmd,checksrc) $(cmd_checksrc) 调用 check 工具分析源码,检查是否存在未使用的变量、类型不匹配问题等;
-
$(call echo-cmd,cc_o_c) $(cmd_cc_o_c); 将源文件编译为目标文件;
-
$(cmd_modversions) 对导出的符号表进行 CRC 校验,确保模块和内核版本兼容;
-
$(call echo-cmd,record_mcount) 和 $(cmd_record_mcount) 跟踪函数的调用流程,为 ftrace 等性能工具提供支持;
-
scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' > $(dot-target).tmp; 调用 fixdep 分析依赖项,例如,drivers/usb/usb.o: drivers/usb/usb.c include/linux/usb.h include/config.h;
8.6.链接目标文件
各个子模块编译生成的输出为 xxx/built-in.o (如 drivers/block/built-in.o),通过 LD 链接文件将这些模块链接为一个 .o 文件,并最终生成 u-boot.bin 文件:
builtin-target := $(obj)/built-in.o
...
#
# Rule to compile a set of .o files into one .o file
#
ifdef builtin-target
quiet_cmd_link_o_target = LD $@
# If the list of objects to link is empty, just create an empty built-in.o
cmd_link_o_target = $(if $(strip $(obj-y)),\$(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \$(cmd_secanalysis),\rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@)$(builtin-target): $(obj-y) FORCE$(call if_changed,link_o_target)targets += $(builtin-target)
endif # builtin-target
8.7.链接生成 Lib 库文件
使用 AR 工具创建新的静态库文件,然后通过 link_l_target 将所有 .o 文件链接生成 .a 库文件:
ifneq ($(strip $(lib-y) $(lib-m) $(lib-)),)
lib-target := $(obj)/lib.a
endif
...
#
# Rule to compile a set of .o files into one .a file
#
ifdef lib-target
quiet_cmd_link_l_target = AR $@
cmd_link_l_target = rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@ $(lib-y)$(lib-target): $(lib-y) FORCE$(call if_changed,link_l_target)targets += $(lib-target)
endif
8.8.递归编译子模块的规则
Makefile.build 脚本会递归搜索子目录中需要编译的模块,需要搜索的子目录保存在变量 subdir-ym 中,该变量在 scripts/Makefile.lib 中定义:
subdir-ym := $(sort $(subdir-y) $(subdir-m))
Makefile.build 脚本中递归搜索子目录的规则如下:
# Descending
# ---------------------------------------------------------------------------PHONY += $(subdir-ym)
$(subdir-ym):$(Q)$(MAKE) $(build)=$@
其执行流程如下:
-
解析 subdir-y 和 subdir-m 列表;
-
对每个子目录执行 make $(build)=子目录路径;
-
递归调用 Makefile.build 脚本;
8.9.默认目标 - __build
当用户未在命令行中指定编译目标时,会与默认的目标 __build 匹配:
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \$(subdir-ym) $(always)@:
默认目标 __build 的依赖项为:
-
内置目标:built-in.o;
-
库文件:lib.a;
-
模块:obj-m;
-
子目录:subdir-ym;
-
强制目标:always;