Kbuild Building System

文中代码基于Linux 5.1 rc6版本

Overview

Kbuild系统终究还是由Makefile构成的,系统主要包括以下部分:

  • Makefile,即根目录下的Makefile
  • .config,即make configmake menuconfig生成的配置文件
  • arch/$(ARCH)/Makefile,该文件会被根Makefile所include
  • scripts/Makefile.*,用于定义Kbuild的规则,它其实是编译时实际用到的Makefile
  • kbuild Makefiles,即各个目录下的Makefile,其文件名可以为Makefile,也可以为Kbuild(e.g. drivers/gpu/drm/nouveau/nvkm/subdev/pci/Kbuild),它其实只是被scripts下的Makefile模板include,因此可以写得很简单

此外还有各级目录下的Kconfig文件,不过它们主要是在用户进行配置(例如make menuconfig)时起一个提示以及约束的作用,另外也提供配置选项的默认值。最终的make过程,只依赖生成的.config文件。

顶层Makefile中定义了一些变量,例如$(MODLIB)定义了modules安装的地址,其默认值为$(INSTALL_MOD_PATH)/lib/modules/$(KERNELRELEASE),其中$(INSTALL_MOD_PATH)需要由用户提供,$(KERNELRELEASE)是一个表示kernel版本号的字符串,相当于uname -r的输出。因此,modules默认的安装地址为/lib/modules/*/

kbuild Makefiles

在kbuild Makefile中,可以定义编译目标obj-y += xxx.o,这将会使xxx.cxxx.S编译为xxx.o。当前目录下的所有obj-y目标会被打包成一个built-in.a,最终链接进vmlinux

这个built-in.a是一个thin archive,没有symbol table,更具体地说,它是由ar rcSTP命令生成的

我们也可以通过obj-m += xxx.o,定义一个kernel module目标,不过这只能生成一个仅由xxx.cxxx.S单个文件构成的module。若要编译由多个文件构成的module,则应使用obj-m += xxx.o xxx-y := aaa.o bbb.o ccc.o这样的形式。

xxx-y非预定义的变量,因此需要初始化。lhs := rhs表示立即求值,而lhs = rhs表示惰性求值。

我们还可以使用lib-ylib-m,一般只用于lib/arch/*/lib目录下,当前目录下的所有lib-ylib-m目标会被打包成一个lib.a文件

一个Makefile只能负责当前目录的编译,其子目录的编译要由子目录中的Makefile负责,我们可以通过obj-y += subdir/obj-m += subdir/来调用子目录的Makefile进行编译。这里的obj-yobj-m没有区别,仅代表该子目录需要编译,具体是编译成module还是编译进内核,由子目录的Makefile决定。

我们可以通过ccflags-yasflags-yldflags-y指定当前目录的编译器、汇编器和链接器flag,这三个变量以前分别叫做EXTRA_CFLAGSEXTRA_AFLGSEXTRA_LDFLAGS,目前仍支持但不推荐这种写法。

可以通过subdir-ccflags-ysubdir-asflags-y指定当前以及所有子目录的编译器和汇编器flag,该条目指定的flag优先级更高。此外,还可以通过CFLAGS_xxx.oAFLAGS_xxx.o的形式,为单独的文件设置编译/链接flag。

如果上述预定义的目标无法满足需求,其实是可以使用通常的Makefile语法进行任意的操作的,这称之为Special Rules。但需要注意Kbuild系统并不是在Makefile所在的目录运行make,因此要:

  • 使用$(src)表示Makefile所在的目录
  • 使用$(obj)表示目标文件所在的目录,它不一定在Makefile所在的目录

Host Program Support

我们还可以利用Kbuild编译一些程序,然后在编译过程中使用这些程序,这个功能称之为Host Program,例如make menuconfig就是先编译了一个程序,再调用它给用户选择编译选项。

使用hostprogs-y := xxx xxx-objs := aaa.o bbb.o ccc.o,即可编译出xxx这个Host Program。若要编译C++程序,可以使用xxx-cxxobjs,实际上也可以混合使用xxx-cxxobjsxxx-objs,编译同时含有C++和C文件的程序。

可以使用HOST_EXTRACFLAGS +=提供额外的编译flag,或用HOSTCFLAGS_xxx.o :=为单个文件提供编译flag。我们还可以用HOSTLDLIBS_xxx :=xxx程序的链接指定flag。

如果在Special Rule中用到了程序xxx,则Kbuild会先编译该程序,否则不会编译。若要始终编译该程序,可以用always := $(hostprogs-y)强制Kbuild进行编译。

Under the hood

实际上顶层的Makefile只是make的总入口点,编译具体的文件时,使用的是scripts/Makefile.*

Simple Object File

首先看一个简单的例子,make xx/xx/xx.o是如何实现的,发现其在Makefile中的定义如下:

1
2
%.o: prepare FORCE
$(Q)$(MAKE) $(build)=$(build-dir) $(build-target)

实际上是再调用了一遍make,这里$(build)定义在scripts/Kbuild.include中:

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

说明这次使用的Makefile为scripts/Makefile.build。在该文件中include了include/config/auto.conf,因此可以访问$(CONFIG_XXX)变量:

1
2
# Read auto.conf if it exists, otherwise ignore
-include include/config/auto.conf

Kconfig会从.config中生成include/config/auto.confinclude/config/tristate.conf,供Makefile使用($(CONFIG_XXX))。另外还会生成include/generated/autoconf.h,供内核中其余.c.h文件使用(#ifdef CONFIG_XXX)。

vmlinux

现在再来看vmlinux的编译:

1
2
3
4
5
6
7
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)))

PHONY += $(vmlinux-dirs)
$(vmlinux-dirs): prepare
$(Q)$(MAKE) $(build)=$@ need-builtin=1

结果仍然是用scripts/Makefile.build进行编译。实际上,每次进入子目录,都会重新调用一遍make,使用的Makefile总是scripts/Makefile.build,它会在开头将该目录中的MakefileKbuild文件include,从而用上了kbuild Makefiles。

1
2
3
4
# 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)

链接vmlinux的过程很简单,就是调用一个shell脚本:

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

在shell脚本中调用vmlinux_link "${kallsymso}" 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
27
28
29
30
31
32
33
34
35
# Link of vmlinux
# ${1} - optional extra .o files
# ${2} - output file
vmlinux_link()
{
local lds="${objtree}/${KBUILD_LDS}"
local objects

if [ "${SRCARCH}" != "um" ]; then
objects="--whole-archive \
${KBUILD_VMLINUX_OBJS} \
--no-whole-archive \
--start-group \
${KBUILD_VMLINUX_LIBS} \
--end-group \
${1}"

${LD} ${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} \
-T ${lds} ${objects}
else
objects="-Wl,--whole-archive \
${KBUILD_VMLINUX_OBJS} \
-Wl,--no-whole-archive \
-Wl,--start-group \
${KBUILD_VMLINUX_LIBS} \
-Wl,--end-group \
${1}"

${CC} ${CFLAGS_vmlinux} -o ${2} \
-Wl,-T,${lds} \
${objects} \
-lutil -lrt -lpthread
rm -f linux
fi
}

最终就是用ld或者cc进行了链接。

bzImage

现在再来看bzImage,也就是vmlinuz的编译。我们以x86为例,其bzImage目标定义在arch/x86/Makefile中:

1
2
3
4
5
6
7
bzImage: vmlinux
ifeq ($(CONFIG_X86_DECODER_SELFTEST),y)
$(Q)$(MAKE) $(build)=arch/x86/tools posttest
endif
$(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/$@

生成完bzImage后,要make install安装内核:

1
2
3
PHONY += install
install:
$(Q)$(MAKE) $(build)=$(boot) $@

显然install最终是定义在arch/x86/boot/Makefile中:

1
2
3
install:
sh $(srctree)/$(src)/install.sh $(KERNELRELEASE) $(obj)/bzImage \
System.map "$(INSTALL_PATH)"

它执行shell脚本cat $2 > $4/vmlinuz; cp $3 $4/System.map,将bzImage改名为了vmlinuz并复制到安装路径。

build image

当然,实际上bzImage也是在arch/x86/boot/Makefile中生成的:

1
2
3
4
5
6
7
8
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`')'

借助arch/x86/boot/tools/build程序,将setup.binvmlinux.bin整合成了一个bzImage文件。build工具的源码位于arch/x86/boot/tools/build.c,是一个很简单的小程序,我们根据源码可以知道它是如何生成bzImage的:

第一步,将setup.bin(包含第一个扇区)拷贝到buf

1
2
3
4
5
6
7
8
9
10
11
12
/* Copy the setup code */
file = fopen(argv[1], "r");
if (!file)
die("Unable to open `%s': %m", argv[1]);
c = fread(buf, 1, sizeof(buf), file);
if (ferror(file))
die("read-error on `setup'");
if (c < 1024)
die("The setup must be at least 1024 bytes");
if (get_unaligned_le16(&buf[510]) != 0xAA55)
die("Boot block hasn't got boot flag (0xAA55)");
fclose(file);

第二步,将vmlinux.binmmap到kernel

1
2
3
4
5
6
7
8
9
10
11
/* Open and stat the kernel file */
fd = open(argv[2], O_RDONLY);
if (fd < 0)
die("Unable to open `%s': %m", argv[2]);
if (fstat(fd, &sb))
die("Unable to stat `%s': %m", argv[2]);
sz = sb.st_size;
printf("System is %d kB\n", (sz+1023)/1024);
kernel = mmap(NULL, sz, PROT_READ, MAP_SHARED, fd, 0);
if (kernel == MAP_FAILED)
die("Unable to mmap '%s': %m", argv[2]);

第三步,将它们写入dest,即bzImage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dest = fopen(argv[4], "w");
if (!dest)
die("Unable to write `%s': %m", argv[4]);

/* ---省略代码若干--- */

crc = partial_crc32(buf, i, crc);
if (fwrite(buf, 1, i, dest) != i)
die("Writing setup failed");

/* Copy the kernel code */
crc = partial_crc32(kernel, sz, crc);
if (fwrite(kernel, 1, sz, dest) != sz)
die("Writing kernel failed");

也就是说仅仅是将setup.binvmlinux.bin拼接了一下,并保证两者分别对齐到512B的扇区。

CONFIG&lowbar;EFI&lowbar;STUB = y,则还需要使用第三个参数zoffset.h中提供的offset,给bzImage中的offset打补丁,以满足EFI的要求,这里我们不考虑这种情况。

setup.bin的由来很简单:

1
2
3
4
5
6
7
LDFLAGS_setup.elf	:= -m elf_i386 -T
$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE
$(call if_changed,ld)

OBJCOPYFLAGS_setup.bin := -O binary
$(obj)/setup.bin: $(obj)/setup.elf FORCE
$(call if_changed,objcopy)

它就是首先编译了一个setup.elf文件,然后用objcopy命令将其转换为了二进制映像,而vmlinux.bin则不那么简单。

vmlinux.bin

arch/x86/boot/Makefile中,产生arch/x86/boot/vmlinux.bin有两个步骤:

1
2
3
4
5
6
$(obj)/compressed/vmlinux: FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@

OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)

显然,第一步中的arch/x86/boot/compressed/vmlinux文件如何产生才是真正的关键,我们来看arch/x86/boot/compressed/Makefile

第一步,从make vmlinux产生的vmlinux文件,生成arch/x86/boot/compressed/vmlinux.bin

1
2
3
OBJCOPYFLAGS_vmlinux.bin :=  -R .comment -S
$(obj)/vmlinux.bin: vmlinux FORCE
$(call if_changed,objcopy)

第二步,压缩vmlinux.bin+vmlinux.reloc,生成arch/x86/boot/compressed/vmlinux.bin.(gz|bz2|lzma|...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
$(obj)/vmlinux.bin.bz2: $(vmlinux.bin.all-y) FORCE
$(call if_changed,bzip2)
$(obj)/vmlinux.bin.lzma: $(vmlinux.bin.all-y) FORCE
$(call if_changed,lzma)
$(obj)/vmlinux.bin.xz: $(vmlinux.bin.all-y) FORCE
$(call if_changed,xzkern)
$(obj)/vmlinux.bin.lzo: $(vmlinux.bin.all-y) FORCE
$(call if_changed,lzo)
$(obj)/vmlinux.bin.lz4: $(vmlinux.bin.all-y) FORCE
$(call if_changed,lz4)

第三步,将压缩后的vmlinux编译进piggy.o

1
2
3
4
5
6
quiet_cmd_mkpiggy = MKPIGGY $@
cmd_mkpiggy = $(obj)/mkpiggy $< > $@

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

这一步的秘诀就在于mkpiggy程序生成的piggy.S中,有一句:

1
.incbin "arch/x86/boot/compressed/vmlinux.bin.(gz|bz2|lzma|...)"

第四步,将解压缩代码和piggy.o一起编译成一个arch/x86/boot/compressed/vmlinux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$(obj)/vmlinux: $(vmlinux-objs-y) FORCE
$(call if_changed,check-and-link-vmlinux)

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-$(CONFIG_ACPI) += $(obj)/acpi.o

$(obj)/eboot.o: KBUILD_CFLAGS += -fshort-wchar -mno-red-zone

vmlinux-objs-$(CONFIG_EFI_STUB) += $(obj)/eboot.o $(obj)/efi_stub_$(BITS).o \
$(objtree)/drivers/firmware/efi/libstub/lib.a
vmlinux-objs-$(CONFIG_EFI_MIXED) += $(obj)/efi_thunk_$(BITS).o

至此,bzImage的来龙去脉就很清晰了,它是由setup.binvmlinux.bin构成,而vmlinux.bin又是由解压缩代码和压缩过的vmlinux构成。

Modules

Phase 1

Kernel Module的编译由两个阶段构成,在顶层Makefile中定义如下,也就是除了最后一句$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost都是第一阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build modules
#
# A module can be listed more than once in obj-m resulting in
# duplicate lines in modules.order files. Those are removed
# using awk while concatenating to the final file.

PHONY += modules
modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin
$(Q)$(AWK) '!x[$$0]++' $(vmlinux-dirs:%=$(objtree)/%/modules.order) > $(objtree)/modules.order
@$(kecho) ' Building modules, stage 2.';
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost

modules.builtin: $(vmlinux-dirs:%=%/modules.builtin)
$(Q)$(AWK) '!x[$$0]++' $^ > $(objtree)/modules.builtin

%/modules.builtin: include/config/auto.conf include/config/tristate.conf
$(Q)$(MAKE) $(modbuiltin)=$*

Step 1

第一阶段做的第一件事,就是将obj-m中定义的模块,编译成.o文件,这一步由$(vmlinux-dirs)这一依赖来完成,与前文编译built-in的文件类似。

Step 1.1

在这一过程中,会在根目录生成modules.order文件,我们考察scripts/Makefile.bulid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ifdef CONFIG_MODULES
modorder-target := $(obj)/modules.order
endif
#
# Rule to create modules.order file
#
# Create commands to either record .ko file or cat modules.order from
# a subdirectory
modorder-cmds = \
$(foreach m, $(modorder), \
$(if $(filter %/modules.order, $m), \
cat $m;, echo kernel/$m;))

$(modorder-target): $(subdir-ym) FORCE
$(Q)(cat /dev/null; $(modorder-cmds)) > $@

其中modorder定义在scripts/Makefile.lib

1
2
3
4
# Determine modorder.
# Unfortunately, we don't have information about ordering between -y
# and -m subdirs. Just put -y's first.
modorder := $(patsubst %/,%/modules.order, $(filter %/, $(obj-y)) $(obj-m:.o=.ko))

也就是说modules.order的内容,就是将要编译为内核模块(obj-m)的module全部列出,其格式为kernel/path-to-module/xxx.ko

Step 1.2

在这一过程中,还会为所有要编译为内核模块(obj-m)的module,在$(MODVERDIR)/下生成xxx.mod(设该模块为xxx.ko)。这里MODVERDIR变量的默认值为.tmp_versions,定义在顶层Makefile

1
2
3
4
# When compiling out-of-tree modules, put MODVERDIR in the module
# tree rather than in the kernel tree. The kernel tree might
# even be read-only.
export MODVERDIR := $(if $(KBUILD_EXTMOD),$(firstword $(KBUILD_EXTMOD))/).tmp_versions

考察scripts/Makefile.build,其中定义了xxx.mod的产生过程:

1
2
3
4
5
6
7
8
9
10
$(single-used-m): $(obj)/%.o: $(src)/%.c $(recordmcount_source) $(objtool_dep) FORCE
$(call cmd,force_checksrc)
$(call if_changed_rule,cc_o_c)
@{ echo $(@:.o=.ko); echo $@; \
$(cmd_undef_syms); } > $(MODVERDIR)/$(@F:.o=.mod)

$(multi-used-m): FORCE
$(call if_changed,link_multi-m)
@{ echo $(@:.o=.ko); echo $(filter %.o,$^); \
$(cmd_undef_syms); } > $(MODVERDIR)/$(@F:.o=.mod)

xxx.mod的第一行为path-to-module/xxx.ko,第二行为它所依赖的.o文件,即它的组成部分。

Step 2

第二件事是在根目录下生成modules.builtin文件,它是递归地使用scripts/Makefile.modbuiltin作为Makefile进行make而生成的:

1
2
3
4
5
6
7
8
9
10
modbuiltin-subdirs := $(patsubst %,%/modules.builtin, $(subdir-ym))
modbuiltin-mods := $(filter %.ko, $(obj-Y:.o=.ko))
modbuiltin-target := $(obj)/modules.builtin

__modbuiltin: $(modbuiltin-target) $(subdir-ym)
@:

$(modbuiltin-target): $(subdir-ym) FORCE
$(Q)(for m in $(modbuiltin-mods); do echo kernel/$$m; done; \
cat /dev/null $(modbuiltin-subdirs)) > $@

结合Makefilescripts/Makefile.modbuiltin的内容可知,modules.builtin文件的内容就是将要编译为bulit-in(obj-y)的module全部列出,其格式为kernel/path-to-module/xxx.ko

Phase 2

第二阶段就是modpost阶段,全部定义在scripts/Makefile.modpost中,注释已经解释得很清楚了:

1
2
3
4
5
6
7
# Stage 2 is handled by this file and does the following
# 1) Find all modules from the files listed in $(MODVERDIR)/
# 2) modpost is then used to
# 3) create one <module>.mod.c file pr. module
# 4) create one Module.symvers file with CRC for all exported symbols
# 5) compile all <module>.mod.c files
# 6) final link of the module to a <module.ko> file

Steps

第一步,在modules变量中存放所有要编译到.ko的module:

1
2
3
4
# Step 1), find all modules listed in $(MODVERDIR)/
MODLISTCMD := find $(MODVERDIR) -name '*.mod' | xargs -r grep -h '\.ko$$' | sort -u
__modules := $(shell $(MODLISTCMD))
modules := $(patsubst %.o,%.ko, $(wildcard $(__modules:.ko=.o)))

第二到第四步,首先确定调用modpost程序使用的参数:

1
2
3
4
5
6
7
8
9
10
11
# Step 2), invoke modpost
# Includes step 3,4
modpost = scripts/mod/modpost \
$(if $(CONFIG_MODVERSIONS),-m) \
$(if $(CONFIG_MODULE_SRCVERSION_ALL),-a,) \
$(if $(KBUILD_EXTMOD),-i,-o) $(kernelsymfile) \
$(if $(KBUILD_EXTMOD),-I $(modulesymfile)) \
$(if $(KBUILD_EXTRA_SYMBOLS), $(patsubst %, -e %,$(KBUILD_EXTRA_SYMBOLS))) \
$(if $(KBUILD_EXTMOD),-o $(modulesymfile)) \
$(if $(CONFIG_SECTION_MISMATCH_WARN_ONLY),,-E) \
$(if $(KBUILD_EXTMOD)$(KBUILD_MODPOST_WARN),-w)

然后,我们调用modpost vmlinux mod1.o mod2.o ...

1
2
3
4
5
6
7
8
9
10
# We can go over command line length here, so be careful.
quiet_cmd_modpost = MODPOST $(words $(filter-out vmlinux FORCE, $^)) modules
cmd_modpost = $(MODLISTCMD) | sed 's/\.ko$$/.o/' | $(modpost) $(MODPOST_OPT) -s -T -

PHONY += __modpost
__modpost: $(modules:.ko=.o) FORCE
$(call cmd,modpost) $(wildcard vmlinux)

# Declare generated files as targets for modpost
$(modules:.ko=.mod.c): __modpost ;

这会在根目录产生一个Module.symvers,包含vmlinux和所有module中导出的变量(EXPORT_SYMBOL/EXPORT_SYMBOL_GPL),以及变量的CRC32值。另外,还会为每个module xxx.o生成一个xxx.mod.c

第五步,将xxx.mod.c编译到xxx.mod.o

1
2
3
4
$(modules:.ko=.mod.o): %.mod.o: %.mod.c FORCE
$(call if_changed_dep,cc_o_c)

targets += $(modules:.ko=.mod.o)

最后一步,将xxx.oxxx.mod.o链接为xxx.ko

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ARCH_POSTLINK := $(wildcard $(srctree)/arch/$(SRCARCH)/Makefile.postlink)

# Step 6), final link of the modules with optional arch pass after final link
quiet_cmd_ld_ko_o = LD [M] $@
cmd_ld_ko_o = \
$(LD) -r $(KBUILD_LDFLAGS) \
$(KBUILD_LDFLAGS_MODULE) $(LDFLAGS_MODULE) \
-o $@ $(real-prereqs) ; \
$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

$(modules): %.ko :%.o %.mod.o FORCE
+$(call if_changed,ld_ko_o)

targets += $(modules)

Modpost

显然,整个module编译过程的关键在于modpost程序,这个程序还是比较复杂的,因为它包含ELF文件解析功能。对vmlinuxxxx.o的解析在函数main-->read_symbols中进行:

1
2
3
4
5
while (optind < argc)
read_symbols(argv[optind++]);

if (files_source)
read_symbols_from_files(files_source);

在这个过程中会将遇到的exported symbol加入全局的哈希表symbolhash,最终在main-->write_dump中遍历symbolhash输出所有symbol到Module.symvers。添加到哈希表这一步是在new_symbol中进行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// read_symbols -->
// handle_modversions -->
// sym_update_crc -->
// new_symbol
// sym_add_exported -->
// new_symbol

/* For the hash of exported symbols */
static struct symbol *new_symbol(const char *name, struct module *module,
enum export export)
{
unsigned int hash;
struct symbol *new;

hash = tdb_hash(name) % SYMBOL_HASH_SIZE;
new = symbolhash[hash] = alloc_symbol(name, 0, symbolhash[hash]);
new->module = module;
new->export = export;
return new;
}

__ksymtab_开头的symbol都会被视为exported symbol:

1
2
3
4
5
/* All exported symbols */
if (strstarts(symname, "__ksymtab_")) {
sym_add_exported(symname + strlen("__ksymtab_"), mod,
export);
}

在完成对输入的解析后,main函数会接着执行一个循环,为所有xxx.o生成xxx.mod.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (mod = modules; mod; mod = mod->next) {
char fname[PATH_MAX];

if (mod->skip)
continue;

buf.pos = 0;

err |= check_modname_len(mod);
err |= check_exports(mod);
add_header(&buf, mod);
add_intree_flag(&buf, !external_module);
add_retpoline(&buf);
add_staging_flag(&buf, mod->name);
err |= add_versions(&buf, mod);
add_depends(&buf, mod);
add_moddevtable(&buf, mod);
add_srcversion(&buf, mod);

sprintf(fname, "%s.mod.c", mod->name);
write_if_changed(&buf, fname);
}

我们以arch/x86/kvm/kvm-intel.mod.c为例来看一下它产生的输出:

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
27
28
29
30
31
32
#include <linux/build-salt.h>
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

BUILD_SALT;

MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);

__visible struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};

MODULE_INFO(intree, "Y");

#ifdef CONFIG_RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif

static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=kvm";

MODULE_ALIAS("cpu:type:x86,ven*fam*mod*:feature:*0085*");

这里产生的__this_module变量就是Kernel Module中常用的THIS_MODULE,其中init_modulecleanup_module就是Kernel Module中用module_initmodule_exit定义的函数。实际上这两个函数在xxx.mod.c中是可选的,如果Kernel Module中没有使用module_init/module_exit,则xxx.mod.c中不会有对应的项:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* handle_modversions() */
if (strcmp(symname, "init_module") == 0)
mod->has_init = 1;
if (strcmp(symname, "cleanup_module") == 0)
mod->has_cleanup = 1;

/* add_header() */
if (mod->has_init)
buf_printf(b, "\t.init = init_module,\n");
if (mod->has_cleanup)
buf_printf(b, "#ifdef CONFIG_MODULE_UNLOAD\n"
"\t.exit = cleanup_module,\n"
"#endif\n");