如何从头开始开发 Linux 驱动程序

大家好!我是大聪明-PLUS

我最近一直在研究物联网,由于设备短缺,在尝试模拟固件时经常遇到缺少必要的 /dev/xxx 的情况。于是我开始思考是否可以自己编写一个驱动程序来让固件正常工作。无论这有多难,也无论我是否成功,你都不会后悔学习如何从头开始开发 Linux 驱动程序。

❯ 介绍

我撰写了一系列主要侧重实践的文章,理论背景较少。我从《Linux 设备驱动程序》一书中学习了如何开发驱动程序,该书中讨论的示例代码可在GitHub上找到。

首先介绍一下基础知识,Linux 操作系统分为内核空间和用户空间。访问硬件设备只能通过内核空间,而设备驱动程序可以被视为内核空间提供的 API,允许用户空间代码访问设备。

基于这些基本概念,我发现了几个促使我学习驱动程序开发的问题。

  1. 在编程中,学习总是从一个Hello World程序开始,那么在这种情况下如何编写一个Hello World程序呢?
  2. 驱动程序如何在/dev下生成设备文件?
  3. 驱动程序究竟如何访问现有硬件?
  4. 我该如何编写系统管理代码?或者,是否可以在不编写代码的情况下提取驱动程序?存储驱动程序的二进制文件在哪里?将来,所有这些都可以进行测试,以确定特定设备的安全性。

❯ 一切从 Hello World 开始

以下是我的Hello World程序的内容:

复制代码
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Dacongming");
int hello_init(void)
{
    printk(KERN_INFO "Hello World\n");
    return 0;
}
void hello_exit(void)
{
    printk(KERN_INFO "Goodbye World\n");
}
module_init(hello_init);
module_exit(hello_exit);

Linux 驱动程序是用 C 开发的,我对 C 语言并不是特别熟悉。在工作中,我经常使用 Libc 库,它不包含在内核中。由于驱动程序是在内核中运行的程序,因此我们使用内核中的库函数。

例如,printk是在内核中定义的输出函数;它类似于 Libc 中的 printf。但对我来说,它更像是 Python 中的日志函数,因为输出printk直接发送到内核日志,并且可以使用命令查看此日志dmesg

驱动程序代码只有一个入口点和一个出口点。当驱动程序加载到内核中时,将执行由 定义的函数module_init,该函数在上面的代码中称为hello_init。当驱动程序从内核中卸载时,将调用 定义的函数module_exit,该函数在上面的代码中称为hello_exit

从上面的代码中可以清楚地看出,在加载时,驱动程序会打印 Hello World,而在卸载时,它会打印Goodbye World

顺便说一句:MODULE_LICENSEMODULE_AUTHOR并不是那么重要。我在这里就不详细讨论了。

还有一点:printk函数输出时必须添加换行符,否则缓冲区不会清空。

❯ 编译驱动程序

该驱动需要使用命令进行编译make,对应的Makefile如下:

复制代码
ifneq ($(KERNELRELEASE),)
    obj-m := hello.o
else
    KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERN_DIR) M=$(PWD) modules
endif
clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

一般来说,内核源代码位于/usr/src/linux-headers-$(shell uname -r)/目录下。

我们需要一个用于存放编译源代码的目录,即/usr/src/linux-headers-4.4.0-135-generic/

驱动程序代码的头文件将从此目录搜索。M

=$(PWD) 参数指定将驱动程序编译输出保存到当前目录。

最后,这是命令obj-m := hello.o,用于加载hello.ohello.ko,ko 是来自内核空间的文件。

❯ 将驱动程序加载到内核

以下是我们需要的一些系统命令:

  • Lsmod:查看当前正在加载的内核模块。
  • Insmod:加载内核模块,然后要求管理员权限。
  • Rmmod:删除模块。

例如:

复制代码
# insmod hello.ko        // Load the hello.ko module into the kernel
# rmmod hello          // Remove the hello module from the kernel

在较旧的内核版本中,内核本身的加载和删除使用相同的方法,但较新的 Linux 内核版本添加了模块验证。我们现在的情况如下:

复制代码
# insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Required key not available

从安全角度来看,当前内核假定此模块不受信任。要加载此模块,必须使用受信任的证书对其进行签名。

这可以通过两种方式完成:

  1. 进入 BIOS 并在 UEFI 中禁用安全启动。
  2. 向内核添加自签名证书并使用它来签署驱动程序模块。

❯ 查看结果

❯ 在 /dev 下添加设备文件

再次强调,我们首先提供代码,然后解释示例代码。

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>   /* printk() */
#include <linux/slab.h>     /* kmalloc() */
#include <linux/fs.h>       /* everything... */
#include <linux/errno.h>    /* error codes */
#include <linux/types.h>    /* size_t */
#include <linux/fcntl.h>    /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h>    /* copy_*_user */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Dacongming");
int scull_major =   0;
int scull_minor =   0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;
struct scull_qset {
    void **data;
    struct scull_qset *next;
};
struct scull_dev {
    struct scull_qset *data;  /* Pointer to first quantum set. */
    int quantum;              /* The current quantum size. */
    int qset;                 /* The current array size. */
    unsigned long size;       /* Amount of data stored here. */
    unsigned int access_key;  /* Used by sculluid and scullpriv. */
    struct mutex mutex;       /* Mutual exclusion semaphore. */
    struct cdev cdev;     /* Char device structure. */
};
struct scull_dev *scull_devices;    /* allocated in scull_init_module */
/*
 * Follow the list.
 */
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
    struct scull_qset *qs = dev->data;
        /* Allocate the first qset explicitly if need be. */
    if (! qs) {
        qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
        if (qs == NULL)
            return NULL;
        memset(qs, 0, sizeof(struct scull_qset));
    }
    /* Then follow the list. */
    while (n--) {
        if (!qs->next) {
            qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
            if (qs->next == NULL)
                return NULL;
            memset(qs->next, 0, sizeof(struct scull_qset));
        }
        qs = qs->next;
        continue;
    }
    return qs;
}
/*
 * Data management: read and write.
 */
ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr; /* the first listitem */
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset; /* how many bytes in the listitem */
    int item, s_pos, q_pos, rest;
    ssize_t retval = 0;
    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;
    if (*f_pos >= dev->size)
        goto out;
    if (*f_pos + count > dev->size)
        count = dev->size - *f_pos;
    /* Find listitem, qset index, and offset in the quantum */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum; q_pos = rest % quantum;
    /* follow the list up to the right position (defined elsewhere) */
    dptr = scull_follow(dev, item);
    if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
        goto out; /* don't fill holes */
    /* read only up to the end of this quantum */
    if (count > quantum - q_pos)
        count = quantum - q_pos;
    if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;
  out:
    mutex_unlock(&dev->mutex);
    return retval;
}
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr;
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset;
    int item, s_pos, q_pos, rest;
    ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */
    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;
    /* Find the list item, qset index, and offset in the quantum. */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum;
    q_pos = rest % quantum;
    /* Follow the list up to the right position. */
    dptr = scull_follow(dev, item);
    if (dptr == NULL)
        goto out;
    if (!dptr->data) {
        dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
        if (!dptr->data)
            goto out;
        memset(dptr->data, 0, qset * sizeof(char *));
    }
    if (!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos])
            goto out;
    }
    /* Write only up to the end of this quantum. */
    if (count > quantum - q_pos)
        count = quantum - q_pos;
    if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;
        /* Update the size. */
    if (dev->size < *f_pos)
        dev->size = *f_pos;
  out:
    mutex_unlock(&dev->mutex);
    return retval;
}
/* Beginning of the scull device implementation. */
/*
 * Empty out the scull device; must be called with the device
 * mutex held.
 */
int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset;   /* "dev" is not-null */
    int i;
    for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
        if (dptr->data) {
            for (i = 0; i < qset; i++)
                kfree(dptr->data[i]);
            kfree(dptr->data);
            dptr->data = NULL;
        }
        next = dptr->next;
        kfree(dptr);
    }
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;
    return 0;
}
int scull_release(struct inode *inode, struct file *filp)
{
    printk(KERN_DEBUG "process %i (%s) success release minor(%u) file\n", current->pid, current->comm, iminor(inode));
    return 0;
}
/*
 * Open and close
 */
int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev; /* device information */
    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev; /* for other methods */
    /* If the device was opened write-only, trim it to a length of 0. */
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
        if (mutex_lock_interruptible(&dev->mutex))
            return -ERESTARTSYS;
        scull_trim(dev); /* Ignore errors. */
        mutex_unlock(&dev->mutex);
    }
    printk(KERN_DEBUG "process %i (%s) success open minor(%u) file\n", current->pid, current->comm, iminor(inode));
    return 0;
}
/*
 * The "extended" operations -- only seek.
 */
loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
    struct scull_dev *dev = filp->private_data;
    loff_t newpos;
    switch(whence) {
      case 0: /* SEEK_SET */
        newpos = off;
        break;
      case 1: /* SEEK_CUR */
        newpos = filp->f_pos + off;
        break;
      case 2: /* SEEK_END */
        newpos = dev->size + off;
        break;
      default: /* can't happen */
        return -EINVAL;
    }
    if (newpos < 0)
        return -EINVAL;
    filp->f_pos = newpos;
    return newpos;
}
struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    // .unlocked_ioctl = scull_ioctl,
    .open =     scull_open,
    .release =  scull_release,
};
/*
 * Set up the char_dev structure for this device.
 */
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be. */
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
    else
        printk(KERN_INFO "scull: %d add success\n", index);
}
void scull_cleanup_module(void)
{
    int i;
    dev_t devno = MKDEV(scull_major, scull_minor);
    /* Get rid of our char dev entries. */
    if (scull_devices) {
        for (i = 0; i < scull_nr_devs; i++) {
            scull_trim(scull_devices + i);
            cdev_del(&scull_devices[i].cdev);
        }
        kfree(scull_devices);
    }
    /* cleanup_module is never called if registering failed. */
    unregister_chrdev_region(devno, scull_nr_devs);
    printk(KERN_INFO "scull: cleanup success\n");
}
int scull_init_module(void)
{
    int result, i;
    dev_t dev = 0;
    /*
     * Get a range of minor numbers to work with, asking for a dynamic major
     * unless directed otherwise at load time.
     */
    if (scull_major) {
        dev = MKDEV(scull_major, scull_minor);
        result = register_chrdev_region(dev, scull_nr_devs, "scull");
    } else {
        result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
        scull_major = MAJOR(dev);
    }
    if (result < 0) {
        printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
        return result;
    } else {
        printk(KERN_INFO "scull: get major %d success\n", scull_major);
    }
        /*
     * Allocate the devices. This must be dynamic as the device number can
     * be specified at load time.
     */
    scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
    if (!scull_devices) {
        result = -ENOMEM;
        goto fail;
    }
    memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));
        /* Initialize each device. */
    for (i = 0; i < scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset = scull_qset;
        mutex_init(&scull_devices[i].mutex);
        scull_setup_cdev(&scull_devices[i], i);
    }
    return 0; /* succeed */
  fail:
    scull_cleanup_module();
    return result;
}
module_init(scull_init_module);
module_exit(scull_cleanup_module);

❯ 驱动程序分类

驱动程序分为三类:字符设备、块设备和网络接口。上面的代码主要针对字符设备,其他两类设备的讨论超出了本文的范围。

如上所示,brw-rw--块设备的访问权限字符串以字母"b"开头,而字符设备的访问权限字符串以字母"c"开头。

❯ 关于主编号和次编号

主编号用于区分一个驱动程序与所有其他驱动程序。原则上,如果设备具有相同的主编号,则表示它们由同一个驱动程序控制。

可以在一个驱动器目录中创建多个设备,它们之间只有次编号不同。主编号和次编号共同表征了驱动程序控制的设备(如上所示)。

复制代码
brw-rw----  1 root disk      8,   0 Dec 17 13:02 sda
brw-rw----  1 root disk      8,   1 Dec 17 13:02 sda1

该设备的最高有效数字sdasda18,最低有效数字有两个:一个设备为0,另一个设备为1。

❯ 驱动程序如何提供 API

我习惯将/dev/xxx其视为文件提供的接口,而在 Linux 中,"一切皆文件"。因此,操作驱动程序时,我们本质上就是在操作文件,而定义/打开/读取/写入......操作的都是驱动程序/dev/xxx。任何使用驱动程序 API 可以想到的操作都属于文件操作。这里涉及哪些文件操作?它们都定义在内核头文件<linux/fs.h>中的 file_operations 结构体中。 在上面的示例代码中:

复制代码
struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    .open =     scull_open,
    .release =  scull_release,
};

我定义该结构体并对其进行赋值。除了 owner 之外,其他所有成员的值都是函数指针。

然后,我使用 [ ]cdev_add为每个驱动程序注册该结构体,用于文件操作,这在 [ ] 函数中完成scull_setup_cdev

例如,当对驱动程序管理的设备执行"打开"操作时,我会执行 scull_open 函数,这相当于在系统调用中"挂载"了 open 函数。

❯ 如何在 /dev 下生成我们需要的设备

编译上述代码后,我们得到scull.ko,然后对其进行签名,最后使用 将其加载到内核中insmod

让我们检查它是否加载成功:

是的,设备驱动程序已成功加载,但它没有在 /dev 目录中创建设备文件。您必须手动使用 mknod 来关联该设备:

❯ 结果

在这个例子中,我们没有对特定的设备执行任何操作;我们只是使用了 [ ]kmalloc并用它来访问内核空间中的一块内存。

我不会进一步讨论代码的细节;您可以通过百度搜索信息或研究头文件来找到它们。

在本文中,我想与您分享如何学习自学开发驱动程序:首先,阅读书籍以掌握基本概念,然后在实际应用时查找有关具体细节的信息。

例如,我不知道驱动程序提供了什么 API。我只需要知道这样的 API 仅限于文件操作。目前,我只需要 [ ] openclose[ ]read和 [ write] 操作。如果需要,我可以检查其他文件操作是如何执行的。

相关推荐
心灵宝贝3 小时前
CentOS 7 安装 net-tools.rpm 包步骤详解(附 rpm 命令和 yum 方法)附安装包
linux·运维·centos
1024find3 小时前
Linux基线配置
linux·运维·服务器
从零开始的ops生活3 小时前
【Day 68】Zabbix-自动监控-Web检测-分布式监控
linux·网络·zabbix
jun~5 小时前
SQLMap数据库枚举靶机(打靶记录)
linux·数据库·笔记·学习·安全·web安全
小码农<^_^>5 小时前
Linux(线程控制)
linux
HappyGame025 小时前
Linux多进程编程(下)
linux
_可乐无糖5 小时前
活到老学到老之Jenkins build triggers中的定时schedule规则细讲
linux·jenkins·pipe
博睿谷IT99_7 小时前
Linux 备份与恢复常用命令
java·linux·服务器
阳懿7 小时前
windows系统电脑远程登录ubuntu系统电脑
linux·运维·ubuntu