我们首先要进行make,编译成功后会生成一个hello_mod.ko文件。接下来要将这个内核模块插入到内核当中,然后还要利用mknod命令生成一个设备文件节点。接下来我们再利用测试程序,对我们写好的驱动程序进行测试。
那么以上的工作都可以通过下面这个Makefile文件完成,直接在shell终端输入make就可编译这个内核模块,输入make clean就可以清除一些中间文件,输入make install就可以将编译好的内核模块插入到内核当中。更重要的是,这个Makefile文件具有很好的移植性。本文通过分析下面给出的Makefile文件,与大家一起更深入的学习Makefile文件的相关语法以及一些shell编程。
TARGET=hello_mod
OBJECTS=hello.o
ifneq ($(KERNELRELEASE),)
obj-m += $(TARGET).o
$(TARGET)-objs:=$(OBJECTS)
# echo $(TARGET)
else
PWD:=$(shell pwd)
KERNEL_DIR :=/lib/modules/`uname -r`/build
endif
All:modules
modules:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
install:
/sbin/insmod $(TARGET).ko
uninstall:
/sbin/rmmod $(TARGET).ko
clean:
rm -rf *.o *.ko *.mod.c .*.cmd .tmp_versions
1.条件语句
首先注意这个新的Makefile文件在逻辑结构上发生了很大的变化,采用了条件语句:ifneq-else-endif。这个条件语句是用来判断括号中逗号前后的两个变量是否不相等。ifneq之后为符合条件时所要执行的语句,相应的else之后为不符合条件时要执行的语句。上述Makefile文件中的ifneq ($(KERNELRELEASE),)是用来判断KERNELRELEASE变量是否为空,不为空则符合条件。
类似的还有下面的条件语句,只不过条件判断的类型不同。
ifeq-else-endif:如果两个变量相等,则满足条件。
下面两种条件语句中,条件判断处为变量名,是用来判断此变量是否被定义过。
ifdef-else-endif:如果变量被定义,满足条件。
ifndef-else-endif:如果变量未被定义,满足条件。
不过,上述两个条件语句所判断的变量定义没有递归性,比如下面例子:
1 | path= |
2 | cur_path=$(path) |
3 | ifdef cur_path |
4 | right=yes |
5 | else |
6 | right=no |
7 | endif |
这个例子中最终执行的是right=yes。虽然path为空,但是cur_path=$(path)却被认为是定义了cur_path变量。正如上面所说定义没有递归行。
2.变量赋值
在Makefile文件中定义一个变量的格式为:变量名 赋值符 变量值
赋值符通常有以下四种类型:=,:=,?=,+=。对于赋值符=与我们平日里使用的等号差不多,但是这里我们需要清除一个概念,那就是递归展开变量。为了更清除的说明上面的概念,请看下面的例子:
1 | first=$(second) |
2 | second=$(third) |
3 | third=yes |
4 | all: |
5 | echo $(first) |
很显然结果为yes。当执行make时,first首先展开为second,接着second又展开成为third,再后来引用third的值即yes。可以看到first是递归展开而得到最后的yes值的。这便是我们刚才所谓的递归展开变量。
而与上述变量赋值符号不同的是,:=赋值符号是立即展开变量的,同样的例子,只不过这次我们使用:=赋值符:
1 | first:=$(second) |
2 | second:=$(third) |
3 | third:=yes |
4 | all: |
5 | echo $(first) |
此时first为空。这是因为在定义first变量时就立即展开了second,因为second此时未定义。即便此句之后为second变量赋了值,但first的值为空。
另外两个赋值符号比较容易理解。首先+=赋值符是在变量原有值的基础上再增加新的值,而不是覆盖原有变量值。而?=赋值符首先会判断变量实现是否已经被赋值,只有之前未被赋值的变量此刻才能被赋值。
OK,了解了赋值符号的含义,那么再次看上述的Makefile文件,就会清晰很多。
3.伪目标
正如上述所言,直接在shell终端输入make就会执行目标all后的命令,这并不是all目标具有什么默认的效果。只不过在Makefile文件中,第一个目标总被认为是最终目标。因此可以想象到,当你交换一下all和clean的位置,直接执行make时会自动执行clean后面的命令。并且不一定总对第一个目标起名为all,你可以使用你喜欢的目标名(也许all是一种无声的约定 )。
4.为什么要用makefile
内核模块化简单实用,但是编译却成了问题:有时候我们只是改动了某个文件的一小部分就不得不编译整个内核,这是个很可怕的事情。但是GNU make引入后,这个问题就迎刃而解了:make只会编译已被改动代码的文件,而不是将所有文件都编译。但是make具体如何对源文件进行编译,怎么编译?这个时候就需要makefile文件了。在之前的文章当中,我们对Makefile文件下过“编译规则”这样的定义,下面通过分析上面的Makefile文件,我们具体感受一下这个“编译规则”。
整个Makefile文件根据KERNELRELEASE的值来划分不同的编译规则(方式),这里的KERNELRELEASE只会在内核源码目录下显示当前内核的版本号。
一般情况下,我们编写的内核模块源文件所在的目录并非位于内核源码根目录(或其子目录)下。那么此时就不符合ifueq条件,即执行else语句下的编译规则。这种情况下,当我们输入make后,就会执行make -C $(KDIR) M=$(PWD) modules这条命令。注意这条命令后的modules,它表示将会编译所有在配置菜单中被选作模块编译的那些内容(也就是赋值给obj-m的那些目标)。接下来由于-C $(KDIR)参数的原因,make会转向内核源码根目录下去执行。根据M后的目录,编译我们写的内核模块源码,生成.o文件。接着联合一些中间文件生成.ko文件。这便是make生成整个内核目标文件的过程。这个过程可以在make之后在终端产生的一系列描述文字得到。
上面这种情况会将我们编写的内核模块源码编译成内核模块目标文件,接下来就是我们熟悉的内核模块插入了。不过当我们所写的内核模块文件处于内核源码目录下时,KERNELRELEASE就会非空(此时为版本号),那么此时就满足ifneq条件了。什么时候我们编写的内核模块源码会处在内核源码目录下?此时的内核编译是那种方式?
我们假设已经写好了驱动代码,然后在Kconfig文件中为这个驱动编写配置选项。在配置菜单中有了此驱动的相关配置选项后,接下来用户可以选择是否会将此驱动源码一起编译进内核。那么此时Makefile文件的作用就是将此内核模块源码编译进内核(obj-m := $(TARGET).o)。不过注意,只是通知内核下次编译“带上我”,并没有实际编译。
如果是多个源文件编译出一个模块,那么假设模块名是mytest.ko,那么源文件名不能有mytest.c,下面是一个例子:
obj-m := mytest.o
mytest-objs := file1.o file2.o file3.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
mytest是我希望生成的ko名,整个ko包含file1.c file2.c file3.c三个.c文件和file.h一个.h文件,这时.h不用管。
关于前面的makefile当中用M=代替SUBDIR=,效果是一样的,但是M=更明确,参见《从 2.4 到 2.6:Linux 内核可装载模块机制的改变对设备驱动的影响》http://www-128.ibm.com/developerworks/cn/linux/l-module26/
对于Makefile,其中建议为(将上面的改成):
ifneq ($(KERNELRELEASE),)
obj-m := mytest.o
mytest-objs := file1.o file2.o file3.o
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
endif
解释为:
KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义, 所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C $(KDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。else之前的内容为kbuild语法的语句, 指明模块源码中各文件的依赖关系,以及要生成的目标模块名。mytest-objs := file1.o file2.o file3.o表示mytest.o 由file1.o,file2.o与file3.o 连接生成。obj-m := mytest.o表示编译连接后将生成mytest.o模块。