LV10-01-内核模块-01-编译与加载

本文主要是内核模块的编译与加载相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

一、内核模块简介

1. 内核模块的概念

什么是Linux内核模块?我查了百度:Linux内核模块_百度百科 (baidu.com)

尽管Linux是”单块内核“(monolithic)的操作系统——这是说整个系统内核都运行于一个单独的保护域中,但是linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。这些代码(包括相关的子线程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。

支持模块的好处是基本内核镜像尽可能的小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便地删除和重新载入内核代码,也方便了调试工作。而且当热插拔新设备时,可通过命令载入新的驱动程序。

类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。可用于加载到linux内核的文件后缀为.ko.ko文件是kernel object文件,也就是kernel下的模块加载文件。

内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。

2. 内核模块的特点

内核模块的出现,解决了以下问题:

  • 单内核扩展性差的缺点。
  • 减小内核镜像文件体积,一定程度上节省内存资源。
  • 提高开发效率。

但是内核模块也有相应的缺点,就是它不能彻底解决稳定性低的缺点:内核模块代码出错可能会导致整个系统崩溃。

二、模块的加载

模块的加载有两种方式:一是静态加载法,在linux源码中进行修改,配置,然后直接编译进linux内核;另一种是动态加载法,编写好模块后,通过一些列命令讲模块插入到linux内核。

1. 静态加载

静态加载就是新功能源码与内核其它代码一起编译进uImage文件内,这种编译方式不会生成.ko内核模块文件,只会生成一个.o文件。

1.1 编写模块源文件

我们需要编写一个模块源文件用于测试,我们在linux-3.14/driver/char/目录下新建myhello.c文件:

1
2
cd ~/5linux/linux-3.14/driver/char/   # 进入相应的目录
vim myhello.c # 创建文件

并添加以下内容:

点击查看 myhello.c 测试源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: myhello.c
* Author : qidaink
* Date : 2022-08-23
* Version :
* Description:
* Others :
* Log :
* ======================================================
*/
#include <linux/module.h> /* MODULE_LICENSE */
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */

/* 模块入口函数 */
int __init myhello_init(void)
{
printk("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
printk("myhello is running!\n");
printk("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
return 0;
}

/* 模块出口函数 */
void __exit myhello_exit(void)
{
printk("myhello will exit\n");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(myhello_init);
module_exit(myhello_exit);

/* 模块信息(通过 modinfo myhello 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("qidaink"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

1.2 修改Kconfig

此步骤可以向linux的图形配置界面添加新功能配置选项。

1
2
cd  ~/5linux/linux-3.14/driver/char/  # 进入myhello.c的同级目录
vim Kconfig # 打开相关配置文件

找到如下语句(应该在第7行):

1
source "drivers/tty/Kconfig"

在该语句下边添加以下内容:

1
2
3
4
config MY_HELLO
tristate "This is my hello test!"
help
This is a test for kernel new function

然后保存退出即可,我们可以试一下有什么效果,我们回到顶层源码目录下,打开图形配置界面:

1
2
cd  ~/5linux/linux-3.14/
make menuconfig

我们按以下层级找到我们新添加的配置项:

1
2
3
Device Drivers  --->
Character devices --->
< > This is a hello test (NEW)

如下图所示:

image-20220831090450726

1.3 修改Makefile

我们添加了新源码文件,自然要参与编译的,我们打开myhello.c同级目录下的Makefile

1
vim ~/5linux/linux-3.14/drivers/char/Makefile

我们找到如下语句(应该在第18行):

1
obj-$(CONFIG_BFIN_OTP)          += bfin-otp.o

我们在这一句下边添加如下内容:

1
obj-$(CONFIG_MY_HELLO)          += myhello.o

【注意】这里需要是CONFIG_xxx,这个xxx就是上边修改Kconfig的时候添加的config MY_HELLOMY_HELLO

1.4 配置新功能编译方式

我们这里打开linux内核图形配置界面:

1
2
cd ~/5linux/linux-3.14
make menuconfig

然后将新功能修改为*,表示将新功能编译到内核中。

1
2
3
Device Drivers  --->
Character devices --->
<*> This is a hello test (NEW)

1.5 编译内核并测试

  • 编译内核
1
2
cd ~/5linux/linux-3.14 # 回到顶层源码目录下
make uImage -j4 # 编译内核

出现以下提示信息表示编译成功:

1
2
3
4
5
6
7
Image Name:   Linux-3.14.0-gc6094bb-dirty
Created: Wed Aug 31 09:17:19 2022
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 2931456 Bytes = 2862.75 kB = 2.80 MB
Load Address: 40008000
Entry Point: 40008000
Image arch/arm/boot/uImage is ready
  • 拷贝内核文件并重启开发板测试
1
cp arch/arm/boot/uImage ~/3tftp/fs4412/

开发板重启后,启动内核时有以下信息出现,则说明新功能添加成功:

image-20220831091956871

2. 动态加载

新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko。然后通过一些命令进行插入或者卸载。

2.1 模块编译

2.1.1 模块源码在linux内核源码中

我们还是使用上边静态加载使创建的文件myhello.c,该文件包含在linux内核源码中,编译模块的步骤如下:

  • (1)编写模块源码文件(同1.1
1
2
cd ~/5linux/linux-3.14/driver/char/   # 进入相应的目录
vim myhello.c # 创建文件
  • (2)配置Kconfig
1
2
cd  ~/5linux/linux-3.14/driver/char/  # 进入myhello.c的同级目录
vim Kconfig # 打开相关配置文件
  • (3)修改Makefile(同1.3
1
vim ~/5linux/linux-3.14/drivers/char/Makefile
  • (4)修改新功能编译方式
1
2
cd ~/5linux/linux-3.14 # 回到顶层源码目录下
make menuconfig

然后将新功能前边配置改为M,表示编译成独立的模块:

1
2
3
Device Drivers  --->
Character devices --->
<M> This is a hello test (NEW)
  • (5)重新编译内核
1
2
cd ~/5linux/linux-3.14 # 回到顶层源码目录下
make uImage -j4 # 编译内核
  • (6)模块编译
1
2
cd ~/5linux/linux-3.14  # 回到顶层源码目录下
make modules -j4 # 编译模块

完成模块编译后,我们就会发现在myhello.c目录下生成了一个myhello.ko文件,这就是我们后边要使用的内核模块文件。

2.1.2 模块源码独立在其他目录

这样每次都要使用linux源码目录下操作,文件也太多了,其实我们也可以在其他的目录单独创建模块驱动源码文件,然后调用linux源码进行编译,生成我们所需要的.ko内核模块文件。一般步骤如下:

  • (1)新建一个目录用于存放我们的模块源码
1
2
mkdir ~/myhello # 新建一个目录
cd ~/myhello # 进入相应目录
  • (2)编写模块源码文件

这里我直接拷贝之前的文件到这里来:

1
cp ~/5linux/linux-3.14/drivers/char/myhello.c .
  • (3)编写Makefile文件
点击查看 Makefile 文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
##============================================================================#
# Copyright © hk. 2022-2025. All rights reserved.
# File name : Makefile
# Author : qidaink
# Date : 2022-08-28
# Version :
# Description: 驱动模块编译Makefile
# Others :
# Log :
##============================================================================#
##

# 模块名和模块测试APP名称
MODULE_NAME := myhello
# NFS 共享目录
ROOTFS ?= /home/hk/4nfs/rootfs
MODULE_DIR ?= $(ROOTFS)/01myDrivers

ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= /home/hk/5linux/linux-3.14
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif

PWD := $(shell pwd)
# 编译模块和测试程序
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

# 拷贝相关文件到nfs共享目录
copy:
sudo cp $(MODULE_NAME).ko $(MODULE_DIR)

.PHONY: clean
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions .cache.mk

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "insmod <module_name>.ko # Load module"
@echo "rmmod <module_name> # Uninstall the module"
@echo "dmesg -C # Clear the kernel print information"
@echo "lsmod # Check the kernel modules that have been inserted"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo "make ARCH=arm # arm platform"
@echo "\033[1;32m======================================================================\033[0m"

print:
@echo "KERNELDIR = $(KERNELDIR)"

else
CONFIG_MODULE_SIG=n
obj-m += $(MODULE_NAME).o
endif

  • (4)编译模块
1
2
make          # 编译可在Ubuntu中运行的模块
make ARCH=arm # 编译可在ARM上运行的模块

2.2 命令介绍

上边我们已经得到了.ko内核模块文件,下边是一些相关的命令。

2.2.1 file

file命令可用于查看指定的.ko文件可以用于哪种平台,一般使用格式如下:

1
file module_name.ko

输出信息中,带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux

2.2.2 insmod

insmod命令可用于加载.ko文件,一般使用格式如下:

1
insmod module_name.ko

【注意】在主机Ubuntu中加载模块的时候需要加上sudo

2.2.3 lssmod

lsmod命令可用显示已经加载的模块,一般使用格式如下:

1
lsmod module_name.ko

【注意】在主机Ubuntu中加载模块的时候不加sudo,也可以正常使用。

2.2.4 rmsmod

rmmod命令可用于卸载已经加载的模块,一般使用格式如下:

1
rmmod module_name

【注意】在主机Ubuntu中加载模块的时候需要加上sudo

2.2.5 dmesg

dmesg命令可用于查看内核的打印的提示信息,主要是在Ubuntu中加载模块的使用后使用,我们的开发板会实时在串口终端界面打提示信息,一般使用格式如下:

1
2
sudo dmesg -C  # 清除内核已打印的信息
dmseg # 查看内核的打印信息

【注意】在主机Ubuntu中加载模块的时候需要加上sudo,在开发板中不需要使用该命令。

2.3 加载测试

  • 拷贝.ko文件到根文件系统中
1
sudo cp myhello.ko ~/4nfs/rootfs/01myDrivers/

【注意】需要加上sudo,否则可能会提示没有权限拷贝。

  • 加载测试
1
insmod myhello.ko

若出现以下提示,则说明加载成功:

image-20220831100115565

三、模块基础代码解析

【注意】一个模块必须要有的三个部分:入口函数、出口函数和MODULE__LICENSE

1. 几个头文件

1
2
3
#include <linux/module.h>   /* MODULE_LICENSE */
#include <linux/kernel.h> /* printk */
#include <linux/init.h> /* module_init module_exit */
  • linux/module.h:包含模块编程相关的宏定义,如MODULE_LICENSE
  • linux/kernel.h:包含内核编程最常用的函数声明,如printk
  • linux/init.h:包含模块加载与卸载函数,如module_initmodule_exit

2. 模块加载与卸载

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下 :

1
2
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

2.1 模块加载

1
2
3
4
5
6
7
8
/* 模块入口函数 */
int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}

module_init(xxx_init); /* 注册模块加载函数 */

模块加载函数位于linux/init.h中,module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用insmod命令加载驱动的时候,xxx_init这个函数就会被调用,也就是说在模块被插入到内核的时候,xxx_init函数被调用,它主要作用是为新功能做好准备工作。

  • __init说明

__init的作用 :

(1)一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记。

(2)指示链接器将该函数放置在.init.text区段。

(3)在模块插入时方便内核从.ko文件指定位置读取入口函数的指令到特定内存位置。

  • module_init()说明

(1)module_init是一个宏,使用格式就是:module_init(模块入口函数名)

(2)动态加载模块的时候,对应的函数被调用;静态加载模块的时候,在内核启动过程中对应函数被调用。

(3)对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。

(4)对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名。

2.2 模块卸载

1
2
3
4
5
6
7
/* 模块出口函数 */
void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}

module_exit(xxx_exit); /* 注册模块卸载函数 */

模块卸载函数位于linux/init.h中,module_exit 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用rmmod命令卸载驱动的时候,xxx_exit这个函数就会被调用,也就是说该函数在模块从内核中被移除时调用,主要作用做些xxx_init函数的反操作,被称为模块的出口函数。

  • __exit说明

__exit的作用 :

(1)一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记。

(2)指示链接器将该函数放置在.exit.text区段。

(3)在模块插入时方便内核从.ko文件指定位置读取出口函数的指令到另一个特定内存位置。

  • module_exit()说明

(1)module_exit是一个宏,使用格式就是:module_exit(模块出口函数名)

(2)动态加载模块在卸载的时候,对应的函数被调用;静态加载的模块,可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略。

(3)对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。

(4)对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名。

3. 模块信息宏

模块信息宏用来描述一些当前模块的信息,有一些是可选的,如MODULE_DESCRIPTION,有一些是必须的,如MODULE_LICENSE,这些宏的本质是定义static字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo字段,可以用modinfo命令来查看这些模块信息,一般格式如下:

1
modinfo  模块文件名

3.1 MODULE_LICENSE

MODULE_LICENSE是一个宏,一般使用格式如下:

1
2
3
MODULE_LICENSE(字符串常量);
/* 例如 */
MODULE_LICENSE("GPL");

字符串常量内容为源码的许可证协议 可以是GPLGPL v2GPL and additional rightsDual BSD/GPL"Dual MIT/GPLDual MPL/GPL等。

其中GPL最常用,其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印信息:

1
2
myhello:module license 'unspecified' taints kernel
Disabling lock debugging due to kernel taint

同时也会导致新模块没法使用一些内核其它模块提供的高级功能。

3.2 MODULE_AUTHOR

MODULE_AUTHOR是一个宏,一般使用格式如下:

1
MODULE_AUTHOR(字符串常量); /* 字符串常量内容为模块作者说明 */

3.3 MODULE_DESCRIPTION

MODULE_DESCRIPTION是一个宏,一般使用格式如下:

1
MODULE_DESCRIPTION(字符串常量); /* 字符串常量内容为模块功能说明 */

3.4 MODULE_ALIAS

MODULE_ALIAS是一个宏,一般使用格式如下:

1
MODULE_ALIAS(字符串常量); /* 字符串常量内容为模块别名 */

4. 模块基础框架

所以一个模块的编写至少需要以下几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: myhello.c
* Author : qidaink
* Date : 2022-08-23
* Version :
* Description:
* Others :
* Log :
* ======================================================
*/
#include <linux/module.h> /* MODULE_LICENSE */
#include <linux/kernel.h> /* printk */
#include <linux/init.h> /* module_init module_exit */

/* 模块入口函数 */
int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}

/* 模块出口函数 */
void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);/* 注册模块加载(入口)函数 */
module_exit(xxx_exit);/* 注册模块卸载(出口)函数 */

/* 模块信息(通过 modinfo xxx 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("qidaink"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

四、Makefile

1. 代码解析

1.1 几个变量

为了方便使用,这里定义了几个变量:

1
2
3
4
5
# 模块名
MODULE_NAME := myhello
# NFS 共享目录
ROOTFS ?= /home/hk/4nfs/rootfs
MODULE_DIR ?= $(ROOTFS)/01myDrivers
  • MODULE_NAME:就是这个模块的名称。
  • ROOTFS:就是NFS共享目录的路径,这里其实是代表了网络挂载的开发板的根文件系统的路径。
  • MODULE_DIR:根文件系统下的驱动存放的目录,我们要在开发板加载驱动模块,需要先将模块拷贝到开发板,这个就是存放的目录路径。

1.2 选择内核源码目录

1
2
3
4
5
6
# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= /home/hk/5linux/linux-3.14
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
  • ARCH:我们使用make参数的时候指定的,我们使用make ARCH=arm的时候,这里的ARCH就是arm
  • KERNELDIR:开发板所使用的Linux内核源码目录,使用绝对路径 。

这一段,根据我们make的时候参数的不同,可以编译出用于x86平台和arm平台的.ko文件:

1
2
make          # 编译可在Ubuntu中运行的模块
make ARCH=arm # 编译可在ARM上运行的模块

1.3 编译命令

1
2
3
4
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
  • -C:表示将当前的工作目录切换到指定目录中 ,也就是KERNERLDIR目录 。
  • M:表示模块源码目录,make modules命令中加入M=dir以后程序会自动到指定的dir目录中读取模块的源码并将其编译为.ko文件。
  • modules: 表示编译模块 。

1.4 obj-m

1
obj-m            += $(MODULE_NAME).o
  • obj-m:它后边是目标文件名,表示将$(MODULE_NAME).c编译成$(MODULE_NAME).ko模块。

2. 多源文件编译

有时候我们要是一个模块依赖于多个.c源文件怎么编译呢?我们修改Makefileobj-m部分即可:

1
2
3
obj-m            += $(MODULE_NAME).o

$(MODULE_NAME)-objs = $(DEPEND1).o $(DEPEND2).o

例如,

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <linux/module.h>   /* MODULE_LICENSE */
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */

void test_func(void);

/* 模块入口函数 */
int __init test_init(void)
{
test_func();
return 0;
}

/* 模块出口函数 */
void __exit test_exit(void)
{
printk("test will exit\n");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(test_init);
module_exit(test_exit);

/* 模块信息(通过 modinfo test 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("qidaink"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

1
2
3
4
5
6
7
#include <linux/kernel.h>

void test_func(void)
{
/* code */
printk("this is test_func\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
##============================================================================#
# Copyright © hk. 2022-2025. All rights reserved.
# File name : Makefile
# Author : qidaink
# Date : 2022-08-28
# Version :
# Description: 驱动模块编译Makefile
# Others :
# Log :
##============================================================================#
##

# 模块名和模块测试APP名称
MODULE_NAME := mytest
# NFS 共享目录
ROOTFS ?= /home/hk/4nfs/rootfs
MODULE_DIR ?= $(ROOTFS)/01myDrivers

ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= /home/hk/5linux/linux-3.14
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif

PWD := $(shell pwd)
# 编译模块和测试程序
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

# 拷贝相关文件到nfs共享目录
copy:
sudo cp $(MODULE_NAME).ko $(MODULE_DIR)

.PHONY: clean
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions .cache.mk

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "insmod <module_name>.ko # Load module"
@echo "rmmod <module_name> # Uninstall the module"
@echo "dmesg -C # Clear the kernel print information"
@echo "lsmod # Check the kernel modules that have been inserted"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo "make ARCH=arm # arm platform"
@echo "\033[1;32m======================================================================\033[0m"

print:
@echo "KERNELDIR = $(KERNELDIR)"

else
CONFIG_MODULE_SIG=n
obj-m += $(MODULE_NAME).o
$(MODULE_NAME)-objs := test.o func.o
endif