From: Павел Колодин <http://ymap.org>
Newsgroups: email
Date: Mon, 31 Jul 2007 14:31:37 +0000 (UTC)
Subject: Разделяемые библиотеки (shared libraries)
Оригинал: http://ymap.org/aaa.html
Разделяемые библиотеки (shared libraries) - это такие библиотеки,
которые загружаются приложениями при запуске. Если такая библиотека
установлена правильно, то все приложения, запускаемые после установки
библиотеки, начинают использовать эту новую разделяемую библиотеку. На
самом деле, всё гораздо гибче и соверщеннее, ибо линуксовая их
реализация позволяет:
* совершенствовать библиотеки, продолжая поддерживать программы,
использующие более старые версии этих библиотек, не совместимые с
новыми.
* заменять какие-то конкретные библиотеки или даже конкретные
функции в библиотеке во время выполнения какой-то конкретной
программы.
* проделывать всё это "на горячую", когда программа запущена и
какие-то библиотеки уже использует.
Соглашения.
Чтобы разделяемые библиотеки всё это умели, они должны соответствовать
определённым нормам. Нужно разбираться в том, что называется soname и
real name и как эти понятия связаны, также в том, где в файловой
системе разделяемые библиотеки должны лежать.
Имена разделяемых библиотек.
Каждая разделяемая библиотека имеет специальное имя, называемое
"soname". Оно имеет префикс "lib", само имя библиотеки, также слово
".so", за которым следует номер стадии и номер версии библиотеки,
которые меняются каждый раз, когда у библиотеки меняется интерфейс.
(Особое исключение: библиотеки низкого уровня языка С не начинаются с
"lib"). Полное имя soname включает в качестве префикса имя дериктории,
в которой библиотека находится. На живых системах полное имя
библиотеки - это просто символическая ссылка на имя real name
разделяемой библиотеки.
Также, каждая разделяемая библиотека имеет имя real name. Это - имя
файла, содержащего собственно код библиотеки. Real name в дополнение к
soname содержит номер стадии, минорный номер (minor number), второй
номер стадии и номер релиза. Второй номер и номер релиза не
обязательны (are optional). Минорный номер и номер релиза дают вам
знать, какие именно версии библиотек установлены у вас в системе.
Кстати, в документации на библиотеку эти два последних номера могут с
библиотечными своими собратьями не совпадать.
Далее. У библиотеки существует ещё один тип имени, который
используется системой, когда библиотеку запрашивают. Называется оно
linker name, которое на самом деле равно soname, но без указания
версии.
Способ содержания разделяемых библиотек в хозяйстве состоит в
различении их по именам. Каждое приложение должно помнить о
разделяемой библиотеке, которая ему нужна, помня только soname этой
библиотеки. Наоборот, когда вы разделяемую библиотеку создаёте, вы
имеете дело только с именем файла, содержащего более детальную
информацию о версии. Когда вашу библиотеку будут устанавливать, её
запишут в один из специально предназначенных для этого каталогов в
файловой системе и запустят программу ldconfig. Эта хитрая программа
смотрит в упомянутые специальные каталоги и создаёт soname-имена как
символические ссылки к реальным именам, публикуя новости с фронтов в
файле /etc/ld.so.cache (речь об нём позже).
ldconfig не создаёт linker-имена. Обычно это делается при
инициализации библиотеки и linter-имена создаются просто как
символические ссылки к последним актуальным soname-именам или к
последним актуальным real-именам. Я бы рекомендовал делать объектом
ссылки soname-имена, ибо меняются они реже, чем real-имена. А причина,
по которой ldconfig автоматически не простраивает linker-имена в том,
чтобы дать юзверям возможность запустить код, который выбирает
lintker-имя библиотеки на своё усмотрение.
Таким образом, /usr/lib/libreadline.so.3 - это полное имя soname,
которое на самом деле является символической ссылкой на реальное имя
типа /usr/lib/libreadline.so.3.0. Linker-имя будет выглядеть как
/usr/lib/libreadline.so, являющееся символической ссылкой на
/usr/lib/libreadline.so.3
Как используются библиотеки.
На GNU системах, использующих glibc, включая все Linux, запуск ELF
бинаря приводит к запуску загрузчика. На Linux загрузчик называется
/lib/ld-linux.so.X, где X - это номер версии. Загрузчик этот находит и
загружает все разделяемые библиотеки, используемые запускаемым
ELF-бинарём (приложением).
Список директорий, в которых ищутся разделяемые библиотеки хранится в
файле /etc/ld.so.conf. Многие Red Hat-овые дистрибутивы по дефолту не
включают /usr/local/lib в файл /etc/ld.so.conf. Это выглядит багом, и
добавление /usr/local/lib + /etc в /etc/ld.so.conf - это общепринятая
заплатка, требующаяся для запуска многих прог под Red-Hat-дистрами.
Если вы хотите заменить только некоторые функции в библиотеке, оставив
какую-то часть библиотеки в строю, вам нужно записать имена библиотек
(файлы "*.o") с заменающими функциями в файл /etc/ld.so.preload. Так
вы поднимаете приоритет заменяющих библиотек над стандартными. Этот
предзагрузочный файл обычно используется для срочных заплаток.
Дистрибутивораспространители обычно плюют на него.
Чтобы как-то разгрузить процесс старта бинарей ELF от рытья в
каталогах, используется кеширование. Софтина ldconfig по дефолту
читает /etc/ld.so.conf, создаёт все нужные символические ссылки и
создаёт файл /etc/ld.so.cache, который далее используется другими
программами. Эта замануха охрененно ускоряет доступ к либам. Фишка в
том, что ldconfig запускается при добавлении DLL, удалении DLL или
когда список каталогов с DLL-ами поменялся. Запуск ldconfig-а часто
является одним из шагов, выполняемых пакетными менеджерами при
установке библиотеки. Далее при запусках софтин динамический загрузчик
юзает уже /etc/ld.so.cache, загружая либы по мере надобности.
Однако FreeBSD юзает слегка другие имена файлов для этого кеша. Во
фрях, ELF кеш - это /var/run/ld-elf.so.hints и a.out-овый кеш - это
/var/run/ld.so.hints. Они также ведаются софтиною ldconfig, так что
различие в именах файлов будет не по барабану только в особых
ситуациях.
Переменные окружения.
LD_LIBRARY_PATH
Для какого-то конкретного запуска приложения, можно подменить
библиотеки. В Linux переменная окружения LD_LIBRARY_PATH - это список
отделённых двоеточиями имён каталогов, где библиотеки следует искать
перед тем, как их будут искать по стандартным путям. Полезно для
отладки новых библиотек или для использования нестандартной библиотеки
для чего-то этакого. Переменная окружения LD_PRELOAD содержит список
разделяемых библиотек, содержащих функции, замещающие стандартные.
LD_PRELOAD работает как /etc/ld.so.preload. Эти две переменные
окружения нюхаются загрузчиком /lib/ld-linux.so. Следует отметить, что
LD_LIBRARY_PATH работает не во всех Unix-системах. Например в HP-UX
LD_LIBRARY_PATH заменена на SHLIB_PATH, а в AIX заменена на LIBPATH.
Синтаксис тот же - список, разбитый двоеточьями.
LD_DEBUG
Другая полезная переменная окружения для GNU C загрузчика - это
LD_DEBUG. Она меняет режим работы всех dl* функции так, что они
начинают выдавать довольно подробную информацию о своих действиях.
Например, закиньте в LD_DEBUG строку "files" и вызовите какую-нибудь
софтину, к примеру, "ls", допустим, корня.
export LD_DEBUG=files
ls /
Помимо самого вывода ls вы увидите некоторую кухню работы ld -
используемые файлы и библиотеки при загрузке библиотек, по которым
можно судить о завивимостях, о том, какие SO-файлы загружаются и др.
Это был вывод при "files". А ещё есть "bindings" - показывает
информацию о запрашиваемых именах подпрограмм, переменных - так
называемые symbols - символы, идентификаторы; "libs" показывает
каталоги, в которых библиотеки ищутся; "versions" показывают
зависимости по версиям. Установка LD_DEBUG равной "help" с вызовом
какой-то программы, покажет доступные опции. Опять таки, LD_DEBUG не
применяется для мирных целей.
Другие переменные окружения.
Есть куча других, влияющих на загружаемый/запускаемый процесс. Имена
их начинаются с "LD_" или "RTLD_". Большинство предназначены для
низкоуровневой отладки процесса загрузчика или для реализации
сверхспособностей. Большинство недокументированы. Лучший путь познать
их - курить исходники загрузчика (он - часть gcc).
Установленный у некоторых программ setuid/setgid плюс рычаги контроля
за динамически загружаемыми библиотеками в руках пользователя -
это потенциально опасно. Поэтому GNU-загрузчик (загружает большинство
программ) игнорирует или сильно ограничивает свои возможности во время
запуска таких программ. Если у приложения различаются UID и EUID или
GID и EGID, загрузчик полагает, что у программы установлен
setuid/setgid и запрещает самому себе делать некоторые вещи. Если
будете читать исходники библиотеки GNU glibc, вы сможете это увидеть.
Обазательно посмотритие elf/rtld.c и sysdeps/generic/dl-sysdep.c.
Переменные окружения имеют полный вес, когда равны UID и EUID, GID и
EGID. Различные UNIX-подобные системы ведут себя здесь различно, но с
одной идеей: программа с установленным setuid/setgid не должна быть
чрезмерно влиятельной из-за особым образом установленных переменных
окружения.
Создание разлеляемых библиотек.
Это просто. Сначала создайте объектные файлы посредством флага gcc
-fPIC или -fpic. Флаги -fPIC и -fpic разрешают генерацию
"позиционно-независимого кода" (position independent code),
требующегося для разделяемых библиотек. Передайте soname-имя
библиотеки опцией gcc -Wl. Эта опция передаёт параметр линкеру.
* Не делайте обработки strip над готовой библиотекой и не
используйте опцию компилятора -fomit-frame-pointer пока не
приспичит. Работать такая библиотека будет, но отладка её
затруднится.
* Не забывайте использовать -fPIC или -fpic. Какую из них выбрать
- это определяется целью. -fPIC работает всегда, но даёт
больший объём кода, чем -fpic (Запомнить это свойство просто: PIC
"крупнее" pic!). Применение -fpic обычно приводит к генерации
меньшего и шустрейшего кода, но он будет иметь некоторые
платформенно-зависимые ограничения вроде количества глобально
видимых символов или размера всего кода. Но линкер вам сообщит,
если код не будет влезать. Когда линкер пристаёт, я пишу -fPIC.
* В некоторых случаях при создании объектного файла понадобится
включить опцию gcc "-Wl,-export-dynamic". Обычно таблица
динамических символов содержит только те символы, которые
используются динамическим объектом. Эта опция (при создании файла
ELF) добавляет все символы в таблицу динамических символов (зырь
ld(1) for more information). Эта опция хороша для создания
библиотек с "обратной зависимостью". Это когда динамически
загружаемая библиотека содержит неудовлетворённые символы, которые
по соглашению должны быть определены в самих приложениях, которые
эти библиотеки загружают. Для жизнеспособности идеи "обратной
зависимости", символы приложения, использующего такие библиотеки,
должны быть выполнены динамически доступными. Кстати, вы могли бы
дать опцию "-rdynamic" вместо "-Wl,export-dynamic", если
компиляция делается для Linux. Но согласно документации на ELF,
флаг "-rdynamic" не всегда работает для gcc в других системах.
Обычно, когда человек разрабатывает разделяемую библиотеку, он
тестирует её на узком круге каких-то приложений. Ему не нужно, чтобы
оставшееся большое количество приложений в системе видело её. Поэтому
при сборке программы, с которой тестируется библиотека, этой программе
можно показать, где библиотеку искать опцией линкера "rpath". Для gcc:
-Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)
Если вы используете эту опцию при создании программы, с которой вы
библиотеку тестируете, то с переменной окружения LD_LIBRARY_PATH иметь
дело не придётся.
Установка и использование разделяемой библиотеки.
Проще всего - скопировать в один из стандартных каталогов (например
/usr/lib) и запустить ldconfig(8).
Сначала вы должны создать разделяемую библиотеку где-либо. Затем вам
понадобится создать необходимые символические ссылки, в частности
ссылку soname, показывающую на реальное имя (также ссылку soname, на
которой нет версии, то есть, soname оканчивающееся на ".so" для
пользователей, которым версия вокруг табуретки). Простейший способ:
ldconfig -n каталог с разделяемыми библиотеками
Наконец, когда вы собираете ваши программы, вам нужно указать линкеру
используемые вами статические и разделяемые библиотеки. Для этого есть
флаги -l и -L. Если вы не устанавливаете библиотеку в какой-то из
стандартных каталогов, вам нужно оставить их где угодно, но показать
на них своей программе. Это делается несколькими способами: флаг gcc
"-L", флаг "-rpath", переменные окружения (например LD_LIBRARY_PATH,
содержащий список каталогов, разделённых двоеточием, в которых будут
предприниматься попытки найти нужную разделяемую библиотеку перед
попытками искать её в стандартных каталогах). Если вы используете
bash, то можете вызывать свою программу так:
LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH my_program
Если своей библиотекой вы хотите заменить только часть функций, можете
сделать это так: создать замещающий объектный файл и установить
LD_PRELOAD. Функции из этого объектного файла заместят только те же
самые функции (не касаясь остальных).
Обычно вы можете заменять библиотеки без специальных предупреждений.
Если новая библиотека реализует изменения в API, тогда автор
библиотеки меняет soname. Таким образом, в одной системе могут
сосуществовать несколько библиотек, но каждая программа в системе
находит нужные ей. Если же программа сломалась при замене библиотеки,
при которой сохранилось старое её soname, вы можете заставить
программу использовать старую версию библиотеки. Для этого скопируйте
старую библиотеку куда-нибудь, затем переименуйте программу (например
прибавьте к старому имени ".orig"), после чего создайте небольшой
"обёрточный" скрипт. Он будет устанавливать переменные окружения,
сообщающие загрузчику альтернативный путь для поиска библиотек и затем
запускать переименованную копию программы. Вот такой:
Пожалуйста, не вооружайтесь этим способом запуска программ, когда
пишете свои собственные программы. Попытайтесь гарантировать, что ваши
новые библиотеки совместимы со старыми версиями или не забывайте
увеличивать номер версии в soname-имени при каждом изменении,
нарушающем совместимость. Описанный метод рассматривайте как помощь в
исключительных ситуациях.
Просмотреть список разделяемых библиотек, используемых программой
возможно с помощью ldd(1). Например так:
ldd /bin/ls
В основном вы увидите список soname-имён в парах с полными именами
файлов. Практически все ваши программы будут зависеть от:
* /lib/ld-linux.so.N (где N больше единицы, но обычно 2). Эта
библиотека загружает все другие библиотеки.
* libc.so.N (где N больше или равно шести). Это стандартная
библиотека языка C. Даже другие языки программирования часто
используют libc (как минимум для реализации своих библиотек).
Но не запускайте ldd для тех программ, которым не доверяете. Как ясно
изложено в руководстве по ldd(1), она выполняет свои функции,
устанавливая специальные переменные окружения (для ELF объектов
LD_TRACE_LOADED_OBJECTS), после чего запускает программу. Таким
образом ненадёжной программе может быть предоставлена возможность
запуска произвольного кода, от имени пользователя, выполняющего ldd.
Несовместиомость библиотек.
Когда новая версия библиотеки бинарно несовместима со старой, тогда
нужно менять soname. Для языка C есть четыре основных причины, по
которым библиотека может оказаться бинарно несовместимой.
1. Так меняется поведение функции, что она больше не соответствует
своей спецификации.
2. Меняется формат выдачи результатов. Исключение: добавление новых
полей в конец структуры или что-то такое.
3. Функция исчезла.
4. Меняется формат входных данных для функции.
Избегайте этих явлений, чтобы добиться бинарной совместимости. Другими
словами, вы можете сохранять Application Binary Interface (ABI)
библиотеки неизменным, избегая перечисленные случаи. Например,
добавляйте новые функции без удаления старых. Добавляйте поля в
структуры данных так, чтобы этого не чувствовали существущие программы
- например, в конец. А ещё можно делать новые поля структуры
опционально включаемыми.
Для С++ и других языков, поддерживающих шаблоны или виртуальные
функции (полиморфизм во время выполнения) всё куда хитрее. Всё хитро
так, как описано выше, но возведённое ещё в некоторую степень (-;
Строго говоря, ничего нового, просто код, порождённый компилятором C++
может вызывать функции очень удивительным образом. Вот список того,
чего нельзя делать в С++, надеясь на сохранение бинарной
совместимости:
1. Добавлять новые реализации виртуальных функций (кроме тех случаев,
когда старые бинари могут безопасно вызывать старые реализации),
так как компилятор обдумывает вызовы
SuperClass()::virtualFunction() во время компиляции.
2. Добавлять или удалять виртуальные функции класса, так как это
повлечёт изменение размера vtable каждого подкласса.
3. Изменять тип любых данных - членов класса или перемещать
любые данные, являющиеся членами класса, доступные inline-функциям
членам.
4. Изменять дерево классов, за исключением добавления новых
наследников (листиков, поколений, подклассов).
5. Добавлять или удалять private данные - члены, так как это
может поменять размер и вид каждого подкласса.
6. Удалять public или protected функции-члены, за исключением inline.
7. Превращать public или protected функции-члены в inline функции.
8. Изменять поведение inline-функции.
9. Менять права доступа (public, protected, private) функций в
переносимой программе, поскольку некоторые компиляторы отражают
права доступа на именах функций.