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

linux.cn發表於2015-09-12

介紹

我不會告訴你怎麼在自己的電腦上去構建、安裝一個定製化的 Linux 核心,這樣的資料太多了,它們會對你有幫助。本文會告訴你當你在核心原始碼路徑裡敲下make 時會發生什麼。

當我剛剛開始學習核心程式碼時,Makefile 是我開啟的第一個檔案,這個檔案看起來真令人害怕。那時候這個 Makefile 還只包含了1591 行程式碼,當我開始寫本文時,核心已經是4.2.0的第三個候選版本 了。

這個 makefile 是 Linux 核心程式碼的根 makefile ,核心構建就始於此處。是的,它的內容很多,但是如果你已經讀過核心原始碼,你就會發現每個包含程式碼的目錄都有一個自己的 makefile。當然了,我們不會去描述每個程式碼檔案是怎麼編譯連結的,所以我們將只會挑選一些通用的例子來說明問題。而你不會在這裡找到構建核心的文件、如何整潔核心程式碼、tags 的生成和交叉編譯 相關的說明,等等。我們將從make 開始,使用標準的核心配置檔案,到生成了核心映象 bzImage 結束。

如果你已經很瞭解 make 工具那是最好,但是我也會描述本文出現的相關程式碼。

讓我們開始吧!

你知道 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 的值,以此來決定 quiet 和Q 的值。符號 @ 控制命令的輸出,如果它被放在一個命令之前,這條命令的輸出將會是 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 的選項 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 命令的目錄,然後要設定 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 之後就要設定SRCARCH 和 hfr-arch,SRCARCH 提供了硬體架構相關程式碼的目錄,hfr-arch 提供了相關標頭檔案的目錄:

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

hdr-arch  := $(SRCARCH)

注意:ARCH 是 SUBARCH 的別名。如果沒有設定過代表核心配置檔案路徑的變數 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

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

然後我們就看到變數 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。

下一步操作是引入下面的檔案:

include scripts/Kbuild.include

檔案 Kbuild 或者又叫做 Kernel Build System 是一個用來管理構建核心及其模組的特殊框架。kbuild 檔案的語法與 makefile 一樣。檔案scripts/Kbuild.include 為 kbuild 系統提供了一些常規的定義。因為我們包含了這個 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
...
...
...

在這些定義好的變數後面,我們又定義了兩個變數:USERINCLUDE 和 LINUXINCLUDE。他們包含了標頭檔案的路徑(第一個是給使用者用的,第二個是給核心用的):

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

這就是全部了,我們已經完成了所有的準備工作,下一個點就是如果構建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 依賴於兩部分: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,等等)有關聯。在定義了編譯 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 包含了編譯兩個主機程式 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,並且它是一個用來將標準輸入介面(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.c 和 relocs_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.s 和 arch/x86/kernel/asm-offsets.s 的 Kbuild 檔案。在此之後,目標 prepare 就完成了它的工作。 vmlinux-dirs 也依賴於第二個目標 scripts ,它會編譯接下來的幾個程式:filealias,mk_elfconfig,modpost 等等。之後,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/
...
...
...

這裡我們藉助函式 patsubst 和 filter去掉了每個目錄路徑裡的符號 /,並且把結果放到 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

vmlinux 和System.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/boot 和 arch/x86/boot/compressed 的程式碼,構建 setup.bin 和 vmlinux.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.h 在 arch/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.bin、vmlinux.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.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 裡的原始檔可以繼續編譯:

  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.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 連線到一起。最終我們會看到輸出結果,就和那些用原始碼編譯過核心的同行的結果一樣:

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 核心的整個流程。

連結

相關文章