• 【博客441】Linux自制内核模块(LKM)


    Linux自制内核模块

    什么是内核模块

    内核模块是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是其代码可以在运行时链接到系统中作为内核的一部分运行或从内核中取下,从而可以动态扩充内核的功能。这种目标代码通常由一组函数和数据结构组成,用来实现一种文件系统,一个驱动程序,或其它内核上层的功能。

    模块机制的完整叫法应该是动态可加载内核模块(Loadable Kernel Module)或 LKM,一般就简称为模块。

    内核模块不是作为一个进程执行的,而像其他静态连接的内核函数一样,它在内核态代表当前进程执行。由于引入了模块机制,Linux的内核可以达到最小,即内核中实现一些基本功能,如从模块到内核的接口,内核管理所有模块的方式等等,而系统的可扩展性就留给模块来完成。

    内核模块特点

    内核模块是在“内核空间”中运行的,而应用程序运行在“用户空间”。

    内核模块必须通过init_module()函数注册,通过cleanup_module()函数注销。这也就是模块最大的特点,可以被动态地装入和卸载。

    insmod是内核模块操作工具集modutils中,把模块装入内核的命令,因为地址空间的原因,内核模块不能像应用程序那样自由地使用在用户空间定义的函数库如libc,例如printf();模块只能使用在内核空间定义的那些资源受到限制的函数,例如printk()。

    应用程序的源代码,可以调用本身没有定义的函数,只需要在连接过程中用相应的函数库解析那些外部引用。应用程序可调用的函数printf(),是在stdio.h中声明,并在libc中存在目标可连接代码。然而对于内核模块来说,它无法使用这个打印函数,而只能使用在内核空间中定义的printk()函数。printk()函数不支持浮点数的输出,而且输出数据量受到内核可用内存空间的限制。

    内核模块的另外一个困难,是内核失效对于整个系统或者对于当前进程常常是致命的,而在应用程序的开发过程中,缺段(segment fault)并不会造成什么危害,我们可以利用调试器轻松地跟踪到出错的地方。所以在内核模块编程的过程中,必须特别的小心

    内核模块优缺点

    优点:

    * 使得内核更加紧凑和灵活
    
    * 修改内核时,不必全部重新编译整个内核,可节省不少时间,避免人工操作的错误。
    * 系统中如果需要使用新模块,只要编译相应的模块然后使用特定用户空间的程序将模块插入即可。
    * 模块可以不依赖于某个固定的硬件平台。
    * 模块的目标代码一旦被链接到内核,它的作用和静态链接的内核目标代码完全等价。
      所以,当调用模块的函数时,无须显式的消息传递。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    缺点:

    * 由于内核所占用的内存是不会被换出的,链接进内核的模块会给整个系统带来一定的性能和内存方面的损失。
    * 装入内核的模块就成为内核的一部分,可以修改内核中的其他部分,因此,模块的使用不当会导致系统崩溃。
    * 为了让内核模块能访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改符号表。
      模块会要求利用其它模块的功能,所以,内核要维护模块之间的依赖性。
    
    • 1
    • 2
    • 3
    • 4

    为什么需要内核模块

    内核模块是一些可以让操作系统内核在需要时载入和执行代码,这同样意味着它可以在不需要时有操作系统卸载。它们扩展了操作系 统内核的功能却不需要重新启动系统。

    举例子来说:设备驱动程序模块,它们用来让操作系统正确识别,使用安装在系统上的硬件设备。如果没有内核模块,我们不得不一次又一次重新编译生成单内核操作系统的内核镜像来加入新的功能。

    即:当需要为内核编写设备驱动程序,扩展内核功能时,就可以通过编写内核模块扩展内核,模块可以动态的加载和移除,所以无须重新编译内核或者重启操作系统

    模块的组成

    基本结构:

    头文件+初始化函数+清除函数+引导内核的模块入口+引导内核的模块出口+模块许可证
    
    • 1

    头文件

    编写内核代码所用到的头文件包含在内核代码树的include/及其子目录中,例如module.h,kernel.h,init.h,这三个头文件全部包含在/include/linux/中。这三个头文件以预处理指令的形式写在模块源代码的首部:
    
     # include 
     # include 
     # include 
    在编译模块源文件之前,由预处理程序对预处理指令进行预处理。对于#include 来说,就是把module.h中的内容读进来,放在#include 的位置,取代了#include 指令行。然后由这些头文件的内容和其他部分一起组成一个完整的,可以用来编译的最后的源程序,然后由编译程序对该源程序正式进行编译,才得到目标程序。内核模块代码编译后得到目标文件后缀为.o
    
    # include 
    
    在内核代码树的位置为linux-2.6.0/include/linux/module.h,头文件module.h包含了对模块的结构定义以及模块的版本控制,可装载模块需要的大量符号和函数定义(初学阶段知道写模块必须包含这个头文件,还有头文件大致的内容,先知道是什么,后面再深入分析源代码)。module.h的源码如下(只是开头部分):
    
    #ifndef _LINUX_MODULE_H
    #define _LINUX_MODULE_H
    /*
     * Dynamic loading of modules into the kernel.  //动态加载到内核的模块
     *
     * Rewritten by Richard Henderson  Dec 1996
     * Rewritten again by Rusty Russell, 2002
     */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #include 
    
    /* Not Yet Implemented */
    #define MODULE_SUPPORTED_DEVICE(name)
    #define print_modules()
    # include 
    
    在内核代码树的位置为linux-2.6.0/include/linux/init.h,在init.h这个文件中包含了两个非常重要的宏__init 和 __exit。在init.h的源代码中,对于两种宏的用法和作用给出了说明
    
    源代码说明如下
    
    /* These macros are used to mark some functions or 
     * initialized data (doesn't apply to uninitialized data)
     * as `initialization' functions. The kernel can take this
     * as hint that the function is used only during the initialization
     * phase and free up used memory resources after
     *
     * Usage:
     * For functions:
     * 
     * You should add __init immediately before the function name, like:
     *
     * static void __init initme(int x, int y)
     * {
     *    extern int z; z = x * y;
     * }
     * 
     */
    宏__ init用于将一些函数标记为“初始化”函数。内核可以将此作为一个提示,即该函数仅在初始化阶段使用,并在初始化阶段之后释放使用的内存资源。模块被装载之后,模块装载器就会将初始化函数扔掉,这样可将该函数占用的资源释放出来。
    
    宏__init的用法如下:
    
     static void __init initme(int x, int y)  //放在函数返回值类型和函数名之间
     {
    	extern int z; z = x * y;
     }
    宏__ exit的用法和__ init一样,它的作用是标记该段代码仅用于模块卸载(编译器将把该函数放在特殊的ELF段中)。即被标记为__ exit的函数只能在模块被卸载时调用。
    
    # include 
    
    kernel.h包含了内核常用的API,比如printk()在kernel.h源代码的定义如下:
    
    int printk(const char * fmt, ...)
    
    • 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
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    模块功能函数

    也可以称为初始化函数,模块功能函数的定义如下:
    
    static int __init name_function(void)
    {
    	/* 模块要实现的功能 */
    	return 0;		
    }
    module_init(name_function);
    模块功能函数是在模块被装入内核后调用的,也就是在模块的代码被装入内核内存后,才调用模块功能函数。注意: __ init 标记只是一个可选项,并不是写所有模块代码都要加 __ init。但是在测试我们自己写的模块时,最好加上 __ init。因为我们在写一个模块功能函数的时候,可能这个函数里面有定义的变量,当调用这个函数的时候,就要为变量分配内存空间,但注意,此时分配给变量的内存,是在内核空间分配的,也就是说分配的是内核内存。所以说如果只是想要测试一下模块的功能,并不需要让模块常驻内核内存,那就应该在执行完函数后,将当初分配给变量的内存释放。为了达到这个效果,只需要把这个函数标记为 __init属性。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    清除函数

    清除函数的定义如下:
    
    static void __exit name_function(void)
    {
    	/* 这里是清除代码*/
    }
    module_exit(name_function);
    __ exit标记该段代码仅用于模块卸载,被标记为 __ exit的函数只能在模块被卸载或者系统关闭时调用。如果一个模块未被定义为清除函数,则内核不允许卸载该模块。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    驱动程序初始化入口点

    源码定义如下:
    
    /**
     * module_init() - driver initialization entry point
     * @x: function to be run at kernel boot time or module insertion
     * 
     * module_init() will either be called during do_initcalls (if
     * builtin) or at module insertion time (if a module).  There can only
     * be one per module.
     */
    # define module_init(x)	__initcall(x);
    module _ init()——驱动程序初始化入口点。 在内核引导时运行的函数,或者在do _ initcalls期间调用module _ init(),或者在模块插入时(如果是模块)调用module _ init()。每个模块只能有一个。
    
    驱动程序初始化出口点
    源码定义如下:
    
    /**
     * module_exit() - driver exit entry point
     * @x: function to be run when driver is removed
     * 
     * module_exit() will wrap the driver clean-up code
     * with cleanup_module() when used with rmmod when
     * the driver is a module.  If the driver is statically
     * compiled into the kernel, module_exit() has no effect.
     * There can only be one per module.
     */
     # define module_exit(x)	__exitcall(x);
    module _ exit()-驱动程序出口点。当驱动程序被删除时运行的函数。当驱动程序是一个模块时,module _ exit()将使用cleanup _ module()包装驱动程序清理代码。如果驱动程序被静态编译到内核中,则module _ exit()没有作用。每个模块只能有一个。
    
    • 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

    模块许可证

    编写内核模块,需要添加模块许可证。如果没有添加模块许可证,会收到内核被污染的警告
    module license unspecified taints kernel
    
    内核被污染可能会导致驱动程序的一些系统调用无法使用。
    
    • 1
    • 2
    • 3
    • 4

    example1

    helloworld.c

    #include         /* Needed by all modules */
    #include           /* Needed for KERN_INFO */
     
    int init_module(void)
    {
        printk(KERN_INFO “Hello World!\n”);
        return 0;
    }
     
    void cleanup_module(void)
    {
        printk(KERN_INFO “Goodbye!\n”);
    }
     
    MODULE_LICENSE(“GPL”);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Makefile

    TARGET = helloworld
     
    KDIR = /usr/src/linux
     
    PWD = $(shell pwd)
     
    obj-m += $(TARGET).o
     
    default:
           make -C $(KDIR) M=$(PWD) modules
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行:

    执行make得到文件“helloworld.ko”,然后执行内核模块的装入命令:
    
    insmod helloworld.ko
    
    执行结果:
    Hello World!
    
    查看模块:lsmod
    Module Size Used by
    helloworld 464 0 (unused)
    
    卸载模块:
    rmmod helloworld
    Goodbye!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    example2:与应用程序进行交互

    由于操作系统限制了应用程序查看内核空间内存的内容,因此,应用程序必须使用API与内核进行通信。尽管从技术上讲,有多种方法可以完成此操作,但最常见的方法是创建设备文件。

    接下来的示例中,我们的自定义设备 lkm_example 将返回 “Hello,World”,虽然这些字符串对于应用程序并没有什么用,但它将显示通过设备文件响应应用程序的过程。

    code

    #include 
    #include 
    #include 
    #include 
    #include 
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Robert W. Oliver II");
    MODULE_DESCRIPTION("A simple example Linux module.");
    MODULE_VERSION("0.01");
    
    #define DEVICE_NAME "lkm_example"
    #define EXAMPLE_MSG "Hello, World!\n"
    #define MSG_BUFFER_LEN 15
    
    /* Prototypes for device functions */
    static int device_open(struct inode *, struct file *);
    static int device_release(struct inode *, struct file *);
    static ssize_t device_read(struct file *, char *, size_t, loff_t *);
    static ssize_t device_write(struct file *, const char *, size_t, loff_t *);
                   
    static int major_num;
    static int device_open_count = 0;
    static char msg_buffer[MSG_BUFFER_LEN];
    static char *msg_ptr;
                   
    /* This structure points to all of the device functions */
    static struct file_operations file_ops = {
     .read = device_read,
     .write = device_write,
     .open = device_open,
     .release = device_release
    };
                   
    /* When a process reads from our device, this gets called. */
    static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) {
    	int bytes_read = 0;
     	/* If we’re at the end, loop back to the beginning */
     	if (*msg_ptr == 0) {
     		msg_ptr = msg_buffer;
     	}
     	/* Put data in the buffer */
     	while (len && *msg_ptr) {
        /* Buffer is in user data, not kernel, so you can’t just reference
         * with a pointer. The function put_user handles this for us */
        put_user(*(msg_ptr++), buffer++);
        len--;
        bytes_read++;
    	}
     	return bytes_read;
    }
                   
    /* Called when a process tries to write to our device */
    static ssize_t device_write(struct file *flip, const char *buffer, size_t len, loff_t *offset) {
    	/* This is a read-only device */
     	printk(KERN_ALERT "This operation is not supported.\n");
     	return -EINVAL;
    }
             
    /* Called when a process opens our device */
    static int device_open(struct inode *inode, struct file *file) {
     	/* If device is open, return busy */
     	if (device_open_count) {
     		return -EBUSY;
     	}
     	device_open_count++;
     	try_module_get(THIS_MODULE);
     	return 0;
    }
             
    /* Called when a process closes our device */
    static int device_release(struct inode *inode, struct file *file) {
     	/* Decrement the open counter and usage count. Without this, the module would not unload. */
     	device_open_count--;
     	module_put(THIS_MODULE);
     	return 0;
    }
             
    static int __init lkm_example_init(void) {
     	/* Fill buffer with our message */
     	strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN);
     	/* Set the msg_ptr to the buffer */
     	msg_ptr = msg_buffer;
     	/* Try to register character device */
     	major_num = register_chrdev(0, "lkm_example", &file_ops);
     	if (major_num < 0) {
      	printk(KERN_ALERT "Could not register device: %d\n", major_num);
      	return major_num;
     	} else {
      	printk(KERN_INFO "lkm_example module loaded with device major number %d\n", major_num);
      	return 0;
     	}
    }
    
    static void __exit lkm_example_exit(void) {
     	/* Remember — we have to clean up after ourselves. Unregister the character device. */
     	unregister_chrdev(major_num, DEVICE_NAME);
     	printk(KERN_INFO "Goodbye, World!\n");
    }
    
    /* Register module functions */
    module_init(lkm_example_init);
    module_exit(lkm_example_exit);
    
    • 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
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103

    Makefile

    obj-m += lkm_example.o
    all:
     	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    test:
      # We put a — in front of the rmmod command to tell make to ignore
      # an error in case the module isn’t loaded.
      -sudo rmmod lkm_example
      # Clear the kernel log without echo
      sudo dmesg -C
      # Insert the module
      sudo insmod lkm_example.ko
      # Display the kernel log
      dmesg
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行 “make test” 时,将看到设备主号码的输出,这是由内核自动分配的,但是,你需要此值来创建设备。

    获取从 “make test” 获得的值,并使用它来创建设备文件,以便我们可以从用户空间与内核模块进行通信:

    sudo mknod /dev/lkm_example c MAJOR 0
    
    • 1

    将MAJOR替换为你运行 “make test” 或 “dmesg” 后得到的值,比如:MAJOR为111,mknod命令中的 “c” 告诉mknod我们需要创建一个字符设备文件。

    现在我们可以从设备中获取内容:

    cat /dev/lkm_example
    
    • 1

    或者通过 “dd” 命令:

    dd if=/dev/lkm_example of=test bs=14 count=100
    
    • 1

    你也可以通过应用程序访问此设备

    完成测试后,将其删除并卸载模块:

    sudo rm /dev/lkm_example
    sudo rmmod lkm_example
    
    • 1
    • 2

    other example

    http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/

    https://github.com/derekmolloy/exploringBB/tree/master/extras/kernel

  • 相关阅读:
    天谋科技 Timecho 完成近亿元人民币天使轮融资,打造工业物联网原生时序数据库
    java基于微信小程序的新冠疫苗接种预约系统 uinapp
    计网课设-发送TCP数据包
    tcpreplay命令后加上“--maxsleep=num“,num表示最大延迟时间(单位毫秒)
    【esp32】xQueueReceive 函数调试踩坑记录
    log4j配置
    使用stream的skip方法进行分页处理
    什么是解构赋值?
    文案配音软件哪个好?(适合新手使用)
    计算机毕业设计springboot健康管理系统1ii1u源码+系统+程序+lw文档+部署
  • 原文地址:https://blog.csdn.net/qq_43684922/article/details/126318039