你知道 Linux 内核是如何构建的吗?
October 17, 2015
Table of Contents
介绍
我不会告诉你怎么在自己的电脑上去构建、安装一个定制化的 Linux 内核,这样的资料太多了,它们会对你有帮助。本文会告诉你当你在内核源码路径里敲下
当我刚刚开始学习内核代码时,Makefile 是我打开的第一个文件,这个文件看起来真令人害怕 :)。那时候这个 Makefile 还只包含了
这个 makefile 是 Linux 内核代码的根 makefile ,内核构建就始于此处。是的,它的内容很多,但是如果你已经读过内核源代码,你就会发现每个包含代码的目录都有一个自己的 makefile。当然了,我们不会去描述每个代码文件是怎么编译链接的,所以我们将只会挑选一些通用的例子来说明问题。而你不会在这里找到构建内核的文档、如何整洁内核代码、tags 的生成和交叉编译 相关的说明,等等。我们将从
如果你已经很了解 make 工具那是最好,但是我也会描述本文出现的相关代码。
让我们开始吧!
(题图来自:adafruit.com)
编译内核前的准备
在开始编译前要进行很多准备工作。最主要的就是找到并配置好配置文件,
内核的根
VERSION = 4 PATCHLEVEL = 2 SUBLEVEL = 0 EXTRAVERSION = -rc3 NAME = Hurr durr I'ma sheep
这些变量决定了当前内核的版本,并且被使用在很多不同的地方,比如同一个
KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
接下来我们会看到很多
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
如果
下一个
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_OUTPUT 的值保存到临时变量saved-output ; - 尝试创建给定的输出目录;
- 检查创建的输出目录,如果失败了就打印错误;
- 如果成功创建了输出目录,那么就在新目录重新执行
make 命令(参见选项-C )。
下一个
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
第一个选项
系统还会检查变量
ifeq ($(KBUILD_SRC),) srctree := . endif objtree := . src := $(srctree) obj := $(objtree) export srctree objtree VPATH
这将会告诉
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 得到机器、操作系统和架构的信息。因为我们得到的是
ifeq ($(ARCH),i386) SRCARCH := x86 endif ifeq ($(ARCH),x86_64) SRCARCH := x86 endif hdr-arch := $(SRCARCH)
注意:
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)
接下来就要设置一组和编译内核的编译器相关的变量。我们会设置主机的
HOSTCC = gcc HOSTCXX = g++ HOSTCFLAGS = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89 HOSTCXXFLAGS = -O2
接下来会去适配代表编译器的变量
然后我们就看到变量
KBUILD_MODULES := KBUILD_BUILTIN := 1 ifeq ($(MAKECMDGOALS),modules) KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1) endif
在这我们可以看到这些变量的定义,并且,如果们仅仅传递了
下一步操作是引入下面的文件:
include scripts/Kbuild.include
文件 Kbuild 或者又叫做
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 := -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 里面更新(比如
下面的两个变量
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
这就是全部了,我们已经完成了所有的准备工作,下一个点就是如果构建
直面内核构建
现在我们已经完成了所有的准备工作,根 makefile(注:内核根目录下的 makefile)的下一步工作就是和编译内核相关的了。在这之前,我们不会在终端看到
all: vmlinux include arch/$(SRCARCH)/Makefile
不要操心我们略过的从
目标
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
第二个目标是
vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)
它是由内核代码下的每个顶级目录的
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)=$@
就像我们看到的,
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
第一个
第一个目标是 makefile 生成的系统调用列表(syscall table)中的
archheaders: $(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all
第二个目标是 makefile 里的
archscripts: scripts_basic $(Q)$(MAKE) $(build)=arch/x86/tools relocs
我们可以看到
scripts_basic: $(Q)$(MAKE) $(build)=scripts/basic
hostprogs-y := fixdep hostprogs-$(CONFIG_BUILD_BIN2C) += bin2c always := $(hostprogs-y) $(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep
第一个工具是
执行 make 之后,终端的第一个输出就是
$ make HOSTCC scripts/basic/fixdep
当目标
$(Q)$(MAKE) $(build)=arch/x86/tools relocs
包含了重定位 的信息的代码
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
在编译完
$(version_h): $(srctree)/Makefile FORCE $(call filechk,version.h) $(Q)rm -f $(old_version_h)
我们可以在输出看到它:
CHK include/config/kernel.release
以及在内核的根 Makefiel 使用
prepare0: archprepare FORCE $(Q)$(MAKE) $(build)=.
注意
build := -f $(srctree)/scripts/Makefile.build obj
或者在我们的例子中,它就是当前源码目录路径:
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=.
脚本 scripts/Makefile.build 通过参数
include $(kbuild-file)
并根据这个构建目标。我们这里
首先,我们先来理解一下
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 := $(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/ ... ... ...
这里我们借助函数
$(vmlinux-dirs): prepare scripts $(Q)$(MAKE) $(build)=$@
符号
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
每个目录下的源代码将会被编译并且链接到
$ 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 ... ...
好了,所有的
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE ... ... +$(call if_changed,link-vmlinux)
你可以看到,调用脚本 scripts/link-vmlinux.sh 的主要目的是把所有的
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
$ ls vmlinux System.map System.map vmlinux
这就是全部了,
制作bzImage
all: bzImage
让我们看看这个目标,它能帮助我们理解这个镜像是怎么构建的。我已经说过了
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 目录执行
boot := arch/x86/boot
现在的主要目标是编译目录
$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE $(call if_changed,ld)
我们已经在目录
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
第一个头文件
#define VO__end 0xffffffff82ab0000 #define VO__text 0xffffffff81000000
这两个地址是内核的起始和结束地址。第二个头文件
$(obj)/zoffset.h: $(obj)/compressed/vmlinux FORCE $(call if_changed,zoffset)
目标
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
MKPIGGY arch/x86/boot/compressed/piggy.S AS arch/x86/boot/compressed/piggy.o
这个汇编文件会包含经过计算得来的、压缩内核的偏移信息。处理完这个汇编文件,我们就可以看到
ZOFFSET arch/x86/boot/zoffset.h
现在
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
所有的源代码会被编译,他们最终会被链接到
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
最后的两件事是创建包含目录
objcopy -O binary arch/x86/boot/setup.elf arch/x86/boot/setup.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,它将会用来把
arch/x86/boot/tools/build arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin arch/x86/boot/zoffset.h arch/x86/boot/bzImage
实际上
Setup is 16268 bytes (padded to 16384 bytes). System is 4704 kB CRC 94a88f9a Kernel: arch/x86/boot/bzImage is ready (#5)
全部结束。
结论
这就是本文的结尾部分。本文我们了解了编译内核的全部步骤:从执行
链接
- GNU make util
- Linux kernel top Makefile
- cross-compilation
- Ctags
- sparse
- bzImage
- uname
- shell
- Kbuild
- binutils
- gcc
- Documentation
- System.map
- Relocation
via: https://github.com/0xAX/linux-insides/blob/master/Misc/how_kernel_compiled.md
0 Comments