Linux的模块编译在2.6内核后有了较大的修改,这使得在内核编译过程中对那些源码属于内核源码集中的模块的编译方便了许多。因此,如果想单独编译某些模块(如开发的驱动程序,这些代码为自主编写的),仅靠一般的makefile文件是不够的。
2.4及其之前的内核模块编译需要在makefile文件中显示调用gcc编译器,并且需要添加多个参数才能实现。而2.6内核中,需要调用内核源码目录当中的顶层的Makefile文件(此处称为Kernel Makefile),并根据读取到的内容,使用由开发人员编写的Kbuild (此处称为Kbuild Makefile)文件,处理后即可生成模块文件。
因此,要编译一个模块,需要三个Makefile文件。
工程Makefile:位于模块源代码目录中,名称为Makefile。它是make命令的入口点,此文件中会定位到Linux内核源码的顶层目录,并执行在那里的Makefile文件,即Kernel Makefile;后返回至源代码目录中,执行Kbuild 文件,生成模块。
Kernel Makefile:位于Linux内核源码的顶层目录,名称仍然为Makefile。它是用来指定编译内核目标和模块时需要的一些配置和环境变量的,因此会在编译内核或模块的过程中首选被读取。对于内核或驱动开发人员来说,这个文件不需要修改。
Kbuild Makefile:由Kbuild规则读入,一般位于模块源代码目录中,名称可以是Kbuild(优先选择该名字)或Makefile(这种情况下Kbuild Makefile文件和工程Makefile文件为同一个文件,两者内容进行了合并,但这样做不利于对编译过程的理解,因此不建议这样做)。当中指定了哪些需要编译进模块,对应源文件是什么。内核或驱动人员需要特别编写该文件。
在内核编译时,输入make命令,源码顶层目录中的Kernel Makefile会被执行,修改配置和增加环境变量;后各模块源码文件夹的Kbuild Makefile文件被读入,这样系统就知道如何编译这些源代码使其成为模块了。假如现在我们编写了一个驱动程序的源代码,现在要将其编译为内核,我们不仅要编写Kbuild Makefile文件,还要显式调用Kernel Makefile文件才可以编译成功。
接下来以一个简单的例子进行说明:
该例子编译一个显示“Hello Module”的模块,包含的源码文件有print.c main.c。print.c中有一个在内核输出函数Print,main.c中的调用该函数在系统中输出“Hello Module”。
以下是源代码内容,该源代码为内核模块代码,其结构不在本文分析范围:
print.c
#include <linux/init.h>
#include <linux/module.h>
void Print(char *s)
{
printk(KERN_INFO"%s\n",s);
}
main.c
#include <linux/init.h>
#include <linux/module.h>
void Print(char *);
static int __init hello_init(void)
{
Print("Hello Module");
return 0;
}
static void __exit hello_exit(void)
{
Print("Goodbye Module");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("Vincent Guo");
MODULE_LICENSE("BSD/GPL");
MODULE_DESCRIPTION("A simple Hello Module");
以下是工程Makefile(名称为Makefile,位于源代码目录)和Kbuild Makefile(名称为Kbuild,位于源代码目录)两个文件的源代码
Makefile
build:kernel_module
kernel_module:
make -C /lib/modules/$(shell uname -r)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(CURDIR) clean
Kbuild
obj-m:=hello_module.o
hello_module-objs:=print.o main.o
上面四个文件所在的目录为/mnt/sda6/driver_begin/,在此目录下执行make命令时,该命令会读取工程Makefile文件的内容,并执行build目标,并最终执行kernel_module目标。该目标的动作中-C参数表示将跳转到紧跟在其后面的目录下读取那里的Makefile文件进行执行。本例中shell命令$(shell uname -r)会获取系统的内核版本号,在我的系统中该命令$(shell uname-r)的结果为“3.0.0-12-generic”,因此make命令会定位到/lib/modules/3.0.0-12-generic/build/目录下寻找名为Makefile的文件执行。/lib/modules/3.0.0-12-generic/build/为一个符号链接,实际指向的位置为/usr/src/linux-headers-3.0.0-12-generic/目录,因此执行的是/usr/src/linux-headers-3.0.0-12-generic/Makefile文件。文件/usr/src/linux-headers-3.0.0-12-generic/Makefile仍然为符号链接,指向/usr/src/linux-headers-3.0.0-12-generic/Makefile,因此该Makefile为实际执行文件。如下图所示。
由上图可知,/lib/moduless/3.0.0-12-generic/build为一个符号链接,它重新定位到/usr/src/linux-headers-3.0.0-12-generic/目录,执行该目录下的Makefile文件(即kernel Makefile)中的modules目标。
当执行完/usr/src/linux-headers-3.0.0-12-generic/目录下的Makefile的modules目标后,需要返回到/mnt/sda6/driver_begin/中执行Kbuild规则,那么就需要在工程Makefile(也就是/mnt/sda6/driver_begin/Makefile文件)设置要返回的目录为/mnt/sda6/driver_begin/。设置方法为Kernel Makefile的make命令中添加“M=要返回的路径”,本例中为“M=$(CURDIR)”(也可以写为“M=$(PWD)”,效果相同),变量CURDIR表示当前文件所在路径,因此实际执行时M=/mnt/sda6/driver_begin/。这样就告诉系统执行完/usr/src/linux-headers-3.0.0-12-generic/目录下的Makefile的modules目标后返回/mnt/sda6/driver_begin/继续执行Kbuild规则。
Kbuild规则会寻找名为Kbuild的文件,如果找不到则寻找名为Makefile的文件。找到文件后以文件中内容为准生成.ko模块文件。
Kuild文件基本格式为:
obj-m:=模块名.o
模块名-objs:=源代码文件名1.o源代码文件名2.o……
本例中有print.c和main.c文件,模块名称为hello_module。因此实际内容便如之前源代码所示。如果存在多个模块,则可写为如下格式:
obj-m:=模块1名称.o 模块2名称.o……
模块1名称-objs:=模块1源代码文件名1.o 模块1源代码文件名2.o……
模块2名称-objs:=模块2源代码文件名1.o 模块2源代码文件名2.o……
……
按照这个规则print.c和main.c先被编译为相应的.o文件,后链接成为hello_module.ko文件,全部执行过程如下图所示:
这样在/mnt/sda6/driver_begin/中生成了hello_module.ko文件,装载该内核模块后查看装载结果:
可以看到模块已被装载,在系统日志文件中可以看到相应的输出信息。同样卸载过程也会输出信息。
这说明模块已经成功实现了加载和卸载,也表明模块编译的Makefile文件书写正确。