你知道 Linux 核心是如何構建的嗎?
介紹
我不會告訴你怎麼在自己的電腦上去構建、安裝一個定製化的 Linux 核心,這樣的資料太多了,它們會對你有幫助。本文會告訴你當你在核心原始碼路徑裡敲下make 時會發生什麼。
當我剛剛開始學習核心程式碼時,Makefile 是我開啟的第一個檔案,這個檔案看起來真令人害怕。那時候這個 Makefile 還只包含了1591 行程式碼,當我開始寫本文時,核心已經是4.2.0的第三個候選版本 了。
這個 makefile 是 Linux 核心程式碼的根 makefile ,核心構建就始於此處。是的,它的內容很多,但是如果你已經讀過核心原始碼,你就會發現每個包含程式碼的目錄都有一個自己的 makefile。當然了,我們不會去描述每個程式碼檔案是怎麼編譯連結的,所以我們將只會挑選一些通用的例子來說明問題。而你不會在這裡找到構建核心的文件、如何整潔核心程式碼、tags 的生成和交叉編譯 相關的說明,等等。我們將從make 開始,使用標準的核心配置檔案,到生成了核心映象 bzImage 結束。
如果你已經很瞭解 make 工具那是最好,但是我也會描述本文出現的相關程式碼。
讓我們開始吧!
(題圖來自: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 核心的整個流程。
連結
- GNU make util
- Linux kernel top Makefile
- cross-compilation
- Ctags
- sparse
- bzImage
- uname
- shell
- Kbuild
- binutils
- gcc
- Documentation
- System.map
- Relocation
相關文章
- 你知道SSL是如何工作的嗎?
- 你知道YouTube的架構是什麼嗎架構
- 你知道如何學習Linux嗎?Linux
- 你知道前端是如何實現水印的嗎前端
- 你知道什麼是三層架構嗎?架構
- 你知道Spring中BeanFactoryPostProcessors是如何執行的嗎?SpringBean
- 你知道MySQL是如何處理千萬級資料的嗎?MySql
- jvm是如何執行i = i++ + ++i的,你知道嗎?JVM
- 你知道Thread執行緒是如何運作的嗎?thread執行緒
- 你知道的反射是這樣嗎?(二)反射
- 網站建設的技巧都有哪些你知道嗎?網站
- Linux黑話你知道嗎?啥是顯示伺服器Linux伺服器
- 你知道什麼是路由器嗎?路由器
- Dart | 你知道 sync*/async* 是怎麼用的嗎?Dart
- 你真的知道Python的字串是什麼嗎?Python字串
- 【嗅探底層】你知道Synchronized作用是同步加鎖,可你知道它在JVM中是如何實現的嗎?synchronizedJVM
- 你真的知道計算機是如何進行減法運算的嗎?計算機
- 每天都在用 Map,這些核心技術你知道嗎?
- Go 泛型的這 3 個核心設計,你都知道嗎?Go泛型
- 計算機網路的 166 個核心概念,你知道嗎?計算機網路
- 如何構建你的聊天介面
- 什麼是OA伺服器,你知道嗎?伺服器
- 你真的知道什麼是系統呼叫嗎?
- 伺服器能做那些有趣是!你知道嗎?????伺服器
- 聚合支付代理是怎麼賺錢的,你知道嗎?
- 塊儲存是做什麼用的,你知道嗎?
- 你知道Ctrl+Alt+Delete是怎麼來的嗎?delete
- 你知道年薪百萬的程式設計師是如何對時間管理嗎?程式設計師
- 搭建團隊架構的重要原則,你知道嗎?架構
- 常見的Linux系統有哪些?你知道嗎?Linux
- 你知道Java是如何解決可見性和有序性問題的嗎?Java
- Lombok經常用,但是你知道它的原理是什麼嗎?Lombok
- 你真的知道什麼是“遊戲障礙”了嗎?遊戲
- 你知道什麼是二次元嗎?二次元
- 阿里的Blink開源了,如何部署你知道嗎?阿里
- 網路釣魚 你知道如何識別嗎?
- 你知道如何用 PHP 實現多程式嗎?PHP
- Google 是如何構建 Web 框架的GoWeb框架