Writing linux kernel module programming guide

Содержание
  1. Пишем простой модуль ядра Linux
  2. Захват Золотого Кольца-0
  3. Не для простых смертных
  4. Необходимые компоненты
  5. Установка среды разработки
  6. Начинаем
  7. Немного интереснее
  8. Тестирование улучшенного примера
  9. Заключение
  10. The Linux Kernel Module Programming Guide
  11. Авторы: Peter Jay Salzman, Michael Burian, Ori Pomerantz
  12. Предисловие
  13. 1. Об авторах
  14. 2. Нумерация версий и дополнительные примечания
  15. 3. Благодарности
  16. Глава 1. Введение.
  17. 1.1. Что такое «Модуль Ядра»?
  18. 1.2. Как модули попадают в ядро?
  19. 1.2.1. Прежде, чем продолжить
  20. 1.2.1.1. Механизм контроля версий
  21. 1.2.1.2. Работа в XWindow
  22. 1.2.1.3. Проблемы компиляции
  23. Глава 2. Hello World
  24. 2.1. «Hello, World» (часть 1): Простейший модуль ядра.
  25. 2.1.1. Знакомство с printk()
  26. 2.2. Сборка модулей ядра
  27. 2.3. Hello World (часть 2)
  28. 2.4. Hello World (часть 3): Макроопределения __init и __exit
  29. 2.5. Hello World (часть 4): Вопросы лицензирования и документирования модулей
  30. 2.6. Передача модулю параметров командной строки
  31. 2.7. Модули, состоящие из нескольких файлов
  32. 2.8. Сборка модулей под существующее ядро
  33. Глава 3. Дополнительные сведения
  34. 3.1. Модули ядра и прикладные программы
  35. 3.2. Функции, которые доступны из модулей
  36. 3.3. Пространство пользователя и пространство ядра
  37. 3.4. Пространство имен
  38. 3.5. Адресное пространство
  39. 3.6. Драйверы устройств
  40. 3.6.1. Старший и младший номер устройства
  41. Глава 4. Файлы символьных устройств
  42. 4.1. Структура file_operations
  43. 4.2. Структура file
  44. 4.3. Регистрация устройства
  45. 4.4. Отключение устройства
  46. 4.5. chardev.c
  47. 4.6. Создание модулей для работы с разными версиями ядра
  48. Глава 5. Файловая система /proc
  49. 5.1. Файловая система /proc: создание файлов, доступных для чтения
  50. 5.2. Файловая система /proc: создание файлов, доступных для записи
  51. Глава 6. Работа с файлами устройств
  52. Глава 7. Системные вызовы

Пишем простой модуль ядра 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 есть доступ к этим данным.

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

    Заключение

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

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

    Источник

    The Linux Kernel Module Programming Guide

    Авторы: Peter Jay Salzman, Michael Burian, Ori Pomerantz

    Copyright 2001, Peter Jay Salzman.

    2004-05-16 ver 2.6.0

    Перевод: Андрей Киселёв (kis_an [at] linuxgazette [dot] ru)

    Оригинальная версия была опубликована на сайте проекта The Linux Documentation Project.

    Данная книга распространяется на условиях Open Software License, version 1.1. Полный текст лицензии вы сможете найти по адресу http://opensource.org/licenses/osl.php .

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

    Авторы приветствуют широкое распространение этой книги как для персонального, так и для коммерческого пользования, при условии соблюдения вышеупомянутого примечания относительно авторских прав, а так же при условии, что распространитель твердо придерживается условий Open Software License. Вы можете копировать и распространять эту книгу как бесплатно, так и с целью получения прибыли. От авторов не требуется никакого явного разрешения для воспроизводства этой книги на любом носителе, будь то твердая копия или электронная.

    Производные работы и переводы этого документа должны размещаться на условиях Open Software License, а первоначальное примечание об авторских правах должно остаться нетронутым. Если вы добавили новый материал в эту книгу, то вам следует сделать его общедоступным. Пожалуйста извещайте руководителя проекта (Peter Jay Salzman ) о внесенных изменениях и дополнениях. Он объединит модификации и обеспечит непротиворечивость изменений документа.

    Если Вы планируете издавать и распространять эту книгу на коммерческой основе, пожертвования, лицензионные отчисления и/или печатные копии будут высоко оценены автором и The Linux Documentation Project. Таким образом вы окажете поддержку свободному программному обеспечению и LDP. Если у вас появятся вопросы или предложения, пожалуйста пишите руководителю проекта по адресу, указанному выше.

    Предисловие

    1. Об авторах

    Эта книга изначально была написана Ори Померанцем (Ori Pomerantz) для ядра Linux версии 2.2. К сожалению у Ори не хватает времени на продолжение работы над этой книгой. В конце концов ядро Linux продолжает быстро развиваться. Питер Зальцман (Peter Jay Salzman) взял на себя труд по адаптации документа для ядра версии 2.4. К сожалению и Питер не нашел достаточно свободного времени, чтобы продолжить работу над книгой и дополнить ее материалами, касающимися ядра версии 2.6. Таким образом, майкл Бариан (Michael Burian) взялся за адаптацию материала книги для ядра 2.6.

    2. Нумерация версий и дополнительные примечания

    Ядро Linux — динамически развивающийся проект. И перед авторами книги всегда остро стоял вопрос — удалять ли устаревшие сведения из книги или сохранять их, как историческую ценность. Майкл и я (Питер Зальцман) решили создать отдельную ветку документа для каждой стабильной серии ядер. Таким образом, LKMPG (Linux Kernel Module Programming Guide), имеющее версию 2.4.x относится к ядру 2.4.x, а LKMPG 2.6.x — к ядру версии 2.6.x. Мы решили не сохранять устаревшие сведения в новых версиях документа. Желающие получить эту информацию должны обращаться к соответствующим версиям документа.

    Исходный код и подаваемый материал не относятся к какой либо конкретной аппаратной архитектуре, но я ничего не могу гарантировать. Одно важное исключение — «Обработка прерываний» (Глава 12), где весь обсуждаемый материал относится к архитектуре x86.

    3. Благодарности

    Ори Померанц выражает свою благодарность Yoav Weiss, за многочисленные предложения, обсуждение материала и внесение исправлений. Он так же хотел бы поблагодарить Фродо Лоойаарда из Нидердандов, Стефана Джадда из Новой Зеландии, Магнуса Альторпа из Швеции и Эммануэль Папиракис из Квебека, Канада.

    Питер выражает свою благодарность Ори, за то что позволил ему принять участие в проекте LKMPG. А так же Джеффа Ньюмиллера, Ронду Франчес Бэйли (ныне Ронда Франчес Зальцман) и Марка Кима за науку и долготерпение. Он так же хотел бы поблагодарить Дэвида Портера, который оказал неоценимую помощь в переводе документа из формата LaTeX в формат docbook. Это была тяжелая и нудная работа, но ее необходимо было сделать.

    Отдельное спасибо всем участникам проекта www.kernelnewbies.org, и особенно Марку Маклолину и Джону Левону, которые, я уверен, могли бы найти много более интересное занятие, нежели «зависать» на kernelnewbies.org и обучать новичков. Если это руководство научит вас чему либо, знайте — в этом есть доля и их «вины»!

    И Ори и я хотели бы сказать слова благодарности в адрес Ричарда М. Столлмана и Линаса Торвальдса не только за эту превосходную операционную систему, но и за то, что позволили исследовать ее исходный код, чтобы разобраться в том, как она работает.

    Хочется выразить благодарность тем, кто внес свои исправления в документ или внес свои предложения по улучшению. Это Игнасио Мартин, Дэвид Портер, Дэниэл Паоло Скарпацца и Димо Велев.

    Глава 1. Введение.

    1.1. Что такое «Модуль Ядра»?

    Итак, Вы хотите писать модули ядра. Вы знакомы с языком C и у вас есть опыт создания обычных программ, а теперь вы хотите забраться туда, где свершается великое таинство. Туда, где один ошибочный указатель может «снести» файловую систему или «подвесить» компьютер.

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

    1.2. Как модули попадают в ядро?

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

    Как же модули загружаются ядром? Когда ядро обнаруживает необходимость в тех или иных функциональных возможностях, еще не загруженных в память, то демон kmod [1] вызывает утилиту modprobe, передавая ей строку в виде:

    Название модуля, например softdog или ppp.

    Универсальный идентификатор, например char-major-10-30.

    Если утилите modprobe передается универсальный идентификатор , то она сначала пытается отыскать имя соответствующего модуля в файле /etc/modules.conf, где каждому универсальному идентификатору поставлено в соответствие имя модуля, например:

    Это соответствует утверждению: «Данному универсальному идентификатору соответствует файл модуля softdog.ko«.

    Затем modprobe отыскивает файл /lib/modules/version/modules.dep, чтобы проверить — не нужно ли загружать еще какие-либо модули, от которых может зависеть заданный модуль. Этот файл создается командой depmod -a и описывает зависимости модулей. Например, модуль msdos.ko требует, чтобы предварительно был загружен модуль fat.ko. Если модуль Б экспортирует ряд имен (имена функций, переменных и т.п.), которые используются модулем А, то говорят, что «Модуль А зависит от модуля Б«.

    И наконец modprobe вызывает insmod, чтобы сначала загрузить необходимые, для удовлетворения зависимостей, модули, а затем и запрошенный модуль. Вызывая insmod, утилита modprobe указывает ей каталог, /lib/modules/version/ [2] — стандартный путь к модулям ядра. Утилита insmod ничего не «знает» о размещении модулей ядра, зато это «знает» утилита modprobe. Таким образом, если вам необходимо загрузить модуль msdos, то вам необходимо дать следующие команды:

    В большинстве дистрибутивов Linux, утилиты modprobe, insmod, depmod входят в состав пакета modutils или mod-utils.

    Прежде чем закончить эту главу, я предлагаю вкратце ознакомиться с содержимым файла /etc/modules.conf:

    Строки, начинающиеся с символа «#» являются комментариями. Пустые строки игнорируются.

    Строка path[misc] сообщает modprobe о том, что модули ядра из категории misc следует искать в каталоге /lib/modules/2.6.?/local. Как видите, здесь вполне допустимы шаблонные символы.

    Строка path[net] задает каталог размещения модулей категории net, однако, директива keep, стоящая выше, сообщает, что каталог

    p/mymodules не замещает стандартный путь поиска модулей (как это происходит в случае с path[misc]), а лишь добавляется к нему.

    Строка alias говорит о том, что если запрошена загрузка модуля по универсальному идентификатору eth0, то следует загружать модуль eepro.ko

    Вы едва ли встретите в этом файле строки, подобные:

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

    1.2.1. Прежде, чем продолжить

    Прежде, чем мы приступим к программированию, необходимо обсудить еще ряд моментов. Любая система имеет свои отличительные черты и каждый из нас имеет разный багаж знаний. Написать, скомпилировать и запустить свою первую программу «Hello World!» для многих может оказаться довольно сложной задачей. Однако, после преодоления этого начального препятствия, работа, как правило, продвигается без особых проблем.

    Читайте также:  Восстановить работу windows 10 без переустановки

    1.2.1.1. Механизм контроля версий

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

    1.2.1.2. Работа в XWindow

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

    Модули не могут использовать функцию printf() для вывода не экран, но они могут регистрировать сообщения об ошибках, которые в конечном итоге попадают на экран, но только в текстовой консоли. Если же модуль загружается из окна терминала, например xterm, то эти сообщения будут попадать только в системный журнал и не будут выводиться на экран. Чтобы видеть выводимые сообщения на экране, работайте в текстовой консоли ( от переводчика: при опробовании примеров из книги мне не удалось вывести ни одного сообщения на экран, так что ищите ваши сообщения в системном журнале, в моем случае это был файл /var/log/kern.log ).

    1.2.1.3. Проблемы компиляции

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

    Не менее частый случай — неполный набор заголовочных файлов ядра. Для сборки своих модулей вам потребуются многие заголовочные файлы ядра Linux. А закон Мэрфи гласит: «Отсутствовать будут как раз те файлы, в которых вы больше всего нуждаетесь».

    Чтобы избежать этих двух проблем, мы рекомендуем собрать и установить наиболее свежее ядро. Скачать исходные тексты ядра вы сможете на любом из зеркал, распространяющих ядро Linux. За более подробной информацией обращайтесь к «Kernel HOWTO».

    Как это ни покажется странным, но проблемы могут крыться и в компиляторе. По-умолчанию gcc может искать заголовочные файлы ядра совсем не там, куда вы их установили (как правило это каталог /usr/src/. Эта проблема легко преодолевается заданием ключа компиляции -I.

    Глава 2. Hello World

    2.1. «Hello, World» (часть 1): Простейший модуль ядра.

    Когда первый пещерный программист высекал свою первую программу для каменного компьютера, это была программа, которая рисовала «Hello, World!» поперек изображения антилопы. Древнеримские учебники по программированию начинались с программы «Salut, Mundi». Я не знаю — что случается с людьми, которые порывают с этой традицией, но думаю, что лучше этого и не знать. Мы начнем с серии программ «Hello world», на которых разберем различные базовые аспекты создания модулей ядра.

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

    Пример 2-1. hello-1.c

    Любой модуль ядра должен иметь по меньшей мере хотя бы две функции: функцию инициализации модуля — init_module(), которую вызывает insmod во время загрузки модуля, и функцию завершения работы модуля — cleanup_module(), которую вызывает rmmod. Начиная с ядра, версии 2.3.13, требования к именованию начальной и конечной функций были сняты. Теперь вы можете давать им свои имена. Как это сделать будет описано в разделе Hello World (часть 2). Новый метод именования является более предпочтительным, однако многие по-прежнему продолжают использовать имена init_module() и cleanup_module().

    Обычно функция init_module() выполняет регистрацию обработчика какого-либо события или замещает какую-либо функцию в ядре своим кодом (который, как правило, выполнив некие специфические действия, вызывает оригинальную версию функции в ядре). Функция cleanup_module() является полной противоположностью, она производит «откат» изменений, сделаных функцией init_module(), что делает выгрузку модуля безопасной.

    И наконец, любой модуль ядра должен подключать заголовочный файл linux/module.h. В нашем примере мы подключали еще один файл — linux/kernel.h, но лишь для того, чтобы получить доступ к определению KERN_ALERT, которое более подробно будет обсуждаться в следующем разделе

    2.1.1. Знакомство с printk()

    Несмотря на столь красноречивое название, функция printk() вовсе не предназначена для вывода информации на экран, даже не смотря на то, что мы использовали ее в своем примере именно для этой цели! Основное назначение этой функции — дать ядру механизм регистрации событий и предупреждений. Поэтому, каждый вызов printk() сопровождается указанием приоритета, в нашем примере это и KERN_ALERT. Всего в ядре определено 8 различных уровней приоритета для функции printk() и каждый из них имеет свое макроопределение, таким образом нет необходимости писать числа, лишенные смысла (имена уровней приоритета и их числовые значения вы найдете в файле linux/kernel.h). Если уровень приоритета не указывается, то по-умолчанию он принимается равным DEFAULT_MESSAGE_LOGLEVEL.

    Найдите время и просмотрите содержимое этого файла. Здесь вы найдете краткое описание значения каждого из уровней. На практике считается дурным тоном указание уровней приоритета числовым значением, например так: . Для этих целей лучше пользоваться именами макроопределений, например: KERN_WARNING.

    Если задан уровень ниже, чем int console_loglevel, то сообщение выводится на экран. Если запущены и syslog, и klogd, то сообщение попадет также и в системный журнал /var/log/messages, при этом оно может быть выведено на экран, а может и не выводиться. Мы использовали достаточно высокий уровень приоритета KERN_ALERT для того, чтобы гарантировать вывод сообщения на экран функцией printk(). Когда вы вплотную займетесь созданием модулей ядра, вы будете использовать уровни приоритета наиболее подходящие под конкретную ситуацию.

    2.2. Сборка модулей ядра

    Чтобы модуль был работоспособен, при компиляции необходимо передать gcc ряд опций. Кроме того, необходимо чтобы модули компилировались с предварительно определенными символами. Ранние версии ядра полностью полагались, в этом вопросе, на программиста и ему приходилось явно указывать требуемые определения в Makefile-ах. Несмотря на иерархическую организацию, в Makefile-ах, на вложенных уровнях, накапливалось такое огромное количество параметров настройки, что управление и сопровождение этих настроек стало довольно трудоемким делом. К счастью появился kbuild, в результате процесс сборки внешних загружаемых модулей теперь полностью интегрирован в механизм сборки ядра. Дополнительные сведения по сборке модулей, которые не являются частью официального ядра (как в нашем случае), вы найдете в файле linux/Documentation/kbuild/modules.txt.

    А теперь попробуем собрать наш с вами модуль hello-1.c . Соответствующий Makefile содержит всего одну строку:

    Пример 2-2. Makefile для модуля ядра

    Для того, чтобы запустить процесс сборки модуля, дайте команду make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules ( от переводчика: если у вас в каталоге /usr/src присутствует символическая ссылка linux на каталог с исходными текстами ядра, то команда сборки может быть несколько упрощена: make -C /usr/src/linux SUBDIRS=$PWD modules ). На экран должно быть выведено нечто подобное:

    Обратите внимание: в ядрах версии 2.6 введено новое соглашение по именованию объектных файлов модулей. Теперь, они имеют расширение .ko (взамен прежнего .o), что отличает их от обычных объектных файлов. Дополнительную информацию по оформлению Makefile-ов модулей вы найдете в linux/Documentation/kbuild/makefiles.txt. Обязательно прочтите этот документ прежде, чем начнете углубляться в изучение Makefile-ов.

    Итак, настал торжественный момент — теперь можно загрузить свежесобранный модуль! Дайте команду insmod ./hello-1.ko (появляющиеся сообщения о «загрязнении» ядра вы сейчас можете просто игнорировать, вскоре мы обсудим эту проблему).

    Любой загруженный модуль ядра заносится в список /proc/modules, так что дружно идем туда и смотрим содержимое этого файла. как вы можете убедиться, наш модуль стал частью ядра. С чем вас и поздравляем, теперь вы стали одним из авторов кода ядра! Вдоволь насладившись ощущением новизны, выгрузите модуль командой rmmod hello-1 и загляните в файл /var/log/messages, здесь вы увидите сообщения, которые сгенерировал ваш модуль. ( от переводчика: в моем случае на экран консоли сообщения не выводились, зато они появились в файле /var/log/kern.log ).

    А теперь небольшое упражнение: Измените содержимое файла hello-1.c так, чтобы функция init_module() возвращала бы какое либо ненулевое значение и проверьте — что получится?

    2.3. Hello World (часть 2)

    Как мы уже упоминали, начиная с ядра, версии 2.3.13, требования к именованию начальной и конечной функций модуля были сняты. Достигается это с помощью макроопределений module_init() и module_exit(). Они определены в файле linux/init.h. Единственное замечание: начальная и конечная функции должны быть определены выше строк, в которых вызываются эти макросы, в противном случае вы получите ошибку времени компиляции. Ниже приводится пример использования этих макроопределений:

    Пример 2-3. hello-2.c

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

    Пример 2-4. Makefile для сборки обоих модулей

    Теперь загляните в файл linux/drivers/char/Makefile. Он может рассматриваться как пример полноценного Makefile модуля ядра. Здесь видно, что ряд модулей жестко «зашиты» в ядро (obj-y), но нигде нет строки obj-m. Почему? Знакомые с языком сценариев командной оболочки легко найдут ответ. Все записи вида obj-$(CONFIG_FOO) будут заменены на obj-y или obj-m, в зависимости от значения переменных CONFIG_FOO. Эти переменные вы сможете найти в файле .config, который был создан во время конфигурирования ядра с помощью make menuconfig или что-то вроде этого.

    2.4. Hello World (часть 3): Макроопределения __init и __exit

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

    То же относится и к макросу __initdata, но только для переменных.

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

    Оба этих макроса определены в файле linux/init.h и отвечают за освобождение неиспользуемой памяти в ядре. Вам наверняка приходилось видеть на экране, во аремя загрузки, сообщение примерно такого содержания: Freeing unused kernel memory: 236k freed. Это как раз и есть результат работы данных макроопределений.

    Пример 2-5. hello-3.c

    2.5. Hello World (часть 4): Вопросы лицензирования и документирования модулей

    Если у вас установлено ядро 2.4 или более позднее, то наверняка, во время запуска примеров модулей, вам пришлось столкнуться с сообщениями вида:

    В ядра версии 2.4 и выше был добавлен механизм контроля лицензий, чтобы иметь возможность предупреждать пользователя об использовании проприетарного (не свободного) кода. Задать условия лицензирования модуля можно с помощью макроопределения MODULE_LICENSE(). Ниже приводится выдержка из файла linux/module.h ( от переводчика: я взял на себя смелость перевести текст комментариев на русский язык ):

    Точно так же, для описания модуля может использоваться макрос MODULE_DESCRIPTION(), для установления авторства — MODULE_AUTHOR(), а для описания типов устройств, поддерживаемых модулем — MODULE_SUPPORTED_DEVICE().

    Все эти макроопределения описаны в файле linux/module.h. Они не используются ядром и служат лишь для описания модуля, которое может быть просмотрено с помощью objdump. Попробуйте с помощью утилиты grep посмотреть, как авторы модулей используют эти макросы (в каталоге linux/drivers).

    Пример 2-6. hello-4.c

    2.6. Передача модулю параметров командной строки

    Имеется возможность передачи модулю дополнительных параметров командной строки, но делается это не с помощью argc/argv.

    Для начала вам нужно объявить глобальные переменные, в которые будут записаны входные параметры, а затем вставить макрос MODULE_PARAM(), для запуска механизма приема внешних аргументов. Значения параметров могут быть переданы модулю с помощью команд insmod или modprobe. Например: insmod mymodule.ko myvariable=5. Для большей ясности, объявления переменных и вызовы макроопределений следует размещать в начале модуля. Пример кода прояснит мое, по общему признанию, довольно неудачное объяснение.

    Макрос MODULE_PARAM() принимает 2 аргумента: имя переменной и ее тип. Поддерживаются следующие типы переменных

    «h» — short int (короткое целое);

    «i» — integer (целое, как со знаком, так и без знака);

    «l» — long int (длинное целое, как со знаком, так и без знака);

    «s» — string (строка, должна объявляться как char*).

    Для переменных типа char *, insmod будет сама выделять необходимую память. Вы всегда должны инициализировать переменные значениями по-умолчанию, не забывайте — это код ядра, здесь лучше лишний раз перестраховаться. Например:

    Параметры-массивы так же допустимы. Целое число, предшествующее символу типа аргумента, обозначает максимальный размер массива. Два числа, разделенные дефисом — минимальное и максимальное количество значений. Например, массив целых, который должен иметь не менее 2-х и не более 4-х значений, может быть объявлен так:

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

    И, наконец, еще одно макроопределение — MODULE_PARAM_DESC(). Оно используется для описания входных аргументов модуля. Принимает два параметра: имя переменной и строку описания, в свободной форме.

    Пример 2-7. hello-5.c

    Давайте немножко поэкспериментируем с этим модулем:

    2.7. Модули, состоящие из нескольких файлов

    Иногда возникает необходимость разместить исходные тексты модуля в нескольких файлах. В этом случае kbuild опять возьмет на себя всю «грязную» работу, а Makefile поможет сохранить наши руки чистыми, а голову светлой! Ниже приводится пример модуля, состоящего из двух файлов:

    Пример 2-8. start.c

    Пример 2-9. stop.c

    Пример 2-10. Makefile для сборки всех модулей

    2.8. Сборка модулей под существующее ядро

    Мы уже рекомендовали вам пересобрать свое ядро, включив некоторые полезные для отладки опции, например такие, как (MODULE_FORCE_UNLOAD) — когда эта опция включена, то вы имеете возможность принудительной выгрузки модуля (посредством команды rmmod -f module_name), даже если ядро «считает» ваши действия небезопасными. Эта опция поможет вам сэкономить время на перезагрузках системы, в процессе отладки модуля.

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

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

    Более подробная информация будет помещена в файл /var/log/messages:

    Другими словами — ваше ядро отказывается «принимать» ваш модуль из-за несоответствия версий (точнее — из-за несоответствия сигнатур версий). Сигнатура версии сохраняется в объектном файле в виде статической строки, начинающейся со слова vermagic:. Эта строка вставляется во время компоновки модуля с файлом init/vermagic.o. Просмотреть сигнатуру версии (так же как и некоторые дополнительные сведения) можно посредством команды modinfo module.ko:

    Для преодоления этого препятствия можно воспользоваться ключом —force-vermagic (команды modprobe, прим. перев. ), но это решение потенциально опасно и совершенно неприменимо при распространении готовых модулей. Следовательно, вам придется пересобрать модуль в окружении идентичном тому, в котором было собрано целевое ядро. Вопрос: «Как это сделать?» и является темой для дальнейшего обсуждения в данной главе.

    Прежде всего вам необходимо установить дерево с исходными текстами ядра той же версии, что и целевое ядро. Найдите файл конфигурации целевого ядра, как правило он располагается в каталоге /boot, под именем, что-то вроде config-2.6.x. Просто скопируйте его в каталог с исходными текстами ядра на своей машине.

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

    Теперь запустите make, чтобы обновить информацию о версии:

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

    Глава 3. Дополнительные сведения

    3.1. Модули ядра и прикладные программы

    Работа программы обычно начинается с исполнения функции main(). После выполнения всей последовательность команд программа завершает свою работу. Модули исполняются иначе. Они всегда начинают работу с исполнения функции init_module, или с функции, которую вы определили через вызов module_init. Это функция запуска модуля, которая подготавливает его для последующих вызовов. После завершения исполнения функции init_module модуль больше ничего не делает, он просто «сидит и ждет», когда ядро обратится к нему для выполнения специфических действий.

    Вторая точка входа в модуль — cleanup_module, вызывается непосредственно перед его выгрузкой. Она производит «откат» изменений, выполненных функцией init_module() и, как бы говорит ядру: «Я ухожу! Больше не проси меня ни о чем!».

    Любой модуль обязательно должен иметь функцию инициализации и функцию завершения. Так как существует более чем один способ определить функции инициализации и завершения, я буду стараться использовать термины «начальная» и «конечная» функции, если я собьюсь и укажу названия init_module и cleanup_module, то думаю, что вы поймете меня правильно.

    3.2. Функции, которые доступны из модулей

    Как программист, вы знаете, что приложение может вызывать функции, которые не определены в самой программе. На стадии связывания (линковки) разрешаются все внешние ссылки, уходящие во внешние библиотеки. Функция printf — одна из таких функций, которая определена в библиотеке libc.

    Читайте также:  Waterfox для windows 32

    Модули ядра в этом плане сильно отличаются от прикладных программ. В примере «Hello World» мы использовали функцию printk(), но не подключали стандартную библиотеку ввода-вывода. Модули так же проходят стадию связывания, но только с ядром, и могут вызывать только те функции, которые экспортируются ядром. Разрешение ссылок на внешние символы производится утилитой insmod. Если у вас есть желание взглянуть на список имен, экспортируемых ядром, загляните в файл /proc/kallsyms.

    Здесь я хочу заострить ваше внимание на различиях между библиотечными функциями и системными вызовами. Библиотечные функции — это верхний уровень, который работает в пространстве пользователя и обеспечивает более удобный интерфейс к функциям, которые выполняют основную работу — системным вызовам. Системные вызовы работают в привилегированном режиме от имени пользователя и предоставляются самим ядром. Библиотечная функция printf() на первый взгляд выглядит как основная функция вывода, но все, что она фактически делает — это формирует строку, в соответствии с заданным форматом, и передает ее низкоуровневому системному вызову write(), который и выводит строку на устройство стандартного вывода.

    Как в этом можно убедиться? Да очень просто! Скомпилируйте следующую программу:

    с помощью команды gcc -Wall -o hello hello.c и запустите ее командой strace hello. Впечатляет? Каждая строка, выводимая на экран, соответствует системному вызову. strace — незаменимый инструмент для того, чтобы выяснить — куда программа, пытается обратиться, включая такие сведения, как имена системных вызовов, передаваемые им аргументы и возвращаемые значения. Здесь вы должны увидеть строку, которая выглядит примерно так: write(1, «hello», 5hello). Это и есть то, что мы ищем. Т.е. скрытая от нас сторона вызова функции printf(). Возможно вы не знакомы с вызовом write(), поскольку большинство программистов предпочитает пользоваться стандартными библиотечными функциями (такими как fopen(), fputs(), fclose()). Если это так, тогда загляните в man 2 write. Второй раздел справочного руководства содержит описания системных вызовов (таких как kill(), read() и т.п.). В третьем разделе описываются библиотечные вызовы (такие как cosh(), random() и пр.).

    Вы можете даже написать модули, которые подменяют системные вызовы ядра, вскоре мы продемонстрируем это. Взломщики довольно часто используют эту возможность для создания «черного хода» в систему или «троянов», но вы можете использовать ее в менее вредоносных целях, например заставить ядро выводить строку «Tee hee, that tickles!» («Хи-хи, щекотно!») каждый раз, когда кто нибудь пробует удалить файл.

    3.3. Пространство пользователя и пространство ядра

    За доступ к ресурсам системы отвечает ядро, будь то видеоплата, жесткий диск или даже память. Программы часто конкурируют между собой за доступ к тем или иным ресурсам. Например, при подготовке этого документа, я сохраняю файл с текстом на жесткий диск, тут же стартует updatedb, чтобы обновить локальную базу данных. В результате мой vim и updatedb начинают конкурировать за обладание жестким диском. Ядро должно обслужить конкурирующие запросы, и «выстроить» их в порядке очередности. К тому же сам центральный процессор может работать в различных режимах. Каждый из режимов имеет свою степень «свободы» действий. Микропроцессор Intel 80386 имеет четыре таких режима, которые часто называют «кольцами». Unix использует только два из них: наивысший (нулевое кольцо, известное так же под названием привилегированный режим ) и низший ( пользовательский режим ).

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

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

    Unix производит переключение из пространства пользователя в пространство ядра всякий раз, когда приложение делает системный вызов или приостанавливается аппаратным прерыванием. Код ядра, исполняющий системный вызов, работает в контексте процесса — от имени вызвавшего процесса и имеет доступ к данным в адресном пространстве процесса. Код, который обрабатывает прерывание, наоборот, являясь асинхронным по своей природе, не относится ни к одному из процессов.

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

    3.4. Пространство имен

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

    Модули ядра компонуются с огромным программным пакетом — ядром, поэтому проблема «загрязнения» пространства имен становится достаточно острой. Коллизии имен могут породить трудноуловимые ошибки, начиная от того, что модуль просто отказывается загружаться, и заканчивая весьма причудливыми сообщениями. Лучший способ избежать «загрязнения» пространства имен — это объявлять все имена как static и использовать префиксы для придания уникальности именам с глобальной областью видимости. По соглашению об именовании, желательно, в качестве префиксов, использовать символы нижнего регистра. Если вы не можете какие-то имена объявить как static, то разрешить проблему можно посредством создания symbol table и регистрации ее в ядре. Эту тему мы обсудим ниже.

    Файл /proc/kallsyms содержит все имена в ядре, с глобальной областью видимости, которые доступны для ваших модулей.

    3.5. Адресное пространство

    Управление памятью — очень сложная тема, она достаточно полно освещается в книге «Understanding The Linux Kernel», выпущенной издательством O’Reilly. Мы не собираемся делать из вас экспертов в области управления памятью, но вам действительно необходимо знать некоторые факты.

    Если вы никогда не задумывалесь над тем, что означает слово segfault, то для вас скорее всего окажется сюрпризом тот факт, что указатели фактически не указывают на какой-то реальный участок физической памяти. В любом случе, эти адреса не являются реальными. Когда запускается процесс, ядро выделяет под него кусок физической памяти и передает его процессу. Эта память используется для размещения исполняемого кода, стека, переменных, динамической «кучи» и других вещей, о чем наверняка знают компьютерные гении. [3] Эта память начинается с логического адреса #0 и простирается до того адреса, который необходим. Поскольку области памяти, выделенные для разных процессов, не пересекаются, то каждый из процессов, обратившись к ячейке памяти с адресом, скажем 0xbffff978, получит данные из различных областей физической памяти! В даннм случае число 0xbffff978 можно рассматривать как смещение относительно начала области памяти, выделенной процессу. Как правило программы, подобные нашей «Hello World», не могут обратиться к памяти, занимаемой другим процессом, хотя существуют обходные пути, позволяющие добиться этого, но оставим пока эту тему для более позднего обсуждения.

    Ядро тоже имеет свое собственное адресное пространство. Поскольку модуль по сути является частью ядра, то он так же работает в адресном пространстве ядра. Если ошибка segmentation fault, возникающая в приложении может быть отслежена и устранена без особых проблем, то в модуле подобная ошибка может стать фатальной для всей системы. Из-за незначительной ошибки в модуле вы рискуете «затоптать» ядро. Результат может быть самым плачевным. Поэтому будьте предельно внимательны!

    Хотелось бы заметить, что это справедливо для любой операционной системы, которая построена на монолитном ядре. [4] Есть операционные системы, в основе которых лежит микроядро. В таких ОС каждый модуль получает свое адресное пространство. Примерами могут служить GNU Hurd и QNX Neutrino.

    3.6. Драйверы устройств

    Драйверы устройств являются одной из разновидностей модулей ядра. Они играют особую роль. Это настоящие «черные ящики», которые полностью скрывают детали, касающиеся работы устройства, и предоставляют четкий программный интерфейс для работы с аппаратурой. В Unix каждое аппаратное устройство представлено псевдофайлом (файлом устройства) в каталоге /dev. Этот файл обеспечивает средства взаимодействия с аппаратурой. Так, например, драйвер звуковой платы es1370.ko связывает файл устройства /dev/sound со звуковой платой Ensoniq IS1370. Пользовательское приложение, например mp3blaster может использовать для своей работы /dev/sound, ничего не подозревая о типе установленной звуковой платы.

    3.6.1. Старший и младший номер устройства

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

    Обратили внимание на столбец с числами, разделенными запятой? Первое число называют «Старшим номером» устройства. Второе — «Младшим номером». Старший номер говорит о том, какой драйвер используется для обслуживания аппаратного обеспечения. Каждый драйвер имеет свой уникальный старший номер. Все файлы устройств с одинаковым старшим номером управляются одним и тем же драйвером. Все из выше перечисленных файлов устройств имеют старший номер, равный 3, потому что все они управляются одним и тем же драйвером.

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

    Устройства подразделяются на две большие группы — блочные и символьные . Основное различие блочных и символьных устройств состоит в том, что обмен данными с блочным устройством производится порциями байт — блоками. Они имеют внутренний буфер, благодаря чему повышается скорость обмена. В большинстве Unix-систем размер одного блока равен 1 килобайту или другому числу, являющемуся степенью числа 2. Символьные же устройства — это лишь каналы передачи информации, по которым данные следуют последовательно, байт за байтом. Большинство устройств относятся к классу символьных, поскольку они не ограничены размером блока и не нуждаются в буферизации. Если первый символ в списке, полученном командой ls-l /dev, ‘b’, тогда это блочное устройство, если ‘c’, тогда — символьное. Устройства, которые были приведены в примере выше — блочные. Ниже приводится список некоторых символьных устройств (последовательные порты):

    Если вам интересно узнать, как назначаются старшие номера устройств, загляните в файл /usr/src/linux/documentation/devices.txt.

    Все файлы устройств создаются в процессе установки системы с помощью утилиты mknod. Чтобы создать новое устройство, например с именем «coffee», со старшим номером 12 и младшим номером 2, нужно выполнить команду mknod /dev/coffee c 12 2. Вас никто не обязывает размещать файлы устройств в каталоге /dev, тем не менее, делается это в соответствии с принятыми соглашениями. Однако, при разработке драйвера устройства, на период отладки, размещать файл устройства в своем домашнем каталоге — наверное не такая уж и плохая идея. Единственное — не забудьте исправить место для размещения файла устройства после того, как отладка будет закончена.

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

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

    К настоящему моменту вы можете сказать об этих файлах устройств, что оба они — блочные устройства, что обслуживаются одним и тем же драйвером (старший номер 2). Вы можете даже заявить, что они оба представляют ваш дисковод для гибких дисков, несмотря на то, что у вас стоит только один дисковод. Но почему два файла? А дело вот в чем, один из них представляет дисковод для дискет, емкостью 1.44 Мб. Другой — тот же самый дисковод, но для дискет емкостью 1.68 Мб, и соответствует тому, что некоторые люди называют «суперотформатированным» диском («superformatted» disk). Такие дискеты могут хранить больший объем данных, чем стандартно-отформатированная дискета. Вот тот случай, когда два файла устройства, с различным младшими номерами, фактически представляют одно и то же физическое устройство. Так что, слово «устройство», в нашем обсуждении, может означать нечто более абстрактное.

    Глава 4. Файлы символьных устройств

    4.1. Структура file_operations

    Структура file_operations определена в файле linux/fs.h и содержит указатели на функции драйвера, которые отвечают за выполнение различных операций с устройством. Например, практически любой драйвер символьного устройства реализует функцию чтения данных из устройства. Адрес этой функции, среди всего прочего, хранится в структуре file_operations. Ниже приводится определение структуры, взятое из исходных текстов ядра 2.6.5:

    Драйвер зачастую реализует далеко не все функции, предусмотренные данной структурой. Например, драйвер, который обслуживает видеоплату, не обязан выполнять операцию чтения каталога ( readdir). Поля структуры, соответствующие нереализованным функциям, заполняются «пустыми» указателями — NULL.

    Компилятор gcc предоставляет программисту довольно удобный способ заполнения полей структуры в исходном тексте. Поэтому, если вы встретите подобный прием в современных драйверах, пусть это вас не удивляет. Ниже приводится пример подобного заполнения:

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

    На мой взгляд все выглядит достаточно понятным. И еще, вы должны знать, что в любое поле структуры, которое вы явно не инициализируете, компилятор gcc запишет «пустой» указатель — NULL. Указатель на struct file_operations обычно именуют как fops.

    4.2. Структура file

    Каждое устройство представлено в ядре структурой file, которая определена в файле linux/fs.h. Эта структура используется исключительно ядром и никогда не используется прикладными программами, работающими в пространстве пользователя. Это совершенно не то же самое, что и FILE, определяемое библиотекой glibc и которое в свою очередь в ядре нигде не используется. Имя структуры может ввести в заблуждение, поскольку она представляет абстракцию открытого файла, а не файла на диске, который представляет структура inode.

    Как правило указатель на структуру file называют filp.

    Загляните в заголовочный файл и посмотрите определение структуры file. Большинство имеющихся полей структуры, например struct dentry *f_dentry, не используются драйверами устройств, и вы можете игнорировать их. Драйверы не заполняют структуру file непосредственно, они только используют структуры, содержащиеся в ней.

    4.3. Регистрация устройства

    Как уже говорилось ранее, доступ к символьным устройствам осуществляется посредством файлов устройств, которые как правило располагаются в каталоге /dev. [5] Старший номер устройства говорит о том, какой драйвер с каким файлом устройства связан. Младший номер используется самим драйвером для идентификации устройства, если он обслуживает несколько таких устройств.

    Добавление драйвера в систему подразумевает его регистрацию в ядре. Это означает — получение старшего номера в момент инициализации модуля. Получить его можно вызовом функции register_chrdev(), определенной в файле linux/fs.h:

    где unsigned int major — это запрашиваемый старший номер устройства, const char *name — название устройства, которое будет отображаться в /proc/devices и struct file_operations *fops — указатель на таблицу file_operations драйвера. В случае ошибки, функция register_chrdev() возвращает отрицательное число. Обратите внимание: функции регистрации драйвера не передается младший номер устройства. Все потому, что ядро не обслуживает его — это прерогатива драйвера.

    А теперь вопрос: Как получить старший номер для своего устройства, чтобы случайно не «занять» уже существующий? Самый простой способ — заглянуть в файл Documentation/devices.txt и выбрать один из неиспользуемых. Но это не самый лучший выход, потому что вы никогда не будете уверены в том, что выбранный вами номер не будет позднее официально связан с каким-либо другим устройством. Правильный ответ — «попросить» ядро выделить вам динамический номер устройства.

    Если вы передадите функции register_chrdev(), в качестве старшего номера, число 0, то возвращаемое положительное значение будет представлять собой, динамически выделенный ядром, старший номер устройства. Один из неприятных моментов здесь состоит в том, что вы заранее не можете создать файл устройства, поскольку старший номер устройства вам заранее не известен. Тем не менее, можно предложить ряд способов решения этой проблемы.

    Драйвер может выводить сообщение в системный журнал (как это делает модуль «Hello World»), а вы затем вручную создадите файл устройства.

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

    Можно «заставить» сам драйвер создавать файл устройства, с помощью системного вызова mknod, после успешной регистрации. А внутри cleanup_module() предусмотреть возможность удаления файла устройства с помощью rm.

    4.4. Отключение устройства

    Мы не можем позволить выгружать модуль по прихоти суперпользователя. Если файл устройства удалить после того как он будет открыт процессом, то может возникнуть ситуация когда процесс попытается обратиться к выгруженному драйверу (в конце концов процесс даже не подозревает, что такое могло произойти). В результате произойдет попытка обращения к тому участку памяти, где ранее находилась функция обработки запроса. Если вам повезет, то этот участок памяти окажется не затертым ядром и вы получите сообщение об ошибке. Если не повезет — то произойдет переход в середину «чужой» функции. Результат такого «вызова» трудно предугадать заранее

    Читайте также:  Все права защищены windows

    Обычно, если какая-то операция должна быть отвергнута, функция возвращает код ошибки (отрицательное число). В случае с функцией cleanup_module() это невозможно, поскольку она не имеет возвращаемого значения. Однако, для каждого модуля в системе имеется счетчик обращений, который хранит число процессов, использующих модуль. Вы можете увидеть это число в третьем поле, в файле /proc/devices. Если это поле не равно нулю, то rmmod не сможет выгрузить модуль. Обратите внимание: вам нет нужды следить за состоянием счетчика в cleanup_module(), это делает система, внутри системного вызова sys_delete_module (определение функции вы найдете в файле linux/module.c). Вы не должны изменять значение счетчика напрямую, тем не менее, ядро предоставляет в ваше распоряжение функции, которые увеличивают и уменьшают значение счетчика обращений:

    try_module_get(THIS_MODULE): увеличивает счетчик обращений на 1.

    try_module_put(THIS_MODULE): уменьшает счетчик обращений на 1.

    Очень важно сохранять точное значение счетчика! Если Вы каким-либо образом потеряете действительное значение, то вы никогда не сможете выгрузить модуль. Тут, милые мои мальчики и девочки, поможет только перезагрузка! Это обязательно случиться с вами, рано или поздно, при разработке какого-либо модуля!

    4.5. chardev.c

    Следующий пример создает устройство с именем chardev. Вы можете читать содержимое файла устройства с помощью команды cat или открывать его на чтение из программы (функцией open()). Посредством этого файла драйвер будет извещать о количестве попыток обращения к нему. Модуль не поддерживает операцию записи (типа: echo «hi» > /dev/chardev), но определяет такую попытку и сообщает пользователю о том, что операция записи не поддерживается.

    Пример 4-1. chardev.c

    Пример 4-2. Makefile

    4.6. Создание модулей для работы с разными версиями ядра

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

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

    Итак, мы уже поняли, что между разными версиями ядра могут существовать весьма существенные отличия. Если у вас появится необходимость в создании модуля, который мог бы работать с разными версиями ядра, то можете воспользоваться директивами условной компиляции, основываясь на сравнении макроопределений LINUX_VERSION_CODE и KERNEL_VERSION. Для версии a.b.c, макрос KERNEL_VERSION вернет код версии, вычисленный в соответствии с выражением: 2^<16>a+2^<8>b+c. Макрос LINUX_VERSION_CODE возвращает текущую версию ядра.

    В предыдущих версиях данного руководства, довольно подробно описывалось, как писать обратно совместимый код, с использованием директив условной компиляции. Но, начиная с этой версии, мы решили порвать с устоявшейся традицией. Теперь, если вы желаете писать модули под определенные версии ядра, обращайтесь к соответствующей версии руководства (LKMPG). Мы решили выпускать этот документ под версиями (номер версии и номер подверсии), совпадающими с версиями обсуждаемого ядра. Таким образом, разработчики, работающие под ядро 2.4.x, должны обращаться к LKMPG версии 2.4.x, работающие под ядро 2.6.x — к LKMPG версии 2.6.x и т.д.

    Глава 5. Файловая система /proc

    5.1. Файловая система /proc: создание файлов, доступных для чтения

    Linux предоставляет ядру и модулям ядра дополнительный механизм передачи информации заинтересованным в ней процессам — это файловая система /proc. Первоначально она создавалась с целью получения сведений о процессах (отсюда такое название). Теперь она интенсивно используется и самим ядром, которому есть что сообщить! Например, /proc/modules — список загруженных модулей, /proc/meminfo — статистика использования памяти.

    Методика работы с файловой системой /proc очень похожа на работу драйверов с файлами устройств: вы создаете структуру со всей необходимой информацией, включая указатели на функции-обработчики (в нашем случае имеется только один обработчик, который обслуживает чтение файла в /proc). Функция init_module регистрирует структуру, а cleanup_module отменяет регистрацию.

    Основная причина, по которой используется proc_register_dynamic [6] состоит в том, что номер inode, для нашего файла, заранее неизвестен, поэтому мы даем возможность ядру определить его самостоятельно, чтобы предотвратить возможные конфликты. В обычных файловых системах, размещенных на диске, не в памяти, как /proc, inode указывает на то место в дисковом пространстве, где размещена индексная запись (index node, сокращенно — inode) о файле. Inode содержит все необходимые сведения о файле, например права доступа, указатель на первый блок с содержимым файла.

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

    Пример 5-1. procfs.c

    Пример 5-2. Makefile

    5.2. Файловая система /proc: создание файлов, доступных для записи

    Пока мы знаем о двух способах получения информации от драйвера устройства: можно зарегистрировать драйвер и создать файл устройства, и создать файл в файловой системе /proc. Единственная проблема — мы пока ничего не можем передать модулю ядра. Для начала попробуем организовать передачу данных модулю ядра посредством файловой системы /proc.

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

    Linux предусматривает возможность регистрации файловой системы. Так как каждая файловая система должна иметь собственные функции, для обработки inode и выполнять файловые операции, [7] то имеется специальная структура, которая хранит указатели на все необходимые функции-обработчики — struct inode_operations, которая включает указатель на struct file_operations. Файловая система /proc, всякий раз, когда мы регистрируем новый файл, позволяет указать — какая struct inode_operations будет использоваться для доступа к нему. В свою очередь, в этой структуре имеется указатель struct file_operations, а в ней уже находятся указатели на наши функции-обработчики.

    Обратите внимание: стандартные понятия «чтение» и «запись», в ядре имеют противоположный смысл. Функции чтения используются для записи в файл, в то время как функции записи используются для чтения из файла. Причина в том, что понятия «чтение» и «запись» рассматриваются здесь с точки зрения пользователя: если процесс читает что-то из ядра — ядро должно записать эти данные, если процесс пишет — ядро должно прочитать то, что записано.

    Еще один интересный момент — функция module_permission. Она вызывается всякий раз, когда процесс пытается обратиться к файлу в файловой системе /proc, и принимает решение — разрешить доступ к файлу или нет. На сегодняшний день, решение принимается только на основе выполняемой операции и UID процесса, но в принципе возможна и иная организация принятия решения, например, разрешать ли одновременный доступ к файлу нескольким процессам и пр..

    Причина, по которой для копирования данных используются функции put_user и get_user, состоит в том, что процессы в Linux (по крайней мере в архитектуре Intel) исполняются в изолированных адресных пространствах, не пересекающихся с адресным пространством ядра. Это означает, что указатель, не содержит уникальный адрес физической памяти — он хранит логический адрес в адресном пространстве процесса.

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

    Пример 5-3. procfs.c

    Хотите еще примеры работы с файловой системой /proc? Хорошо, но имейте ввиду, ходят слухи, что /proc уходит в небытие и вместо нее следует использовать sysfs. Дополнительные сведения о файловой системе /proc вы найдете в linux/Documentation/DocBook/. Дайте команду make help, она выведет инструкции по созданию документации в различных форматах, например: make htmldocs.

    Глава 6. Работа с файлами устройств

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

    Однако, этого не всегда бывает достаточно. Допустим, что у вас есть модем, подключенный к компьютеру через последовательный порт (это может быть и внутренний модем, с точки зрения CPU он «выглядит» как модем, связанный с последовательным портом). Естественное решение — использовать файл устройства для передачи данных модему (это могут быть команды модема или данные, которые будут посланы в телефонную линию) и для чтения данных из модема (ответы модема на команды или данные, полученные из телефонной линии). Однако, это оставляет открытым вопрос о том, как взаимодействовать непосредственно с последовательным портом, например, как настроить скорость обмена.

    Ответ: в Unix следует использовать специальную функцию с именем ioctl (сокращенно от Input Output ConTroL). Любое устройство может иметь свои команды ioctl, которые могут читать (для передачи данных от процесса ядру), писать (для передачи данных от ядра к процессу), и писать и читать, и ни то ни другое, [8] Функция ioctl вызывается с тремя параметрами: дескриптор файла устройства, номер ioctl и третий параметр, который имеет тип long, используется для передачи дополнительных аргументов. [9]

    Номер ioctl содержит комбинацию бит, составляющих старший номер устройства, тип команды и тип дополнительного параметра. Обычно номер ioctl создается макроопределением (_IO, _IOR, _IOW или _IOWR, в зависимости от типа) в файле заголовка. Этот заголовочный должен подключаться директивой #include, к исходным файлам программы, которая использует ioctl для обмена данными с модулем. В примере, приводимом ниже, представлены файл заголовка chardev.h и программа, которая взаимодействует с модулем ioctl.c.

    Если вы предполагаете использовать ioctl в ваших собственных модулях, то вам надлежит обратиться к файлу Documentation/ioctl-number.txt с тем, чтобы не «занять» зарегистрированные номера ioctl.

    Пример 6-1. chardev.c

    Пример 6-2. chardev.h

    Пример 6-3. ioctl.c

    Пример 6-4. Makefile

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

    Пример 6-5. build.sh

    Глава 7. Системные вызовы

    До сих пор, все что мы делали, это использовали ранее определенные механизмы ядра для регистрации файлов в файловой системе /proc и файлов устройств. Это все замечательно, но это годится только для создания драйверов устройств. А что если вы хотите сделать действительно что-то необычное, например изменить реакцию системы на какое либо событие?

    Мы как раз вступаем в ту область, где программирование действительно становится опасным. При разработке примера, приведенного ниже, я «уничтожил» системный вызов open. В результате система потеряла возможность открывать любые файлы, что равносильно отказу системы выполнять любые программы. Я не мог даже остановить систему командой shutdown. Из-за этого пришлось прибегнуть к «помощи» кнопки выключения питания. К счастью, ни один файл не был уничтожен. Чтобы обезопасить себя от потери данных, в аналогичных ситуациях, перед загрузкой подобных модулей всегда выполняйте резервное копирование.

    Забудьте про /proc, забудьте и про файлы устройств. Реальный механизм взаимодействия процессов с ядром — это системные вызовы. Когда процесс запрашивает какую-либо услугу ядра (например, открытие файла, запуск нового процесса или выделение дополнительной памяти), используется механизм системных вызовов. Если вы хотите изменить поведение ядра, то системные вызовы — это как раз то место, куда можно приложить свои знания и умения. Между прочим, если вы захотите увидеть — какие системные вызовы используются той или иной программой, запустите: strace .

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

    Системные вызовы являются исключением из этого правила. Чтобы исполнить системный вызов, процесс заполняет регистры микропроцессора соответствующими значениями и выполняет специальную инструкцию, которая производит переход в предопределенное место в пространстве ядра (разумеется, точка перехода доступна пользовательским процессам на чтение). Для платформы Intel — это инструкция прерывания с вектором 0x80. Микропроцессор воспринимает это как переход из ограниченного пользовательского режима в защищенный режим ядра, где позволено делать все, что вам заблагорассудится.

    Точка перехода, в ядре, называется system_call. Процедура, которая там находится, проверяет номер системного вызова, который сообщает ядру — какую именно услугу запрашивает процесс. Затем, она просматривает таблицу системных вызовов ( sys_call_table), отыскивает адрес функции ядра, которую следует вызвать, после чего вызывается нужная функция. По окончании работы системного вызова, выполняется ряд дополнительных проверок и лишь после этого управление возвращается вызывающему процессу (или другому процессу, если вызывающий процесс исчерпал свой квант времени). Код, выполняющий все вышеперечисленные действия, вы найдете в файле arch//kernel/entry.S, после строки ENTRY(system_call).

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

    Ниже приводится исходный текст такого модуля. Он «шпионит» за выбранным пользователем, и посылать через printk сообщение всякий раз, когда данный пользователь открывает какой-либо файл. Для этого, системный вызов open(), подменяется функцией с именем our_sys_open. Она проверяет UID (User ID) текущего процесса, и если он равен заданному, то вызывает printk, чтобы сообщить имя открываемого файла, и в заключение вызывает оригинальную функцию open() с теми же параметрами, которая открывает требуемый файл.

    Функция init_module изменяет соответствующий указатель в sys_call_table и сохраняет его первоначальное значение в переменной. Функция cleanup_module восстанавливает указатель в sys_call_table, используя эту переменную. В данном подходе кроются свои «подводные камни» из-за возможности существования двух модулей, перекрывающих один и тот же системный вызов. Представьте себе: имеется два модуля, А и B. Пусть модуль A перекрывает системный вызов open, своей функцией A_open, а модуль B — функцией B_open. Первым загружается модуль A, он заменяет системный вызов open на A_open. Затем загружается модуль B, который заменит системный вызов A_open на B_open. Модуль B полагает, что он подменил оригинальный системный вызов, хотя на самом деле был подменен вызов A_open.

    Теперь, если модуль B выгрузить первым, то ничего страшного не произойдет — он просто восстановит запись в таблице sys_call_table в значение A_open, который в свою очередь вызывает оригинальную функцию sys_open. Однако, если первым будет выгружен модуль А, а затем B, то система «рухнет». Модуль А восстановит адрес в sys_call_table, указывающий на оригинальную функцию sys_open, «отсекая» таким образом модуль B от обработки действий по открытию файлов. Затем, когда будет выгружен модуль B, он восстановит адрес в sys_call_table на тот, который запомнил сам, потому что он считает его оригинальным. Т.е. вызовы будут направлены в функцию A_open, которой уже нет в памяти. На первый взгляд, проблему можно решить, проверкой — совпадает ли адрес в sys_call_table с адресом нашей функции open и если не совпадает, то не восстанавливать значение этого вызова (таким образом B не будет «восстанавливать» системный вызов), но это порождает другую проблему. Когда выгружается модуль А, он «видит», что системный вызов был изменен на B_open и «отказывается» от восстановления указателя на sys_open. Теперь, функция B_open будет по прежнему пытаться вызывать A_open, которой больше не существует в памяти, так что система «рухнет» еще раньше — до удаления модуля B.

    Обратите внимание: подобные проблемы делают такую «подмену» системных вызовов неприменимой для широкого распространения.С целью предотвращения потенциальной опасности, связанной с подменой адресов системных вызовов, ядро более не экспортирует sys_call_table. Поэтому, если вы желаете сделать нечто большее, чем просто пробежать глазами по тексту данного примера, вам надлежит наложить «заплату» на ядро. В каталоге с примерами вы найдете файл README и «заплату». Как вы наверняка понимаете, подобные модификации сопряжены с определенными трудностями, поэтому я не рекомендую производить их на системах, владельцем которых вы не являетесь или не в состоянии быстро восстановить. Если вас одолевают сомнения, то лучшим выбором будет отказ от прогона этого примера.

    Пример 7-1. syscall.c

    Пример 7-2. «Заплата» на ядро (export_sys_call_table_patch_for_linux_2.6.x)

    Источник

    Оцените статью