Состояние дел


Это обсуждение относится к ядру операционной системы Linux, и представляет интерес для разработчиков модулей ядра, драйверов под эту операционную систему. Для всех прочих эти заметки вряд ли представляют интерес.

Каждый, кто написал свой самый простейший модуль ядра Linux знает, и это написано во всех существующих книгах по технике написания драйверов Linux, что использовать в собственном коде модуля можно только те имена (главным образом это функции API ядра), которые экспортируются ядром. Это одно из самых путанных понятие из области ядра Linux — экспорт символов ядра. Для того, чтобы имя из пространства ядра было доступно для связывания в другом модуле, для этого имени должны выполняться два условия: а). имя должно иметь глобальную область видимости (в вашем модуле такие имена не должны объявляться static) и б). имя должно быть явно объявлено экспортируемым, оно должно быть явно записано параметром макроса EXPORT_SYMBOL (или EXPORT_SYMBOL_GPL, что далеко не одно и то же по последствиям).

Все имена, известные в ядре, динамически отображаются в псевдо-файле /proc/kallsyms, и число их огромно:

$ uname -r 
3.13.0-37-generic 
$ cat /proc/kallsyms | wc -l 
108960 

Число же экспортируемых ядром имён (предоставляемых для использования в программном коде модулей) значительно меньше:

$ cat /lib/modules/`uname -r`/build/Module.symvers | wc -l 
17533 

Как легко видеть, в ядре определено несколько сот тысяч имён (в зависимости от версии ядра). Но только малая часть (порядка 10%) этих имён объявлены как экспортируемые, и доступны для использования (связывания) в коде модулей ядра.

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

Как итог этого краткого экскурса, мы можем сформулировать, что разработчики ядра Linux предоставляют для разработчиков расширений (модулей ядра) весьма ограниченный (и крайне плохо документированный) набор API, который, по их мнению, достаточен для написания расширений ядра. Но это мнение может не совпадать с мнением самих разработчиков драйверов, которые хотели бы иметь в руках весь арсенал ядра. И воспользоваться ним вполне возможно, обсуждением чего мы и займёмся в оставшейся части текста.

Поиск адреса по имени


Взглянем на структуру строки-записи любого (из 108960 ) имени ядра в /proc/kallsyms:

$ sudo cat /proc/kallsyms | grep ' T ' | grep sys_close 
c1176ff0 T sys_close 

Это экспортируемое имя обработчика системного вызова (POSIX) close(). (В некоторых дистрибутивах Linux адреса в строке будут заполнены только если считывание выполняется с правами root, для других пользователей в поле адреса будет показано нулевое значение.)

Мы вполне могли бы использовать вызов функции sys_close() в коде своего модуля. Но мы не сможем сделать это с совершенно симметричным ему вызовом sys_open(), потому что это имя не экспортируется ядром. При сборке такого модуля мы получим предупреждение подобно следующему:

$ make 
...
  MODPOST 2 modules 
WARNING: "sys_open" [/home/olej/2011_WORK/LINUX-books/examples.DRAFT/sys_call_table/md_0o.ko] 
 undefined! 
... 

Но попытка загрузить такой модуль закончится неудачей:

$ sudo insmod md_0o.ko 
insmod: error inserting 'md_0o.ko': -1 Unknown symbol in module 
$ dmesg 
md_0o: Unknown symbol sys_open 

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

Означает ли показанное выше, что только экспортируемые символы ядра доступны в коде нашего модуля. Нет, это означает только, что рекомендуемый способ связывания по имени (по абсолютному адресу имени) применим только к экспортируемым именам. Экспортирование обеспечивает ещё один дополнительный рубеж контроля для обеспечения целостности ядра — минимальная некорректность приводит к полному краху операционной системы, иногда при этом она даже не успевает сделать сообщение: Oops…

Раз в псевдо-файле /proc/kallsyms отображаются все символы ядра, то код модуля мог бы взять их оттуда. Более того, это значит, что в API ядра есть методы локализации всех имён, и эти методы можно использовать в своём коде для этих же целей. Опуская путь промежуточных решений, рассмотрим только 2 варианта, 2 экспортируемых вызова (все определения в <linux/kallsyms.h> в ядре, или см. lxr.free-electrons.com/source/include/linux/kallsyms.h):

Вызов:

unsigned long kallsyms_lookup_name( const char *name );

Здесь name — имя которое мы ищем, а возвращается его абсолютный адрес. Недостаток этого варианта в том, что он появляется в ядре где-то между версиями ядра 2.6.32 и 2.6.35 (или примерно между пакетными дистрибутивами издания лета 2010г. и весны 2011г.), точнее он присутствовал и ранее, но не экспортировался. Для встраиваемых и малых систем это может стать серьёзным препятствием.

Более общий вызов:

int kallsyms_on_each_symbol( int (*fn)(void*, const char*, struct module*, unsigned long), void *data );

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

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

int func( void *data, const char *symb, struct module *mod, unsigned long addr ); 

Здесь:
data — блок параметров, заполненный в вызывающей единице, и переданный из вызова функции kallsyms_on_each_symbol() (2-й параметр вызова), как это описано выше, здесь, как раз, и хорошо передать имя того символа, который мы разыскиваем;
symb — символьное изображение (строка) имени из таблицы имён ядра, которое обрабатывается на текущем вызове func;
mod — модуль ядра, к которому относится обрабатываемый символ;
addr — адрес символа в адресном пространстве ядра (это, собственно, и есть то, что мы и ищем);

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

Для пользования вызовом kallsyms_on_each_symbol() мы подготовим собственную функцию обёртку, аналогичную по смыслу kallsyms_lookup_name():

static void* find_sym( const char *sym ) {  // find address kernel symbol sym 
   static unsigned long faddr = 0;          // static !!! 
   // ----------- nested functions are a GCC extension --------- 
   int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) { 
      if( 0 == strcmp( (char*)data, sym ) ) { 
         faddr = addr; 
         return 1; 
      } 
      else return 0; 
   }; 
   // -------------------------------------------------------- 
   kallsyms_on_each_symbol( symb_fn, (void*)sym ); 
   return (void*)faddr; 
} 

Здесь использован трюк с вложенным определением функции symb_fn(), что является совершенно легальным использованием расширения компилятора GCC (относительно стандарта языка C), но для компиляции модулей ядра мы используем исключительно GCC. Такой код позволяет избежать объявления глобальной промежуточной переменной, препятствует засорению пространства имён и способствует локализации кода.

Пример использования


Одним из самых сакральных мест в операционной системе Linux является селекторная таблица sys_call_table, через которую происходит любой системный вызов: подготовив предварительно соответствующим образом параметры, записав 1-м параметром номер (селектор) системного вызова, система выполняет команду перехода в ядро: int 80h (в старых версиях) или sysenter, что по существу одно и то же. Номер системного вызова (селектор, 1-й параметр) и является индексом в таблице sys_call_table (массиве) указателей на функции обработки системных вызовов ядром. Номера всех системных вызовов мы можем посмотреть, например, для архитектуры i386:

$ cat /usr/include/i386-linux-gnu/asm/unistd_32.h 
...
#define __NR_restart_syscall 0 
#define __NR_exit 1 
#define __NR_fork 2 
#define __NR_read 3 
#define __NR_write 4 
#define __NR_open 5 
#define __NR_close 6 
#define __NR_waitpid 7 
#define __NR_creat 8 
...

Здесь изображена таблица индексов (номеров) системных вызовов, используемая в адресном пространстве пользователя, реализуемая стандартной библиотекой C libc.so. Точный аналог этой таблицы присутствует и в заголовочных файлах ядра, в адресном пространстве ядра. И аналогичные таблицы индексов системных вызовов наличествуют для всех архитектур, поддерживаемых Linux (таблицы для разных архитектур различаются и размерностью, и составом, и численными значениями индексов для аналогичных вызовов!).

Начиная с версий 2.6 ядра символ sys_call_table был исключён из числа экспортируемых, исходя из весьма своеобразно понимаемых командой разработчиков ядра соображений защищённости (могу предположить, что защищённость здесь предполагалось толковать в смысле: защищённость куска хлеба разработчиков ядра от сторонних программистов). Все книги по написанию драйверов Linux утверждают, что использовать sys_call_table в коде драйвера невозможно. Сейчас, а ещё больше в последующих частях обсуждения, мы будем показывать что это не так!

Достаточно продолжительное время (с 2011 года) работая с обсуждаемой тематикой я перечитал множество публикаций на этот предмет. Вирусописатели и всякая прочая шваль, пугающие самих себя страшным словом хакер, чего только не выдумывали для поиска sys_cal_table — даже динамически декодируют дампы двоичных фрагментов памяти, занимаемых ядром, проделывая сканирование участков памяти ядра (в поисках, например, позиции sys_close(), которй экспортируется всегда). Как будет сейчас показано, всё это делается куда как проще. Только секрет устойчивости Linux состоит не в том. что пакостники не могут чего-то там найти, а в том, что регламентация прав доступа не позволит (без root прав) сделать никакие гадости за пределами этого регламента … а root права никто пакостникам не даёт.

Но вернёмся к задаче разрешения не экспортируемых символов ядра. Первый вариант (файл mod_kct.c) демонстрирует использование kallsyms_lookup_name() (для простоты и укорочения не показаны включение заголовочных файлов, необходимые макросы вида MODULE_*() … — всё это есть в файлах архива):

static int __init ksys_call_tbl_init( void ) { 
   void** sct = (void**)kallsyms_lookup_name( "sys_call_table" ); 
   printk( "+ sys_call_table address = %p\n", sct ); 
   if( sct ) { 
      int i; 
      char table[ 120 ] = "sys_call_table : "; 
      for( i = 0; i < 10; i++ ) 
         sprintf( table + strlen( table ), "%p ", sct[ i ] ); 
      printk( "+ %s ...\n", table ); 
   } 
   return -EPERM; 
} 
module_init( ksys_call_tbl_init ); 

Здесь извлекается адрес таблицы sys_call_table и далее содержащиеся в ней адреса обработчиков первых 10-ти системных вызовов (__NR_restart_syscall … __NR_link):

$ sudo insmod mod_kct.ko 
insmod: ERROR: could not insert module mod_kct.ko: Operation not permitted 
$ dmesg | tail -n 2 
[39473.496040] + sys_call_table address = c1666140 
[39473.496045] + sys_call_table : c1067840 c1059280 c1055eb0 c1179ee0 c1179f70 c1178cb0 c1176ff0 c1059570 c1178d10 c1188860  ... 

(Ошибка 'Operation not permitted ' не должна смущать — мы и не собирались загружать модуль, на что и указывает ненулевой код возврата -EPERM, мы просто выполняем свой код в привилегированном режиме, супервизоре, нулевом кольце защиты процессора).

Удостоверимся, чему соответствуют найденные адреса, занесённые в начало массива sys_call_table:

$ sudo cat /proc/kallsyms | grep c1067840 
c1067840 T sys_restart_syscall 
$ sudo cat /proc/kallsyms | grep c1059280 
c1059280 T SyS_exit 
c1059280 T sys_exit 
$ sudo cat /proc/kallsyms | grep c1055eb0 
c1055eb0 T sys_fork 

… ну и так далее (сравните с таблицей номеров системных вызовов, показанной ранее).

Следующий вариант будет чуть сложнее для понимания, он использует функцию kallsyms_on_each_symbol(), но он и более универсальный (файл mod_koes.c):

static int __init ksys_call_tbl_init( void ) { 
   void **sct = find_sym( "sys_call_table" );   // table sys_call_table address 
   printk( "+ sys_call_table address = %p\n", sct ); 
   if( sct != NULL ) { 
      int i; 
      char table[ 120 ] = "sys_call_table : "; 
      for( i = 0; i < 10; i++ ) 
         sprintf( table + strlen( table ), "%p ", sct[ i ] ); 
      printk( "+ %s ...\n", table ); 
   } 
   return -EPERM; 
} 
module_init( ksys_call_tbl_init ); 

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

$ sudo insmod mod_koes.ko 
insmod: ERROR: could not insert module mod_koes.ko: Operation not permitted 
$ dmesg | tail -n2 
[42451.186648] + sys_call_table address = c1666140 
[42451.186654] + sys_call_table : c1067840 c1059280 c1055eb0 c1179ee0 c1179f70 c1178cb0 c1176ff0 c1059570 c1178d10 c1188860  ... 


Обсуждение


Скептик может возразить: «Ну и что?». А то, что показаны необходимые и достаточные механизмы для того, чтобы использовать любые API ядра в собственно коде модулей ядра, подгружаемых динамически. Показанная техника расширяет спектр возможностей автора модуля ядра на порядки! Это настолько объёмные перспективы, что для их рассмотрения нам потребуются последующие части этого обсуждения.

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

Вам говорили, что код модуля ядра осуществляет вывод в системный журнал (printk()) и не может осуществлять вывод на терминал (printf())? Сейчас мы покажем, что это не так… Вот такой простой модуль ядра производит вывод на терминал:

static asmlinkage long (*sys_write) ( 
   unsigned int, const char __user *, size_t ); 

static int __init wr_init( void ) { 
   char buf[ 80 ] = "Hello from kernel!\n"; 
   int len = strlen( buf ), n; 
   sys_write = find_sym( "sys_write" ); 
   printk( "+ sys_write address = %p\n", sys_write ); 
   printk( "+ [%d]: %s", len, buf ); 
   if( sys_write != NULL ) { 
      mm_segment_t fs = get_fs(); 
      set_fs( get_ds() ); 
      n = sys_write( 1, buf, len ); 
      set_fs( fs ); 
      printk( "+ printf() return : %d\n", n ); 
   } 
   return -EPERM; 
} 

module_init( wr_init ); 

А вот его исполнение (попытка загрузки с аварийным кодом завершения):

$ sudo insmod mod_wrc.ko 
Hello from kernel! 
insmod: ERROR: could not insert module mod_wrc.ko: Operation not permitted 
$ dmesg | tail -n3 
[23942.974587] + sys_write address = c1179f70 
[23942.974591] + [19]: Hello from kernel! 
[23942.974612] + printf() return : 19 

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

Откуда я взял такой «хитрый» прототип описания адресной переменной sys_write? Конечно, я бессовестно списал его из оригинального определения функции sys_write() в ядре, в заголовочном файле <linux/syscalls.h>, что и показано комментарием в коде (в полном коде, в архиве):

/* <linux/syscalls.h> 
asmlinkage long sys_write( unsigned int fd, 
                           const char __user *buf, 
                           size_t count ); */ 

И только так следует поступать для всех используемых не экспортируемых имён ядра — списывая прототипы реализующих функций из соответствующих заголовочных файлов. Любое минимальное несоответствие прототипа приведёт к немедленному краху операционной системы!

Что означают несколько похожих вызовов вида: get_ds(), get_fs(), set_fs()? Это небольшой трюк, состоящий во временной подмене сегментов данных в ядре. Дело в том, что в прототипе обработчика системного вызова sys_write() стоит квалификатор __user, показывающий что указатель указывает на данные в пространстве пользователя. Код системного вызова проверяет принадлежность (только диапазону численного значения адреса), и если адрес указывает на область пространства ядра (как в нашем случае) вызовет аварийное завершение. Таким трюком мы показываем контролирующему коду, что наш адрес следует толковать как принадлежащий к пространству пользователя. В подобных случаях этот трюк можно использовать механически, не особенно задумываясь над его смыслом.

Примечания


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

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

С удовлетворением могу констатировать, что все обсуждаемые коды адекватно выполняются в виртуальных машинах в среде Oracle VirtualBox, по крайней мере, в относительно последних версиях, начиная от состояния 2013 года.

Поэтому настоятельно рекомендую работать с такими кодами первоначально в виртуальных машинах, дабы избежать серьёзных неприятностей.
Упоминание Oracle VirtualBox вовсе не означает, что это состояние дел не будет сохраняться в других менеджерах виртуальных машин, просто я не проверял коды в этих менеджерах (почти наверняка всё будет благополучно в QEMU/KVM, поскольку VirtualBox заимствует код виртуализации из QEMU).

Архив файлов (кодов) для экспериментов, который упоминается в тексте, можно взять здесь или здесь.

Комментарии (28)


  1. martin74ua
    23.09.2015 15:17

    Ещё хуже то, что система заваливается в неопределённом неустойчивом состоянии, и существует конечная (не высокая) вероятность того, что система не восстановится и после перезагрузки.


    Простите, а что имеется в виду???


    1. Olej
      23.09.2015 16:05
      +1

      Я не могу вам рассказать абсолютно конкретно, но несколько (2-3) раз попадал в такую ситуацию
      Это всё равно что вы вырубаете питание рубильником. Если система что-то пишет в это время на диск, сохраняет какие-то состояния и конфигурации, то после такого вырубания (без сброса кэшей на диск и т.п.) то, что там будет — непредсказуемо.


      1. martin74ua
        23.09.2015 16:10

        А, фух… Обычный fsck лечит ;) я уж думал все, неизлечимо ;)


        1. Olej
          23.09.2015 16:16

          Лечит или не лечит — это уже как карта ляжет.


      1. amarao
        23.09.2015 20:38

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


        1. Olej
          23.09.2015 22:00

          Я как-раз имел в виду порчу каких-то данных (внутри файловой системы), критических для системы, а не нарушение елостности структуры самой файловой системы.
          Но всё это не имеет, собственно, прямого касательства к расматриваемому предмету.


  1. jcmvbkbc
    23.09.2015 16:11
    +1

    Показанная техника расширяет спектр возможностей автора модуля ядра на порядки!

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


    1. Olej
      23.09.2015 16:15

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


      1. jcmvbkbc
        23.09.2015 16:59
        +1

        Даже интересно, как вы защититесь от изменений API, изменений прототипов и семантики функций?


        1. Olej
          23.09.2015 19:33

          Кончно же что-то типа:

          #if LINUX_VERSION_CODE < KERNEL_VERSION(3,10,0)
          

          Или вы знаете другой способ?


          1. jcmvbkbc
            23.09.2015 20:05

            Или вы знаете другой способ?

            Знаю: заслать драйвер в ядро и добиться его включения.


            1. Olej
              23.09.2015 20:48

              Знаю:

              Это не решение, а так… отговорка:
              1. «засланные» модули все пестрят LINUX_VERSION_CODE — как поле брани телами погибших…
              2. это хороший совет, когда вы пишете код… ночами и на коленке, в порядке хобби… удовлетворения либидо; если же это промышленный проект, выполняемый в рамках штатнго расписаия, то на это никто не даст разрешения, да и времени на это нет;
              3. и тем более, если это какая-то частная, специальная разработка, и вовсе даже не драйвер (в традиционном смысле), а модуль… некоторых «специальных эффектов», который для команды ядра представлять интереса не может, потому что просто не стыкуется с их философией.


              1. jcmvbkbc
                23.09.2015 21:13

                1. «засланные» модули все пестрят LINUX_VERSION_CODE — как поле брани телами погибших…

                Ну неправда же. Если драйвер пишут с нуля или портируют с прицелом на мэйнлайн, то там всё вылизано. Если нет — мейнтейнер обязательно потыкает в очевидное носом. Так бывает когда разработчик сидит 10 лет на своих исходниках, а потом решает их опубликовать. Но тогда им дорога в staging.
                2. это хороший совет, когда вы пишете код… ночами и на коленке, в порядке хобби… удовлетворения либидо; если же это промышленный проект, выполняемый в рамках штатнго расписаия, то на это никто не даст разрешения, да и времени на это нет;

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


                1. Olej
                  24.09.2015 09:06

                  Ну неправда же.

                  Ну, здесь я увлёкся и неправ.
                  Имел в виду великое множество проектов, совершенно открытых и свободных, которые годами сопровождают свои изделия, и которыми пользуется весь мир:
                  — чипы Broadcom
                  — чипы Ralink для WiFi
                  — проект Zaptel/DAHDI, который является интерфейсом к десяткам моделей разнородного оборудования и единственным интерфейсом, используемым всеми софтверными PBX: Asterisk, FreeSWITCH, YATE; но это вообще не драйвер в общепринятом смысле — там ведётся активная обработка потока в режиме ядра, например, нижний уровень сигнализации SS7.

                  Это только малая часть, названная только для того, чтобы было понятно о чём речь. Все они прекрасно справляются с поддержкой своих проектов ещё с версии ядра даже 2.4, используя LINUX_VERSION_CODE.
                  Это не говоря уже про проприетарые или полу-проприетарные проекты, которые имеют точно такое же право быть:
                  — VirtualBox
                  — NVIDIA со своими драйверами и поддержкой CUDA.

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

                  И закончим на этом, потому что публикация и обсуждение совершенно о другом.


            1. Olej
              23.09.2015 20:59
              -1

              заслать драйвер в ядро и добиться его включения.

              Это как-раз и есть расплата за монолитность ядра в противовес микроядерному, о чём так увлечённо спорили Торвальдс с Таненбаумом. Но эта тема уведёт нас слишком далеко… от моих, по крайней мере, намерений.


  1. Vorb
    23.09.2015 18:51

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


    1. Olej
      23.09.2015 19:43

      Вы спешите…
      Если бы вы были чуть проницательнее, ил читали чуть размереннее и внимательнее, то заподозрили бы, что меня интересует, в подавляющем большинстве случаев, sys_call_table.
      Спешите…

      ну зачем могут понадобиться файловые операции из ядра?

      Зачем?
      Ну, например, для чтения своих конфигурационных параметров из /etc/…, или записи некоторых специфических логов.
      Плюс — теряется совместимость между разными версиями ядра,

      Объясните (смоделируйте, опишите ситуацию) почему прототипы неэкспортируемых функций, например, должны меняться чаще, чем экспортируемых… только конкретно, на примерах, без… «из сображений банальной эрудиции»…


      1. jcmvbkbc
        23.09.2015 20:03

        Объясните (смоделируйте, опишите ситуацию) почему прототипы неэкспортируемых функций, например, должны меняться чаще, чем экспортируемых… только конкретно, на примерах, без… «из сображений банальной эрудиции»…

        Потому что прототипы меняются, чаще всего, когда кто-то делает рефакторинг (или переписывает всё). Тот, кто делает рефакторинг видит экспортированные функции и знает, что кроме модулей входящих в состав ядра, могут быть и внешние модули. И даже это его в большинстве случаев не остановит от того, чтобы поменять прототип или семантику. С неэкспортируемыми функциями вообще никто не парится.
        Вот официальная политика ядра: www.kernel.org/doc/Documentation/stable_api_nonsense.txt
        И на вопрос «что делать, чтобы модуль не ломался с каждой новой версией ядра» она даёт ответ: «Simple, get your kernel driver into the main kernel tree.… If your driver is in the tree, and a kernel interface changes, it will be fixed up by the person who did the kernel change in the first place. This ensures that your driver is always buildable, and works over time, with very little effort on your part.».


        1. Olej
          23.09.2015 20:32

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

          Это всё сображения «с точки зрения банальной эрудиции»… может быть, а может и не быть…
          Но опыт показывает, что API экспортируемых функций меняется произвольно, и часто, и даже чаще, чем многие не экспортируемые. А чтобы мои утверждения не выглядели так же голословно (может быть, а может и не быть) я приведу некоторые (на самом дел их гораздо больше), из самых последних:
          — версия 3.9 — меняется API WiFi
          — версия 3.12 — меняется полостью API драйверов блочных устройств;
          — версия 3.15 — меняется весь API, отвечающий за работу poll, select;
          — версия 3.17 — меняется API сетевого стека;
          — версия 3.17 — снова меняется API WiFi
          Это не изменения в реализации названных подсистем. Это изменения в экспортируемых API, которые делают модули, работающие до сих пор с этими подсистемами, не компилирующимися.


      1. Vorb
        23.09.2015 22:22

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


        1. Olej
          23.09.2015 22:33

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

          Не видел — это не обязательно означает, что такое невозможно.


          1. Vorb
            23.09.2015 22:36

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


            1. Olej
              23.09.2015 22:53

              Можете привести пример, когда штатного modprobe, /proc и т.д.

              Ну, например, когда в ядре 3.10 все модули, использующие API procfs (описанный во всех книгах, LDD3 и т.д. и т.п.) единомоментно перестали компилироваться «на дух» ;-).

              А вообще… Мне довольно часто приходилось кого-то чему-то обучать (хотя никогда штатным преподавателем не был). И за многие годы такой практики я выработал такое правило: в программировании (любом!) самое бессмысленное занятие — спрашивать «зачем», единственный осмысленный вопрос — «как». Это касается любых аспектов: языков программирования, трюков на этих языках… и всё-всё-всё: если какая-то фича тебе не нравится — не используй.
              Как там говорилось?:
              — Что-то меня беспокоит Гондурас!
              — Беспокоит? А ты его не чеши…



              1. Vorb
                23.09.2015 22:56

                Слабый пример)
                Конечно, главной вопрос «как». Но ответ явно должен отображать оптимальное и обдуманное решение, а не «через задницу».
                Ладно, пустой треп. Спасибо за статью!


      1. niamster
        24.09.2015 00:45

        Зачем вам syscall table? filp_open, vfs_read уже не в моде?
        Я применял kallsyms_lookup_name только когда писал модуль установки HW WP на уровне ядра(на то время такого финта не существовало, да и сейчас скорее всего нет такой фишки). Другого применения не вижу.


        1. Olej
          24.09.2015 01:29

          Зачем вам syscall table?

          Вот когда мы дойдём до sys_call_table, вот тогда и посмотрим зачем.

          Другого применения не вижу.


          — Ты видишь суслика? И я нет. А он есть!


  1. Olej
    23.09.2015 20:19

    В дополнение к тексту… нужны, как кажется по комментариям, некоторые дополнения, которые не включались в текст из нежелания раздувать и так немалый объём для короткой заметки.
    Необходимость кодирования для ядра Linux возникает у программистов 2-х категорий: а). коммитеров, добавляющих (пытающихся) свои коммиты в дерево кодов ядра и б). разработчики автономных проектов, в рамках которых возникают опросы создания модулей ядра (или драйверов, как их простейшей формы). Но коммитеры предпочитают статические изменения в ядре, которые вносятся патчем с последующей сборкой ядра. Меня же интересовали, главным образом, потребности практикующих разработчики, которым требуется динамически внести изменения в функционирование отдельных ветвей ядра (сетевые протоколы, например).
    Я не хочу обсуждать здесь вопрос зачем. Сконцентрируемся на вопроса как. Некоторую ясность в вопрос зачем добавят, возможно и не сняв его полностью, последующие части бсуждения (не зря в заголовке написано: часть 1).
    Очень много попутных вопросов-ответов, аргументов-возражений и т.д. обкатывались на протяжении последих лет 5-ти в темах, обсуждаемых а форуме Linux изнутри. Я не могу все их обсудить ни в комментариях ни в тексте, но их намётки можно найти в этом форуме. Кроме того, итогом почти 10-ти летней работы с модулями вылились в учебный курс, выполенный на заказ, и тренинги по которому были проведен с 5-ю различными группами (в разных городах-филиалах) разработчиков одной из крупнейших международных софтверных компаний. Полный текст (>400 страниц) и архив из доброй сотни модулей для самых разнообразных случаев можно свободно взять (всё под лицензий Creative Commons Attribution ShareAlike) в моём блоге: Практикум по Linux Kernel.
    Вот такое пространное объяснение мне показалось необходимым к тому, чтобы в комментариях мы обсуждали только вещи, относящиеся к публикации. Просто потому что:

    Нельзя объять необъятное.

    (с) Козьма Прутков.


  1. zim32
    07.10.2015 21:35

    Огромное спасибо. Теперь не надо копипастой заниматься