Модули ядра Linux: загрузка, зависимости, символы и жизненный цикл

Модуль ядра Linux — это объектный файл в формате ELF (расширение .ko), который загружается в адресное пространство ядра во время работы системы без перезагрузки. Модули позволяют добавлять драйверы, файловые системы, сетевые протоколы и отладочные механизмы «на лету», не пересобирая ядро целиком.

Что представляет собой модуль на уровне формата​


Файл .ko — это обычный ELF-объект с дополнительными секциями, специфичными для ядра:

СекцияНазначение
.modinfoМетаданные модуля: автор, описание, зависимости, параметры, лицензия
__ksymtabТаблица экспортируемых символов
__kcrctabCRC-значения экспортируемых символов (при включённом CONFIG_MODVERSIONS)
__versionsИмена и CRC импортируемых символов (при CONFIG_BASIC_MODVERSIONS)
.init.textКод, выполняемый только при инициализации модуля
.exit.textКод, выполняемый только при выгрузке модуля
.gnu.linkonce.this_moduleСтруктура struct module, через которую ядро управляет модулем

После загрузки секции .init.text освобождаются, что экономит память. Секция .exit.text остаётся в памяти, если модуль не поддерживает выгрузку (например, встроенный драйвер критичной подсистемы).

Жизненный цикл модуля​


Загрузка (insmod / modprobe)​


Когда пользователь выполняет insmod module.ko или modprobe module, происходит следующая последовательность:

  1. Ядро читает ELF-заголовок и проверяет секции.
  2. Разрешаются зависимости: ядро ищет все символы, которые модуль импортирует, среди уже загруженных модулей и встроенного ядра.
  3. Если включён CONFIG_MODVERSIONS, CRC каждого импортируемого символа сравнивается с CRC, записанным в __versions модуля. Несоответствие — отказ в загрузке.
  4. Выделяется память под код и данные модуля.
  5. Выполняются релокации.
  6. Вызывается функция инициализации, указанная макросом module_init().

modprobe отличается от insmod тем, что автоматически подтягивает зависимости, читая файл /lib/modules/$(uname -r)/modules.dep, сгенерированный утилитой depmod.

Работа​


После инициализации модуль функционирует как часть ядра: его код выполняется в контексте ядра, имеет полный доступ к памяти и аппаратным ресурсам. Модуль может регистрировать обработчики прерываний, создавать устройства в sysfs, выделять память через kmalloc/vmalloc, использовать workqueue и другие ядерные API.

Выгрузка (rmmod / modprobe -r)​


При выгрузке:

  1. Проверяется счётчик ссылок (refcnt). Если другие модули или процессы используют данный модуль, выгрузка отклоняется.
  2. Вызывается функция очистки, указанная макросом module_exit().
  3. Освобождается память, занятая модулем.
  4. Модуль удаляется из списка загруженных.

Важно: если функция module_exit() не определена или модуль помечен как неудаляемый, выгрузка невозможна.

Минимальный пример модуля​


C:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int __init mymodule_init(void)
{
    pr_info("mymodule: loaded\n");
    return 0;
}

static void __exit mymodule_exit(void)
{
    pr_info("mymodule: unloaded\n");
}

module_init(mymodule_init);
module_exit(mymodule_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("Minimal kernel module");

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

Система сборки kbuild​


Модули собираются через kbuild — встроенную систему сборки ядра. Для внешнего (out-of-tree) модуля достаточно двух файлов.

Kbuild-файл​


Makefile:
obj-m := mymodule.o

Если модуль состоит из нескольких исходных файлов:

Makefile:
obj-m := mymodule.o
mymodule-y := core.o hardware.o protocol.o

Makefile-обёртка​


Makefile:
KDIR ?= /lib/modules/$(shell uname -r)/build

default:
	$(MAKE) -C $(KDIR) M=$$PWD

clean:
	$(MAKE) -C $(KDIR) M=$$PWD clean

Сборка выполняется командой:

Bash:
make -C /lib/modules/$(uname -r)/build M=$PWD

Параметр -C переключает make в каталог с исходниками ядра, а M=$PWD сообщает kbuild, что собирается внешний модуль. Результат — файл mymodule.ko в текущем каталоге.

Начиная с Linux 6.13 доступна альтернатива с флагом -f, которая избегает смены рабочего каталога:

Bash:
make -f /lib/modules/$(uname -r)/build/Makefile M=$PWD

Установка модуля​


Bash:
make -C /lib/modules/$(uname -r)/build M=$PWD modules_install

По умолчанию внешние модули устанавливаются в /lib/modules/$(uname -r)/updates/. Каталог можно изменить переменной INSTALL_MOD_DIR.

Экспорт и импорт символов​


Модуль может использовать функции и переменные, экспортированные ядром или другими модулями. Экспорт выполняется макросами:

МакросВидимость
EXPORT_SYMBOL(sym)Все модули
EXPORT_SYMBOL_GPL(sym)Только модули с GPL-совместимой лицензией
EXPORT_SYMBOL_NS(sym, ns)Экспорт в именованное пространство символов

При загрузке модуля ядро проверяет, что каждый неопределённый символ (undefined symbol) из таблицы .symtab модуля разрешается через экспортированные символы. Если символ не найден — загрузка завершается ошибкой Unknown symbol.

Проверить, какие символы экспортирует загруженный модуль:

Bash:
cat /proc/kallsyms | grep mymodule

Или посмотреть в Module.symvers после сборки ядра.

Версионирование модулей (CONFIG_MODVERSIONS)​


При включённом CONFIG_MODVERSIONS для каждого экспортируемого символа вычисляется CRC на основе полного прототипа. Файл Module.symvers хранит эти значения в формате:

Код:
<CRC> <Symbol> <Module> <Export Type> <Namespace>
0xe1cc2a05 usb_stor_suspend drivers/usb/storage/usb-storage EXPORT_SYMBOL_GPL USB_STORAGE

При загрузке модуля ядро сравнивает CRC из секции __versions модуля с текущими значениями. Если прототип функции изменился между сборкой модуля и сборкой ядра, CRC не совпадёт и модуль не загрузится. Это защищает от бинарной несовместимости.

Ограничение: CONFIG_BASIC_MODVERSIONS поддерживает имена символов длиной до 64 байт. Для более длинных имён (например, в Rust-модулях) требуется CONFIG_EXTENDED_MODVERSIONS.

Зависимости между модулями​


Утилита depmod сканирует все .ko-файлы в /lib/modules/$(uname -r)/ и строит граф зависимостей, записывая результат в modules.dep. Формат строки:

Код:
kernel/drivers/net/ethernet/intel/e1000/e1000.ko: kernel/lib/crc32.ko

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

Посмотреть зависимости конкретного модуля:

Bash:
modinfo -F depends mymodule

Параметры модуля​


Модули могут принимать параметры при загрузке:

C:
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Verbosity level (0-3)");

Передача при загрузке:

Bash:
insmod mymodule.ko debug_level=2
modprobe mymodule debug_level=2

После загрузки параметры доступны через sysfs:

Bash:
cat /sys/module/mymodule/parameters/debug_level

Если права доступа 0644, значение можно изменить без перезагрузки модуля:

Bash:
echo 3 > /sys/module/mymodule/parameters/debug_level

Диагностика и типичные ошибки​


Модуль не загружается: «Unknown symbol»​


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

Bash:
dmesg | tail -20

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

Несовпадение версий (version magic)​


Каждый модуль содержит строку vermagic, включающую версию ядра, конфигурационные флаги (SMP, preempt и др.). Если vermagic модуля не совпадает с текущим ядром, загрузка отклоняется. Проверка:

Bash:
modinfo mymodule.ko | grep vermagic
uname -r

Ошибка при выгрузке: «Module is in use»​


Счётчик ссылок ненулевой. Проверка:

Bash:
lsmod | grep mymodule

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

Модуль загружается, но не работает​


Частая причина — ошибка в module_init(), которая возвращает ненулевой код. Ядро при этом выгружает модуль автоматически. Логи:

Bash:
dmesg | grep mymodule
journalctl -k --since "1 min ago"

Безопасность и ограничения​


  • Загрузка модуля требует CAP_SYS_MODULE. В системах с включённым Secure Boot и подписанными модулями (CONFIG_MODULE_SIG_FORCE) неподписанный модуль не загрузится.
  • Модуль выполняется в кольце 0 с полным доступом к памяти. Ошибка в коде модуля приводит к kernel panic.
  • Для продакшена рекомендуется включать CONFIG_MODULE_SIG и подписывать модули ключом, встроенным в ядро.
  • CONFIG_MODULE_SIG_FORCE полностью запрещает загрузку неподписанных модулей.

Проверить, включена ли проверка подписей:

Bash:
cat /proc/sys/kernel/modules_disabled
cat /sys/module/module/parameters/sig_enforce

Значение sig_enforce = Y означает, что ядро принимает только подписанные модули.

Быстрая проверка результата после сборки и загрузки​


Bash:
## Сборка
make -C /lib/modules/$(uname -r)/build M=$PWD

## Загрузка
sudo insmod mymodule.ko

## Проверка
lsmod | grep mymodule
dmesg | tail -5

## Выгрузка
sudo rmmod mymodule

## Подтверждение выгрузки
dmesg | tail -3

Если lsmod не показывает модуль после insmod, а dmesg содержит ошибку — модуль не прошёл инициализацию. Если rmmod возвращает «in use» — есть активные ссылки, и нужно найти зависимый модуль или процесс.

Источники​


 

Похожие темы

Назад
Верх Низ