2.4. Компиляция и загрузка
Пример "Привет, мир" с которого начинается эта глава небольшая демонстрация построения модуля и загрузки его в систему. Конечно весь процесс на много больше чем вы сумели увидеть. Этот раздел предоставляет больше подробностей как авторы модулей включают исходный код в выполняемую подсистему в ядре.
2.4.1. Компиляция модулей
Для начала, давайте немного посмотрим на то, как должны собираться модули. Процесс сборки модуля сильно отличается от сборки пользовательского приложения. Ядро -- это большая отдельная программа с детальными и явными требованиями как его части собираются вместе. Процесс сборки также отличается от того, что был принят в предыдущих версиях ядра. Новая система сборки проще в использовании, даёт более корректный результат, но сильно отличается того, что было раньше. Система сборки ядра сложна, и мы рассмотрим только небольшой кусочек её. Файлы, расположенные в каталоге Documentation/kbuild дерева исходных кодов ядра, обязательны для прочтения всем, кто хочет понять как всё происходит на самом деле.
Существует несколько предварительных требований, которые необходимо выполнить, прежде чем заниматься построением модулей ядра. Во-первых, вы должны быть уверены, что используете текущие версии компилятора, утилит модулей и другого необходимого инструментария. Файл Documentation/Changes в каталоге документации ядра всегда содержит список версий инструментария; вы должны свериться с ним прежде чем двигаться дальше.
Попытки собрать ядро (и его модули) при помощи инструментария неправильных версий могут в конечном счете привести к тяжелым проблемам. Помните, что компилятор одной из последних версий может вызвать так же много проблем, как и устаревший компилятор; исходные коды ядра содержат очень большое количество допущений о компиляторе и в новой версии может отсутствовать что-нибудь нужное.
Если у вас до сих пор нет под рукой дерева ядра или оно не скофигурировано и не собрано, сейчас самое время сделать это. Вы не сможете собрать загружаемые модули ядра версии 2.6 при отсутствии этого дерева в вашей системе. Так же полезно (но не необходимо), чтобы при работе было загружено ядро, для которого вы что-то собираете.
Теперь, когда все настроено, создать makefile для вашего модуля очень легко. Фактически для примера "Привет мир", описанного ранее в этой главе, в этот файл достаточно включить одну строчку:
obj-m := hello.o
Читатели, хорошо знакомые с make и не очень хорошо с системой сборки ядра 2.6 скорее всего удивятся, как такой makefile может работать, ведь строка, приведённая выше, не похожа на то, как выглядит обычный makrfile. Ответ, конечно, заключается в том, что за всё остальное отвечает система сборки ядра. Показанное присваивание, использующее расширенный синтаксис GNU make, указывает, что должен быть собран один модуль из объектного файла hello.o. После сборки результирующий файл будет называться hello.ko.
Если у вас есть модуль module.ko, который сгенерирован из двух исходных файлов (названных, скажем, file2.c и file3.c), правильными будут следующие строки:
obj-m := module.o
module-objs := file2.o file3.o
Для того, чтобы описанный выше makefile сработал, он должен быть вызван внутри контекста большой сборки ядра системы. Если дерево исходных файлов ядра расположено, скажем, в каталоге ~/kernel-2.6, команда make, необходимая для сборки модуля (вводится в каталоге, содержащем исходные файлы модуля и makefile), будет выглядеть так:
make -C ~/kernel-2.6 M=`pwd` modules
Эта команда начинается с замены её текущего каталога на каталог, указанный в опции -C (то есть на каталог исходных файлов ядра). Там можно найти makefile ядра более высокого уровня. Опция M= означает, что makefile вернется назад в каталог исходников модуля перед попыткой собрать конечный модуль. Этот конечный модуль, в свою очередь, ссылается на список модулей, найденных в переменной obj-m, которую мы установили равным module.o в наших примерах.
Каждый раз набирать предыдущую команду make может стать утомительным, поэтому разработчики ядра придумали нечто вроде диалекта makefile, который помогает облегчить жизнь тем, кто собирает модули вне дерева ядра. Хитрость заключается в том, чтобы написать свой makefile, примерно такой:
# Если KERNELRELEASE определен, вызов был произведен из
# из системы сборки ядра и мы можем пользоваться её языком.
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# Иначе, вызов был из командной строки;
# вызываем систему сборки.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
Давайте ещё раз посмотрим на расширенный синтаксис утилиты GNU make в действии. Этот makefile читается дважды при типичной сборке. Помните, что когда makefile вызывается из командной строки переменная KERNELRELEASE не установлена. Makefile находит каталог исходников ядра, воспользовавшись тем, что символическая ссыка, построенная в каталоге установленных модулей ядра, указывает обратно на дерево сборки ядра. Если у вас запущено не то ядро, для которого вы собираете модуль, вы можете воспользоваться опцией KERNELDIR= командной строки, установить значение переменной окружения KERNELDIR или переписать строку в makefile, где устанавливается KERNELDIR. После того как найдено дерево исходных файлов ядра, makefile делает вызов по умолчанию: второй раз запускается команда make (указанная в makefile в параметре $(MAKE)) для вызова сбоорки ядра системы, как это описано выше.
Этот механизм создания модулей может показаться вам немного громоздким и неясным. Однако, как только вы привыкните к нему, вы скорее всего оцените те возможности, которые были заложены в процесс сборки ядра системы. Обратите внимание, что приведенный выше makefile, не является полным, реальный makefile обычно включает в себя инструкции для очистки ненужных файлов, ссылки на устанавливаемые модули и т.п. Смотрите makefile-ы в каталоге примеров исходных кодов для получения представления о полноценных makefile-ах.
2.4.2. Загрузка и выгрузка модулей
Следующий шаг после сборки модуля - это загрузка его в ядро. Как мы уже говорили, эту работу выполняет insmod. Программа загружает код и данные модуля в ядро, которое в свою очередь выполняет функцию, аналогичную ld, оно связывает неразрешенные символы модуля с таблицей символов ядра. Однако, в отличие от linker, ядро не изменяет файл модуля на диске, и даже его копию в оперативной памяти. insmod допускает определенное количество параметров командной строки (подробнее см. руководство man) и может присвоить значения параметрам вашего модуля перед привязкой его к текущему ядру. Таким образом, если модуль корректно разработан, он может быть сконфигурирован во время загрузки; конфигурирование во время загрузки дает пользователю большую гибкость, чем конфигурирование во время компиляции, которое до сих пор иногда еще используется. Конфигурирование во время загрузки описано в разделе 2.8 этой главы.
Заинтересованные читатели возможно захотят посмотреть, как ядро поддерживает insmod: оно полагается на системные вызовы описанные в kernel/module.c. Функция sys_init_module выделяет память ядра для загрузки модуля (эта память выделяется при помощи функции vmalloc; см. раздел 8.4 в главе 2); затем текст модуля копируется в отведенную область памяти, происходит разрешение ссылок модуля на ядро при помощи таблицы символов ядра и вызывается функция инициализации модуля для получения всего необходимого для дальнейшей работы.
Если вы когда-нибудь посмотрите исходные коды ядра, вы увидите, что имена системных вызовов начинаются с префикса sys_. Это справедливо для всех системных вызовов и никогда не используется для других функций. Это полезно помнить при поиске системных вызовов в исходных кодах.
Стоит отметить быстроту отклика утилиты modprobe. modprobe, так же как и insmod, загружает модуль в ядро. Она отличается тем, что ищет в загружаемом модуле символы, которые не определены в настоящее время в ядре. Если имеется хотя бы одна такая ссылка, modprobe ищет другие модули, в которых соответствующие символы определены. Когда modprobe находит такие модули (они необходимы для загрузки текущего модуля), она тоже грузит их в ядро. В подобной ситуации insmod напротив прерывает свою работу с выдачей сообщения "неразрешенные ссылки" в системный журнал.
Как упоминалось ранее, модули могут быть удалены из ядра при помощи утилиты rmmod. Помните, что модуль будет удален с ошибкой, если ядро полагает, что модуль все еще используется (например, программа все еще держит открытым файл устройства экспортируемого через модули), или ядро было сконфигурировано с запретом удаления модуля. Возможно сконфигурировать ядро с возможностью "принудительного" удаления модулей, даже когда они заняты работой. Если вы достигли точки, когда собираетесь ипользовать эту возможность, нужно быть уверенным что ситуация лучше той, когда перезагрузка будет наилучшим выходом из положения.
Программа lsmod предоставляет список модулей, загруженных в ядро в текущий момент времени. Также в ней предусмотрена выдача некоторой другой информации, такой как указание различных модулей, использующих конкретный модуль. lsmod получает информацию посредством чтения виртуального файла /proc/modules. Информацию о загруженных в данный момент модулях можно также найти в виртуальной файловой системе sysfs в /sys/module.
2.4.3. Зависимости версий
Имейте в виду, что код вашего модуля должен быть скомпилирован для каждой версии ядра, с которой он будет использоваться. В каждом модуле определяется символ __module_kernel_version, который insmod проверяет на совпадение с номером версии текущего ядра. Этот символ находится в .modinfo в секции Executable Linking and Format (ELF), которая детально описана в главе 11. Пожалуйста, имейте в виду, что сказанное выше годится только для версий ядра 2.2 и 2.4, в Линуксе 2.0 это работало по-другому.
Ядро не просто допускают что полученные модули были собраны непосредственно подходящей версией ядра. Один из шагов процесса сборки это связывание вашего модуля непосредственно с файлом (который называется vermagic.o) в текущем каталоге дерева ядра, этот объект содержит приличное количество информации о ядре в котором был собран модуль, включая целевую версию ядра, версию компилятора и установки числовых значений важных конфигурационных переменных. При попытке загрузить модуль эта информация может быть проверенна на совместимость с запущенным ядром. Если что-то происходит не так, то модуль не загружается и вы увидите что-то похожее:
# insmod hello.ko
Error inserting './hello.ko': -1 Invalid module format
Просмотр файла системного лога (/var/log/messages или то, что его заменяет в системе) покажет действительную проблему отказа загрузки модуля.
Если вам необходимо откомпилировать модуль для определенной версии ядра, вы должны будете использовать систему построения и дерево исходных кодов именно для этой конкретной версии. Ловким выходом в этом случае будет простое изменение переменной KERNELDIR в makefile, как это показано в примере ранее.
Интерфейс ядра часто меняется от версии к версии. Если вы пишите модуль который рассчитан работать с разными версиями ядра (особенно если он должен работать в основных выпусках), вы вероятно сделаете макрос и используете конструкцию #ifdef, для того чтобы собрать ядро должным образом. Это издание книги имеет отношение только к одной основной версии ядра, так что вы не часто увидите проверки версии в кодах наших примеров. Но необходимость в таких проверках иногда возникает. В этих случаях вы возможно захотите использовать определения, содержащиеся в файле linux/version.h. Этот заголовочный файл автоматически подключается через linux/module.h, определяя следующий макрос:
UTS_RELEASE
Этот макрос разворачивается в строку, описывающую версию этого дерева ядра, например, "2.6.10".
LINUX_VERSION_CODE
Этот макрос раскрывается в двоичное представление версии ядра, по одному байту на каждую часть номера версии. Например, код для 2.6.10 будет 132618 (т.е. 0x02060a).[2] Используя эту информацию, вы можете (почти) легко определить с какой версией ядра вы имеете дело.
[2] Это позволяет иметь до 256 промежуточных версий между стабильными версиями.
KERNEL_VERSION(major,minor,release)
Этот макрос используется для построения целочисленного кода версии из отдельных чисел, составляющих номер версии. Например,
KERNEL_VERSION(2,6,10)
разворачивается в 132618. Макрос очень полезен, если требуется сравнение текущей и известной заранее версии ядра.Большинство зависимостей от версии ядра можно обойти с помощью условий препроцессора, используя KERNEL_VERSION и LINUX_VERSION_CODE. Однако, зависимости от версий не должны замусоривать код кучей #ifdef. Лучший способ борьбы с несовместимостями -- выделить всё необходимое в отдельный заголовочный файл. Общее правило в данном случае сводится к тому, что код, который явно зависит от версии или платформы должен быть спрятан за низкоуровневыми макросами или функциями. Тогда высокоуровневый код может просто вызывать эти функции не заботясь о низкоуровневых деталях. Код, написанный таким образом становится проще и лучше читается.
2.4.4. Платформенные зависимости
Каждая компьютерная платформа имеет свои особенности, и дизайнеры ядра свободны использовать все эти особенности для достижения максимальной производительности в целевом объектном файле.
В отличии от разработчиков приложений, которые должны связывать свой код с скомпилированными библиотеками и придерживаться соглашений прохождения параметров, разработчики ядра могут предназначать некоторым регистрам процессора особую роль что они и делают. Более того, код ядра может быть оптимизирован для отдельного процессора в семействе ЦПУ используя лучшее в платформе с которой работают: в отличии от приложений которые часто поставляются в скомпилированном формате, специальная компиляция ядра может быть оптимизирована для определенного типа компьютеров.
Например, архитектура IA32 (x86) разделяется на несколько различных типов процессоров. Старый процессор 80386 всё ещё поддерживается (пока), хотя его набор инструкций по сегодняшним меркам весьма ограничен. Более современные процессоры в своей архитектуре имеют ряд новых возможностей, включающих более быстрые инструкции для входа в ядро, межпроцессорных блокировок, копирования данных и т.д. Более новые процессоры также могут, работая в нужном режиме, использовать 36-битные (или большие) физические адреса, что позволяет им адресовать более 4GB физической памяти. Другие семейства процессоров имеют аналогичные улучшения. Ядро, в зависимости от разных опций конфигурации, может быть собрано с использованием этих дополнительных возможностей.
Совершенно ясно, что если модуль работает с данным ядром, он должен быть собран с таким же пониманием целевого процессора, как и ядро, работающее на нем. Тут опять вступает в игру объектный файл vermagic.o. Когда модуль загружается, для него проверяются параметры конфигурации конкретного процессора на предмет соответствия запущенному ядру. Если модуль был скомпилирован с другими параметрами, он не будет загружен.
Если вы планируете писать драйвер для основного дистрибутива, вам может стать интересно, как же обеспечить поддержку всех этих различных вариантов. Возможно, лучший ответ на это - выпустить ваш драйвер под GPL-совместимой лицензией и включить его в состав ядра. В противном случае лучшим ответом будет распространение драйвера в виде исходного кода и скриптов для компиляции кода на компьютере пользователя. Некоторые продавцы программного обеспечения выпустили инструменты, облегчающие подобную задачу. Если вы должны распространять ваш драйвер в бинарном виде, вы должны следить за измерениями сопровождаемого ядра ваших целевых дистрибутивов и обеспечивать версию модуля для каждого из них. Не забудьте принять во внимание все исправления ядра, которые произошли со времени последнего распространения дистрибутива. Кроме того, в этом случае должны быть рассмотрены вопросы лицензирования, как мы это обсуждали в разделе 1.6. Как правило, распространение программной вещицы в виде исходных кодов - это более простой способ найти свой путь в мире.
Переведено на сайте www.notabenoid.com
http://notabenoid.com/book/11832/38204
Внимание! Этот перевод, возможно, ещё не готов,
так как модераторы установили для него статус
"перевод редактируется"
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.