Как разработать драйвер Linux с нуля

от автора

image

Недавно я занимался изучением IoT и, так как мне не хватало устройств, при попытках симулировать работу прошивки я часто сталкивался с неимением нужного /dev/xxx. Так что я стал задумываться, а могу ли написать драйвер самостоятельно, чтобы заставить прошивку работать. Независимо от того, насколько сложно это будет, и удастся ли воплотить такое намерение, в любом случае вы не пожалеете, если научитесь разрабатывать драйвер Linux с нуля.

Введение

Я написал серию статей, ориентированных в основном на практику, о теории там мало что говорится. Разрабатывать драйверы я научился по книге Linux Device Drivers, а код к примерам, разобранным в этой книге, выложен на 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("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, причём, на таком, который не слишком мне привычен. При работе я часто пользуюсь библиотекой Libc, которая отсутствует в ядре. Поскольку драйвер – это программа, работающая в ядре, именно в ядре мы используем и библиотечные функции.

Например, printk — это функция вывода, определяемая в ядре, она аналогична printf из Libc. Но мне она в большей степени напоминает логирующую функцию из Python, так как вывод printk идёт разу в лог ядра, а этот лог можно просмотреть командой dmesg.

В коде драйвера есть ровно одна точка входа и одна точка выхода. При загрузке драйвера в ядро выполнится функция, определяемая функцией module_init, которая в вышеприведённом коде называется hello_init. При выгрузке драйвера из ядра вызывается функция, определяемая в функции module_exit, которая в вышеприведённом коде называется hello_exit.

Из показанного выше кода понятно, что, загружаясь, драйвер выводит Hello World, а выгружаясь — Goodbye World.

Кстати: MODULE_LICENSE и MODULE_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)/, например:

$ uname -r 4.4.0-135-generic/usr/src/linux-headers-4.4.0-135/ --> каталог исходного кода ядра /usr/src/linux-headers-4.4.0-135-generic/ --> каталог скомпилированного исходного кода для данного ядра

А нам нужен каталог для скомпилированных исходников, а именно /usr/src/linux-headers-4.4.0-135-generic/.

Поиск заголовочных файлов для драйверного кода осуществляется именно из этого каталога.

Параметр M=$(PWD) указывает, что вывод от компиляции драйвера попадает именно в текущий каталог.

Наконец, вот команда obj-m := hello.o, предназначенная для загрузки hello.o в hello.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. Добавить в ядро самоподписываемый сертификат, и именно с его помощью подписать модуль драйвера (подробнее об этом написано тут).

View the Results

image

Добавляем файлы устройств под /dev

Once again, we firstly provide the code, and then explain the example code.

#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("Hcamael"); 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);

Классификация драйверов

image

Драйверы делятся на три категории: символьные устройства, блочные устройства и сетевые интерфейсы. В вышеприведённом коде было разобрано символьное устройство, а обсуждение двух других категорий выходит за рамки этой статьи.

Как было показано выше, brw-rw-- — строка о правах доступа для блочных устройств начинается с буквы «b», а для символьных устройств начинается с буквы «c».

О старших и младших числах

Старшее число отличает один драйвер от всех остальных. В принципе, если старшее число у устройств совпадает, это означает, что они управляются одним и тем же драйвером.
В одном каталоге drive может быть создано множество устройств, и отличаться они будут младшими числами. Вместе старшее и младшее число характеризуют устройство, управляемое драйвером (как показано выше).

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

Старшее число аппаратуры sda и sda1 это 8, а младших чисел здесь два: у одного устройства 0, а у другого 1.

Как драйвер предоставляет API

Я привык считать, что /dev/xxx – это интерфейс, предоставляемый файлом, а в Linux «всё – файл». Поэтому, оперируя драйвером, мы, фактически, оперируем файлом, и именно в драйвере определяется define/open/read/write… что произойдёт с /dev/xxx. Любые мыслимые действия с API драйвера – это операции над файлами.

Какие операции над файлами здесь присутствуют? Все они определяются в структуре file_operations в заголовочном файле ядра <linux/fs.h>.

В коде, приведённом выше в качестве примера:

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.

Например, совершая операцию “open” (открыть) с устройством под управлением драйвера, я выполняю функцию scull_open, что эквивалентно «перехвату» функции open в системном вызове.

Как сгенерировать нужное нам устройство под /dev

Скомпилировав вышеприведённый код, получим scull.ko, затем подпишем его и, наконец, загрузим в ядро при помощи insmod.

Проверим, удачно ли он загрузился:

image

Да, драйвер устройства загрузился успешно, но он не создаёт файла устройства в каталоге /dev. Необходимо вручную воспользоваться mknod для связывания устройства:

image

Итоги

В данном примере мы не совершали никаких операций с конкретным устройством, а просто воспользовались kmalloc, и с его помощью применили блок памяти в пространстве ядра.

Далее не буду вдаваться в детали о коде, все их можно уточнить, погуглив информацию или изучив заголовочные файлы.

В этой статье я хотел поделиться с вами, как самостоятельно научиться разрабатывать драйвера: сначала читать книги, чтобы усвоить базовые концепции, а затем искать информацию по конкретным деталям, когда дойдёт дело до практического применения.

Например, я не знаю, какой API может предоставить драйвер. Всё, что мне нужно знать – что такой API ограничивается файловыми операциями. На данный момент мне понадобятся только операциями open, close, read и write. Как делаются другие операции с файлами – можно уточнить при необходимости.

Ссылки


ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/724282/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *