Здесь мы исходим из того, что во время выполнения разделяемые библиотеки будут доступны в подкаталоге lib, находящийся внутри каталога с нашим исполняемым файлом. Теперь можно предоставить пользователю простой установочный пакет с программой и нужными библиотеками, который можно установить в любое место и запускать оттуда программу (так называемое приложение под ключ).
При разрешении зависимостей библиотеки динамический компоновщик в первую очередь смотрит, содержит ли строка в списке rpath слеш (/); это бывает в тех случаях, когда при компоновке исполняемого файла был явно указан путь к библиотеке. Если слеш присутствует, то строка rpath интерпретируется в качестве пути (полного или относительного), по которому следует загружать библиотеку. В противном случае динамический компоновщик ищет библиотеку в соответствии со следующими правилами.
1. Если исполняемый файл содержит в своем списке rpath запись DT_RPATH с какими-либо каталогами и при этом не содержит списка DT_RUNPATH, то поиск будет выполнен по данным каталогам.
2. Если определена переменная среды LD_LIBRARY_PATH, то поиск будет выполнен последовательно по каждому каталогу, который в ней указан (и разделен двоеточиями). Если исполняемый файл устанавливает пользовательский или групповой идентификатор, то переменная LD_LIBRARY_PATH игнорируется. Это делается в целях безопасности, чтобы не дать пользователю обмануть динамический компоновщик, заставив его загрузить вместо требуемой библиотеки ее приватную версию с тем же именем.
3. Если в записи DT_RUNPATH списка rpath указаны какие-либо каталоги, то они используются во время поиска (в том порядке, в котором были перечислены во время компоновки программы).
4. Проверяется файл /etc/ld.so.cache, чтобы узнать, содержит ли он запись для соответствующей библиотеки.
5. Выполняется поиск по каталогам /lib и /usr/lib (именно в таком порядке).
Представьте, что глобальный символ (то есть функция или переменная) определен сразу в нескольких местах — например, в исполняемом файле и разделяемой библиотеке или в нескольких разных библиотеках. Как будет разрешена ссылка на этот символ?
Допустим, у нас есть главная программа и разделяемая библиотека, и в обеих определена глобальная функция xyz(), которая вызывается из другой библиотечной функции, как показано на рис. 41.5.
Рис. 41.5.
Собрав разделяемую библиотеку и исполняемый файл и затем запустив полученную программу, мы увидим следующее:
$ gcc — g — c — fPIC — Wall — c foo.c
$ gcc — g — shared — o libfoo.so foo.o
$ gcc — g — o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. /prog
main-xyz
В последней строчке мы видим, что определение xyz() из главной программы переопределяет (перекрывает) одноименную функцию в разделяемой библиотеке.
На первый взгляд это может показаться необычным, однако для такого поведения есть серьезные исторические причины. Первые реализации разделяемых библиотек были спроектированы так, что по умолчанию семантика процедуры разрешения символов в точности отражала ситуацию, когда приложение скомпоновано со статическими аналогами тех же библиотек. То есть в данном случае действует следующая логика:
• определение глобального символа в главной программе перекрывает определение в библиотеке;
• если глобальный символ определен сразу в нескольких библиотеках, то ссылка на него привязывается к первому определению, найденному в ходе сканирования библиотек, которое происходит в порядке их перечисления в командной строке во время статической компоновки (слева направо).
Приведенные правила обеспечивают относительно прямолинейный переход от статических к динамическим библиотекам, но могут вызвать ряд затруднений. Главная проблема такова: данная семантика несовместима с принципом, согласно которому разделяемая библиотека должна быть реализована в качестве самодостаточной подсистемы. Разделяемая библиотека по умолчанию не гарантирует, что ссылка на один из ее собственных глобальных символов будет привязана к ее же определению этого символа. Следовательно, свойства библиотеки могут измениться при включении ее в более крупный модуль. Это может привести к непредвиденным сбоям в приложении и усложнить раздельную отладку (например, когда вы пытаетесь воспроизвести проблему, используя другие разделяемые библиотеки или уменьшая их количество).
Для гарантии того, что в вышеописанном сценарии вызов xyz() в разделяемой библиотеке приведет к запуску именно той функции, которая в этой библиотеке определена, на этапе сборки компоновщику можно передать параметр — Bsymbolic:
$ gcc — g — c — fPIC — Wall — c foo.c