首页
社区
课程
招聘
[原创]Linux内核入门——驱动模块
发表于: 2022-1-19 20:54 35069

[原创]Linux内核入门——驱动模块

2022-1-19 20:54
35069

一个hello world程序

这里会给出一个简单的例子来展示Linux内核模块的编写过程。下面是虚拟机环境信息:

1
2
3
4
5
6
7
8
➜  ~ uname -a
Linux unravel 5.11.0-46-generic #51~20.04.1-Ubuntu SMP Fri Jan 7 06:51:40 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
 
➜  ~ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
 
➜  ~ make --version
GNU Make 4.2.1

要编译内核模块,还需要安装以下软件包:

1
sudo apt-get install build-essential

例子

以下代码实现了一个简单的内核模块。MODULE_LICENSE是模块声明,不加这句在ubuntu 20下编译会报error。模块加载时会执行init_module,模块卸载时会执行cleanup_module

1
2
3
4
5
6
7
8
9
10
11
12
#include /* Needed by all modules */
#include /* Needed for KERN_INFO */
MODULE_LICENSE("GPL");
 
int init_module(void) {
    printk(KERN_INFO "Hello world - unr4v31.\n");
    return 0;
}
 
void cleanup_module(void) {
    printk(KERN_INFO "Goodbye world - unr4v31.\n");
}

然后编辑Makefile来构建模块(这里需要注意制表符的长度,Makefile对格式有严格要求):

1
2
3
4
5
6
7
obj-m += sample.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

按照以上的Makefile进行编译,最终会得到一个sample.ko的内核模块(还有一些编译过程中产生的文件):

1
2
➜  sample ls
Makefile  modules.order  Module.symvers  sample.c  sample.ko  sample.mod  sample.mod.c  sample.mod.o  sample.o

可以使用modinfo命令查看刚才创建的模块信息:

1
2
3
4
5
6
7
8
➜  sample modinfo sample.ko
filename:       /home/unravel/Desktop/sample/sample.ko
license:        GPL
srcversion:     AFEF977BDFC4AE5E3821000
depends:       
retpoline:      Y
name:           sample
vermagic:       5.11.0-46-generic SMP mod_unload modversions

注册和删除模块

  • insmod命令在Linux内核中注册模块
  • lsmod命令检查已经注册过的模块
1
2
3
4
5
6
7
8
9
10
11
12
13
➜  sample lsmod
Module                  Size  Used by
sample                 16384  0
nls_iso8859_1          16384  1
nvidia_uvm           1028096  0
intel_rapl_msr         20480  0
intel_rapl_common      24576  1 intel_rapl_msr
nvidia_drm             61440  10
nvidia_modeset       1150976  13 nvidia_drm
x86_pkg_temp_thermal    20480  0
......
sample                 16384  0
......
  • dmesg命令可以查看模块输出的消息(部分情况需要root权限)
1
2
3
4
5
6
7
➜  sample dmesg
[    0.000000] microcode: microcode updated early to revision 0xea, date = 2021-01-05
[    0.000000] Linux version 5.11.0-46-generic (buildd@lgw01-amd64-010) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #51~20.04.1-Ubuntu SMP Fri Jan 7 06:51:40 UTC 2022 (Ubuntu 5.11.0-46.51~20.04.1-generic 5.11.22)
......
[   20.474554] audit: type=1400 audit(1642506075.478:44): apparmor="DENIED" operation="open" profile="snap.snap-store.ubuntu-software" name="/etc/PackageKit/Vendor.conf" pid=2760 comm="snap-store" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0
[ 6287.886473] perf: interrupt took too long (2501 > 2500), lowering kernel.perf_event_max_sample_rate to 79750
[ 8920.311392] Hello world - unr4v31.
  • rmmod命令从内核移除已注册的模块
1
2
3
4
5
6
7
➜  sample sudo rmmod sample
➜  sample dmesg
[    0.000000] microcode: microcode updated early to revision 0xea, date = 2021-01-05
[    0.000000] Linux version 5.11.0-46-generic (buildd@lgw01-amd64-010) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #51~20.04.1-Ubuntu SMP Fri Jan 7 06:51:40 UTC 2022 (Ubuntu 5.11.0-46.51~20.04.1-generic 5.11.22)
......
[ 8920.311392] Hello world - unr4v31.
[ 8961.827092] Goodbye world - unr4v31.

字符设备模块

字符设备驱动是一种在不使用缓冲区高速缓存的情况下一次读取和写入一个字符数据的驱动程序,例如:键盘、声卡、打印机驱动程序。

 

此外还有块设备和网络驱动程序:

  • 块设备驱动程序允许通过缓冲区告诉缓存和块单元中的I/O进行随机访问,例如硬盘。
  • 网络设备驱动程序位于网络堆栈和网络硬件之间,负责发送和接受数据,例如以太网、网卡。

file_operations结构是为字符设备、块设备驱动程序与通用程序之间的通信提供的接口。可以使用结构体内的函数指针,例如:read, write, open, release, unlocked_ioctl。而网络设备不使用file_operations 结构,应当使用include/linux/netdevice.h中的net_device结构体。

 

下面是Linux 5.11的file_operations结构体内容:

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
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, bool spin);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
            loff_t, size_t, unsigned int);
    loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
                   struct file *file_out, loff_t pos_out,
                   loff_t len, unsigned int remap_flags);
    int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

使用下面的方式绑定设备模块中的open函数,在使用open系统调用时内核会输出“chardev_open”:

1
2
3
4
5
6
7
8
static int chardev_open(struct inode *inode, struct file *file)
{
    printk("chardev_open");
    return 0;
}
struct file_operations chardev_fops = {
    .open    = chardev_open,
};

一个简单的例子

环境信息(这里我换了个ubuntu的环境,不过小版本差别不大):

1
2
3
4
5
6
7
8
➜  ~ uname -a
Linux unravel 5.11.0-43-generic #47~20.04.2-Ubuntu SMP Mon Dec 13 11:06:56 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
 
➜  ~ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
 
➜  ~ make --version
GNU Make 4.2.1

编写如下模块源码,保存为chardev.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
 
#define DEVICE_NAME "chardev"
#define DEVICE_FILE_NAME "chardev"
#define MAJOR_NUM 100
MODULE_LICENSE("GPL");
 
static int chardev_open(struct inode *inode, struct file *file)
{
    printk("chardev_open");
    return 0;
}
 
struct file_operations chardev_fops = {
    .open    = chardev_open,
};
 
static int chardev_init(void)
{
    int ret_val;
    ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &chardev_fops);
 
    if (ret_val < 0) {
    printk(KERN_ALERT "%s failed with %d\n",
           "Sorry, registering the character device ", ret_val);
    return ret_val;
    }
 
    printk(KERN_INFO "%s The major device number is %d.\n",
       "Registeration is a success", MAJOR_NUM);
    printk(KERN_INFO "If you want to talk to the device driver,\n");
    printk(KERN_INFO "you'll have to create a device file. \n");
    printk(KERN_INFO "We suggest you use:\n");
    printk(KERN_INFO "mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM);
    printk(KERN_INFO "The device file name is important, because\n");
    printk(KERN_INFO "the ioctl program assumes that's the\n");
    printk(KERN_INFO "file you'll use.\n");
 
    return 0;
}
 
static void chardev_exit(void)
{
    unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
}
 
module_init(chardev_init);
module_exit(chardev_exit);

关于这段代码:

  • chardev_init函数在注册时执行。在这个函数中,register_chrdev函数注册对应字符设备的主设备号。
  • 当在用户空间使用open系统调用时会调用chardev_open函数。chardev_open函数会让内核输出一条信息。
  • chardev_exit函数在模块从内核删除时调用,对应的主设备号由unregister_chrdev函数删除。

编写Makefile:

1
2
3
4
5
6
obj-m := chardev.o
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

make后会得到驱动文件chardev.ko和一些其他文件:

 

图片描述
然后测试使用insmod装载模块并用dmesg查看内核输出的信息:

 

图片描述
接下来我们创建设备文件,然后测试open系统调用是否触发内核输出“chardev_open”。步骤如下:

  • 使用mknod命令将加载的模块创建为设备文件。这里介绍一下mknod命令的用法:
    • 基本格式:mknod <设备文件名> <设备文件格式> <主设备号> <次设备号>
    • 设备文件格式有三种:p(FIFO先进先出)、b(block device file 块设备文件)、cu(character special file字符特殊文件,无缓冲的特殊文件)
    • 主设备号和次设备号:主设备号是分配给块设备或字符设备的数字;次设备号是分配给由MAJOR限定的字符设备组之一的编号。简单来说就是可以用这两个数字来识别设备。
  • 使用chmod命令配置普通用户的读写权限。
  • 使用echo命令打开设备文件,保存“A”,很明显它不会被保存,我们目的只是触发一个open系统调用。
  • 使用dmesg检查内核是否输出字符串。也就是说,可以通过echo命令操作chardev_open函数。

    图片描述
    图片描述

    另一个例子

这次在file_operations绑定更多的函数:open, release, read, write

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
 
MODULE_LICENSE("GPL");
 
#define DRIVER_NAME "chardev"
#define BUFFER_SIZE 256
 
static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM  = 2;
static unsigned int chardev_major;
static struct cdev chardev_cdev;
static struct class *chardev_class = NULL;
 
static int     chardev_open(struct inode *, struct file *);
static int     chardev_release(struct inode *, struct file *);
static ssize_t chardev_read(struct file *, char *, size_t, loff_t *);
static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *);
 
struct file_operations chardev_fops = {
    .open    = chardev_open,
    .release = chardev_release,
    .read    = chardev_read,
    .write   = chardev_write,
};
 
struct data {
    unsigned char buffer[BUFFER_SIZE];
};
 
static int chardev_init(void)
{
    int alloc_ret = 0;
    int cdev_err = 0;
    int minor;
    dev_t dev;
 
    printk("The chardev_init() function has been called.");
 
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
    if (alloc_ret != 0) {
        printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
        return -1;
    }
    //Get the major number value in dev.
    chardev_major = MAJOR(dev);
    dev = MKDEV(chardev_major, MINOR_BASE);
 
    //initialize a cdev structure
    cdev_init(&chardev_cdev, &chardev_fops);
    chardev_cdev.owner = THIS_MODULE;
 
    //add a char device to the system
    cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM);
    if (cdev_err != 0) {
        printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }
 
    chardev_class = class_create(THIS_MODULE, "chardev");
    if (IS_ERR(chardev_class)) {
        printk(KERN_ERR  "class_create\n");
        cdev_del(&chardev_cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }
 
    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor);
    }
 
    return 0;
}
 
static void chardev_exit(void)
{
    int minor;
    dev_t dev = MKDEV(chardev_major, MINOR_BASE);
 
    printk("The chardev_exit() function has been called.");
 
    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_destroy(chardev_class, MKDEV(chardev_major, minor));
    }
 
    class_destroy(chardev_class);
    cdev_del(&chardev_cdev);
    unregister_chrdev_region(dev, MINOR_NUM);
}
 
static int chardev_open(struct inode *inode, struct file *file)
{
    char *str = "helloworld";
    int ret;
 
    struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL);
 
    printk("The chardev_open() function has been called.");
 
    if (p == NULL) {
        printk(KERN_ERR  "kmalloc - Null");
        return -ENOMEM;
    }
 
    ret = strlcpy(p->buffer, str, sizeof(p->buffer));
    if(ret > strlen(str)){
        printk(KERN_ERR "strlcpy - too long (%d)",ret);
    }
 
    file->private_data = p;
    return 0;
}
 
static int chardev_release(struct inode *inode, struct file *file)
{
    printk("The chardev_release() function has been called.");
    if (file->private_data) {
        kfree(file->private_data);
        file->private_data = NULL;
    }
    return 0;
}
 
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct data *p = filp->private_data;
 
    printk("The chardev_write() function has been called.");  
    printk("Before calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
    if (copy_from_user(p->buffer, buf, count) != 0) {
        return -EFAULT;
    }
    printk("After calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
    return count;
}
 
static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct data *p = filp->private_data;
 
    printk("The chardev_read() function has been called.");
 
    if(count > BUFFER_SIZE){
        count = BUFFER_SIZE;
    }
 
    if (copy_to_user(buf, p->buffer, count) != 0) {
        return -EFAULT;
    }
 
    return count;
}
 
module_init(chardev_init);
module_exit(chardev_exit);

源码解读:

  • 内核注册模块时调用chardev_init ,它处理以下函数:
    • 使用alloc_chrdev_region函数在系统中注册字符设备号(与上一节给定的设备号100不同,我们应该让内核来分配设备号)。
    • 使用majormkdev函数来获取在设备中使用的主设备号和次设备号。
    • 使用cdev_init函数初始化chardev_cdev结构。
    • 使用cdev_add函数将字符设备添加到系统。
    • 使用class_create函数创建要在系统中创建的设备类。
    • 使用device_create函数在系统中创建设备。
  • 内核删除模块时调用chardev_exit,它处理以下函数:
    • 使用device_destroy函数销毁由device_create函数创建的设备
    • 使用class_destroy函数销毁由class_create函数创建的设备类
    • 使用cdev_del函数删除cdev_add函数添加的字符设备
    • 使用unregister_chrdev_region函数将alloc_chrdev_region函数注册的设备号返还给系统
  • 在用户态使用open系统调用,都会调用chardev_open,它处理以下函数:
    • 使用kmalloc函数在内核堆中分配一个与data结构体大小相同的空间
    • 使用strcpy函数将str变量中的值复制到p->buffer
  • 在用户态关闭设备时调用chardev_release,它处理以下函数:
    • 使用kfree释放分配的堆区域
  • 在用户态向设备写入数据时调用chardev_write,它处理以下函数:
    • 使用copy_from_user从用户空间接受数据,从buf复制到p->buffer
  • 当从设备向用户空间写入数据时调用chardev_read,它处理以下函数:
    • 使用copy_to_user函数将存储在内核区域p->buffer的内容拷贝到用户空间的buf

Makefile如下:

1
2
3
4
5
6
obj-m := chardev.o
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

这次需要使用C代码来调用模块接口。编写如下测试程序test.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
#include
#include
#include
#include
#include
 
#define TEXT_LEN 12
 
int main()
{
    static char buff[256];
    int fd;
 
    if ((fd = open("/dev/chardev0", O_RDWR)) < 0){
        printf("Cannot open /dev/chardev0. Try again later.\n");
    }
 
    if (write(fd, "unr4v31", TEXT_LEN) < 0){
        printf("Cannot write there.\n");
    }
 
    if (read(fd, buff, TEXT_LEN) < 0){
        printf("An error occurred in the read.\n");
    }else{
        printf("%s\n", buff);
    }
 
    if (close(fd) != 0){
        printf("Cannot close.\n");
    }
    return 0;
}

现在make一下模块文件:

 

图片描述
在将模块注册到内核之前,先将注册模块时自动创建的设备文件的规则保存到/etc/udev/rules.d路径下(需要root权限):

1
echo 'KERNEL == "chardev[0-9]*",GROUP="root",MODE="0666"' >> /etc/udev/rules.d/80-chardev.rules

图片描述
当使用insmod注册模块时,会在/dev路径下自动创建两个设备chardev0chardev1,这两个设备的权限是666,普通用户也可以访问:

1
sudo insmod chardev.ko

图片描述
然后编译test.c运行,并观察openreadwriterelease函数对应在内核中的输出:

1
2
gcc -o test test.c
./test

图片描述
或者以另一种方式来测试模块文件。编写如下代码为test1.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
#include
#include
#include
#include
#include
 
int main()
{
    static char buff[256];
    int fd0_A, fd0_B, fd1_A;
 
    if ((fd0_A = open("/dev/chardev0", O_RDWR)) < 0) perror("open");
    if ((fd0_B = open("/dev/chardev0", O_RDWR)) < 0) perror("open");
    if ((fd1_A = open("/dev/chardev1", O_RDWR)) < 0) perror("open");
 
    if (write(fd0_A, "0_A", 4) < 0) perror("write");
    if (write(fd0_B, "0_B", 4) < 0) perror("write");
    if (write(fd1_A, "1_A", 4) < 0) perror("write");
 
    if (read(fd0_A, buff, 4) < 0) perror("read");
    printf("%s\n", buff);
    if (read(fd0_B, buff, 4) < 0) perror("read");
    printf("%s\n", buff);
    if (read(fd1_A, buff, 4) < 0) perror("read");
    printf("%s\n", buff);
 
    if (close(fd0_A) != 0) perror("close");
    if (close(fd0_B) != 0) perror("close");
    if (close(fd1_A) != 0) perror("close");
 
    return 0;
}

编译运行后,从test1的运行结果得知,相同的模块或相同的设备,但用于存储传递的字符串的堆地址不同:

 

图片描述

ioctl(Input/Output control)

ioctl是一种用于获取硬件控制和状态信息的操作。通过readwrite可以实现数据读写等功能,但无法检查硬件控制和状态信息,例如SPI通信速度、I2C等不能仅通过读写操作完成。例如CD-ROM设备驱动程序提供了一个ioctl请求代码,可以使物理设备弹出磁盘。

 

ioctl函数原型如下:


[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

收藏
免费 12
支持
分享
最新回复 (3)
雪    币: 748
活跃值: (3133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
2022-2-9 10:56
0
雪    币: 4168
活跃值: (15932)
能力值: ( LV9,RANK:710 )
在线值:
发帖
回帖
粉丝
3
加油!
2022-3-18 17:04
0
雪    币: 4168
活跃值: (15932)
能力值: ( LV9,RANK:710 )
在线值:
发帖
回帖
粉丝
4
加油!
2022-3-18 17:05
0
游客
登录 | 注册 方可回帖
返回