Меню Рубрики

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

Пишем простой модуль ядра Linux

Захват Золотого Кольца-0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Начинаем

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

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

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

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

  • Мы устанавливаем init (загрузка) и exit (выгрузка) как статические функции, которые возвращают целые числа.
  • Обратите внимание на использование printk вместо printf . Также параметры printk отличаются от printf . Например, флаг KERN_INFO для объявления приоритета журналирования для конкретной строки указывается без запятой. Ядро разбирается с этими вещами внутри функции printk для экономии памяти стека.
  • В конце файла можно вызвать module_init и module_exit и указать функции загрузки и выгрузки. Это даёт возможность произвольного именования функций.
  • Впрочем, пока мы не можем скомпилировать этот файл. Нужен Makefile. Такого базового примера пока достаточно. Обратите внимание, что make очень привередлив к пробелам и табам, так что убедитесь, что используете табы вместо пробелов где положено.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Теперь наш пример делает нечто большее, чем просто вывод сообщения при загрузке и выгрузке, так что понадобится менее строгая процедура тестирования. Изменим Makefile только для загрузки модуля, без его выгрузки.

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

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

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

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

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

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

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

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

    Заключение

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

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

    Источник

    Часть 1. Первые шаги

    Серия контента:

    Этот контент является частью # из серии # статей: Разработка модулей ядра Linux

    Этот контент является частью серии: Разработка модулей ядра Linux

    Следите за выходом новых статей этой серии.

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

    • детали внутреннего устройства и функционирования ядра Linux, а также выявляющиеся в нём проблемы. Эти вопросы относятся к сфере компетенции команды разработчиков ядра, возглавляемой Линусом Торвальдсом.
    • утилиты и библиотеки, поставляемые в составе Linux. Разработчику достаточно уметь использовать эти инструменты при построении модулей ядра, но более глубокие знания — это уже прерогатива сообществ GNU, FSF и разработчиков независимых проектов.
    • вопросы интеграции создаваемого модуля в дерево исходных кодов Linux или любого его дистрибутива. Эти вопросы должны решаться системотехниками или внедренцами, берущими на себя ответственность за дальнейшую судьбу разрабатываемого проекта. Поэтому демонстрируемые примеры модулей будут создаваться не в дереве исходных кодов системы, а в отдельных каталогах целевых проектов.

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

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

    Создание первого модуля ядра

    Лучший способ научиться плавать — начать плавать. Поэтому, вместо скучных обсуждений терминологии, систематизации и архитектуры цикл начинается сразу с написания кода модулей ядра. Такой код будет интуитивно понятен любому программисту без особых пояснений. Хотя в дальнейшем, конечно, обязательно придётся вернуться к обсуждению скучных вещей в объёме, необходимом для достижения поставленной цели. Обычно любую иллюстрацию из мира программирования начинают с примера «Hello world!». На самом деле, «Hello world!» модуль — это не самый лучший пример, так как все пишущие о ядре авторы используют именно его. Вместо этого лучше создать несколько простейших модулей, в конечном итоге имеющих суммарно такую же потребительскую ценность, как и «Hello world!», но при этом сразу иллюстрирующих гораздо больше понятий из мира модулей ядра и открывающих дорогу для дальнейших экспериментов. В листинге 1 приведен код вызываемого модуля (файл md1.c), а в листинге 2 — код вызывающего модуля (файл md2.c)

    Листинг 1. Вызываемый модуль ядра Linux
    Листинг 2. Вызывающий модуль ядра Linux

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

    Как и для сборки любого проекта, в данном случае понадобится файл сценария сборки с именем Makefile, приведенный в листинге 3. Опытные пользователи легко поймут, что в нем собираются 3 независимых модуля.

    Листинг 3. Типовой сценарий сборки

    Примечание. Согласно синтаксическим правилам утилиты make , каждая строка, имеющая отступ в листинге 3 и не начинающаяся с первого символа строки, должна начинаться с одного или нескольких символов TAB, но ни в коем случае не с пробелов.

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

    Листинг 4. Результат успешной сборки модуля

    В листинге 4 приведен пример успешной сборки модуля (предупреждение касательно строки 14 файла md1.c не существенно), результаты сборок других модулей должны выглядеть аналогично. На этом вся работа по программированию и сборке завершена, и можно переходить к дальнейшим экспериментам и наблюдениям. Все ключевые понятия и термины, возникающие в ходе этих экспериментов, имеющие существенное значение для понимания природы и техники модулей, буду выделяться таким шрифтом.

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

    Это файлы объектного формата компилятора gcc, расширенные некоторыми дополнительными символами (в терминологии объектных модулей):

    Попробуем инсталлировать (загрузить) один из новых модулей в системе:

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

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

    Всё прошло нормально, но в разработанных модулях присутствовали вызовы функции printk() , до сих пор не рассматривавшейся, но, по аналогии с printf() она должна была выводить текстовые сообщения по ходу загрузки модулей. Всё это так, но в пространстве пользователя, при запуске приложения и вызове printf() вывод осуществляется на управляющий терминал, а таким терминалом является текстовая консоль или приложение графического терминала, если выполнение происходит в среде X Window System. Загрузка модулей в свою очередь выполнялась в пространстве ядра, где нет и не может быть никакого управляющего терминала, поэтому вывод printk() направляется демону системного журналирования, который помещает его, в частности, в системный журнал (/var/log/messages). Этот вопрос еще будет обсуждаться дальше, а пока достаточно просто воспользоваться командой чтения системного журнала (dmesg), как показано ниже:

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

    После успешного создания и загрузки модулей можно их выгрузить с помощью команды rmmod :

    Однако на этом шаге снова возникает ошибка. Рассмотрим листинг выполнения команды lsmod , расположенный несколькими абзацами выше:

    • на модуль md1 ссылается некоторые другие модули или объекты ядра: цифра 1 — это число таких ссылающихся модулей, которое называется счётчиком ссылок;
    • дальше за счётчиком ссылок указывается список тех модулей, откуда исходят такие ссылки, в данном случае, это один модуль md2;
    • до тех пор, пока число ссылок на любой модуль в системе не станет нулевым, модуль не может быть выгружен;
    • другими словами, модуль может быть выгружен только после того, как будут выгружены все ссылающиеся на него другие модули (загруженные после него — здесь не может возникнуть цикличности или перекрёстности ссылок);
    • бывают случаи (как будет показано дальше), когда модуль вообще не может быть выгружен, в частности, когда счётчик ссылок для модуля не может быть сделан нулевым в силу каких-то причин.

    Следующая попытка выгрузить модули уже учитывает эти правила, сначала выгружая модуль md2, и только потом md1:

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

    1. Модуль md1экспортирует для использования другими модулями имя процедуры md1_proc() и, что далеко не так очевидно, имя структуры данных md1_data . Любой другой модуль (md2) может использовать в своём коде любые экспортируемые имена. Это могут быть имена, экспортируемые ранее загруженными модулями, но гораздо чаще это имена, экспортируемые ядром. Это множество экспортируемых имён ядра далее будет называться API ядра. Примером одного из вызовов из набора API ядра в показанных фрагментах кода является вызов printk() .
    2. Модуль md2, использующий экспортируемое имя, связывается с этим именем по прямому абсолютному адресу. Как следствие этого, любые изменения (новая сборка), вносимые в ядро или экспортирующие модули, делают собранный модуль непригодным для использования. Именно поэтому бессмысленно предоставлять модуль в собранном виде — он должен собираться только на месте использования.
    3. Модуль сможет использовать только те имена, которые явно экспортированы. В модуле md1 специально показаны два других имени: md1_local() является локальным именем (модификатор static ), непригодным для связывания, а имя md1_noexport() не объявлено как экспортируемое имя и также не может быть использовано вне модуля.
    4. Почему в качестве строки, выводимой md1_proc() , была выбрана строка «Привет мир!» в русскоязычном написании? Для того, чтобы сразу проверить прозрачность настроек самых разных подсистем Linux на работу с UNICODE представлением символьных данных в кодировке UTF-8 — в ранних версиях Linux всё было не так однозначно. Что в данном случае понимается под прозрачностью? Это единообразное и слаженное поведение на таких кодировках и подсистемы клавиатуры, и отображение в графических терминалах и текстовой консоли, и поведение системного журнала. Кроме того, для большей степени общности, интересно работать со строковыми представлениями, для которых значения strlen() значительно больше визуально видимой длины строки.
    5. Зачем каждую выводимую строку предварять строкой » + «? Это маркер, отмечающий вывод из собственных модулей. В качестве него можно выбрать любой символ или вообще отказаться от него (что чаще всего и происходит). Но если настройки Linux таковы, что работают различные сервисы аудита или подобные службы, то они могут «засыпать» системный журнал достаточно плотным потоком своих сообщений, а сообщения собственных модулей будут сильно разрежены таким потоком. Так что их придётся потом разыскивать в этом потоке. Заблаговременно предварять сообщения собственных модулей фиксированными маркерами — это простейший способ позже осуществить их отбор и группировку, по крайней мере, в иллюстрационных целях, как и было показано. Отобрать собственные сообщения можно с помощью команд, подобных приведенной ниже:

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

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

    В листинге 5 приведен исходный код последнего из модулей, представленных в архиве проекта (файл md3.c):

    Листинг 5. Модифицированная версия модуля md2

    Это ещё одна форма модуля, которая не часто упоминается в других статьях, посвященных модулям ядра, но очень удобная для отладочных действий с модулями и для других специальных действий:

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

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

    Заключение

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

    • код модуля объявляет две функции: инициализации и финализации (завершения);
    • имена функций, выполняющих эти задачи, объявляются в макросах module_init() и module_exit() ;
    • эти функции должны в точности соответствовать прототипу, показанному в листингах примеров;
    • функция инициализации выполняется при загрузке модуля в ядро, если эта функция возвращает нулевое значение (успех), то код модуля остаётся резидентным и выполняет дальнейшую работу;
    • функция завершения вызывается при выгрузке модуля командой rmmod ;
    • функция завершения по своему прототипу не имеет возвращаемого значения, поэтому, начавшись, она уже не имеет механизмов сообщить о своём неудачном выполнении;
    • большинство операций над модулями ( insmod , rmmod , modprobe ) требуют прав root, но некоторые индикативные команды ( lsmod ) могут выполняться и от имени ординарного пользователя.

    Названные выше соглашения по объявлению функций инициализации и завершения являются общепринятыми. Но существует ещё один, не документированный, способ описания этих функций: воспользоваться их предопределёнными именами, а именно init_module() и cleanup_module() , как показано ниже:

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

    Источник

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *

  • Руководство по командам и shell программированию в linux
  • Руководство для начинающих linux mint
  • Руководство администратора linux эви немет гарт снайдер трент р хейн
  • Руководство администратора linux 2 е издание
  • Российский почтовый сервер linux