147. Ядро Linux: сборка, модуль, символьное устройство, ioctl
Всё, что выше в этой части, — программы в пользовательском режиме: они
просят ядро через системные вызовы. Драйвер живёт по другую сторону:
внутри ядра, с его памятью, без libc, без защиты от собственных ошибок.
Раздел показывает, из чего состоит ядро как проект, как собрать его
модуль и как выглядит драйвер простейшего устройства — кольцевого
буфера, доступного через /dev/ringdev, с read, write и ioctl.
Модуль собран против заголовков ядра Debian 6.1 в stm-sys:1
(_verify/sys147/) — сборка, modinfo, символы и секции настоящие.
Загрузить его в ядро контейнера нельзя: ядро хоста другой версии
(6.6, WSL2) и без прав, insmod отвечает Operation not permitted.
Загрузка — на настоящей машине или виртуальной с тем же ядром.
Как это называется в документации
Оригинал Перевод kernel space, user space пространство ядра, пространство пользователя loadable kernel module ( .ko)загружаемый модуль ядра Kbuild, obj-mсистема сборки ядра, цель «модуль» vermagicотпечаток версии и настроек ядра, с которым собран модуль character device, major/minor символьное устройство, старший/младший номер file_operationsтаблица операций над файлом устройства copy_to_user,copy_from_userкопирование между ядром и пользователем ioctl команда устройству вне read/writeprintk,dmesgпечать ядра и её журнал tainted kernel ядро «запятнано» несвободным или сбойным модулем ftrace, kprobes трассировка функций ядра
147.1 Ядро — это программа
Ядро — один большой исполняемый файл на C: vmlinux — обычный ELF,
bzImage — он же сжатый для загрузчика. Ядро всегда в памяти, всегда
запущено и одно на всех: планирует процессы, делит память, говорит с
железом. Всё остальное — драйверы — либо вкомпилировано в него, либо
подгружается модулями .ko по требованию.
Дерево исходников: arch/ — процессоры, drivers/ — драйверы (больше
половины всего объёма), fs/ — файловые системы, net/ — сеть, mm/ —
память, kernel/ — планировщик и системные вызовы, include/ —
заголовки. В одном include/linux — 1464 заголовка; fs.h — 3776
строк. Ядро исполняется без libc: свои kmalloc вместо malloc,
printk вместо printf, mutex_lock вместо pthread.
Сборка всего ядра: make defconfig или make localmodconfig (только
модули, загруженные сейчас, — сборка в разы короче), правка .config
через make menuconfig, make -j$(nproc); на Debian — make deb-pkg и
dpkg -i. Модуль собирается без сборки ядра — против его заголовков.
147.2 Модуль: вход, выход, параметры
Модуль — файл .ko с двумя обязательными точками: что делать при
загрузке и при выгрузке:
module_init(ring_init);
module_exit(ring_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Кольцевой буфер как символьное устройство");
MODULE_VERSION("1.0");
Параметр задаётся при загрузке — insmod ringdev.ko ring_size=256:
static int ring_size = 64; /* параметр: insmod ringdev.ko ring_size=256 */
module_param(ring_size, int, 0444);
MODULE_PARM_DESC(ring_size, "размер кольцевого буфера в байтах");
Сборка — три строки Makefile, всё остальное делает Kbuild ядра:
obj-m := ringdev.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
Готовый модуль читается modinfo:
version: 1.0
description: Кольцевой буфер как символьное устройство
license: GPL
name: ringdev
vermagic: 6.1.0-53-amd64 SMP preempt mod_unload modversions
parm: ring_size:размер кольцевого буфера в байтах (int)
vermagic — ключ: настройки ядра (SMP preempt mod_unload modversions)
должны совпасть. Номер 6.1.0-53-amd64 при modversions не сравнивается —
вместо него ядро сверяет контрольные суммы каждого символа, которым пользуется
модуль. Ядро 6.6 его отвергнет: суммы разойдутся, и в dmesg будет
disagrees about version of symbol ….
Поэтому модули собирают под каждое ядро отдельно (DKMS делает это при
обновлении). Файл .ko — 279 672 байта, из них код и данные — 14 080
(strip --strip-debug), остальное отладочная информация.
Секции модуля: .init.text — код ring_init, выбрасывается после
загрузки; .exit.text — код выгрузки; .modinfo — то, что печатает
modinfo; __param — параметры. Символы, которые модуль требует от
ядра (nm -u): alloc_chrdev_region, cdev_add, _copy_to_user,
_printk, __kmalloc, mutex_lock, device_create… Большинство из них
экспортированы для любых модулей, и только четыре — device_create,
device_destroy, class_destroy и __class_create (за ним стоит макрос
class_create) — помечены EXPORT_SYMBOL_GPL: для несвободного модуля их
не существует. Модуль с другой лицензией даже не соберётся — modpost
остановит сборку сообщением «GPL-incompatible module … uses GPL-only
symbol». Тип экспорта каждого символа виден в Module.symvers рядом
с заголовками ядра.
147.3 Символьное устройство и file_operations
Устройство в Unix — файл в /dev. У символьного устройства байты идут
потоком (терминал, порт, наш буфер), у блочного — блоками с
произвольным доступом (диск). Файлу соответствует пара номеров: старший
выбирает драйвер, младший — устройство внутри драйвера. Драйвер отдаёт
ядру таблицу операций — тот же приём, что таблица функций в разделе 146:
static const struct file_operations ring_fops = { /* «класс» устройства: таблица функций (раздел 146) */
.owner = THIS_MODULE,
.read = ring_read,
.write = ring_write,
.unlocked_ioctl = ring_ioctl,
};
В ядре 6.1 struct file_operations объявлена в fs.h на строке 2174 и
начинается с owner, llseek, read, write; всего в ней несколько
десятков указателей, и незаполненные означают «операция не
поддерживается» (-EINVAL для read на устройстве без .read).
Регистрация в четыре шага, с откатом при любой ошибке:
rc = alloc_chrdev_region(&devno, 0, 1, "ringdev"); /* ядро выдаёт старший номер */
if (rc) goto err_buf;
cdev_init(&cdev, &ring_fops);
rc = cdev_add(&cdev, devno, 1);
if (rc) goto err_region;
device_create(cls, NULL, devno, NULL, "ringdev"); /* udev создаст /dev/ringdev */
goto err_… — стиль ядра для освобождения в обратном порядке: в ядре нет
исключений и нет второго шанса. class_create + device_create сообщают
о новом устройстве udev, и файл /dev/ringdev появляется сам; иначе
его пришлось бы делать mknod по номерам из /proc/devices.
147.4 read, write, ioctl
Пользователь вызывает read(fd, buf, n) — ядро зовёт ring_read с
теми же аргументами, но buf указывает в чужое адресное
пространство. Писать туда напрямую нельзя: только copy_to_user, которая
проверяет адрес и возвращает, сколько не скопировалось:
static ssize_t ring_read(struct file *f, char __user *ubuf, size_t n, loff_t *off)
{
size_t done = 0;
mutex_lock(&lock);
while (done < n && count > 0) {
if (copy_to_user(ubuf + done, &buf[tail], 1)) { mutex_unlock(&lock); return -EFAULT; } /* в пользователя — только copy_to_user */
tail = (tail + 1) % ring_size; count--; done++;
}
mutex_unlock(&lock);
return done; /* 0 — «конец файла» для cat */
}
Пометка __user — для статического анализатора (sparse): смешать
указатели ядра и пользователя — ошибка. Мьютекс — потому что read и
write могут идти из двух процессов одновременно; в ядре гонки такие
же, как в разделе 139, только последствия — паника всей машины. Именно
мьютекс, а не спин-блокировка: copy_to_user может заснуть, пока ядро
подкачивает страницу пользователя, а под спин-блокировкой спать нельзя.
Здесь пустой буфер отдаёт 0 — «конец файла»; настоящие драйверы в этом месте
усыпляют читателя через wait_event_interruptible и будят его из write,
как канал.
Возврат: число байт, 0 — конец, отрицательный errno — ошибка. write
в полный буфер возвращает -ENOSPC, а не 0: при нуле echo повторял бы
запись вечно.
ioctl — команды, не укладывающиеся в поток байтов: очистить буфер,
узнать заполнение. Номер команды кодирует направление, «магическое»
число, порядковый номер и размер данных, и заголовок с ними общий у
модуля и программы:
#define RING_IOC_MAGIC 'R'
#define RING_IOC_CLEAR _IO(RING_IOC_MAGIC, 0) /* без данных */
#define RING_IOC_COUNT _IOR(RING_IOC_MAGIC, 1, int) /* ядро → пользователь: сколько байт в буфере */
#define RING_IOC_SIZE _IOR(RING_IOC_MAGIC, 2, int)
RING_IOC_CLEAR = 0x5200, RING_IOC_COUNT = 0x80045201, RING_IOC_SIZE = 0x80045202
0x52 — код буквы R; старший бит 0x8000… — «читать из ядра», 04 —
четыре байта данных. Чужую команду драйвер отвергает -ENOTTY.
Пользовательская сторона — обычные open, write, ioctl(fd,
RING_IOC_COUNT, &n), read.
147.5 Загрузка, журнал, отладка
| Команда | Что делает |
|---|---|
insmod файл.ko [параметры] |
загрузить по пути, без зависимостей |
modprobe имя |
загрузить из /lib/modules/$(uname -r)/ с зависимостями |
rmmod имя, modprobe -r |
выгрузить |
lsmod |
что загружено, кто кем пользуется |
dmesg -w |
журнал ядра, вживую |
printk печатает в журнал ядра с уровнем: pr_err, pr_warn,
pr_info, pr_debug — обёртки над printk(KERN_ERR …); восемь уровней
от KERN_EMERG до KERN_DEBUG — это строка-префикс перед сообщением.
pr_debug печатает, только если модуль собран с -DDEBUG (ccflags-y += -DDEBUG
в Makefile) или сообщения включены на лету:
echo 'module ringdev +p' > /sys/kernel/debug/dynamic_debug/control.
Сообщение модуля ringdev: major N, буфер 64 байт (pr_info в ring_init) появляется в
dmesg при загрузке.
Отладчик к работающему ядру подключают иначе, чем к процессу. Ядро в QEMU
запускают с ключами -s -S — сервер отладчика на порту 1234, процессор
стоит до команды c — и с nokaslr в командной строке ядра, иначе адреса
не совпадут с vmlinux. Тогда gdb vmlinux (ядро с отладочной
информацией) после target remote :1234 ставит точки останова и в ядре, и
в модуле. Символы модулей подгружает команда lx-symbols из scripts/gdb —
для неё ядро собирают с CONFIG_GDB_SCRIPTS. На железе то же даёт kgdb
через последовательный порт (CONFIG_KGDB). В ядре Debian оба ключа
выключены, поэтому чаще обходятся без отладчика: printk, ftrace
(/sys/kernel/tracing: какая функция ядра сколько раз вызвана и за
сколько), kprobes (точка останова на функции ядра с обработчиком),
perf. Ошибка в модуле — Oops с трассировкой стека в dmesg, а часто и
зависание всей машины: модули отлаживают в виртуальной машине, не на
рабочем компьютере.
Безопасная загрузка. Ядро Debian собрано с
CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT: если на машине включена безопасная
загрузка (Secure Boot), оно уходит в режим lockdown и неподписанный модуль
не берёт и от root — insmod отвечает тем же Operation not permitted,
что в контейнере, а в dmesg ядро пишет, что загрузку запретил lockdown.
Ядро, которое требует подписи всегда (module.sig_enforce=1), отвечает
Key was rejected by service. Модуль подписывают своим ключом — парой из
openssl req -x509:
/usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 key.priv key.der ringdev.ko
После этого modinfo показывает sig_hashalgo: sha256, а ключ
регистрируют в прошивке: mokutil --import key.der и подтверждение при
следующей перезагрузке. Для опытов проще виртуальная машина без
безопасной загрузки.
147.6 Грабли
| Симптом | Причина |
|---|---|
insmod: Invalid module format |
vermagic не совпал — собрать под своё ядро |
insmod: Operation not permitted |
нет прав, контейнер или безопасная загрузка: нужен root на настоящей машине и подписанный модуль (147.5) |
insmod: Key was rejected by service |
ядро требует подписи, а модуль не подписан или подписан чужим ключом |
Unknown symbol in module |
функция EXPORT_SYMBOL_GPL, а лицензия модуля не GPL |
class_create не собирается |
с 6.4 один аргумент, до — два (THIS_MODULE, имя) |
cat /dev/ringdev не завершается |
read не возвращает 0 при пустом буфере |
echo x > /dev/ringdev зациклился |
write вернул 0 при полном буфере — нужен -ENOSPC |
Oops при read |
запись в пользовательский буфер напрямую вместо copy_to_user |
ioctl отвечает ENOTTY |
номер команды у программы и модуля разный — общий заголовок |
после rmmod осталась запись в /dev |
не вызваны device_destroy/class_destroy |
.ko 280 КБ |
отладочная информация; код — 14 КБ |
147.7 Итоги
- Ядро — ELF без libc; модуль
.koсобирается Kbuild против заголовков своего ядра,vermagicпривязывает его к версии. module_init/module_exit,module_param,MODULE_LICENSE("GPL").- Символьное устройство:
alloc_chrdev_region→cdev_add→class_create→device_create;file_operations— таблица функций. - Данные пользователя — только через
copy_to_user/copy_from_user;readвозвращает 0 в конце,write—-ENOSPCпри полном буфере. ioctl:_IO/_IOR/_IOW, общий заголовок; чужое —-ENOTTY.insmod/modprobe/rmmod/dmesg; отладкаprintk,ftrace,gdbк ядру в QEMU, в виртуалке; при безопасной загрузке — только подписанный модуль.
Полные исходники
ringdev.h — команды ioctl — общий заголовок модуля и программы.
/* Раздел 147: команды ioctl — общий заголовок модуля и пользовательской программы. */
#ifndef RINGDEV_H
#define RINGDEV_H
#include <linux/ioctl.h>
#define RING_IOC_MAGIC 'R'
#define RING_IOC_CLEAR _IO(RING_IOC_MAGIC, 0) /* без данных */
#define RING_IOC_COUNT _IOR(RING_IOC_MAGIC, 1, int) /* ядро → пользователь: сколько байт в буфере */
#define RING_IOC_SIZE _IOR(RING_IOC_MAGIC, 2, int)
#endif
ringdev.c — модуль ядра: символьное устройство с кольцевым буфером.
/* Раздел 147: модуль ядра — символьное устройство /dev/ringdev с кольцевым буфером:
read/write/ioctl, параметр модуля, регистрация через cdev и класс устройств.
Собирается против заголовков ядра; загрузка требует настоящего ядра той же версии. */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/version.h>
#include "ringdev.h"
static int ring_size = 64; /* параметр: insmod ringdev.ko ring_size=256 */
module_param(ring_size, int, 0444);
MODULE_PARM_DESC(ring_size, "размер кольцевого буфера в байтах");
static char *buf;
static size_t head, tail, count;
static DEFINE_MUTEX(lock);
static dev_t devno;
static struct cdev cdev;
static struct class *cls;
static ssize_t ring_read(struct file *f, char __user *ubuf, size_t n, loff_t *off)
{
size_t done = 0;
mutex_lock(&lock);
while (done < n && count > 0) {
if (copy_to_user(ubuf + done, &buf[tail], 1)) { mutex_unlock(&lock); return -EFAULT; } /* в пользователя — только copy_to_user */
tail = (tail + 1) % ring_size; count--; done++;
}
mutex_unlock(&lock);
return done; /* 0 — «конец файла» для cat */
}
static ssize_t ring_write(struct file *f, const char __user *ubuf, size_t n, loff_t *off)
{
size_t done = 0;
mutex_lock(&lock);
while (done < n && count < (size_t)ring_size) {
if (copy_from_user(&buf[head], ubuf + done, 1)) { mutex_unlock(&lock); return -EFAULT; }
head = (head + 1) % ring_size; count++; done++;
}
mutex_unlock(&lock);
return done ? done : -ENOSPC; /* полон — ошибка, а не 0 (иначе echo зациклится) */
}
static long ring_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case RING_IOC_CLEAR: mutex_lock(&lock); head = tail = count = 0; mutex_unlock(&lock); return 0;
case RING_IOC_COUNT: return put_user((int)count, (int __user *)arg);
case RING_IOC_SIZE: return put_user(ring_size, (int __user *)arg);
}
return -ENOTTY; /* чужая команда */
}
static const struct file_operations ring_fops = { /* «класс» устройства: таблица функций (раздел 146) */
.owner = THIS_MODULE,
.read = ring_read,
.write = ring_write,
.unlocked_ioctl = ring_ioctl,
};
static int __init ring_init(void)
{
int rc;
buf = kmalloc(ring_size, GFP_KERNEL);
if (!buf) return -ENOMEM;
rc = alloc_chrdev_region(&devno, 0, 1, "ringdev"); /* ядро выдаёт старший номер */
if (rc) goto err_buf;
cdev_init(&cdev, &ring_fops);
rc = cdev_add(&cdev, devno, 1);
if (rc) goto err_region;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
cls = class_create("ringdev"); /* с 6.4 — без владельца */
#else
cls = class_create(THIS_MODULE, "ringdev");
#endif
if (IS_ERR(cls)) { rc = PTR_ERR(cls); goto err_cdev; }
device_create(cls, NULL, devno, NULL, "ringdev"); /* udev создаст /dev/ringdev */
pr_info("ringdev: major %d, буфер %d байт\n", MAJOR(devno), ring_size);
return 0;
err_cdev: cdev_del(&cdev);
err_region: unregister_chrdev_region(devno, 1);
err_buf: kfree(buf);
return rc;
}
static void __exit ring_exit(void)
{
device_destroy(cls, devno);
class_destroy(cls);
cdev_del(&cdev);
unregister_chrdev_region(devno, 1);
kfree(buf);
pr_info("ringdev: выгружен\n");
}
module_init(ring_init);
module_exit(ring_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Кольцевой буфер как символьное устройство");
MODULE_VERSION("1.0");
Дальше: раздел 148 — базы данных из C.