Руководстве по программированию модулей

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

▍ Готовые части руководства:

  • Вы тут —> Часть 1
  • Часть 2
  • Часть 3
  • Часть 4
  • Часть 5
  • Часть 6
  • Часть 7

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

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

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

Производные работы и переводы этого документа должны также публиковаться под лицензией Open Software License с упоминанием оригинального источника. Если вы внесёте в книгу новый материал, то сделайте этот материал и исходный код доступными для своих ревизий. Ревизии и обновления должны быть доступны непосредственно мейнтейнеру документа, Джиму Хуангу <jserv@ccns.ncku.edu.tw>. Это позволит делать мерджи обновлений и предоставлять согласованные ревизии сообществу 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:

sudo 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 <linux/kernel.h> /* необходим для pr_info() */
#include <linux/module.h> /* необходим для всех модулей */
 
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(), чтобы безопасно выгрузить модуль.

Наконец, каждый модуль ядра должен включать <linux/module.h>. Нам нужно было включить <linux/kernel.h> только для расширения макроса уровня журнала 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 <linux/init.h> /* Необходим для макросов */
#include <linux/kernel.h> /* Необходим для pr_info() */
#include <linux/module.h> /* Необходим всем модулям */
 
static int __init hello_2_init(void)
{
    pr_info("Hello, world 2n");
    return 0;
}
 
static void __exit hello_2_exit(void)
{
    pr_info("Goodbye, world 2n");
}
 
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_FOOy или 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 <linux/init.h> /* Необходим для макросов */
#include <linux/kernel.h> /* Необходим для pr_info() */
#include <linux/module.h> /* Необходим для всех модулей */
 
static int hello3_data __initdata = 3;
 
static int __init hello_3_init(void)
{
    pr_info("Hello, world %dn", hello3_data);
    return 0;
}
 
static void __exit hello_3_exit(void)
{
    pr_info("Goodbye, world 3n");
}
 
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 <linux/init.h> /* Необходим для макросов */
#include <linux/kernel.h> /* Необходим для pr_info() */
#include <linux/module.h> /* Необходим для всех модулей */
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LKMPG");
MODULE_DESCRIPTION("A sample driver");
 
static int __init init_hello_4(void)
{
    pr_info("Hello, world 4n");
    return 0;
}
 
static void __exit cleanup_hello_4(void)
{
    pr_info("Goodbye, world 4n");
}
 
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 <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
 
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 5n=============n");
    pr_info("myshort is a short integer: %hdn", myshort);
    pr_info("myint is an integer: %dn", myint);
    pr_info("mylong is a long integer: %ldn", mylong);
    pr_info("mystring is a string: %sn", mystring);
 
    for (i = 0; i < ARRAY_SIZE(myintarray); i++)
        pr_info("myintarray[%d] = %dn", 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 5n");
}
 
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 <linux/kernel.h> /* Выполнение работы ядра. */
#include <linux/module.h> /* В частности, модуля. */
 
int init_module(void)
{
    pr_info("Hello, world - this is the kernel speakingn");
    return 0;
}
 
MODULE_LICENSE("GPL");

Второй файл:

/*
 * stop.c – пример модулей, состоящих из нескольких файлов.
 */
 
#include <linux/kernel.h> /* Выполнение работы ядра. */
#include <linux/module.h> /* В частности, модуля. */
 
void cleanup_module(void)
{
    pr_info("Short is the life of a kernel modulen");
}
 
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, поскольку в этот момент необходимые вам файлы уже готовы.

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

Продолжение

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

▍ Готовые части руководства:

  • Вы тут —> Часть 1
  • Часть 2
  • Часть 3
  • Часть 4
  • Часть 5
  • Часть 6
  • Часть 7

Мощные VPS на SSD со скидками до 53%. Панель ISPmanager в подарок*.

Модули StarLine 2CAN, 2CAN+LIN и CAN+LIN позволяют реализовать такие функции, как управление центральным замком, штатной системой охраны и функцией «комфорт», чтение статусов концевых выключателей, зажигания, функцию SLAVE, функцию бесключевого обхода, запуск двигателя и т.д. при подключении к цифровым интерфейсам автомобиля CAN.

Модули 2CAN+LIN и CAN+LIN  дополнительно поддерживают протоколы интерфейсов LIN и RX/TX.

Список поддерживаемых функций для выбранного автомобиля можно уточнить на сайте can.starline.ru

Модуль 2CAN+LIN предназначен для установки в охранные комплексы, имеющие на упаковке иконку :

  • StarLine A93
  • StarLine E65
  • StarLine E95
  • StarLine E95
  • StarLine E61.1
  • StarLine E91.1
  • StarLine E91.1
  • StarLine E63
  • StarLine E93
  • StarLine E60.1
  • StarLine E90.1
  • StarLine E90.2

Модуль CAN+LIN предназначен для установки в охранные комплексы, имеющие на упаковке иконку .

Модуль 2CAN предназначен для установки в охранные комплексы, имеющие на упаковке иконку .

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

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

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

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

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

Производные работы и переводы этого документа должны также публиковаться под лицензией Open Software License с упоминанием оригинального источника. Если вы внесёте в книгу новый материал, то сделайте этот материал и исходный код доступными для своих ревизий. Ревизии и обновления должны быть доступны непосредственно мейнтейнеру документа, Джиму Хуангу <jserv@ccns.ncku.edu.tw>. Это позволит делать мерджи обновлений и предоставлять согласованные ревизии сообществу 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 <linux/kernel.h> /* необходим для pr_info() */
#include <linux/module.h> /* необходим для всех модулей */
 
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(), чтобы безопасно выгрузить модуль.

Наконец, каждый модуль ядра должен включать <linux/module.h>. Нам нужно было включить <linux/kernel.h> только для расширения макроса уровня журнала 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 <linux/init.h> /* Необходим для макросов */
#include <linux/kernel.h> /* Необходим для pr_info() */
#include <linux/module.h> /* Необходим всем модулям */
 
static int __init hello_2_init(void)
{
    pr_info("Hello, world 2n");
    return 0;
}
 
static void __exit hello_2_exit(void)
{
    pr_info("Goodbye, world 2n");
}
 
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_FOOy или 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 <linux/init.h> /* Необходим для макросов */
#include <linux/kernel.h> /* Необходим для pr_info() */
#include <linux/module.h> /* Необходим для всех модулей */
 
static int hello3_data __initdata = 3;
 
static int __init hello_3_init(void)
{
    pr_info("Hello, world %dn", hello3_data);
    return 0;
}
 
static void __exit hello_3_exit(void)
{
    pr_info("Goodbye, world 3n");
}
 
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 <linux/init.h> /* Необходим для макросов */
#include <linux/kernel.h> /* Необходим для pr_info() */
#include <linux/module.h> /* Необходим для всех модулей */
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LKMPG");
MODULE_DESCRIPTION("A sample driver");
 
static int __init init_hello_4(void)
{
    pr_info("Hello, world 4n");
    return 0;
}
 
static void __exit cleanup_hello_4(void)
{
    pr_info("Goodbye, world 4n");
}
 
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 <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
 
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 5n=============n");
    pr_info("myshort is a short integer: %hdn", myshort);
    pr_info("myint is an integer: %dn", myint);
    pr_info("mylong is a long integer: %ldn", mylong);
    pr_info("mystring is a string: %sn", mystring);
 
    for (i = 0; i < ARRAY_SIZE(myintarray); i++)
        pr_info("myintarray[%d] = %dn", 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 5n");
}
 
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 <linux/kernel.h> /* Выполнение работы ядра. */
#include <linux/module.h> /* В частности, модуля. */
 
int init_module(void)
{
    pr_info("Hello, world - this is the kernel speakingn");
    return 0;
}
 
MODULE_LICENSE("GPL");

Второй файл:

/*
 * stop.c – пример модулей, состоящих из нескольких файлов.
 */
 
#include <linux/kernel.h> /* Выполнение работы ядра. */
#include <linux/module.h> /* В частности, модуля. */
 
void cleanup_module(void)
{
    pr_info("Short is the life of a kernel modulen");
}
 
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, поскольку в этот момент необходимые вам файлы уже готовы.

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

Продолжение

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

Автор: Дмитрий Брайт

Источник

Программирование – это процедура написания утилит и софта для разного рода консолей. Существуют различные методы и концепции соответствующего процесса. Вести разработку можно на кроссплатформенных и нативных языках.

Огромную популярность обрело СИ-семейство. Пример – C. Этот язык относительно прост и удобен для понимания. Он предусматривает так называемое модульное программирование. Далее речь зайдет о подобном способе написания софта.

Понятие

Модульное программирование – своеобразная организация утилиты (программного обеспечения) в виде связи независимых блоков, которые носят название модулей. Структура и поведение оных будут подчиняться конкретным принципам и алгоритмам.

Мо дульное программи рование – особый принцип коддинга, при котором отдельные компоненты (блоки) предельно изолированы друг от друга. Детали одного модуля не будут оказывать никакого влияния на реализацию другого. Получить подобного рода результат удастся получить посредством интерфейсов. Возможны и иные виды представления, через которые нельзя получить прямой доступ к кодификации.

Способы реализации

Модульное программирование c, как и в любом другом языке, предусматривает несколько способом реализаций. В зависимости от них будут меняться принципы применения тех или иных функций:

  1. Установка зависимости. В данной ситуации каждый элемент будет обладать собственным интерфейсом. Модули взаимодействуют посредством interfaces.
  2. Фабричный прием. Базируется на существовании некоторого объекта, который необходим для создания других. В приложение внедряется прототип, объединяющие ключевые черты для большинства функций. Параметры здесь будут наследоваться от «завода».
  3. Сервисный подход. Программист должен сделать общий единый интерфейс. Он послужит для функций и их взаимодействия своеобразным буфером.

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

Требования к модулям

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

Модуль – это своеобразная последовательность логически связанных фрагментов (функций), которые оформлены в виде самостоятельных утилитных блоков. А вот требования, которые к нему предъявляются:

  1. Должен отвечать только за одну единственную функцию. При построении модуля применяется концепция «один блок – одна функция». Отвечает за самостоятельную задачу. На входе можно передать определенных набор исходных материалов, обработать их в соответствие с техническим заданием, а потом возвратить результат обработки.
  2. Ссылка осуществляется через специализированное имя. Он должен обладать всего одним входом и выходом. Это гарантирует замкнутость «блока».
  3. Функции должны выступать в виде завершенных алгоритмов.
  4. Модуль должен передавать управление в точку первоначального вызова. Тут должна присутствовать возможность самостоятельного вызова других «обособленных функций».
  5. История вызовов не должна сохраняться. Она же не применяется при функционировании «блока кода».
  6. Логическая независимость. Результат программируемого модуля находится в зависимости от исходных материалов. От других функций утилиты – нет.
  7. Наличие слабых информационных связей с иными «блоками» утилиты. Обмен информацией производится редко. От предельно минимизирован.
  8. Модуль C должен быть относительно небольшой. Это касается и его размера, и сложности. Опытные разработчики стараются создавать не более двух страничек печатного текста.

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

Как выделить

При работе с рассматриваемым объектом в СИ требуется запомнить, что он состоит из реализации и заголовочного файла. Первый имеет документ вида .c, второй — .h.

Код, который отвечает за подключение «блока», будет на этапе компиляции нуждаться только в интерфейсе. При предпроцессинге заголовочный документ просто копируется в кодификацию директивой #include “somelib.h”

Реализация должна обязательно полностью воплощать в жизнь задуманный интерфейс. Благодаря этому она будет включать тоже свой файл заголовка. Вот пример того, как проект из одного ключевого документа и «блока» будет выглядеть в редакторе:

Это – база, которую может и должен знать каждый разработчик. Программу при помощи рассмотренной концепции создать не составит никакого труда. Особенно если заранее продумать ее структуру и логику.

На что обратить внимание

Приведенный пример предусматривает следующие особенности:

  1. В документе «мейн.с» не нужно подключать «стдио.h». Это несмотря на то, что он применяется в hello.c.
  2. Данный момент связан с тем, что никакие типы из stdio не требуются для того, чтобы обрабатывать интерфейс hello.h, который оказывается в «мейне» в процессе компилирования.
  3. Если бы требовалось для обработки interfaces «блоков» брать определения из той или иной библиотеки, последние должны прописывается не в hello.c, а в hello.h. Данный примем обеспечивает отсутствие ошибок в тех пространствах, где подключается hello.

Это – то, что нужно помнить о модульном программировании C. Информация поможет начать более глубокое изучение концепции.

Лучшее решение для быстрого понимания темы

Для того, чтобы лучше разбираться в функциях и «изолированности кода» в СИ-семействе рекомендуется закончить специализированные онлайн курсы. Они могут быть пройдены в любое время, когда удобно пользователю.

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

Интересует разработка на C? Обратите внимание на курс «Программист C» в Otus.

Контроллеры ALPHA XL

Ознакомительная брошюра

Руководство по эксплуатации

Руководство по программированию

Руководство. Средства связи

Моноблочные контроллеры серии FX (FX1S, FX1N, FX3G, FX3U)

Ознакомительная брошюра

Руководство для программиста

Сертификат ГОСТ Р

Пособие начинающему

Коммуникационные модули ПЛК FX

Моноблочные контроллеры FX3U

Ознакомительная брошюра

Руководство по эксплуатации

Руководство по анал/модулям

Моноблочные контроллеры нового поколения FX5U

Ознакомительная брошюра

Руководство по установке контроллера FX5U

Модули расширения серии FX5U для автоматизации- информация по установке модулей входов/выходов

Руководство по установке модуля конвертора шины FX5-CNV-BUS

Руководство по установке расширительного модуля питания FX5-1PSU-5V

Руководство по установке специальных коммуникационных адаптеров FX5-232ADP и FX5-485ADP

Моноблочные контроллеры FX3G

Ознакомительная брошюра

Руководство по эксплуатации

Руководство по анал/модулям

GX Developer — курс обучения

iQ Works Mitsubishi- краткий курс по программированию

GX IEC Developer- курс обучения

Модульные контроллеры для профессионалов System Q

Руководство для начинающего

Ознакомительная брошюра

Каталог ПЛК семейства Q/L Mitsubishi

Руководство для начинающего

Пособие начинающему программисту

Графические панели управления GT1020 и GT1030

Обзор GT1020 и GT1030 брошюра

Руководство по эксплуатации

Графические панели оператора GT1040 и GT1050

Обзор GT1040 и GT1050 брошюра

Руководство по эксплуатации

Графические панели оператора GT1150 и GT1155

Как подключить gt11 к плк FX

Как подключить gt11 к FR-F740

Руководство по эксплуатации

Графические панели управления GT1265 и GT1275 c Ethernet

Обзор GT12 Mitsubishi брошюра

Панели оператора

GOT2000 — следующий уровень сенсорных графических панелей оператора

GT14 — новый стандарт для 5,7″ панелей с сенсорным экраном

Обзор всех панелей оператора Mitsubishi

Пособие начинающему

Программирование панелей оператора Мицубиси GOT

GT Designer для начинающих

GT Designer 3 пособие

iQ_Works — пособие начинающему

Понравилась статья? Поделить с друзьями:
  • Medion md 42780 инструкция на русском
  • Мануалы по ремонту форсунок
  • Таурин в таблетках инструкция по применению цена отзывы аналоги
  • Ципрофлоксацин инструкция по применению в ветеринарии для кошек
  • Омепразол акрихин инструкция в капсулах для чего полезен женщинам