Где linux ищет библиотеки
Перейти к содержимому

Где linux ищет библиотеки

  • автор:

Где linux ищет библиотеки

Как уже неоднократно упоминалось в предыдущей главе, библиотека — это набор скомпонованных особым образом объектных файлов. Библиотеки подключаются к основной программе во время линковки. По способу компоновки библиотеки подразделяют на архивы (статические библиотеки, static libraries) и совместно используемые (динамические библиотеки, shared libraries). В Linux, кроме того, есть механизмы динамической подгрузки библиотек. Суть динамической подгрузки состоит в том, что запущенная программа может по собственному усмотрению подключить к себе какую-либо библиотеку. Благодаря этой возможности создаются программы с подключаемыми плагинами, такие как XMMS. В этой главе мы не будем рассматривать динамическую подгрузку, а остановимся на классическом использовании статических и динамических библиотек.

С точки зрения модели КИС, библиотека — это сервер. Библиотеки несут в себе одну важную мысль: возможность использовать одни и те же механизмы в разных программах. В Linux библиотеки используются повсеместно, поскольку это очень удобный способ «не изобретать велосипеды». Даже ядро Linux в каком-то смысле представляет собой библиотеку механизмов, называемых системными вызовами.

Статическая библиотека — это просто архив объектных файлов, который подключается к программе во время линковки. Эффект такой же, как если бы вы подключали каждый из файлов отдельно.

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

Рассмотрим преимущества и недостатки статических и совместно используемых библиотек. Статические библиотеки делают программу более автономной: программа, скомпонованная со статической библиотекой может запускаться на любом компьютере, не требуя наличия этой библиотеки (она уже «внутри» бинарника). Программа, скомпонованная с динамической библиотекой, требует наличия этой библиотеки на том компьютере, где она запускается, поскольку в бинарнике не код, а ссылка на код библиотеки. Не смотря на такую зависимость, динамические библиотеки обладают двумя существенными преимуществами. Во-первых, бинарник, скомпонованный с совместно используемой библиотекой меньше размером, чем такой же бинарник, с подключенной к нему статической библиотекой (статически скомпонованный бинарник). Во-вторых, любая модернизация динамической библиотеки, отражается на всех программах, использующих ее. Таким образом, если некоторую библиотеку foo используют 10 программ, то исправление какой-нибудь ошибки в foo или любое другое улучшение библиотеки автоматически улучшает все программы, которые используют эту библиотеку. Именно поэтому динамические библиотеки называют совместно используемыми. Чтобы применить изменения, внесенные в статическую библиотеку, нужно пересобрать все 10 программ.

В Linux статические библиотеки обычно имеют расширение .a (Archive), а совместно используемые библиотеки имеют расширение .so (Shared Object). Хранятся библиотеки, как правило, в каталогах /lib и /usr/lib. В случае иного расположения (относится только к совместно используемым библиотекам), приходится немного «подшаманить», чтобы программа запустилась.

3.2. Пример статической библиотеки

Теперь давайте создадим свою собственную библиотеку, располагающую двумя функциями: h_world() и g_world(), которые выводят на экран «Hello World» и «Goodbye World» соответственно. Начнем со статической библиотеки.

Начнем с интерфейса. Создадим файл world.h:

 /* world.h */ void h_world (void); void g_world (void);  

Здесь просто объявлены функции, которые будут использоваться.

Теперь надо реализовать серверы. Создадим файл h_world.c:

 /* h_world.c */ #include #include "world.h" void h_world (void) 

Теперь создадим файл g_world.c, содержащий реализацию функции g_world():

 /* g_world.c */ #include #include "world.h" void g_world (void) 

Можно было бы с таким же успехом уместить обе функции в одном файле (hello.c, например), однако для наглядности мы разнесли код на два файла.

Теперь создадим файл main.c. Это клиент, который будет пользоваться услугами сервера:

 /* main.c */ #include "world.h" int main (void) 

Теперь напишем сценарий для make. Для этого создаем Makefile:

 # Makefile for World project binary: main.o libworld.a gcc -o binary main.o -L. -lworld main.o: main.c gcc -c main.c libworld.a: h_world.o g_world.o ar cr libworld.a h_world.o g_world.o h_world.o: h_world.c gcc -c h_world.c g_world.o: g_world.c gcc -c g_world.c clean: rm -f *.o *.a binary  

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

 $ make gcc -c main.c gcc -c h_world.c gcc -c g_world.c ar cr libworld.a h_world.o g_world.o gcc -o binary main.o -L. -lworld $ 

Осталось только проверить, работает ли программа и разобраться, что же мы такое сделали:

 $ ./binary Hello World Goodbye World $ 

Итак, в приведенном примере появились три новые вещи: опции -l и -L компилятора, а также команда ar. Начнем с последней. Как вы уже догадались, команда ar создает статическую библиотеку (архив). В нашем случае два объектных файла объединяются в один файл libworld.a. В Linux практически все библиотеки имеют префикс lib.

Как уже говорилось, компилятор gcc сам вызывает линковщик, когда это нужно. Опция -l, переданная компилятору, обрабатывается и посылается линковщику для того, чтобы тот подключил к бинарнику библиотеку. Как вы уже заметили, у имени библиотеки «обрублены» префикс и суффикс. Это делается для того, чтобы создать «видимое безразличие» между статическими и динамическими библиотеками. Но об этом речь пойдет в других главах книги. Сейчас важно знать лишь то, что и библиотека libfoo.so и библиотека libfoo.a подключаются к проекту опцией -lfoo. В нашем случае libworld.a «урезалось» до -lworld.

Опция -L указывает линковщику, где ему искать библиотеку. В случае, если библиотека располагается в каталоге /lib или /usr/lib, то вопрос отпадает сам собой и опция -L не требуется. В нашем случае библиотека находится в репозитории (в текущем каталоге). По умолчанию линковщик не просматривает текущий каталог в поиске библиотеки, поэтому опция -L. (точка означает текущий каталог) необходима.

3.3. Пример совместно используемой библиотеки

Для того, чтобы создать и использовать динамическую (совместно используемую) библиотеку, достаточно переделать в нашем проекте Makefile.

 # Makefile for World project binary: main.o libworld.so gcc -o binary main.o -L. -lworld -Wl,-rpath,. main.o: main.c gcc -c main.c libworld.so: h_world.o g_world.o gcc -shared -o libworld.so h_world.o g_world.o h_world.o: h_world.c gcc -c -fPIC h_world.c g_world.o: g_world.c gcc -c -fPIC g_world.c clean: rm -f *.o *.so binary  

Внешне ничего не изменилось: программа компилируется, запускается и выполняет те же самые действия, что и в предыдущем случае. Изменилась внутренняя суть, которая играет для программиста первоочередную роль. Рассмотрим все по порядку.

Правило для сборки binary теперь содержит пугающую опцию -Wl,-rpath,. Ничего страшного тут нет. Как уже неоднократно говорилось, компилятор gcc сам вызывает линковщик ld, когда это надо и передает ему нужные параметры сборки, избавляя нас от ненужной платформенно-зависимой волокиты. Но иногда мы все-таки должны вмешаться в этот процесс и передать линковщику «свою» опцию. Для этого используется опция компилятора -Wl,option,optargs. Расшифровываю: передать линковщику (-Wl) опцию option с аргументами optargs. В нашем случае мы передаем линковщику опцию -rpath с аргументом . (точка, текущий каталог). Возникает вопрос: что означает опция -rpath? Как уже говорилось, линковщик ищет библиотеки в определенных местах; обычно это каталоги /lib и /usr/lib, иногда /usr/local/lib. Опция -rpath просто добавляет к этому списку еще один каталог. В нашем случае это текущий каталог. Без указания опции -rpath, линковщик «молча» соберет программу, но при запуске нас будет ждать сюрприз: программа не запустится из-за отсутствия библиотеки. Попробуйте убрать опцию -Wl,-rpath,. из Makefile и пересоберите проект. При попытке запуска программа binary завершится с кодом возврата 127 (о кодах возврата будет рассказано в последующих главах). То же самое произойдет, если вызвать программу из другого каталога. Верните обратно -Wl,-rpath. пересоберите проект, поднимитесь на уровень выше командой cd .. и попробуйте запустить бинарник командой world/binary. Ничего не получится, поскольку в новом текущем каталоге библиотеки нет.

Есть один способ не передавать линковщику дополнительных опций при помощи -Wl — это использование переменной окружения LD_LIBRARY_PATH. В последующих главах мы будем подробно касаться темы окружения (environment). Сейчас лишь скажу, что у каждого пользователя есть так называемое окружение (environment) представляющее собой набор пар ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, используемых программами. Чтобы посмотреть окружение, достаточно набрать команду env. Чтобы добавить в окружение переменную, достаточно набрать export ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, а чтобы удалить переменную из окружения, надо набрать export -n ПЕРЕМЕННАЯ. Будьте внимательны: export — это внутреннаяя команда оболочки BASH; в других оболочках (csh, ksh, . ) используются другие команды для работы с окружением. Переменная окружения LD_LIBRARY_PATH содержит список дополнительных «мест», разделенных двоеточиеями, где линковщих должен искать библиотеку.

Не смотря на наличие двух механизмов передачи информации о нестандартном расположении библиотек, лучше помещать библиотеки в конечных проектах в /lib и в /usr/lib. Допускается расположение библиотек в подкаталоги /usr/lib и в /usr/local/lib (с указанем -Wl,-rpath). Но заставлять конечного пользователя устанавливать LD_LIBRARY_PATH почти всегда является плохим стилем программирования.

Следующая немаловажная деталь — это процесс создания самой библиотеки. Статические библиотеки создаются при помощи архиватора ar, а совместно используемые — при помощи gcc с опцией -shared. В данном случае gcc опять же вызывает линковщик, но не для сборки бинарника, а для создания динамической библиотеки.

Последнее отличие — опциии -fPIC (-fpic) при компиляции h_world.c и g_world.c. Эта опция сообщает компилятору, что объектные файлы, полученные в результате компиляции должны содержать позиционно-независимый код (PIC — Position Independent Code), который используется в динамических библиотеках. В таком коде используются не фиксированные позиции (адреса), а плавающие, благодаря чему код из библиотеки имеет возможность подключаться к программе в момент запуска.

Использование библиотек в Ubuntu

У меня есть исходный код либы, я его собрал в двух вариантах(релиз и дебаг), после сборки двух вариантах генерятся сами либы и вспомогательный софт(графический софт чтобы проверить устройства подключенные) соответственно версии релиз и дебаг тоже . После сборки библиотеки (cmake->make -jn -> make install) у меня в папках релиз и дебаг появились 3 файла: bin, lib, include. В директории bin папки релиз лежат файлы с такими же именами что и в директории bin в папке дебаг, у них только размеры разные(из за разных режимов сборок). У меня вопрос, почему дебажные либы не помечаются как в винде буквой «d» как в виндовс? После сборки я добавил директории lib из папок релиз и дебаг в LD_LIBRARY_PATH. Когда начал проверять какие либы тянет софт( использовал lld), который лежит в папке бин, оказалось, что софт из директории bin папки релиз использует библиотеки, которые расположены в дебаге.
Как мне заставить использовать софт из директории bin папки релиз библиотеки, которые находятся в директории lib той жей папки релиз? (Либу которую я собираю intel realsense, JC: ubuntu 20.04, g++ 9.3) И как мне вообще заставить софт которые собран под дебаг использовать дебажные либы, а не релизные. Я хочу сделать так, как было в Windows на Visual Studio: когда пишешь код, то дебажная версия использует только либы версии дебаг, а релизная — релиз, соответственно при отладке использовалась дебажная версия бибилиотеки. Я новичок в использовании linux, недавно начал активно пользоваться.

Отслеживать
задан 27 мая 2021 в 9:46
71 4 4 бронзовых знака

в линукс не используются виндовые соглашения о именовании файлов. Я бы строго не рекомендовал добавлять одновременно и релиз и дебаг каталоги в пути поиска. Скорее всего Ваш софт умеет работать с правильными библиотеками, просто попробуйте запустить в чистом окружении.

27 мая 2021 в 9:53

@KoVadim Но как раз я и проверил с помощью команды lld и увидел что релизная версия тянет дебажную либу. Что значит запустить в чистом окружении?

27 мая 2021 в 10:07
например, в виртуалке
27 мая 2021 в 10:42

@KoVadim получается что бы запустить нужную версию софта с нужной версией либы, необходимо в пафе указывать только путь к нужной версией либы?

27 мая 2021 в 11:08

крайне желательно. Но вообще то в линуксе принятно хранить либы в правильных каталогах /usr/lib, /usr/local/libs и тому подобное. Это обычно и делает install — копирует либы туда. Понятно, что в момент разработки это не очень хорошая идея и поэтому используют LD_PRELOAD и LD_LIBRARY_PATH

Создание разделяемых библиотек

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


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

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

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

2.- Краткая история процесса создания программы.

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

В начале программы писались непосредственно в машинном коде. Позднее стало ясно, что можно писать программы на языке более высокого уровня, так как последующую трансляцию в машинный код можно автоматизировать благодаря систематической природе трансляции. Это увеличило производительность программ.

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

Скоро было замечено, что процесс компиляции очень трудоемок и отнимает очень много ресурсов, включая машинное время, и что многие функции, входящие в эти программы использовались еще и еще раз в различных программах. Более того, когда кто-то изменял часть программы, компиляция вставленного кода означала новую компиляцию всего исходного кода, включая новую трансляцию всего неизменившегося кода.

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

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

3.- Что такое библиотека?

Описанная выше проблема привела к созданию библиотек. Это ничто иное, как особый вид файла (если быть точным, то архив, tar(1) или cpio(1) ) с особыми параметрами, формат которых компоновщик понимает, и когда мы указываем ему библиотечный архив, КОМПОНОВЩИК ВЫБИРАЕТ ТОЛЬКО ТЕ МОДУЛИ, КОТОРЫЕ НУЖНЫ ПРОГРАММЕ и исключает все остальное. Появилось новое преимущество. Теперь можно было разрабатывать программы, которые используют большие библиотеки функций, и программисту вовсе не обязательно знать все зависимости функций в этой библиотеке.

Те библиотеки, которые мы пока обсудили, не пошли в развитии дальше. К ним только добавился файл, часто находящийся в начале архива, содержащий описания модулей и идентификаторы, которые компоновщик должен разрешать без чтения всей библиотеки (и тем самым устраняя необходимость в чтении библиотеки по нескольку раз). Этот процесс (добавление таблицы символов в архив библиотеки) в Линуксе выполняется командой ranlib(1). Описанные пока библиотеки известны как СТАТИЧЕСКИЕ БИБЛИОТЕКИ.

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

Развивая это последнее нововведение на один шаг дальше, кто-то (я не знаю, кто это был, но идея была отличная 😉 подумал, что очень часто многие программы используют одну и ту же библиотеку, но, будучи разными программами, части используемой библиотеки вовсе не обязательно были теми же частями, используемыми другой программой. Более того, основной код был другой (они же разные программы), поэтому их тексты не были разделяемыми. Что же, наш герой подумал, что если разные программы, использующие одну и ту же библиотеку, могли делить между собой эту библиотеку, то мы бы смогли выгадать немного памяти. Теперь разные программы, имея разный текст, работают с одним и тем же кодом библиотеки.

Однако теперь процесс стал сложнее. Исполняемая программа не полностью скомпонована, но разрешение ссылок на идентификаторы библиотек откладываются до момента загрузки программы. Компоновщик (в случае Линукса это ld(1)) распознает вызовы разделяемых библиотек и не включает их код в программу. Сама система, ядро, при исполнении exec() распознает запуск программы, использующей разделяемые библиотеки и исполняет специальный код, загружающий разделяемые библиотеки (выделяя разделяемую память для их текста, выделяя закрытую память для значений библиотеки и т.д.). Теперь этот процесс выполняется при загрузке исполняемого файла и вся процедура сильно усложнилась.

Конечно же, когда компоновщик встречается с обычной библиотекой, он продолжает вести себя также, как и раньше.

Разделяемая библиотека не является архивом, содержащим объектный код, скорее это файл, содержащий сам объектный код. Во время компоновки программы с разделяемой библиотекой, компоновщик не исследует библиотеку, какие модули надо добавлять в программу, а какие нет. Он только удостоверяется, что неразрешенные ссылки становятся разрешенными, и определяет, что необходимо добавить в список при включении библиотеки. Можно сделать архивную ar(1) библиотеку всех разделяемых библиотек, но часто это не делается, так как разделяемые библиотеки часто являются результатом компоновки различных модулей, поэтому библиотека понадобится позднее, во время исполнения. Возможно, название разделяемая библиотека не является самым уместным и было бы точнее назвать ее разделяемым объектом (тем не менее, поскольку нас могут не понять, мы не используем этот термин).

4.- Виды библиотек.

Как мы уже говорили, в Линукс существует два вида библиотек: статические и разделяемые. Статические библиотеки являются набором модулей, объединенных в архив при помощи утилиты ar(1) и проиндексированных утилитой ranlib(1). Эти модули часто хранятся в файле с окончанием .a (я не использую термин расширение потому, что в Линуксе концепция расширения файла не используется). Компоновщик распознает окончание файла .a и начинает искать модули, как если бы это была статическая библиотека, выбирает и добавляет в программу те модули, которые разрешают неразрешенные ссылки.

В отличие от статических, разделяемые библиотеки являются не архивами, а перемещаемыми объектами, обозначенными особым кодом (который обозначает их как разделяемые библиотеки). Компоновщик ld(1), как уже говорилось, не добавляет модули в программный код, а выбирает идентификаторы, предоставляемые библиотекой как разрешенные, добавляет те, которые необходимы самой библиотеке, и продолжает работу, не добавив никакого кода, считая, что требуемый код уже был добавлен в основной код. Компоновщик ld(1) распознает разделяемые библиотеки по окончанию .so (не .so.xxx.yyy, мы обсудим этот вопрос позднее).

5.- Процесс компоновки в Линукс.

Каждая программа состоит из объектных модулей, скомпонованных в исполняемый файл. Эту операцию выполняет используемый в Линуксе компоновщик ld(1).

ld(1) поддерживает различные опции, которые изменяют его поведение, но мы ограничимся здесь только теми, которые связаны с использованием библиотек в общем. ld(1) вызывается не непосредственно пользователем, а самим компилятором gcc(1) на его завершающей стадии. Небольшие познания о его modus operandis помогут нам понять способ использования библиотек в Линукс.

Для правильной работы ld(1) требуется список объектов, которые необходимо скомпоновать с программой. Эти объекты можно задавать в любом порядке (*) пока мы выполняем указанное выше соглашение, как уже говорилось, разделяемая библиотека распознается по окончанию .so (а не .so.xx.yy) и статическая библиотека по .a (и конечно же, простыми объектными файлами являются те, чье имя заканчивается на .o).

(*) Это не совсем верно. ld(1) включает только те модули, которые разрешают ссылки на момент включения библиотеки, поэтому в модуле, влкюченном позднее, все равно может быть ссылка, которая позднее, поскольку это не проявляется на момент включения этой библиотеки, может вызвать команду на влкючение необходимых библиотек.

С другой стороны, ld(1) позволяет включать стандартные библиотекии благодаря опциям -l и -L.

Но. Что мы понимаем под стандартной библиотекой, в чем разница? Никакой. Только то, что ld(1) ищет стандартные библиотеки в определенных местах, тогда как те, что описаны в списке параметров как объекты, ищутся по именам их файлов.

По умолчаиню библиотеки ищутся в каталогах /lib и /usr/lib (хотя я слышал, что в зависимости от версии/реализации ld(1) , могут быть дополнительные каталоги). -L позволяет нам добавить каталоги к тем, которые просматриваются при обычном поиске библиотек. Она используется заданием -L каталог для каждого каталога , который мы хотим добавить. Стандартные библиотеки указываются опцией -l Имя (где Имя указывает библиотеку, которую необходимо загрузить) и ld(1) будет искать, в соответствующем порядке, в соответствующих каталогах файл с именем libИмя.so. Если он не будет найден, то будет сделана попытка найти libИмя.a , его статическую версию.

Если ld(1) находит файл libИмя.so , он скомпонует ее как разделяемую библиотеку, тогда как если он найдет файл libИмя.a, он скомпонует модули, полученные из нее, если они разрешают любые неразрешенные ссылки.

6.- Динамическая компоновка и загрузка разделяемых библиотек

Динамическая компоновка выполняется в момент загрузки исполняемого файла особым модулем (на самом деле этот особый модуль является самой разделяемой библиотекой), называемым /lib/ld-linux.so .

На самом деле существуют два модуля для компоновки динамических библиотек: /lib/ld.so (для библиотек, использующих старый формат a.out) и /lib/ld-linux.so (для библиотек, использующих новый формат ELF).

Особенность этих модулей заключается в том, что они должнны загружаться каждый раз, когда происходит динамическая компоновка программ. Их имена стандартны (причина в том, что их нельзя перемещать из каталога /lib , а также нельзя изменять их имена). Если мы заменим имя /etc/ld-linux.so , то мы автоматически остановим использование любой программы, использующей разделяемые библиотеки, поскольку этот модуль отвечает за разрешение всех ссылок, не разрешенных во время исполнения.

Последнему модулю помогает существование файла /etc/ld.so.cache , в котором для каждой библиотеки указываются наиболее подходящий исполняемый файл, содержащий эту библиотеку. Мы вернемся к этой теме позднее.

7.- soname. Версии исполняемых библиотек. Совместимость.

Мы подошли к наиболее запутанной теме, связанной с разделяемыми библиотеками: их версии.

Часто встречается сообщение ‘library libX11.so.3 not found,’ оставляющее нас в растеряности: обладая библиотекой libX11.so.6 мы неспособны ничего сделать. Как стало возможным, что ld.so(8) признает взаимозаменяемыми библиотеки libpepe.so.45.0.1 и libpepe.so.45.22.3 и не признает libpepe.so.46.22.3?

В Линукс (и во всех операционных системах, использующих формат ELF) библиотеки идентифицируются отличающей их последовательностью символов: soname.

soname включается в саму библиотеку и эта последовательность определяется при компоновке объектов, формирующих библиотеку. При создании разделяемой библиотеки, чтобы дать значение этой символьной строке необходимо передать ld(1) опцию ( -soname ).

Эта последовательность символов используется динамическим загрузчиком для идентификации разделяемой библиотеки, которую необходимо загрузить, и идентификации исполняемого файла. Это выглядит примерно так:
Ld-linux.so определяет, что программа требует библиотеку и определяет ее soname. Затем идет поиск имени в /etc/ld.so.cache и определяется имя файла, содержащего эту библиотеку. Далее запрошенное soname сравнивается с именем существующей библиотеки, и если они идентичны, то значит она нам и нужна! Если нет, то поиск будет продолжаться до тех пор, пока она не будет найдена, или, если она не будет найдена, будет выдано сообщение об ошибке.

По soname можно определить, подходит ли библиотека для загрузки, потому что ld-linux.so проверяет, совпадает ли требуемое soname с требуемым файлом. В случае различия мы можем получить знаменитое ‘libXXX.so.Y not found’ . Ищется именно soname и выдаваемая ошибка определяется soname.

Если мы поменяем имя библиотеки, может возникнуть большая путаница, при этом сама проблема останется. Но изменять soname тоже не очень хорошая идея, потому что в сообществе Линукс есть соглашение по назначению soname:

soname библиотекии, по умолчанию, должно идентифицировать соответствующую библиотеку и ИНТЕРФЕЙС этой библиотеки. Если мы внесем изменения в библиотеку, которые затрагивают только внутреннюю функциональность, но интерфейс останется неизменным (количество функциий, переменных, параметры функций), то две библиотеки будут взаимозаменяемыми и в целом мы скажем, что изменения были незначительными (обе библиотеки совместимы и мы можем заменить одну на другую). Если это происходит, то часто изменяется минорный номер (который не входит в состав soname) и библиотека может быть заменена без значительных проблем.

Однако, когда мы добавляем функции, убираем функции и в целом ИЗМЕНЯЕМ ИНТЕРФЕЙС библиотеки, то уже невозможно утверждать, что эта библиотека взаимозаменяема с предыдущей (например замена libX11.so.3 на libX11.so.6 является частью перехода с X11R5 на X11R6, при этом вводятся новые функциии и поэтому изменяется интерфейс). Переход с X11R6-v3.1.2 на X11R6-v3.1.3 вероятно не вызовет изменений в интерфейсе и у библиотеки останется то же soname — хотя, чтобы сохранить старую версию, нам потребуется дать ей другое имя (по этой причине номер версии завершает имя библиотеки, тогда как в soname задаются только мажорные номера).

8.- ldconfig(8)

Как мы уже говорили раньше, /etc/ld.so.cache позволяет ld-linux.so конвертировать soname файла, содержащегося в библиотеке. Это бинарный (для большей эффективности) файл, созданный утилитой ldconfig(8) .
ldconfig(8) создает для каждой динамической библиотеки, найденной в каталогах, указанных в /etc/ld.so.conf , символическую ссылку с именем библиотеки soname. В этом случае, когда ld.so хочет получить имя файла, то что он делает, это выбирает в списке каталогов файл с требуемым soname. И поэтому нет необходимости каждый раз запускать ldconfig(8) при добавлении библиотеки. Мы запускаем ldconfig только когда мы добавляем каталог к списку.

9.- Я хочу сделать динамическую библиотеку.


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

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

Хорошим примером динамической библиотеки является стандартная библиотека C (она используется всеми программами, написанными на C). В среднем используются все функции.

В статическую библиотеку вовсе не обязательно включать нечасто используемые функции; пока они содержатся в своем собственном модуле, они не будут скомпонованы ни с одной программой, которой они не требуются.

9.1.- Компиляция исходных кодов

Компиляция исходных кодов выполняется точно также, как и в случае с обычным исходным кодом, за исключением того, что для создания кода, который можно загружать в различных позициях в пространстве виртуальных адресов процесса, мы будем использовать опцию ‘-f PIC’ (позиционно-независимый код).

Этот шаг является фундаментальным, так как в статической библиотеке положение библиотечных объектов разрешается при компоновке, поэтому занимает фиксированное время. Выполнить этот шаг в старых исполняемых файлах a.out было невозможно, что приводило к размещению каждой разделяемой библиотеки в фиксированном положении в пространстве виртуальных адресов. И как следствие, в любой момент могли возникнуть конфликты, если программа хотела использовать две библиотеки и загружала их в перекрывающиеся области виртуальной памяти. Это означало, что вы были вынуждены вести список, в котором каждый, захотевший сделать библиотеку динамической, должен был объявить диапазон используемых адресов с тем, чтобы никто другой не мог им воспользоваться.

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

9.2.- Компоновка объектов в библиотеку

После компиляции всех объектов, их необходимо скомпоновать, задав опцию, которая создает динамически загружаемый объект.

gcc -shared -o libИмя.so.xxx.yyy.zzz -Wl,-soname,libИмя.so.xxx


    -shared .
    Здесь компоновщику говорится, что в итоге он должен созадть разделяемую библиотеку и поэтому в выходном файле, соответствующем библиотеке, должен содержаться исполняемый код.

-o libИмя.so.xxx.yyy.zzz .
Это имя выходного файла. Вовсе не обязательно следовать соглашению по имени, но если мы хотим, чтобы эта библиотека стала стандартом для будущих разработок, то лучше им следовать.

-Wl,-soname,libИмя.so.xxx .
Опция -Wl говорит gcc(1) , что далее идут опции (разделенные запятыми), которые предназначены для компоновщика. Этот механизм используется gcc(1) для передачи опций ld(1) . В примере мы передаем компоновщику следующие опции:

-soname libИмя.so.xxx
9.3.- Установка библиотеки

Что ж, у нас уже есть соответствующий исполняемый файл. Теперь, чтобы его можно было использовать, его необходимо установить в соответствующее место.

Для компиляции программы, которая требует нашу новую библиотеку, необходимо использовать следующую команду:

gcc -o program libИмя.so.xxx.yyy.zzz

или, если библиотека была установлена в каталог (/usr/lib) , будет достаточно:

gcc -o программа -lИмя


    Скопируйте библиотеку в каталог /lib или /usr/lib . Если вы решите скопировать ее в другое место (например в /usr/local/lib) , то вы не сможете быть уверенными, что при компоновке программ компоновщик ld(1) найдет ее автоматически.

Выполните ldconfig(1) для создания символьной ссылки libИмя.so.xxx.yyy.zzz на libИмя.so.xxx. На этом шаге нам станет известно, правильно ли мы выполнили все предыдущие шаги и распознается ли библиотека как динамическая. На этом шаге оказывается влияние только на загрузку библиотеки во время исполнения, а не на компоновку программ.

10.- Создание статической библиотеки

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

Замечание: Компоновщик при поиске библиотек сначала ищет файл с именем libИмя.so , и лишь затем libИмя.a. Если мы назовем обе этих библиотеки (статическую и динамическую версии) одним и тем же именем, в общем-то будет невозможно определить, какая из двух будет скомпонована в каждом случае (динамическая всегда компонуется первой, поскольку компоновщик обнаруживает ее первой).

По этой причине, если необходимо иметь две версии одной библиотеки, всегда рекомендуется называть статическую в виде libИмя_s.a , а динамическую libИмя.so . Тогда при компоновке нужно будет указать:

gcc -o program -lИмя_s

для компоновки со статической версией, тогда как для динамической:

gcc -o program -lИмя
10.1.- Компиляция исходного кода

Для компиляции исходного кода вовсе не обязательно принимать каких-либо особых мер. Точно также положение объектов решается на стадии компоновки, не обязательно компилировать с опцией -f PIC (хотя возможно продолжать ее использовать).

10.2.- Компоновка объектов в библиотеку

В случае статических библиотек компоновка не выполняется. Все объекты архивируются в библиотечный файл командой ar(1) . Далее, чтобы быстро разрешить символы желательно выполнить команду ranlib(1) над библиотекой. Хотя это и не обязательно, невыполнение этой команды может разкомпоновать модули в исполняемом файле потому, что при обработке модуля компоновщиком во время создания библиотеки не все непрямые зависимости между модулями разрешаются немедленно: скажем, модулю, находящемуся в конце архива, требуется другой модуль, находящийся в начале архива, это означает, что для разрешения всех ссылок требуется несколько проходов по одной и той же библиотеке.

10.3.- Установка библиотеки

Статические библиотеки желательно называть в формате libName.a только в том случае, если есть желание иметь только статические библиотеки. В случае двух видов библиотек я бы порекомендовал называть их libИмя_s.a , с тем, чтобы было легче различать, когда загружать статическую, а когда динамическую библиотеку.

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

11.- Сравнение статической и динамической компоновки

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

Создать такую программу можно двумя способами. Первый заключается в создании статически скомпонованного исполняемого файла (используя только библиотеки .a и не используя динамического загрузчика). Этот вид программ загружается один раз и не требуют ни одной библиотеки из системы (даже /lib/ld-linux.so) . Однако у них есть недостаток — все необходимое нужно держать в одном бинарном файле и поэтому это обычно очень большие файлы. Вторым вариантом является создание динамически скомпонованной программы, то есть в среде, в котором наше приложение будет выполняться, должны быть все соответсвующие динамические библиотеки. Исполняемый файл может быть очень маленьким, хотя иногда невозможно иметь абсолютно все библиотеки (например есть люди, у которых нет Motif).

Существует третий вариант, смешанный, в котором некоторые библиотеки скомпонованны динамически, а остальные статически. В этом случае было бы логично выбрать конфликтующую библиотеку в ее статической форме и все остальные в их динамической форме. Этот вариант является очень распространенной формой дистрибутива программ.

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

gcc -static -o program.static program.o -lm_s -lXm_s -lXt_s -lX11_s\ -lXmu_s -lXpm_s gcc -o program.dynamic program.o -lm -lXm -lXt -lX11 -lXmu -lXpm gcc -o program.mixed program.o -lm -lXm_s -lXt -lX11 -lXmu -lXpm


    Прочтите ELF-HOWTO.


    Перевод на русский: Владимир Попов
    © 1998 Luis Colorado
    This website is mantained by Miguel A Sepulveda .

    Где linux ищет библиотеки

    О создании собственных библиотек мы поговорим в следующем разделе, а пока о том, как использовать уже существующие библиотеки со своими приложениями. Для использования библиотеки со своим приложением, объектный код приложения должен быть скомпонован с отдельными объектными модулями, извлекаемыми из библиотеки. Этой работой занимается компоновщик (линкер) из комплекта программ проекта GCC, вот как проделывается это в два шага, детализировано, двумя командами — на примере любого простейшего приложения (это могут быть показанные две последовательные команды в терминале, или две строки сценария Makefile ): 3

    $ gcc -c hello.c -o hello.o $ ld /lib/crt0.o hello.o -lc -o hello 

    Первая команда только компилирует (ключ -c ) С-код приложения ( hello.c ) в объектный вид, а вторая команда вызывает компоновщик, имя которого ld , и который компонует полученный объектный файл с
    а). стартовым объектным модулем, указанным абсолютным именем /lib/crt0.o и
    б). со всеми необходимыми объектными модулями функций из стандартной библиотеки С — libc.so
    (о том, как соотносятся имя библиотеки, указываемое в ключах сборки, и имя файла библиотеки — смотрите чуть ниже).

    Но обычно компоновщик не вызывается явно, он вызывается неявно из gcc и с требуемыми умалчиваемыми параметрами. Вот форма, полностью эквивалентная предыдущей:

    $ gcc -c hello.c -o hello.o $ gcc hello.o -o hello

    — здесь gcc, вызвав компоновщик ld , передаст ему и имя стартового объектного модуля ( /lib/crt0.o ) и имя стандартной библиотеки С ( libc.so ) как параметры по умолчанию. Эквивалентным (по результату) будет и вызов:

    $ gcc hello.c -o hello

    — свёрнутый в одну строку, в котором сначала вызывается компилятор, а затем компоновщик, эту форму мы уже неоднократно видели, но за ней скрывается весь механизм, который мы только-что развёрнуто рассмотрели.

    В зависимости от того, какой формы библиотеки используются для сборки приложения, они могут (должны) компоноваться с приложением (способ компоновки определяется ключом -B ):

    Причём, в смешанной записи статические и динамические библиотеки могут чередоваться произвольное число раз:

    Если способ связывания не определён в командной строке (не указан ключ -B), то по умолчанию предполагается динамический способ связывания.

    Теперь относительно имён библиотек и файлов. В качестве значения опции -l мы указываем имя библиотеки. Но сами библиотеки содержатся в файлах! Имена файлов, содержащих статические библиотеки, имеет расширение .a (archive), а файлов, содержащих динамические библиотеки — .so (shared objects). А само имя файла библиотеки образуется конкатенацией префикса lib и имени библиотеки. Таким образом, в итоге, если мы хотим скомпоновать свою программу prog.c с разделяемой библиотекой с именем xxx , то мы предполагаем наличие (на путях поиска) библиотечного файла с именем libxxx.so , и записываем команду компиляции так:

    $ gcc prog.c -lxxx -oprog 

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

    $ gcc prog.c -Bstatic -lxxx -oprog 

    Посмотреть на какие файлы разделяемых библиотек ссылается уже собранная бинарная (формата ELF) программа можно командой:

    $ ldd hello linux-gate.so.1 => (0x00f1b000) libc.so.6 => /lib/libc.so.6 (0x00b56000) /lib/ld-linux.so.2 (0x00b33000)

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

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

    $ man ld LD(1) GNU Development Tools LD(1) NAME ld - The GNU linker SYNOPSIS .

    Это обычно /lib и /usr/lib .

    Последовательность (в порядке приоритетов) поиска библиотек компоновщиком:

    1. По путям, указанным ключом -L
    2. По путям, указанным списком в переменной окружения LD_LIBRARY_PATH
    3. По стандартным путям: /lib и /usr/lib
    4. По путям, которые сохранены в кэше загрузчика ( /etc/ld.so.cache )
    $ strings '/etc/ld.so.cache' | head -n8 ld.so-1.7.0 glibc-ld.so.cache1.1 libzrtpcpp-1.4.so.0 /usr/lib/libzrtpcpp-1.4.so.0 libzltext.so.0.13 /usr/lib/libzltext.so.0.13 libzlcore.so.0.13 /usr/lib/libzlcore.so.0.13 .

    Как попадают пути в кэш загрузчика? Посредством утилиты обновления (добавления) ldconfig из файла /etc/ld.so.conf . но в новых системах этот файл фактически пустой:

    $ cat /etc/ld.so.conf include ld.so.conf.d/*.conf

    — и включает в себя последовательное содержимое всех файлов каталога /etc/ld.so.conf.d . Поэтому, информация для обновления кэша накапливается из файлов этого каталога:

    $ ls /etc/ld.so.conf.d mysql-i386.conf qt4-i386.conf qt-i386.conf usr-local-lib.conf xulrunner-32.conf

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

    $ cat /etc/ld.so.conf.d/usr-local-lib.conf /usr/local/lib

    Таким образом и каталог /usr/local/lib попадает в пути загрузки. Обычно ldconfig запускается последним шагом инсталляции программных пакетов (особенно из исходников), но не лишне бывает выполнить его и вручную, а детали вызова смотрим справкой:

    $ ldconfig --help Использование: ldconfig [КЛЮЧ. ] Конфигурирует связи времени выполнения для динамического компоновщика. .

    Это всё касалось поиска любых (статических и динамических) библиотек на этапе компоновки. Но для динамических библиотек нужен ещё поиск периода старта приложения, использующего библиотеку (библиотека могла быть, например, перенесена со времени сборки приложения). Такой поиск производится функциями (динамического линкера), содержащимися в динамической библиотеке /lib/ld-2.13.so , с которой компонуется по умолчанию (без нашего вмешательства) любая программа, использующая разделяемые библиотеки. Требуемые приложению библиотеки ищутся на путях, указанных в ключах -rpath и -rpath-link при сборке программы, а затем по значению путей в списке переменной окружения с именем LD_RUN_PATH . Далее проводится поиск по п.п. 3,4 показанных ранее. Существует два различных метода использования динамической библиотеки из приложения (оба метода берут свое начало в Sun Solaris). Первый способ – это динамическая компоновка вашего приложения с совместно используемой библиотекой. Это более традиционный способ который мы уже неявно начали рассматривать. При этом способе загрузку библиотеки при запуске приложения берёт на себя операционная система. Второй называют динамической загрузкой. В этом случае программа явно загружает нужную библиотеку, а затем вызывает определенную библиотечную функцию (или набор функций). На этом втором методе обычно основан механизм загрузки подключаемых программных модулей – плагинов. Этот способ может быть иногда особенно интересен разработчикам встраиваемого оборудования. Компоновщик не получает никакой информации о библиотеках и обьектах в ходе компоновки. Библиотека libdl.so (компонуемая к приложению) предоставляет интерфейс к динамическому загрузчику. Этот интерфейс составляют 4 функции: dlopen(), dlsym(), dlclose() и dlerror() . Первая принимает на вход строку с указанием имени библиотеки для загрузки, загружает библиотеку в память (если она еще не была загружена) или увеличивает количество ссылок на библиотеку (если она уже найдена в памяти). Возвращает дескриптор, который потом используется в функциях dlsym() и dlclose() . Функция dlclose() , соответственно, уменьшает счетчик ссылок на библиотеку и выгружает ее, если счетчик становится равным 0. Функция dlsym() по имени функции возвращает указатель на ее код. Функция dlopen() в качестве второго параметра получает управляющий флаг, определяющий детали загрузки библиотеки. Он может иметь следующие значения: RTLD_LAZY — разрешение адресов по именам объектов происходит только для используемых объектов (это не относится к глобальным переменным — они разрешаются немедленно, в момент загрузки). RTLD_NOW — разрешение всех адресов происходит до возврата из функции. RTLD_GLOBAL — разрешает использовать объекты, определенные в загружаемой библиотеке для разрешения адресов из других загружаемых библиотек. RTLD_LOCAL — запрещает использовать объекты из загружаемой библиотеки для разрешения адресов из других загружаемых библиотек. RTLD_NOLOAD — указывает не загружать библиотеку в память, а только проверить ее наличие в памяти. При использовании совместно с флагом RTLD_GLOBAL позволяет «глобализовать» библиотеку, предварительно загруженную локально ( RTLD_LOCAL ). RTLD_DEEPBIND — помещает таблицу загружаемых объектов в самый верх таблицы глобальных символов. Это гарантирует использование только своих функций и нивелирует их переопределение функций другими библиотеками. RTLD_NODELETE — отключает выгрузку библиотеки при уменьшении счетчика ссылок на нее до 0. Изложенной информации более чем достаточно, чтобы скомпоновать наше собственное приложение с любой, уже имеющейся в нашем распоряжении, библиотекой объектных модулей.

    3. Конкретный пути и имена стартового объектного файла, и указание стандартной библиотеки в командной строке — могут заметно разниться от версии к версии и дистрибутива системы, показанная командная строка взята из CentOS 5.2 (ядро 2.6.18).

    Предыдущий раздел: Оглавление Следующий раздел:
    Библиотеки: использование Библиотеки: построение

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *