[Перевод] Пособие по программированию модулей ядра Linux. Часть 1

-7ylhpzc6raohz-vopxottyejfu.png


Перед вами последняя версия пособия по программированию модулей ядра Linux, вышедшего 2 июля 2022 года. Пособие большое, поэтому материал будет разбит на серию статей. В первой части мы разберём, что такое модули ядра, рассмотрим необходимые подготовительные этапы для их создания и в завершении по традиции напишем первый простейший модуль «Hello world», попутно разобрав лицензирование, передачу аргументов командной строки и прочие нюансы. Это пособие вы можете смело воспроизводить и изменять в соответствии с условиями Open Software License v 3.0.

1. Вступление


Эта книга задумана для распространения в качестве полезного подручного материала, но не предоставляет никаких гарантий, в том числе гарантий соответствия ожиданиям читателя или пригодности для конкретной цели.

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

Производные работы и переводы этого документа должны также публиковаться под лицензией Open Software License с упоминанием оригинального источника. Если вы внесёте в книгу новый материал, то сделайте этот материал и исходный код доступными для своих ревизий. Ревизии и обновления должны быть доступны непосредственно мейнтейнеру документа, Джиму Хуангу . Это позволит делать мерджи обновлений и предоставлять согласованные ревизии сообществу Linux.

Если вы будете публиковать или распространять книгу в коммерческих целях, то автор и проект документирования Linux (LDP) будут весьма признательны за пожертвования, ройялти-отчисления и предоставление печатных версий. Участие в общем деле таким образом показывает вашу поддержку бесплатного ПО и LDP. По всем вопросам можете писать на приведённый выше адрес.

▍ 1.1 Авторство


Первое «Руководство по программированию модулей ядра» написал Ори Померанц для ядер версии 2.2. В конечном итоге у Ори не стало времени для поддержания актуальности этого документа, что не удивительно, ведь ядро очень динамично в своём развитии. После этого поддержку руководства взял на себя Питер Джей Зальцман, который обновил его под версию 2.4. Но в итоге и Питеру стало нехватать времени, чтобы довести пособие до соответствия ядру 2.6. В этой ситуации на выручку пришёл Майкл Буриан, который помог его обновить. Следующим был Боб Моттрам, доработавший примеры под ядро 3.8+. Последним же на данный момент является Джим Хуанг, который довёл руководство до соответствия последним версиям ядра 5.х и скорректировал документ LaTeX.

▍ 1.2 Благодарности


Ниже перечислены сторонние участники, которые вносили изменения и давали полезные рекомендации:

2011eric, 25077667, Arush Sharma, asas1asas200, Benno Bielmeier, Bob Lee, Brad Baker, ccs100203, Chih-Yu Chen, Ching-Hua (Vivian) Lin, ChinYikMing, Cyril Brulebois, Daniele Paolo Scarpazza, David Porter, demonsome, Dimo Velev, Ekang Monyet, fennecJ, Francois Audeon, gagachang, Gilad Reti, Horst Schirmeier, Hsin-Hsiang Peng, Ignacio Martin, JianXing Wu, linD026, lyctw, manbing, Marconi Jiang, mengxinayan, RinHizakura, Roman Lakeev, Stacy Prowell, Steven Lung, Tristan Lelong, Tucker Polomik, VxTeemo, Wei-Lun Tsai, xatier, Ylowy.


▍ 1.3 Что такое модуль ядра?


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

Что же конкретно такое модуль ядра? Модули — это элементы кода, которые по необходимости можно загружать в ядро и выгружать. Они расширяют его функциональность, не требуя перезагрузки системы. К примеру, одним из типов модулей является драйвер устройств, который позволяет ядру обращаться к подключённому аппаратному обеспечению. Не имея модулей, нам бы пришлось строить монолитные ядра и добавлять новую функциональность непосредственно в их образы. И мало того что это привело бы к увеличению размеров ядра, но ещё и вынудило бы нас пересобирать и перезагружать его при каждом добавлении новой функциональности.

▍ 1.4 Пакеты модулей ядра


В дистрибутивах Linux для работы с пакетами модулей есть команды modprobe, insmod и depmod.

В Ubuntu/Debian:

sudo apt-get install build-essential kmod


В Arch Linux:

1sudo pacman -S gcc kmod


▍ 1.5 Какие модули содержатся в моём ядре?


Узнать, какие модули загружены в ядро, можно командой lsmod:

sudo lsmod


Хранятся модули по пути /proc/modules, значит, их также можно просмотреть с помощью:

sudo cat /proc/modules


Список может оказаться длинным, и вам потребуется искать что-то конкретное. Вот пример поиска модуля fat:

sudo lsmod | grep fat


▍ 1.6 Нужно ли скачивать и компилировать ядро?


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

▍ 1.7 Перед началом


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

  1. Версионирование модулей. Модуль, скомпилированный для одного ядра, не загрузится для другого, если не включить в этом ядре CONFIG_MODVERSIONS. Подробнее о версионировании мы ещё поговорим позднее. Если в вашем ядре версионирование включено, то поначалу, пока мы не разберём эту тему подробнее, примеры могут у вас не работать. Правда, включено оно обычно в большинстве базовых дистрибутивов, и если из-за этого у вас возникнут проблемы с загрузкой модулей, скомпилируйте ядро, отключив их версионирование.
  2. Использование X Window System. Настоятельно рекомендуем извлекать, компилировать и загружать все приводимые в руководстве примеры из консоли. Работать с ними в X Window System не стоит.


Модули не могут выводить информацию на экран подобно printf(). При этом они могут логировать информацию и предупреждения, которые в итоге на экран выводятся, но только в консоли. Если вы вставите модуль (insmod) из xterm, то информация и предупреждения залогируются, но только в системный журнал. То есть увидеть вы все эти данные сможете лишь через journalctl. Подробности описаны в разделе 4. Для получения прямого доступа ко всей этой информации, выполняйте все действия в консоли.

2. Заголовочные файлы


Прежде чем вы сможете что-либо создавать, вам нужно установить для ядра заголовочные файлы. Для начала выполните:

в Ubuntu/Debian:

sudo apt-get update
apt-cache search linux-headers-`uname -r`


в Arch Linux:

sudo pacman -S linux-headers


Так вы узнаете, какие заголовочные файлы ядра доступны. Затем можно выполнить, например:

sudo apt-get install kmod linux-headers-5.4.0-80-generic


3. Примеры


Все примеры этого документа доступны в подкаталоге examples.

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

4. Hello World


▍ 4.1 Простейший модуль


Большинство людей, изучающих программирование, начинают с какого-нибудь примера «Hello world». Не знаю, что бывает с теми, кто от этой традиции отходит, но, думаю, лучше и не знать. Мы начнём с серии программ «Hello world», которые продемонстрируют различные основы написания модуля ядра.

Ниже описан простейший пример модуля.

Создайте тестовый каталог:

mkdir -p ~/develop/kernel/hello-1
cd ~/develop/kernel/hello-1


Вставьте следующий код в редактор и сохраните как hello-1.c:

/*
 * hello-1.c – простейший модуль ядра.
 */
#include  /* необходим для pr_info() */
#include  /* необходим для всех модулей */
 
int init_module(void)
{
    pr_info("Hello world 1.\n");
 
    /* Если вернётся не 0, значит, init_module провалилась; модули загрузить не получится. */
    return 0;
}
 
void cleanup_module(void)
{
    pr_info("Goodbye world 1.\n");
}
 
MODULE_LICENSE("GPL");


Теперь вам потребуется Makefile. Если вы будете копировать следующий код, то сделайте отступы табами, не пробелами.

obj-m += hello-1.o
 
PWD := $(CURDIR)
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


В Makefile инструкция $(CURDIR) может быть установлена на абсолютный путь текущего рабочего каталога (затем идёт обработка всех опций , если таковые присутствуют). Подробнее о CURDIR читайте в мануале GNU make.

В завершении просто выполните make.

make


Если в Makefile не будет инструкции PWD := $(CURDIR), он может не скомпилироваться корректно с помощью sudo make. Поскольку некоторые переменные среды регулируются политикой безопасности, наследоваться они не могут. По умолчанию эта политика определяется файлом sudoers. В нём изначально включена опция env_reset, которая запрещает переменные среды. В частности, переменные PATH из пользовательской среды не сохраняются, а устанавливаются на значения по умолчанию (подробнее можно почитать в мануале по sudoers).

Установки для переменных среды можно посмотреть так:

$ sudo -s
# sudo -V


Вот пример простого Makefile, демонстрирующий описанную выше проблему:

all:
    echo $(PWD)


Далее можно использовать флаг –p для вывода всех значений переменных среды из Makefile.

$ make -p | grep PWD
PWD = /home/ubuntu/temp
OLDPWD = /home/ubuntu
    echo $(PWD)


Переменная PWD при выполнении sudo унаследована не будет.

$ sudo make -p | grep PWD
    echo $(PWD)


Тем не менее эту проблему можно решить тремя способами.

1. Использовать флаг -E для их временного сохранения.

$ sudo -E make -p | grep PWD
    PWD = /home/ubuntu/temp
    OLDPWD = /home/ubuntu
    echo $(PWD)


2. Отключить env_reset, отредактировав /etc/sudoers из-под рут-пользователя с помощью visudo.

## файл sudoers.
  ##
  ...
  Defaults env_reset
  ## В предыдущей строке измените env_reset на !env_reset, чтобы сохранить все переменные среды.


Затем выполните env и sudo env по отдельности:

# отключить env_reset
    echo "user:" > non-env_reset.log; env >> non-env_reset.log
    echo "root:" >> non-env_reset.log; sudo env >> non-env_reset.log
    # включить env_reset
    echo "user:" > env_reset.log; env >> env_reset.log
    echo "root:" >> env_reset.log; sudo env >> env_reset.log


Можете просмотреть и сравнить эти логи, чтобы понять отличия между env_reset и !env_reset.

3. Сохранить переменные среды, добавив их в env_keep в /etc/sudoers.

  Defaults env_keep += "PWD"


После применения этого изменения можете проверить установки переменных сред с помощью:

         $ sudo -s
         # sudo -V


Если всё пройдёт гладко, вы получите скомпилированный модуль hello-1.ko. Информацию о нём можно вывести командой:

modinfo hello-1.ko


На этом этапе команда:

sudo lsmod | grep hello


не должна ничего возвращать. Можете попробовать загрузить свой новоиспечённый модуль с помощью:

sudo insmod hello-1.ko


При этом символ тире превратится в нижнее подчёркивание. Теперь, когда вы снова выполните:

sudo lsmod | grep hello


то увидите загруженный модуль. Удалить его можно с помощью:

sudo rmmod hello_1


Обратите внимание — тире было заменено нижним подчёркиванием. Чтобы увидеть произошедшее в логах, выполните:

sudo journalctl --since "1 hour ago" | grep kernel


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

Модули ядра должны иметь не менее двух функций:

  • стартовую (инициализация), которая называется init_module() и вызывается при внедрении (insmod) модуля в ядро;
  • завершающую (очистка), которая зовётся cleanup_module() и вызывается непосредственно перед извлечением модуля из ядра.


В действительности же с версии 2.3.13 произошли кое-какие изменения. Теперь стартовую и завершающую функцию модулей можно называть на своё усмотрение, и об этом будет подробнее сказано в разделе 4.2. На деле этот новый метод даже предпочтительней, хотя многие по прежнему используют названия init_module() и cleanup_module().

Как правило, init_module() или регистрирует обработчик чего-либо с помощью ядра, или заменяет одну из функций ядра собственным кодом (обычно кодом, который выполняет определённые действия и вызывает исходную функцию). Что касается cleanup_module(), то она должна отменять всё, что сделала init_module(), чтобы безопасно выгрузить модуль.

Наконец, каждый модуль ядра должен включать . Нам нужно было включить только для расширения макроса уровня журнала pr_alert(), о чём подробнее сказано в пункте 2.

  1. Примечание о стиле написания кода. Есть нюанс, который может не быть очевиден тем, кто только начинает заниматься программированием ядра. Имеется в виду то, что отступы в коде должны делаться с помощью табов, а не пробелов. Это одно из общих соглашений. Оно вам может не нравиться, но придётся привыкать, если вы соберётесь отправлять патч в основную ветку ядра.
  2. Добавление макросов вывода. Изначально использовалась функция printk, обычно сопровождаемая приоритетом уровня журнала KERN_DEBUG или KERN_INFO. Недавно же появилась возможность выражать это в сокращённой форме с помощью макросов вывода pr_info и pr_debug. Такой подход просто избавляет от лишних нажатий клавиш и выглядит более лаконично. Найти эти макросы можно в include/linux/printk.h. Рекомендую уделить время и прочесть о доступных макросах приоритетов.
  3. Насчёт компиляции. Модули ядра нужно компилировать несколько иначе, нежели обычные приложения пространства пользователя. Прежние версии ядра требовали от нас особого внимания к этим настройкам, которые обычно хранились в Makefile. И несмотря на иерархическую организованность, в make-файлах нижнего уровня скапливалось множество лишних настроек, что делало эти файлы большими и усложняло их обслуживание. К счастью, появился новый способ делать всё это, который называется kbuild, и процесс сборки для внешних загружаемых модулей теперь полностью интегрирован в стандартный механизм сборки ядра. Подробнее о компиляции модулей, не являющихся частью официального ядра (таких как примеры в этом руководства), читайте в Documentation/kbuild/modules.rst.


Дополнительные подробности о make-файлах для модулей ядра доступны в Documentation/kbuild/makefiles.rst. Обязательно прочтите эту документацию и изучите связанные с ней файлы — это наверняка избавит вас от большого объёма лишней работы.

А вот вам одно бонусное упражнение. Видите комментарий над инструкцией return в init_module()? Измените возвращаемое значение на отрицательное, после чего перекомпилируйте и заново загрузите модуль. Что произойдёт?

▍ 4.2 Hello и Goodbye


В ранних версиях ядра вам нужно было использовать функции init_module и cleanup_module, как в нашем первом примере «Hello world», но сегодня их уже можно именовать на своё усмотрение с помощью макросов module_init и module_exit, которые определены в include/linux/module.h. Единственное требование — это чтобы функции инициализации и очистки были определены до вызова этих макросов, в противном случае возникнут ошибки компиляции.

Вот пример:

/*
 * hello-2.c – демонстрация макросов module_init() и module_exit().
 * Этот вариант предпочтительнее использования init_module() и cleanup_module().
 */
#include  /* Необходим для макросов */
#include  /* Необходим для pr_info() */
#include  /* Необходим всем модулям */
 
static int __init hello_2_init(void)
{
    pr_info("Hello, world 2\n");
    return 0;
}
 
static void __exit hello_2_exit(void)
{
    pr_info("Goodbye, world 2\n");
}
 
module_init(hello_2_init);
module_exit(hello_2_exit);
 
MODULE_LICENSE("GPL");


Теперь у нас есть уже два реальных модуля ядра. Добавить ещё один будет совсем несложно:

obj-m += hello-1.o
obj-m += hello-2.o
 
PWD := $(CURDIR)
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


Загляните в drivers/char/Makefile, чтобы увидеть реальный пример. Как видите, некоторые элементы включаются в ядро жёстко (obj-y), но куда делись все obj-m? Те, кто знаком со скриптами оболочки, смогут без проблем их обнаружить. Для остальных подскажу, что записи obj-$(CONFIG_FOO), которые вы видите повсюду, расширяются на obj-y или obj-m в зависимости от того, на какое значение была установлена переменная CONFIG_FOO — y или m. Попутно отмечу, что именно эти переменные вы установили в файле .config в каталоге верхнего уровня дерева исходного кода в последний раз, когда выполнили make menuconfig или что-то в том духе.

▍ 4.3 Макросы __init и __exit


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

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

Макрос __exit приводит к пропуску функции, если модуль встроен в ядро, то есть аналогично __init не влияет на загружаемые модули. Опять же, если учесть, когда выполняется функция очистки, то это полностью оправданно. Встроенным драйверам не требуется очистка, а вот загружаемым модулям как раз да.

Эти макросы определены в include/linux/init.h и используются для освобождения памяти ядра. Если при его загрузке вы видите сообщение вроде Freeing unused kernel memory: 236k freed, то знайте — это тот самый процесс.

/*
 * hello-3.c – демонстрация макросов __init, __initdata и __exit.
 */
#include  /* Необходим для макросов */
#include  /* Необходим для pr_info() */
#include  /* Необходим для всех модулей */
 
static int hello3_data __initdata = 3;
 
static int __init hello_3_init(void)
{
    pr_info("Hello, world %d\n", hello3_data);
    return 0;
}
 
static void __exit hello_3_exit(void)
{
    pr_info("Goodbye, world 3\n");
}
 
module_init(hello_3_init);
module_exit(hello_3_exit);
 
MODULE_LICENSE("GPL");


▍ 4.4 Лицензирование и документирование модулей


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

$ sudo insmod xxxxxx.ko
loading out-of-tree module taints kernel.
module license 'unspecified' taints kernel.


Для обозначения лицензии вашего модуля вы можете использовать ряд макросов, например: «GPL», «GPL v2», «GPL and additional rights», «Dual BSD/GPL», «Dual MIT/GPL», «Dual MPL/GPL» и «Proprietary». Определены они в include/linux/module.h.

Для указания используемой лицензии существует макрос MODULE_LICENSE. Он и ещё пара макросов, описывающих модуль, приведены в примере ниже.

/*
 * hello-4.c – Демонстрирует документирование модуля.
 */
#include  /* Необходим для макросов */
#include  /* Необходим для pr_info() */
#include  /* Необходим для всех модулей */
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LKMPG");
MODULE_DESCRIPTION("A sample driver");
 
static int __init init_hello_4(void)
{
    pr_info("Hello, world 4\n");
    return 0;
}
 
static void __exit cleanup_hello_4(void)
{
    pr_info("Goodbye, world 4\n");
}
 
module_init(init_hello_4);
module_exit(cleanup_hello_4);


▍ 4.5 Передача в модуль аргументов командной строки


Модулям можно передавать аргументы командной строки, но не через argc/argv, к которым вы, возможно, привыкли.

Чтобы получить такую возможность, нужно объявить переменные, которые будут принимать значения аргументов командной строки как глобальные и затем использовать макрос module_param() (определяемый в include/linux/moduleparam.h) для настройки этого механизма. Во время выполнения insmod будет заполнять эти переменные получаемыми аргументами, например, insmod mymodule.ko myvariable=5. Для большей ясности объявления переменных и макросов необходимо размещать в начале модулей. Более наглядно всё это продемонстрировано в примере кода.

Макрос module_param() получает 3 аргумента: имя переменной, её тип и разрешения для соответствующего файла в sysfs. Целочисленные типы могут быть знаковыми, как обычно, или беззнаковыми. Если вы хотите использовать массивы целых чисел или строк, к вашим услугам module_param_array() и module_param_string().

int myint = 3;
module_param(myint, int, 0);


Массивы тоже поддерживаются, но в современных версиях работает это несколько иначе, нежели раньше. Для отслеживания количества параметров необходимо передать указатель на число переменных в качестве третьего аргумента. При желании вы можете вообще проигнорировать подсчёт и передать NULL. Вот пример обоих вариантов:

int myintarray[2];
module_param_array(myintarray, int, NULL, 0); /* если подсчёт не интересует */
 
short myshortarray[4];
int count;
module_param_array(myshortarray, short, &count, 0); /* подсчёт происходит в переменной "count" */


Хорошим применением для этого варианта будет предустановка значений переменных модуля, таких как порт или адрес ввода-вывода. Если переменные содержат предустановленные значения, выполнять автообнаружение. В противном случае оставлять текущее значение. Позже об этом будет сказано подробнее.

Наконец, есть ещё макрос MODULE_PARM_DESC(), используемый для документирования аргументов, которые может принять модуль. Он получает два параметра: имя переменной и строку в свободной форме, эту переменную описывающую.

Пример передачи аргументов командной строки в модуль
/*
 * hello-5.c – демонстрирует передачу аргументов командной строки в модуль.
 */
#include 
#include 
#include 
#include 
#include 
 
MODULE_LICENSE("GPL");
 
static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";
static int myintarray[2] = { 420, 420 };
static int arr_argc = 0;
 
/* module_param(foo, int, 0000)
 * Первым аргументом указывается имя параметра.
 * Вторым указывается его тип.
 * Третьим указываются биты разрешений
 * для представления параметров в sysfs (если не нуль) позднее.
 */
module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "A short integer");
module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(myint, "An integer");
module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "A long integer");
module_param(mystring, charp, 0000);
MODULE_PARM_DESC(mystring, "A character string");
 
/* module_param_array(name, type, num, perm);
 * Первым аргументом идёт имя параметра (в данном случае массива).
 * Второй аргумент – это тип элементов массива.
 * Третий – это указатель на переменную, которая будет хранить количество элементов массива, инициализированных пользователем при загрузке модуля.
 * Четвёртый аргумент – это биты разрешения.
 */
module_param_array(myintarray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintarray, "An array of integers");
 
static int __init hello_5_init(void)
{
    int i;
 
    pr_info("Hello, world 5\n=============\n");
    pr_info("myshort is a short integer: %hd\n", myshort);
    pr_info("myint is an integer: %d\n", myint);
    pr_info("mylong is a long integer: %ld\n", mylong);
    pr_info("mystring is a string: %s\n", mystring);
 
    for (i = 0; i < ARRAY_SIZE(myintarray); i++)
        pr_info("myintarray[%d] = %d\n", i, myintarray[i]);
 
    pr_info("got %d arguments for myintarray.\n", arr_argc);
    return 0;
}
 
static void __exit hello_5_exit(void)
{
    pr_info("Goodbye, world 5\n");
}
 
module_init(hello_5_init);
module_exit(hello_5_exit);


Рекомендую поэкспериментировать со следующим кодом:

$ sudo insmod hello-5.ko mystring="bebop" myintarray=-1
$ sudo dmesg -t | tail -7
myshort is a short integer: 1
myint is an integer: 420
mylong is a long integer: 9999
mystring is a string: bebop
myintarray[0] = -1
myintarray[1] = 420
got 1 arguments for myintarray.

$ sudo rmmod hello-5
$ sudo dmesg -t | tail -1
Goodbye, world 5

$ sudo insmod hello-5.ko mystring="supercalifragilisticexpialidocious" myintarray=-1,-1
$ sudo dmesg -t | tail -7
myshort is a short integer: 1
myint is an integer: 420
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintarray[0] = -1
myintarray[1] = -1
got 2 arguments for myintarray.

$ sudo rmmod hello-5
$ sudo dmesg -t | tail -1
Goodbye, world 5

$ sudo insmod hello-5.ko mylong=hello
insmod: ERROR: could not insert module hello-5.ko: Invalid parameters


▍ 4.6 Модули, состоящие из нескольких файлов


Иногда есть смысл поделить модуль на несколько файлов.

Вот пример такого модуля:

/*
 * start.c – пример модулей, состоящих из нескольких файлов.
 */
 
#include  /* Выполнение работы ядра. */
#include  /* В частности, модуля. */
 
int init_module(void)
{
    pr_info("Hello, world - this is the kernel speaking\n");
    return 0;
}
 
MODULE_LICENSE("GPL");


Второй файл:

/*
 * stop.c – пример модулей, состоящих из нескольких файлов.
 */
 
#include  /* Выполнение работы ядра. */
#include  /* В частности, модуля. */
 
void cleanup_module(void)
{
    pr_info("Short is the life of a kernel module\n");
}
 
MODULE_LICENSE("GPL");


И, наконец, Makefile:

obj-m += hello-1.o
obj-m += hello-2.o
obj-m += hello-3.o
obj-m += hello-4.o
obj-m += hello-5.o
obj-m += startstop.o
startstop-objs := start.o stop.o
 
PWD := $(CURDIR)
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


Это полный Makefile для всех примеров, которые мы успели рассмотреть. Первые пять строчек не представляют ничего особенного, но для последнего примера нам потребуется две строки. В первой мы придумываем имя объекта для нашего комбинированного модуля, а во второй сообщаем make, какие объектные файлы являются его частью.

▍ 4.7 Сборка модулей для скомпилированного ядра


Естественно, мы настоятельно рекомендуем вам перекомпилировать ядро, чтобы иметь возможность активировать ряд полезных функций отладки, таких как принудительная выгрузка модулей ( MODULE_FORCE_UNLOAD ): когда эта опция включена, можно с помощью команды sudo rmmod -f module принудить ядро выгрузить модуль, даже если оно сочтёт это небезопасным. В процессе разработки модуля эта опция может сэкономить вам много времени и избавить от лишних перезагрузок. Если вы не хотите перекомпилировать ядро, то рассмотрите вариант выполнения примеров внутри тестового дистрибутива на виртуальной машине. В таком случае при нарушении работоспособности вы сможете легко перезагрузиться или восстановить VM.

Существует ряд случаев, в которых вам может потребоваться загрузить модуль в уже скомпилированное работающее ядро. Как вариант, это может быть типичный дистрибутив Linux или ядро, которое вы сами скомпилировали ранее. Бывает, что загрузить модуль нужно в работающее ядро, перекомпилировать которое нет возможности, или на машину, перезагружать которую нежелательно. Если вам сложно представить случай, который может вынудить вас использовать модули для уже скомпилированного ядра, то просто пропустите этот раздел и расценивайте оставшуюся часть главы как большое примечание.

Итак, если вы просто установите дерево исходного кода, используете его для компиляции модуля и попытаетесь внедрить этот модуль в ядро, то в большинстве случаев получите ошибку:

insmod: ERROR: could not insert module poet.ko: Invalid module format


Более понятная информация логируется в системный журнал:

kernel: poet: disagrees about version of symbol module_layout


Иными словами, ваше ядро отказывается принимать модуль, потому что строки версии (точнее, vermagic, см. include/linux/vermagic.h) не совпадают. К слову, строки версии хранятся в объекте модуля в виде статической строки, начинающейся с vermagic:. Данные версии вставляются в модуль, когда он линкуется с файлом kernel/module.o. Для просмотра сигнатуры версии и прочих строк, хранящихся в конкретном модуле, выполните команду modinfo module.ko:

$ modinfo hello-4.ko
description:    A sample driver
author:         LKMPG
license:        GPL
srcversion:     B2AA7FBFCC2C39AED665382
depends:
retpoline:      Y
name:           hello_4
vermagic:       5.4.0-70-generic SMP mod_unload modversions


Для преодоления этой проблемы можно задействовать опцию --force-vermagic, но такое решение не гарантирует безопасность и однозначно будет неприемлемым в создании модулей. Следовательно, модуль нужно скомпилировать в среде, которая была идентична той, где создано наше скомпилированное ядро. Этому и будет посвящён остаток текущей главы.

Во-первых, убедитесь, что дерево исходного кода ядра вам доступно и имеет одинаковую версию с вашим текущим ядром. Далее найдите файл конфигурации, который использовался для компиляции ядра. Обычно он доступен в текущем каталоге boot под именем вроде config-5.14.x. Его будет достаточно скопировать в дерево исходного кода вашего ядра:

cp /boot/config-`uname -r` .config


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

Это небольшое отличие, а именно пользовательская строка, которая присутствует в версии модуля, но отсутствует в версии ядра, вызвано изменением относительно оригинала в Makefile, который содержат некоторые дистрибутивы. Далее вам нужно просмотреть собственный Makefile и обеспечить, чтобы представленная информация версии в точности соответствовала той, что указана в текущем ядре. Например, ваш Makefile может начинаться так:

VERSION = 5
PATCHLEVEL = 14
SUBLEVEL = 0
EXTRAVERSION = -rc2


В этом случае необходимо восстановить значение символа EXTRAVERSION на -rc2. Мы рекомендуем держать резервную копию Makefile, используемого для компиляции ядра, в /lib/modules/5.14.0-rc2/build. Для этого будет достаточно выполнить:

cp /lib/modules/`uname -r`/build/Makefile linux-`uname -r`


Здесь linux-`uname -r` — это исходный код ядра, которое вы собираетесь собрать.

Теперь выполните make для обновления конфигурации вместе с заголовками версии и объектами:

$ make
  SYNC    include/config/auto.conf.cmd
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
  HOSTCC  scripts/kconfig/confdata.o
  HOSTCC  scripts/kconfig/expr.o
  LEX     scripts/kconfig/lexer.lex.c
  YACC    scripts/kconfig/parser.tab.[ch]
  HOSTCC  scripts/kconfig/preprocess.o
  HOSTCC  scripts/kconfig/symbol.o
  HOSTCC  scripts/kconfig/util.o
  HOSTCC  scripts/kconfig/lexer.lex.o
  HOSTCC  scripts/kconfig/parser.tab.o
  HOSTLD  scripts/kconfig/conf


Если же вы не хотите фактически компилировать ядро, то можете прервать процесс сборки (CTRL-C) сразу же после строки SPLIT, поскольку в этот момент необходимые вам файлы уже готовы.

Теперь можно вернуться в каталог модуля и скомпилировать его: он будет собран в точном соответствии с настройками текущего ядра и загрузится в него без каких-либо ошибок.

Продолжение


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

sz7jpfj8i1pa6ocj-eia09dev4q.png

© Habrahabr.ru