内核模块编译:Shared Makefile 运行机理

版权声明:本文为博主 "猫科龙" 原创文章,未经博主允许不得转载。 https://blog.csdn.net/maokelong95/article/details/78419516

内核模块编译: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

为了弄清其作用机理,有两个问题需要解决:

  1. 为何 modname 的值被正确设置了,而非空值;
  2. obj-m 是如何传递给内核源码目录的 Makefile 的。

尽管这些问题在事后看来都是很简单的…

1.2. 现有的解释

在我看来,现在的有关内核模块编译的资料有很多问题,总结起来归为如下几种:

  1. 它们完全规避了机理问题,只谈 How,不谈 Why;
  2. 它们是针对整个内核编译过程的,对「内核模块」这个点的分析不够充分;
  3. 它们对内核模块编译的所谓解析十分简单,甚至充满谬误,乃至将 Makefile 第二次执行的时机解释为跳转到内核源码目录执行;
  4. 它们没有基于较新的内核版本,v2.6.0 是较为常见的版本,然而 Linux 对未来的布局只会体现在新内核中,而新内核的 kbuild 系统有所变化,感觉主要是模块化有所增强;
  5. 它们排版不美观,缺乏代码高亮以及层次感;

当然,在我眼中,最重要的原因其实是第五点…

2. 简化的结论

执行用户代码目录下的 makefile (记为 Usr Makefile,UM) 时,UM 将当前路径以变量 M 的值的形式,传递给内核源码目录下的 makefile (记为 Kernel Makefile,KM),并转而执行 KM。

KM 在初始化变量之前,会 include UM。之后,KM 同时满足:

  1. 具备 UM 传递的命令行变量,$(M) 等(若 KM 通过 make -f 执行别的 makefile,则命令行变量 M 也会被别的 makefile 所继承);
  2. 具备 UM 全部上下文,包括变量、条件判断及规则等;
  3. 将重新计算 UM 上下文,包括变量、条件判断等;
  4. 具备执行 UM 逻辑真分支的条件,因为$(KERNELRELEASE) 已被 KM 初始化;
  5. 其中 obj-m 的值被 UM 上下文覆盖

因为条件 1) 和条件 2),问题 1# 得到解决;因为上述全部条件,问题 2# 得到解决。

  1. KM 不是指内核源码目录下的 makefile,而是指内核源码目录中,所有用到的 makefile。正因如此,所以上述括号中的备注其实很重要。具体细节见下述介绍。
  2. 根据,《跟我一起写 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:include scripts/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
  • 文件 makefileexport KBUILD_EXTMOD KBUILD_EXTMOD,此时若再调用别的 makefile,就不必通过传参传值了,只需读取环境变量 KBUILD_CHECKSRCKBUILD_EXTMOD 即可;

    
    # maokelong: @ line 340
    
    export KBUILD_CHECKSRC KBUILD_SRC KBUILD_EXTMOD
    ...
    
    # maokelong: @ line 412
    
    export VERSION PATCHLEVEL SUBLEVEL KERNELRELEASE KERNELVERSION
  • 文件 makefile:定义生成目标 modulesmodules 依赖 module-dirsmodule-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 时生成的两个目标和所需的一个命令,其作用分别为:

    1. crmodverdir:在模块的当前目录下建立一个文件夹 .tmp_versions,并将其内部文件全部删除;
    2. $(objtree)/Module.symvers:检查内核源码根目录下是否存在文件 Module.symvers
    3. $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=$(patsubst _module_%,%,$@)解决问题的核心,将在下面进行介绍。

3.3. 第二跳:Makefile.build 执行流程

  • 文件 $(srctree)/scripts/Makefile.buildinclude 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.buildobj-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 能否

  1. 继承来自命令行的变量,即来自于如下实验中的 make -f test/hell M=$(M) 的变量 M
  2. 继承来自文件的变量,即来自于如下实验中的 C = $(M) 的变量 C

4.2. 目录配置

建立这么一个目录树,其中 testtest2 为目录名,hellfucmakefile 是随便命名的 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

  • [x] 继承来自命令行的变量,即来自于如上实验中的 make -f test/hell M=$(M) 的变量 M
  • [ ] 不能继承来自文件的变量,即来自于如上实验中的 C = $(M) 的变量 C

5. 尾记

做完之后,觉得这几天的调研还是很有意义的,比如我对 makefile 的执行流程的理解又加深了(先获得内容再构建规则再执行),比如我学会了如何阅读和调试 makefile($(warning …)),比如我知道哪怕内核的 kbuild 也不是玄学的(所有机理都有迹可循,不是吗),比如我知道了 make 命令行变量的生命力居然这么强(在获得命令行变量后执行 make -f,该变量会被传递给下一个 makefile)…

不过,限于时间与精力有限,以上工作难免有部分不足:

  1. 对 makefile 的具体处理流程不清楚。网上介绍的寥寥数步实在难以解释一些特殊情况,比如 include $(varName) 为何打破了先 include 再处理变量的流程,比如 export varName 在什么时候执行等;
  2. 未对 obj-m 的具体加工过程进行调研;
  3. 刚买了个暖耳罩子,这篇文章是我煲机的时候写的,因此写得有点飘…

吐槽: CSDN 这谜一样的 markdown 引擎… 哎

猜你喜欢

转载自blog.csdn.net/maokelong95/article/details/78419516