Вопрос ковыряния ядра 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: обработка прерываний .