《linux内核设计与分析》内核模块编程

时间:2022-01-13 15:46:16

一、实验环境

虚拟机:VMware Workstation 12.0;

操作系统:ubuntu16.04(32位);

当前内核版本:4.4.0-21-generic

 

二、知识储备

    现在,先让我们了解一下什么是内核模块:

    模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。

    比较一下应用程序和内核模块的差别:

《linux内核设计与分析》内核模块编程

    接下来,让我们先写一个小程序,以这个作为起点,来探索如何编写内核模块:

hello.c
1
#include <linux/module.h> 2 #include <linux/kernel.h>
3 #include <linux/init.h>
4 /* 必要的头文件*/
5
6 static int __init hello_init( void )
7 {
8 printk(“Hello,World! \n”);
9 return 0;
10 }
11
12 static void __exit hello_exit( void )
13 {
14 printk(“Finish!\n”);
15 }
16 module_init(lkp_init);
17 module_exit(lkp_cleanup);
18
19 MODULE_LICENSE(“GPL”);

 

简单的分析一下:

第1行:linux/module.h是必要的头文件,内核模块代码中必须包含。

第2行:linux/kernel.h包含了常用的内核函数。

第3行:linux/init.h含了宏_init和_exit,它们允许释放内核占用的内存。

第6行:hello_init()函数是模块的入口点,是模块的初始化函数,它必需包含诸如要编译的代码、初始化数据结构等内容。它通过module_init()例程注册到系统中,在内核装载时被调用。模块的所有初始化函数必须符合这样的形式:int xx_init(void);因为init函数通常不会被外部函数直接调用,所以你不必导出该函数,故它可标记为static。Init函数会返回一个int型数值,如果初始化顺利完成,那么它的返回值为0;否则返回一个非零值。

第8行:Printk的功能类似于C语言中的printf,这个函数是由内核定义的。

第12行:hello_exit()函数是模块的出口函数,它有module_exit()例程注册到系统。在模块从内核卸载时,内核就会调用hello_exit()。退出函数可能会在返回前负责清理资源,以保证硬件处于一致状态;或者做其他的一些操作。退出函数必须符合这样的形式:void xx_exit(void),也可以标记为static。

第16/17行:函数module_init()和cleanup_exit()是模块编程中最基本也是必须的两个函数。调用module_init()不是真正的函数调用,而是一个宏调用,它唯一的参数便是模块的初始化函数。Module_exit()同,唯一的参数是出口函数。

第19行:MODULE_LICENSE(“GPL”);宏用于指定模块的版权。

   

    代码已经有了,但是我们并不用编译c代码的方式gcc来编译它,因为在大型的开发项目中,通常有几十到上百个的源文件,如果每次均手工键入 gcc 命令进行编译的话会非常不方便。因此,人们通常利用 make工具来自动完成编译工作。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。Makefile写法如下:

1 obj-m:=hello.o  //产生hello模块的目标文件
2 CURRENT_PATH:=$(shell pwd)//模块所在的当前路径
3 LINUX_KERNEL_PATH:=/usr/src/linux-headers-4.4.0-21-generic//linux内核源代码的绝对路径
4 all:
5 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules //编译模块
6 clean:
7 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean //清理

注意: 在每个命令前(例如make命令前)要键入一个制表符(按TAB键产生)

有了Makefile,执行make命令,会自动形成相关的后缀为.o和.ko文件。

 

备注:如何查看你的内核版本?

命令:uname –a

 《linux内核设计与分析》内核模块编程

 

模块编译好后,将它插入内核:(需要root权限)

insmod hello.ko

 

输入dmesg可以查看程序的输出。

dmesg

 

当不需要模块时,对模块进行卸载:

rmmod hello

 

 

三、进阶模块

学姐的代码,可以直接运行,功能是输出当前进程的相关信息。

代码:1.c

 1 #include<linux/init.h>
2 #include<linux/module.h>
3 #include<linux/kernel.h>
4 #include<linux/sched.h>
5
6 static struct task_struct *pcurrent;
7 static int __init print_init(void)
8 {
9 printk(KERN_INFO "print current task info\n");
10 printk("pid\ttgid\tprio\tstate\n");
11 for_each_process(pcurrent){
12 printk("%d \t",pcurrent->pid);
13 printk("%d \t",pcurrent->tgid);
14 printk("%d \t",pcurrent->prio);
15 printk("%ld \n",pcurrent->state);
16 }
17 return 0;
18 }
19 static void __exit print_exit(void)
20 {
21 printk(KERN_INFO "Finished\n");
22 }
23 module_init(print_init);
24 module_exit(print_exit);

 

Makefile文件:

 

1 obj-m:=1.o
2 CURRENT_PATH:=$(shell pwd)
3 LINUX_KERNEL_PATH:=/usr/src/linux-headers-4.4.0-21-generic
4 all:
5 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
6 clean:
7 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

 

运行结果:

 《linux内核设计与分析》内核模块编程

四、高级模块

①列出当前进程虚拟内存的空间的各段权限。

②查找指定虚拟地址的所在的vma,并打印该段的起始地址、终止地址和段标志。③找出指定虚拟地址对应的物理地址。

④建立文件,进行代码的测试。

源代码来源于网络,但由于其内核版本过老,许多函数接口已经发生变化,代码并不能使用,所以现在正在改进,敬请期待~~