Содержание


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

Часть 39. Поиск символов

Comments

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

Этот контент является частью # из серии # статей: Нестандартные сценарии использования модулей ядра

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

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

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

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

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

Об экспорте символов ядра

Экспорт символов — это один из самых сложных аспектов функционирования ядра. Для того, чтобы имя из пространства ядра можно было «связать» в модуле, должны выполняться два условия:

  • имя должно иметь глобальную область видимости (в коде такие имена не должны объявляться как static);
  • имя должно быть явно объявлено экспортируемым, т.е. записано параметром макровызова EXPORT_SYMBOL (или EXPORT_SYMBOL_GPL).

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

$ cat /proc/kallsyms | grep ' T ' | grep sys_open 
c04deb28 T do_sys_open 
c04dec0c T sys_openat 
c04dec35 T sys_open 
$ cat /proc/kallsyms | grep ' T ' | grep sys_close 
c04dea99 T sys_close

Оба имени sys_open и sys_close известны в таблице символов ядра как глобальные имена в секции кода (T). Подготовим простейший модуль ядра, исходный код которого можно найти в файле md_0c.c в архиве export.tgz в разделе "Материалы для скачивания".

Листинг 1. Экспортируемый символ sys_close
#include <linux/module.h> 
extern int sys_close( int fd ); 
static int __init sys_init( void ) { 
   void* Addr; 
   Addr = (void*)sys_close; 
   printk( "sys_close address: %p\n", Addr ); 
   return -1; 
} 
module_init( sys_init );

Проверим, что у нас получилось.

$ sudo insmod md_0c.ko 
insmod: error inserting 'md_0c.ko': -1 Operation not permitted 
$ dmesg 
sys_close address: c04dea99

Всё сработало, как и ожидалось: адрес обработчика системного вызова sys_close (экспортированный ядром и полученный в ходе выполнения) совпадает со значением, прочитанным ранее из /proc/kallsyms. Теперь подготовим аналогичный модуль md_0o.c для обработки симметричного системного вызова sys_open.

Листинг 2. Неэкспортируемый символ sys_open.
#include <linux/module.h> 
extern int sys_open( int fd ); 
static int __init sys_init( void ) { 
   void* Addr; 
   Addr = (void*)sys_open; 
   printk( KERN_INFO "sys_open address: %p\n", Addr ); 
   return -1; 
} 
module_init( sys_init );

Прототип sys_open(), описанный в листинге 2, не соответствует реальному формату вызова обработчика системного вызова для 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

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

$ cat /proc/kallsyms | wc -l 
69698

Информацию об экспортируемых символах можно найти здесь:

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

Каждая строка в файле Module.symvers соответствует описанию одного экспортируемого символа и имеет следующий формат:

  • имя символа (2-я колонка);
  • модуль, который экспортирует символ, с указанием пути к файлу модуля, или vmlinux, если символ экспортируется непосредственно ядром;
  • тип экспортирования, например, EXPORT_SYMBOL или EXPORT_SYMBOL_GPL.
$ cat /lib/modules/`uname -r`/build/Module.symvers | grep sys_
0x00000000	sys_close	vmlinux	EXPORT_SYMBOL 
0x00000000	sys_copyarea	vmlinux	EXPORT_SYMBOL 
0x00000000	fb_sys_write	vmlinux	EXPORT_SYMBOL_GPL 
0x00000000	nfnetlink_subsys_register	net/netfilter/nfnetlink	EXPORT_SYMBOL_GPL 
...

Как было показано выше, число экспортируемых символов почти на порядок (69698:9594 ~ 10:1) меньше общего числа имён ядра, например, sys_close экспортируется самим ядром (vmlinux), а sys_open — нет. Но в качестве источника экспортируемых символов могут выступать и модули (net/netfilter/nfnetlink).

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

Неэкспортируемые символы ядра

Означает ли показанное выше, что для нашего модуля доступны только экспортируемые символы ядра? Нет, это означает только, что рекомендуемый способ связывания по имени применим только к экспортируемым именам. Экспортирование обеспечивает дополнительный уровень контроля для обеспечения целостности ядра, так как минимальная некорректность приводит к полному краху операционной системы, иногда при этом она даже не успевает вывести финальное сообщение. Особенно это относится к модулям, подобным тем, к рассмотрению которых мы приступаем. Файл call_table.tgz с исходным кодом модулей можно найти в разделе "Материалы для скачивания".

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

  1. модуль должен считать файл /proc/kallsyms;
  2. найти в нём адрес интересующего его имени;
  3. воспользоваться найденным именем.

Правда, эта схема настолько поверхностна, как будет показано дальше, что, скорее, её можно рассматривать в качестве модели, объясняющей общий принцип. Хотя такая модель не очень эффективна и имеет более удачные альтернативы, но она очень хорошо поясняет суть, и поэтому мы начнём рассмотрение с неё. Полная реализация данного подхода приведена в файле mod_rct.c, который можно найти в архиве call_table.tgz. В листинге приводится только центральная часть кода, считывающая и перебирающая символы ядра из /proc/kallsyms в поисках символа sys_call_table (адрес таблицы системных вызовов).

Листинг 3. Перебор имён ядра из файла /proc/kallsyms.
...
static char* file = "/proc/kallsyms";
...
    char buff[ BUF_LEN + 1 ] = ""; 
    f = filp_open( file, O_RDONLY, 0 ); 
    while( 1 ) { 
       char *p = buff, *find; 
       int k; 
       *p = '\0'; 
       do { 
           if( ( k = kernel_read( f, n, p++, 1 ) ) < 0 ) { 
               printk( "+ failed to read\n" ); 
               return -EIO; 
           }; 
           *p = '\0'; 
           if( 0 == k ) break; 
           n += k; 
           if( '\n' == *( p - 1 ) ) break; 
       } while( 1 ); 
       if( ( debug != 0 ) && ( strlen( buff ) > 0 ) ) { 
           if( '\n' == buff[ strlen( buff ) - 1 ] ) printk( "+ %s", buff ); 
           else printk( "+ %s|\n", buff ); 
       } 
       if( 0 == k ) break;   // EOF 
       
       if( NULL == ( find = strstr( buff, "sys_call_table" ) ) ) continue; 
       put_table( buff ); 
    } 
   printk( "+ close file: %s\n", file ); 
    filp_close( f, NULL ); 
...

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

$ time sudo insmod mod_rct.ko 
insmod: error inserting 'mod_rct.ko': -1 Operation not permitted 
real	0m0.728s 
user	0m0.003s 
sys	0m0.719s 
$ dmesg | tail -n4 
[57478.736476] + openning file: /proc/kallsyms 
[57478.912136] + sys_call_table address = c07c2438 
[57478.912140] + sys_call_table : c044e80c c0443af8 c0408a04 
c04e39e3 c04e3a45 c04e2e59 c04e1e59 c0443dce c04e2ead c04ed654  ... 
[57479.453508] + close file: /proc/kallsyms

Формат вывода dmesg и все адреса обработчиков в таблице sys_call_table отличаются от показанных ранее, так как эта демонстрация производится на другой версии ядра (причину использования нескольких версий ядра мы разберём позже). Помимо адреса таблицы sys_call_table модуль выводит для контроля 10 первых точек входа этой таблицы. Проверим некоторые из этих адресов обратным поиском в /proc/kallsyms:

$ cat /proc/kallsyms | grep c044e80c 
c044e80c T sys_restart_syscall 
$ cat /proc/kallsyms | grep c0443af8 
c0443af8 T sys_exit 
$ cat /proc/kallsyms | grep c0408a04 
c0408a04 t ptregs_fork 
$ cat /proc/kallsyms | grep c04e39e3 
c04e39e3 T sys_read 
$ cat /proc/kallsyms | grep c04e3a45 
c04e3a45 T sys_write 
...

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

$ cat /usr/include/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 
...

Заключение

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

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

Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=856074
ArticleTitle=Нестандартные сценарии использования модулей ядра: Часть 39. Поиск символов
publish-date=01242013