目錄
前言
頂層 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 過程。