Linux内核学习之编译篇

Linux内核学习之编译篇

Linux内核发展至今,文件数已经接近6万,代码量相当巨大。这一方面是内核功能不断增强补充的原因,另一方面当然是Linux的兼容性考虑,导致整个工程非常浩荡,而且很多文件名都一样,处于不同目录而已,这样一来,读者在学习阅读时就容易困惑,到底那个文件才是我需要的呢?这就涉及到本篇要谈到的问题。当然本文不只是告诉你怎么找文件,更主要是谈谈编译方面的机制。

  • 了解Makefile

  • Kconfig概述

  • Linux Makefile


了解Makefile

关于Makefile的语法,基本规则很简单,就是告诉make程序你的工程结构,你所需要编译的目标,以及目标又依赖于什么文件生成。最简单的makfile只要有这几个基本要素就够了。比如Linux 根目录下的Makefile中的一个规则如下:

config: scripts_basic outputmakefile FORCE
    $(Q)$(MAKE) $(build)=scripts/kconfig $@

含义:config是编译目标,依赖于3个文件(或伪目标):scripts_basic outputmakefile FORCE,而接下来的一行以TAB键开始(必须如此),就是针对这个目标所要执行的命令,这个命令是Linux环境下可执行的程序或shell脚本。
更加具体的语法,我强烈推荐阅读陈皓的文章:跟我一起写Makefile


Kconfig概述

这又是什么东西?其实我很想直接就开始分析Linux Makefile的内容,但是Linux这个系统的配置比较复杂,又绕不开这个。本节主要简要介绍其作用,而具体的语法不打算详述。可以参考linux工程目录下的文档:Documentation\kbuild\kconfig-language.txt,英文偏弱的,就去度娘搜一些中文材料看看。学习时最好结合配置界面和Kconfig文件,对照着看。
我所用的版本为4.1.6,具体的版本可以用任意ftp软件连接ftp.kernel.org获取,或者直接登录kernel网页版

OK,我们随便打开一个Linux子目录的Makefile来大概了解一下,比如mm目录下的Makefile部分内容:

obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
obj-$(CONFIG_FRONTSWAP)    += frontswap.o
obj-$(CONFIG_ZSWAP)    += zswap.o
obj-$(CONFIG_HAS_DMA)  += dmapool.o
obj-$(CONFIG_HUGETLBFS)    += hugetlb.o

以上你会看到有很多CONFIG_ 开头的宏,这个哪里来的?别急,打开同一个目录下的Kconfig文件,截取部分如下:

config FRONTSWAP
    bool "Enable frontswap to cache swap pages if tmem is present"
    depends on SWAP
    default n
    help
      Frontswap is so named because it can be thought of as the opposite
      of a "backing" store for a swap device.  The data is stored into
      "transcendent memory", memory that is not directly accessible or
      addressable by the kernel and is of unknown and possibly
      time-varying size.  When space in transcendent memory is available,
      a significant swap I/O reduction may be achieved.  When none is
      available, all frontswap calls are reduced to a single pointer-
      compare-against-NULL resulting in a negligible performance hit
      and swap data is stored as normal on the matching swap device.

      If unsure, say Y to enable frontswap.

Now,你看到了”config FRONTSWAP“,这个配置项最终会构成”CONFIG_FRONTSWAP“,其值可以为n或y。这样就和前面提到的obj-$(CONFIG_FRONTSWAP)匹配上了,y表示obj-y指定的文件会最终编译进内核,n表示obj-n指定的文件不参与编译。看到这里,我希望你明白Kconfig里面的配置项和最终的宏的关系,也就是会自动添加上”CONFIG_“。

接下来,我们就应该要知道怎么来配置这些宏了,最一般的方式,我们通过在根目录下面执行如下命令来手动配置:
weimh1@weimh1-ubuntu:~/work/sourcecode/linux-4.1.6$ make menuconfig
这个编译目标(menuconfig)对应于根Makefile中的这条规则:

%config: scripts_basic outputmakefile FORCE
    $(Q)$(MAKE) $(build)=scripts/kconfig $@

上面编译命令最终又会进入scripts/kconfig 中的Makefile执行这个目标:

menuconfig: $(obj)/mconf
    $< $(silent) $(Kconfig)

之后,运行的结果如下图所示,请和前面的config FRONTSWAP 内容核对,就会大概明白其中一些含义:
首页面:
Linux内核学习之编译篇_第1张图片

mm页面FRONTSWAP配置:
Linux内核学习之编译篇_第2张图片

只是,因为kernel配置项繁多,要了解全部配置项基本是不可能,也不必要的。这种方式只是给你提供了一个入口,然后针对性修改相关项。通常,你可以基于某一个默认配置项文件进行配置,然后再通过这个menuconfig修改特定项。不同的架构有不同的默认文件,比如x86平台,可以在arch/x86/configs找到相关文件:i386_defconfig。通过执行make i386_defconfig 即可基于这个文件生成.config文件,在此基础上可以再运行make menuconfig 来进行个别的调整。更多的细节请参考:Documentation\kbuild\kconfig.txt。 总之,无论怎么配置,最终都是为了生成.config文件,这个文件的内容如下形式:

Linux内核学习之编译篇_第3张图片

这些宏最终将影响Makefile中参与编译的文件。正如你前面看到的obj-xxx类型的部分。


Linux Makefile

有了前面2个的基础,现在再来看Linux Makefile就相对明朗了些。本节开始,我们将开始真正进入内核的编译。当然,前面的config也是Makefile的一部分。Makefile的阅读,一般直接从头开始。

内核完整的版本号,在开头即写明:

VERSION = 4
PATCHLEVEL = 1
SUBLEVEL = 6
EXTRAVERSION =
NAME = Series 4800

忽略掉注释部分,继续往下看:
MAKEFLAGS += -rR --include-dir=$(CURDIR)
MAKEFLAGS是make内置的环境变量,这些参数的含义,可以通过man make 获得详细解释,以上”-rR“表示禁用内置的隐含规则和变量定义,”–include-dir” 指明嵌套脚本的搜索路径。

以下表示可以通过命令行参数make V=1 来输出完整的命令,便于跟踪。

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

接下来一部分,给你提供了 make O=out 的方式将编译输出的目标文件放在一个单独的目录,比如这里指定的out目录,这样可以更加清晰区分开源文件和目标文件。当然,你也可以不这么做。比如,如果不指定O参数,.config文件就在当前根目录生成,如果指定O=out,那么.config文件会放在out/.config

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)
$(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: FORCE
    $(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
    -f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))

# Leave processing to above invocation of make
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)

从上可以看出,当指定了O=out 时,将会进入out目录执行:
$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))

继续往下看:

ifeq ("$(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
endif
...
...
...
all: vmlinux
...
...
...
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
    $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_SAMPLES
    $(Q)$(MAKE) $(build)=samples
endif
ifdef CONFIG_BUILD_DOCSRC
    $(Q)$(MAKE) $(build)=Documentation
endif
ifdef CONFIG_GDB_SCRIPTS
    $(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
endif
    +$(call if_changed,link-vmlinux)

这部分允许你单独编译某个模块,用make M=dir 的形式。而且可以看到,如果不是编译模块,那么默认的目标是all,否则是modules。all又依赖与vmlinux,vmlinux又依赖于vmlinux-deps等,以此一层层完成编译,最终生成vmlinux映像。

下面几个宏也需要了解一下:

ARCH        ?= $(SUBARCH)
CROSS_COMPILE   ?= $(CONFIG_CROSS_COMPILE:"%"=%)
...
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
...

其中,ARCHCROSS_COMPILE 可以作为命令行参数传入,尤其是在编译嵌入式的Linux系统时,需要明确这2个宏。我这里仅仅举个例子,因为这个具体内容并不影响我们理解Makefile内容,不再赘述:
make ARCH=arm64 CROSS_COMPILE=/home/weimh1/work/k5_dev/prebuilts/gcc/linux-x86/aarch64/cit-aarch64-linux-android-4.9/bin/aarch64-linux-android-

下面2个宏也需要关注,看懂了这个,你就能够明白在C代码中#include xxxx 该怎么写相对路径:

USERINCLUDE    := \
        -I$(srctree)/arch/$(hdr-arch)/include/uapi \
        -Iarch/$(hdr-arch)/include/generated/uapi \
        -I$(srctree)/include/uapi \
        -Iinclude/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/$(hdr-arch)/include \
        -Iarch/$(hdr-arch)/include/generated/uapi \
        -Iarch/$(hdr-arch)/include/generated \
        $(if $(KBUILD_SRC), -I$(srctree)/include) \
        -Iinclude \
        $(USERINCLUDE)

接下来,将面对大量的目标和依赖关系,这些内容梳理清楚了,整个工程结构也就大概清楚了。
写到这里,发现如果要写得很详细,篇幅会很长,所以只能点到为止,权当入门参考。


几个重要的文件

接下来我们先转去介绍几个相关文件。

Kbuild.include

根Makefile中有下面这行:
include scripts/Kbuild.include
打开这个文件,里面有几个定义需要了解。
首先是build:

###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

它用在哪里?举个例子:
比如前面谈配置的时候提到的:

%config: scripts_basic outputmakefile FORCE
    $(Q)$(MAKE) $(build)=scripts/kconfig $@

把上面的build定义,展开后就很清晰了:

%config: scripts_basic outputmakefile FORCE
    $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=scripts/kconfig $@

可见,实际上build的作用是执行scripts/Makefile.build, obj指向具体的目录。于是,这里引出了第二个需要关注的文件Makefile.build,我们留到后面再简述。
继续看Kbuild.include的另外几个定义

# echo command.
# Short version is used, if $(quiet) equals `quiet_', otherwise full one.
echo-cmd = $(if $($(quiet)cmd_$(1)),\
    echo '  $(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)

# printing commands
cmd = @$(echo-cmd) $(cmd_$(1))
...
# 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)

请记住上面2个变量,很多地方用上,比如:

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)

展开后实际上最终执行了cmd_link_o_target。其他不再赘述,自己对照原文去理解。

Makefile.build

前文已经提到这个文件,这个文件主要定义了一些通用的模式匹配规则,比如:

 # Built-in and composite module parts
$(obj)/%.o: $(src)/%.c $(recordmcount_source) FORCE
    $(call cmd,force_checksrc)
    $(call if_changed_rule,cc_o_c)

上面这个规则定义了所有默认的c文件如何编译成.o目标文件。执行的函数为$(call if_changed_rule,cc_o_c),而if_changed_rule请在前文描述的Kbuild.include中查找。

其他规则类似,请自行阅读。
写到这里,如果你已经学会联系着这几个文件来阅读Makefile,我想你就已经入门了。至于要看到多深入,那纯粹是个人喜好问题,对于大多数人而言,其实大概了解也就够了。


OK,我们又回到根目录的总Makefile继续。剩下的部分,我们挑选几个来看看,不再从头到尾一个个描述。为了方便,我将几个重要的列在一块,中间省略的用... 表示。

init-y      := init/
drivers-y   := drivers/ sound/ firmware/
net-y       := net/
libs-y      := lib/
core-y      := usr/
...
core-y      += kernel/ mm/ fs/ ipc/ security/ crypto/ block/

vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
             $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
             $(net-y) $(net-m) $(libs-y) $(libs-m) )
...

init-y      := $(patsubst %/, %/built-in.o, $(init-y))
core-y      := $(patsubst %/, %/built-in.o, $(core-y))
drivers-y   := $(patsubst %/, %/built-in.o, $(drivers-y))
net-y       := $(patsubst %/, %/built-in.o, $(net-y))
libs-y1     := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2     := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y      := $(libs-y1) $(libs-y2)

# Externally visible symbols (used by link-vmlinux.sh)
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y)
export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
export LDFLAGS_vmlinux
# used by scripts/pacmage/Makefile
export KBUILD_ALLDIRS := $(sort $(filter-out arch/%,$(vmlinux-alldirs)) arch Documentation include samples scripts tools virt)

vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)

# Final link of vmlinux
      cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
quiet_cmd_link-vmlinux = LINK    $@

# Include targets which we want to
# execute if the rest of the kernel build went well.
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
    $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_SAMPLES
    $(Q)$(MAKE) $(build)=samples
endif
ifdef CONFIG_BUILD_DOCSRC
    $(Q)$(MAKE) $(build)=Documentation
endif
ifdef CONFIG_GDB_SCRIPTS
    $(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
endif
    +$(call if_changed,link-vmlinux)

以上内容主要描述了vmlinux是如何生成的,它依赖于vmlinux-deps,而vmlinux-deps又依赖于$(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)
这三个变量又是什么?
1. KBUILD_LDS :定义的是各个目标在最终映像文件中的布局;
2. KBUILD_VMLINUX_INIT: 除了init-y指定的内容,还有一个此文中没有出现过的$(head-y),这个文件和具体的架构相关,所以它在另外一个文件中定义,请看下面文件:
include arch/$(SRCARCH)/Makefile
如果你的SRCARCH 是x86,那么请打开文件arch/x86/Makefile,你可以找到下面内容:

head-y := arch/x86/kernel/head_$(BITS).o
head-y += arch/x86/kernel/head$(BITS).o
head-y += arch/x86/kernel/head.o

其中,BITS的值可以是32或者64位,取决你的机器实际配置。
head-y和init-y就构成了linux的启动和初始化代码,并被链接到最后的vmlinux中。
3.KBUILD_VMLINUX_MAIN : (corey) ( c o r e − y ) (libs-y) (driversy) ( d r i v e r s − y ) (net-y)
这个定义的则是整个内核的核心文件。编译的时候会依次进入这些目录,编译出对应的built-in.o 并最终链接到vmlinux中。

其他的诸多内容,最终都是为上面服务所引出的一系列规则。已经不再影响大局,不再赘述。


你可能感兴趣的:(编译器,linux内核)