Нестандартные сценарии использования модулей ядра
Часть 39. Поиск символов
Серия контента:
Этот контент является частью # из серии # статей: Нестандартные сценарии использования модулей ядра
Этот контент является частью серии:Нестандартные сценарии использования модулей ядра
Следите за выходом новых статей этой серии.
На протяжении предыдущих частей мы рассмотрели несколько примеров нетривиальных действий, которые можно выполнить из кода модуля: операции чтения-записи с файлами данных, запуск пользовательских приложений, работа с сигналами 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, если сможет сам считать их. В простейшем случае это могло бы выглядеть так:
- модуль должен считать файл /proc/kallsyms;
- найти в нём адрес интересующего его имени;
- воспользоваться найденным именем.
Правда, эта схема настолько поверхностна, как будет показано дальше, что, скорее, её можно рассматривать в качестве модели, объясняющей общий принцип. Хотя такая модель не очень эффективна и имеет более удачные альтернативы, но она очень хорошо поясняет суть, и поэтому мы начнём рассмотрение с неё. Полная реализация данного подхода приведена в файле 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 ...
Заключение
В этой части цикла мы приступили к рассмотрению многообещающей возможности поиска из кода модуля символов, непосредственно не экспортируемых ядром. И рассмотрение далеко идущих последствий этой возможности мы продолжим в следующих частях:
- найдём эффективные способы реализации данного подхода;
- рассмотрим, как найденные неэкспортируемые символы использовать в коде модуля.
Ресурсы для скачивания
- этот контент в PDF
- Образец кода (export.tgz | 4KB)
- Образец кода (call_table.tgz | 4KB)
Похожие темы
- Инструкция по работе с примерами исходного кода в цикле статей "Разработка модулей ядра Linux".
- Нестандартные сценарии использования модулей ядра: Часть 38. Работа с UNIX-сигналами.
- Нестандартные сценарии использования модулей ядра: Часть 40. Оптимальный подход к реализации поиска символов в ядре.
Комментарии
Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.