vmlinux是如何被编译出来的?

前言

如何编译Linux内核源码?| 量子孤岛一文中提到了,vmlinux是原始的,未经压缩的内核可执行文件,即kernel编出来的原始产物。

本文的主题只有一个:vmlinux到底是怎么生成的?

日期 内核版本 架构
2022-9-14 Linux5.4.200 ARM64

按图索骥-内核子系统编译

kernel中编译的起点是根目录下的Makefile。

https://elixir.bootlin.com/linux/v5.4.200/source/Makefile#L1099

1
2
3
4
5
6
7
8
9
# Final link of vmlinux with optional arch pass after final link
cmd_link-vmlinux = \
$(CONFIG_SHELL) $< $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux) ; \
$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

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

targets := vmlinux

由脚本可以看出,产物vmlinux的scripts/link-vmlinux.sh有两个依赖:

autoksyms_recursive:初步看可是ko有关的语法,暂时跳过。

1
2
3
4
5
ifdef CONFIG_TRIM_UNUSED_KSYMS
autoksyms_recursive: descend modules.order
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/adjust_autoksyms.sh \
"$(MAKE) -f $(srctree)/Makefile vmlinux"
endif

vmlinux-deps重点看。

1
vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_OBJS) $(KBUILD_VMLINUX_LIBS)

依次展开看。

KBUILD_LDS

1
export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds

vmlinux.lds是非常重要的链接文件,打算后面另起一文详细讲,这里暂不展开。

KBUILD_VMLINUX_OBJS

1
2
3
# Externally visible symbols (used by link-vmlinux.sh)
export KBUILD_VMLINUX_OBJS := $(head-y) $(init-y) $(core-y) $(libs-y2) \
$(drivers-y) $(net-y) $(virt-y)

这里岛主对比了2.6版本的Makefile写法,5.4版本删掉了原来的KBUILD_VMLINUX_INIT:

1
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)

而直接把$(head-y) $(init-y)追加到了KBUILD_VMLINUX_OBJS后面。

为了一探究竟,我们再依次展开KBUILD_VMLINUX_OBJS的各个组成部分.

head-y:与架构相关,arm64架构定义在arch/arm64/Makefile

https://elixir.bootlin.com/linux/v5.4.200/source/arch/arm64/Makefile#L107

1
2
# Default value
head-y := arch/arm64/kernel/head.o

init-y

https://elixir.bootlin.com/linux/v5.4.200/source/Makefile#L637

1
2
3
4
ifeq ($(KBUILD_EXTMOD),)
# Objects we will link into vmlinux / subdirs we need to visit
init-y := init/
init-y := $(patsubst %/, %/built-in.a, $(init-y))

按照语句执行顺序,这里init-y展开后为init/built-in.o

继续展开剩余的:

core-y

1
2
3
core-y		:= usr/
core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
core-y := $(patsubst %/, %/built-in.a, $(core-y))

libs-y2

1
2
3
libs-y		:= lib/
libs-y1 := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2 := $(patsubst %/, %/built-in.a, $(filter-out %.a, $(libs-y)))

drivers-y

1
2
drivers-y	:= drivers/ sound/
drivers-y := $(patsubst %/, %/built-in.a, $(drivers-y))

net-y

1
2
net-y		:= net/
net-y := $(patsubst %/, %/built-in.a, $(net-y))

virt-y

1
2
virt-y      := virt/
virt-y := $(patsubst %/, %/built-in.o, $(virt-y))

通过观察不难发现:几乎所有的依赖条件中,都会生成一个built-in.o文件.

KBUILD_VMLINUX_LIBS

1
2
3
4
5
export KBUILD_VMLINUX_LIBS := $(libs-y1)
=>
libs-y1 := $(patsubst %/, %/lib.a, $(libs-y))
=>
libs-y := lib/

依次展开后也不难发现,调用了Linux中的各个子系统的根目录,想必vmlinux也是这些根目标链接而成的。

备注:

简单回忆一下patsubst.查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换

1
$(patsubst <pattern>,<replacement>,<text> )

我们再回到根目录下的Makefile中,target:vmlinux后面还有一些重要信息需要澄清:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
targets := vmlinux

# The actual objects are generated when descending,
# make sure no implicit rule kicks in
$(sort $(vmlinux-deps)): descend ;

...
descend: $(build-dirs)
...
build-dirs := $(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) $(virt-y)))

# Handle descending into subdirectories listed in $(build-dirs)
# Preset locale variables to speed up the build process. Limit locale
# tweaks to this spot to avoid wrong language settings when running
# make menuconfig etc.
# Error messages still appears in the original language
PHONY += descend $(build-dirs)
descend: $(build-dirs)
$(build-dirs): prepare
$(Q)$(MAKE) $(build)=$@ \
single-build=$(if $(filter-out $@/, $(single-no-ko)),1) \
need-builtin=1 need-modorder=1

我们来慢慢分析。在vmlinux-deps中的变量,都依赖于vmlinux-dirs变量的值。而vmlinux-dirs展开是众多的子目录,其目标规则最终定定位到$(Q)$(MAKE) $(build)=$@。按照之前我们的已经掌握的先验知识,在scripts/Kbuild.include展开后即

1
make -f scripts/Makefile.build obj=$@

即依次回到各个子目录执行编译。

粘合剂

上面详细展开了内核子系统的编译流程。当各个子模块都编译完成,这时就需要工具将它们全部粘合起来制作成vmlinux

开篇的第一行代码还没聊,这回该它登场了!

1
2
3
4
# Final link of vmlinux with optional arch pass after final link
cmd_link-vmlinux = \
$(CONFIG_SHELL) $< $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux) ; \
$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

$<表示依赖关系中的第一个依赖,即scripts/link-vmlinux.sh,这个脚本完成后续的链接工作。

来吧,看看scripts/link-vmlinux.sh

1
2
3
4
5
#link vmlinux.o
info LD vmlinux.o
modpost_link vmlinux.o
...
vmlinux_link vmlinux "${kallsymso}" ${btf_vmlinux_bin_o}

vmlinux_link是一个link函数,其中:

1
2
3
4
${LD} ${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux}	\
${strip_debug#-Wl,} \
-o ${output} \
-T ${lds} ${objects}

这里ld的objects就是前面分析的vmlinux-deps.

总结

用一张图来汇总上面的分析。

x3SX59.png

-------------本文结束感谢您的阅读-------------

本文标题:vmlinux是如何被编译出来的?

文章作者:孤岛violet

发布时间:2022年09月13日 - 22:33

最后更新:2022年10月06日 - 22:37

原始链接:http://yoursite.com/2022/09/13/vmlinux/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

undefined