前言
在嵌入式Linux的开发过程中,内核编译是一个永远也绕不开的话题。
对内核编译系统的清晰把握,至少可以:
- 了解整个内核的构造
- 节省编译时间
- 在编译报错时快速定位问题
- 进一步了解内核的启动
本文从Linux kernel中.o文件的编译探索kbuild机制。
日期 | 内核版本 | 架构 |
---|---|---|
2022-9-13 | Linux5.4.200 | arm |
实验
目标log展开
我们以page_alloc.o的编译为例开始本次实验。
make mm/page_alloc.o
在kernel根目录下的Makefile中有:
https://elixir.bootlin.com/linux/v5.4.200/source/Makefile#L1733
# Single targets
...
$(build-dirs): prepare
$(Q)$(MAKE) $(build)=$@ \
single-build=$(if $(filter-out $@/, $(single-no-ko)),1) \
need-builtin=1 need-modorder=1
直接看不是很直观,kernel version 2.6的Makefile中语句较为清晰:
%.o: %.c prepare scripts FORCE
$(Q)$(MAKE) $(build)=$(build-dir) $(target-dir)$(notdir $@)
逻辑比较清晰,.o
的文件依赖于同名的.c
文件,然后执行一个命令来生成。
Tip:这里有个小技巧,注释掉构建语句,通过error log来逆向观察语句展开的结构
$(build-dirs): prepare
#$(Q)$(MAKE) $(build)=$@ \
single-build=$(if $(filter-out $@/, $(single-no-ko)),1) \
need-builtin=1 need-modorder=1
再次执行编译命令,则有:
#@make -f ./scripts/Makefile.build obj=mm \
single-build=1 \
need-builtin=1 need-modorder=1
从log可知,执行make mm/page_alloc.o
又调用了一次make,使用script/Makefile.build
这个规则文件,传入的参数是obj=mm。
build
由第一节的log我们知道$(build)
展开后是:
-f ./scripts/Makefile.build obj
在哪定义的呢?全局搜索后,答案是scripts/Kbuild.include
,类似头文件的东西。
https://elixir.bootlin.com/linux/v5.4.200/source/scripts/Kbuild.include#L160
###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj
好,将注意力转移到scripts/Makefile.build
这个文件,寻找和编译.o
相关的语句。
$(obj)/%.o: $(src)/%.c $(recordmcount_source) $$(objtool_dep) FORCE
$(call if_changed_rule,cc_o_c) #调用if_changed_rull这个变量
$(call cmd,force_checksrc) #代码检查
重点关注$(call if_changed_rule,cc_o_c)
,这里调用了if_changed_rull
变量,该变量同样定义在scripts/Kbuild.include
# Usage: $(call if_changed_rule,foo)
# Will check if $(cmd_foo) or any of the prerequisites changed,
# and if so will execute $(rule_foo).
if_changed_rule = $(if $(any-prereq)$(cmd-check),$(rule_$(1)),@:)
这是一条逻辑判断语句,当条件为真,执行逗号之前的动作。当条件为假,则执行后面的@:
。这里的@:
为的是减少一些log输出,具体可以看提交:kernel/git/torvalds/linux.git - Linux kernel source tree
而如果条件为真,rule_$(1)
展开就是rule_cc_o_c
。
好,现在回到scripts/Makefile.build
,寻找rule_cc_o_c
的定义:
define rule_cc_o_c
$(call cmd_and_fixdep,cc_o_c)
$(call cmd,gen_ksymdeps)
$(call cmd,checksrc)
$(call cmd,checkdoc)
$(call cmd,objtool)
$(call cmd,modversions_c)
$(call cmd,record_mcount)
endef
OK,离故事接近真相还差最后一步,$(call cmd_and_fixdep,cc_o_c)
语句是我们重点关注的。同样是call函数,所以在此回到头文件scripts/Kbuild.include
寻找定义:
cmd_and_fixdep = \
$(echo-cmd) $(cmd_$(1)); \
...
根据经验,再次展开就是cmd_cc_o_c
了,它还是在scripts/Makefile.build文件定义。
cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<
终于,真相大白!
总结
.o
文件的编译一共分为如下四个步骤:
Makefile
---------------
1: %.o: %.c
make -f scripts/Makefile.build obj=mm
scripts/Makefile.build
---------------
2: $(obj)/%.o: $(src)/%.c
$(call if_changed_rule,cc_o_c)
scripts/Makefile.build
---------------
3: rule_cc_o_c
$(call cmd_and_fixdep,cc_o_c)
scripts/Makefile.build
---------------
4: cmd_cc_o_c
$(CC) $(c_flags) -c -o $@ $<
这里重点强调下前面反复提到的两个文件:
scripts/Makefile.build
:包含了几乎所有的重要规则scripts/Kbuild.include
:类似于头文件,包含很多重要函数