|
|||||||
4. Hello World
Время создания: 20.09.2022 09:44
Текстовые метки: linux, ядро, модуль, программирование, язык, C, Си, пособие, документация
Раздел: Компьютер - Linux - Ядро - Пособие по программированию модулей ядра Linux
Запись: xintrea/mytetra_syncro/master/base/16636562492jizz157ln/text.html на raw.github.com
|
|||||||
|
|||||||
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 Теперь вам известны основы создания, компиляции, установки и удаления модулей. Далее мы подробнее разберём, как они работают. Модули ядра должны иметь не менее двух функций:
В действительности же с версии 2.3.13 произошли кое-какие изменения. Теперь стартовую и завершающую функцию модулей можно называть на своё усмотрение, и об этом будет подробнее сказано в разделе 4.2. На деле этот новый метод даже предпочтительней, хотя многие по прежнему используют названия init_module() и cleanup_module(). Как правило, init_module() или регистрирует обработчик чего-либо с помощью ядра, или заменяет одну из функций ядра собственным кодом (обычно кодом, который выполняет определённые действия и вызывает исходную функцию). Что касается cleanup_module(), то она должна отменять всё, что сделала init_module(), чтобы безопасно выгрузить модуль. Наконец, каждый модуль ядра должен включать <linux/module.h>. Нам нужно было включить <linux/kernel.h> только для расширения макроса уровня журнала pr_alert(), о чём подробнее сказано в пункте 2.
Дополнительные подробности о 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 2\n"); return 0; }
static void __exit hello_2_exit(void) { pr_info("Goodbye, world 2\n"); }
module_init(hello_2_init); module_exit(hello_2_exit);
MODULE_LICENSE("GPL"); Теперь у нас есть уже два реальных модуля ядра. Добавить ещё один будет совсем несложно: obj-m += hello-1.o obj-m += hello-2.o
PWD := $(CURDIR)
all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean Загляните в drivers/char/Makefile, чтобы увидеть реальный пример. Как видите, некоторые элементы включаются в ядро жёстко (obj-y), но куда делись все obj-m? Те, кто знаком со скриптами оболочки, смогут без проблем их обнаружить. Для остальных подскажу, что записи obj-$(CONFIG_FOO), которые вы видите повсюду, расширяются на obj-y или obj-m в зависимости от того, на какое значение была установлена переменная CONFIG_FOO — y или m. Попутно отмечу, что именно эти переменные вы установили в файле .config в каталоге верхнего уровня дерева исходного кода в последний раз, когда выполнили make menuconfig или что-то в том духе. ▍ 4.3 Макросы __init и __exitМакрос __init приводит к отбрасыванию функции инициализации и освобождению занятой ей памяти по завершении её выполнения для встроенных драйверов, но не загружаемых модулей. И это вполне разумно, если учесть, когда эта функция вызывается. Также есть __initdata, которая работает аналогично __init, но для переменных инициализации, а не для функций. Макрос __exit приводит к пропуску функции, если модуль встроен в ядро, то есть аналогично __init не влияет на загружаемые модули. Опять же, если учесть, когда выполняется функция очистки, то это полностью оправданно. Встроенным драйверам не требуется очистка, а вот загружаемым модулям как раз да. Эти макросы определены в include/linux/init.h и используются для освобождения памяти ядра. Если при его загрузке вы видите сообщение вроде Freeing unused kernel memory: 236k freed, то знайте – это тот самый процесс. /* * hello-3.c – демонстрация макросов __init, __initdata и __exit. */ #include <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 %d\n", hello3_data); return 0; }
static void __exit hello_3_exit(void) { pr_info("Goodbye, world 3\n"); }
module_init(hello_3_init); module_exit(hello_3_exit);
MODULE_LICENSE("GPL"); ▍ 4.4 Лицензирование и документирование модулейДаже не знаю, кто вообще загружает или вообще задумывается об использовании проприетарных модулей? Если вы из числа таких людей, то наверняка видели нечто подобное: $ sudo insmod xxxxxx.ko loading out-of-tree module taints kernel. module license 'unspecified' taints kernel. Для обозначения лицензии вашего модуля вы можете использовать ряд макросов, например: «GPL», «GPL v2», «GPL and additional rights», «Dual BSD/GPL», «Dual MIT/GPL», «Dual MPL/GPL» и «Proprietary». Определены они в include/linux/module.h. Для указания используемой лицензии существует макрос MODULE_LICENSE. Он и ещё пара макросов, описывающих модуль, приведены в примере ниже. /* * hello-4.c – Демонстрирует документирование модуля. */ #include <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 4\n"); return 0; }
static void __exit cleanup_hello_4(void) { pr_info("Goodbye, world 4\n"); }
module_init(init_hello_4); module_exit(cleanup_hello_4); ▍ 4.5 Передача в модуль аргументов командной строкиМодулям можно передавать аргументы командной строки, но не через argc/argv, к которым вы, возможно, привыкли. Чтобы получить такую возможность, нужно объявить переменные, которые будут принимать значения аргументов командной строки как глобальные и затем использовать макрос module_param() (определяемый в include/linux/moduleparam.h) для настройки этого механизма. Во время выполнения insmod будет заполнять эти переменные получаемыми аргументами, например, insmod mymodule.ko myvariable=5. Для большей ясности объявления переменных и макросов необходимо размещать в начале модулей. Более наглядно всё это продемонстрировано в примере кода. Макрос module_param() получает 3 аргумента: имя переменной, её тип и разрешения для соответствующего файла в sysfs. Целочисленные типы могут быть знаковыми, как обычно, или беззнаковыми. Если вы хотите использовать массивы целых чисел или строк, к вашим услугам module_param_array() и module_param_string(). int myint = 3; module_param(myint, int, 0); Массивы тоже поддерживаются, но в современных версиях работает это несколько иначе, нежели раньше. Для отслеживания количества параметров необходимо передать указатель на число переменных в качестве третьего аргумента. При желании вы можете вообще проигнорировать подсчёт и передать NULL. Вот пример обоих вариантов: int myintarray[2]; module_param_array(myintarray, int, NULL, 0); /* если подсчёт не интересует */
short myshortarray[4]; int count; module_param_array(myshortarray, short, &count, 0); /* подсчёт происходит в переменной "count" */ Хорошим применением для этого варианта будет предустановка значений переменных модуля, таких как порт или адрес ввода-вывода. Если переменные содержат предустановленные значения, выполнять автообнаружение. В противном случае оставлять текущее значение. Позже об этом будет сказано подробнее. Наконец, есть ещё макрос MODULE_PARM_DESC(), используемый для документирования аргументов, которые может принять модуль. Он получает два параметра: имя переменной и строку в свободной форме, эту переменную описывающую. Пример передачи аргументов командной строки в модуль /* * hello-5.c – демонстрирует передачу аргументов командной строки в модуль. */ #include <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 5\n=============\n"); pr_info("myshort is a short integer: %hd\n", myshort); pr_info("myint is an integer: %d\n", myint); pr_info("mylong is a long integer: %ld\n", mylong); pr_info("mystring is a string: %s\n", mystring);
for (i = 0; i < ARRAY_SIZE(myintarray); i++) pr_info("myintarray[%d] = %d\n", i, myintarray[i]);
pr_info("got %d arguments for myintarray.\n", arr_argc); return 0; }
static void __exit hello_5_exit(void) { pr_info("Goodbye, world 5\n"); }
module_init(hello_5_init); module_exit(hello_5_exit); Рекомендую поэкспериментировать со следующим кодом: $ sudo insmod hello-5.ko mystring="bebop" myintarray=-1 $ sudo dmesg -t | tail -7 myshort is a short integer: 1 myint is an integer: 420 mylong is a long integer: 9999 mystring is a string: bebop myintarray[0] = -1 myintarray[1] = 420 got 1 arguments for myintarray. $ sudo rmmod hello-5 $ sudo dmesg -t | tail -1 Goodbye, world 5 $ sudo insmod hello-5.ko mystring="supercalifragilisticexpialidocious" myintarray=-1,-1 $ sudo dmesg -t | tail -7 myshort is a short integer: 1 myint is an integer: 420 mylong is a long integer: 9999 mystring is a string: supercalifragilisticexpialidocious myintarray[0] = -1 myintarray[1] = -1 got 2 arguments for myintarray. $ sudo rmmod hello-5 $ sudo dmesg -t | tail -1 Goodbye, world 5 $ sudo insmod hello-5.ko mylong=hello insmod: ERROR: could not insert module hello-5.ko: Invalid parameters ▍ 4.6 Модули, состоящие из нескольких файловИногда есть смысл поделить модуль на несколько файлов. Вот пример такого модуля: /* * start.c – пример модулей, состоящих из нескольких файлов. */
#include <linux/kernel.h> /* Выполнение работы ядра. */ #include <linux/module.h> /* В частности, модуля. */
int init_module(void) { pr_info("Hello, world - this is the kernel speaking\n"); 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 module\n"); }
MODULE_LICENSE("GPL"); И, наконец, Makefile: obj-m += hello-1.o obj-m += hello-2.o obj-m += hello-3.o obj-m += hello-4.o obj-m += hello-5.o obj-m += startstop.o startstop-objs := start.o stop.o
PWD := $(CURDIR)
all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean Это полный Makefile для всех примеров, которые мы успели рассмотреть. Первые пять строчек не представляют ничего особенного, но для последнего примера нам потребуется две строки. В первой мы придумываем имя объекта для нашего комбинированного модуля, а во второй сообщаем make, какие объектные файлы являются его частью. ▍ 4.7 Сборка модулей для скомпилированного ядраЕстественно, мы настоятельно рекомендуем вам перекомпилировать ядро, чтобы иметь возможность активировать ряд полезных функций отладки, таких как принудительная выгрузка модулей ( MODULE_FORCE_UNLOAD ): когда эта опция включена, можно с помощью команды sudo rmmod -f module принудить ядро выгрузить модуль, даже если оно сочтёт это небезопасным. В процессе разработки модуля эта опция может сэкономить вам много времени и избавить от лишних перезагрузок. Если вы не хотите перекомпилировать ядро, то рассмотрите вариант выполнения примеров внутри тестового дистрибутива на виртуальной машине. В таком случае при нарушении работоспособности вы сможете легко перезагрузиться или восстановить VM. Существует ряд случаев, в которых вам может потребоваться загрузить модуль в уже скомпилированное работающее ядро. Как вариант, это может быть типичный дистрибутив Linux или ядро, которое вы сами скомпилировали ранее. Бывает, что загрузить модуль нужно в работающее ядро, перекомпилировать которое нет возможности, или на машину, перезагружать которую нежелательно. Если вам сложно представить случай, который может вынудить вас использовать модули для уже скомпилированного ядра, то просто пропустите этот раздел и расценивайте оставшуюся часть главы как большое примечание. Итак, если вы просто установите дерево исходного кода, используете его для компиляции модуля и попытаетесь внедрить этот модуль в ядро, то в большинстве случаев получите ошибку: insmod: ERROR: could not insert module poet.ko: Invalid module format Более понятная информация логируется в системный журнал: kernel: poet: disagrees about version of symbol module_layout Иными словами, ваше ядро отказывается принимать модуль, потому что строки версии (точнее, vermagic, см. include/linux/vermagic.h) не совпадают. К слову, строки версии хранятся в объекте модуля в виде статической строки, начинающейся с vermagic:. Данные версии вставляются в модуль, когда он линкуется с файлом kernel/module.o. Для просмотра сигнатуры версии и прочих строк, хранящихся в конкретном модуле, выполните команду modinfo module.ko: $ modinfo hello-4.ko description: A sample driver author: LKMPG license: GPL srcversion: B2AA7FBFCC2C39AED665382 depends: retpoline: Y name: hello_4 vermagic: 5.4.0-70-generic SMP mod_unload modversions Для преодоления этой проблемы можно задействовать опцию --force-vermagic, но такое решение не гарантирует безопасность и однозначно будет неприемлемым в создании модулей. Следовательно, модуль нужно скомпилировать в среде, которая была идентична той, где создано наше скомпилированное ядро. Этому и будет посвящён остаток текущей главы. Во-первых, убедитесь, что дерево исходного кода ядра вам доступно и имеет одинаковую версию с вашим текущим ядром. Далее найдите файл конфигурации, который использовался для компиляции ядра. Обычно он доступен в текущем каталоге boot под именем вроде config-5.14.x. Его будет достаточно скопировать в дерево исходного кода вашего ядра: cp /boot/config-`uname -r` .config Далее мы ещё раз сосредоточимся на предыдущем сообщении об ошибке: более пристальное рассмотрение строк версий говорит о том, что даже в случае двух абсолютно одинаковых файлов конфигурации небольшое отличие в версии всё же возможно, и будет достаточно исключить внедрение модуля в ядро. Это небольшое отличие, а именно пользовательская строка, которая присутствует в версии модуля, но отсутствует в версии ядра, вызвано изменением относительно оригинала в Makefile, который содержат некоторые дистрибутивы. Далее вам нужно просмотреть собственный Makefile и обеспечить, чтобы представленная информация версии в точности соответствовала той, что указана в текущем ядре. Например, ваш Makefile может начинаться так: VERSION = 5 PATCHLEVEL = 14 SUBLEVEL = 0 EXTRAVERSION = -rc2 В этом случае необходимо восстановить значение символа EXTRAVERSION на -rc2. Мы рекомендуем держать резервную копию Makefile, используемого для компиляции ядра, в /lib/modules/5.14.0-rc2/build. Для этого будет достаточно выполнить: cp /lib/modules/`uname -r`/build/Makefile linux-`uname -r` Здесь linux-`uname -r` — это исходный код ядра, которое вы собираетесь собрать. Теперь выполните make для обновления конфигурации вместе с заголовками версии и объектами: $ make SYNC include/config/auto.conf.cmd HOSTCC scripts/basic/fixdep HOSTCC scripts/kconfig/conf.o HOSTCC scripts/kconfig/confdata.o HOSTCC scripts/kconfig/expr.o LEX scripts/kconfig/lexer.lex.c YACC scripts/kconfig/parser.tab.[ch] HOSTCC scripts/kconfig/preprocess.o HOSTCC scripts/kconfig/symbol.o HOSTCC scripts/kconfig/util.o HOSTCC scripts/kconfig/lexer.lex.o HOSTCC scripts/kconfig/parser.tab.o HOSTLD scripts/kconfig/conf Если же вы не хотите фактически компилировать ядро, то можете прервать процесс сборки (CTRL-C) сразу же после строки SPLIT, поскольку в этот момент необходимые вам файлы уже готовы. Теперь можно вернуться в каталог модуля и скомпилировать его: он будет собран в точном соответствии с настройками текущего ядра и загрузится в него без каких-либо ошибок. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|