PVOID - Общие библиотеки

Общие библиотеки

Общие библиотеки — это библиотеки, которые загружаются программами при старте. Когда общая библиотека установлена правильно, все программы, которые запускаются впоследствии, автоматически используют новую общую библиотеку. На самом деле, они намного более гибкие и сложные, потому что подход, используемый Linux, позволяет вам:

  • обновлять библиотеки и по-прежнему поддерживать программы, которые хотят использовать более старые, несовместимые версии этих библиотек;

  • переопределить определенные библиотеки или даже определенные функции в библиотеке при выполнении определенной программы;

  • делать все это, пока программы работают с использованием существующих библиотек.

 

Соглашения


Чтобы общие библиотеки поддерживали все эти желаемые свойства, необходимо соблюдать ряд соглашений и рекомендаций. Вы должны понимать разницу между именами библиотек, в частности его «soname» и «настоящее имя» (и как они взаимодействуют). Вы также должны понимать, где они должны быть размещены в файловой системе.

 

Имена общих библиотек


 Каждая общая библиотека имеет специальное имя, называемое «soname». У soname есть префикс «lib», имя библиотеки, фраза «.so», за которой следует точка и номер версии, которые увеличиваются при изменении интерфейса (как исключение, библиотеки C самого низкого уровня не начинаются с «lib»). Полное soname включает в качестве префикса каталог, в котором находится библиотека; в работающей системе полностью определенное soname — это просто символическая ссылка на «настоящее имя» общей библиотеки.

 У каждой общей библиотеки также есть «настоящее имя», то есть имя файла, содержащее фактический код библиотеки. Настоящее имя добавляет к soname точку, младший (minor) номер, еще точку и номер релиза. Последние точка и номер релиза являются опциональными. Младший номер и номер релиза поддерживают управление конфигурацией, позволяя вам точно знать, какие версии библиотеки установлены. Обратите внимание, что эти числа могут не совпадать с числами, используемыми для описания библиотеки в документации, хотя это облегчает задачу.

 Кроме того, есть имя, которое компилятор использует при запросе библиотеки (назовем это linker name - «имя для компоновщика»), которое является просто soname без какого-либо номера версии.

 Ключ к управлению общими библиотеками — разделение этих имен. Программы, внутренне перечисляющие необходимые им общие библиотеки, должны перечислять только необходимые soname. Наоборот, когда вы создаете общую библиотеку, вы должны создать библиотеку с конкретным именем файла (с более детальной информацией о версии). Когда вы устанавливаете новую версию библиотеки, вы устанавливаете ее в одну из нескольких специальных директорий, а затем запускаете программу ldconfig(8). ldconfig проверяет существующие файлы и создает имена soname как символические ссылки на реальные имена, а также настраивает файл кэша /etc/ld.so.cache (описан немного ниже).

 ldconfig не настраивает имена для компоновщика; обычно это делается во время установки библиотеки, а имена для компоновщика просто создаются как символические ссылки на «последнее» soname или последнее реальное имя. Я бы рекомендовал, чтобы имя для компоновщика было символической ссылкой на soname, поскольку в большинстве случаев, когда вы обновляете библиотеку, вы хотите автоматически использовать ее при компоновке. Я спрашивал H. J. Lu почему ldconfig не настраивает автоматически имена для компоновщика. Его объяснение было в основном таким, что вы можете хотеть запустить код, используя последнюю версию библиотеки, но вместо этого во время разработки вы можете захотеть ссылаться на старую (возможно, несовместимую) библиотеку. Следовательно, ldconfig не делает никаких предположений о том, с чем вы хотите компоновать свои программы, так что инсталляторы должны специально модифицировать символические ссылки для обновления того, что компоновщик будет использовать для библиотеки.

 Таким образом, /usr/lib/libreadline.so.3 является полным soname, которое ldconfig установит как символическую ссылку на некоторое реальное имя вроде /usr/lib/libreadline.so.3.0. Здесь также должно быть имя для компоновщика, /usr/lib/libreadline.so, которое может быть символической ссылкой, ссылающейся на /usr/lib/libreadline.so.3.

 

 Расположение в файловой системе


 Общие библиотеки должны быть расположены где-то в файловой системе. Большинство программного обеспечения с открытым исходным кодом имеет тенденцию следовать стандартам GNU; для дополнительной информации смотрите info-файл документации info:standards#Directory_Variables. Стандарты GNU рекомендуют устанавливать по умолчанию все библиотеки в /usr/local/lib при распространении исходного кода (а все команды (исполняемые айлы) должны располагаться в /usr/local/bin). Они также определяют соглашение для переопределения этих умолчаний и для вызова процедур установки.

 Стандарт иерархии файловой системы (FHS) обсуждает, что и где следует располагать при развертывании (смотрите http://www.pathname.com/fhs). Согласно FHS, большинство библиотек должны быть установлены в /usr/lib, но библиотеки, требуемые для запуска, должны быть в /lib, а библиотеки, не являющиеся частью операционной системы, должны быть в /usr/local/lib.

 Между этими двумя документами нет конфликта; стандарты GNU рекомендуют умолчания для разработчиков исходного кода, тогда как FHS рекомендует умолчания для дистрибьюторов (которые избирательно переопределяют умолчания исходного кода, обычно через системный менеджер пакетов). На практике это работает хорошо: «последний» (возможно, нестабильный!) исходный код, который вы загрузили, автоматически устанавливается в директорию «local» (/usr/local), и когда этот код будет доработан, пакетный менеджер может обычным способом перезаписать умолчание, чтобы расположить код в стандартном месте для дистрибутивов. Обратите внимание, что если ваша библиотека вызывает программы, которые могут быть вызваны только через библиотеки, вы должны разместить эти программы в /usr/local/libexec (который станет /usr/libexec при развертывании). Одним из осложнений является то, что системы, производные от Red Hat, не включают по умолчанию /usr/local/lib в поиск библиотек; смотрите обсуждение /etc/ld.so.conf ниже. Другие стандартные размещения библиотек включают /usr/X11R6/lib для X-windows. Отметим, что /lib/security используется для модулей PAM, но они обычно загружаются как DL библиотеки (также обсуждается ниже).

 

 Как используются библиотеки


 На системах, основанных на GNU glibc, включая все системы Linux, запуск двоичного исполняемого ELF файла автоматически приводит к загрузке и запуску программы загрузчика. На системах Linux этот загрузчик называется /lib/ld-linux.so.X (где X это номер версии). Этот загрузчик, в свою очередь, находит и загружает все прочие общие библиотеки, используемые программой.

 Список директорий для поиска хранится в файле /etc/ld.so.conf. Многие дистрибутивы, производные от Red Hat, обычно не включают /usr/local/lib в файл /etc/ld.so.conf. Я считаю это ошибкой, и добавление /usr/local/lib в /etc/ld.so.conf является общим «исправлением», требуемым для запуска многих программ на системах, производных от Red Hat.

  Если вы хотите только перезаписать несколько функций в библиотеке, но сохранить оставшуюся часть библиотеки, вы можете внести имена перезаписываемых библиотек (файлы .o) в /etc/ld.so.preload; эти «предзагружаемые» библиотеки будут иметь приоритет перед стандартным набором. Этот файл обычно используется для экстренных патчей; дистрибутив обычно не включает такой файл при развертывании.

 Поиск всех этих каталогов при запуске программы будет крайне неэффективным, так что в действительности используется механизм кэширования. Программа ldconfig(8) по умолчанию читает файл /etc/ld.so.conf, настраивает необходимые символические ссылки в соответствующих каталогах (чтобы они следовали стандартным соглашениям), а затем записывает кэш в /etc/ld.so.cache, который затем используется другими программами. Это намного ускоряет доступ к библиотекам. Подразумевается, что ldconfig должен быть запущен всякий раз при добавлении DLL, удалении DLL, или при изменении набора директорий DLL; запуск ldconfig часто является одним из шагов, производимых менеджерами при установке библиотек. Затем, при старте, динамический загрузчик в действительности использует файл /etc/ld.so.cache, а затем загружает необходимые библиотеки.

 Кстати, FreeBSD использует немного другие имена файлов для этого кэша. В FreeBSD кэш ELF это /var/run/ld-elf.so.hints, а кэш a.out это /var/run/ld.so.hints. Они также обновляются при помощи ldconfig(8), так что это различие в расположении имеет значение только в нескольких экзотических ситуациях.

 

 Переменные окружения


 Различные переменные среды могут управлять этим процессом, и есть переменные среды, которые позволяют вам переопределить этот процесс.

 LD_LIBRARY_PATH

 Вы можете временно заменить библиотеку для данного конкретного исполнения. В Linux переменная окружения LD_LIBRARY_PATH является набором директорий, разделенных двоеточием, в которых должен производиться поиск библиотек в первую очередь, перед стандартным набором директорий; это полезно при отладке новой библиотеки или использовании нестандартной библиотеки для специальных целей. Переменная окружения LD_PRELOAD перечисляет общие библиотеки с функциями, которые переопределяют стандартный набор, так же, как /etc/ld.so.preload. Это реализовано загрузчиком /lib/ld-linux.so. Необходимо отметить, что хотя переменная LD_LIBRARY_PATH работает на многих Unix-подобных системах, она работает не на всех; например, эта функциональность доступна на HP-UX, но как переменная окружения SHLIB_PATH, а на AIX эта функциональность реализована через переменную LIBPATH. (с тем же синтаксисом, разделенный двоеточиями список).

 LD_LIBRARY_PATH удобна для разработки и тестирования, но обычно не должна модифицироваться процессом установки для обычного использования; для объяснения причин, смотрите статью "Why LD_LIBRARY_PATH is Bad"(http://xahlee.org/UnixResource_dir/_/ldpath.html), автор: David Barr. Но это все еще полезно для разработки и тестирования, а также для решения проблем, которые нельзя решить иначе. Если вы не хотите устанавливать переменную окружения LD_LIBRARY_PATH, на Linux вы можете доже вызвать загрузчик программ напрямую и передать ему аргументы. Например, следующий пример будет использовать переданный PATH вместо содержимого переменной окружения LD_LIBRARY_PATH, и запустит переданный исполняемый файл:

/lib/ld-linux.so.2 --library-path PATH EXECUTABLE

 Просто запустив ld-linux.so без аргументов, вы получите больше информации по его использованию, но опять же, не применяйте это для обычного использования — все они предназначены для отладки.

 LD_DEBUG

 Другая полезная переменная окружения для загрузчика GNU C это LD_DEBUG. Она запускает функции dl*, так что они дают достаточно подробную информацию о том, что они делают. Например:

export LD_DEBUG=files
command_to_run

 отображает обработку файлов и библиотек при работе с библиотеками, показывая вам обнаруженные зависимости, и какие SO загружены и в каком порядке. Установка LD_DEBUG в «bindings» отображает информацию о привязке символов, установка в «libs» отображает пути поиска библиотек, а установка в «versions» отображает зависимости версий.

 Установка LD_DEBUG в «help» и последующий запуск программы перечислит возможные опции. Опять же, LD_DEBUG не предназначена для обычного использования, но может быть удобна при отладке и тестировании.

 Другие переменные окружения

 В действительности существует множество других переменных окружения, контроллирующих процесс загрузки; их имена начинаются с LD_ или RTLD_. Большинство других предназначены для низкоуровневой отладки процесса загрузки или для реализации специальных возможностей. Большинство из них плохо задокументированы; если вам необходимо узнать о них, лучшим способом будет чтение исходного кода загрузчика (часть gcc).

 Разрешение пользователю контролировать динамически загружаемые библиотеки было бы катастрофическим для setuid/setgid программ, если бы не были приняты специальные меры. Следовательно, в загрузчике GNU (который загружает остальную часть программы при запуске программы), если для программы установлен setuid или setgid, эти переменные (и другие подобные переменные) игнорируются или сильно ограничены в возможностях. Загрузчик определяет, установлен ли для программы setuid или setgid, проверяя полномочия программы; если uid и euid различны, или gid и egid различны, загрузчик полагает, что программа является setuid/setgid (или производной) и, следовательно, сильно ограничивает ее возможности управлять процессом загрузки библиотек. Если вы читали исходный код библиотеки GNU libc, вы могли видеть это; особенно посмотрите файлы elf/rtdl.c и sysdeps/generic/dl-sysdep.c. Это означает, что если вы выставите uid и gid равными euid и egid, а затем вызовете программу, эти переменные будут иметь полный эффект. Другие Unix-подобные системы решают эту проблему по-разному, но по той же причине: программы setuid / setgid не должны подвергаться чрезмерному влиянию установленных переменных окружения.

 

 Создание общих библиотек


 Создать общую библиотеку легко. Сначала создайте объектные файлы, которые войдут в общую библиотеку, используя флаг gcc -fPIC или -fpic. Флаги -fPIC и -fpic включают генерацию «позиционно независимого кода», что является требованием для общих библиотек; смотрите различия ниже. Вы передаете soname, используя опцию gcc -Wl. Опция -Wl передает параметры компоновщику (в данном случае, опцию компоновщика -soname) — запятые после -Wl не являются опечаткой, и вы не должны включать неэкранированные пробелы в эту опцию. Затем создайте общую библиотеку. используя следующий формат:

gcc -shared -Wl,-soname,your_soname \
    -o library_name file_list library_list

 Вот пример, который создает два объектных файла (a.o и b.o), а затем создает общую библиотеку, содержащую их оба. Обратите внимание, что эта сборка включает отладочную информацию (-g) и будет генерировать предупреждения (-Wall), которые не требуются для общих библиотек, но являются рекомендуемыми. Сборка генерирует объектные файлы (используя -c) и включает обязательную опцию -fPIC. Обратите внимание: руководство GCC 4.5 говорит, что если вы используете -shared, «Для предсказуемых результатов, необходимо также указать тот же набор параметров, которые использовались для генерации кода (-fpic, -fPIC или подопции модели)». Таким образом, вы должны включить -fPIC при компоновке, а также во время первоначальной компиляции.[1]

gcc -fPIC -g -c -Wall a.c
gcc -fPIC -g -c -Wall b.c
gcc -shared -fPIC -Wl,-soname,libmystuff.so.1 \
    -o libmystuff.so.1.0.1 a.o b.o -lc

 Вот несколько моментов, на которые стоит обратить внимание:

  •  Не удаляйте символьную информацию (strip) результирующей библиотеки, а также не используйте опцию компилятора -fomit-frame-pointer, если вам это действительно не нужно. Библиотека будет работать, но эти действия делают отладчики в основном бесполезными.

  •  Используйте -fPIC или -fpic для генерации кода. Выбор -fPIC или -fpic для генерации кода зависит от целей. -fPIC работает всегда, но может создавать больший код, чем -fpic (мнемоническим правилом для запоминания этого является то, что PIC записывается большими буквами, так что он может производить большие объёмы кода). Использование опции -fpic обычно генерирует меньший и более быстрый код, но будет иметь ограничения, зависящие от платформы, например, количество глобально видимых символов или размер кода. Компоновщик сообщит, подходит ли он, при создании общей библиотеки. В случае сомнений я выбираю -fPIC, потому что это всегда работает.

  •  В некоторых случаях вызов gcc для создания объектного файла также должен включать опцию «-Wl,-export-dynamic». Обычно таблица динамических символов содержит только символы, которые используются динамическим объектом. Эта опция (при создании файла ELF) добавляет все символы в таблицу динамических символов (смотрите ld(1) для дополнительной информации). Вам нужно использовать эту опцию, когда имеются «обратные зависимости», т.е. DL библиотека имеет неразрешенные символы, которые по соглашению должны быть определены в программах, которые намереваются загрузить эти библиотеки. Чтобы «обратные зависимости» работали, гоавная программа должна сделать эти символы доступными динамически. Отметим, что можно передавать опцию «-rdynamic» вместо «-Wl,export-dynamic», если вы работаете только с системами Linux, но согласно документации ELF, флаг «-rdynamic» не всегда работает на других системах.

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

-Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)

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

 

 Установка и использование общих библиотек


 После создания общей библиотеки, вы захотите ее установить. Простым подходом является простое копирование библиотеки в один из стандартных каталогов (например, /usr/lib) и запуск ldconfig(8).

 Сначала вам необходимо создать общие библиотеки где-либо. Затем необходимо настроить необходимые символические ссылки, в частности, ссылку от soname на реальное имя (так же, как и ссылку от soname без версии, то есть soname, заканчивающееся на «.so» для пользователей, которые не указывают версию вообще). Простейшим подходом является выполнение:

ldconfig -n directory_with_shared_libraries

 Наконец, когда вы компилируете свои программы, вам необходимо указать компоновщику о любых используемых статических и общих библиотеках. Для этого используйте опции -L и -l.

 Если вы не хотите устанавливать библиотеку в стандартное место (например, у вас нет прав на изменение /usr/lib), то вам необходимо изменить подход. В этом случае вам необходимо установить ее в любом месте, а затем передать вашей программе достаточно информации так, чтобы программа могла найти библиотеку... и есть несколько способов сделать это. В простейших случаях вы можете использовать флаг -L для gcc. Вы можете использовать подход «rpath» (описан выше), в частности, если вы только имеете конкретную программу, использующую библиотеку, расположенную в «нестандартном» месте. Также вы можете использовать переменные окружения, чтобы управлять этим. В частности, вы можете установить переменную LD_LIBRARY_PATH, являющуюся разделенным двоеточиями списком виректорий, в которых производится поиск общих библиотек перед поиском в обычных местах. Если вы используете bash, вы можете запустить my_program таким образом, используя:

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH  my_program

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

 Обычно вы можете обновлять библиотеки без проблем; если произошло изменение API, создатель библиотеки должен изменить soname. Таким образом, множество библиотек может находиться в одной системе, и правильная библиотека выбрана для каждой программы. Однако, если программа прерывается при обновлении библиотеки, сохраняющей такое же soname, вы можете заставить ее использовать старую версию библиотеки путем копирования старой библиотеки куда-либо, переименования программы (скажем, на старое имя плюс «.orig»), и написания небольшого скрипта «обертки», который сбросит используемую библиотеку и вызовет настоящую (переименованную) программу. Вы можете поместить старую библиотеку в ее собственное специальное место, если хотите, хотя соглашения о нумерации разрешают нескольким версиям жить в одном каталоге. Скрипт обертки может выглядеть примерно так:

#!/bin/sh
  export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH
  exec /usr/bin/my_program.orig $*

 Пожалуйста, не опирайтесь на это при написании ваших собственных программ; попытайтесь удостовериться, что ваши библиотеки либо обратно совместимы, либо вы увеличивали номер версии в soname каждый раз, когда вносили несовместимые изменения. Это просто «экстренный» подход к решению проблем в наихудшем случае.

 Вы можете посмотреть список общих библиотек, используемых программой, с помощью ldd(1). Так, например, вы можете посмотреть общие библиотеки, используемые ls, написав:

ldd /bin/ls

 Как правило, вы увидите список имен soname, от которых зависит ls, а также каталоги, в которых они расположены. Практически во всех случаях у вас будет как минимум две зависимости:

  • /lib/ld-linux.so.N (где N равно 1 или более, обычно не менее 2). Это библиотека, которая загружает все остальные библиотеки.

  • libc.so.N (где N равно 6 или более). Это библиотека языка Си. Даже другие языки, как правило, используют библиотеку Си (по крайней мере, для реализации своих собственных библиотек), поэтому большинство программ включают, по крайней мере, её.

 Осторожно: не запускайте ldd для программ, которым вы не доверяете. Как ясно указано в руководстве ldd (1), ldd работает (в некоторых случаях), устанавливая специальную переменную окружения (для объектов ELF, LD_TRACE_LOADED_OBJECTS), а затем исполняет программу. Возможно, ненадежная программа может заставить пользователя ldd выполнить произвольный код (вместо простого отображения информации ldd). Так что, из соображений безопасности не используйте ldd для программ, которым вы не доверяете.

 

 Несовместимые библиотеки


 Когда новая версия библиотеки является двоично-несовместимой со старой, необходимо изменить soname. В языке Си существует четыре основных причины, по которым библиотека перестает быть двоично-совместимой:

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

  2. Изменяются экспортируемые элементы данных (исключение: добавление необязательных элементов в конец структур разрешено, если эти структуры выделены только в библиотеке).

  3. Удаляется экспортируемая функция.

  4. Интерфейс экспортируемой функции изменяется.

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

 Для C++ (и других языков, поддерживающих скомпилированные шаблоны и/или раннее связывание), ситуация сложнее. Все вышеперечисленные вопросы применимы, но также и многие другие вопросы. Причиной является то, что некоторая информация «скрыта» в скомпилируемом коде, в результате появляются зависимости, которые могут быть неочевидны, если вы не знаете, как это обычно реализуется в C++. Строго говоря, это не «новые» вопросы, просто скомпилированный код C++ вызывает их способами, которые могут вас удивить. Далее приведен (возможно, неполный) список вещей, которые вы можете и не можете делать в C++ и сохранить бинарную совместимость (о них первоначально сообщалось в техническом FAQ Troll Tech; более свежий список находится в KDE’s Policies/Binary Compatibility Issues With C++):

  1. добавлять переопределения виртуальных функций (до тех пор, пока старые бинарные файлы могут безопасно вызывать оригинальную реализацию), потому что компилятор оценивает вызовы SuperClass::virtualFunction() во время компиляции (не во время компоновки).

  2. добавлять или удалять виртуальные методы, потому что это изменит размер и расположение vtbl каждого подкласса.

  3. изменять тип любого поля или перемещать любое поле, которое может быть доступно через встроенные методы.

  4. изменять иерархию классов, кроме добавления новых листьев.

  5. удалять или добавлять закрытые поля, потому что это может изменить размер и расположение каждого подкласса.

  6. удалять открытые и защищенные методы, если они не являются встроенными.

  7. делать открытые и защищенные методы встроенными.

  8. изменять поведение встроенных функций, если старая версия продолжает работать.

  9. изменять права доступа (т.е. public, protected или private) методов переносимой программы, потому что некоторые компиляторы встраивают права доступа в имя функции.

 Учитывая этот длинный список, разработчики библиотек C++, в частности, должны планировать больше, чем просто случайные обновления, которые нарушают бинарную совместимость. К счастью, в Unix-подобных системах (включая Linux) вы можете загружать одновременно несколько версий библиотеки, поэтому, несмотря на некоторую потерю дискового пространства, пользователи могут запускать «старые» программы, требующие старых библиотек.

 

 Заметки


[1]

     Спасибо Бриттону Крейну за то, что указал на это!


Этот раздел является переводом руководства Program Library HOWTO


   <<< Назад Содержание Вперед >>>