目录
前言
顶层 Makefile
源码简析
版本号
MAKEFLAGS 变量
命令输出
静默输出
设置编译结果输出目录
代码检查
模块编译
设置目标架构和交叉编译器
调用 scripts/Kbuild.include 文件
交叉编译工具变量设置
头文件路径变量
导出变量
make xxx_defconfig 过程
Makefile.build 脚本分析
scripts_basic 目标对应的命令
%config 目标对应的命令
前言
在移植linux内核之前,我们先来学习一下 Linux 内核的顶层 Makefile 文件,因为顶层 Makefile 控制着 Linux 内核的编译流程。
顶层 Makefile
Linux 的顶层 Makefile 和 uboot 的顶层 Makefile 非常相似,感兴趣的可以看下:U-Boot 顶层 Makefile 简析。
源码简析
版本号
顶层 Makefile 一开始就是 Linux 内核的版本号,如下所示:
VERSION = 4
PATCHLEVEL = 1
SUBLEVEL = 15
EXTRAVERSION =
可以看出, Linux 内核版本号为 4.1.15。
MAKEFLAGS 变量
MAKEFLAGS 变量设置如下所示:
MAKEFLAGS += -rR --include-dir=$(CURDIR)
选项 | 作用 |
---|---|
| 禁用内置规则:取消Make预定义的隐式规则(如 |
| 禁用内置变量:忽略Make预定义的隐含变量(如 |
| 指定头文件搜索路径:将当前目录加入头文件搜索路径 |
命令输出
Linux 编译的时候也可以通过“V=1”来输出完整的命令,这个和 uboot 一样,相关代码如下所示:
# 检查变量V是否来自命令行参数
ifeq ("$(origin V)", "command line")KBUILD_VERBOSE = $(V) # 如果通过make V=1调用,则继承该值
endif# 设置默认详细级别(未指定时默认为0)
ifndef KBUILD_VERBOSEKBUILD_VERBOSE = 0 # 默认关闭详细输出模式
endif# 根据详细级别设置编译行为
ifeq ($(KBUILD_VERBOSE),1)quiet = # 空值表示显示完整命令Q = # 空值取消命令隐藏
elsequiet = quiet_ # 前缀用于生成简洁日志Q = @ # @符号隐藏命令回显
endif
静默输出
Linux 编译的时候使用“make -s”就可实现静默编译,编译的时候就不会打印任何的信息,同 uboot 一样,相关代码如下:
# 检测Make版本是否为4.x系列
ifneq ($(filter 4.%,$(MAKE_VERSION)),) # make-4.x版本处理逻辑# 检查MAKEFLAGS是否包含-s选项(静默模式)ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)quiet=silent_ # 启用静默模式输出前缀endif
else # make-3.8x版本处理逻辑# 兼容旧版本make的-s选项检测ifneq ($(filter s% -s%,$(MAKEFLAGS)),)quiet=silent_ # 启用静默模式输出前缀endif
endif# 导出关键变量到子make进程
export quiet Q KBUILD_VERBOSE
设置编译结果输出目录
Linux 编译的时候使用“O=xxx”即可将编译产生的过程文件输出到指定的目录中,相关代码如下:
# 检查是否在源码目录内构建(KBUILD_SRC为空表示是)
ifeq ($(KBUILD_SRC),)# 当前直接在内核源码目录执行make# 检测是否通过命令行参数O指定输出目录ifeq ("$(origin O)", "command line")KBUILD_OUTPUT := $(O) # 使用用户指定的输出目录路径endif
endif
代码检查
Linux 也支持代码检查:
- 使用命令“make C=1”使能代码检查,检查那些需要重新编译的文件。
- “make C=2”用于检查所有的源码文件。
顶层 Makefile 中的代码如下:
# 检查变量C是否来自命令行参数(如 make C=1)
ifeq ("$(origin C)", "command line")KBUILD_CHECKSRC = $(C) # 继承用户指定的值
endif# 设置默认源码检查级别(未指定时默认为0)
ifndef KBUILD_CHECKSRCKBUILD_CHECKSRC = 0 # 默认关闭源码检查
endif
模块编译
Linux 允许单独编译某个模块,使用命令“make M=dir”即可,旧语法“make SUBDIRS=dir”也是支持的。
顶层 Makefile 中的代码如下:
# 处理外部模块构建目录指定方式(两种兼容语法)
# 1. 老式语法:make ... SUBDIRS=$PWD
# 2. 新式语法:make M=dir
# 环境变量 KBUILD_EXTMOD 优先级最高
ifdef SUBDIRSKBUILD_EXTMOD ?= $(SUBDIRS) # 兼容旧版SUBDIRS参数
endif# 检查命令行是否指定M参数
ifeq ("$(origin M)", "command line")KBUILD_EXTMOD := $(M) # 使用新式M参数指定模块目录
endif# 根据是否构建外部模块设置默认构建目标
# 内部构建:依赖all目标
# 外部模块构建:依赖modules目标
PHONY += all
ifeq ($(KBUILD_EXTMOD),)_all: all # 常规内核构建
else_all: modules # 外部模块构建
endif# 设置源码树路径
ifeq ($(KBUILD_SRC),)# 在源码目录内构建srctree := . # 源码树设为当前目录
elseifeq ($(KBUILD_SRC)/,$(dir $(CURDIR)))# 在源码树的子目录构建srctree := .. # 源码树设为上级目录else# 完全外部构建srctree := $(KBUILD_SRC) # 使用指定的源码树路径endif
endif# 设置对象树路径(总是当前目录)
objtree := .
src := $(srctree) # 源码路径别名
obj := $(objtree) # 构建路径别名# 设置VPATH(Makefile搜索路径)
# 包含源码树和外部模块目录(如果指定)
VPATH := $(srctree)$(if $(KBUILD_EXTMOD),:$(KBUILD_EXTMOD))# 导出关键路径变量
export srctree objtree VPATH
外部模块编译过程和 uboot 也一样,最终导出 srctree、 objtree 和 VPATH 这三个变量的值,其中 srctree=.,也就是当前目录, objtree 同样为“.”。
设置目标架构和交叉编译器
同 uboot 一样, Linux 编译的时候需要设置目标板架构ARCH 和交叉编译器 CROSS_COMPILE,在顶层 Makefile 中代码如下:
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?= $(CONFIG_CROSS_COMPILE:"%"=%)
为了方便,一般直接修改顶层 Makefile 中的 ARCH 和 CROSS_COMPILE,直接将其设置为对应的架构和编译器,比如本教程将 ARCH 设置为为 arm, CROSS_COMPILE 设置为 armlinux-gnueabihf-,如下所示:
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-
设置好以后我们就可以使用如下命令编译 Linux 了:
make xxx_defconfig //使用默认配置文件配置 Linux
make menuconfig //启动图形化配置界面
make -j16 //编译 Linux
调用 scripts/Kbuild.include 文件
同 uboot 一样, Linux 顶层 Makefile 也会调用文件 scripts/Kbuild.include,
顶层 Makefile 相应代码如下:
# We need some generic definitions (do not try to remake the file).
scripts/Kbuild.include: ;
include scripts/Kbuild.include
交叉编译工具变量设置
顶层 Makefile 中其他和交叉编译器有关的变量设置如下:
# 汇编器 (Assembler)
AS = $(CROSS_COMPILE)as # 用于将汇编代码编译为目标文件# 链接器 (Linker)
LD = $(CROSS_COMPILE)ld # 负责目标文件的链接和重定位# C编译器 (C Compiler)
CC = $(CROSS_COMPILE)gcc # 主编译工具,处理C语言源文件# 预处理器 (C Preprocessor)
CPP = $(CC) -E # 只进行预处理不编译(-E选项)# 静态库工具 (Archiver)
AR = $(CROSS_COMPILE)ar # 创建和管理静态库(.a文件)# 符号表查看器 (Symbol Lister)
NM = $(CROSS_COMPILE)nm # 列出目标文件的符号表# 二进制精简工具 (Binary Stripper)
STRIP = $(CROSS_COMPILE)strip # 去除调试符号减小文件体积# 二进制转换工具 (Object Copier)
OBJCOPY = $(CROSS_COMPILE)objcopy # 转换目标文件格式# 反汇编工具 (Object Dumper)
OBJDUMP = $(CROSS_COMPILE)objdump # 反汇编和调试信息提取
LA、 LD、 CC 等这些都是交叉编译器所使用的工具。
头文件路径变量
顶层 Makefile 定义了两个变量保存头文件路径: USERINCLUDE 和 LINUXINCLUDE,
相关代码如下:
# 用户空间头文件包含路径(UAPI接口)
USERINCLUDE := \-I$(srctree)/arch/$(hdr-arch)/include/uapi \ # 架构特定UAPI头文件-Iarch/$(hdr-arch)/include/generated/uapi \ # 生成的架构UAPI头文件-I$(srctree)/include/uapi \ # 通用UAPI头文件-Iinclude/generated/uapi \ # 生成的通用UAPI头文件-include $(srctree)/include/linux/kconfig.h # 强制包含kconfig头文件# 内核空间头文件包含路径(兼容O=外部构建选项)
LINUXINCLUDE := \-I$(srctree)/arch/$(hdr-arch)/include \ # 架构特定内核头文件-Iarch/$(hdr-arch)/include/generated/uapi \ # 生成的架构UAPI头文件(重复)-Iarch/$(hdr-arch)/include/generated \ # 生成的架构私有头文件$(if $(KBUILD_SRC), -I$(srctree)/include) \ # 外部构建时包含源码树头文件-Iinclude \ # 当前构建目录头文件$(USERINCLUDE) # 包含用户空间路径
LINUXINCLUDE变量,其中srctree=., hdr-arch=arm, KBUILD_SRC 为空,因此,将 USERINCLUDE 和 LINUXINCLUDE 展开以后为:
USERINCLUDE := \
-I./arch/arm/include/uapi \
-Iarch/arm/include/generated/uapi \
-I./include/uapi \
-Iinclude/generated/uapi \
-include ./include/linux/kconfig.hLINUXINCLUDE := \
-I./arch/arm/include \
-Iarch/arm/include/generated/uapi \
-Iarch/arm/include/generated \
-Iinclude \
-I./arch/arm/include/uapi \
-Iarch/arm/include/generated/uapi \
-I./include/uapi \
-Iinclude/generated/uapi \
-include ./include/linux/kconfig.h
导出变量
顶层 Makefile 会导出很多变量给子 Makefile 使用,导出的这些变量如下:
# 内核版本信息导出(用于版本标识)
export VERSION PATCHLEVEL SUBLEVEL KERNELRELEASE KERNELVERSION# 基础构建配置导出
export ARCH SRCARCH CONFIG_SHELL # 架构和shell配置
export HOSTCC HOSTCFLAGS # 主机工具链(用于构建host程序)
export CROSS_COMPILE # 交叉编译前缀(如arm-linux-gnueabi-)
export AS LD CC # 核心工具链(汇编器、链接器、编译器)# 二进制工具集导出
export CPP AR NM STRIP OBJCOPY OBJDUMP # 预处理器、静态库、符号表等工具# 脚本解释器和工具
export MAKE AWK GENKSYMS INSTALLKERNEL # make/awk/符号生成工具/内核安装脚本
export PERL PYTHON # 脚本解释器
export UTS_MACHINE # 机器标识符# 主机C++工具链(用于需要C++的构建步骤)
export HOSTCXX HOSTCXXFLAGS# 模块构建专用参数
export LDFLAGS_MODULE # 模块链接参数# 代码检查工具
export CHECK CHECKFLAGS # 静态分析工具(如sparse)# 预处理和包含路径
export KBUILD_CPPFLAGS NOSTDINC_FLAGS LINUXINCLUDE # 预处理标志和头文件路径# 二进制工具参数
export OBJCOPYFLAGS LDFLAGS # objcopy和链接器参数# C编译器标志集
export KBUILD_CFLAGS # 全局C标志
export CFLAGS_KERNEL # 内核核心编译标志
export CFLAGS_MODULE # 模块编译标志
export CFLAGS_GCOV # GCOV覆盖率测试标志
export CFLAGS_KASAN # KASAN内存检测标志# 汇编器标志集
export KBUILD_AFLAGS # 全局汇编标志
export AFLAGS_KERNEL # 内核核心汇编标志
export AFLAGS_MODULE # 模块汇编标志# 模块构建专用标志
export KBUILD_AFLAGS_MODULE KBUILD_CFLAGS_MODULE KBUILD_LDFLAGS_MODULE# 内核核心构建专用标志
export KBUILD_AFLAGS_KERNEL KBUILD_CFLAGS_KERNEL# 静态库工具参数
export KBUILD_ARFLAGS # ar命令参数
make xxx_defconfig 过程
第一次编译 Linux 之前都要使用“make xxx_defconfig”先配置 Linux 内核,在顶层 Makefile中有“%config”这个目标,如下所示:
# 初始化构建模式标志
config-targets := 0 # 是否为配置目标(如menuconfig)
mixed-targets := 0 # 是否混合了配置和编译目标
dot-config := 1 # 是否需要读取.config文件# 检查是否需要忽略.config(针对clean/mrproper等目标)
ifneq ($(filter $(no-dot-config-targets), $(MAKECMDGOALS)),)ifeq ($(filter-out $(no-dot-config-targets), $(MAKECMDGOALS)),)dot-config := 0 # 当只有clean类目标时禁用.config依赖endif
endif# 检测配置类目标(仅在内核构建时检查)
ifeq ($(KBUILD_EXTMOD),)ifneq ($(filter config %config,$(MAKECMDGOALS)),)config-targets := 1 # 标记为配置目标ifneq ($(words $(MAKECMDGOALS)),1)mixed-targets := 1 # 多个目标混合时标记endifendif
endif# 混合目标处理(如 make menuconfig all)
ifeq ($(mixed-targets),1)PHONY += $(MAKECMDGOALS) __build_one_by_one# 将目标重定向到顺序执行$(filter-out __build_one_by_one, $(MAKECMDGOALS)): __build_one_by_one@: # 空命令# 逐个目标执行__build_one_by_one:$(Q)set -e; \for i in $(MAKECMDGOALS); do \$(MAKE) -f $(srctree)/Makefile $$i; \done# 纯配置目标处理(如 make menuconfig)
else ifeq ($(config-targets),1)# 包含架构相关配置include arch/$(SRCARCH)/Makefileexport KBUILD_DEFCONFIG KBUILD_KCONFIG# 处理config类目标config: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@%config: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@# 常规构建目标处理
else# [常规构建流程...]
endif
这段代码里,最开始是设置定义变量 config-targets、 mixed-targets 和 dot-config的值,最终这三个变量的值为:
config-targets= 1
mixed-targets= 0
dot-config= 1
因此会引用 arch/arm/Makefile 这个文件,这个文件很重要,因为 zImage、 uImage 等这些文件就是由 arch/arm/Makefile 来生成的。
“make xxx_defconfig”与目标“%config”匹配,因此执行。
%config: scripts_basic outputmakefile FORCE$(Q)$(MAKE) $(build)=scripts/kconfig $@
“%config”依赖scripts_basic、 outputmakefile 和 FORCE,“%config”真正有意义的依赖就只有 scripts_basic。
scripts_basic 的规则如下:
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
$(Q)rm -f .tmp_quiet_recordmcount
其中,build 定义在文件 scripts/Kbuild.include 中,值为:
build := -f $(srctree)/scripts/Makefile.build obj
因此 scripts_basic展开为:
scripts_basic:
@make -f ./scripts/Makefile.build obj=scripts/basic //也可以没有@,视配置而定
@rm -f . tmp_quiet_recordmcount //也可以没有@
所以目标“%config”代入scripts_basic的值,展开为:
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
Makefile.build 脚本分析
我们现在已经知道:“ make xxx_defconfig“配置 Linux 的时候如下两行命令会执行脚本scripts/Makefile.build:
@make -f ./scripts/Makefile.build obj=scripts/basic
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
scripts_basic 目标对应的命令
打开文件 scripts/Makefile.build,有如下代码:
# Kbuild文件优先级高于Makefile
# 确定子目录路径(支持绝对路径和相对路径)
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))# 确定构建规则文件名(优先查找Kbuild,不存在则用Makefile)
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)# 包含找到的构建规则文件
include $(kbuild-file)
其中:将 kbuild-dir、kbuild-file都展开代入:
kbuild-dir =./scripts/basic
kbuild-file = ./scripts/basic/Makefile
include ./scripts/basic/Makefile
继续分析 scripts/Makefile.build,如下代码:
# 默认构建目标(通过__build伪目标实现)
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \$(subdir-ym) $(always)@: # 空命令(实际工作由依赖项完成)
__build 是默认目标,在顶层 Makefile 中, KBUILD_BUILTIN 为 1, KBUILD_MODULES 为空,因此展开后目标__build 为:
__build:$(builtin-target) $(lib-target) $(extra-y)) $(subdir-ym) $(always)@:
可以看出目标__build 有 5 个依赖: builtin-target、 lib-target、 extra-y、 subdir-ym 和 always。这 5 个依赖的具体内容如下:
builtin-target =
lib-target =
extra-y =
subdir-ym =
always = scripts/basic/fixdep scripts/basic/bin2c
只有 always 有效,因此__build 最终为:
__build: scripts/basic/fixdep scripts/basic/bin2c@:
__build 依赖于 scripts/basic/fixdep 和 scripts/basic/bin2c,所以要先将 scripts/basic/fixdep 和scripts/basic/bin2c.c 这两个文件编译成 fixdep 和 bin2c。
@make -f ./scripts/Makefile.build obj=scripts/basic
综上所述, scripts_basic 目标的作用就是编译出 scripts/basic/fixdep 和 scripts/basic/bin2c 这两个软件。
%config 目标对应的命令
%config 目 标 对 应 的 命 令 为 :
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
此命令会使用到的各个变量值如下:
src= scripts/kconfig
kbuild-dir = ./scripts/kconfig
kbuild-file = ./scripts/kconfig/Makefile
include ./scripts/kconfig/Makefile
可以看出, Makefile.build 会读取 scripts/kconfig/Makefile 中的内容,
此文件有如下所示内容:
%_defconfig: $(obj)/conf$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@
$(Kconfig)
目标%_defconfig 与 xxx_defconfig 匹配,所以会执行这条规则,将其展开就是:
%_defconfig: scripts/kconfig/conf
@ scripts/kconfig/conf --defconfig=arch/arm/configs/%_defconfig Kconfig
_defconfig依赖scripts/kconfig/conf,所以会编译scripts/kconfig/conf.c生成conf 这个软件。
此软件就会将%_defconfig 中的配置输出到.config 文件中,最终生成 Linux kernel 根目录下的.config 文件。
我们下一讲内容再来说明make 过程、built-in.o 文件编译生成过程、make zImage 过程。