Пишем модуль ядра linux

Обновлено: 05.07.2024

1. Концепция модуля
Модули ядра - это коды, которые могут быть загружены и выполнены ядром операционной системы при необходимости и могут быть выгружены, когда они не нужны. Это хорошая функция, которая расширяет функции ядра операционной системы без перезапуска системы.Это технология динамической загрузки.
Возможности: динамическая загрузка, загрузка в любое время, выгрузка в любое время, расширенные функции
2. Функция загрузки модуля ядра
Модуль ядра просто предварительно регистрирует себя в ядре Linux, чтобы его можно было использовать в будущих запросах; он состоит из объектных кодов и не образует законченную исполняемую программу. Он просто сообщает ядру, что у него есть новые функции, и оно не использует (выполняет) немедленно, просто подождите, пока приложение вызовет; и приложение начинает выполняться после его загрузки.
3. Функции, используемые модулем ядра
Нет внешней библиотеки функций для написания кода модуля ядра, могут использоваться только функции, экспортированные ядром. Прикладная программа привыкла использовать внешние библиотечные функции, связывая вместе программу и библиотечные функции во время компиляции. Например, сравните printf () и printk ().
Таким образом, файлы заголовков, используемые драйвером, берутся из исходного кода ядра, а файлы заголовков, используемые приложением, берутся из функций библиотеки.
4. Свободное место для кода модуля ядра.
Код ядра выполняется в пространстве ядра, а прикладная программа - в пространстве пользователя. Запуск приложения формирует новый процесс, но модуль ядра, как правило, этого не делает. Каждый раз, когда приложение выполняет системный вызов, режим выполнения Linux переключается с пользовательского пространства на пространство ядра.

1.1.2 Структура модуля ядра Linux

Минимум две точки входа
* Функция загрузки модуля module_init ()
* Функция удаления модуля module_exit ()
Два макроопределения module_init () и module_exit () объявляют функцию загрузки и функцию выгрузки модуля, которые определены в linux3.14 / include / linux / init.h. Содержание:

Каждый модуль может иметь только один module_init и один module_exit.
Давайте сравним приложение и увидим разницу между приложением и модулем ядра:

Таблица 1

Сравнение приложения и модуля ядра резюмируется следующим образом.
Таблица 2

1.1.3 Скомпилировать и загрузить модуль ядра Linux

Собственно, компиляция ядра уже упоминалась, теперь давайте рассмотрим:
Makefile ядра linux3.14 разделен на 5 компонентов:
Makefile на верхнем уровне Makefile

.config Текущий файл конфигурации ядра, который становится частью Makefile фиксированного уровня при компиляции.
arch / $ (ARCH) / Makefile Makefile, связанный с архитектурой
s / Makefile. * Некоторые общие правила Makefile
На всех уровнях Makefile kbuild содержится около 500 файлов, которые компилируются в соответствии с макросами, переданными из верхнего Makefile.

Определение и другие правила компиляции, компиляция исходного кода в модули или в ядро
Makefile верхнего уровня считывает содержимое файла .config и отвечает за сборку ядра и модулей в целом. Arch Makefile предоставляет дополнительную информацию, связанную с архитектурой. Makefile в каталоге s содержит все определения и правила, необходимые для сборки ядра на основе Makefile kbuild. (Содержимое .config является результатом конфигурации через файл Kconfig при создании menuconfig, как упоминалось выше)
Для разработчиков и пользователей большинства модулей ядра или драйверов устройств наиболее частым контактом является сборочный файл kbuild, основанный на архитектуре kbuild на каждом уровне каталога. Основные части:
1. Определение цели
Целевое определение используется для определения того, что нужно скомпилировать как модуль, а что - скомпилировать и связать с ядром.
Самым простым является всего одна строка, например

Это означает скомпилировать foo.o из файла foo.c или foo.s и связать его с ядром, в то время как obj-m означает, что файл скомпилирован как модуль. За исключением y и m, все цели в форме obj-x не будут компилироваться.
Поскольку он может быть скомпилирован в модуль или в ядро, более часто метод компиляции файла определяется в соответствии с переменной CONFIG_ файла .config, например:

В дополнение к целям в форме obj-, есть также цели, такие как библиотека lib-y, хост-программа hostprogs-y, но они в основном используются в определенных каталогах и случаях.
2. Несколько целей
Модуль ядра компилируется из нескольких исходных файлов, что отличается от Makefile.
Используйте форму имени модуля плюс суффикс -objs или суффикс -y, чтобы определить файлы компонентов модуля.
как в следующем примере:

Имя модуля - ext2. Два объектных файла Balloc.o и bitmap.o, наконец, связаны для создания ext2.o до файла ext2.ko. Включать ли xattr.o зависит от конфигурации файла конфигурации ядра. Не имеет значения, если значение CONFIG_EXT2_FS равно y, файл ext2.o, созданный во время этого процесса, будет связан со встроенным.o и, наконец, связан с ядром. Здесь следует отметить, что каталог, в котором находится Makefile kbuild, больше не должен содержать исходные файлы с тем же именем, что и модуль, например ext2.c / ext2.s
или записывается как -objs:

3. Итерация каталога

Если значение CONFIG_EXT2_FS равно y или m, kbuild отобразит каталог ext2 в нисходящей итерации, но его роль ограничена этим. Должны ли файлы в каталоге ext2 быть скомпилированы как модули или связаны с ядром, или Определяется содержимым Makefile в каталоге ext2
4. Различные методы компиляции модулей.
При компиляции модуля вы можете поместить его в дерево кода и использовать Make modules для компиляции модуля. Вы также можете поместить каталог файлов, связанных с модулем, за пределы дерева кода. Location, используйте следующую команду для компиляции модуля:

-C указывает корневой каталог исходного кода ядра, $ PWD или PWD Это переменная среды текущего каталога, указывающая kbuild вернуться в текущий каталог для выполнения операции сборки.
5. Установка модуля
Если вам нужно установить модуль в место, отличное от места по умолчанию, вы можете использовать INSTALL_MOD_PATH, чтобы указать префикс, например:

Модуль будет установлен в каталог / foo / lib / modules.
Примечание. Модуль ядра имеет суффикс .ko, который отличает модуль ядра от обычных объектных файлов.

1.2 Начало работы с компиляцией модуля ядра Makefile

1.2.1 Компиляция модулей

В предыдущей компиляции ядра, часть переноса драйвера, о которой мы говорили о компиляции драйвера, делится на статическую компиляцию и динамическую компиляцию; статическая компиляция означает, что драйвер непосредственно компилируется в ядро, а динамическая компиляция означает, что драйвер компилируется в модуль.
Существует два типа динамической компиляции:
a - внутренняя компиляция
скомпилировать в исходном каталоге ядра
b - внешняя компиляция
Компилировать вне каталога исходного кода ядра

1.2.2 Анализ конкретного процесса компиляции

Примечание: эта компиляция является внешней компиляцией. Используемый исходный код ядра - это исходный код Ubuntu, а не исходный код ядра Linux 3.14, используемый платой разработки. Работающая платформа - X86.
Для обычного модуля драйвера устройства Linux следующий код представляет собой классический код make-файла. Используйте следующий make-файл для компиляции большинства драйверов. Вам нужно только изменить драйвер, который будет скомпилирован во время использования. Имя хорошее. Просто измените значение obj-m.

1. Переменные в make-файле
Сначала объясните значение некоторых переменных в следующем make-файле:
(1) KERNELRELEASE определен в make-файле верхнего уровня в исходном коде ядра Linux.
(2) shell pwd для получения текущего рабочего пути
(3) shell uname -r получает номер версии текущего ядра.
(4) Каталог текущего исходного кода ядра KDIR.
Есть два каталога с исходным кодом Linux, а именно

Но если вы скомпилировали ядро, вы будете знать, что исходный код в каталоге usr обычно загружается и распаковывается нами самими, в то время как исходный код в каталоге lib автоматически копируется во время компиляции.Файловая структура этих двух файлов абсолютно одинакова, поэтому иногда Установите исходный каталог ядра в / usr / src / linux-header- $ (оболочка uname -r) /. Каталог исходного кода ядра может быть изменен в соответствии с его собственным местом хранения.
(5)make -C $ (LINUX_KERNEL_PATH) M= $ (CURRENT_PATH) modules
Это скомпилированный модуль:
a - Сначала измените каталог на местоположение, указанное параметром -C (т. е. каталог исходного кода ядра), где хранится make-файл верхнего уровня ядра;
b - параметр M = заставляет make-файл возвращаться в каталог исходного кода модуля перед созданием целевого объекта модулей; затем цель modueles указывает на модуль, установленный в переменной obj-m; в приведенном выше примере , Мы устанавливаем переменную hello.o.
2. Этапы выполнения make
a - Когда вы впервые входите, макрос «KERNELRELEASE» не определен, поэтому введите else;
b - записать путь к ядру, записать текущий путь;
Поскольку после make нет цели, make выполнит первую цель в Makefile, которая не начинается с., в качестве цели по умолчанию. По умолчанию выполняется правило all.
c – make -C $ (KDIR) M= $ (PWD) modules
-C Войдите в каталог ядра и выполните Makefile. KERNELRELEASE будет назначен во время выполнения. M = $ (PWD) означает возврат в текущий каталог, повторное выполнение make-файла, и модули компилируются в модули Смысл
Итак, на самом деле здесь выполняется

d-Запустить make-файл снова, KERNELRELEASE имеет значение, и будет выполнен obj-m: = hello.o
obj-m: означает связывание hello.o и других объектных файлов с файлом модуля hello.ko и компиляцию hello.c в файл hello.o при компиляции.
Здесь видно, что make вызывается всего 3 раза
1)– make
2) - Генерируется вызов верхнего уровня makedile дерева исходных текстов ядра Linux. o файл
3) вызов make-файла дерева исходных текстов ядра Linux, ссылка на файл .o в файл ko
3. Скомпилируйте несколько файлов.
Если имеется несколько исходных файлов, используйте следующий метод:

1.2.3 Простое описание внутренней компиляции

Если вы переместите модуль hello в исходный код ядра. Например, если вы поместите его в / usr / src / linux / driver /, будет определено KERNELRELEASE.
в / usr / src / linux / Makefile

В настоящее время модуль hello больше не компилируется только с помощью make, а компилируется с помощью модулей make в ядре. В это время модуль драйвера компилируется с ядром.

1.3 Передача параметров модуля

1.3.1 определение module_param ()

Обычно при программировании в пользовательском режиме, то есть в приложении, вы можете передавать параметры командной строки через main (), а когда вы пишете модуль ядра, вы передаете параметры через module_param ().
Макрос module_param () недавно добавлен в ядро ​​Linux 2.6. Этот макрос определен в файле include / linux / moduleparam.h и определяется следующим образом:

Итак, мы определяем параметр модуля через макрос module_param ():

Значение параметров:
name - это не только имя параметра, которое видит пользователь, но и переменная, которая принимает параметр в модуле;
type указывает тип данных параметра, который может быть одним из следующих: byte, short, ushort, int, uint, long, ulong, charp, bool, invbool;
perm определяет права доступа к соответствующим файлам в sysfs. Права доступа управляются так же, как разрешения доступа к файлам Linux, например 0644, или используются макросы в stat.h, такие как S_IRUGO.
0 означает полностью закрыть соответствующий элемент в sysfs.

1.3.2 Как использовать module_param ()

Макрос module_param () не объявляет переменные, поэтому вы должны объявить переменные перед использованием макроса. Типичное использование выглядит следующим образом:

Они должны быть записаны в начале исходного файла модуля. То есть int_var является глобальным. Также возможно сделать имя переменной внутри исходного файла модуля и имя внешнего параметра разными именами, которые определяются с помощью module_param_ named ().
a – module_param_named()
module_param_named(name, variable, type, perm);
имя внешнее (пространство пользователя) имя видимого параметра;
переменная Имя глобальной переменной в исходном файле;
тип
разрешения на доступ
И параметр module_param реализуется параметром module_param_ named, за исключением того, что имя совпадает с именем переменной.
Например:

b-строковый параметр
Если параметр модуля является строкой, для определения параметра модуля обычно используется тип charp. Ядро копирует строку, предоставленную пользователем, в память, и соответствующая переменная указывает на эту строку.
Например:

Другой метод - использовать макрос module_param_string (), чтобы позволить ядру скопировать строку непосредственно в массив символов в программе.

Здесь name - это имя внешнего параметра, string - это имя внутренней переменной, len - это размер буфера, названного строкой (он может быть меньше размера буфера, но не имеет значения), а perm означает права доступа к sysfs (или perm равен нулю, что означает полный Закройте соответствующий элемент sysfs).
Например:

параметр c-массива
параметры массива, значения, указанные в списке, разделенном запятыми, также поддерживаются загрузчиком модуля. Чтобы объявить параметр массива, используйте:

Имя массива имен (также имя параметра),
type Тип элемента массива,
num целочисленная переменная,
Обычное значение разрешения perm.
Если параметр массива установлен при загрузке, num устанавливается равным количеству предоставленных чисел.Загрузчик модуля отказывается иметь больше значений, чем может вместить массив.

1.3.3 Пример использования

Скомпилируйте и сгенерируйте исполняемый файл hello

Примечание:
a-Если при загрузке модуля hello параметры не вводятся, то начальное значение who - "world", а начальное значение times - 1;
b - При одновременной передаче строки в указатель нельзя передать такую ​​строку who = "hello world!", то есть в строке не должно быть пробелов;
c - / sys / module / hello / parameters Узел файла, соответствующий переменной, созданной в этом каталоге.

1.4 Экспорт таблицы символов

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

1.4.1 Определение макроса EXPORT_SYMBOL анализ

1. Исходный код

1.4.2 Как использовать EXPORT_SYMBOL


Сначала используйте EXPORT_SYMBOL (имя функции) после определения функции модуля.
Во-вторых, используйте extern, чтобы объявить его в модуле, который вызывает функцию
В-третьих, сначала загрузите модуль, определяющий функцию, а затем загрузите модуль, который вызывает функцию.
Для вызова интерфейсов функций и глобальных переменных, реализованных другими модулями, необходимо экспортировать символ /usr/src/linux-headers-2.6.32-33-generic/Module.symvers

Функция A сначала экспортирует функцию show (), а функция B использует extern для ее объявления. Примечание.
a - После компиляции модуля a скопируйте Module.symvers в модуль b.
b - только после этого можно скомпилировать модуль b.
c - Загрузить: сначала загрузить модуль a, затем модуль b
d - Удаление: сначала удалите модуль b, затем удалите модуль a

1.4.3 Примеры

После компиляции загрузите и выгрузите модули, и вы можете использовать dmesg для просмотра информации о печати ядра.

Подсказки:

Описание файла заголовка драйвера

В каталоге файлов заголовков всего 32 файла заголовков .h. Среди них 13 в основном каталоге, 4 в подкаталоге asm, 10 в подкаталоге linux и 5 в подкаталоге sys. Функции этих файлов заголовков заключаются в следующем:
1. Главный каталог

2. Подкаталог include / asm файлов заголовков, относящихся к архитектуре.
Эти файлы заголовков в основном определяют некоторые структуры данных, макрофункции и переменные, тесно связанные с архитектурой ЦП. Всего 4 файла.

3. Подкаталог специального заголовочного файла ядра Linux include / linux

4. Подкаталог с системной структурой данных include / sys


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

Модуль ядра Linux — это скомпилированный двоичный код, который вставляется непосредственно в ядро Linux, работая в кольце 0, внутреннем и наименее защищённом кольце выполнения команд в процессоре x86–64. Здесь код исполняется совершенно без всяких проверок, но зато на невероятной скорости и с доступом к любым ресурсам системы.

Не для простых смертных

Написание модуля ядра Linux — занятие не для слабонервных. Изменяя ядро, вы рискуете потерять данные. В коде ядра нет стандартной защиты, как в обычных приложениях Linux. Если сделать ошибку, то повесите всю систему.

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

Можно в основном забыть традиционные парадигмы разработки приложений. Кроме загрузки и выгрузки модуля, вы будете писать код, который реагирует на системные события, а не работает по последовательному шаблону. При работе с ядром вы пишете API, а не сами приложения.

У вас также нет доступа к стандартной библиотеке. Хотя ядро предоставляет некоторые функции вроде printk (которая служит заменой printf ) и kmalloc (работает похоже на malloc ), в основном вы остаётесь наедине с железом. Вдобавок, после выгрузки модуля следует полностью почистить за собой. Здесь нет сборки мусора.

Необходимые компоненты

Прежде чем начать, следует убедиться в наличии всех необходимых инструментов для работы. Самое главное, нужна машина под Linux. Знаю, это неожиданно! Хотя подойдёт любой дистрибутив Linux, в этом примере я использую Ubuntu 16.04 LTS, так что в случае использования других дистрибутивов может понадобиться слегка изменить команды установки.

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

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

Установка среды разработки

На Ubuntu нужно запустить:


Устанавливаем самые важные инструменты разработки и заголовки ядра, необходимые для данного примера.

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

Начинаем

Приступим к написанию кода. Подготовим нашу среду:


Запустите любимый редактор (в моём случае это vim) и создайте файл lkm_example.c следующего содержания:


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

  • В include перечислены файлы заголовков, необходимые для разработки ядра Linux.
  • В MODULE_LICENSE можно установить разные значения, в зависимости от лицензии модуля. Для просмотра полного списка запустите:


Если мы запускаем make , он должен успешно скомпилировать наш модуль. Результатом станет файл lkm_example.ko . Если выскакивают какие-то ошибки, проверьте, что кавычки в исходном коде установлены корректно, а не случайно в кодировке UTF-8.

Теперь можно внедрить модуль и проверить его. Для этого запускаем:


Если всё нормально, то вы ничего не увидите. Функция printk обеспечивает выдачу не в консоль, а в журнал ядра. Для просмотра нужно запустить:


Вы должны увидеть строку “Hello, World!” с меткой времени в начале. Это значит, что наш модуль ядра загрузился и успешно сделал запись в журнал ядра. Мы можем также проверить, что модуль ещё в памяти:


Для удаления модуля запускаем:


Если вы снова запустите dmesg, то увидите в журнале запись “Goodbye, World!”. Можно снова запустить lsmod и убедиться, что модуль выгрузился.

Как видите, эта процедура тестирования слегка утомительна, но её можно автоматизировать, добавив:


в конце Makefile, а потом запустив:


для тестирования модуля и проверки выдачи в журнал ядра без необходимости запускать отдельные команды.

Теперь у нас есть полностью функциональный, хотя и абсолютно тривиальный модуль ядра!

Немного интереснее

Копнём чуть глубже. Хотя модули ядра способны выполнять все виды задач, взаимодействие с приложениями — один из самых распространённых вариантов использования.

Поскольку приложениям запрещено просматривать память в пространстве ядра, для взаимодействия с ними приходится использовать API. Хотя технически есть несколько способов такого взаимодействия, наиболее привычный — создание файла устройства.

Вероятно, раньше вы уже имели дело с файлами устройств. Команды с упоминанием /dev/zero , /dev/null и тому подобного взаимодействуют с устройствами “zero” и “null”, которые возвращают ожидаемые значения.

В нашем примере мы возвращаем “Hello, World”. Хотя это не особенно полезная функция для приложений, она всё равно демонстрирует процесс взаимодействия с приложением через файл устройства.

Вот полный листинг:

Тестирование улучшенного примера


Теперь после запуска make test вы увидите выдачу старшего номера устройства. В нашем примере его автоматически присваивает ядро. Однако этот номер нужен для создания нового устройства.

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


(в этом примере замените MAJOR значением, полученным в результате выполнения make test или dmesg )

Параметр c в команде mknod говорит mknod, что нам нужно создать файл символьного устройства.

Теперь мы можем получить содержимое с устройства:


или даже через команду dd :


Вы также можете получить доступ к этому файлу из приложений. Это необязательно должны быть скомпилированные приложения — даже у скриптов Python, Ruby и PHP есть доступ к этим данным.

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

Заключение

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

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

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

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

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

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

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

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

Рядом положим файл Makefile:

obj - m += hello . o

Говорим make . В результате должен появиться файл hello.ko.

Теперь попробуем следующие команды:

Начнем с более детального рассмотрения pr_info() . Среди символов, экспортируемых ядром, вы его не найдете. Поиск по заголовочным файлам показывает, что на самом деле это макрос, объявленный в linux/printk.h:

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

$ sudo cat /proc/kallsyms | grep 'T printk'
.
801833e0 T printk_nmi_direct_exit
809f1508 T printk
809f16f4 T printk_deferred
.

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

static char * name = "%username%" ;

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

sudo insmod param.ko name =Alex
sudo rmmod param
tail / var / log / syslog

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

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

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

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

Введение в написание модулей ядра Linux.
Valerie Henson
14 февраля 2008 12:33

Версия для печати (откроется в новом окне)

С давних пор, первым шагом в изучении нового языка программирования, является написание программы, которая печатает «Hello, world!». В этой статье мы используем такой подход при изучении написания простых модулей ядра Linux и драйверов устройств. Мы изучим, как напечатать «Hello, world!» из модуля ядра тремя различными способами: printk(), из /proc файла и из устройства в /dev.

Приготовления.

Модуль ядра — это часть кода ядра, которая может быть динамически загружена и выгружена во время работы ядра. Так как он запускается как часть ядра и ему необходимо близко взаимодействовать с ядром, модуль ядра не может быть скомпилирован в вакууме. Ему нужны, по крайней мере, заголовочные файлы и настройки для ядра, в которое он будет загружен. Компилирование модуля также требует набор средств разработки, такие как компилятор. Для простоты, мы сжато опишем как установить требуемые вещи для создания модуля ядра для Debian, Fedora и «ванильного» ядра Linux из тарболла. Во всех случаях, вы должны скомпилировать свой модуль относительно исходного кода работающего на вашей системе ядра.

Исходные коды ядра обычно находятся в директории /usr/src/linux, права доступа к которой имеет только root. В настоящее время, рекомендуется размещать исходные коды ядра в домашней директории, права на которую будут иметь не-root пользователи. Комманды в этой стстье запускаются не из-под root, используя sudo для временного получения привелегий root-а только тогда, когда это необходимо. Для установки sudo, прочитайте sudo(8), visudo(8) и sudoers(5). Вы можете запускать все комманды и из-под root, если пожелаете. В этом случае, вам потребуются права root-а для того, чтобы следовать инструкциям из этой статьи.

Приготовления для компилирования модулей ядра в Debian.

Пакет module-assistant для Debian настраивает систему для сборки внешних модулей ядра. Установите его с помощью:

Это все. Теперь вы можете скомпилировать модуль ядра. Дополнительно, вы можете прочитать Debian Linux Kernel Handbook, чтобы иметь более глубокое представление о работе с ядром в Debian.

Приготовления для компилирования модулей в Fedora

Пакет kernel-devel в Fedora имеет все необходимые заголовочные файлы ядра и инструменты для сборки внешних модулей ядра. Установите его с помощью:

И снова, это все, что нужно, — теперь вы можете компилировать модули ядра. Документацию по теме можно найти в Fedora release notes.

«Ванильное» ядро и его настройки.
Если вы решили использовать ванильное ядро Linux, то сначала вы должны его сконфигурировать, скомпилировать, установить и перезагрузиться с вашим новым ванильным ядром. Эти действия не так уж и просты, но эта статья покрывает только базовые знания по работе с ванильным ядром.

Распакуйте исходные коды ядра:

Теперь ваше ядро расположено в linux- /. Войдите в эту директорию с начните конфигурацию:

Существует набор параметров для make, чтобы автоматически собрать и установить ядро в различной форме: пакет Debian, пакет RPM, тарболл и т.д. Чтобы узнать их всех, наберите:

Параметр, который будет работать почти на каждом дистрибутиве:

Когда сборка завершится, установите новое ядро с помощью:

Затем создайте ссылку на дерево исходных кодов ядра в его стандартном расположении:

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

«Hello, World!» с помощью printk()

Он содержит два файла: Makefile, который содержит инструкции для сборки модуля, и hello_printk.c, исходный код модуля. Для начала, осмотрим Makefile:

obj-m — это список модулей ядра для сборки. .o будет автоматически собран из соответствующего .c файла (нет необходимости указывать список исходных файлов).

KDIR — это расположение исходного кода ядра. Текущий стандарт это ссылка на соответствующее дерево исходных кодов, содержащее скомпилированные модули.

PWD — это текущая директория и расположения наших исходных кодов модуля.

default это стандартный параметр make. До тех пор, пока не будет указан другой параметр, make будет исполнять правила для этого параметра. Правило, указанное здесь запускает make с рабочей директорией, содержащей исходные коды ядра и компилирует модули только в $(PWD) (локальной) директории. Это позволяет нам использовать все правила для компилирования модулей, определенные в главном дереве исходного кода ядра.

А сейчас, давайте пройдемся по коду hello_printk.c.

Эти включения заголовочных файлов, предоставляемых ядром, требуются для всех модулей. Они включают такие вещи, как определение макроса module_init(), который мы увидим позже.

Макрос module_init() сообщает ядру, какую функцию выполнить при загрузке модуля. Все остальное, происходящее внутри ядра — результат установок функции инициализации модуля.

Проще говоря, функция выхода запускается один раз, когда модуль выгружается, а макрос module_exit() идентифицирует функцию выхода. Ключевое слово __exit указывает ядру, что этот код нужно выполнить единожды, во время выгрузки модуля.

MODULE_LICENSE() сообщает ядру, под какой лицензией распространяется исходный код модуля, что влияет на то, к каким символам (функциям, переменным и т.д.) он может получить доступ в главном ядре. Модуль под лицензией GPLv2 (как на примере) имеет доступ ко всем символам. Другие лицензии модуля предупредят ядро о том, что был загружен закрытый модуль, или модуль, которому нельзя доверять. Модули без MODULE_LICENSE() распознаются ядром, как модули, выпущенные не под GPLv2. Макросы MODULE_*() полезны для идентификации информации о модуле в стандартной форме.

Теперь скомпилируем и запустим код. Войдите в директорию с кодом и соберите модуль:

Вы успешно скомпилировали и установили модуль ядра!

«Hello, World!» с помощью /proc

Один из простейших и наиболее популярных способов сделать связь между ядром и пользовательскими программами — файл в /proc. /proc это псевдофайловая система, где операция чтения из файлов возвращает данные, синтезированные ядром, а данные, записываемые в файлы, читаются и оперируются ядром. До появления /proc, связь пользователь-ядро осуществлялась посредством системных вызовов. Использование системных вызовов подразумевает выбор между нахождением системного вызова, который осуществляет нужное действие (зачастую это невозможно), созданием нового системного вызова (требует больших изменений в ядре, использования номера системного вызова, и в общем-то не одобряется), или использованием системного вызова ioctl(), который требует создания специального файла, с которым ioctl() оперирует (может возникнуть ошибка, и ещё больше не одобряется). /proc предоставляет простой, предопределенный путь передачи данных между ядром и пользовательским пространством с полезными инструментами и достаточной свободой действий модулей ядра.

Для наших целей, мы создадим файл в /proc, который выведет «Hello, world!» при чтении. Мы будем использовать /proc/hello_world. Скачайте и распакуйте тарболл hello_proc модуля. Заглянем в код hello_proc.c:

На этот раз, мы добавили заголовочный файл для procfs, который включает поддержку работы с /proc.

Следующая функция вызовется, когда процесс выполнит read() на созданный нами файл в /proc. Это проще, чем реализовывать это полностью на системном вызове read(), так как нам надо разрешить считать строку «Hello, world!» только один раз.

Аргументы этой функции нуждаются в подробном объяснении. buffer это указатель на буффер ядра, в который мы пишем выходные данные из read(). start используется для более сложных /proc файлов; здесь мы его игнорируем. offset указывает на то место, с которого надо начинать считывание внутри «файла»; здесь мы передадим 0 для простоты. size это размер буффера в байтах; мы должны удостовериться в том, что мы не пишем данные после того, как буффер закончится. eof указывает на конец файла. Аргумент data, опять же, для более сложных файлов и здесь игнорируется.

А теперь, тело функции:

Затем, нам надо зарегистрировать подсистему /proc в функции инициализации нашего модуля:

И отменить эту регистрацию, когда модуль выгружен (если мы этого не сделаем, то когда процесс попробует считать /proc/hello_world, файловая система /proc попробует выполнить функцию, которая больше не существует, что вызовет kernel panic).

Теперь мы готовы скомпилировать и загрузить модуль:

Появится файл под именем /proc/hello_world, который будет выводить «Hello, world!» при чтении:

Вы можете создать много других /proc файлов для одного и того же драйвера, добавить функции, позваляющие запись в /proc файлы, создавать директории, полные /proc файлов, и так далее. Для чего-то более сложного, чем этот драйвер, проще и безопаснее использовать вспомогательные функции seq_file при написании интерфейсных функции для /proc. Для расширения своих знаний в этой области, прочтите Driver porting: The seq_file interface.

«Hello, World!» с помощью /dev/hello_world

Теперь мы реализуем «Hello, world!» с помощью файла устройства в /dev, /dev/hello_world. Раньше файл устройства был специальным файлом, создаваемым запуском хитроумного шелл-скрипта под названием MAKEDEV, который вызывал комманду mknod для создания любого возможного файла в /dev, невзирая на то, запущено ли это устройство в системе. Потом был devfs, который создавал /dev файлы, когда к ним впервые обращались, что вызывало много интересных проблем блокировки и множество попыток открыть файл устройства, чтобы проверить его существование. Сейчас с /dev работает udev, который связывает /dev с программами пользовательского окружения. Когда модули ядра регистрируют устройсва, они появляются в файловой системе sysfs, смонтированной в /sys. Программа из пользовательского окружения, udev, следит за изменениями в /sys и динамечески создает записи в /dev, исходя из настроек, обычно размещаемых в /etc/udev.

Как мы видим из списка необходимых заголовочных файлов, для создания устройства требуется немного больше поддержки ядра, чем в предыдущих методах. fs.h включает в себя определения структур для операций над файлами, которые мы должны использовать, применительно к нашему /dev файлу. miscdevice.h включает поддержку регистрации различных файлов устройств. asm/uaccess.h влючает функции для тестирования на чтение или запись в памяти пользовательского окружения, без нарушения прав.

hello_read() — функция, вызываемая процессом системным вызовом read() к /dev/hello. Она выведет «Hello, world!» в буффер, передаваемый вызовом read().

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

Теперь создадим структуру, содержащую информацию для регистрации стороннего устройства в ядре:

Как обычно, мы регистрируем устройство в функции инициализации модуля:

И не забываем выгружать устройство в функции выхода:

Скомпилируем и загрузим модуль:

В системе появилось устройство с именем /dev/hello, которое выводит «Hello, world!» в момент считывания root-ом:

Но оно не может считаться обычным пользователем:

Это происходит при стандартном правиле udev, которое, при появлении нового устройства, создает файл под названием /dev/ и присваивает ему права на чтение 0660. Нам необходимо создать вместо устройства ссылку, читаемую обычными пользователями, с именем /dev/hello_world. Для того, чтобы это сделать, напишем правило udev.

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

Разобьем это правило на части и более подробно разберем каждую из них.

KERNEL=="hello" приводит к выполнению правила, когда устройство с именем в строке (== это оператор сравнения) появляется в /sys. Устройство hello появляется когда мы вызываем misc_register() со структура, содержащей имя устройства «hello». Вот результат в /sys:

SYMLINK+="hello_world" приказывает добавить hello_world к списку ссылок, которые должны создаваться при появлении устройства. В нашем случае, мы знаем, что список будет состоять из одной ссылки, но другие устройства могут иметь много udev правил, которые создают другие ссылки, так что было бы неплохо это сделать.

MODE="0444" назначает права доступа к оригинальному файлу устройства на 0444, что позволяет получить доступ на чтение к этому файлу.

Очень важно использовать правильный оператор (==, += или =), во избежание появления неожиданного результата.

Теперь, когда мы понимаем, что делает это правило, давайте установим его в директорию /etc/udev. Файлы правил udev раположены в той же манере, что и скрипты инициализации System V в /etc/init.d/. Udev выполняет каждый скрипт из директории правил udev, /etc/udev/rules.d, в алфавитном/номерном порядке. Также, как и скрипты инициализации System V, файлы в /etc/udev/rules.d являются обычными ссылками на реально существующие файлы, так что правила будут выполнятся в правильном порядке.

Теперь, перезагрузите драйвер hello world и посмотрите на записи в /dev:

Теперь у нас есть /dev/hello_world! Наконец, проверьте, что вы можете считывать устройства «Hello, world!», как обычный пользователь:

За более подробной информацией по написанию правил udev, обращайтесь к Writing udev rules, написанную Дэниэлом Дрэйком.

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

Contents

Обзор

Чтобы создать модуль ядра, вы можете прочитать The Linux Kernel Module Programming Guide. Модуль можно сконфигурировать как вкомпилированный, а можно как загружаемый. Чтобы иметь возможность динамически загружать или выгружать модуль, его необходимо сконфигурировать как загружаемый модуль в настройке ядра (в этом случае строка, относящаяся к модулю должна быть отмечена буквой M ).

Модули хранятся в /usr/lib/modules/kernel_release . Чтобы узнать текущую версию вашего ядра, используйте команду uname -r .

Примечание: Часто в названии модулей используются подчёркивания ( _ ) или дефисы ( - ); однако, эти символы взаимозаменяемы как при использовании команды modprobe , так и в конфигурационных файлах в /etc/modprobe.d/ .

Получение информации

Чтобы узнать, какие модули ядра загружены в настоящий момент:

Чтобы показать информацию о модуле:

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

Чтобы отобразить настройки для всех модулей:

Чтобы отобразить настройки для отдельного модуля:

Чтобы узнать зависимости модуля (или его псевдонима), включая сам модуль:

Автоматическое управление модулями

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

Смотрите modules-load.d(5) для дополнительной информации.

Управление модулями вручную

Управление модулями ядра производится с помощью утилит, предоставляемых пакетом kmod . Вы можете использовать эти утилиты вручную.

Примечание: Если вы обновили ваше ядро, но ещё не перезагрузились, modprobe не сработает без каких либо уведомлений об ошибках и завершится с ошибкой 1, потому что путь /lib/modules/$(uname -r)/ больше не существует. Проверьте вручную существование этого пути, если modprobe не работает, чтобы убедиться, что это ваш случай.

Загрузка модуля из другого места (для тех модулей, которых нет в /lib/modules/$(uname -r)/ ):

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

Настройка параметров модуля

Чтобы передать параметр модулю ядра, вы можете воспользоваться конфигурационным файлом в modprobe или использовать командную строку ядра.

С помощью файлов в /etc/modprobe.d/

Файлы в директории /etc/modprobe.d/ можно использовать для передачи настроек модуля в udev, который через modprobe управляет загрузкой модулей во время загрузки системы. Конфигурационные файлы в этой директории могут иметь любое имя, оканчивающееся расширением .conf . Синтаксис следующий:

Примечание: Если какой-либо из затрагиваемых модулей загружается из initramfs, тогда вам придётся добавить соответствующий .conf файл в FILES в файле /etc/mkinitcpio.conf или использовать modconf хук, чтобы он был включен в initramfs.

С помощью командной строки ядра

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

Просто добавьте это в загрузчике в строку с ядром, как описано в параметрах ядра.

Создание псевдонимов

Псевдонимы (алиасы) - это альтернативные названия для модуля. Например: alias my-mod really_long_modulename означает, что вы можете использовать modprobe my-mod вместо modprobe really_long_modulename . Вы также можете использовать звёздочки в стиле shell, то есть alias my-mod* really_long_modulename будет иметь тот же эффект, что и modprobe my-mod-something . Создайте алиас:

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

Запрет загрузки

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

Некоторые модули загружаются как часть initramfs. Команда mkinitcpio -M напечатает все автоматически обнаруженные модули: для предотвращения initramfs от загрузки каких-то из этих модулей, занесите их в чёрный список в /etc/modprobe.d/modprobe.conf . Команда mkinitcpio -v отобразит все модули, которые необходимы некоторым хукам (например, filesystems хук, block хук и т.д.). Не забудьте добавить этот .conf файл в секцию FILES в /etc/mkinitcpio.conf , если вы этого ещё не сделали, пересоберите initramfs после того, как вы запретили загрузку модулей, а затем перезагрузитесь.

С помощью файлов в /etc/modprobe.d/

Создайте .conf файл в /etc/modprobe.d/ и добавьте строку для каждого модуля, который вы хотите запретить, используя ключевое слово blacklist . Например, если вы хотите запретить загружать модуль pcspkr :

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

Можно изменить такое поведение. Команда install заставляет modprobe запускать вашу собственную команду вместо вставки модуля в ядро как обычно. Поэтому вы можете насильно сделать так, чтобы модуль никогда не загружался:

Читайте также: