kbuild系统浅析

编译内核的这一套叫做kbuild,是在makefile的基础上,为了方便和统一内核编译搭建的一套自成体系的系统。我们现在从整体结构上来学习以下kbuild系统。

根Makefile的结构

首先我们从根Makefile开始,因为最基本的一切都是从根Makefile开始的。

this-makefile := $(lastword $(MAKEFILE_LIST))
abs_srctree := $(realpath $(dir $(this-makefile)))
abs_output := $(CURDIR)

sub_make_done != 1     // 第一次一定会被执行,用来设置参数
    设置了一些参数,如:
    KBUILD_EXTMOD := $(M)  真正干活的时候,会以这个作区分,执行的操作不一样
    abs_output如果用户指定输出目录,会变化。影响到下面的need_sub_make。
    export sub_make_done := 1
end

// 根据目标输出目录是否是当前目录,判断是否要嵌套执行
ifneq ($(abs_output),$(CURDIR))
need-sub-make := 1
endif

need-sub-make == 1
        // 如果目标输出目录核当前目录不一样,就重新执行一次make
        $(Q)$(MAKE) $(no-print-directory) -C $(abs_output) \
        -f $(abs_srctree)/Makefile $(MAKECMDGOALS)
else

        // 接下来判断是不是要逐个make

        设置下面几个参数,决定这次构建的方式
        config-build     :=
        mixed-build      :=
        need-config      := 1
        may-sync-config  := 1
        single-build     :=

        // 根据MAKECMDGOALS来判断是否需要设置上面的值,来决定接下来做什么共作
        mixed-build == 1
            对$(MAKECMDGOALS)中的目标,依次执行
            make -f $(srctree)Makefile $$i
        else
            // 到这里才开始真正干活

            包含kbuild核心文件,其中定义了build := -f $(srctree)/scripts/Makefile.build obj
            include scripts/Kbuild.include

            // out of tree规则
            ifdef building_out_of_srctree
            endif

            // 目标是不是config文件
            config-build
                构建配置,如make menuconfig
            else
                // 读取配置,看上去和.config一样
                include include/config/auto.conf

                // 如果需要.config但是没有,报错

                // 根据配置include需要的Makefile

                // 先是内核的规则,后是模块的规则
                if !KBUILD_EXTMOD
                    build-dir := .
                else
                endif

                // 如果有single target, 如.o, mm/等
                if single-build
                endif

                $(build-dir): prepare
                	$(Q)$(MAKE) $(build)=$@ need-builtin=1 need-modorder=1 $(single-goals)
            end
        end
end  # need-sub-make

我把根Makefile的骨架子,通过注释的方式列了出来。感觉终于对根Makefile有了点了解。

首先根Makefile会判断以下是否需要重新执行一下make -f Makefile。接下来会根据编译目标,MAKECMDGOALS来区分,包括

  • config-build

  • single-build

  • mixed-build

其中config-build就是生成.config配置的。mixed-build是有混合目标时触发的,如同时有config/clean/真实目标,kbuild会把他们分开,依次执行make。single-build比较特殊,单独划出了一类目标处理。这么一看感觉两千多行的Makefile,也不是那么晦涩了。

Debug小tip

kbuild涉及的文件较多,还有条件判断,这样导致目标的依赖不一定很清楚。在研究kbuild的过程中,常常会陷入代码的汪洋大海,而不知道应该看哪里。下面列举在学习过程中发现的小窍门。

V=1

现在用下来感觉这个选项不错,不会打印太多东西,但是会把每次规则的调用打出来,让你知道中间都用了哪些make来生成。

-p

而make -p可以把整个编译的目标、依赖和命令都输出出来。可以帮助我们理解构建时的具体内容。

比如

make -p modules

可以打印出目标是modules时所有的变量和规则定义。

不过这个输出还是有点大的。。。

获取当前目录

内核在编译时会包含多个makefile,所以根Makefile通过MAKEFILE_LIST来获取当前执行时的目录。

MAKEFILE_LIST保存了读取的所有makefile,最后一个是当前的。通过这种方式来得到当前真实目录。

sub_make判断

这是根Makefile的第一部分,其中我们又可以分成两部分:

  • sub_make_done

  • need-sub-make

主要是解决嵌套执行的。

变量sub_make_done控制了一些变量只要设置一次。设置完后,sub_make_done会设置为1并export,表示以后不会再被运行。

这部分设置的变量比如有:

  • quiet/Q/KBUILD_VERBOSE: 用来控制编译时输出是否简化

  • KBUILD_EXTMOD: 是否用了make M=dir,将dir设置到变量KBUILD_EXTMOD,并export

  • output/abs_output: 编译结果存放的目录/绝对目录,默认是执行make的当前目录

设置完后,根据abs_output和CURDIR的值是否相等设置need-sub-make。如果不相等说明需要嵌套执行make,会重新调用一遍make。如果相等,那接下来才是正经的make工作。

在研究真正的工作前,我们看看sub_make是怎么做的。

其中:

  • -C 表示先进入到对应的目录,再执行make,而且CURDIR被设置为该目录,而不是执行make时的目录

  • -f 使用哪个makefile,这个会出现在MAKEFILE_LIST中

这时我们再来看Makefile开头的变量定义:

结合上面的命令行我们可以得到:

  • 源代码目录由-f传入的Makefile指定

  • 输出结果目录由-C传入的目录指定

当然对输出结果的目录还有一个小插曲,就是可以通过-O选项来指定输出目录。好在这一切都发生在sub_make前,也就是当我们执行这个嵌套make的时候已经决定好了。

几个用到的变量

在sub make阶段,以及刚开始处理的时候,由几个很相似的变量让我困惑。

  • abs_srctree: 这个变量只有在最开头的时候设置过,后面不再变化。它被设置为Makefile所在的目录,一般就是内核源码的根目录

  • abs_output: 这个变量含义时编译输出结果保存到哪个目录。实际上经过sub make后,就一直是CURDIR,也不会再变了

  • srcroot: 如果有M选项,就是这个模块的目录;如果没有就是内核根目录

  • srctree: 如果有M选项,就是内核根目录;如果没有就是srcroot(也是内核根目录?)

single-build

如果命令行中指定了单个要编译的目标,如 xxx.o,那么规则就会走到这里,由这里来处理。

这部分由下面代码来检测:

所以当我们编译单个目标时,就到这里来看规则。值的注意的是make mm/也算是单个目标。

但是这里出现了一个神奇的地方。

这个是一个空规则。。。。所以到底是怎么编译这个单个目标的呢? 玄机隐藏再build-dir中。

在Makefile中有一个针对build-dir的规则,实际上的单个目标是通过这条命令编译的。比如我们要编译mm/mmu_gather.o,展开后是这样。

到这里还没有结束,接下去的深入探索我们放到单个.o文件的编译中。

kbuild

  • Makefile.build文件

  • 常用函数

从Makefile.build开始

在上面我摘出来的根Makefile文件末尾,我保留了一段具体的目标规则的代码。这部分,就是内核编译过程中干活的核心。

在内核编译文件内,我们可以看到很多类似下面的代码:

$(Q)$(MAKE) $(build)=dir

其中build是在Kbuild.include定义的变量。

build := -f $(srctree)/scripts/Makefile.build obj

所以在调用时,真正执行的是

make -f scripts/Makefile.build obj=dir

所以内核编译时,在继承了根Makefile导出的变量设置后,具体的工作交给了每个单独的Makefile.build来完成。

了解了这个调用方式后,就来看看这个Makefile.build的庐山真面目。

从上面的片段看,Makefile.build不是一个人在战斗。还有他的同伴Kbuild.include,Makefile.lib在一起协作。

每次调用Makefile.build时,就会去obj指定的目录下找到Kbuild或者Makefile。而这两个文件中就是按照kbuild定义的目标obj-y/obj-m。如果是目录,就会递归得进入下层目录继续编译。并且因为subdir-ym是默认目标的依赖,所以保证了下层目标会先生成。

层次结构

整个内核代码从kbuild的角度来看,可能是这样的。

每个层级的Kbuild/Makefile定义了obj-y/obj-m,如果需要则先进入下层目录,形成递归。

常用函数

scripts/Kbuild.include

if_changed

内核Makefile中常见的构建方法就是

$(call if_changed,xxx)

这个函数的作用是判断目标的依赖是否有变化,如果有变化,则调用xxx函数进行构建。

来看一下定义

就是当if-changed-cond不是空,就执行cmd_and_savecmd。也就是调用了cmd后,再把构建当前目标的命令保存到一个临时文件里。比如查看.vmlinux.o.cmd,就可以知道在链接vmlinux.o时的具体命令。

接下来就是看这个if-changed-cond是如何判断命令行和依赖是否有更新。

这里判断了依赖和命令行有没有变化。最后这个FORCE的作用还不是很清楚。

Last updated