内核模块编译:Shared Makefile 运行机理
文章目录
1. 引言
1.1. 我想解决的问题
作为练手,我想写这样一个 Makefile,其能自动编译当前目录下所有 .C
文件,并生成一个以当前目录名为名的内核模块。
在经过一番尝试后,以某常见的 Shared Makefile 为蓝本,我改编出了这样一个很奇怪的 Makefile:
modname ?= $(shell basename $(M))
srclist ?= $(shell ls $(M) | grep "\.c")
objlist ?= $(srclist:.c=.o)
#headerdir ?= $(wildcard include)
#==========================================================
ifneq ($(KERNELRELEASE),)
# kbuild part of makefile
obj-m := $(modname).o
#ccflags-y := -I$(headerdir)
$(modname)-y := $(objlist)
else
# normal makefile
KDIR ?= /lib/modules/$(shell uname -r)/build
default:
$(MAKE) -C $(KDIR) M=$$PWD
clean:
$(MAKE) -C $(KDIR) M=$$PWD clean
.PHONY = default install clean
endif
为了弄清其作用机理,有两个问题需要解决:
- 为何 modname 的值被正确设置了,而非空值;
- obj-m 是如何传递给内核源码目录的 Makefile 的。
尽管这些问题在事后看来都是很简单的…
1.2. 现有的解释
在我看来,现在的有关内核模块编译的资料有很多问题,总结起来归为如下几种:
- 它们完全规避了机理问题,只谈 How,不谈 Why;
- 它们是针对整个内核编译过程的,对「内核模块」这个点的分析不够充分;
- 它们对内核模块编译的所谓解析十分简单,甚至充满谬误,乃至将 Makefile 第二次执行的时机解释为跳转到内核源码目录执行;
- 它们没有基于较新的内核版本,v2.6.0 是较为常见的版本,然而 Linux 对未来的布局只会体现在新内核中,而新内核的 kbuild 系统有所变化,感觉主要是模块化有所增强;
- 它们排版不美观,缺乏代码高亮以及层次感;
当然,在我眼中,最重要的原因其实是第五点…
2. 简化的结论
执行用户代码目录下的 makefile (记为 Usr Makefile,UM) 时,UM 将当前路径以变量 M
的值的形式,传递给内核源码目录下的 makefile (记为 Kernel Makefile,KM),并转而执行 KM。
KM 在初始化变量之前,会 include UM。之后,KM 同时满足:
- 具备 UM 传递的命令行变量,
$(M)
等(若 KM 通过make -f
执行别的 makefile,则命令行变量M
也会被别的 makefile 所继承); - 具备 UM 全部上下文,包括变量、条件判断及规则等;
- 将重新计算 UM 上下文,包括变量、条件判断等;
- 具备执行 UM 逻辑真分支的条件,因为
$(KERNELRELEASE)
已被 KM 初始化; - 其中
obj-m
的值被 UM 上下文覆盖
因为条件 1) 和条件 2),问题 1# 得到解决;因为上述全部条件,问题 2# 得到解决。
注:
- KM 不是指内核源码根目录下的 makefile,而是指内核源码目录中,所有用到的 makefile。正因如此,所以上述括号中的备注其实很重要。具体细节见下述介绍。
- 根据,《跟我一起写 Makefile(三)》,Makefile 会先读入所有的 Makefile,然后读入所有被 include 的 Makefile,再初始化文件中的变量。
3. Shared Makefile 详细执行过程
3.1. 用户代码目录执行过程
-
初始化变量,显然上下文中不存在变量
$(KERNELRELEASE)
,因此将执行为$(KIDR)
赋值的分支;ifneq ($(KERNELRELEASE),) ... else KDIR ?= /lib/modules/$(shell uname -r)/build
-
根据生成命令生成伪目标
default
;其中$(MAKE) -C $(KDIR) M=$$PWD
表示将当前路径以变量 M 的值的形式,传递给内核源码根目录下的 makefile,并执行该 makefile。default: $(MAKE) -C $(KDIR) M=$$PWD rm -rf modules.order .tmp_versions *.mod* *.o *.o.cmd .*.cmd
3.2. 第一跳:内核源码目录执行过程
注:以下分析系基于 Linux-4.4.92 内核版本。限于篇幅和个人精力,我只摘取了与外部模块编译有关的、且较为重要的步骤。
-
文件
makefile
:includescripts/Kbuild.include
。该文件只是被 include 进去,其中的变量将在稍后统一初始化;# maokelong: @ line 344 include scripts/Kbuild.include
-
文件
makefile
:判定 M 变量源自命令行,将KBUILD_EXTMOD
置为 M 的值;从下面开始,将使用KBUILD_EXTMOD
代替M
;# maokelong: @ line 190 ifeq ("$(origin M)", "command line") KBUILD_EXTMOD := $(M) endif
-
文件
scripts/Kbuild.include
:定义了一个重要的变量$(build)
,通过该变量,开发者仅需通过使用$(Q)$(MAKE) $(build)=dir
便可以完成等同于$(Q)$(MAKE) -f scripts/Makefile.build obj=dir
的功能:# maokelong: @ line 175 build := -f $(srctree)/scripts/Makefile.build obj
-
文件
makefile
:_all
(是第一项伪目标,也即缺省伪目标)依赖module
;# maokelong: @ line 197 ifeq ($(KBUILD_EXTMOD),) _all: all else _all: modules endif
-
文件
makefile
:export KBUILD_EXTMOD KBUILD_EXTMOD
,此时若再调用别的 makefile,就不必通过传参传值了,只需读取环境变量KBUILD_CHECKSRC
和KBUILD_EXTMOD
即可;# maokelong: @ line 340 export KBUILD_CHECKSRC KBUILD_SRC KBUILD_EXTMOD ... # maokelong: @ line 412 export VERSION PATCHLEVEL SUBLEVEL KERNELRELEASE KERNELVERSION
-
文件
makefile
:定义生成目标modules
,modules
依赖module-dirs
,module-dirs
又依赖于crmodverdir
以及$(objtree)/Module.symvers
,根据前文对$(build)
的解释,生成该依赖项的时候还会执行相当于这样的命令:$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=$(patsubst _module_%,%,$@)
。# maokelong: @ line 902 ifeq ($(KBUILD_EXTMOD),) ... # maokelong: @ line 1369 else # KBUILD_EXTMOD ... ## maokelong: @ line 1402 module-dirs := $(addprefix _module_,$(KBUILD_EXTMOD)) PHONY += $(module-dirs) modules $(module-dirs): crmodverdir $(objtree)/Module.symvers $(Q)$(MAKE) $(build)=$(patsubst _module_%,%,$@) modules: $(module-dirs) @$(kecho) ' Building modules, stage 2.'; $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost ... # maokelong: @ line 1446 endif # KBUILD_EXTMOD
生成
module-dirs
时生成的两个目标和所需的一个命令,其作用分别为:crmodverdir
:在模块的当前目录下建立一个文件夹.tmp_versions
,并将其内部文件全部删除;$(objtree)/Module.symvers
:检查内核源码根目录下是否存在文件Module.symvers
;$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=$(patsubst _module_%,%,$@)
:解决问题的核心,将在下面进行介绍。
3.3. 第二跳:Makefile.build 执行流程
-
文件
$(srctree)/scripts/Makefile.build
:include UM;# maokelong: @ line 41 # 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)
-
文件
$(srctree)/scripts/Makefile.build
:obj-m
原始定义处,obj-m
将在变量初始化过程中被 UM 中的obj-m
覆盖。在计算obj-m
的时候,UM 上下文继承了命令行变量M
(源于$(MAKE) -C $(KDIR) M=$$PWD
,这是经下节的小实验证实的),并据此计算出我所期望的模块名modname ?= $(shell basename $(M))
,obj-m
的值就此计算完毕:obj-m := $(modname).o
。# maokelong: @ line 10 # Init all relevant variables used in kbuild files so # 1) they have correct type # 2) they do not inherit any value from the environment obj-y := obj-m :=
-
略…
4. 探究实验
4.1. 实验目的
探究 make -f
所执行的 makefile 能否
- 继承来自命令行的变量,即来自于如下实验中的
make -f test/hell M=$(M)
的变量M
- 继承来自文件的变量,即来自于如下实验中的
C = $(M)
的变量C
4.2. 目录配置
建立这么一个目录树,其中 test
和 test2
为目录名,hell
、fuc
和 makefile
是随便命名的 makefile。
+--test
| |
| +--hell
|
+--test2
| |
| +--fuc
|
+--makefile
4.3. 文件配置
# makefile
M = $(shell pwd)
C = $(M)
default:
@echo $(C)
make -f test/hell M=$(M)
# hell
default:
@echo hell
@echo $(C)
@echo $(M)
make -f test2/fuc
# fuc
default:
@echo fuc
@echo $(M)
4.4. 实验结果及说明
/mnt/d/pwd/maketest
make -f test/hell M=/mnt/d/pwd/maketest
make[1]: Entering directory '/mnt/d/pwd/maketest'
hell
/mnt/d/pwd/maketest
make -f test2/fuc
make[2]: Entering directory '/mnt/d/pwd/maketest'
fuc
/mnt/d/pwd/maketest
make[2]: Leaving directory '/mnt/d/pwd/maketest'
make[1]: Leaving directory '/mnt/d/pwd/maketest'
所有变量 M
均顺利打印;C
仅在定义其的文件中打印成功。
实验结果说明 make -f
所执行的 makefile
- 能继承来自命令行的变量,即来自于如上实验中的
make -f test/hell M=$(M)
的变量M
- 不能继承来自文件的变量,即来自于如上实验中的
C = $(M)
的变量C
5. 尾记
做完之后,觉得这几天的调研还是很有意义的,比如我对 makefile 的执行流程的理解又加深了(先获得内容再构建规则再执行),比如我学会了如何阅读和调试 makefile($(warning …)),比如我知道哪怕内核的 kbuild 也不是玄学的(所有机理都有迹可循,不是吗),比如我知道了 make 命令行变量的生命力居然这么强(在获得命令行变量后执行 make -f,该变量会被传递给下一个 makefile)…
不过,限于时间与精力有限,以上工作难免有部分不足:
- 对 makefile 的具体处理流程不清楚。网上介绍的寥寥数步实在难以解释一些特殊情况,比如
include $(varName)
为何打破了先 include 再处理变量的流程,比如export varName
在什么时候执行等; - 未对
obj-m
的具体加工过程进行调研; - 刚买了个暖耳罩子,这篇文章是我煲机的时候写的,因此写得有点飘…
吐槽: CSDN 这谜一样的 markdown 引擎… 哎