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. Как делаются другие операции с файлами – можно уточнить при необходимости.

Ссылки




Комментарии (6)


  1. DownFvll
    00.00.0000 00:00
    +11

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

    Где же explanation?)

    Приводить в пример scull драйвер - это, конечно, сильно, особенно, если вы не собираетесь объяснять, что там происходит. Почему бы не написать обычный "Hello World", как, например, в SimpleLinuxDriver? Есть даже статья, которая достаточно хорошо описывает, что, как и зачем делается, включая то, почему ваше устройство нужно создавать руками через mknod и как это сделать из LKM.

    В старых версиях ядра таким же методом загружалось и удалялось само ядро

    Что? Приведите, пожалуйста, ссылку на источник, где вы это нашли

    и именно в драйвере определяется define/open/read/write

    Интересная операция define, никогда о ней не слышал))

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

    Не совсем правда, никто ничего не перехватывает, VFS вызывает ->open(...) и в зависимости от файловой системы -> будут вызваны разные функции для разных FS, т.к. будут получены разные структуры file.

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

    IMHO, этот гайд применим для изучения абсолютно всего, что угодно.


  1. staticmain
    00.00.0000 00:00
    +2

    (подробнее об этом написано тут).

    Где? Ссылку украли.


    1. Shyhartskoi Автор
      00.00.0000 00:00

      https://gohalo.me/post/kernel-modules.html

      Пофиксил.


  1. Apoheliy
    00.00.0000 00:00
    +5

    Можно было бы что-то поприличнее выбрать для перевода.

    Или, накрайняк, написать своё.

    В частности:

    mknod штука, конечно, работающая. Однако есть udev и с помощью конфигурационного файлика можно сделать назначение имени автоматически.

    Кстати: MODULE_LICENSE и MODULE_AUTHOR не так важны. Здесь я не буду подробно их разбирать.

    Вот это нужно перевести по-другому: "Что это такое - я не знаю и здесь я не буду их разбирать". На деле получается, что некоторые функции вам будут недоступны, если выбирается неподходящая лицензия.

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

    Это просто апофеоз статьи. Можно сразу было написать: используйте ваш любимый поисковик, чтобы написать свой первый драйвер.


  1. DarkHost
    00.00.0000 00:00
    +1

    Было интересно прочитать.


  1. CrashLogger
    00.00.0000 00:00
    +2

    Что-то подобное я уже читал тут не так давно. И даже не один раз. Почему-то все авторы дальше hello-world не идут, хотя тема обширная и интересная.