Linux内核模块驱动加载与调试 1、理论基础 1.1、KO文件 :是kernel object 文件,也就是linux kernel 下的模块加载文件
1.2、ko文件的作用 :一般情况下,内核中会有许多已经加载进去的模块,而有些驱动模块并不需要一直加载到内核 中,为了减少内核的压力 ,就将这些模块写好,当需要的时候再加载带内核 中去
1.3、模块机制 :(Linux提供的模块机制)
1.3.1、模块自身不被编译到内核映像中,从而不影响内核映像的大小
1.3.2、一旦模块被加载,模块和内核中的其他部分的功能完全一样
1.4、编译ko的方法
1.4.1、整编内核 :以整个内核去编译该文件
1.4.2、单编ko :内核源码为基础编译一个外部模块(推荐 ,速度快 )
理论参考:如何编译linux驱动ko - 知乎 (zhihu.com)
2、内核模块模型说明 1、驱动和一般应用程序的执行方式很大不同
一般应用由main函数开始执行,流程基本由程序自身控制
驱动程序没有main函数,由回调方式驱动运行
2、回调方式
先向内核注册函数,然后应用程序触发这些函数的执行(例如:驱动程序在初始化时,向内核注册处理某个设备写操作的函数。当应用程序使用write系统调用写该设备时,内核就会调用注册的上述函数)
3、内核模型常见的回调函数举例
DriverInitialize
驱动初始化函数,通过宏静态注册;
insmod PrintModule.ko,安装驱动并触发该函数,通常会创建设备对象;
DriverUninitialize
驱动销毁函数,通过宏静态注册;
$ rmmod PrintModule,卸载驱动并触发该函数;
DriverOpen
打开设备函数,动态注册;
应用调用open函数打开设备对象时,会触发该函数;
DriverRead
读设备函数,动态注册;
应用调用read函数读设备时,会触发该函数;
DriverWrite
写设备函数,动态注册;
应用调用write函数写设备时,会触发该函数;
DriverIOControl
设备控制函数,动态注册;
应用调用ioctl函数操作设备时,会触发该函数;
DriverMMap
设备内存映射函数,动态注册;
应用调用mmap函数时,会触发该函数;
4、单个源文件 demo_hello.c驱动源程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <linux/kernel.h> /*Needed by all modules*/ #include <linux/module.h> /*Needed for KERN_* */ #include <linux/init.h> /* Needed for the macros */ MODULE_LICENSE("GPL"); static int year=2024; static int hello_init(void) { printk(KERN_WARNING "Hello kernel, it's %d!\n",year); //加载成功显示的内容 return 0; } static void hello_exit(void) { printk("Bye, kernel!\n"); //删除成功显示的内容 } /* main module function*/ module_init(hello_init); module_exit(hello_exit);
4.1、Makefile编写: 1、文件名一定要是Makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .PHONY: all clean #指定内核所在目录 KERNELDIR := /lib/modules/$(shell uname -r)/build #指定源码所在位置 PWD := $(shell pwd) #指定编译工具 CROSS_ARCH := /usr/local/arm/gcc-linaro-5.3.1-2016.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc # 指定要编译的模块对象文件 obj-m += hello.o # 目标 'all': 编译模块 #“M=”参数的作用是以内核源码为基础编译一个外部模块。命令中“M=DIR”,程序会自动跳转到所指定的PWD目录中查找模块源码,编译生成ko文件。 all: $(MAKE) $(CROSS_ARCH) -C $(KERNELDIR) M=$(PWD) modules #C make进入linux源码所在文件夹,从而获得内核的顶层Makefile,从而利用kbuild进行外部模块的编译 #M make在执行目标前返回到模块源代码所在文件夹 #modules目标将会编译所有包含在obj-m变量下的module.o文件 # 目标 'clean': 清理构建生成的文件 clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
2、在当前源文件所在目录下执行以下编译命令
1 2 3 4 5 6 7 #生成构建文件 make #在内核中加载hello模块文件(执行) sudo insmod ./hello.ko #删除ko驱动模块文件 sudo rmmod ./hello.ko
3、验证方式一:输入以下命令查看程序执行结果(进入kern.log中查看程序运行的结果)
1 2 3 4 #进入目标日志文件目录 cd /var/log #直接在命令行打印kern.log文件中的内容 tail /var/log/kern.log
4、验证方式二:同时输入,查看该驱动的加载情况
实例参考链接:,头文件不存在等问题的解决方法 - NeroHwang - 博客园 (cnblogs.com)
5、多个源文件 test.c驱动源程序:
1 2 3 4 5 6 7 8 9 10 // test.c // -------------------------------- #include "test.h" void test(void) { printk("hello world!\n"); return; }
test.h程序:
1 2 3 4 5 6 7 8 9 10 // test.h // -------------------------------- #include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("z_mss"); void test(void);
main.c驱动源程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // main.c // -------------------------------- #include "test.h" static int hello_init(void) { printk("hello init!\n"); //加载成功显示的内容 test(); return 0; } static void hello_exit(void) { printk("hello exit!\n"); //删除成功显示的内容 return; } module_init(hello_init); module_exit(hello_exit);
5.1、Makefile编写: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .PHONY: all clean MODULE_NAME := hello #指定内核所在目录 KERNELDIR := /lib/modules/$(shell uname -r)/build #指定源码所在位置 PWD := $(shell pwd) #指定编译工具 CROSS_ARCH := /usr/local/arm/gcc-linaro-5.3.1-2016.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc # 指定要编译的模块对象文件 obj-m += $(MODULE_NAME).o $(MODULE_NAME)-objs := main.o test.o # 目标 'all': 编译模块 #“M=”参数的作用是以内核源码为基础编译一个外部模块。命令中“M=DIR”,程序会自动跳转到所指定的PWD目录中查找模块源码,编译生成ko文件。 all: $(MAKE) $(CROSS_ARCH) -C $(KERNELDIR) M=$(PWD) modules # 目标 'clean': 清理构建生成的文件 clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
验证方式一:
6、内核模块驱动调试方式
通过以下指令,将程序中printk打印的信息输出到log.txt文件中