Представьте, что у вас имеется образ ядра Linux для телефона на базе Android, но вы не располагаете ни соответствующими исходниками, ни заголовочными файлами ядра. Представьте, что ядро имеет поддержку подгрузки модулей (к счастью), и вы хотите собрать модуль для данного ядра. Существует несколько хороших причин, почему нельзя просто собрать новое ядро из исходников и просто закончить на том (например, в собранном ядре отсутствует поддержка какого-нибудь важного устройства, вроде LCD или тачскрина). С постоянно меняющимся ABI ядра Linux и отсутствием исходников и заголовочных файлов, вы можете подумать, что окончательно зашли в тупик.
Как констатация факта, если вы соберете модуль ядра, используя другие заголовочные файлы (нежели те, что были использованы для сборки того образа ядра, которым вы располагаете, — прим. пер.), модуль не сможет загрузиться с ошибками, зависящими от того, насколько заголовочные файлы отличались от требуемых. Он может жаловаться о плохих сигнатурах, плохих версиях и о прочих вещах.
Но больше об этом далее.
Конфигурация ядра
Первый шаг — найти исходники ядра наиболее близкие к тому образу ядра, насколько это возможно. Наверное, получение правильной конфигурации — наиболее сложная составляющая всего процесса сборки модуля. Начните с того номера версии ядра, который может быть прочитан из
/proc/version
. Если, как я, вы собираете модуль для устройства Android, попробуйте ядра Android от Code Aurora, Cyanogen или Android, те, что наиболее ближе к вашему устройству. В моем случае, это было ядро msm-3.0. Заметьте, вам не обязательно необходимо искать в точности ту же версию исходников, что и версия вашего образа ядра. Небольшие отличия версии, наиболее вероятно, не станут помехой. Я использовал исходники ядра 3.0.21, в то время как версия имеющегося образа ядра была 3.0.8. Не пытайтесь, однако, использовать исходники ядра 3.1, если у вас образ ядра 3.0.x. Если образ ядра, что у вас есть, достаточно любезен, чтобы предоставить файл
/proc/config.gz
, вы можете начать с этого, в противном случае, вы можете попытаться начать с конфигурацией по умолчанию, но в этом случае нужно быть крайне аккуратным (хотя я и не буду углубляться в детали использования дефолтной конфигурации, поскольку мне посчастливилось не прибегать к этому, далее будут некоторые детали относительно того, почему правильная конфигурация настолько важна).Предполагая, что
arm-eabi-gcc
у вас доступен по одному из путей в переменной окружения PATH, и что терминал открыт в папке с исходными файлами ядра, вы можете начать конфигурацию ядра и установку заголовочных файлов и скриптов:$ mkdir build
$ gunzip config.gz > build/.config # или что угодно, для того, чтобы приготовить .config
$ make silentoldconfig prepare headers_install scripts ARCH=arm CROSS_COMPILE=arm-eabi- O=build KERNELRELEASE=`adb shell uname -r`
Сборка
silentoldconfig
, наиболее вероятно, спросит, хотите ли вы включить те или иные опции. Вы можете выбрать умолчания, но это вполне может и не сработать.Можно использовать что-нибудь другое в
KERNELRELEASE
, однако это должно совпадать в точности с версией ядра, с которого вы планируете подгружать модуль.Написание простого модуля
Чтобы создать пустой модуль, необходимо создать два файла: исходник и
Makefile
. Расположите следующий код в файле hello.c
, в некоторой отдельной директории:#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h> /* Needed for the macros */
static int __init hello_start(void)
{
printk(KERN_INFO "Hello world\n");
return 0;
}
static void __exit hello_end(void)
{
printk(KERN_INFO "Goodbye world\n");
}
module_init(hello_start);
module_exit(hello_end);
Поместите следующий текст в файл
Makefile
в той же директории:obj-m = hello.o
Сборка модуля достаточна проста, однако на данном этапе полученный модуль не сможет загрузиться.
Сборка модуля
При обычной сборки ядра система сборки ядра создает файл
hello.mod.c
, содержимое которого может создать различные проблемы:MODULE_INFO(vermagic, VERMAGIC_STRING);
Значение
VERMAGIC_STRING
определяется макросом UTS_RELEASE
, который располагается в файле include/generated/utsrelease.h
, генерируемом системой сборки ядра. По умолчанию, это значение определяется версией ядра и статуса git-репозитория. Это то, что устанавливает KERNELRELEASE
при конфигурации ядра. Если VERMAGIC_STRING
не совпадает с версией ядра, загрузка модуля приведет к сообщению подобного рода в dmesg
:hello: version magic '3.0.21-perf-ge728813-00399-gd5fa0c9' should be '3.0.8-perf'
Далее, также имеем здесь определение структуры модуля:
struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
Само по себе, это определение выглядит безобидно, но структура
struct module
, определенная в include/linux/module.h
, несет в себе неприятный сюрприз:struct module
{
(...)
#ifdef CONFIG_UNUSED_SYMBOLS
(...)
#endif
(...)
/* Startup function. */
int (*init)(void);
(...)
#ifdef CONFIG_GENERIC_BUG
(...)
#endif
#ifdef CONFIG_KALLSYMS
(...)
#endif
(...)
(... plenty more ifdefs ...)
#ifdef CONFIG_MODULE_UNLOAD
(...)
/* Destruction function. */
void (*exit)(void);
(...)
#endif
(...)
}
Это означает, что для того, чтобы указатель
init
оказался в правильном месте, CONFIG_UNUSED_SYMBOLS
должен быть определен в соответствии с тем, что использует наш образ ядра. Что же насчет указателя exit, — это CONFIG_GENERIC_BUG
, CONFIG_KALLSYMS
, CONFIG_SMP
, CONFIG_TRACEPOINTS
, CONFIG_JUMP_LABEL
, CONFIG_TRACING
, CONFIG_EVENT_TRACING
, CONFIG_FTRACE_MCOUNT_RECORD
и CONFIG_MODULE_UNLOAD
. Начинаете понимать, почему обычно предполагается использовать в точности те же заголовочные файлы, с которыми было собрано наше ядро?
Далее, определения версий символов:
static const struct modversion_info ____versions[]
__used
__attribute__((section("__versions"))) = {
{ 0xsomehex, "module_layout" },
{ 0xsomehex, "__aeabi_unwind_cpp_pr0" },
{ 0xsomehex, "printk" },
};
Эти определения берутся из файла
Module.symvers
, который генеруется в соответствии с заголовочными файлами. Каждая такая запись представляет символ, требуемый модулю, и то, какую сигнатуру должен иметь символ. Первый символ,
module_layout
, зависит от того, как выглядит struct module
, то есть, зависит от того, какие опции конфигурации, упомянутые ранее, включены. Второй, __aeabi_unwind_cpp_pr0
, — функция, специфичная ABI ARM, и последний — для наших вызовов функции printk
.Сигнатура каждого символа может отличаться в зависимости от кода ядра для данной функции и компилятора, использованного для сборки ядра. Это означает, что если вы соберете ядро из исходников, а также модули для данного ядра, и затем повторно соберете ядро после модификации, например, функции
printk
, даже совместимым путем, модули, собранные изначально, не загрузятся с новым ядром.Так, если мы соберем ядро с исходниками и конфигурацией, достаточно близкими к тем, при помощи которых был собран имеющийся у нас образ ядра, есть шанс того, что мы не получим те же самые сигнатуры, что и в нашем образе ядра, и оно ругнулось бы при загрузке модуля:
hello: disagrees about version of symbol symbol_name
Что значит, что нам нужен правильный, соответствующий образу ядра, файл
Module.symvers
, которым мы не располагаем. Изучаем ядро
Поскольку ядро делает эти проверки при загрузке модулей, оно также содержит список символов, которые экспортирует и соответствующие сигнатуры. Когда ядро загружает модуль, оно проходит по всем символам, которые требуются модулю, для того, чтобы найти их в своей таблице символов (или прочих таблицах символов модулей, которые использует данный модуль) и проверить соответствующие сигнатуры.
Ядро использует следующую функцию для поиска в своей таблицы символов (в kernel/module.c):
bool each_symbol_section(bool (*fn)(const struct symsearch *arr,
struct module *owner,
void *data),
void *data)
{
struct module *mod;
static const struct symsearch arr[] = {
{ __start___ksymtab, __stop___ksymtab, __start___kcrctab,
NOT_GPL_ONLY, false },
{ __start___ksymtab_gpl, __stop___ksymtab_gpl,
__start___kcrctab_gpl,
GPL_ONLY, false },
{ __start___ksymtab_gpl_future, __stop___ksymtab_gpl_future,
__start___kcrctab_gpl_future,
WILL_BE_GPL_ONLY, false },
#ifdef CONFIG_UNUSED_SYMBOLS
{ __start___ksymtab_unused, __stop___ksymtab_unused,
__start___kcrctab_unused,
NOT_GPL_ONLY, true },
{ __start___ksymtab_unused_gpl, __stop___ksymtab_unused_gpl,
__start___kcrctab_unused_gpl,
GPL_ONLY, true },
#endif
};
if (each_symbol_in_section(arr, ARRAY_SIZE(arr), NULL, fn, data))
return true;
(...)
Структура, используемая в данной функции, определена в include/linux/module.h:
struct symsearch {
const struct kernel_symbol *start, *stop;
const unsigned long *crcs;
enum {
NOT_GPL_ONLY,
GPL_ONLY,
WILL_BE_GPL_ONLY,
} licence;
bool unused;
};
Примечание: данный код ядра не изменился значительно за последние четыре года (видимо, с момента рассматриваемого релиза ядра 3.0, — прим. пер.).
То, что мы имеем выше в функции
each_symbol_section
— три (или пять, когда конфиг CONFIG_UNUSED_SYMBOLS
включен) поля, каждое из которых содержит начало таблицы символов, ее конец и два флага.Данные эти статичны и постоянны, что означает, что они появятся в бинарнике ядра как есть. Сканируя ядро на предмет трех идущих друг за другом последовательностей состоящих из трех указателей в адресном пространстве ядра и следом идущих значений типа
integer
из определений в each_symbol_section
, мы можем определить расположение таблиц символов и сигнатур, и воссоздать файл Module.symvers из бинарника ядра.К несчастью, большинство ядер сегодня сжатые (
zImage
), так что простой поиск по сжатому образу невозможен. Сжатое ядро на самом деле представляет небольшой бинарник, следом за которым идет сжатый поток. Можно просканировать файл zImage
с тем, чтобы найти сжатый поток и получить из него распакованный образ.Я написал скрипт для декомпрессии и извлечения информации о символах ядра в автоматическом режиме. Это должно работать с любой свежей версией ядра, при условии, что ядро не перемещаемое (relocatable) и вы знаете базовый адрес в памяти, куда оно грузится. Скрипт принимает опции для количества и порядка следования битов (endianness) архитектуры, и по умолчанию использует значения, подходящие для ARM. Базовый адрес, однако, должен быть указан. Он может быть найден, на ядрах ARM, в
dmesg
:$ adb shell dmesg | grep "\.init"
<5>[01-01 00:00:00.000] [0: swapper] .init : 0xc0008000 - 0xc0037000 ( 188 kB)
(прим. пер. — однако, не все ядра выводят эти данные в лог, мне довелось встретить один такой практически уникальный случай, когда, видимо, ввиду урезанных опций конфигурации эта информация не выводилась, в таком случае можно обратиться к конфигу PAGE_OFFSET в файле arch/arm/Kconfig и просто надеяться, что вендор использовал одно из дефолтных значений).
Базовый адрес в примере выше —
0xc0008000
.Если как я, вы интересуетесь загрузкой модуля на девайсе Android, тогда бинарник ядра, что вы имеете — полный образ boot. Образ boot содержит другие вещи помимо ядра, так что вы не можете напрямую использовать его со скриптом выше. Исключение составляет только тот случай, если ядро в образе boot сжато, при этом часть скрипта, которая ожидает на входе сжатый образ, все равно найдет ядро.
Если ядро не сжато, вы можете использовать программу unbootimg как изложено в данном посте, для того, чтобы получить образ ядра из вашего образа boot. Как только у вас есть образ ядра, скрипт может быть запущен следующим образом:
$ python extract-symvers.py -B 0xc0008000 kernel-filename > Module.symvers
Сборка ядра
Теперь, когда мы имеем правильный файл
Module.symvers
для ядра, из которого мы хотим загрузить модуль, мы наконец можем собрать модуль (опять, полагая, arm-eabi-gcc
доступно из PATH
, и что терминал открыт в директории с исходниками):$ cp /path/to/Module.symvers build/
$ make M=/path/to/module/source ARCH=arm CROSS_COMPILE=arm-eabi- O=build modules
Вот и собственно все. Вы можете скопировать файл hello.ko на девайс и загрузить модуль:
$ adb shell
# insmod hello.ko
# dmesg | grep insmod
<6>[mm-dd hh:mm:ss.xxx] [id: insmod]Hello world
# lsmod
hello 586 0 - Live 0xbf008000 (P)
# rmmod hello
# dmesg | grep rmmod
<6>[mm-dd hh:mm:ss.xxx] [id: rmmod]Goodbye world
Данная статья является переводом публикации в блоге Mike Hommey.
Комментарии (8)
mapron
20.06.2017 09:26+1«Представьте, что у вас имеется образ ядра Linux для телефона на базе Android, но вы не располагаете ни соответствующими исходниками, ни заголовочными файлами ядра»
А нельзя просто написать производителю и напомнить про GPL или я чего-то не понимаю? Где FSF вообще?oleg_krv
20.06.2017 14:30Есть куча старых планшетов, и новых так-же, на которые китайские производители не то что исходники, а и исправление ошибок не выкладывают.
Я однажды вышел на программиста написавшего драйвер для моего планшета (года 3-4 назад) по адресу в модуле. Так он сам заявил что не располагает конфигурацией, а писал только для демо-платы. Письма в тех отдел производителя уходили в /dev/null или еще глубже.
Тогда похожими методами мне удалось прикрутить нормальную работу Bluetooth usb и откалибровать датчики.RadicalDreamer
20.06.2017 14:40Собственно, да, у самого имелся один старый аппарат на базе MTK — а с MTK, как правило, всегда туго с этим, если повезет найти исходники ядра другого девайса на том же процессоре — уже хорошо, впрочем, не стоит ждать, что оно будет полностью рабочим, либо сенсоры не заработают, либо еще какое устройство, тачскрин или дисплей.
delvin-fil
22.06.2017 19:37самостоятельно собранное ядро не имеет /proc/config.gz если речь о четвертой ветке. У вас третья — попробую.
RadicalDreamer
22.06.2017 21:57По умолчанию, этот конфиг (config IKCONFIG_PROC) выключен.
Однако это вовсе не означает, что конфиг напрочь отсутствует в LK 4.x, просто нужно включить его при конфигурации ядра (тоже самое и в 3.x). http://cateee.net/lkddb/web-lkddb/IKCONFIG_PROC.html
- found in Linux kernels: 2.6.0–2.6.39, 3.0–3.19, 4.0–4.11, 4.12-rc+HEAD
crazylh
Я правильно понимаю, что данный метод несовместим с ядрами которые защищены через kdump — он требует именно перемещаемое ядро:
https://www.kernel.org/doc/Documentation/kdump/kdump.txt
RadicalDreamer
Если верить первоисточнику — то нет, хотя есть предположение, что ядро должно быть не перемещаемым только из-за того, что иначе не получится (вернее, будет весьма затруднительно) вытащить Module.symvers, без которого и сигнатуры функций не будут совпадать с требуемыми.