Linux find symbol in lib

GLib undefined symbol bug in Debian

Dec 4, 2018 · 2 min read

After a regular software upgrade, my Debian sid refused to start lightdm service. Viewing the log by running journalctl -xe , I saw something like this:

Following the discussion in that thread, I started inspecting some GLib files:

It turns out that my /lib/x86_64-linux-gnu/libglib-2.0.so.0 was a symlink to /lib/x86_64-linux-gnu/libglib-2.0.so.0.4800.1 , while my /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0 was a symlink to /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.5800.1 . One of them must belong to a wrong version.

Which GLib version do I have?

And the result is libglib2.0–0/unstable,now 2.58.1–2 amd64 [installed] .

SOLUTION

It is now c l ear that when upgrading the libglib2.0–0 package, Debian failed to clean the old one (2.48) completely. As /lib comes before /usr/lib , it is causing this problem. I tried to do dpkg-reconfigure libglib2.0–0 , but it didn’t help. So in the end I removed the files under /lib:

CAUSE

Now my lightdm works as expected. How did this happen?

In the bug report, Michael Biebl suspected that the files of an older version of GLib could have been introduced by a third party package. I do not think so. Let’s compare two file lists:

File list of package libglib2.0–0 in stretch of architecture amd64

File list of package libglib2.0–0 in sid of architecture amd64

The symlink libglib-2.0.so.0 and the file it links to, used to sit under /lib/x86_64-linux-gnu/ in stretch, but now under /usr/lib/x86_64-linux-gnu/ in sid. This change was introduced in 2.56.0–5. The installation script failed to clean the old ones.

Источник

Делаем доступным все символы ядра Linux. Часть 1

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

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

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

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

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

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

Читайте также:  Format ntfs with linux

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Читайте также:  High rise building windows

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

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

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

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

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

Здесь изображена таблица индексов (номеров) системных вызовов, используемая в адресном пространстве пользователя, реализуемая стандартной библиотекой 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_*() … — всё это есть в файлах архива):

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

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

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

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

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

Читайте также:  Nport windows driver manager что это

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

Обсуждение

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

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

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

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

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

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

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

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

Примечания

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

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

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

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

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

Источник

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