Linux中國

你知道 Linux 內核是如何構建的嗎?

(題圖來自:adafruit.com)

編譯內核前的準備

在開始編譯前要進行很多準備工作。最主要的就是找到並配置好配置文件,make 命令要使用到的參數都需要從這些配置文件獲取。現在就讓我們深入內核的根 makefile

內核的根 Makefile 負責構建兩個主要的文件:vmlinux (內核鏡像可執行文件)和模塊文件。內核的 Makefile 從定義如下變數開始:

VERSION = 4
PATCHLEVEL = 2
SUBLEVEL = 0
EXTRAVERSION = -rc3
NAME = Hurr durr I'ma sheep

這些變數決定了當前內核的版本,並且被使用在很多不同的地方,比如同一個 Makefile 中的 KERNELVERSION

KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)

接下來我們會看到很多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_VERBOSE 就會為 0。然後系統會檢查 KBUILD_VERBOSE 的值,以此來決定 quietQ 的值。符號 @ 控制命令的輸出,如果它被放在一個命令之前,這條命令的輸出將會是 CC scripts/mod/empty.o,而不是Compiling .... scripts/mod/empty.o(LCTT 譯註:CC 在 makefile 中一般都是編譯命令)。在這段最後,系統導出了所有的變數。

下一個 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 時總是空的),我們會設置變數 KBUILD_OUTPUT 為傳遞給選項 O 的值(如果這個選項被傳進來了)。下一步會檢查變數 KBUILD_OUTPUT ,如果已經設置好,那麼接下來會做以下幾件事:

  • 將變數 KBUILD_OUTPUT 的值保存到臨時變數 saved-output
  • 嘗試創建給定的輸出目錄;
  • 檢查創建的輸出目錄,如果失敗了就列印錯誤;
  • 如果成功創建了輸出目錄,那麼就在新目錄重新執行 make 命令(參見選項-C)。

下一個 ifeq 語句會檢查傳遞給 make 的選項 CM

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 命令的目錄,然後要設置 objtree 和其他變數為這個目錄,並且將這些變數導出。接著就是要獲取 SUBARCH 的值,這個變數代表了當前的系統架構(LCTT 譯註:一般都指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 之後就要設置SRCARCHhfr-archSRCARCH 提供了硬體架構相關代碼的目錄,hfr-arch 提供了相關頭文件的目錄:

ifeq ($(ARCH),i386)
        SRCARCH := x86
endif
ifeq ($(ARCH),x86_64)
        SRCARCH := x86
endif

hdr-arch  := $(SRCARCH)

注意:ARCHSUBARCH 的別名。如果沒有設置過代表內核配置文件路徑的變數 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)

接下來就要設置一組和編譯內核的編譯器相關的變數。我們會設置主機的 CC++ 的編譯器及相關配置項:

HOSTCC       = gcc
HOSTCXX      = g++
HOSTCFLAGS   = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89
HOSTCXXFLAGS = -O2

接下來會去適配代表編譯器的變數 CC,那為什麼還要 HOST* 這些變數呢?這是因為 CC 是編譯內核過程中要使用的目標架構的編譯器,但是 HOSTCC 是要被用來編譯一組 host 程序的(下面我們就會看到)。

然後我們就看到變數 KBUILD_MODULESKBUILD_BUILTIN 的定義,這兩個變數決定了我們要編譯什麼東西(內核、模塊或者兩者):

KBUILD_MODULES :=
KBUILD_BUILTIN := 1

ifeq ($(MAKECMDGOALS),modules)
  KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
endif

在這我們可以看到這些變數的定義,並且,如果們僅僅傳遞了 modulesmake,變數 KBUILD_BUILTIN 會依賴於內核配置選項 CONFIG_MODVERSIONS

下一步操作是引入下面的文件:

include scripts/Kbuild.include

文件 Kbuild 或者又叫做 Kernel Build System 是一個用來管理構建內核及其模塊的特殊框架。kbuild 文件的語法與 makefile 一樣。文件scripts/Kbuild.includekbuild 系統提供了一些常規的定義。因為我們包含了這個 kbuild 文件,我們可以看到和不同工具關聯的這些變數的定義,這些工具會在內核和模塊編譯過程中被使用(比如鏈接器、編譯器、來自 binutils 的二進位工具包 ,等等):

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
...
...
...

在這些定義好的變數後面,我們又定義了兩個變數:USERINCLUDELINUXINCLUDE。他們包含了頭文件的路徑(第一個是給用戶用的,第二個是給內核用的):

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

LINUXINCLUDE    := 
        -I$(srctree)/arch/$(hdr-arch)/include 
        ...

以及給 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_IGNORERCS_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

這就是全部了,我們已經完成了所有的準備工作,下一個點就是如果構建vmlinux

直面內核構建

現在我們已經完成了所有的準備工作,根 makefile(註:內核根目錄下的 makefile)的下一步工作就是和編譯內核相關的了。在這之前,我們不會在終端看到 make 命令輸出的任何東西。但是現在編譯的第一步開始了,這裡我們需要從內核根 makefile 的 598 行開始,這裡可以看到目標vmlinux

all: vmlinux
    include arch/$(SRCARCH)/Makefile

不要操心我們略過的從 export RCS_FIND_IGNORE.....all: vmlinux..... 這一部分 makefile 代碼,他們只是負責根據各種配置文件(make *.config)生成不同目標內核的,因為之前我就說了這一部分我們只討論構建內核的通用途徑。

目標 all: 是在命令行如果不指定具體目標時默認使用的目標。你可以看到這裡包含了架構相關的 makefile(在這裡就指的是 arch/x86/Makefile)。從這一時刻起,我們會從這個 makefile 繼續進行下去。如我們所見,目標 all 依賴於根 makefile 後面聲明的 vmlinux

vmlinux: scripts/link-vmlinux.sh $(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 文件里。此時我們還沒有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

下一個可以被執行的目標如下:

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

就像我們看到的,vmlinux-dir 依賴於兩部分:preparescripts。第一個 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 ,後者又展開到 archheaderarchscripts,這兩個變數定義在 x86_64 相關的 Makefile。讓我們看看這個文件。x86_64 特定的 makefile 從變數定義開始,這些變數都是和特定架構的配置文件 (defconfig,等等)有關聯。在定義了編譯 16-bit 代碼的編譯選項之後,根據變數 BITS 的值,如果是 32, 彙編代碼、鏈接器、以及其它很多東西(全部的定義都可以在arch/x86/Makefile找到)對應的參數就是 i386,而 64 就對應的是 x86_84

第一個目標是 makefile 生成的系統調用列表(syscall table)中的 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

scripts/basic/Makefile 包含了編譯兩個主機程序 fixdepbin2 的目標:

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,並且它是一個用來將標準輸入介面(LCTT 譯註:即 stdin)收到的二進位流通過標準輸出介面(即:stdout)轉換成 C 頭文件的非常小的 C 程序。你可能注意到這裡有些奇怪的標誌,如 hostprogs-y 等。這個標誌用於所有的 kbuild 文件,更多的信息你可以從documentation 獲得。在我們這裡, hostprogs-y 告訴 kbuild 這裡有個名為 fixed 的程序,這個程序會通過和 Makefile 相同目錄的 fixdep.c 編譯而來。

執行 make 之後,終端的第一個輸出就是 kbuild 的結果:

$ make
  HOSTCC  scripts/basic/fixdep

當目標 script_basic 被執行,目標 archscripts 就會 make arch/x86/tools 下的 makefile 和目標 relocs:

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

包含了重定位 的信息的代碼 relocs_32.crelocs_64.c 將會被編譯,這可以在make 的輸出中看到:

  HOSTCC  arch/x86/tools/relocs_32.o
  HOSTCC  arch/x86/tools/relocs_64.o
  HOSTCC  arch/x86/tools/relocs_common.o
  HOSTLD  arch/x86/tools/relocs

在編譯完 relocs.c 之後會檢查 version.h:

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

我們可以在輸出看到它:

CHK     include/config/kernel.release

以及在內核的根 Makefiel 使用 arch/x86/include/generated/asm 的目標 asm-generic 來構建 generic 彙編頭文件。在目標 asm-generic 之後,archprepare 就完成了,所以目標 prepare0 會接著被執行,如我上面所寫:

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

注意 build,它是定義在文件 scripts/Kbuild.include,內容是這樣的:

build := -f $(srctree)/scripts/Makefile.build obj

或者在我們的例子中,它就是當前源碼目錄路徑:.

$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=.

腳本 scripts/Makefile.build 通過參數 obj 給定的目錄找到 Kbuild 文件,然後引入 kbuild 文件:

include $(kbuild-file)

並根據這個構建目標。我們這裡 . 包含了生成 kernel/bounds.sarch/x86/kernel/asm-offsets.sKbuild 文件。在此之後,目標 prepare 就完成了它的工作。 vmlinux-dirs 也依賴於第二個目標 scripts ,它會編譯接下來的幾個程序:filealiasmk_elfconfigmodpost 等等。之後,scripts/host-programs 就可以開始編譯我們的目標 vmlinux-dirs 了。

首先,我們先來理解一下 vmlinux-dirs 都包含了那些東西。在我們的例子中它包含了下列內核目錄的路徑:

init usr arch/x86 kernel mm fs ipc security crypto block
drivers sound firmware arch/x86/pci arch/x86/power
arch/x86/video net lib arch/x86/lib

我們可以在內核的根 Makefile 里找到 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)))

init-y      := init/
drivers-y   := drivers/ sound/ firmware/
net-y       := net/
libs-y      := lib/
...
...
...

這裡我們藉助函數 patsubstfilter去掉了每個目錄路徑里的符號 /,並且把結果放到 vmlinux-dirs 里。所以我們就有了 vmlinux-dirs 里的目錄列表,以及下面的代碼:

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

符號 $@ 在這裡代表了 vmlinux-dirs,這就表明程序會遞歸遍歷從 vmlinux-dirs 以及它內部的全部目錄(依賴於配置),並且在對應的目錄下執行 make 命令。我們可以在輸出看到結果:

  CC      init/main.o
  CHK     include/generated/compile.h
  CC      init/version.o
  CC      init/do_mounts.o
  ...
  CC      arch/x86/crypto/glue_helper.o
  AS      arch/x86/crypto/aes-x86_64-asm_64.o
  CC      arch/x86/crypto/aes_glue.o
  ...
  AS      arch/x86/entry/entry_64.o
  AS      arch/x86/entry/thunk_64.o
  CC      arch/x86/entry/syscall_64.o

每個目錄下的源代碼將會被編譯並且鏈接到 built-io.o 里:

$ find . -name built-in.o
./arch/x86/crypto/built-in.o
./arch/x86/crypto/sha-mb/built-in.o
./arch/x86/net/built-in.o
./init/built-in.o
./usr/built-in.o
...
...

好了,所有的 built-in.o 都構建完了,現在我們回到目標 vmlinux 上。你應該還記得,目標 vmlinux 是在內核的根makefile 里。在鏈接 vmlinux 之前,系統會構建 samples, Documentation 等等,但是如上文所述,我不會在本文描述這些。

vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
    ...
    ...
    +$(call if_changed,link-vmlinux)

你可以看到,調用腳本 scripts/link-vmlinux.sh 的主要目的是把所有的 built-in.o 鏈接成一個靜態可執行文件,和生成 System.map。 最後我們來看看下面的輸出:

  LINK    vmlinux
  LD      vmlinux.o
  MODPOST vmlinux.o
  GEN     .version
  CHK     include/generated/compile.h
  UPD     include/generated/compile.h
  CC      init/version.o
  LD      init/built-in.o
  KSYM    .tmp_kallsyms1.o
  KSYM    .tmp_kallsyms2.o
  LD      vmlinux
  SORTEX  vmlinux
  SYSMAP  System.map

vmlinuxSystem.map 生成在內核源碼樹根目錄下。

$ ls vmlinux System.map 
System.map  vmlinux

這就是全部了,vmlinux 構建好了,下一步就是創建 bzImage.

製作bzImage

bzImage 就是壓縮了的 linux 內核鏡像。我們可以在構建了 vmlinux 之後通過執行 make bzImage 獲得bzImage。同時我們可以僅僅執行 make 而不帶任何參數也可以生成 bzImage ,因為它是在 arch/x86/kernel/Makefile 里預定義的、默認生成的鏡像:

all: bzImage

讓我們看看這個目標,它能幫助我們理解這個鏡像是怎麼構建的。我已經說過了 bzImage 是被定義在 arch/x86/kernel/Makefile,定義如下:

bzImage: vmlinux
    $(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 目錄執行 make,在我們的例子里是這樣的:

boot := arch/x86/boot

現在的主要目標是編譯目錄 arch/x86/bootarch/x86/boot/compressed 的代碼,構建 setup.binvmlinux.bin,最後用這兩個文件生成 bzImage。第一個目標是定義在 arch/x86/boot/Makefile$(obj)/setup.elf:

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

我們已經在目錄 arch/x86/boot 有了鏈接腳本 setup.ld,和擴展到 boot 目錄下全部源代碼的變數 SETUP_OBJS 。我們可以看看第一個輸出:

  AS      arch/x86/boot/bioscall.o
  CC      arch/x86/boot/cmdline.o
  AS      arch/x86/boot/copy.o
  HOSTCC  arch/x86/boot/mkcpustr
  CPUSTR  arch/x86/boot/cpustr.h
  CC      arch/x86/boot/cpu.o
  CC      arch/x86/boot/cpuflags.o
  CC      arch/x86/boot/cpucheck.o
  CC      arch/x86/boot/early_serial_console.o
  CC      arch/x86/boot/edd.o

下一個源碼文件是 arch/x86/boot/header.S,但是我們不能現在就編譯它,因為這個目標依賴於下面兩個頭文件:

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

第一個頭文件 voffset.h 是使用 sed 腳本生成的,包含用 nm 工具從 vmlinux 獲取的兩個地址:

#define VO__end 0xffffffff82ab0000
#define VO__text 0xffffffff81000000

這兩個地址是內核的起始和結束地址。第二個頭文件 zoffset.harch/x86/boot/compressed/Makefile 可以看出是依賴於目標 vmlinux的:

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

目標 $(obj)/compressed/vmlinux 依賴於 vmlinux-objs-y —— 說明需要編譯目錄 arch/x86/boot/compressed 下的源代碼,然後生成 vmlinux.binvmlinux.bin.bz2,和編譯工具 mkpiggy。我們可以在下面的輸出看出來:

  LDS     arch/x86/boot/compressed/vmlinux.lds
  AS      arch/x86/boot/compressed/head_64.o
  CC      arch/x86/boot/compressed/misc.o
  CC      arch/x86/boot/compressed/string.o
  CC      arch/x86/boot/compressed/cmdline.o
  OBJCOPY arch/x86/boot/compressed/vmlinux.bin
  BZIP2   arch/x86/boot/compressed/vmlinux.bin.bz2
  HOSTCC  arch/x86/boot/compressed/mkpiggy

vmlinux.bin 是去掉了調試信息和注釋的 vmlinux 二進位文件,加上了佔用了 u32 (LCTT 譯註:即4-Byte)的長度信息的 vmlinux.bin.all 壓縮後就是 vmlinux.bin.bz2。其中 vmlinux.bin.all 包含了 vmlinux.binvmlinux.relocs(LCTT 譯註:vmlinux 的重定位信息),其中 vmlinux.relocsvmlinux 經過程序 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.hvoffset.h 已經生成了,arch/x86/boot 里的源文件可以繼續編譯:

  AS      arch/x86/boot/header.o
  CC      arch/x86/boot/main.o
  CC      arch/x86/boot/mca.o
  CC      arch/x86/boot/memory.o
  CC      arch/x86/boot/pm.o
  AS      arch/x86/boot/pmjump.o
  CC      arch/x86/boot/printf.o
  CC      arch/x86/boot/regs.o
  CC      arch/x86/boot/string.o
  CC      arch/x86/boot/tty.o
  CC      arch/x86/boot/video.o
  CC      arch/x86/boot/video-mode.o
  CC      arch/x86/boot/video-vga.o
  CC      arch/x86/boot/video-vesa.o
  CC      arch/x86/boot/video-bios.o

所有的源代碼會被編譯,他們最終會被鏈接到 setup.elf

  LD      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

最最後,我們編譯主機程序 arch/x86/boot/tools/build.c,它將會用來把 setup.binvmlinux.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.binvmlinux.bin 連接到一起。最終我們會看到輸出結果,就和那些用源碼編譯過內核的同行的結果一樣:

Setup is 16268 bytes (padded to 16384 bytes).
System is 4704 kB
CRC 94a88f9a
Kernel: arch/x86/boot/bzImage is ready  (#5)

全部結束。

結論

這就是本文的結尾部分。本文我們了解了編譯內核的全部步驟:從執行 make 命令開始,到最後生成 bzImage。我知道,linux 內核的 makefile 和構建 linux 的過程第一眼看起來可能比較迷惑,但是這並不是很難。希望本文可以幫助你理解構建 linux 內核的整個流程。

鏈接

via: https://github.com/0xAX/linux-insides/blob/master/Misc/how_kernel_compiled.md

譯者:oska874 校對:wxy

本文由 LCTT 原創翻譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國