Linux makefile分析
阅读数:246 评论数:0
跳转到新版页面分类
Linux
正文
一、概述
Linux内核源码中含有很多个Makefile文件,这些Makefile文件又要包含其他一些文件(比如配置信息、通用规则 等)。这些文件构成了Linux的Makefile体系。
名称 | 描述 |
顶层Makefile | 它是所有Makefile文件的核心,从总体上控制着内核的编译、连接 |
.config | 配置文件,在配置内核时生成。所有Makefile(包括顶层目录及各级子目录)都是根据.config来决定使用哪些文件 |
arc/${ARCH}/Makefile | 对应体系结构的Makefile,它用来决定哪些体系结构相关的文件参与内核的生成,并提供一些规则来生成特定格式的内核映像 |
scripts/Makefile.* | Makefile共用的通用规则、脚本等 |
kbuild Makefiles | 各级子目录下的Makefile,它们相对简单,被上一层Makefile调用来编译当前目录下的文件 |
内核文档Documentation/kbuild/makefiles.txt对内核中Makefile的作用、用法讲解得非常透彻。
二、决定编译哪些文件
Linux内核的编译过程从顶层Makefile开始,然后递归地进入各级子目录调用它们的Makefile,分为3个步骤:
(1)顶层Makefile决定内核根目录哪些子目录将被编译进内核。
(2)arch/${ARCH}/Makefile决定arch/${ARCH}目录下哪些文件、哪些目录被编译进内核。
(3)各级子目录的Makefile决定所在目录下哪些文件将被 编进内核,哪些文件将被编成模块(即驱动程序 ),进入哪些子目录继续调用它们的Makefile。
先来看步骤(1),在顶层Makefile中可以看到如下内容:
init-y := init/
drivers-y := drivers/ sound/ firmware/
net-y := net/
libs-y := lib/
core-y := usr/
virt-y := virt/
...
core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
除了include目录,还有一个arch目录没有出现在内核中,它在arc/${ARCH}/Makefile中被包含进内核,在顶层Makefile中直接包含了这个Makefile,如下所示:
SRCARCH := $(ARCH)
...
include arch/$(SRCARCH)/Makefile
对于ARCH变量,可以在执行make命令时传入,比如make ARCH=arm ...。另外,对于非x86平台,还需要指定交叉编译工具,这也可以在执行make命令进传入,比如make CROSS_CoMPILE=arm-linux-...。为了方便,常在顶层Makefile中进行如下修改
#修改前
ARCH ?=$(SUBARCH)
CROSS_COMPILE ?=$(CONFIG_CROSS_COMPILE:"%"=%)
#修改后
ARCH ?= arm
CROSS_COMPILE ?=arm-linux-
对于步骤(2)的arch/$(ARCH)/Makefile,以ARM体系为例,在arch/arm/Makefile中可以看到以下内容:
head-y := arch/arm/kernel/head$(MMUEXT).o
...
core-$(CONFIG_FPE_NWFPE) += arch/arm/nwfpe/
core-$(CONFIG_FPE_FASTFPE) += $(FASTFPE_OBJ)
core-$(CONFIG_VFP) += arch/arm/vfp/
core-$(CONFIG_XEN) += arch/arm/xen/
core-$(CONFIG_KVM_ARM_HOST) += arch/arm/kvm/
core-$(CONFIG_VDSO) += arch/arm/vdso/
...
core-y += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
core-y += arch/arm/probes/
core-y += arch/arm/net/
core-y += arch/arm/crypto/
core-y += arch/arm/firmware/
core-y += $(machdirs) $(platdirs)
...
libs-y := arch/arm/lib/ $(libs-y)
MMUEXT在arch/arm/Makefile前面定义,对于没有MMU的处理器,MMUEXT的值为-nommu,使用文件 head-nommu.S,对于有MMU的处理器,MMUEXT的值为空,使用文件head.S。
core-y、libs-y的内容,这些都是体系结构相关的目录。
编译内核时,将依次进入init-y、core-y,drivers-y和net-y所列出的目录中执行它们的Makefile,每个子目录会生成一个built-in.o(libs-y所列目录下,有可能生成lib.a文件),最后,head-y所表示的文件将和这些build-in.o、lib.a一起被连接成内核映像文件vmlinux。
最后,看一下步骤(3)是怎么进行的。
在配置内核时,生成配置文件.config,内核顶层Makefile使用如下语句间接包含.config文件,以后根据.config中定义的各个变量决定编译哪些文件。之所以说是间接包含,是因为包含的include/config/auto.conf文件,而它只是将.config文件中的注释去掉,并根据顶层的Makefile中定义的变量增加了一些变量而已。
# Read in config
-include include/config/auto.conf
在include/config/auto.conf文件中,变量的值主要有两类:“y”、“m”。各级子目录的Makefiel使用这些变量来决定哪些文件被编译进内核,哪些文件被编译成模块,要进入哪些下级子目录继续编译。
(1)obj-y用来定义哪些文件被编进内核
obj-y中定义的.o文件由当前目录下的.c或.S文件编译生成,它们连同下级子目录的built-in.o文件一起被组合成(使用$(LD) -r)当前目录下的built-in.o文件。这个built-in.o文件将被它的上一层Makefile使用。
obj-y中各个.o文件的顺序是有意义的,因为内核中使用module_init()或__initcall定义的函数将按照它们的连接顺序被调用。
例:
obj-$(CONFIG_ISDN) += isdn.o
obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o
当上面的CONFIG_ISDN、CONFIG_ISDN_PPP_BSDCOMP在.config中被定义为y时isdn.c或isdn.S、isdn_bsdcomp.c或isdn_bsdcomp.S被编译成isdn.o、isdn_bsdcomp.o。这两个.o文件被组合进built-in.o文件中,最后连接进入内核。假如isdn.o isdn_bsdcomp.o中分别用module_init(A) module_init(B)定义了函数A、B,则内核启动时A先被调用、然后是B。
(2)obj-m用来定义哪些文件被编译成可加载的模块
obj-m中定义的.o文件由当前目录下的.c或.S文件编译生成,它们不会被编进build-in.o中,而是被编成可加载模块。
一个模块可以由一个或几个.o文件组成。对于只有一个源文件的模块,在obj-m中直接增加它的.o文件即可。对于有多个源文件的模块,除在obj-m中增加一个.o文件外,还要定义一个<module_name>-objs变量来告诉Makefil这个.o文件由哪些文件组成。
例1:
#当CONFIG_ISDN_PPP_BSDCOMP在.config文件中被定义为m时,isdn_bsdcomp.c或isdn_bsdcomp.S
#将被编译成isdn_bsdcomp.o文件
obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o
例2
#当下面的CONFIG_ISDN在.config文件中被定义为m时,将会生成一个isdn.o文件,
#它由isdn-objs中定义的isdn_net_lib.o isdn_v110.o isdn_common.o等
#3个文件组合
obj-$(CONFIG_ISDN) += isdn.o
isdn-objs := isdn_net_lib.o isdn_v110.o isdn_common.o
(3)lib-y用来定义哪些文件被编译成库文件
lib-y中定义的.o文件由当前目录下的.c或.S文件编译生成,它们被打包成当前目录下的一个库文件:lib.a。
同时出现在obj-y、lib-y中的.o文件,不会被包含进lib.a中。
要把这个lib.a编进内核中,需要在顶层Makefile中libs-y变量中列出当前目录。要编进库文件的内核代码一般都在这两个目录下:lib/ 、arch/$(ARCH)/lib
(4)obj-y、obj-m还可以用来指定要进入的下一层子目录
Linux中一个Makefile文件只负责生成当前目录下的目标文件,子目录下的目标文件由子目录的Makefile生成。Linux的编译系统会自动进入这些子目录调用它们的Makefile,只是在这之前指定这些子目录。
这要用到obj-y、obj-m,只要在其中增加这些子目录名即可。
例子:
#fs/Makefile中有如下一行
obj-$(CONFIG_JFFS2_FS) += jffs2/
三、怎么编译这些文件
即编译选项、连接选项是什么。这些选项分为3类:全局的,适用于整个内核代码;局部的,仅适用于某个Makefile中的所有文件;个体的,仅适用于某个文件。
全局选项在顶层Makefile和arch/$(ARCH)/Makefile中定义,这些选项的名称为CFLAG、AFLAG、LDFLAG、ARFLAG,它们分别是编译C文件的选项、编译汇编文件的选项、连接文件的选项、制作库文件的选项。
需要使用局部选项时,它们在各个子目录中定义,名称为:EXTRA_CFLAGS、EXTRA_AFLAGS、EXTRA_LDFLAGS、EXTRA_ARFLAGS,它们的用途与前述选项相同,只是作用范围比较小,它们针对当前Makefile中的所有文件。
另外,如果想针对某个文件定义它的编译选项,可以使用CFLAG_$@, AFLAGS_$@,前者用于编译某个C文件,后者用编译某个汇编文件。$@表示某个文件名,比如
CFLAGS_aha152x.o = -DAHA152X_STAT -DAUTOCONF
需要注意的是,这3类选项是一起使用的。
四、怎么连接这些文件,它们的顺序如何
前面分析有哪些文件要编译进内核,顶层的Makefile和arch/$(ARCH)/Makefile定义几类目录(或文件):head-y, init-y, drivers-y, net-y, libs-y, core-y。除head-y外,其余的都是目录名。这些目录的名面直接加上了built-in.a或lib.a,表示要链接进内核的文件
# 以init-y为例,其实就是变成了init-y/built-in.a
init-y := $(patsubst %/, %/built-in.a, $(init-y))
core-y := $(patsubst %/, %/built-in.a, $(core-y))
drivers-y := $(patsubst %/, %/built-in.a, $(drivers-y))
net-y := $(patsubst %/, %/built-in.a, $(net-y))
libs-y1 := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2 := $(patsubst %/, %/built-in.a, $(filter-out %.a, $(libs-y)))
virt-y := $(patsubst %/, %/built-in.a, $(virt-y))
顶层Makefile再往下看
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y)
export KBUILD_VMLINUX_LIBS := $(libs-y1)
export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds
对于ARM体系,连接脚本就是arch/$(ARM)/kernel/vmlinux.lds,它由arch/arm/kernel/vmlinux.lds.S文件生成,规则在scripts/Makefile.build中
$(obj)/%.lds: $(src)/%.lds.S FORCE
$(call if_changed_dep,cpp_lds_S)
顶层makefile
先是定义了内核的版本,接下来会自到很ifeq条件判断语句,它们负责检查传递给make的参数。内核的Makefile提供了一个特殊的编译选项make help,这个选项可以生成所有的可用目标和一些能传给make的有效命令行参数。例如,make V=1会在构建过程中输出详细的编译信息,第一个ifeq就是检查传递给make的V=n选项。
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
export quiet Q KBUILD_VERBOSE
如果V=n这个选项传给了make,系统就会给变量KBUILD_VERBOSE选项附上V的值,否则KBUILD_VERSION就会为0。然后根据KBUILD_VERBOSE的值来决定quiet和Q的值,符号@控制命令的输出,如果它被放在一个命令之前,这条命令的输出将会是CC scripts/mod/empty.o,而不是Compiling...scripts/mod/empty.o,在这段最后,导出了所有的变量。
下一个ifeq语句检查的是传递给make的选项O=/dir,这个选项允许在指定的目录dir输出所有的结果文件。
ifeq ($(KBUILD_SRC),)
ifeq ("$(origin O)", "command line")
KBUILD_OUTPUT := $(O)
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)"))
sub-make: FORCE
$(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,它代表内核代码的顶层目录,它代表内核代码的顶层目录,如果它是空的(第一次执行makefile时总是空的),我们会设置变量KBULD_OUTPUT为传递选项O的值(如果这个选项项被传递进来了),下一步会检查变量KBUILD_OUTPUT,如果已经设置好那么接下来会做以下几件事:
(1)将变量KBUILD_OUTPUT的值保存到临时变量saved-output
(2)尝试创建给定的输出目录
(3)检查 创建的输出目录,如果失败了就打印错误
(4)如果成功创建了输出目录,那么就在新目录重新执行make命令
下一个ifeq语句检查传递给make的选项C和M
ifeq ("$(origin C)", "command line")
KBUILD_CHECKSRC = $(C)
endif
ifndef KBUILD_CHECKSRC
KBUILD_CHECKSRC = 0
endif
ifeq ("$(origin M)", "command line")
KBUILD_EXTMOD := $(M)
endif
第一个选项C会告诉makefile需要使用环境变量$CHECK提供的工具来检查全部C代码,默认情况下使用sparse。第二个选项M会用来编译外部模块。
系统还会检查变量KBUILD_SRC,如果KBUILD_SRC没有被设置,系统会设置变量srctree为当前目录。
ifeq ($(KBUILD_SRC),)
srctree := .
endif
objtree := .
src := $(srctree)
obj := $(objtree)
export srctree objtree VPATH
这将告诉Makefile内核的源码树就在执行make命令的目录,并且将这些变量输出。
下一步操作是引入下面的文件
include scripts/Kbuild.include
文件Kbuild或者又叫做Kernel Build System是一个用来管理构建内核及模块的特殊框架。kbuild文件的语法与makefile一样。文件scripts/Kbuild.include为kbuild系统提供了一些常规的定义。
接着就是要获取SUBARCH的值,这个变量代表了当前系统架构(一般指CPU架构)
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/ )
执行uname得到机器、操作系统和架构的信息。因为我们得到的是uname的输出,所以我们需要做一些处理再赋给变量SUBARCH。获得SUBARCH之后就要设置SRCARCH,SRCARCH提供了硬件架构相关的代码的目录。
ifeq ($(ARCH),i386)
SRCARCH := x86
endif
ifeq ($(ARCH),x86_64)
SRCARCH := x86
endif
如果没有设置过代表内核配置文件路径的变量KCONFIG_CONFIG,下一步系统会设置它默认情况下就是.config.
KCONFIG_CONFIG ?= .config
export KCONFIG_CONFIG
以及编译内核过程中要用到的shell
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
else if [ -x /bin/bash ]; then echo /bin/bash; \
else echo sh; fi ; fi)
接下来就要设置一组和编译内核的编译器相关的变量。我们会设置主机的C和C++的编译器及相关配置项。
HOSTCC = gcc
HOSTCXX = g++
HOSTCFLAGS = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89
HOSTCXXFLAGS = -O2
接着又定义了两个变量: USERINCLUDE和LINUXINCLUDE,它们包含了头文件的路径(第一个是组用户用的,第二个是给内核用的)
# Use USERINCLUDE when you must reference the UAPI directories only.
USERINCLUDE := \
-I$(srctree)/arch/$(SRCARCH)/include/uapi \
-I$(objtree)/arch/$(SRCARCH)/include/generated/uapi \
-I$(srctree)/include/uapi \
-I$(objtree)/include/generated/uapi \
-include $(srctree)/include/linux/kconfig.h
# Use LINUXINCLUDE when you must reference the include/ directory.
# Needed to be compatible with the O= option
LINUXINCLUDE := \
-I$(srctree)/arch/$(SRCARCH)/include \
-I$(objtree)/arch/$(SRCARCH)/include/generated \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-I$(objtree)/include \
$(USERINCLUDE)
以及C编译器的标准标志
KBUILD_CFLAGS := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
-fno-strict-aliasing -fno-common \
-Werror-implicit-function-declaration \
-Wno-format-security \
-std=gnu89
这并不是最终确定的编译器标志,它们还可以在其他 makefile 里面更新(比如 arch/
里面的 kbuild)。变量定义完之后,全部会被导出供其他 makefile 使用。
下面的两个变量RCS_FIND_IGNORE和RCS_TAR_IGNORE包含了被版本控制系统忽略的文件 。
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
再往后,我们就就看到变量KBUILD_MODULES和KBUILD_BUILTIN的定义,这两上变量决定了我们要编译会么东西(内核、模块或者两者)
KBUILD_MODULES :=
KBUILD_BUILTIN := 1
ifeq ($(MAKECMDGOALS),modules)
KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
endif
如果仅仅传递了modules给make,变量KBUILD_BUILTIN会依赖于内核配置选项CONFIG_MODVERSIONS。
直接到
all: vmlinux
include arch/$(SRCARCH)/Makefile
从export RCS_FIND_IGNORE到all:vmlinux这一部分的makefile代码,主要负责根据各种配置文件 生成不同目标内核。
目标all是在命令行如果不指定具体目标时默认使用的目标。目标all依赖于根makefile后面声明的vmlinux
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
vmlinux是linux内核的静态链接可执行文件格式。脚本scripts/link-vmlinux.sh把不同的编译好的子模块链接到一起形成了vmlinux。
第二个目标是vmlinux-deps,它的定义如下
vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)
它是由内核代码下的每个每个顶级目录的built-in.o组成。kbuild会编译各个目录下的所$(obj-y)对应的源文件,然后调用$(LD) -r把这些文件合并成一个build-in.o文件里。
下一个可被执行的目标 如下
$(sort $(vmlinux-deps)): $(vmlinux-dirs) ;
$(vmlinux-dirs): prepare scripts
$(Q)$(MAKE) $(build)=$@
vmlinux-dir依赖于两个部分:prepare和scripts。第一个prepare定义在内核的根makefile中
prepare: prepare0
prepare0: archprepare FORCE
$(Q)$(MAKE) $(build)=.
archprepare: archheaders archscripts prepare1 scripts_basic
prepare1: prepare2 $(version_h) include/generated/utsrelease.h \
include/config/auto.conf
$(cmd_crmodverdir)
prepare2: prepare3 outputmakefile asm-generic
第一个prepare0展开到archprepare,后者又展开到archheader和archscripts,这两个变量定义在x86_64相关的Makefile。X86_64特定的makefile从变量定义开始,这些变量都是和特定架构的配置(defconfig等)有关联。
第一个目标是makefile生成的系统调用列表中的archheaders
archheaders:
$(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all
第二个目标是makefile里的archscripts
archscripts: scripts_basic
$(Q)$(MAKE) $(build)=arch/x86/tools relocs
可以看到archscripts是依赖于根Makefile里的scripts_basic。而scripts_basic是按照scripts/basic的makefile执行make的
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
script/basic/Makefile包含了编译两个主机程序fixdep和bin2的目标
hostprogs-y := fixdep
hostprogs-$(CONFIG_BUILD_BIN2C) += bin2c
always := $(hostprogs-y)
$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep
第一个工具是fixdep,用来优化gcc生成的依赖列表,然后重新编译源文件的时候告诉make。第二个工具是bin2c,它依赖于内核配置选项CONFIG_BUILD_BIN2C,它是一个用来将标准输入stdin收到的二进制流通过标准输出接口stdout转换成C头文件的非常小的C程序。hostprog-y告诉kbuild这里有个名为fixed的程序,这个程序会通过和Makefile相同目录 的fixdep.c编译而来。
当目标scripts_basic被执行,目标archscripts就会make arch/x86/tools下的makefile和目标relocs,包含了重定位的信息的代码relocs_32.c和relocs_64.c将会被编译。
在编译完relocs.c之后会检查version.h
$(version_h): $(srctree)/Makefile FORCE
$(call filechk,version.h)
$(Q)rm -f $(old_version_h)
在内核的根Makefile使用arch/x86/include/generated/asm的目标asm-generic来构建generic汇编头文件。这样archprepare就完成了,prepare0会接着被执行