linux内核编译学习笔记

不打扰是莪最后的温柔 提交于 2019-11-30 22:44:27

 

linux内核由于庞大的代码量和复杂的代码结构,使用通用的makefile形式不仅存在很大的工作量,而且内核的可配置性不好,每次裁剪模块都需要深入到每一层的目录结构修改makefile,并不现实。所以linux提供了一套configure和makefile体系,根据config中的配置操作生成各个子目录下的makefile,决定哪些文件参与编译。内核本身包含了顶层makefile文件,该文件指示了通用的框架。而各个子目录下的makefile文件也不像传统的makefile文件编写格式,它们是结合scripts/目录下的一系列规则文件使用的,只需要指明需要参与编译的子目录/文件即可。在学习了解内核编译体系结构时,因之前基础薄弱,而且对makefile的语法并不熟悉,所以感觉晦涩难懂。后来学习参考同行的博客,对整个脉络有了整体的思路,对makefile体系架构有了更深的了解。

本文重点记录已经学习到的知识,从最终生成的image文件反推,一步一步看如何生成了image文件。首先需要知道,make命令肯定是要执行顶层目录下的makefile文件。

一、makefile文件系统结构

1、scripts/目录下的makefile规则文件

scripts目录下包含makefile规则文件。这些文件相当于制定了一套规则,会解析子目录下的makefile文件。由顶层makefile文件、/sripts子目录下的makefile规则文件和子目录下的makefile文件共同形成了内核的makefile体系。

2、顶层Makefile文件

在顶层目录下,执行make命令生成内核镜像文件,必然是由顶层Makefile作为入口。对于linux来说,顶层Makefile定义的是通用的规则,与体系架构相关的都在各自目录/arch/*/的makefile中定义;顶层Makefile中生成的通用的中间文件是vmlinux,这个文件是将参与编译的所有源码(不包含镜像压缩后的解压缩代码)编译、链接后生成的目标文件;在/arch/arm/和/arch/x86/目录下,定义了由vmlinux生成最终镜像文件的过程,对于arm体系架构来说,要生成的是uImage,对于x86体系架构来说,要生成的内核镜像文件是bzImage。

3、子目录下的makefile文件

子目录下的makefile文件,基本(还是全部?)都是在顶层makfile中嵌套调用的,而且这些makefile文件不是直接执行的,而是通过/scripts/makefile.build文件对其解析后执行。

二、生成vmlinux

在顶层makefile中,直接搜索vmlinux,即可以vmlinux的生成规则。

 按照makefile的编译规则,vmlinux为目标文件,依赖于冒号后面跟的文件。生成由依赖文件生成目标文件的动作由后面的代码实现。

宏定义的部分暂不讨论,重点看call  if_changed,link-vmlinux 一句话。$(call if_changed,link-vmlinux)是调用了if_changed函数,并向其传入了link-vmlinux参数,其实现的功能是调用  cmd_link-vmlinux函数。cmd_link-vmlinux函数的定义,将第一个语句展开:

/bin/bash  scripts/link-vmlinux.sh   ld -m elf_i386 --emit-relocs --build-id           (以i386架构为例)

所以调用了link-vmlinux.sh脚本文件执行,在link-vmlinux.sh中通过编译、链接,最终通过$(vmlinux-deps)变量生成了vmlinux。

 1 # Final link of vmlinux with optional arch pass after final link
 2 cmd_link-vmlinux =                                                 \
 3     $(CONFIG_SHELL) $< $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux) ;    \
 4     $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)
 5 
 6 vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
 7 ifdef CONFIG_HEADERS_CHECK
 8     $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
 9 endif
10 ifdef CONFIG_GDB_SCRIPTS
11     $(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
12 endif
13     +$(call if_changed,link-vmlinux)

1、$(vmlinux-deps)

 作为vmlinux唯一的依赖文件,说明几乎所有内核源码最终都包含在了$(vmlinux-deps)中,那么对这个变量的分析至关重要。这个变量的定义也在顶层makefile中找到。下面结合其代码具体分析。

vmlinux-deps通过KBUILD_VMLINUX-INIT/  MAIN/  LIBS/  LDS变量,最终指向了 $(head-y) $(init-y) $(core-y)  $(libs-y) $(drivers-y)  $(net-y) $(virt-y)变量,以及相应体系架构下的链接文件vmlinux.lds。

# Externally visible symbols (used by link-vmlinux.sh)
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y)
export KBUILD_VMLINUX_LIBS := $(libs-y1)
export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
export LDFLAGS_vmlinux
# used by scripts/package/Makefile
export KBUILD_ALLDIRS := $(sort $(filter-out arch/%,$(vmlinux-alldirs)) arch Documentation include samples scripts tools)

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

由$(head-y)  $(virt-y)等变量的名字可以大概看出,它们是按照内核的通用分类规则定义的,包含了内核的各个组成部分。core-y定义了内核的核心内容,包括kernel、mm、fs、ipc、usr等目录下的内容,drivers-y定义了驱动相关的driver目录下的内容,net-y定义了网络相关的net目录下的内容。他们都是在顶层Makefile中定义的。

ifeq ($(KBUILD_EXTMOD),)
# Objects we will link into vmlinux / subdirs we need to visit
init-y        := init/
drivers-y    := drivers/ sound/ firmware/
net-y        := net/
libs-y        := lib/
core-y        := usr/
virt-y        := virt/

core-y        += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
endif # KBUILD_EXTMOD

在顶层Makefile中是没有定义$(head-y)的,因为这是与体系结构相关的内容。所以是在顶层makefile中包含的/arch/x86/Makefile文件定义的。另外,处理器架构相关的makefile文件中,还将处理器架构自有的内容补充到了相应的变量中。详见如下代码。

head-y := arch/x86/kernel/head_$(BITS).o
head-y += arch/x86/kernel/head$(BITS).o
head-y += arch/x86/kernel/ebda.o
head-y += arch/x86/kernel/platform-quirks.o

libs-y  += arch/x86/lib/

# See arch/x86/Kbuild for content of core part of the kernel
core-y += arch/x86/

# drivers-y are linked after core-y
drivers-$(CONFIG_MATH_EMULATION) += arch/x86/math-emu/
drivers-$(CONFIG_PCI)            += arch/x86/pci/

# must be linked after kernel/
drivers-$(CONFIG_OPROFILE) += arch/x86/oprofile/

# suspend and hibernation support
drivers-$(CONFIG_PM) += arch/x86/power/

drivers-$(CONFIG_FB) += arch/x86/video/

  

三、生成bzImage

在顶层Makefile中,包含对应体系架构的makefile,执行make命令也会自动执行包含的makefile。

include  arch/$(SRCARCH)/Makefile

在arch/x86/Makefile中,有如下代码:

  

1 bzImage: vmlinux
2 ifeq ($(CONFIG_X86_DECODER_SELFTEST),y)
3     $(Q)$(MAKE) $(build)=arch/x86/tools posttest
4 endif
5     $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)
6     $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot
7     $(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@

当我们输入make bzImage时,编译器就会找到该文件中对bzImage的定义。显然,bzImage依赖于vmlinux文件。通过第一个语句make $(build)=$(boot) $(KBUILD_IMAGE),生成/arch/x86/boot/bzImage.   后两句是创建/arch/i386/boot/bzImage或者/arch/x64/boot/bzImage链接到生成的bzImage。

解析第一句的语法,其中$(build)的定义为:

/scripts/build.include文件: 

###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

  展开第一个语句,即make    -f    $(srctree)/scripts/Makefile.build       dir=.     obj=arch/x86/boot      arch/x86/boot/bzImage

make命令的参数-f是用于指定专门的makefile文件,-f指定了使用的makefile文件为scripts目录下的makefile.build,   然后传入了dir和obj两个参数,  生成的是/arch/x86/boot下的zImage。

makefile.build文件制定了内核编译体系的规则,几乎子目录下的所有makefile都是通过makefile.build文件解析后执行。可以简单看一下Makefile.build文件:

1)在其中包含了$(obj)路径下的Makefile,(src在最开始被赋值为obj,kbuild-file为src目录下的Kbuild文件或者Makefile文件)

# The filename Kbuild has precedence over Makefile
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)

2)递归调用$(obj)路径下的子目录 

subdir-ym为子目录,同样采用$build参数,再次makefile.build,只不过现在向其传入的obj是当前obj目录下的子目录;

PHONY += $(subdir-ym)
$(subdir-ym):
    $(Q)$(MAKE) $(build)=$@ need-builtin=$(if $(findstring $@,$(subdir-obj-y)),1) 

 没有指明生成的目标文件的情况,默认生成makefile文件(makefile.build)中定义的第一个目标文件,即__build。__build依赖于我们的$(builtin-target)  $(lib-target)  $(extra-y)变量。其中$(builtin-target)变量定义的就是我们的当前目录下的/built-in.a。

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
     $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
     $(subdir-ym) $(always)

$(builtin-target)是依赖于$(real-obj-y)变量生成的,call if_changed函数相当于调用cmd_ar_builtin。cmd_ar_builtin函数删除了原目标文件,将$(real-obj-y)链接生成了$(obj)/built-in.a。

quiet_cmd_ar_builtin = AR $@
cmd_ar_builtin = rm -f $@; \
$(AR) rcSTP$(KBUILD_ARFLAGS) $@ $(filter $(real-obj-y), $^)

$(builtin-target): $(real-obj-y) FORCE
$(call if_changed,ar_builtin)

$(real-obj-y)是在Makefile.build包含的Makefile.lib文件中定义的。obj-y最开始是在当前的$(obj)目录的Makefile中配置的,参与编译的文件和目录会添加到obj-y中。第一行代码是将obj-y中的目录(最后一个字符是/)添加built-in.a;第二行代码没太看懂????感觉是对$(obj)目录下的文件进行处理。比如abc.c定义为 abc.c: .o = -objs。第三行代码是在前面添加绝对路径。

所以$(real-obj-y) = 子目录下的built-in.a + 当前目录下的.o

obj-y        := $(patsubst %/, %/built-in.a, $(obj-y))

real-obj-y := $(foreach m, $(obj-y), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))),$($(m:.o=-objs)) $($(m:.o=-y)),$(m)))
  real-obj-y := $(addprefix $(obj)/,$(real-obj-y))

  子目录下的built-in.a又是在本次Makefile.build中嵌套调用 makefile.build 传入子目录的obj参数后生成的。这样层层嵌套调用,直到最底层目录。通过makefile.build的子目录嵌套调用机制,将Linux的层级目录统统编译到内核镜像文件中。

 前面说到,arch/x86/Makefile中执行Makefile.build传入boot目录这一行代码,定义了生成的目标文件是/arch/x86/boot/bzImage,所以就需要看在哪里定义了bzImage的生成规则。显然不会在通用规则文件里,那么就只能是在的/arch/x86/boot目录(传入的$obj参数)的makefile文件中了。

此处的$(obj)即是/arch/x86/boot目录。该目录下的目标文件是bzImage,依赖于setup.bin  vmlinux.bin 和 tools/build工具,调用cmd_image函数生成目标文件bzImage。cmd_image中调用tools/build工具,根据 /arch/x86/boot/目录下的setup.bin   vmlinux.bin 和 zoffset.h 生成bzImge。

quiet_cmd_image = BUILD   $@
silent_redirect_image = >/dev/null
cmd_image = $(obj)/tools/build $(obj)/setup.bin $(obj)/vmlinux.bin \
                   $(obj)/zoffset.h $@ $($(quiet)redirect_image)

$(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE
    $(call if_changed,image)
    @$(kecho) 'Kernel: $@ is ready' ' (#'`cat .version`')'

 那接下来就要依次去看这几个文件是如何生成的?

首先是核心的内核文件vmlinux.bin,它是由$(obj)/compressed/vmlinux文件生成的。call  if_changed,objcopy  调用了cmd_objcopy函数,该函数在Makefile.lib中定义,就是调用了GNU的objcopy工具,将$(obj)/compressed目录下的vmlinux生成了$(obj)/vmlinux.bin。

$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
    $(call if_changed,objcopy)

arch/x86/boot/compressed/vmlinux如何生成?自然是去compressed目录下的Makefile。在Makefile.build的规则中已经提到,通过Makefile.build传入/arch/x86/boot路径后,会自动include /arch/x86/boot路径下的Makefile,同时对/arch/x86/boot路径下的子目录执行Makefile.build脚本,自然会处理子目录compressed下的makefile文件。

顾名思义,compressed文件夹是定义了内核文件压缩的相关内容。所以compressed目录下必然会有对内核文件的压缩动作、添加解压缩的相关代码等工作。compressed目录下的vmlinux文件是依赖于$(vmlinux-objs-y)变量的,$(vmlinux-objs-y)变量包含的文件链接生成了vmlinux。

# We need to run two commands under "if_changed", so merge them into a
# single invocation.
quiet_cmd_check-and-link-vmlinux = LD      $@
      cmd_check-and-link-vmlinux = $(cmd_check_data_rel); $(cmd_ld)

$(obj)/vmlinux: $(vmlinux-objs-y) FORCE
    $(call if_changed,check-and-link-vmlinux)

$(vmlinux-objs-y)变量是在当前的Makefile定义的,下面的代码为$(vmlinux-objs-y)的一部分内容,会根据CONFIG配置来决定是否包含某些文件。

vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \
    $(obj)/string.o $(obj)/cmdline.o $(obj)/error.o \
    $(obj)/piggy.o $(obj)/cpuflags.o

vmlinux-objs-$(CONFIG_EARLY_PRINTK) += $(obj)/early_serial_console.o
vmlinux-objs-$(CONFIG_RANDOMIZE_BASE) += $(obj)/kaslr.o
ifdef CONFIG_X86_64
    vmlinux-objs-$(CONFIG_RANDOMIZE_BASE) += $(obj)/kaslr_64.o
    vmlinux-objs-y += $(obj)/mem_encrypt.o
    vmlinux-objs-y += $(obj)/pgtable_64.o
endif

正常来说,$(vmlinux-objs-y)肯定是需要包含根目录下通用makefile生成的vmlinux文件的,但是并没有看到根目录下vmlinux的显示定义。为什么呢????其实,根目录下的vmlinux是隐式包含在了piggy.o中。而piggy.s也很神奇,它并不是compressed文件夹下自带的文件,在下载的内核源码中是找不到piggy.s文件的,它是在Makefile在编译过程中动态生成的。在compressed/Makefile的末尾,定义了piggy.s文件:

quiet_cmd_mkpiggy = MKPIGGY $@
      cmd_mkpiggy = $(obj)/mkpiggy $< > $@ || ( rm -f $@ ; false )

targets += piggy.S
$(obj)/piggy.S: $(obj)/vmlinux.bin.$(suffix-y) $(obj)/mkpiggy FORCE
    $(call if_changed,mkpiggy)

可见,piggy.s文件依赖于arch/x86/boot/compressed/vmlinux.bin.$(suffix-y)文件,通过cmd_mkpiggy函数,展开该函数如下

/arch/x86/boot/compressed/mkpiggy  /arch/x86/boot/compressed/vmlinux.bin.$(suffix-y)  >  piggy.S  || (rm -f piggy.S ; false)

其中$(suffix-y)是通过config定义的压缩形式,包括gz/ bz2/ xz等,此处以gz为例;

suffix-$(CONFIG_KERNEL_GZIP)	:= gz

所以是调用mkpiggy程序,传入了/arch/x86/boot/compressed/vmlinux.bin.gz)参数,打印消息生成了piggy.S。而mkpiggy也是在make过程中由源码文件mkpiggy.c生成的,mkpiggy.c中的打印消息,就是生成的piggy.S内容。

其中的.incbin  "arch/x86/boot/compressed/vmlinux.bin.gz"就是采用包含二进制的方式,直接添加vmlinux.bin.gz文件。也就是在生成的piggy.o中会包含vmlinux.bin.gz的全部内容。

    printf(".section \".rodata..compressed\",\"a\",@progbits\n");
    printf(".globl z_input_len\n");
    printf("z_input_len = %lu\n", ilen);
    printf(".globl z_output_len\n");
    printf("z_output_len = %lu\n", (unsigned long)olen);

    printf(".globl input_data, input_data_end\n");
    printf("input_data:\n");
    printf(".incbin \"%s\"\n", argv[1]);
    printf("input_data_end:\n");

那么再看如何生成了arch/x86/boot/compressed/vmlinux.bin.gz文件,同样在compressed目录的Makefile中给出了定义。就是调用gzip工具,将arch/x86/boot/compressed/vmlinux.bin压缩成了vmlinux.bin.gz。

vmlinux.bin.all-y := $(obj)/vmlinux.bin
vmlinux.bin.all-$(CONFIG_X86_NEED_RELOCS) += $(obj)/vmlinux.relocs

$(obj)/vmlinux.bin.gz: $(vmlinux.bin.all-y) FORCE
    $(call if_changed,gzip)

那么arch/x86/boot/compressed/vmlinux.bin是如何生成呢,也是在compressed目录的Makefile中定义。利用GNU的objcopy工具处理vmlinux文件,生成了$(obj)/vmlinux.bin文件。注意,vmlinux是没有添加目录前缀的,是顶层目录下的vmlinux。

$(obj)/vmlinux.bin: vmlinux FORCE
	$(call if_changed,objcopy)

  至此,从顶层目录vmlinux到bzImage的创建过程分析完成。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!