前言
学习Linux内核最直接的方法就是阅读源码,但是Linux庞大的源代码却让很多初学者感到无从下手,而编译内核所用到的Makefile文件更是让初学者迷茫。我第一次阅读Makefile文件的感受是这是Makefile文件吗?甚至我已经怀疑自己,我会读写Makefile文件吗?
初学者首先应该学习make工具,并且能够熟练地读写简单的Makefile,当然系统全面地学习更好。只是,Makefile文件的规则很多,我们平时编译helloworld的时候根本无法涉及到较为复杂的规则。所以,更为有效的学习方法是,在掌握基本规则的前提下遇到某个规则的时候查阅资料,采用99 + 1的学习方法逐步掌握make工具及其Makefile文件规则。
本文不是专门讲述Makefile文件规则的文章,故无法通过本文学习Makefile文件规则。本文的初衷是讲述顶层Makefile与各层Makefile之间的关系,以此来让初学者明白内核映像文件是如何制作的。由于时间的关系,我会逐步完善本文。而因为本人也是个初学者,还往源码学习的老鸟们指正我的错误,以免让别人 学到不正确的内容而误入歧途。本文所使用的Linux源码版本是4.10,其他版本可能会有所不同,但变化不大。
本文第一次编辑于2017-3-24。
Makefile文件层次结构
首先我们在此说明一下,将Linux源码下载压缩后会产生linux-x.x(例如linux-4.10)的目录,在该目录下还有各个子模块源码的子目录。我们将这个linux-x.x目录称为顶层目录,而各个子目录称为模块子目录。在顶层目录中的Makefile称为顶层Makefile。
顶层Makefile中包含全局性质的变量和设置,子Makefile中包含各自的变量和设置。
顶层Makefile
版本及名称
在顶层Makefile最开始的地方定义了当前源码的版本及名称信息
VERSION = 4
PATCHLEVEL = 10
SUBLEVEL = 0
EXTRAVERSION =
NAME = Fearless Coyote
内置隐含规则和变量设置
因为make工具内置的隐含规则和变量设置可能会导致一些无法预料的结果,而使得调试变得更困难。因此,应该将他们禁用掉。
MAKEFLAGS += -rR --include-dir=$(CURDIR)
这一点很重要,这使得编译内核的整个过程都完全掌握在我们手中,规则完全由我们来制定。
修改C语言区域设置
unexport LC_ALL
LC_COLLATE=C
LC_NUMERIC=C
export LC_COLLATE LC_NUMERIC
避免shell环境设置的干扰
unexport GREP_OPTIONS
设置编译信息的输出模式
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
如果在命令行中对变量V赋值(例如:make V=1
),则输出full模式;否则,输出quiet模式。同时对变量KBUILD_VERBOSE赋值。
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
如果命令行中有 -s 的参数,则输出slient模式。
full模式:输出完整编译命令。
quiet模式:输出Compiling ***
的格式。
silent模式:输出任何信息。
设置输出目录
Kbuild支持将输出文件保存在单独的目录中,由两种方式可以实现:
- 在命令行中设置变量O,例如:
make O=dir/to/store/output/files/
- 设置变量KBUILD_OUTPUT的值,例如
export KBUILD_OUTPUT=dir/to/store/output/files/
变量O的优先级高于变量KBUILD_OUTPUT。
ifeq ($(KBUILD_SRC),)
ifeq ("$(origin O)", "command line")
KBUILD_OUTPUT := $(O)
endif
PHONY := _all
_all:
$(CURDIR)/Makefile Makefile: ;
ifneq ($(words $(subst :, ,$(CURDIR))), 1)
$(error main directory cannot contain spaces nor colons)
endif
ifneq ($(KBUILD_OUTPUT),)
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:
$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)
endif # ifeq ($(KBUILD_SRC),)
KBUILD_SRC变量是在调用make的时候设置。
如果设置了输出目录,则创建输出目录。
取消Enter/Leaving directory…的信息
MAKEFLAGS += --no-print-directory
设置是否进行对源代码的检查
对命令行参数变量C进行设置,可以选择使用sparse工具检查源代码。
- 如果C=1,则只对修改的源代码进行检查;
- 如果C=2,则对全部代码进行检查;
- 如果不设置C的值,则不进行检查。
ifeq ("$(origin C)", "command line")
KBUILD_CHECKSRC = $(C)
endif
ifndef KBUILD_CHECKSRC
KBUILD_CHECKSRC = 0
endif
编译外部模块
如果编译外部模块,则对命令行参数变量M进行赋值
ifdef SUBDIRS
KBUILD_EXTMOD ?= $(SUBDIRS)
endif
ifeq ("$(origin M)", "command line")
KBUILD_EXTMOD := $(M)
endif
PHONY += all
ifeq ($(KBUILD_EXTMOD),)
_all: all
else
_all: modules
endif
包含Makefile自定义函数文件Kbuild.include
include scripts/Kbuild.include
该条语句将文件 scripts/Kbuild.include 包含进顶层Makefile文件中,Kbuild.include 文件中定义了Makefile 文件用到的自定义函数。
设置目录变量
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)
获取编译环境平台
SUBARCH := $(shell uname -m | sed -e s/i.86/x86/ -e s/x86_64/x86/ \
-e s/sun4u/sparc64/ \
-e s/arm.*/arm/ -e s/sa110/arm/ \
-e s/s390x/s390/ -e s/parisc64/parisc/ \
-e s/ppc.*/powerpc/ -e s/mips.*/mips/ \
-e s/sh[234].*/sh/ -e s/aarch64.*/arm64/ )
设置目标平台
如果设置了变量ARCH,则即是设置了目标平台;否则,当前编译环境平台就是目标平台。
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?= $(CONFIG_CROSS_COMPILE:"%"=%)
UTS_MACHINE := $(ARCH)
SRCARCH := $(ARCH)
ifeq ($(ARCH),i386)
SRCARCH := x86
endif
ifeq ($(ARCH),x86_64)
SRCARCH := x86
endif
ifeq ($(ARCH),sparc32)
SRCARCH := sparc
endif
ifeq ($(ARCH),sparc64)
SRCARCH := sparc
endif
ifeq ($(ARCH),sh64)
SRCARCH := sh
endif
ifeq ($(ARCH),tilepro)
SRCARCH := tile
endif
ifeq ($(ARCH),tilegx)
SRCARCH := tile
endif
设置与平台相关的头文件的目录
hdr-arch := $(SRCARCH)
设置 configure 文件
KCONFIG_CONFIG ?= .config
export KCONFIG_CONFIG
设置调用bash的短名称
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
else if [ -x /bin/bash ]; then echo /bin/bash; \
else echo sh; fi ; fi)
设置针对编译环境的变量
HOSTCC = gcc
HOSTCXX = g++
HOSTCFLAGS = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89
HOSTCXXFLAGS = -O2
ifeq ($(shell $(HOSTCC) -v 2>&1 | grep -c "clang version"), 1)
HOSTCFLAGS += -Wno-unused-value -Wno-unused-parameter \
-Wno-missing-field-initializers -fno-delete-null-pointer-checks
endif
其中,如果编译器为clang,则添加其特有的编译参数。
判断是编译内核还是模块,还是两者均是
KBUILD_MODULES :=
KBUILD_BUILTIN := 1
ifeq ($(MAKECMDGOALS),modules)
KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
endif
ifneq ($(filter all _all modules,$(MAKECMDGOALS)),)
KBUILD_MODULES := 1
endif
ifeq ($(MAKECMDGOALS),)
KBUILD_MODULES := 1
endif
export KBUILD_MODULES KBUILD_BUILTIN
export KBUILD_CHECKSRC KBUILD_SRC KBUILD_EXTMOD
包含 scripts/Kbuild.include 文件
scripts/Kbuild.include: ;
include scripts/Kbuild.include
在此文件中定义了一些Makefile文件中用到的通用的自定义函数。
定义关于编译工具的变量
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
AWK = awk
GENKSYMS = scripts/genksyms/genksyms
INSTALLKERNEL := installkernel
DEPMOD = /sbin/depmod
PERL = perl
PYTHON = python
CHECK = sparse
CHECKFLAGS := -D__linux__ -Dlinux -D__STDC__ -Dunix -D__unix__ \
-Wbitwise -Wno-return-void $(CF)
NOSTDINC_FLAGS =
CFLAGS_MODULE =
AFLAGS_MODULE =
LDFLAGS_MODULE =
CFLAGS_KERNEL =
AFLAGS_KERNEL =
LDFLAGS_vmlinux =
CFLAGS_GCOV = -fprofile-arcs -ftest-coverage -fno-tree-loop-im -Wno-maybe-uninitialized
CFLAGS_KCOV := $(call cc-option,-fsanitize-coverage=trace-pc,)
设置用户态头文件目录
USERINCLUDE := \
-I$(srctree)/arch/$(hdr-arch)/include/uapi \
-I$(objtree)/arch/$(hdr-arch)/include/generated/uapi \
-I$(srctree)/include/uapi \
-I$(objtree)/include/generated/uapi \
-include $(srctree)/include/linux/kconfig.h
内核态头文件
LINUXINCLUDE := \
-I$(srctree)/arch/$(hdr-arch)/include \
-I$(objtree)/arch/$(hdr-arch)/include/generated/uapi \
-I$(objtree)/arch/$(hdr-arch)/include/generated \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-I$(objtree)/include
LINUXINCLUDE += $(filter-out $(LINUXINCLUDE),$(USERINCLUDE))
定义 KBuild 编译参数
KBUILD_CPPFLAGS := -D__KERNEL__
KBUILD_CFLAGS := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
-fno-strict-aliasing -fno-common \
-Werror-implicit-function-declaration \
-Wno-format-security \
-std=gnu89 $(call cc-option,-fno-PIE)
KBUILD_AFLAGS_KERNEL :=
KBUILD_CFLAGS_KERNEL :=
KBUILD_AFLAGS := -D__ASSEMBLY__ $(call cc-option,-fno-PIE)
KBUILD_AFLAGS_MODULE := -DMODULE
KBUILD_CFLAGS_MODULE := -DMODULE
KBUILD_LDFLAGS_MODULE := -T $(srctree)/scripts/module-common.lds
设置内核版本
KERNELRELEASE = $(shell cat include/config/kernel.release 2> /dev/null)
KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
编译模块时的目录
export MODVERDIR := $(if $(KBUILD_EXTMOD),$(firstword $(KBUILD_EXTMOD))/).tmp_versions
包含 include arch/$(SRCARCH)/Makefile 文件
include arch/$(SRCARCH)/Makefile
arch/x86/Makefile
确定 defconfig
ifeq ($(ARCH),x86)
ifeq ($(shell uname -m),x86_64)
KBUILD_DEFCONFIG := x86_64_defconfig
else
KBUILD_DEFCONFIG := i386_defconfig
endif
else
KBUILD_DEFCONFIG := $(ARCH)_defconfig
endif
确定16位实模式代码的编译参数
CODE16GCC_CFLAGS := -m32 -Wa,$(srctree)/arch/x86/boot/code16gcc.h
M16_CFLAGS := $(call cc-option, -m16, $(CODE16GCC_CFLAGS))
REALMODE_CFLAGS := $(M16_CFLAGS) -g -Os -D__KERNEL__ \
-DDISABLE_BRANCH_PROFILING \
-Wall -Wstrict-prototypes -march=i386 -mregparm=3 \
-fno-strict-aliasing -fomit-frame-pointer -fno-pic \
-mno-mmx -mno-sse \
$(call cc-option, -ffreestanding) \
$(call cc-option, -fno-stack-protector) \
$(call cc-option, -mpreferred-stack-boundary=2)
export REALMODE_CFLAGS
需要注意的是,gcc 4.9之后才支持-m16
参数,而 clang从3.5才支持。早期编译器,需要传递一个包含.code16gcc
的头文件。
生成系统调用表
archheaders:
$(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all
内核目标
head-y := arch/x86/kernel/head_$(BITS).o
head-y += arch/x86/kernel/head$(BITS).o
head-y += arch/x86/kernel/ebda.o
head-y += arch/x86/kernel/platform-quirks.o
libs-y += arch/x86/lib/
core-y += arch/x86/
drivers-$(CONFIG_MATH_EMULATION) += arch/x86/math-emu/
drivers-$(CONFIG_PCI) += arch/x86/pci/
drivers-$(CONFIG_OPROFILE) += arch/x86/oprofile/
drivers-$(CONFIG_PM) += arch/x86/power/
drivers-$(CONFIG_FB) += arch/x86/video/
drivers-$(CONFIG_RAS) += arch/x86/ras/
制作内核映像(未压缩及压缩)
boot := arch/x86/boot
BOOT_TARGETS = bzlilo bzdisk fdimage fdimage144 fdimage288 isoimage
all: bzImage
KBUILD_IMAGE := $(boot)/bzImage
bzImage: vmlinux
ifeq ($(CONFIG_X86_DECODER_SELFTEST),y)
$(Q)$(MAKE) $(build)=arch/x86/tools posttest
endif
$(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)
$(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot
$(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@
$(BOOT_TARGETS): vmlinux
$(Q)$(MAKE) $(build)=$(boot) $@
由$(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)
这一句可看出,make进入arch/x86/boot进行编译。
arch/x86/boot/Makefile
设置SVGA 模式
SVGA_MODE := -DSVGA_MODE=NORMAL_VGA
编译目标
targets := vmlinux.bin setup.bin setup.elf bzImage
targets += fdimage fdimage144 fdimage288 image.iso mtools.conf
subdir- := compressed
setup-y += a20.o bioscall.o cmdline.o copy.o cpu.o cpuflags.o cpucheck.o
setup-y += early_serial_console.o edd.o header.o main.o memory.o
setup-y += pm.o pmjump.o printf.o regs.o string.o tty.o video.o
setup-y += video-mode.o version.o
setup-$(CONFIG_X86_APM_BOOT) += apm.o
setup-y += video-vga.o
setup-y += video-vesa.o
setup-y += video-bios.o
targets += $(setup-y)
编译
KBUILD_CFLAGS := $(REALMODE_CFLAGS) -D_SETUP
KBUILD_AFLAGS := $(KBUILD_CFLAGS) -D__ASSEMBLY__
GCOV_PROFILE := n
UBSAN_SANITIZE := n
$(obj)/bzImage: asflags-y := $(SVGA_MODE)
quiet_cmd_image = BUILD $@
cmd_image = $(obj)/tools/build $(obj)/setup.bin $(obj)/vmlinux.bin \
$(obj)/zoffset.h $@
$(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE
$(call if_changed,image)
@echo 'Kernel: $@ is ready' ' (#'`cat .version`')'
OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
SETUP_OBJS = $(addprefix $(obj)/,$(setup-y))
sed-zoffset := -e 's/^\([0-9a-fA-F]*\) [ABCDGRSTVW] \(startup_32\|startup_64\|efi32_stub_entry\|efi64_stub_entry\|efi_pe_entry\|input_data\|_end\|_ehead\|_text\|z_.*\)$$/\#define ZO_\2 0x\1/p'
quiet_cmd_zoffset = ZOFFSET $@
cmd_zoffset = $(NM) $< | sed -n $(sed-zoffset) > $@
targets += zoffset.h
$(obj)/zoffset.h: $(obj)/compressed/vmlinux FORCE
$(call if_changed,zoffset)
AFLAGS_header.o += -I$(objtree)/$(obj)
$(obj)/header.o: $(obj)/zoffset.h
LDFLAGS_setup.elf := -T
$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE
$(call if_changed,ld)
OBJCOPYFLAGS_setup.bin := -O binary
$(obj)/setup.bin: $(obj)/setup.elf FORCE
$(call if_changed,objcopy)
$(obj)/compressed/vmlinux: FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@
镜像默认命令行参数
image_cmdline = default linux $(FDARGS) $(if $(FDINITRD),initrd=initrd.img,)