Основы написания модулей ядра в Linux
29 сентября 2021
Вопрос ковыряния ядра Linux впервые поднимался в этом блоге еще в далеком 2016-м году. Мы научились собирать ядро из исходников и цепляться к нему отладчиком. Но на этом все и заглохло. Тогда найти актуальную информацию по разработке ядерного кода в Linux, да еще и в удобоваримом виде, было проблемой. Я предпочел дождаться появления свежих книг по теме, а пока заняться изучением чего-то другого. И вот, спустя пять лет, такие книги были опубликованы. В связи с чем я решил попробовать написать пару модулей ядра, и посмотреть, как пойдет.
Проводить эксперименты было решено на Raspberry Pi 3 Model B+. На то есть три основные причины. Во-первых, малинка широко доступна и стоит недорого (особенно третья малинка, после выхода четвертой), что делает эксперименты повторяемыми. Во-вторых, запускать модули ядра на той же машине, где вы их разрабатываете, в любом случае не лучшая затея. Ведь ошибка в ядерном коде может приводить к какими угодно последствиям, не исключая повреждения ФС. И в-третьих, в отличие от виртуальной машины, малинка не отъедает ресурсы на вашей основной системе и позволяет реально взаимодействовать с реальным железом.
Образ системы был записан на SD-карту при помощи Raspberry Pi Imager. Приложение использовало образ на основе Raspbian 10 с ядром Linux 5.10. Это LTS-версия ядра, поддержка которого прекратится в декабре 2026-го года.
Для написания модулей ядра необходимо установить пакет с заголовочными файлами. В Raspbian это делается так:
ls /lib/modules/$(uname -r)
В других системах пакет может называться linux-headers-*
или как-то иначе.
Создадим новую директорию с файлом hello.c:
#include <linux/module.h>
int init_module(void) {
pr_info("Hello world\n");
return 0;
}
void cleanup_module(void) {
pr_info("Goodbye world\n");
}
MODULE_LICENSE("GPL");
Рядом положим файл Makefile:
all:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Говорим make
. В результате должен появиться файл hello.ko.
Теперь попробуем следующие команды:
modinfo hello.ko
# загрузить модуль
sudo insmod hello.ko
# список загруженных модулей ядра
lsmod | grep hello
# выгрузить модуль
sudo rmmod hello
# почитать логи
tail /var/log/syslog
При загрузке модуля будет вызвана процедура init_module()
, а при выгрузке — cleanup_module()
. Они напишут соответствующие логи через pr_info()
, и мы увидим их в /var/log/syslog. С этим все понятно. Давайте перейдем к чему-то поинтересней.
Начнем с более детального рассмотрения pr_info()
. Среди символов, экспортируемых ядром, вы его не найдете. Поиск по заголовочным файлам показывает, что на самом деле это макрос, объявленный в linux/printk.h:
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
А вот printk()
уже является экспортируемым символом:
...
801833e0 T printk_nmi_direct_exit
809f1508 T printk
809f16f4 T printk_deferred
...
Первая колонка — это адрес символа. Он отображаются, только если читать /proc/kallsyms под суперпользователем. В противном случае, мы увидим нули. Во второй колонке показано, откуда экспортируется символ. Согласно man nm
, T
и t
соответствуют секции кода (.text). Заглавная буква означает, что символ виден глобально, а значит, может использоваться в модулях ядра. Теперь мы чуть лучше понимаем, как происходит общение между ядром и его модулями.
Далее, рассмотрим модуль посложнее:
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Aleksander Alekseev");
MODULE_DESCRIPTION("A simple driver");
static char* name = "%username%";
module_param(name, charp, 0);
MODULE_PARM_DESC(name, "Enter your name");
static int __init init(void) {
pr_info("Hello, %s\n", name);
return 0;
}
static void __exit cleanup(void) {
pr_info("Goodbye, %s\n", name);
}
module_init(init);
module_exit(cleanup);
Из этого примера мы узнаем ряд важных вещей. Во-первых, что процедуры, вызываемые при загрузке и выгрузке модуля, могут называться как угодно. Во-вторых, что в модуле можно указать не только его лицензию, но также автора и краткое описание. Сравните вывод modinfo
для этого модуля и предыдущего. И в-третьих, модуль может принимать параметры:
sudo rmmod param
tail /var/log/syslog
В логах мы предсказуемо увидим:
Goodbye, Alex
Параметры, переданные модулю, видны через sysfs. Но чтобы это работало, код нужно немного изменить:
module_param(name, charp, S_IRUGO);
Если теперь пересобрать модуль, то можно сделать так:
Думаю, что для первого раза удивительных открытий достаточно. Полная версия кода доступна в этом репозитории на GitHub. Там же есть список дополнительных материалов для самостоятельного изучения.
Дополнение: В продолжение темы см статьи Модули ядра Linux: пример символьного устройства, Модули ядра Linux: таймеры и GPIO и Модули ядра Linux: обработка прерываний.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.