大家好!我是大聪明-PLUS!

我最近一直在研究物联网,由于设备短缺,在尝试模拟固件时经常遇到缺少必要的 /dev/xxx 设备的问题。于是我开始思考是否可以自己编写驱动程序来让固件正常工作。无论难度如何,也无论最终能否成功,学习如何从零开始开发 Linux 驱动程序绝对不会让你后悔。
❯ 介绍
我撰写了一系列文章,主要侧重于实践,理论背景较少。我从《Linux 设备驱动程序》这本书中学习了如何开发驱动程序。
首先,Linux 操作系统分为内核空间和用户空间。对硬件设备的访问只能通过内核空间进行,而设备驱动程序可以被视为内核空间提供的一种 API,允许用户空间代码访问设备。
基于这些基本概念,我发现了一些问题,这些问题促使我去学习驱动程序开发。
- 在编程中,学习总是从"Hello World"程序开始,那么在这种情况下,你该如何编写"Hello World"程序呢?
- 驱动程序如何在 /dev 目录下生成设备文件?
- 驱动程序究竟是如何访问现有硬件的?
- 如何编写系统管理代码?或者,是否可以在不编写代码的情况下提取驱动程序?存储驱动程序的二进制文件在哪里?未来,所有这些都可以进行测试,以确定特定设备的安全性。
❯ 一切都始于"Hello World"
`#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamal");
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 库,该库并未包含在内核中。由于驱动程序是一个运行在内核中的程序,因此我们需要使用内核中的库函数。
例如,` printkprintf` 是一个定义在内核中的输出函数,它类似于 Libc 中的 `printf` 函数。但对我来说,它更像是 Python 中的日志函数,因为输出printk会直接写入内核日志,而该日志可以通过 `log` 命令查看dmesg。
驱动程序代码只有一个入口点和一个出口点。当驱动程序加载到内核时,会执行由module_init`printf` 定义的函数(在上面的代码中称为 `printf`)hello_init。当驱动程序从内核卸载时,会执行由 ` module_exitprintf` 定义的函数(在上面的代码中称为 `printf`)hello_exit。
从上面的代码可以看出,加载时,驱动程序会打印"Hello World",卸载时,它会打印"" Goodbye World。
顺便说一下, `printf`MODULE_LICENSE和 ` MODULE_AUTHORprintf` 并不重要,这里我就不赘述了。
还有一点:要输出 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)/ 目录中,例如:
我们需要一个用于存放已编译源代码的目录,即 `<directory> /usr/src/linux-headers-4.4.0-135-generic/`。
驱动程序代码的头文件将从该目录中查找。
`M=$(PWD)` 参数指定驱动程序编译输出将保存到当前目录。最后,这里是用于将 `<directory>` 加载到 `<directory>` 中的命令,其中
` ko` 是内核空间中的一个文件。obj-m := hello.o``hello.o``hello.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`
从安全角度来看,当前内核假定此模块不受信任。要加载此模块,必须使用受信任的证书对其进行签名。
这可以通过两种方式实现:
- 进入BIOS,在UEFI中禁用安全启动。
- 向内核添加自签名证书,并使用它来对驱动程序模块进行签名。
❯ 查看结果

❯ 在 /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");
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`
该设备的最高有效数字为 8,最低有效数字sda有sda1两个:一个设备的最低有效数字为 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,
};`
我定义并赋值结构体。除了所有者之外,所有其他成员的值都是函数指针。
然后,我使用方括号 [ ]cdev_add为每个驱动程序注册文件操作结构体,这在方括号 [ ] 函数中完成scull_setup_cdev。
例如,当对驱动程序管理的设备执行"打开"操作时,我执行 scull_open 函数,这相当于在系统调用中"钩住"了 open 函数。
❯ 如何在 /dev 下生成我们需要的设备
编译上述代码后,我们得到scull.ko一个文件,然后对其进行签名,最后使用 将其加载到内核中insmod。
让我们检查一下是否加载成功:

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

❯ 结果
在这个例子中,我们没有对特定设备执行任何操作;我们只是使用了方括号 [ ]kmalloc来访问内核空间中的一块内存。
我不会深入讲解代码细节;你可以通过谷歌搜索或研究头文件来找到所有相关信息。
在本文中,我想和大家分享如何自学开发驱动程序:首先,阅读书籍来掌握基本概念,然后在实际应用中查找具体细节的信息。
例如,我不知道驱动程序提供了哪些 API。我只需要知道这样的 API 仅限于文件操作。目前,我只需要 [ ] open、close[ ]read和[ ] 操作write。如有需要,我可以查看其他文件操作是如何实现的。