zoukankan      html  css  js  c++  java
  • Linux内核剖析(五)Linux内核的构建过程

    参考
    一次实验引发的故事 – kernel build system探索—vmlinux是如何炼成的– kernel makefile

    深度探索Linux操作系统:系统构建和原理解析.pdf

    问题


    在前面的博文中,我们先是为自己的Ubuntu安装了一套内核源码树,然后为了方便进行嵌入式交叉编译,我们又为arm板子构建了一套源码树。
    那么现在我们已经知道如何自己的电脑上去构建、安装一个定制化的Linux内核,但是我们还是要在唠叨一些。
    当你在内核源码路径里敲下make时究竟发生什么
    这里写图片描述

    当我们刚刚开始接触内核代码时,毫无头绪,这时候Makefile是往往是我们打开的第一个文件,这个makefile是Linux内核代码的根makefile,内核构建就始于此处。是的,它的内容很多,但是如果你已经读过内核源代码,你就会发现每个包含代码的目录都有一个自己的Makefile。当然了,我们不会去描述每个代码文件是怎么编译链接的,所以我们将只会挑选一些通用的例子来说明问题。而你不会在这里找到构建内核的文档、如何整洁内核代码、tags的生成和交叉编译相关的说明,等等。

    我们仅仅将从make开始,使用标准的内核配置文件,一直到生成了内核镜像bzImage或者zImage结束。
    当然在着之前我们需要了解,我们make究竟是要构建一个什么样的目标,我想这个

    构建的目标vmlinux,vmlinuz,bzImage,zImage


    对于Linux内核,编译可以生成不同格式的映像文件,例如:

    make zImag
    make uImage

     

    zImage是ARM Linux常用的一种压缩映像文件,uImage是U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的“头”,说明这个映像文件的类型、加载位置、生成时间、大小等信息。换句话说,如果直接从uImage的0x40位置开始执行,zImage和uImage没有任何区别。另外,Linux2.4内核不支持uImage,Linux2.6内核加入了很多对嵌入式系统的支持,但是uImage的生成也需要设置。

    几种linux内核文件的区别:

    1、vmlinux 编译出来的最原始的内核文件,未压缩。
    2、zImage 是vmlinux经过gzip压缩后的文件。适用于小内核
    3、bzImage bz表示“big zImage”,不是用bzip2压缩的。两者的不同之处在于,zImage解压缩内核到低端内存(第一个640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么采用zImage或bzImage都行,如果比较大应该用bzImage。适用于大内核
    4、uImage U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的tag。
    5、vmlinuz 是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接。
    6、initrd 是“initial ramdisk”的简写。一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。

    vmlinux


    vmlinux是未压缩的内核,是make工作编译出的原始内核,vmlinuz是vmlinux的压缩文件。

    vmlinux 是ELF文件,即编译出来的最原始的文件。

    zImage, bzImage和vmlinuz


    vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制。Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。vmlinuz是可执行的Linux内核,它位于/boot/vmlinuz,它一般是一个软链接,是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接。

    vmlinuz的建立有两种方式。

    一是编译内核时通过“make zImage”创建,然后通过:“cp /usr/src/linux-2.4/arch/i386/linux/boot/zImage /boot/vmlinuz”产生。zImage适用于小内核的情况,它的存在是为了向后的兼容性。

    二是内核编译时通过命令make bzImage创建,然后通过:“cp /usr/src/linux-2.4/arch/i386/linux/boot/bzImage /boot/vmlinuz”产生。bzImage是压缩的内核映像,需要注意,bzImage不是用bzip2压缩的,bzImage中的bz容易引起误解,bz表示“big zImage”。 bzImage中的b是“big”意思。

    zImage(vmlinuz)和bzImage(vmlinuz)都是用gzip压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。所以你不能用gunzip 或 gzip –dc解包vmlinuz。

    内核文件中包含一个微型的gzip用于解压缩内核并引导它。两者的不同之处在于,老的zImage解压缩内核到低端内存(第一个640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么可以采用zImage 或bzImage之一,两种方式引导的系统运行时是相同的。大的内核采用bzImage,不能采用zImage。

    但是注意通常情况下是不能用vmlinuz解压缩得到vmlinux的

    initrd-x.x.x.img


    initrd是“initial ramdisk”的简写。initrd一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。

    initrd 映象文件是使用mkinitrd创建的。mkinitrd实用程序能够创建initrd映象文件。这个命令是RedHat专有的。其它Linux发行版或许有相应的命令。这是个很方便的实用程序。具体情况请看帮助:man mkinitrd下面的命令创建initrd映象文件。

    最后生成的内核镜象有两种 zImage 以及 uImage 。其中 zImage 下载到目标板中后,可以直接用 uboot 的命令 go 来进行直接跳转。这时候内核直接解压启动。但是无法挂载文件系统,因为 go 命令没有将内核需要的相关的启动参数传递给内核。传递启动参数我们必须使用命令 bootm 来进行跳转。 Bootm 命令跳转只处理 uImage 的镜象。

    uboot 源代码的 tools/ 目录下有 mkimage 工具,这个工具可以用来制作不压缩或者压缩的多种可启动映象文件。

    mkimage 在制作映象文件的时候,是在原来的可执行映象文件的前面加上一个 0x40 字节的头,记录参数所指定的信息,这样 uboot 才能识别这个映象是针对哪个 CPU 体系结构的,哪个 OS 的,哪种类型,加载内存中的哪个位置, 入口点在内存的那个位置以及映象名是什么

    uImage文件


      vmlinux是内核文件,zImage是一般情况下默认的压缩内核映像文件,压缩vmlinux,加上一段解压启动代码得到。而uImage 则是使用工具mkimage对普通的压缩内核映像文件(zImage)加工而得。它是uboot专用的映像文件,它是在zImage之前加上一个长度为 64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。
    其实就是一个自动跟手动的区别,有了uImage头部的描述,u-boot就知道对应Image的信息,如果没有头部则需要自己手动去搞那些参数。
    如何生成 uImage文件?首先在uboot的/tools目录下寻找mkimage文件,把其copy到系统/usr/local/bin目录下,这样就完成制 作工具。然后在内核目录下运行make uImage,如果成功,便可以在arch/arm/boot/目录下发现uImage文件,其大小比 zImage多64个字节。
    此外,平时调试用uImage,不用去管调整了哪些东西;zImage则是一切OK后直接烧0X0。开机就运行

    编译内核前的准备


    在开始编译前要进行很多准备工作。最主要的就是找到并配置好配置文件,make命令要使用到的参数都需要从这些配置文件获取。现在就让我们深入内核的根makefile吧

    内核版本设定


    内核的根Makefile负责构建两个主要的文件:vmlinux(内核镜像可执行文件)和模块文件moudles。
    我们先看看内核的Makefile开始的几行head -n 6 Makefile

    VERSION = 4
    PATCHLEVEL = 2
    SUBLEVEL = 3
    EXTRAVERSION =
    NAME = Hurr durr I'ma sheep
    

     

    这里写图片描述

    Makefile在开始的时候定义了几个变量,这些变量决定了当前内核的版本,并且被使用在很多不同的地方,比如同一个Makefile中的KERNELVERSION
    这里写图片描述

    关于版本号

    Linux内核有三个不同的命名方案。

    参见 https://zh.wikipedia.org/wiki/Linux%E5%86%85%E6%A0%B8

    早期版本
      第一种方式用于1.0版本之前(包括1.0)。第一个版本的内核是0.01。其次是0.02,0.03,0.10,0.11,0.12(第一GPL版本),0.95,0.96,0.97,0.98,0.99及1.0。从0.95版有许多的补丁发布于主要版本版本之间。

    旧计划
      第二种方式用于1.0之后到2.6,版本的格式为A.B.C,其中A,B,C代表:

      A大幅度转变的内核。这是很少发生变化,只有当发生重大变化的代码和核心发生才会发生。在历史上曾改变两次的内核:1994年的1.0及1996年的2.0。

      B是指一些重大修改的内核。内核使用了传统的奇数次要版本号码的软件号码系统(用偶数的次要版本号码来表示稳定版本)。

      C是指轻微修订的内核。这个数字当有安全补丁,bug修复,新的功能或驱动程序,内核便会有变化。
      这样稳定版本来源于上一个测试版升级版本号,而一个稳定版本发展到完全成熟后就不再发展。

      自2.6.0(2003年12月)发布后,人们认识到,更短的发布周期将是有益的。自那时起,版本的格式为A.B.C.D,其中A,B,C,D代表:

      A和B是无关紧要的
      C是内核的版本

    新计划
      自3.0(2011年7月)发布后,版本的格式为3.A.B,其中A,B代表:

      A是内核的版本
      B是安全补丁

      使用一种“time-based”的方式。3.0版本之前,是一种“A.B.C.D”的格式。七年里,前两个数字A.B即“2.6”保持不变,C随着新版本的发布而增加,D代表一些bug修复,安全更新,添加新特性和驱动的次数。
      3.0版本之后是“A.B.C”格式,B随着新版本的发布而增加, C代表一些bug修复,安全更新,新特性和驱动的次数。第三种方式中不再使用偶数代表稳定版,奇数代表开发版这样的命名方式。举个例子:3.7.0代表的不是开发版,而是稳定版!

      而4.0(2015年4月)发布后,则延续3.A.B的命名格式,只是将主版号变更为4。

    make参数传递


      接下来我们会看到很多ifeq条件判断语句,它们负责检查传递给make的参数。内核的Makefile提供了一个特殊的编译选项makehelp,这个选项可以生成所有的可用目标和一些能传给make的有效的命令行参数。
      举个例子,首先出现的就是-V ,那么make V=1会在构建过程中输出详细的编译信息,第一个ifeq就是检查传递给make的V=n选项。

    参数-v在控制构建过程中输出编译信息

    使用cat -n Makefile | head -n 83 | tail -n +23 查看

    # Avoid interference with shell env settings
    unexport GREP_OPTIONS
    
    # We are using a recursive build, so we need to do a little thinking
    # to get the ordering right.
    #
    # Most importantly: sub-Makefiles should only ever modify files in
    # their own directory. If in some directory we have a dependency on
    # a file in another dir (which doesn't happen often, but it's often
    # unavoidable when linking the built-in.o targets which finally
    # turn into vmlinux), we will call a sub make in that other dir, and
    # after that we are sure that everything which is in that other dir
    # is now up to date.
    #
    # The only cases where we need to modify files which have global
    # effects are thus separated out and done before the recursive
    # descending is started. They are now explicitly listed as the
    # prepare rule.
    
    # Beautify output
    # ---------------------------------------------------------------------------
    #
    # Normally, we echo the whole command before executing it. By making
    # that echo $($(quiet)$(cmd)), we now have the possibility to set
    # $(quiet) to choose other forms of output instead, e.g.
    #
    #         quiet_cmd_cc_o_c = Compiling $(RELDIR)/$@
    #         cmd_cc_o_c       = $(CC) $(c_flags) -c -o $@ $<
    #
    # If $(quiet) is empty, the whole command will be printed.
    # If it is set to "quiet_", only the short version will be printed.
    # If it is set to "silent_", nothing will be printed at all, since
    # the variable $(silent_cmd_cc_o_c) doesn't exist.
    #
    # A simple variant is to prefix commands with $(Q) - that's useful
    # for commands that shall be hidden in non-verbose mode.
    #
    #   $(Q)ln $@ :<
    #
    # If KBUILD_VERBOSE equals 0 then the above command will be hidden.
    # If KBUILD_VERBOSE equals 1 then the above command is displayed.
    #
    # To put more focus on warnings, be less verbose as default
    # Use 'make V=1' to see the full commands
    
    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=n这个选项传给了make,系统就会给变量KBUILD_VERBOSE选项附上V的值,否则的话KBUILD_VERBOSE就会为0。然后系统会检查KBUILD_VERBOSE的值,以此来决定quiet和Q的值。符号@控制命令的输出,如果它被放在一个命令之前,这条命令的输出将会是CCscripts/mod/empty.o,而不是Compiling….scripts/mod/empty.o(LCTT译注:CC在makefile中一般都是编译命令)。在这段最后,系统导出了所有的变量。

    参数-s

    然后是-s 的控制

    # If the user is running make -s (silent mode), suppress echoing of
    # commands
    
    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
    
    export quiet Q KBUILD_VERBOSE

    参数-O


      下一个ifeq语句检查的是传递给make的选项O=/dir,这个选项允许在指定的目录dir输出所有的结果文件
    使用 cat -n Makefile | head -n 153 | tail -n +127 查看

    # Cancel implicit rules on top Makefile
    $(CURDIR)/Makefile Makefile: ;
    
    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),)
    endif # ifeq ($(KBUILD_SRC),)

      
      系统会检查变量KBUILD_SRC,它代表内核代码的顶层目录,如果它是空的(第一次执行makefile时总是空的),我们会设置变量KBUILD_OUTPUT为传递给选项O的值(如果这个选项被传进来了)。下一步会检查变量KBUILD_OUTPUT,如果已经设置好,那么接下来会做以下几件事:
      将变量KBUILD_OUTPUT的值保存到临时变量saved-output;
      尝试创建给定的输出目录;
      检查创建的输出目录,如果失败了就打印错误;
      如果成功创建了输出目录,那么就在新目录重新执行make命令(参见选项-C)。
      

    选项C

    下一个ifeq语句会检查传递给make的选项C
    使用 cat -n Makefile | head -n 178 | tail -n +153 查看

    # We process the rest of the Makefile if this is the final invocation of make
    ifeq ($(skip-makefile),)
    
    # Do not print "Entering directory ...",
    # but we want to display it when entering to the output directory
    # so that IDEs/editors are able to understand relative filenames.
    MAKEFLAGS += --no-print-directory
    
    # Call a source code checker (by default, "sparse") as part of the
    # C compilation.
    #
    # Use 'make C=1' to enable checking of only re-compiled files.
    # Use 'make C=2' to enable checking of *all* source files, regardless
    # of whether they are re-compiled or not.
    #
    # See the file "Documentation/sparse.txt" for more details, including
    # where to get the "sparse" utility.
    
    ifeq ("$(origin C)", "command line")
      KBUILD_CHECKSRC = $(C)
    endif
    ifndef KBUILD_CHECKSRC
      KBUILD_CHECKSRC = 0
    endif
    

    选项C会告诉makefile需要使用环境变量$CHECK提供的工具来检查全部c代码,默认情况下会使用sparse。

    我们可以看到之前先检查了skip-makefile ,这个变量在选项O的时候被定义为1

    选项M

    选项M会用来编译外部模块
    使用cat -n Makefile | head -n 198 | tail -n +178 查看

    # Use make M=dir to specify directory of external module to build
    # Old syntax make ... SUBDIRS=$PWD is still supported
    # Setting the environment variable KBUILD_EXTMOD take precedence
    ifdef SUBDIRS
      KBUILD_EXTMOD ?= $(SUBDIRS)
    endif
    
    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

    设置objtree

    紧接着系统检查了变量KBUILD_SRC ,如果KBUILD_SRC 没有被设置,系统会设置变量srctree为当前目录./, 使用cat -n Makefile | head -n 215 | tail -n +198 进行查看

    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)
    
    VPATH       := $(srctree)$(if $(KBUILD_EXTMOD),:$(KBUILD_EXTMOD))
    
    export srctree objtree VPATH
    

    这将会告诉Makefile内核的源码树就在执行make命令的目录,然后要设置objtree和其他变量为这个目录,并且将这些变量导出。

    SUBARCH获取系统架构

    接着就是要获取SUBARCH的值,这个变量代表了当前的系统架构(LCTT译注:一般都指CPU架构):
    使用cat Makefile | head -n 230 | tail -n +217查看

    # SUBARCH tells the usermode build what the underlying arch is.  That is set
    # first, and if a usermode build is happening, the "ARCH=um" on the command
    # line overrides the setting of ARCH below.  If a native build is happening,
    # then ARCH is assigned, getting whatever value it gets normally, and
    # SUBARCH is subsequently ignored.
    
    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 -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/ 

     

    我的机子是Ubuntu-Gnome14.04 LTS x86的(即x86架构)运行一下
    得到如下信息
    这里写图片描述

    如你所见,系统执行uname得到机器、操作系统和架构的信息。因为我们得到的是uname的输出,所以我们需要做一些处理再赋给变量SUBARCH。

    依据SUBARCH设置SRCARCH和hfr-arch

    获得SUBARCH之后就要设置SRCARCHhfr-arch

    SRCARCH提供了硬件架构相关代码的目录
    hfr-arch提供了相关头文件的目录

    ARCH        ?= $(SUBARCH)
    CROSS_COMPILE   ?= $(CONFIG_CROSS_COMPILE:"%"=%)
    
    # Architecture as present in compile.h
    UTS_MACHINE     := $(ARCH)
    SRCARCH     := $(ARCH)
    
    # Additional ARCH settings for x86
    ifeq ($(ARCH),i386)
            SRCARCH := x86
    endif
    ifeq ($(ARCH),x86_64)
            SRCARCH := x86
    endif
    
    # Additional ARCH settings for sparc
    ifeq ($(ARCH),sparc32)
           SRCARCH := sparc
    endif
    ifeq ($(ARCH),sparc64)
           SRCARCH := sparc
    endif
    
    # Additional ARCH settings for sh
    ifeq ($(ARCH),sh64)
           SRCARCH := sh
    endif
    
    # Additional ARCH settings for tile
    ifeq ($(ARCH),tilepro)
           SRCARCH := tile
    endif
    ifeq ($(ARCH),tilegx)
           SRCARCH := tile
    endif
    
    # Where to locate arch specific headers
    hdr-arch  := $(SRCARCH)
    

    注意:ARCH是SUBARCH的别名。

    设置KCONFIG_CONFIG

    如果没有设置过代表内核配置文件路径的变量KCONFIG_CONFIG,下一步系统会设置它,默认情况下就是.config,这个文件是不是很熟悉,它就是我们make menuconfig后的那个.config配置文件,里面写入我们内核编译的所有信息
    使用cat -n Makefile | head -n 292 | tail -n +289 查看

    KCONFIG_CONFIG  ?= .config
    export KCONFIG_CONFIG

    CONFIG_SHELL编译内核过程中要用到的shell

    使用cat -n Makefile | head -n 297 | tail -n +292 查看

    # SHELL used by kbuild
    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++的编译器及相关配置项
    使用cat -n Makefile | head -n 307 | tail -n +297 查看

    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

    我们可以看到Makefile在这里开始适配代表C/C++编译器的变量CC和CXX
    那为什么还要HOST*这些变量呢?这是因为CC是编译内核过程中要使用的目标架构的编译器,但是HOSTCC是要被用来编译一组host程序的(下面我们就会看到)。

    KBUILD_编译的目标

    然后我们就看到变量KBUILD_MODULES和KBUILD_BUILTIN的定义,这两个变量决定了我们要编译什么东西(内核、模块或者两者都有):
    使用cat -n Makefile | head -n 337 | tail -n +307 查看

    
    # Decide whether to build built-in, modular, or both.
    # Normally, just do built-in.
    
    KBUILD_MODULES :=
    KBUILD_BUILTIN := 1
    
    # If we have only "make modules", don't compile built-in objects.
    # When we're building modules with modversions, we need to consider
    # the built-in objects during the descend as well, in order to
    # make sure the checksums are up to date before we record them.
    
    ifeq ($(MAKECMDGOALS),modules)
      KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
    endif
    
    # If we have "make <whatever> modules", compile modules
    # in addition to whatever we do anyway.
    # Just "make" or "make all" shall build modules as well
    
    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
    
    

    在这我们可以看到这些变量的定义,并且,如果们仅仅传递了modules给make,变量KBUILD_BUILTIN会依赖于内核配置选项CONFIG_MODVERSIONS。

    Kbuild

    接着下一步操作是引入下面的文件:
    使用查看 cat Makefile | head -n 341 | tail -n +337

    # We need some generic definitions (do not try to remake the file).
    scripts/Kbuild.include: ;
    include scripts/Kbuild.include

    文件Kbuild或者又叫做KernelBuildSystem是一个用来管理构建内核及其模块的特殊框架。kbuild文件的语法与makefile一样。文件scripts/Kbuild.include为kbuild系统提供了一些常规的定义。因为我们包含了这个kbuild文件,我们可以看到和不同工具关联的这些变量的定义,这些工具会在内核和模块编译过程中被使用(比如链接器、编译器、来自binutils的二进制工具包,等等):

    # Make variables (CC, etc...)
    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)
    CFLAGS_MODULE   =
    AFLAGS_MODULE   =
    LDFLAGS_MODULE  =
    CFLAGS_KERNEL   =
    AFLAGS_KERNEL   =
    CFLAGS_GCOV = -fprofile-arcs -ftest-coverage

    在这些定义好的变量后面,我们又定义了两个变量:USERINCLUDE和LINUXINCLUDE。他们包含了头文件的路径(第一个是给用户用的,第二个是给内核用的),使用cat Makefile | head -n 387 | tail -n +369 查看

    # Use USERINCLUDE when you must reference the UAPI directories only.
    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)
    

    以及给C编译器的标准标志,使用cat Makefile | head -n 419 | tail -n +387查看

    
    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
    
    KBUILD_AFLAGS_KERNEL :=
    KBUILD_CFLAGS_KERNEL :=
    KBUILD_AFLAGS   := -D__ASSEMBLY__
    KBUILD_AFLAGS_MODULE  := -DMODULE
    KBUILD_CFLAGS_MODULE  := -DMODULE
    KBUILD_LDFLAGS_MODULE := -T $(srctree)/scripts/module-common.lds
    
    # Read KERNELRELEASE from include/config/kernel.release (if it exists)
    KERNELRELEASE = $(shell cat include/config/kernel.release 2> /dev/null)
    KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
    
    export VERSION PATCHLEVEL SUBLEVEL KERNELRELEASE KERNELVERSION
    export ARCH SRCARCH CONFIG_SHELL HOSTCC HOSTCFLAGS CROSS_COMPILE AS LD CC
    export CPP AR NM STRIP OBJCOPY OBJDUMP
    export MAKE AWK GENKSYMS INSTALLKERNEL PERL PYTHON UTS_MACHINE
    export HOSTCXX HOSTCXXFLAGS LDFLAGS_MODULE CHECK CHECKFLAGS
    
    export KBUILD_CPPFLAGS NOSTDINC_FLAGS LINUXINCLUDE OBJCOPYFLAGS LDFLAGS
    export KBUILD_CFLAGS CFLAGS_KERNEL CFLAGS_MODULE CFLAGS_GCOV CFLAGS_KASAN
    export KBUILD_AFLAGS AFLAGS_KERNEL AFLAGS_MODULE
    export KBUILD_AFLAGS_MODULE KBUILD_CFLAGS_MODULE KBUILD_LDFLAGS_MODULE
    export KBUILD_AFLAGS_KERNEL KBUILD_CFLAGS_KERNEL
    export KBUILD_ARFLAGS
    

    这并不是最终确定的编译器标志,它们还可以在其他makefile里面更新(比如arch/里面的kbuild)。变量定义完之后,全部会被导出供其他makefile使用。

    下面的两个变量RCS_FIND_IGNORE和RCS_TAR_IGNORE包含了被版本控制系统忽略的文件,使用cat Makefile | head -n 432 | tail -n +419 查看

    # When compiling out-of-tree modules, put MODVERDIR in the module
    # tree rather than in the kernel tree. The kernel tree might
    # even be read-only.
    export MODVERDIR := $(if $(KBUILD_EXTMOD),$(firstword $(KBUILD_EXTMOD))/).tmp_versions
    
    # Files to ignore in find ... statements
    
    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

    然后后面的一大块内容负责根据各种配置文件(make*.config)生成不同目标内核的
    可以使用cat Makefile | head -n 593 | tail -n +432 进行查看,内容较多,我们在这里就不一一列举了。
    下面让我么直接进入make构建的过程。

    内核编译过程


    现在我们已经完成了所有的配置工作,根makefile的下一步工作就是和编译内核相关的了。
    在这之前,我们不会在终端看到make命令输出的任何东西。

    但是现在编译的第一步开始了,好吧,我们知道make后,最终的结果叫vmlinux,那我们就找找这个神奇的东西是怎么产生的吧。

    终极目标vmlinux


    这里我们需要从内核根makefile的594行开始,这里可以看到目标vmlinux的构建命令

    使用 cat Makefile | head -n 606 | tail -n +594 查看

    
    # The all: target is the default when no target is given on the
    # command line.
    # This allow a user to issue only 'make' to build a kernel including modules
    # Defaults to vmlinux, but the arch makefile usually adds further targets
    all: vmlinux
    
    # The arch Makefile can set ARCH_{CPP,A,C}FLAGS to override the default
    # values of the respective KBUILD_* variables
    ARCH_CPPFLAGS :=
    ARCH_AFLAGS :=
    ARCH_CFLAGS :=
    include arch/$(SRCARCH)/Makefile

    目标all:是在命令行如果不指定具体目标时默认使用的目标。
    你可以看到这里包含了架构相关的makefile(在这里就指的是arch/x86/Makefile)。从这一时刻起,我们会从这个makefile继续进行下去。
    如我们所见,目标all依赖于根makefile后面声明的vmlinux,我们可以使用cat Makefile | head -n 922 | tail -n +920 来查看

    # Include targets which we want to
    # execute if the rest of the kernel build went well.
    vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORC

    vmlinux是linux内核的静态链接可执行文件格式。脚本scripts/link-vmlinux.sh把不同的编译好的子模块链接到一起形成了vmlinux。

    vmlinux-deps

    同时我们可以发现vlinux依赖于是vmlinux-deps,我们查找一下它cat -n Makefile | grep vmlinux-deps
    这里写图片描述
    发现它定义在914行,内容如下

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

     

    它是由内核代码下的每个顶级目录的built-in.o组成的。
    之后我们还会检查内核所有的目录,kbuild会编译各个目录下所有的对应$(obj-y)的源文件。接着调用$(LD)-r把这些文件合并到一个build-in.o文件里。当然此时我们还没有vmlinux-deps,所以目标vmlinux现在还不会被构建。对我而言vmlinux-deps包含下面的文件:

    arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_64.o
    arch/x86/kernel/head64.o    arch/x86/kernel/head.o
    init/built-in.o             usr/built-in.o
    arch/x86/built-in.o         kernel/built-in.o
    mm/built-in.o               fs/built-in.o
    ipc/built-in.o              security/built-in.o
    crypto/built-in.o           block/built-in.o
    lib/lib.a                   arch/x86/lib/lib.a
    lib/built-in.o              arch/x86/lib/built-in.o
    drivers/built-in.o          sound/built-in.o
    firmware/built-in.o         arch/x86/pci/built-in.o
    arch/x86/power/built-in.o   arch/x86/video/built-in.o
    net/built-in.o

    vmlinux-dirs


    内核中有这么多目录,Makefile是怎么知道这些目录的呢,让我们继续往下看,使用cat -n Makefile | head -n 940 | tail -n +936 查看

    # The actual objects are generated when descending,
    # make sure no implicit rule kicks in
    $(sort $(vmlinux-deps)): $(vmlinux-dirs) ;

    我们会发现vmlinux-deps是基于vmlinux-dirs
    继续往下,使用cat Makefile | head -n 950 | tail -n +940
    查看

    # Handle descending into subdirectories listed in $(vmlinux-dirs)
    # Preset locale variables to speed up the build process. Limit locale
    # tweaks to this spot to avoid wrong language settings when running
    
    # make menuconfig etc.
    
    # Error messages still appears in the original language
    
    PHONY += $(vmlinux-dirs)
    $(vmlinux-dirs): prepare scripts
        $(Q)$(MAKE) $(build)=$@
    

    就像我们看到的,vmlinux-dir依赖于两部分:prepare和scripts。

    prepare


    第一个prepare定义在内核的根makefile中,准备工作分成三个阶段。

    我们继续往下看,使用 cat -n Makefile | head -n 996 | tail -n +959

    
    # Things we need to do before we recursively start building the kernel
    # or the modules are listed in "prepare".
    # A multi level approach is used. prepareN is processed before prepareN-1.
    # archprepare is used in arch Makefiles and when processed asm symlink,
    # version.h and scripts_basic is processed / created.
    
    # Listed in dependency order
    PHONY += prepare archprepare prepare0 prepare1 prepare2 prepare3
    
    # prepare3 is used to check if we are building in a separate output directory,
    # and if so do:
    # 1) Check that make has not been executed in the kernel src $(srctree)
    prepare3: include/config/kernel.release
    ifneq ($(KBUILD_SRC),)
        @$(kecho) '  Using $(srctree) as source for kernel'
        $(Q)if [ -f $(srctree)/.config -o -d $(srctree)/include/config ]; then 
            echo >&2 "  $(srctree) is not clean, please run 'make mrproper'"; 
            echo >&2 "  in the '$(srctree)' directory.";
            /bin/false; 
        fi;
    endif
    
    # prepare2 creates a makefile if using a separate output directory
    prepare2: prepare3 outputmakefile asm-generic
    
    prepare1: prepare2 $(version_h) include/generated/utsrelease.h 
                       include/config/auto.conf
        $(cmd_crmodverdir)
    
    archprepare: archheaders archscripts prepare1 scripts_basic
    
    prepare0: archprepare FORCE
        $(Q)$(MAKE) $(build)=.
    
    # All the preparing..
    prepare: prepare0
    

    第一个prepare0展开到archprepare,后者又展开到archheader和archscripts,这两个变量定义在对应架构目录下的Makefile,x86架构就是arch/x86让我们看看这个文件。
    x86特定的makefile从变量定义开始,这些变量都是和特定架构的配置文件(defconfig,等等)有关联。在定义了编译16-bit代码的编译选项之后,根据变量BITS的值,如果是32,汇编代码、链接器、以及其它很多东西(全部的定义都可以在arch/x86/Makefile找到)对应的参数就是i386,而64就对应的是x86_84。

    archheaders

    首先是archheaders

    查找archheaders

    archheaders:
        $(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all
    

    archscripts

    接着是archscripts

    查找archscripts

    archscripts: scripts_basic
        $(Q)$(MAKE) $(build)=arch/x86/tools relocs
    

    scripts_basic

    然后是scripts_basic
    通过查找发现我们可以看到archscripts是依赖于根Makefile里的scripts_basic。
    查找scrpits_basic
    查找scripts_basic
    使用cat Makefile | head -n 441 | tail -n +432 查看

    #====================================================
    # Rules shared between *config targets and build targets
    
    # Basic helpers built in scripts/
    PHONY += scripts_basic
    scripts_basic:
        $(Q)$(MAKE) $(build)=scripts/basic
        $(Q)rm -f .tmp_quiet_recordmcount

    首先我们可以看出scripts_basic是按照scripts/basic的makefile执行make的

    下面我们看看scripts/basic下的makefile都有什么
    scripts/basic/Makefile

    scripts/basic/Makefile包含了编译两个主机程序fixdep和bin2的目标

    第一个工具是fixdep:
    用来优化gcc生成的依赖列表,然后在重新编译源文件的时候告诉make。
    第二个工具是bin2c,
    它依赖于内核配置选项CONFIG_BUILD_BIN2C,并且它是一个用来将标准输入接口(LCTT译注:即stdin)收到的二进制流通过标准输出接口(即:stdout)转换成C头文件的非常小的C程序。你可能注意到这里有些奇怪的标志,如hostprogs-y等。这个标志用于所有的kbuild文件,更多的信息你可以从documentation获得。

    在我们这里,hostprogs-y告诉kbuild这里有个名为fixed的程序,这个程序会通过和Makefile相同目录的fixdep.c编译而来。

    我们make时执行make之后,终端的第一个输出就是kbuild的结果:
    这里写图片描述

    现在scripts_basic的工作完成了,现在archscripts 开始工作了,重新回到archscripts的地方,

        $(Q)$(MAKE) $(build)=arch/x86/tools relocs

     

    当目标script_basic被执行,目标archscripts就会make arch/x86/tools下的makefile和目标relocs
    包含了重定位的信息的代码relocs_32.c和relocs_64.c将会被编译,这可以在make的输出中看到,下面仍然是make的工作
    这里写图片描述

    下面我们继续接着进行make,我们发现在编译完relocs.c之后会检查version.h
    这里写图片描述
    使用 cat Makefile | head -n 1021 | tail -n +1017 查看

    $(version_h): $(srctree)/Makefile FORCE
        $(call filechk,version.h)
        $(Q)rm -f $(old_version_h)

    以及在内核的根Makefiel使用arch/x86/include/generated/asm的目标asm-generic来构建generic汇编头文件。
    这里写图片描述
    在目标asm-generic之后,archprepare就完成了,所以目标prepare0会接着被执行,如我上面所写:

    prepare0: archprepare FORCE
        $(Q)$(MAKE) $(build)=.

    注意build,它是定义在文件scripts/Kbuild.include,内容是这样的:
    这里写图片描述
    脚本scripts/Makefile.build通过参数obj给定的目录找到Kbuild文件,然后引入kbuild文件

    include $(kbuild-file)

     

    并根据这个构建目标。我们这里.包含了生成kernel/bounds.s和arch/x86/kernel/asm-offsets.s的Kbuild文件。在此之后,目标prepare就完成了它的工作。

    scripts


    vmlinux-dirs也依赖于第二个目标scripts,它会编译接下来的几个程序:filealias,mk_elfconfig,modpost等等。
    这里写图片描述
    与prepare类似,所以我们在这里就不细讲了。

    开始编译vmlinux-dirs


    之后,scripts/host-programs就可以开始编译我们的目标vmlinux-dirs了。

    首先,我们先来理解一下vmlinux-dirs都包含了那些东西。在我们的例子中它包含了下列内核目录的路径
    这里写图片描述
    我们可以在内核的根Makefile里找到vmlinux-dirs的定义:
    这里写图片描述
    使用cat -n Makefile | head -n 905 | tail -n +891 查看vmlinux-dirs的定义

    vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) 
                 $(core-y) $(core-m) $(drivers-y) $(drivers-m) 
                 $(net-y) $(net-m) $(libs-y) $(libs-m)))
    
    vmlinux-alldirs := $(sort $(vmlinux-dirs) $(patsubst %/,%,$(filter %/, 
                 $(init-) $(core-) $(drivers-) $(net-) $(libs-))))
    
    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)
    

    前面我们已经知道vmlinux-dir会依赖与prepare和scripts

    $(vmlinux-dirs): prepare scripts
    $(Q)$(MAKE) $(build)=$@

     

    符号$@在这里代表了vmlinux-dirs,这就表明程序会递归遍历从vmlinux-dirs以及它内部的全部目录(依赖于配置),并且在对应的目录下执行make命令。我们可以在输出看到结果
    这里写图片描述

    在make的最后阶段,当所有的目录编译结束后,每个目录下的源代码将会被编译并且链接到built-io.o里。

    神秘的built-in.o


    在最后的链接过程中,我们可以看到,几乎所有的依赖条件中,都会生成一个built-in.o的文件。 那这个文件,是怎么生成的呢?
    这里写图片描述

    生成vmlinux


    那么问题来了,makefile是怎么把内核目录中编译生成的build-in.o链接在一起生成vmlinux的呢?
    现在我们回到目标vmlinux上。你应该还记得,目标vmlinux是在内核的根makefile里。在链接vmlinux之前,系统会构建samples,Documentation等等。
    接着我们使用cat -n Makefile | head -n 936 | tail -n +922 查看

    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) 但是还需要一个shell脚本scripts/link-vmlinux.sh 这个脚本是用来干嘛的,不急我们慢慢来。
    我们直接看最后使用+$(call if_changed,link-vmlinux),真相正在一步步浮出水面。

    我们查看一下这个命令
    查找link-vmlinux

    这个命令是cmd_link-vmlinux,就定义在主Makefile中第917行

    cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)

     

    (CONFIGSHELL)bashshell

    < 表示第一个以来目标,那么在vmlinux目标中,第一个目标是 scripts/link-vmlinux.sh
    那么这个命令展开就成为

    /bin/bash scripts/link-vmlinux.sh ld -m elf_i386 --emit-relocs --build-id

     

    现在明晰了在这里调用脚本scripts/link-vmlinux.sh的,把所有的built-in.o链接成一个静态可执行文件vmlinux,和生成System.map。
    那么link-vmlinux.sh是怎么做到得呢,使用该cat -n link-vmlinux.sh | head -n 239 | tail -n +229 查看这个脚本的信息
    这里写图片描述

    info LD vmlinux
    vmlinux_link "${kallsymso}" vmlinux
    
    if [ -n "${CONFIG_BUILDTIME_EXTABLE_SORT}" ]; then
        info SORTEX vmlinux
        sortextable vmlinux
    fi
    
    info SYSMAP System.map
    mksysmap vmlinux System.map
    

    使用了脚本中vmlinux_link这个函数来生成vmlinux,使用mksysmap生成System.map
    下面是vmlinux_link 函数的定义,cat -n link-vmlinux.sh | head -n 69 | tail -n +51

    vmlinux_link()
    {
        local lds="${objtree}/${KBUILD_LDS}"
    
        if [ "${SRCARCH}" != "um" ]; then
            ${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2}                  
                -T ${lds} ${KBUILD_VMLINUX_INIT}                     
                --start-group ${KBUILD_VMLINUX_MAIN} --end-group ${1}
        else
            ${CC} ${CFLAGS_vmlinux} -o ${2}                              
                -Wl,-T,${lds} ${KBUILD_VMLINUX_INIT}                 
                -Wl,--start-group                                    
                     ${KBUILD_VMLINUX_MAIN}                      
                -Wl,--end-group                                      
                -lutil ${1}
            rm -f linux
        fi
    }

    然后是mksysmap,使用cat -n link-vmlinux.sh | head -n 107 | tail -n +103 查看

    mksysmap()
    {
        ${CONFIG_SHELL} "${srctree}/scripts/mksysmap" ${1} ${2}
    }

    最后我们来看看下面的输出:
    生成vmlinux
    vmlinux和System.map生成在内核源码树根目录下。

    这就是全部了,vmlinux构建好了,下一步就是创建bzImage.
    这里写图片描述

    构建bzImage


    bzImage就是压缩了的linux内核镜像。我们可以在构建了vmlinux之后通过执行makebzImage获得bzImage。同时我们可以仅仅执行make而不带任何参数也可以生成bzImage,因为它是在arch/x86/kernel/Makefile里预定义的、默认生成的镜像。

    我们在makefile中查找一下

    这里写图片描述

    我们可以看到bzImage是依赖于vmlinux生成的,

    我们使用cat -n Makefile | head -n 237 | tail -n +215 查看其构建信息

    ####
    # boot loader support. Several targets are kept for legacy purposes
    
    boot := arch/x86/boot
    
    BOOT_TARGETS = bzlilo bzdisk fdimage fdimage144 fdimage288 isoimage
    
    PHONY += bzImage $(BOOT_TARGETS)
    
    # Default kernel to build
    all: bzImage
    
    # KBUILD_IMAGE specify target image being built
    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/$@
    
    

    setup.bin


    在这里我们可以看到第一次为$(boot)==arch/x86/boot 目录执行了make 操作

    我们进入这个目录看看。这个makefile是如何工作生成bzImage的
    这里写图片描述
    我们会发现bzImage依赖于setup.bin和vmlinux.bin
    使用cat -n Makefile | head -n 112 | tail -n +107 我们可以查看到

    $(obj)/setup.bin: $(obj)/setup.elf FORCE
        $(call if_changed,objcopy)
    
    $(obj)/compressed/vmlinux: FORCE
        $(Q)$(MAKE) $(build)=$(obj)/compressed $@

    那么我们现在的主要目标是编译目录arch/x86/boot和arch/x86/boot/compressed的代码,构建setup.bin和vmlinux.bin,最后用这两个文件生成bzImage。

    这里写图片描述

    第一个目标是定义在arch/x86/boot/Makefile的$(obj)/setup.elf:
    这里写图片描述
    我们接着使用cat -n Makefile | head -n 105 | tail -n +103 查看如何生成setup.elf

    $(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE
        $(call if_changed,ld)

    通过setup.ld来检列所有的setup_objs的目标文件来生成setup.elf

    vmlinux.bin


    下一个源码文件是arch/x86/boot/header.S,这个是一个汇编文件
    这里写图片描述

    1. 列表内容

    但是我们不能现在就编译它,因为这个目标依赖于下面两个头文件:

    $(obj)/header.o: $(obj)/voffset.h $(obj)/zoffset.h

     

    第一个头文件voffset.h是使用sed脚本生成的
    这里写图片描述
    包含用nm工具从vmlinux获取的两个地址:

    #define VO__end 0xffffffff82ab0000
    #define VO__text 0xffffffff81000000

    这两个地址是内核的起始和结束地址。

    第二个头文件zoffset.h在arch/x86/boot/compressed/Makefile可以看出是依赖于目标vmlinux的
    这里写图片描述

    $(obj)/zoffset.h: $(obj)/compressed/vmlinux FORCE
        $(call if_changed,zoffset)

    然后编译目录arch/x86/boot/compressed下的源代码,然后生成vmlinux.bin、vmlinux.bin.bz2,和编译工具mkpiggy。
    这里写图片描述

    vmlinux.bin是去掉了调试信息和注释的vmlinux二进制文件,加上了占用了u32(LCTT译注:即4-Byte)的长度信息的vmlinux.bin.all压缩后就是vmlinux.bin.bz2。其中vmlinux.bin.all包含了vmlinux.bin和vmlinux.relocs(LCTT译注:vmlinux的重定位信息),其中vmlinux.relocs是vmlinux经过程序relocs处理之后的vmlinux镜像(见上文所述)。
    我们现在已经获取到了这些文件,汇编文件piggy.S将会被mkpiggy生成、然后编译:

    MKPIGGY arch/x86/boot/compressed/piggy.S
     AS      arch/x86/boot/compressed/piggy.o

    这个汇编文件会包含经过计算得来的、压缩内核的偏移信息。处理完这个汇编文件,我们就可以看到zoffset生成了:

    ZOFFSET arch/x86/boot/zoffset.h

     

    现在zoffset.h和voffset.h已经生成了,arch/x86/boot里的源文件可以继续编译,直到
    所有的源代码会被编译,他们最终会被链接到setup.elf
    这里写图片描述

    ld -m elf_x86_64   -T arch/x86/boot/setup.ld arch/x86/boot/a20.o arch/x86/boot/bioscall.o arch/x86/boot/cmdline.o arch/x86/boot/copy.o arch/x86/boot/cpu.o arch/x86/boot/cpuflags.o arch/x86/boot/cpucheck.o arch/x86/boot/early_serial_console.o arch/x86/boot/edd.o arch/x86/boot/header.o arch/x86/boot/main.o arch/x86/boot/mca.o arch/x86/boot/memory.o arch/x86/boot/pm.o arch/x86/boot/pmjump.o arch/x86/boot/printf.o arch/x86/boot/regs.o arch/x86/boot/string.o arch/x86/boot/tty.o arch/x86/boot/video.o arch/x86/boot/video-mode.o arch/x86/boot/version.o arch/x86/boot/video-vga.o arch/x86/boot/video-vesa.o arch/x86/boot/video-bios.o -o arch/x86/boot/setup.elf

     

    最后的两件事是创建包含目录arch/x86/boot/*下的编译过的代码的setup.bin:

    objcopy  -O binary arch/x86/boot/setup.elf arch/x86/boot/setup.bin

     

    以及从vmlinux生成vmlinux.bin:

    objcopy  -O binary -R .note -R .comment -S arch/x86/boot/compressed/vmlinux arch/x86/boot/vmlinux.bin

     

    生成bzImage


    最后,我们编译主机程序arch/x86/boot/tools/build.c,它将会用来把setup.bin和vmlinux.bin打包成bzImage:

    arch/x86/boot/tools/build arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin arch/x86/boot/zoffset.h arch/x86/boot/bzImage

     

    实际上bzImage就是把setup.bin和vmlinux.bin连接到一起。最终我们会看到输出结果,就和那些用源码编译过内核的同行的结果一样:
    aa

  • 相关阅读:
    swagger序列化对example属性的特殊处理造成的json格式异常问题
    Elasticsearch 6.2.4 xpack白金版破解-仅供交流学习使用
    Logback多进程写入同一日志文件时滚动日期混乱问题
    mycat事务中上来执行select马上提交——小猫如此折腾,我选dble
    我家很管事的猫——mycat初步部署实践与问题排查
    certbot https签发证书与自动更新——acme实在太难用,certbot一键式全搞定
    自力更生Collections.sort发现比较结果混乱?Comparator的锅还是强转类型导致?
    Java SPI、servlet3.0与@HandlesTypes源码分析
    真——Springcloud支持Https
    Controller层的方法访问标志与Spring装配与AspectJ切面处理
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8991298.html
Copyright © 2011-2022 走看看