linux-kernel-module/

Вопрос ковыряния ядра Linux впервые поднимался в этом блоге еще в далеком 2016-м году. Мы научились собирать ядро из исходников и цепляться к нему отладчиком . Но на этом все и заглохло. Тогда найти актуальную информацию по разработке ядерного кода в Linux, да еще и в удобоваримом виде, было проблемой. Я предпочел дождаться появления свежих книг по теме, а пока заняться изучением чего-то другого. И вот, спустя пять лет, такие книги были опубликованы . В связи с чем я решил попробовать написать пару модулей ядра, и посмотреть, как пойдет.

Проводить эксперименты было решено на Raspberry Pi 3 Model B+ . На то есть три основные причины. Во-первых, малинка широко доступна и стоит недорого (особенно третья малинка, после выхода четвертой), что делает эксперименты повторяемыми. Во-вторых, запускать модули ядра на той же машине, где вы их разрабатываете, в любом случае не лучшая затея. Ведь ошибка в ядерном коде может приводить к какими угодно последствиям, не исключая повреждения ФС. И в-третьих, в отличие от виртуальной машины, малинка не отъедает ресурсы на вашей основной системе и позволяет реально взаимодействовать с реальным железом.

Образ системы был записан на SD-карту при помощи Raspberry Pi Imager . Приложение использовало образ на основе Raspbian 10 с ядром Linux 5.10. Это LTS-версия ядра, поддержка которого прекратится в декабре 2026-го года.

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

sudo apt install raspberrypi-kernel-headers
ls / lib / modules / $ ( uname -r )

В других системах пакет может называться linux-headers-* или как-то иначе.

Создадим новую директорию с файлом hello.c:

#include <linux/kernel.h>
#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:

obj m += hello . o

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:

#define pr_info(fmt, …)
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

А вот printk() уже является экспортируемым символом:

$ sudo cat /proc/kallsyms | grep ‘T printk’

801833e0 T printk_nmi_direct_exit
809f1508 T printk
809f16f4 T printk_deferred

Первая колонка — это адрес символа. Он отображаются, только если читать /proc/kallsyms под суперпользователем. В противном случае, мы увидим нули. Во второй колонке показано, откуда экспортируется символ. Согласно man nm , T и t соответствуют секции кода (.text). Заглавная буква означает, что символ виден глобально, а значит, может использоваться в модулях ядра. Теперь мы чуть лучше понимаем, как происходит общение между ядром и его модулями.

Далее, рассмотрим модуль посложнее:

#include <linux/init.h>
#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 insmod param.ko name =Alex
sudo rmmod param
tail / var / log / syslog

В логах мы предсказуемо увидим:

Hello, Alex
Goodbye, Alex

Параметры, переданные модулю, видны через sysfs. Но чтобы это работало, код нужно немного изменить:

// module_param(name, charp, 0);
module_param ( name , charp , S_IRUGO ) ;

Если теперь пересобрать модуль, то можно сделать так:

cat / sys / module / param / parameters / name

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

Дополнение: В продолжение темы см статьи Модули ядра Linux: пример символьного устройства , Модули ядра Linux: таймеры и GPIO и Модули ядра Linux: обработка прерываний .

EnglishRussianUkrainian