1. make 和 makefile

        make是linux下的一个程序软件,makefile相当于针对make程序的配置文件,当我们执行make命令时,make将会在当前目录寻找Makefile文件,然后根据Makefile的配置对源文件进行编译。

        linux内核源代码的编译也是使用make工具和makefile,但是它在普通的C程序编译的基础上对配置和编译选项进行了扩展,这就是kbuild系统,专门用于linux的内核编译,使得linux内核的编译更加简洁而高效。

2. makefile目录层次关系的处理

        一个makefile只负责处理本目录中的编译关系,其他目录中的文件编译由其他目录的makefile负责,整个linux内核的makefile组成一个树状结构。对于上层makefile的子目录而言,只需要让kbuild知道它应该怎样进行递归地进入目录即可。

        kbuild利用目录指定的方式来进行目录指定操作,举个例子:

        obj-$(CONFIG_FOO) += foo/  

        当CONFIG_FOO被配置成y或者m时,kbuild就会进入到foo/目录中,但是需要注意的是,这个信息仅仅是告诉kbuild应该进入到哪个目录,而不对其目录中的编译做任何指导。

3. kbuild系统

        在linux中,由于内核代码的分层模型,以及兼容很多平台的特性,makefile文件分布在各个目录中,对每个模块进行分离编译,降低耦合性,使编译方式更加灵活。
makefile主要是以下五个部分:

        1)顶层makefile : 在源代码的根目录有个顶层makefile,顶层makefile的作用就是负责生成两个最重要的部分:编译生成vmlinux和各种模块。

        2).config文件 : 这个config文件主要是产生自用户对内核模块的配置,有三种配置方式:

                编译进内核

                编译成可加载模块

                不进行编译。

        3)arch/$(ARCH)/Makefile : 从目录可以看出,这个makefile主要是根据指定的平台对内核镜像进行相应的配置,提供平台信息给顶层makefile。

        4)scirpts/makefile. * : 这些makefile配置文件包含了构建内核的规则。

        5)kbuild makefiles : 每一个模块都是单独被编译然后再链接的,所以这一种kbiuld makefile几乎在每个模块中都存在。在这些模块文件(子目录)中,也可以使用Kbuild文件代替Makefile,当两者同时存在时,优先选择Kbuild文件进行编译工作,只是用户习惯性地使用Makefile来命名。

4. kbuild makefile

4.1 以把模块编译进内核时,kbuild makefile的执行流程为例

        如果需要将一个模块配置进内核,需要在makefile中进行配置:

        obj-y += foo.o

        将foo.o编译进内核,根据make的自动推导原则,make将会自动将foo.c编译成foo.o。

        上述方式基本上用于开发时的模块单独编译,当需要一次编译整个内核时,通常是在top makefile中这样写:obj-$(CONFIG_FOO) += foo.o

        在.config文件中将CONFIG_FOO变量配置成y,当需要修改模块的编译行为时,就可以统一在配置文件中修改,而不用到makefile中去找。

        kbuild编译所有的obj-y的文件,然后调用$(AR) rcSTP将所有被编译的目标文件进行打包,打包成build-in.o文件,需要注意的是这仅仅是一份压缩版的存档,这个目标文件里面并不包含符号表,既然没有符号表,它就不能被链接。

        紧接着调用scripts/link-vmlinux.sh,将上面产生的不带符号表的目标文件添加符号表和索引,作为生成vmlinux镜像的输入文件,链接生成vmlinux。

        对于这些被编译进内核的模块,模块排列的顺序是有意义的,允许一个模块被重复配置,系统将会取用第一个出现的配置项,而忽略随后出现的配置项,并不会出现后项覆盖前项的现象。

        链接的顺序同时也是有意义的,因为编译进内核的模块通常由xxx_initcall()来描述,内核对这些模块分了相应的初始化优先级,相同优先级的模块初始化函数将会被依次放置在同一个段中,而这些模块执行的顺序就取决于放置的先后顺序,由链接顺序所决定。

4.2 kbuild中的变量

        顶层makefile中定义了以下变量:

        KERNELRELEASE:这是一个字符串,用于构建安装目录的名字(一般使用版本号来区分)或者显示当前的版本号。

        ARCH:定义当前的目标架构平台,比如:"X86","ARM",默认情况下,ARCH的值为当前编译的主机架构,但是在交叉编译环境中,需要在顶层makefile或者是命令行中指定架构:make ARCH=arm ...

        INSTALL_PATH指定安装目录,安装目录主要是为了放置需要安装的镜像和map(符号表)文件,系统的启动需要这些文件的参与。

        INSTALL_MOD_PATH, MODLIB:

        INSTALL_MOD_PATH:为模块指定安装的前缀目录,这个变量在顶层makefile中并没有被定义。 MODLIB为模块指定安装目录。默认情况下,模块会被安装到$(INSTALL_MOD_PATH)/lib/modules/$(KERNELRELEASE)中,默认INSTALL_MOD_PATH不会被指定,所以会被安装到/lib/modules/$(KERNELRELEASE)中。

        INSTALL_MOD_STRIP:

        如果这个变量被指定,模块就会将一些额外的、运行时非必要的信息剥离出来以缩减模块的大小,当INSTALL_MOD_STRIP为1时,--strip-debug选项就会被使用,模块的调试信息将被删除,否则就执行默认的参数,模块编译时会添加一些辅助信息。

        这些全局变量一旦在顶层makefile中被定义就全局有效,但是有一点需要注意,在驱动开发时,一般编译单一的模块,执行make调用的是当前目录下的Makefile。在这种情况下这些变量是没有被定义的,只有先调用了顶层makefile之后,这些变量在子目录中的makefile才被赋值。

4.3 生成header文件

        vmlinux中打包了所有模块编译生成的目标文件,在驱动开发者眼中,在内核启动完成之后,它的作用相当于一个动态库,既然是一个库,如果其他开发者需要使用里面的接口,就需要相应的头文件。自然地,build也会生成相应的header文件供开发者使用,一个最简单的方式就是用下面这个指令:

        make headers_install ARCH=arm INSTALL_HDR_PATH=/DIR

        ARCH:指定CPU的体系架构,默认是当前主机的架构,可以使用以下命令查看当前源码支持哪些架构:ls -d include/asm-* | sed 's/.*-//'

        INSTALL_HDR_PATH:指定头文件的放置目录,默认是./usr。

        至此,build工具将在指定的DIR目录生成基于arm架构的头文件,开发者在开发时就可以引用这些头文件。

5. 添加一个驱动到内核

        整个linux内核的编译都是采用一种分布式的思想,添加一个驱动到内核我们需要做的事情就是:

        1)将驱动源文件放在内核对应目录中,一般的驱动文件放在drivers目录下,字符设备放在drivers/char中,块设备就放在drivers/blok中,文件的位置遵循这个规律,如果是单个的字符设备源文件,就直接放在drivers/char目录下,如果内容较多足以构成一个模块,则新建一个文件夹。

        2)如果是新建文件夹,因为是分布式编译,需要在文件夹下添加一个Makefile文件和Kconfig文件并修改成指定格式。同时还需要修改上级目录的Makefile和Kconfig,以将文件夹添加到整个源码编译树中。

        3)如果是单个文件直接添加,则直接修改当前目录下的Makefile和Kconfig文件将其添加进去即可。

6.  Kconfig的语法简述

常用的语法选项:

source drivers/xxx/Kconfig

config TEST

            bool "item name"

            depends on NET

            select NET

            help

              just for test

        source:相当于C语言中的include,表示包含并引用其他Kconfig文件

        config:负责新建一个条目,对应linux中的编译模块,条目前带有选项。config后面跟的标识会被当成名称写入到.config文件中,比如:当此项被选择为[y],即编译进内核时,最后会在.config文件中添加这样一个条目:CONFIG_TEST=y 。CONFIG_TEST变量被传递给makefile进行编译工作。

        bool "item name":其中bool表示选项支持的种类,bool表示两种,编译进内核或者是忽略;还有另一种选项tristate,表示支持三种配置选项:编译进内核、编译成可加载模块、忽略。item name就是显示在menu中的名称。

        depends on:表示当前模块需要依赖另一个选项,如果另一个选项没有没选择编译,当前条目选项不会出现在窗口中

        select:同样是依赖相关的选项,表示当前选项需要另外其他选项的支持,如果选择了当前选项,那么需要支持的那些选项就会被强制选择编译。

        help:允许添加一些提示信息

        menu/menuend:

            menu "Test option"

            ...

            endmenu

        这一对关键词创建一个选项目录,该选项目录不能被配置,选项目录中可以包含多个选项。

7. 配置内核选项

        在编译linux源码前,需要进行配置,以决定哪些部分编译进内核、哪些部分编译成模块。通常使用make menuconfig来修改配置。在配置完成之后将生成一个.config文件,makefile根据.config文件选择性地进入子目录中执行编译工作。其中,make oldconfig是沿用之前的配置。

        make menuconfig执行的原理是什么?

        make menuconfig是读取Kconfig配置文件来陈列出所有的配置选项。

        顶层makefile是怎样执行子目录中的编译工作的?

        递归地进入到每个子目录中,然后通过调用子目录中的makefile来执行各级子目录的编译,最后统一链接。

        以ARM平台为例,具体配置过程:

        1)执行make menuconfig 时,系统首先读取arch/arm/Kconfig生成整个配置界面

        2)在读取配置界面的同时,系统会读取顶层目录下的.config文件,生成所有配置选项的默认值。

        3)当修改完配置并保存后,系统会更新顶层目录下的.config。

        4)当执行make时,各层的Makefile会根据.config文件中的编译选项来决定哪些文件会被编译到内核中,或者编译成模块。

        8. 编译示例

        梳理了整个添加的流程,接下来就以一个具体的示例来进行详细的说明。

        需要添加的源代码为字符设备驱动,名为cdev_test.c,新建一个目录cdev_test。

        1)将新增文件夹放置目录

        鉴于是字符设备,所以将源文件目录放置在$KERNEL_ROOT/drivers/char/下面。

        如果是块设备,一般被放置在block下面。

        放置后目标文件位置为:$KERNEL_ROOT/drivers/char/cdev_test

        2)配置Kconfig文件

        在$KERNEL_ROOT/drivers/char/cdev_test目录下创建一个Kconfig文件,并修改文件如下:

       menu "cdev test dir"

       config CDEV_TEST

            bool "cdev test support"

            default n

            help

                just for test ,hehe

       endmenu

        这个Kconfig文件的作用,就是在menuconfig的菜单中,在Device Driver(对应drivers目录) ---> Character devices(对应char目录)菜单下创建一个名为"cdev test dir"的菜单选项,

        在"cdev test dir"的菜单选项下创建一个"cdev test support"的条目,这个条目的选项只有两个,[*]表示编译进内核和[]表示不进行编译,默认选择n,不进行编译。

        选择help可以查看相应的提示信息。

        3)配置上层目录中的Kconfig文件

        在上文中还提到,Kconfig分布式地存在于子目录下,同时需要注意的是,在编译时,配置工具并非无差别地进入每个子目录,收集所有的Kconfig信息,而是遵循一定的规则递归进入。

        那么,既然是新建的目录,怎么让编译器知道要进入到这个子目录下呢?答案是,在上级目录的Kconfig中包含当前路径下的Kconfig文件。
        打开char目录下的Kconfig文件,并且在文件的靠后位置添加:

        source "drivers/char/cdev_test/Kconfig"

        就把新的Kconfig文件包含到系统中检索目录中了,那么drivers/char/又是怎么被检索到的呢?

就是在drivers的Kconfig中添加drivers/char/目录下的Kconfig索引,以此类推。

        4)配置Makefile文件

        在$KERNEL_ROOT/drivers/char/cdev_test目录下创建一个Makefile文件,并且编译Makefile文件如下:

        obj-$(CONFIG_CDEV_TEST) += cdev_test.o

        表示当前子目录下Makefile的作用就是将cdev_test.c源文件编译成cdev_test.o目标文件。

CONFIG_CDEV_TEST是怎么被配置的呢?在上文提到的Kconfig文件编写时,有这么一行:

        config CDEV_TEST

        ...

        在Kconfig被添加到配置菜单中,且被选中编译进内核时,就会在$KERNEL_ROOT/.config文件中添加一个变量:

        CONFIG_CDEV_TEST=y

        自动添加CONFIG_前缀,而名称CDEV_TEST则是由Kconfig指定。

        5)配置上层目录中的Makefile

        是不是这样就已经将当前子目录添加到内核编译树中了呢?其实并没有,就像Kconfig一样,Makefile也是分布式存在于整个源码树中,顶层makefile根据配置递归地进入到子目录中,调用子目录中的Makefile进行编译。需要修改drivers/char/目录下的Makefile文件,添加一行:

        obj-$(CONFIG_CDEV_TEST)         += cdev_test/

        在编译时,如果CONFIG_CDEV_TEST变量为y,cdev_test/Makefile就会被调用。

        6)配置编译选项

        执行:make menuconfig

        进入目录选项Device Driver --> Character devices--->cdev test dir.

        然后按'y'选中模块cdev test support

        保存退出,然后执行编译:

        7)编译内核

参考:​​​​​​linux内核makefile概览 - 牧野星辰 - 博客园

linux设备驱动程序——将驱动程序编译进内核 - 牧野星辰 - 博客园

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐