Общая библиотека Linux, чем открыть
Обновлено: 21.11.2024
Одна из проблем с разработанными программами заключается в том, что они имеют тенденцию становиться все больше и больше, увеличивая общую компиляцию и связывая время с большим числом, а также загрязняя make-файл и каталог, в котором мы разместили исходные файлы. Первый раз, когда написанная нами программа достигает этого состояния, обычно это происходит, когда мы ищем другой способ управления нашими проектами.
Именно здесь мы начинаем думать об объединении исходного кода в небольшие блоки связанных файлов, которыми можно управлять с помощью отдельного make-файла, возможно, другим программистом (для проекта с несколькими программистами).
Что такое библиотека "C"? Для чего это нужно?
Одним из инструментов, которые нам предоставляют компиляторы, являются библиотеки. Библиотека — это файл, содержащий несколько объектных файлов, которые можно использовать как единое целое на этапе компоновки программы. Обычно библиотеки индексируются, поэтому в них легко найти символы (функции, переменные и т.д.). По этой причине компоновка программы, объектные файлы которой упорядочены в библиотеках, выполняется быстрее, чем компоновка программы, объектные файлы которой находятся на диске отдельно. Кроме того, при использовании библиотеки нам нужно искать и открывать меньше файлов, что еще больше ускоряет связывание.
Системы Unix (как и большинство других современных систем) позволяют нам создавать и использовать два типа библиотек — статические библиотеки и разделяемые (или динамические) библиотеки.
Статические библиотеки — это просто наборы объектных файлов, которые подключаются к программе на этапе компоновки компиляции и не имеют значения во время выполнения. Этот последний комментарий кажется очевидным, так как мы уже знаем, что объектные файлы также используются только на этапе компоновки и не требуются во время выполнения - для запуска программы необходим только исполняемый файл программы.
Общие библиотеки (также называемые динамическими библиотеками) подключаются к программе в два этапа. Во-первых, во время компиляции компоновщик проверяет, что все символы (опять же, функции, переменные и т.п.), необходимые программе, связаны либо с программой, либо с одной из ее разделяемых библиотек. Однако объектные файлы из динамической библиотеки не вставляются в исполняемый файл. Вместо этого, когда программа запускается, программа в системе (называемая динамическим загрузчиком) проверяет, какие общие библиотеки были связаны с программой, загружает их в память и прикрепляет к копии программы в памяти.
Сложная фаза динамической загрузки немного замедляет запуск программы, но это очень незначительный недостаток, который перевешивается большим преимуществом - если выполняется вторая программа, связанная с той же общей библиотекой, она может использовать ту же копию разделяемой библиотеки, что экономит много памяти. Например, стандартная библиотека «C» обычно является общей библиотекой и используется всеми программами на C. Тем не менее, в любой момент времени в памяти хранится только одна копия библиотеки. Это означает, что мы можем использовать гораздо меньше памяти для запуска наших программ, а исполняемые файлы намного меньше, что также экономит много места на диске.
Однако у такой схемы есть один недостаток. Если мы перекомпилируем динамическую библиотеку и попытаемся запустить вторую копию нашей программы с новой библиотекой, мы скоро застрянем — динамический загрузчик обнаружит, что копия библиотеки уже хранится в памяти, и, таким образом, прикрепить его к нашей программе, а не загружать новую (модифицированную) версию с диска. Есть и другие способы обойти это, как мы увидим в последнем разделе нашего обсуждения.
Создание статической библиотеки "C" с использованием "ar" и "ranlib"
Основным инструментом, используемым для создания статических библиотек, является программа под названием «ar», что означает «архиватор». Эту программу можно использовать для создания статических библиотек (которые на самом деле являются архивными файлами), изменения объектных файлов в статической библиотеке, перечисления имен объектных файлов в библиотеке и т. д. Чтобы создать статическую библиотеку, мы можем использовать такую команду:
ar rc libutil.a util_file.o util_net.o util_math.o
Эта команда создает статическую библиотеку с именем «libutil.a» и помещает в нее копии объектных файлов «util_file.o», «util_net.o» и «util_math.o». Если файл библиотеки уже существует, к нему добавляются или заменяются объектные файлы, если они новее, чем файлы внутри библиотеки. Флаг 'c' указывает ar создать библиотеку, если она еще не существует. Флаг 'r' указывает заменить старые объектные файлы в библиотеке новыми объектными файлами.
После создания или изменения архива его необходимо проиндексировать. Позже этот индекс используется компилятором для ускорения поиска символов внутри библиотеки и для того, чтобы гарантировать, что порядок символов в библиотеке не будет иметь значения во время компиляции (это станет лучше понятно, когда мы более подробно рассмотрим процесс связывания в конце этого руководства). Команда, используемая для создания или обновления индекса, называется ranlib и вызывается следующим образом:
В некоторых системах архиватор (который не всегда является ar ) уже позаботится об индексе, поэтому ranlib не нужен (например, когда компилятор Sun C создает архив, он уже проиндексирован). Однако, поскольку 'ar' и 'ranlib' используются многими make-файлами для многих пакетов, такие платформы, как правило, предоставляют команду ranlib, которая ничего не делает. Это помогает использовать один и тот же make-файл на обоих типах платформ.
- Используйте ranlib для повторного создания индекса.
- При копировании файла архива в другое место используйте 'cp -p' вместо только 'cp'. Флаг '-p' указывает 'cp' сохранить все атрибуты файла, включая права доступа, владельца (если 'cp' вызывается суперпользователем) и дату его последней модификации. Это заставит компилятор думать, что индекс внутри файла все еще обновляется. Этот метод полезен для make-файлов, которым по какой-то причине необходимо скопировать библиотеку в другой каталог.
Использование библиотеки "C" в программе
После того как мы создали наш архив, мы хотим использовать его в программе. Это делается путем добавления имени библиотеки в список имен объектных файлов, переданных компоновщику, с использованием специального флага, обычно '-l'. Вот пример:
cc main.o -L. -lutil -o программа
При этом будет создана программа, использующая объектный файл "main.o" и любые требуемые символы из статической библиотеки "util". Обратите внимание, что мы опустили префикс «lib» и суффикс «.a» при упоминании библиотеки в команде ссылки. Компоновщик присоединяет эти части обратно к имени библиотеки, чтобы создать имя файла для поиска. Обратите также внимание на использование флага «-L» — этот флаг сообщает компоновщику, что библиотеки могут быть найдены в заданном каталоге («.», ссылающемся на текущий каталог), в дополнение к стандартным расположениям, в которых компилятор ищет системные файлы. библиотеки.
Пример программы, использующей статическую библиотеку, можно найти в нашем каталоге примеров статической библиотеки.
Создание общей библиотеки "C" с помощью "ld"
- Компиляция для «Позиционно-независимого кода» (PIC). Когда создаются объектные файлы, мы понятия не имеем, где в памяти они будут вставлены в программу, которая будет их использовать. Многие разные программы могут использовать одну и ту же библиотеку, и каждая из них загружает ее в память с разным адресом. Таким образом, нам нужно, чтобы все вызовы перехода ("goto" на языке ассемблера) и вызовы подпрограмм использовали относительные адреса, а не абсолютные адреса. Таким образом, нам нужно использовать флаг компилятора, который вызовет генерацию этого типа кода.
В большинстве компиляторов это делается путем указания '-fPIC' или '-fpic' в команде компиляции. - Создание файла библиотеки. В отличие от статической библиотеки, общая библиотека не является архивным файлом. Он имеет формат, специфичный для архитектуры, для которой он создается. Таким образом, нам нужно использовать компилятор (либо драйвер компилятора, либо его компоновщик) для создания библиотеки и сообщить ему, что он должен создать разделяемую библиотеку, а не окончательный программный файл.
Это делается с помощью флага "-G" в некоторых компиляторах или флага "-shared" в других компиляторах.
Таким образом, набор команд, которые мы будем использовать для создания общей библиотеки, будет примерно таким:
Первые три команды компилируют исходные файлы с параметром PIC, поэтому их можно использовать в общей библиотеке (их можно использовать напрямую в программе, даже если они были скомпилированы с параметром PIC). Последняя команда просит компилятор сгенерировать разделяемую библиотеку
Использование общей библиотеки "C" — особенности и решения
- Время компиляции — здесь нам нужно указать компоновщику сканировать разделяемую библиотеку при сборке исполняемой программы, чтобы он убедился, что нет пропущенных символов. На самом деле он не будет брать объектные файлы из общей библиотеки и вставлять их в программу.
- Время выполнения — когда мы запускаем программу, нам нужно сообщить системному динамическому загрузчику (процесс, отвечающий за автоматическую загрузку и связывание общих библиотек с работающим процессом), где найти нашу общую библиотеку. .
Часть компиляции проста. Делается это почти так же, как и при линковке со статическими библиотеками:
cc main.o -L. -lutil -o программа
Компоновщик будет искать файл 'libutil.so' ( -lutil ) в текущем каталоге ( -L. ) и связать его с программой, но не будет помещать свои объектные файлы в результирующий исполняемый файл, ' прог.
Часть выполнения немного сложнее. Обычно системный динамический загрузчик ищет разделяемые библиотеки в некоторых системных каталогах (таких как /lib, /usr/lib, /usr/X11/lib и т. д.). Когда мы создаем новую разделяемую библиотеку, которая не является частью системы, мы можем использовать переменную среды «LD_LIBRARY_PATH», чтобы указать динамическому загрузчику искать в других каталогах. Способ сделать это зависит от типа используемой оболочки («tcsh» и «csh» по сравнению с «sh», «bash», «ksh» и подобных оболочек), а также от того, указан ли «LD_LIBRARY_PATH». уже определено.Чтобы проверить, определена ли эта переменная, попробуйте:
Если вы получили сообщение типа "LD_LIBRARY_PATH: Неопределенная переменная". , то он не определен.
-
'tcsh' или 'csh', LD_LIBRARY_PATH не определен:
После того, как вы определили LD_LIBRARY_PATH , вы можете проверить, правильно ли система находит библиотеку для данной программы, связанной с этой библиотекой:
Вы получите несколько строк, в каждой из которых слева будет указано название библиотеки, а справа — полный путь к библиотеке. Если библиотека не найдена ни в одном из системных каталогов по умолчанию или в каталогах, указанных в «LD_LIBRARY_PATH», вы получите сообщение «библиотека не найдена». В таком случае убедитесь, что вы правильно определили путь к каталогу внутри 'LD_LIBRARY_PATH' и при необходимости исправьте его. Если все пойдет хорошо, вы можете запустить свою программу, как любую другую программу, и увидеть ее роль.
Пример программы, использующей общую библиотеку, можно найти в нашем каталоге примеров общей библиотеки.
Динамическое использование общей библиотеки "C" — программный интерфейс
Одной из редко используемых функций общих библиотек является возможность связать их с процессом в любой момент его жизни. Метод связывания, который мы показали ранее, автоматически загружает разделяемую библиотеку динамическим загрузчиком системы. Тем не менее, можно выполнить операцию связывания в любое другое время, используя библиотеку 'dl'. Эта библиотека предоставляет нам средства для загрузки общей библиотеки, ссылки на любой из ее символов, вызова любой из ее функций и, наконец, отключения ее от процесса, когда она больше не нужна.
Вот сценарий, в котором это может быть привлекательным: предположим, что мы написали приложение, которое должно иметь возможность читать файлы, созданные различными текстовыми процессорами. Обычно нашей программе может потребоваться читать десятки различных форматов файлов, но при одном запуске вполне вероятно, что потребуется только один или два таких формата документов. Мы могли бы написать одну общую библиотеку для каждого такого формата, все с одинаковым интерфейсом (например, readfile и writefile) и один фрагмент кода, определяющий формат файла. Таким образом, когда нашу программу попросят открыть такой файл, она сначала определит его формат, затем загрузит соответствующую разделяемую библиотеку, которая может читать и переводить этот формат, и вызовет функцию readfile для чтения документа. . У нас могут быть десятки таких библиотек, но только одна из них будет размещена в памяти в любой момент времени, что позволит нашему приложению использовать меньше системных ресурсов. Это также позволит нам поставлять приложение с небольшим набором поддерживаемых форматов файлов и добавлять новые форматы файлов без необходимости замены всего приложения, просто отправляя клиенту дополнительный набор общих библиотек.
Загрузка общей библиотеки с помощью dlopen()
Чтобы открыть и загрузить разделяемую библиотеку, следует использовать функцию dlopen(). Он используется следующим образом:
Функция dlopen() получает два параметра. Первый — это полный путь к общей библиотеке. Другой — это флаг, определяющий, нужно ли проверять все символы, на которые ссылается библиотека, немедленно или только при их использовании. В нашем случае мы можем использовать ленивый подход ( RTLD_LAZY ) проверки только при его использовании. Функция возвращает указатель на загруженную библиотеку, который впоследствии можно использовать для ссылки на символы в библиотеке. Он вернет NULL в случае возникновения ошибки. В этом случае мы можем использовать функцию dlerror() для вывода удобочитаемого сообщения об ошибке, как мы сделали здесь.
Динамический вызов функций с использованием dlsym()
Как видите, ошибки могут возникать в любом месте кода, поэтому мы должны тщательно проверять ошибки. Конечно, вы также проверите, что 'a_file' не равно NULL после вызова вашей функции.
Выгрузка общей библиотеки с помощью dlclose()
Последний шаг — закрыть библиотеку, чтобы освободить занимаемую ею память. Это следует делать только в том случае, если мы не собираемся использовать его в ближайшее время. Если делаем - лучше оставить открытым, так как загрузка библиотеки требует времени. Чтобы закрыть библиотеку, мы используем что-то вроде этого:
Это освободит все ресурсы, используемые библиотекой (в частности, память, занимаемую ее исполняемым кодом).
Функции автоматического запуска и очистки
Наконец, библиотека динамической загрузки дает нам возможность определить две специальные функции в каждой библиотеке, а именно _init и _fini . Функция _init, если она найдена, вызывается автоматически при открытии библиотеки и перед возвратом функции dlopen(). Его можно использовать для вызова некоторого кода запуска, необходимого для инициализации структур данных, используемых библиотекой, чтения файлов конфигурации и т. д.
Функция _fini вызывается, когда библиотека закрывается с помощью dlclose() . Его можно использовать для выполнения операций очистки, требуемых библиотекой (освобождение структур данных, закрытие файлов и т. д.).
Пример программы, использующей интерфейс 'dl', можно найти в каталоге примеров нашей динамической общей библиотеки.
Получить более глубокое понимание — полная история связывания
Важность порядка ссылок
Чтобы полностью понять, как выполняется связывание, и иметь возможность преодолевать проблемы связывания, мы должны помнить, что порядок, в котором мы представляем объектные файлы и библиотеки компоновщику, является порядком, в котором компоновщик связывает их в результирующий двоичный файл.
Компоновщик проверяет каждый файл по очереди. Если это объектный файл, он полностью помещается в исполняемый файл. Если это библиотека, компоновщик проверяет, есть ли в библиотеке какие-либо символы, на которые есть ссылки (т. е. используемые) в предыдущих объектных файлах, но не определенные (т. е. содержащиеся) в них. Если такой символ найден, то весь объектный файл из библиотеки, содержащей символ, добавляется в исполняемый файл. Этот процесс продолжается до тех пор, пока не будут обработаны все объектные файлы и библиотеки в командной строке.
Этот процесс означает, что если библиотека "А" использует символы из библиотеки "Б", то библиотека "А" должна появиться в команде ссылки перед библиотекой "Б". В противном случае символы могут отсутствовать — компоновщик никогда не вернется к уже обработанным библиотекам. Если в библиотеке «Б» также используются символы, найденные в библиотеке «А», то единственный способ обеспечить успешное связывание — снова упомянуть библиотеку «А» в команде ссылки после библиотеки «Б», например так:
Это означает, что связывание будет происходить медленнее (библиотека "А" будет обработана дважды). Это также намекает на то, что нужно стараться не иметь таких взаимных зависимостей между двумя библиотеками. Если у вас есть такие зависимости - либо переделайте содержимое ваших библиотек, либо объедините две библиотеки в одну большую библиотеку.
Обратите внимание, что объектные файлы, найденные в командной строке, всегда полностью включаются в исполняемый файл, поэтому порядок их упоминания не имеет большого значения. Таким образом, хорошим правилом является всегда упоминать библиотеки после всех объектных файлов.
Статическая ссылка по сравнению со статической. Динамическое связывание
Когда мы обсуждали статические библиотеки, мы сказали, что компоновщик попытается найти файл с именем 'libutil.a'. Мы солгали. Прежде чем искать такой файл, он будет искать файл с именем «libutil.so» — как общую библиотеку. Только если он не может найти разделяемую библиотеку, он будет искать «libutil.a» как статическую библиотеку. Таким образом, если мы создали две копии библиотеки, одну статическую и одну общую, то общая будет предпочтительнее. Это можно переопределить с помощью некоторых флагов компоновщика ("-Wl,static" для некоторых компоновщиков, "-Bstatic" для других типов компоновщиков. Информацию об этих флагах см. в руководстве по компилятору или компоновщику).
Материалы в этом документе предоставляются КАК ЕСТЬ, без каких-либо явных или подразумеваемых гарантий или заявлений о пригодности для определенной цели. Ни автор, ни кто-либо из соавторов не несут ответственности за любой ущерб, прямо или косвенно понесенный в результате использования материалов, содержащихся в этом документе.
настоящим предоставляется разрешение копировать этот документ (в электронном виде или на бумаге, для личного или внутреннего использования организации) или публиковать его в Интернете при условии, что документ скопирован как есть, это уведомление об авторских правах сохранено, а ссылка к исходному документу написано в теле документа или на странице со ссылкой на копию этого документа.
Разрешение на перевод этого документа также предоставляется в соответствии с этими условиями — при условии, что перевод сохраняет смысл текста, уведомление об авторских правах сохраняется как есть, а ссылка на исходный документ указана в теле документа. , или на странице со ссылкой на копию этого документа.
По любым вопросам о документе и его лицензии обращайтесь к автору.
В этом посте я попытаюсь объяснить внутреннюю работу динамической загрузки общих библиотек в системах Linux. Этот пост длинный — для TL;DR, пожалуйста, прочитайте шпаргалку по отладке.
Этот пост не является практическим руководством, хотя в нем показано, как компилировать и отлаживать общие библиотеки и исполняемые файлы. Он оптимизирован для понимания внутренней работы динамической загрузки. Это было написано, чтобы устранить мой долг знаний по этому вопросу, чтобы стать лучшим программистом. Я надеюсь, что это поможет и вам стать лучше.
Библиотека — это файл, содержащий скомпилированный код и данные. Библиотеки в целом полезны, потому что они обеспечивают быстрое время компиляции (вам не нужно компилировать все источники ваших зависимостей при компиляции вашего приложения) и модульный процесс разработки. Статические библиотеки связаны с скомпилированным исполняемым файлом (или другой библиотекой). После компиляции новый артефакт содержит содержимое статической библиотеки. Общие библиотеки загружаются исполняемым файлом (или другой общей библиотекой) во время выполнения. Это делает их немного более сложными, так как есть совершенно новое поле возможных препятствий, которые мы обсудим в этом посте.
Чтобы исследовать мир общих библиотек, в этом посте мы будем использовать один пример. Мы начнем с трех исходных файлов:
main.cpp будет основным файлом для нашего исполняемого файла. Это мало что даст — просто вызовем функцию из случайной библиотеки, которую мы скомпилируем:
Библиотека random определяет одну функцию в своем заголовочном файле random.h :
Это обеспечит простую реализацию в исходном файле random.cpp:
Примечание. Я запускаю все свои примеры в Ubuntu 14.04.
Перед компиляцией фактической библиотеки мы создадим объектный файл из random.cpp:
Как правило, инструменты сборки не печатают на стандартный вывод, когда все в порядке. Здесь описаны все параметры:
- -o random.o : определить имя выходного файла как random.o .
- -c : не пытаться связывать (только компилировать).
- random.cpp: выберите входной файл.
Далее мы скомпилируем объектный файл в общую библиотеку:
Новый флаг -shared указывает, что необходимо создать общую библиотеку. Обратите внимание, что мы назвали разделяемую библиотеку librandom.so. Это не случайно — общие библиотеки должны называться lib .so, чтобы их можно было корректно компоновать позже (как мы увидим в разделе компоновки ниже).
Сначала мы создадим общий объект для main.cc :
Это точно так же, как и раньше с random.o . Теперь попробуем создать исполняемый файл:
Хорошо, нам нужно сообщить clang, что мы хотим использовать librandom.so . Давайте сделаем это 1 :
Хммммф. Мы сказали нашему компилятору, что хотим использовать файл librandom. Поскольку он загружается динамически, зачем он нам нужен во время компиляции? Ну, причина в том, что нам нужно убедиться, что библиотеки, от которых мы зависим, содержат все символы, необходимые для нашего исполняемого файла. Также обратите внимание, что в качестве имени библиотеки мы указали random, а не librandom.so. Помните, существует соглашение об именовании файлов библиотек? Здесь он используется.
Итак, нам нужно сообщить clang, где искать общие библиотеки. Мы делаем это с флагом -L. Обратите внимание, что пути, указанные с помощью -L, влияют на путь поиска только при связывании, а не во время выполнения. Укажем текущий каталог:
Отлично. Теперь запустим!
Это ошибка, которую мы получаем, когда не удается найти зависимость. Это произойдет еще до того, как наше приложение выполнит хотя бы одну строку кода, поскольку общие библиотеки загружаются до символов в нашем исполняемом файле.
В связи с этим возникает несколько вопросов:
- Откуда main узнает, что это зависит от librandom.so?
- Где main ищет librandom.so?
- Как мы можем указать main искать librandom.so в этом каталоге?
Чтобы ответить на эти вопросы, нам нужно немного углубиться в структуру этих файлов.
Общая библиотека и формат исполняемого файла называется ELF (Executable and Linkable Format). Если вы прочитаете статью в Википедии, то увидите, что это полный бардак, поэтому мы не будем вдаваться в подробности. Таким образом, файл ELF содержит:
- Заголовок ELF
- Данные файла, которые могут содержать:
- Таблица заголовков программы (список заголовков сегментов)
- Таблица заголовков разделов (список заголовков разделов)
- Данные, на которые указывают два заголовка выше.
Заголовок ELF указывает размер и количество сегментов в таблице заголовков программы, а также размер и количество разделов в таблице заголовков разделов. Каждая такая таблица состоит из записей фиксированного размера (я использую запись для описания либо заголовка сегмента, либо заголовка раздела в соответствующей таблице). Записи являются заголовками и содержат «указатель» (смещение в файле) на местоположение фактического тела сегмента или раздела. Это тело существует в части данных файла. Чтобы усложнить ситуацию, каждый раздел является частью сегмента, а сегмент может содержать множество разделов. .
По сути, на одни и те же данные ссылаются либо как на часть сегмента, либо как на раздел в зависимости от текущего контекста. разделы используются при связывании, а сегменты используются при выполнении.
Мы будем использовать readelf, чтобы… ну, читать ELF. Давайте начнем с просмотра ELF-заголовка main :
Мы видим, что это файл ELF (64-разрядный) в Unix. Его тип - EXEC , который, как и ожидалось, является исполняемым файлом. Он имеет 9 заголовков программ (то есть 9 сегментов) и 30 заголовков разделов (т. е. разделов).
Далее — заголовки программ:
Снова мы видим, что у нас есть 9 заголовков программ. Их типы: LOAD (два из них), DYNAMIC , NOTE и т. д. Мы также можем видеть принадлежность каждого сегмента к разделу.
Наконец - заголовки разделов:
Я обрезал это для краткости. Мы видим наши 30 разделов в списке с разными именами (например, .note.ABI-tag ) и типами (например, SYMTAB ).
Возможно, вы уже запутались. Не волнуйтесь - его не будет на тесте.Я объясняю это, потому что нас интересует определенная часть этого файла: в своей таблице заголовков программ файлы ELF могут иметь (и, в частности, общие библиотеки, должны иметь) заголовок сегмента , который описывает сегмент типа PT_DYNAMIC . В этом сегменте есть раздел .dynamic, который содержит полезную информацию для понимания динамических зависимостей.
Мы можем использовать утилиту readelf для дальнейшего изучения раздела .dynamic нашего исполняемого файла 2 . В частности, этот раздел содержит все динамические зависимости нашего файла ELF. Мы указали только librandom.so в качестве зависимости, поэтому мы ожидаем, что в списке будет ровно одна зависимость:
Мы видим librandom.so , который мы указали, но мы также получаем четыре дополнительные зависимости, которых не ожидали. Эти зависимости появляются во всех скомпилированных разделяемых библиотеках. Какие они?
- libstdc++: стандартная библиотека C++.
- libm : библиотека, содержащая основные математические функции.
- libgcc_s : библиотека времени выполнения GCC (GNU Compiler Collection).
- libc : библиотека C: библиотека, которая определяет «системные вызовы» и другие основные средства, такие как open , malloc , printf , exit и т. д.
Хорошо, значит, мы знаем, что main знает, что это зависит от librandom.so . Так почему же main не может найти librandom.so во время выполнения?
ldd — это инструмент, который позволяет нам видеть рекурсивные зависимости общей библиотеки. Это означает, что мы можем видеть полный список всех разделяемых библиотек, необходимых артефакту во время выполнения. Это также позволяет нам видеть, где расположены эти зависимости. Давайте запустим его на main и посмотрим, что произойдет:
Сразу же мы видим, что librandom.so есть в списке, но не найден. Мы также видим, что у нас есть две дополнительные библиотеки (vdso и ld-linux-x86-64). Это косвенные зависимости. Что еще более важно, мы видим, что ldd сообщает о местоположении библиотек. Рассмотрим libstdС++. ldd сообщает о своем расположении как /usr/lib/x86_64-linux-gnu/libstdc++.so.6 . Откуда он знает?
Каждая общая библиотека в наших зависимостях просматривается в следующих местах 3 по порядку:
- Каталоги, перечисленные в rpath исполняемого файла.
- Каталоги в переменной среды LD_LIBRARY_PATH, которая содержит список каталогов, разделенных двоеточием (например, /path/to/libdir:/another/path )
- Каталоги, перечисленные в пути выполнения исполняемого файла.
- Список каталогов в файле /etc/ld.so.conf. Этот файл может включать в себя другие файлы, но в основном это список каталогов — по одному в строке.
- Системные библиотеки по умолчанию — обычно /lib и /usr/lib (пропускаются, если скомпилированы с параметром -z nodefaultlib).
Хорошо. Мы проверили, что librandom.so входит в список зависимостей, но не может быть найдено. Мы знаем, где ищутся зависимости. Мы убедимся, что наш каталог на самом деле не находится на пути поиска, снова используя ldd:
Я обрезал вывод, потому что он очень... болтлив. Неудивительно, что наша разделяемая библиотека не найдена — каталога, в котором находится librandom.so, нет в пути поиска! Самый нестандартный способ решить эту проблему — использовать LD_LIBRARY_PATH :
Он работает, но не очень портативный. Мы не хотим указывать наш каталог lib каждый раз, когда запускаем нашу программу. Лучше всего поместить наши зависимости внутри файла.
Введите rpath и runpath .
rpath и runpath — самые сложные элементы в нашем «контрольном списке» путей поиска во время выполнения. rpath и runpath исполняемой или совместно используемой библиотеки являются необязательными элементами в разделе .dynamic, который мы рассматривали ранее 4 . Оба они представляют собой список каталогов для поиска.
Единственная разница между rpath и runpath заключается в порядке их поиска. В частности, их отношение к LD_LIBRARY_PATH: rpath ищется в до LD_LIBRARY_PATH, а runpath ищется в после. Смысл этого в том, что rpath нельзя изменить динамически с помощью переменных среды, в то время как runpath может.
Давайте запечем rpath в наш исполняемый файл и посмотрим, сможем ли мы заставить его работать:
Флаг -Wl передает компоновщику следующие разделенные запятыми флаги. В этом случае мы передаем -rpath. . Чтобы вместо этого установить путь выполнения, нам также нужно было бы передать --enable-new-dtags 5 . Посмотрим на результат:
Исполняемый файл запускается, но это добавляет . в rpath, который является текущим рабочим каталогом. Это означает, что он не будет работать из другого каталога:
У нас есть несколько способов решить эту проблему. Самый простой способ — скопировать librandom в каталог, указанный в пути поиска (например, /lib ). Более сложный способ, который, очевидно, мы и собираемся сделать, — указать rpath относительно исполняемого файла.
Пути в rpath и runpath могут быть абсолютными (например, /path/to/my/libs/), относительными к текущему рабочему каталогу (например, . ), но они также могут быть относительными исполняемому файлу< /эм>. Это достигается использованием переменной $ORIGIN 6 в определении rpath:
Обратите внимание, что нам нужно избегать знака доллара (или использовать одинарные кавычки), чтобы наша оболочка не пыталась его расширить. В результате main работает из любого каталога и корректно находит librandom.so:
Если вы когда-либо меняли пароль пользователя Linux из командной строки, возможно, вы использовали утилиту passwd:
Хэш пароля хранится в файле /etc/shadow, который защищен root. Как же тогда, спросите вы, ваш пользователь без полномочий root может изменить этот файл?
Ответ заключается в том, что в программе passwd установлен бит setuid, что можно увидеть с помощью ls :
Это s (четвертый символ строки). Все программы, для которых установлен этот бит разрешения, запускаются от имени владельца этой программы. В этом примере пользователь root (третье слово в строке).
«Какое это имеет отношение к общим библиотекам?» — спросите вы. Посмотрим на примере.
Теперь у нас будет librandom в каталоге libs рядом с основным, и мы запечем $ORIGIN/libs 7 в rpath нашего основного:
Если мы запустим main , он будет работать как положено. Давайте установим бит setuid для нашего основного исполняемого файла и запустим его от имени пользователя root:
Хорошо, rpath не работает. Вместо этого попробуем установить LD_LIBRARY_PATH:
Что здесь происходит?
В целях безопасности при запуске исполняемого файла с повышенными привилегиями (такими как setuid , setgid , специальные возможности и т. д.) список путей поиска отличается от обычного: LD_LIBRARY_PATH игнорируется, как и любой путь в rpath или runpath. который содержит $ORIGIN .
Причина в том, что использование этих путей поиска позволяет использовать исполняемый файл с повышенными привилегиями для запуска от имени пользователя root . Подробности об этом эксплойте можно найти здесь. По сути, это позволяет вам сделать так, чтобы исполняемый файл с повышенными привилегиями загружал вашу собственную библиотеку, которая будет работать от имени пользователя root (или другого пользователя). Запуск собственного кода от имени пользователя root дает вам практически полный контроль над используемой вами машиной.
Если ваш исполняемый файл должен иметь повышенные привилегии, вам нужно указать свои зависимости в абсолютных путях или поместить их в местоположения по умолчанию (например, /lib ).
Важно отметить, что для подобных приложений ldd лжет нам в лицо:
ldd не заботится о setuid и расширяет $ORIGIN при поиске наших зависимостей. Это может быть ловушкой при отладке зависимостей в приложениях с setuid.
Если вы когда-нибудь получите эту ошибку при запуске исполняемого файла:
Вы можете попробовать сделать следующее:
- Узнайте, каких зависимостей не хватает в ldd .
- Если вы не идентифицируете их, вы можете проверить, являются ли они прямыми зависимостями, запустив readelf -d | ТРЕБУЕТСЯ grep .
- Убедитесь, что зависимости действительно существуют. Может быть, вы забыли их скомпилировать или переместить в каталог libs?
- Узнайте, где выполняется поиск зависимостей, с помощью LD_DEBUG=libs ldd .
- Если вам нужно добавить каталог в поиск:
- Специально: добавьте каталог в переменную среды LD_LIBRARY_PATH.
- Запеченный в файле: добавьте каталог в исполняемую или разделяемую библиотеку rpath или runpath, передав -Wl,-rpath, (для rpath ) или -Wl,--enable-new-dtags,-rpath, (для runpath ). Используйте $ORIGIN для путей относительно исполняемого файла.
Если вы все еще не можете понять это - вам нужно прочитать все это еще раз :)
Обратите внимание, что мы решили динамически связать librandom.so с main . Можно сделать это статически — и загрузить все символы из случайной библиотеки прямо в основной исполняемый файл. ↩︎
Исполняемый файл objdump может дать аналогичные результаты. Например, в этом случае: objdump -p librandom.so | grep NEEDED выведет очень похожий вывод. ↩︎
Путь поиска различается для приложений setuid/setguid. Подробности смотрите ниже. ↩︎
Тип rpath — DT_RPATH, а тип runpath — DT_RUNPATH. ↩︎
Обратите внимание, что $ORIGIN не является переменной среды. Если вы экспортируете ORIGIN=/path, это не будет иметь никакого эффекта. Это всегда каталог, в котором находится исполняемый файл. ↩︎
По какой-то странной причине этот пример вел себя не так, как я ожидал, когда librandom.so находился в том же каталоге, что и main, и когда main имел просто $ORIGIN в своем rpath. Если вы знаете, почему это так, пожалуйста, ответьте на мой вопрос об этом в Stack Overlow. ↩︎
Обсудите этот пост в Hacker News, /r/Programming или в разделе комментариев ниже.
Подпишитесь на меня в Twitter и Facebook
< small>Спасибо Ханнану Ааронову, Йонатану Накару и Шачару Охане за то, что прочитали черновики.Мне известно, что общие объекты в Linux используют "так-номера", а именно, что разные версии общего объекта получают разные расширения, например:
Я понимаю, что идея состоит в том, чтобы иметь два отдельных файла, чтобы в системе могли существовать две версии библиотеки (в отличие от «DLL Hell» в Windows). Хотелось бы узнать, как это работает на практике? Часто я вижу, что example.so на самом деле является символической ссылкой на example.so.2, где .2 — последняя версия. Как тогда приложение, зависящее от более старой версии example.so, правильно идентифицирует ее? Существуют ли какие-либо правила относительно того, какие числа следует использовать? Или это просто условность? Дело в том, что, в отличие от Windows, где двоичные файлы программного обеспечения передаются между системами, если в системе есть более новая версия общего объекта, она автоматически связывается со старой версией при компиляции из исходного кода?
Подозреваю, что это связано с ldconfig, но не знаю как.
4 ответа 4
Как видите, это указывает, например, на libpthread.so.0 , а не только libpthread.so .
Символическая ссылка предназначена для компоновщика. Если вы хотите связать libpthread.so напрямую, вы даете gcc флаг -lpthread , и он автоматически добавляет префикс lib и суффикс .so. Вы не можете сказать ему добавить суффикс .so.0, поэтому символическая ссылка указывает на новейшую версию библиотеки, чтобы облегчить это
@bmacnaughton Вероятно, это приведет к ошибке, потому что ldd требует полного пути к исполняемому файлу. =ls делает это в zsh, но я изменил его, так как не все используют эту оболочку
Интересно. Я запускаю bash на Ubuntu, и, похоже, он работает без полного пути. Спасибо за объяснение - я не использую zsh.
», поэтому символическая ссылка указывает на новейшую версию библиотеки, чтобы облегчить это, «причина символической ссылки заключается в том, чтобы указать на файл .so.X, который соответствует установленным заголовкам в системе. Не указывать на файл .so.X самой новой версии. Часто это самая новая версия, но также часто бывают случаи, когда это не так (особенно во время разработки... кроме этого, символическая ссылка в любом случае бесполезна).
Номера в общих библиотеках используются в Linux для идентификации API библиотеки. Обычно используется следующий формат:
Как вы заметили, обычно существует символическая ссылка от libFOO.so к libFOO.so.MAJOR.MINOR. ldconfig отвечает за обновление этой ссылки до последней версии.
MAJOR обычно увеличивается при изменении API (удаляются новые точки входа или изменяются параметры или типы). MINOR обычно увеличивается для выпусков исправлений ошибок или когда новые API вводятся без нарушения существующих API.
Привет, Мигель, спасибо за это, жаль, что я не могу принять два ответа, потому что они прекрасно дополняют вышеизложенное. +1 от меня, тоже отличная ссылка, еще раз спасибо!
Общие библиотеки должны иметь версии в соответствии со следующей схемой:
- X = версия ABI, несовместимая с предыдущими версиями
- Y = обратно совместимый выпуск ABI
- Z = только внутренние изменения – никаких изменений в ABI.
Обычно вы видите только первую цифру, например hello.so.1, потому что первая цифра — это единственное, что нужно для определения «версии» библиотеки, поскольку все остальные цифры обратно совместимы.
ldconfig ведет таблицу, в которой указано, какие общие библиотеки доступны в системе и где находится путь к этой библиотеке. Вы можете проверить это, запустив:
Когда пакет собирается для чего-то вроде Red Hat, общие библиотеки, вызываемые в двоичном файле, будут найдены и добавлены как зависимости пакета во время сборки RPM. Поэтому, когда вы приступите к установке пакета, установщик проверит, установлен ли в системе hello.so.1, проверив ldconfig .
Вы можете увидеть зависимости пакета, выполнив что-то вроде:
Эта система (в отличие от Windows) позволяет установить в системе несколько версий hello.so и одновременно использовать их в разных приложениях.
Общие библиотеки должны иметь версии в соответствии со следующей схемой (…). Не могли бы вы предоставить ссылку на это заявление?
не могли бы вы указать источник этого? Мне очень нравится ответ, но без достоверного источника мне тяжело
libNAME.so — это имя файла, используемое компилятором/компоновщиком при первом поиске библиотеки, указанной параметром -lNAME. Внутри файла общей библиотеки есть поле SONAME. Это поле устанавливается, когда сама библиотека впервые связывается с общим объектом (so) в процессе сборки. Это SONAME на самом деле является тем, что компоновщик хранит в исполняемом файле в зависимости от того, что с ним связан общий объект. Обычно SONAME имеет форму libNAME.so.MAJOR и изменяется каждый раз, когда библиотека становится несовместимой с существующими исполняемыми файлами, связанными с ней, и обе основные версии библиотеки могут быть установлены по мере необходимости (хотя только одна будет указана для разработки). как libNAME.so) Кроме того, для облегчения обновления между младшими версиями библиотеки, libNAME.so.MAJOR обычно является ссылкой на файл, например libNAME.so.MAJOR.MINOR. Можно установить новую второстепенную версию, и после ее завершения ссылка на старую второстепенную версию будет изменена, чтобы указать на новую второстепенную версию, немедленно обновляющую все новые исполнения для использования обновленной библиотеки. Также см. мой ответ на Linux, GNU GCC, ld, сценарии версий и двоичный формат ELF. Как это работает?
Связано
Связанные
Горячие вопросы о сети
Чтобы подписаться на этот RSS-канал, скопируйте и вставьте этот URL-адрес в программу для чтения RSS.
дизайн сайта / логотип © 2022 Stack Exchange Inc; вклады пользователей под лицензией cc by-sa. версия 2022.3.18.41718
Linux является зарегистрированным товарным знаком Линуса Торвальдса. UNIX является зарегистрированным товарным знаком The Open Group.
Этот сайт никоим образом не связан с Линусом Торвальдсом или The Open Group.В этой статье мы узнаем, как создавать общие библиотеки и правильно их устанавливать на нескольких платформах. В качестве руководства мы рассмотрим цели и историю динамического связывания в операционных системах на базе UNIX.
Содержание статьи основано на изучении способов создания общей библиотеки, изучении небрежных соглашений, которые люди рекомендуют в Интернете, и тестировании на нескольких Unix-подобных системах. Надеюсь, это поможет исправить ситуацию и улучшить качество библиотек с открытым исходным кодом.
Общий шаблон UNIX
Схема, обычно используемая в настоящее время для динамического связывания (в BSD, MacOS и Linux), пришла из SunOS в 1988 году. В документе "Общие библиотеки в SunOS" подробно объясняются цели, структура и реализация.
Основными мотивами авторов были экономия места на диске и в памяти, а также обновление библиотек (или ОС) без необходимости перекомпоновки программ. Мотивация использования ресурсов, вероятно, менее важна для современных мощных персональных компьютеров, чем это было в 1988 году. Однако гибкость обновления библиотек по-прежнему полезна, как и возможность легко проверить, какие версии библиотек использует каждое приложение.
У динамического связывания есть свои критики, и оно подходит не во всех ситуациях. Он работает немного медленнее из-за позиционно-независимого кода (PIC) и поздней загрузки. (В документе SunOS это называется «классическим компромиссом между пространством и временем».) Сложность загрузчика в некоторых системах увеличивает поверхность атаки. Наконец, обновленные библиотеки могут воздействовать на одни программы иначе, чем на другие, например нарушать работу тех, которые основаны на недокументированном поведении.
Редактор ссылок и загрузчик
Во время компиляции редактор ссылок разрешает символы в указанных библиотеках и делает пометку в результирующем двоичном файле для загрузки этих библиотек. Во время выполнения приложения вызывают код для сопоставления символов общей библиотеки в памяти с правильными адресами памяти.
SunOS и последующие UNIX-подобные системы добавили к компоновщику (ld) флаги времени компиляции для создания или связывания с динамически подключаемыми библиотеками. Разработчики также добавили специальную системную библиотеку (ld.so) с кодом для поиска и загрузки других библиотек для приложения. Процедура инициализации программы premain() загружает ld.so и запускает ее из программы для поиска и загрузки остальных необходимых библиотек.
Версии
Как уже упоминалось, приложения могут использовать преимущества обновленных библиотек без перекомпиляции. Обновления библиотеки можно разделить на три категории:
- Улучшения реализации текущего интерфейса. Исправление ошибок, производительность. (выпуск исправления)
- Новые функции, дополнения к интерфейсу. (Небольшой выпуск)
- Обратно несовместимое изменение интерфейса или его работы. (Основной выпуск)
Приложение, связанное с библиотекой в данном основном выпуске, будет продолжать работать правильно при загрузке любого нового дополнительного выпуска или исправления. Приложения могут работать неправильно при загрузке другого основного выпуска или более раннего дополнительного выпуска, чем тот, который использовался во время компоновки.
На компьютере может одновременно существовать несколько приложений, и для каждого из них могут потребоваться разные выпуски одной библиотеки. Система должна предоставлять возможность хранить несколько выпусков библиотек и загружать нужный для каждого приложения. Как мы увидим позже, в разных системах это делается по-разному.
Идентификаторы версий
Каждый выпуск библиотеки может быть помечен идентификатором версии (или «версией»), который предназначен для сбора информации об истории выпусков библиотеки. Существует несколько способов сопоставить историю выпусков с идентификатором версии.
Двумя наиболее распространенными системами сопоставления являются семантическое управление версиями и управление версиями libtool. Семантическое управление версиями подсчитывает количество выпущенных выпусков различных типов и записывает их в лексикографическом порядке. . Управление версиями Libtool учитывает различные интерфейсы библиотек.
Семантическая версия записывается как major.minor.patch, а libtool — как current:revision:age. Интуиция такова, что текущие изменения интерфейса учитываются. Каждый раз, когда интерфейс изменяется, незначительно или существенно, ток увеличивается. Вот как каждая система будет записывать одну и ту же историю событий выпуска:
Event Semver Libtool Исходный 1.0.0 1:0:0 Второстепенный 1.1.0 td> 2:0:1 Незначительное 1.2.0 3:0:2 Исправление 1.2.1 3:1:2 Основной 2.0.0 4:0:0 Исправление 2.0.1 4:1:0 Исправление 2.0.2< /td> 4:2:0 Младшая 2.1.0 5:0:1 тд> Вот как приложения отвечают на вопрос "Могу ли я загрузить данную библиотеку?"
Semver Имеет ли загружаемая библиотека ту же основную версию, что и библиотека, с которой я связан, а вспомогательная версия не меньше этой? Libtool Является ли текущий номер интерфейса библиотеки, с которой я связан, между текущим возрастом и текущей загружаемой библиотекой?
В этом руководстве мы будем использовать семантическое управление версиями, поскольку управление версиями libtool относится только к libtool, инструменту для абстрагирования создания библиотек на разных платформах. Я считаю, что мы можем создавать переносимые библиотеки без libtool. Я упоминаю обе системы только для того, чтобы показать, что существует несколько способов создания идентификаторов версий.
Последнее примечание: идентификаторы версии говорят, что что-то изменилось, но опускают что изменилось. Существуют более сложные системы для отслеживания совместимости библиотек. В Solaris, например, была разработана система управления версиями символов. Управление версиями символов преследует цель экономии места за счет усложнения операций, и мы рассмотрим это позже.
API и ABI
Одна тонкость управления версиями заключается в том, что изменения могут происходить либо в программном интерфейсе библиотеки (API), либо в бинарном интерфейсе (ABI). Программный интерфейс библиотеки C определяется через файлы заголовков. Обратно несовместимое изменение API означает, что программа, написанная для предыдущей версии, не будет компилироваться при включении заголовков из новой версии.
Двоичный интерфейс, напротив, представляет собой концепцию среды выполнения. Это касается соглашений о вызовах функций или расположения в памяти (и значения) данных, совместно используемых программой и библиотекой. ABI обеспечивает совместимость во время загрузки и выполнения, а API обеспечивает совместимость во время компиляции и компоновки.
Эти два интерфейса обычно меняются рука об руку, и люди иногда их путают. Однако одно может измениться без другого.
Примеры нарушения ABI, но стабильности API:
При этих изменениях в библиотеке код приложения менять не нужно, но его нужно перекомпилировать с новыми заголовками библиотеки, чтобы он работал во время выполнения.
Примеры стабильности ABI, но нарушения работы API:
При этих изменениях в библиотеке потребуется изменить код приложения для успешной компиляции с новой библиотекой, даже если код, скомпилированный до внесения изменений, мог без проблем загрузить и вызвать библиотеку.
- Изменение аргумента с const foo * на foo * . Указатель на константный объект не может быть неявно преобразован в указатель на неконстантный объект. Однако ABI это не волнует, и он перемещает одни и те же байты. (Если библиотека действительно изменяет разыменованное значение, это, конечно, может стать неприятным сюрпризом для приложения.)
- Изменение имени элемента структуры с сохранением его значения и сохранением его положения относительно других элементов.
Обычно легко отличить добавленную функциональность от обратной совместимости, но есть инструменты, которые можно проверить наверняка. Например, средство проверки соответствия ABI может обнаруживать сбои в библиотеках C и C++.
В свете предыдущего обсуждения версий, какие изменения должен описывать идентификатор версии? По крайней мере, АБИ. Когда загрузчик ищет библиотеку, ABI определяет, будет ли библиотека совместима во время выполнения. Однако я думаю, что разумнее использовать более консервативную схему управления версиями, при которой вы обновляете версию при изменении либо API или ABI. В конечном итоге у вас может быть установлено больше версий библиотек, но каждая общая версия API/ABI будет предоставлять гарантии как во время компиляции, так и во время выполнения.
Различия компоновщика и загрузчика в зависимости от системы
Линкеры (ld, lld)
После компиляции объектных файлов внешний интерфейс компилятора (gcc, clang, cc, c99) вызывает компоновщик (ld, lld), чтобы найти неразрешенные символы и сопоставить их в объектных файлах или общих библиотеках. Компоновщик ищет только общие библиотеки, запрошенные внешним интерфейсом, в порядке, указанном в командной строке. Если в указанной библиотеке обнаружен неразрешенный символ, компоновщик помечает зависимость от этой библиотеки в сгенерированном исполняемом файле.
Опция -l добавляет библиотеку в список кандидатов для поиска символов. Чтобы добавить libfoo.so (или libfoo.dylib на Mac), укажите -lfoo . Компоновщик ищет файлы библиотеки в своем пути поиска. Чтобы добавить каталоги к путям поиска по умолчанию, используйте -L , например -L/usr/local/lib .
Что произойдет, если в одном каталоге будет несколько версий библиотеки? Например, две основные версии, libfoo.so.1 и libfoo.so.2?OpenBSD знает о номерах версий и автоматически выберет самую старшую версию для -lfoo . Linux и Mac не будут соответствовать ни одному из них, потому что они ищут точное совпадение с libfoo.so (или libfoo.dylib). Точно так же, что, если и статическая, и динамическая библиотеки существуют в одном и том же каталоге, libfoo.a и libfoo.so? Все системы выберут динамическую.
Необходим больший контроль. В GCC есть опция двоеточия для решения проблемы, например -l:libfoo.so.1 . Однако в clang его нет, поэтому действительно портативная сборка не должна полагаться на него. Некоторые системы решают проблему, создавая символическую ссылку из libfoo.so на нужную библиотеку. Однако, когда это делается в системном расположении, таком как /usr/local/lib , он назначает единую негибкую версию времени компоновки для всей системы. Позже я предложу другое решение, в котором файлы времени компоновки хранятся отдельно от библиотек времени загрузки.
Загрузчики (ld.so, dyld)
Во время запуска программы с зависимостями динамических библиотек загружают и запускают ld.so (или dyld на Mac), чтобы найти и загрузить остальные свои зависимости. Библиотека загрузки проверяет теги DT_NEEDED ELF (или имена LOAD_DYLIB в Mach-O на Mac), чтобы определить, какое имя файла библиотеки нужно найти в системе. Интересно, что эти значения задаются не разработчиком программы, а разработчиком библиотеки. Они извлекаются из самих библиотек во время компоновки.
Динамические библиотеки содержат внутреннее «имя среды выполнения», называемое SONAME в ELF или install_name в Mach-O. Приложение может ссылаться на файл с именем libfoo.so , но библиотека SONAME может сказать: «ищите меня по имени файла libfoo.so.1.2 во время загрузки». Загрузчик заботится только об именах файлов, он никогда не обращается к SONAMES. И наоборот, выходные данные компоновщика заботятся только о SONAMES, а не об именах файлов входной библиотеки.
Загрузчики в разных операционных системах немного по-разному находят зависимые библиотеки. ld.so в OpenBSD полностью соответствует модели SunOS и понимает семантические версии. Например, если будет предложено загрузить libfoo.so.1.2, он попытается найти libfoo.so.1.x с наибольшим значением x ≥ 2. FreeBSD также утверждает, что имеет такое поведение, но я не наблюдал его в своих тестах. .
В 1995 году в Solaris 2.5 появился способ отслеживать семантическое управление версиями на уровне символов, а не всей библиотеки. При управлении версиями символов будет один, например. libfoo.so, который со временем просто увеличивается. Каждая функция внутри помечена номером версии. Одно и то же имя функции может даже существовать в нескольких версиях с разными реализациями.
- Сложнее точно определить, какие версии установлены в системе. Версии скрыты в библиотеках, а не видны в именах файлов.
- Разработчики библиотек должны поддерживать отдельный файл сопоставления символов для компоновщика.
Управление версиями символов быстро нашло применение в Linux и стало одним из основных элементов Glibc. Из-за предпочтений Linux в отношении версий символов, его ld.so не предпринимает никаких усилий для рандеву с последней младшей версией библиотеки (а-ля SunOS или OpenBSD). Ld.so ищет точное совпадение между SONAME и именем файла.
Однако даже в Linux большинство библиотек не используют управление версиями символов. Кроме того, их SONAME обычно записывают только основную версию (например, libfoo.so.2). В этой основной версии вам просто нужно надеяться, что скрытая дополнительная версия достаточно нова для всех приложений, скомпилированных или установленных в системе. Если приложение использует функции, добавленные в более позднюю дополнительную версию библиотеки, при попытке вызвать их произойдет сбой. (Установка переменной среды LD_BIND_NOW=1 вместо этого попытается разрешить все символы при запуске программы, чтобы заранее обнаружить сбой.)
MacOS использует совершенно другой формат объектов (Mach-O, а не ELF) и библиотеку загрузчика с другим именем (dyld, а не ld.so). Динамически подключаемые библиотеки Mac называются .dylib , а номера их версий предшествуют расширению.
Стандартные приложения для Mac обычно устанавливаются в отдельные выделенные каталоги с библиотеками внутри. Таким образом, у загрузчика есть специальные условия для поиска библиотек, такие как ключевые слова @executable_path , @loader_path и @rpath в install_name . MacOS также поддерживает системные библиотеки, при этом dyld обращается к DYLD_FALLBACK_LIBRARY_PATH , по умолчанию $(HOME)/lib:/usr/local/lib:/lib:/usr/lib .
Как и Linux, Mac точно соответствует названию — нет рандеву младших версий. В отличие от Linux, библиотеки могут записывать свою полную семантическую версию внутри себя и версию «совместимости». Версия совместимости копируется в приложение во время компоновки и сообщает, что приложению требуется по крайней мере эта версия во время выполнения.
Например, libfoo.1.dylib с полной версией 1.2.3 должна иметь версию совместимости 1.2.0 в соответствии с правилами семантического управления версиями. Приложение, связанное с ним, отказывается загружать libfoo с меньшей младшей версией, например 1.1.5. Во время загрузки пользователь увидит явную ошибку:
Оптимальные методы переноса
Связывание
Стандартная практика заключается в создании символических ссылок libfoo.so -> libfoo.so.x -> libfoo.so.x.y.z в общем системном каталоге. Первая ссылка (без номера версии) предназначена для ссылки во время сборки. Проблема в том, что он привязан к одной версии. Нет переносимого способа выбрать версию для ссылки, когда установлено несколько версий.
Кроме того, стандартная практика еще меньше заботится об управлении версиями заголовочных файлов. Иногда последняя установленная версия перезаписывает их в /usr/local/include. Иногда заголовки поддерживаются только на уровне основной версии, в /usr/local/include/libfoo-n.
Чтобы решить эти проблемы, я предлагаю объединить все файлы библиотеки разработки (связывания) вместе в другую структуру каталогов для каждой версии. Поскольку ранее я выступал за то, чтобы «общая» версия библиотеки увеличивалась при каждом изменении API или ABI, одна и та же версия безопасно применяется к заголовкам и двоичным файлам.
Сначала выберите ПРЕФИКС установки. Если в системе есть каталог /opt, выберите его, иначе /usr/local. Добавьте в этот каталог динамические и/или статические библиотеки, заголовки, справочные страницы и файлы pkg-config по желанию:
Связать с libfoo.x.y.z очень просто. В Makefile установите флаги следующим образом:
Гибкость версий с помощью pkg-config
Pkg-config может позволить приложению выражать диапазон допустимых версий библиотек, а не жестко запрограммировать конкретную версию. В скрипте configure мы проверим наличие и версию библиотеки и выведем флаги в config.mk :
Тогда наш Makefile становится таким:
Чтобы выбрать конкретную версию libfoo, мы можем добавить ее в путь поиска pkg-config и запустить скрипт configure:
Чтобы создать файлы pkg-config .pc для библиотеки, см. руководство Дэна Николсона. Чтобы предложить как статическую, так и динамическую библиотеку, лучший способ, который я мог вообразить, состоял в том, чтобы выпустить отдельные файлы, libfoo.pc и libfoo-static.pc, которые отличаются своим флагом -L. Один использует lib, а другой lib/static. (Флаг --static в Pkg-config немного неверен и просто передает элементы в Libs.private в дополнение Libs в процессе сборки.)
Загрузка
В этом разделе рассказывается об установке динамических библиотек для общесистемной загрузки. Библиотеки, установленные для этой цели, предназначены не для компоновки во время компиляции, а для загрузки во время выполнения.
Установка ELF (BSD/Linux)
У объектов ELF не так много метаданных версии. SONAME об этом. Это, в сочетании с тусклым поведением загрузчиков на некоторых системах, означает, что традиционная техника установки работает не слишком хорошо.
Давайте рассмотрим традиционный способ установки библиотек ELF, а затем разработанный мной более безопасный метод.
Традиционный метод установки
- Для версии x.y.z скомпилируйте libfoo.so с SONAME libfoo.so.x
- Скопируйте libfoo.so в /usr/local/lib/libfoo.so.x.y.z
- Создать символическую ссылку libfoo.so.x -> libfoo.x.y.z
Этот способ позволяет системному администратору точно видеть, какие версии установлены, и одновременно устанавливать несколько основных версий. Он не позволяет использовать несколько дополнительных версий для одной основной версии (хотя обычно требуется только последняя дополнительная версия) и, что более важно, не обеспечивает защиту от загрузки слишком старой дополнительной версии.
Безопасный метод установки
Для версии x.y.z скомпилируйте libfoo.so с SONAME libfoo.so.x.y
Скопируйте libfoo.so в /usr/local/lib/libfoo.so.x.y.z
Заполнить символические ссылки дополнительных версий в DEST:
За счет потенциально большого количества символических ссылок на второстепенные версии этот метод эмулирует поведение SunOS и OpenBSD при рандеву второстепенных версий. Кроме того, поскольку SONAME имеет степень детализации major.minor, это защитит от загрузки слишком старой дополнительной версии.
(В качестве альтернативы символическим ссылкам FreeBSD имеет libmap.conf)
Установка Mach-O (MacOS)
В Mach-O больше метаданных версии, чем в ELF, поэтому традиционная установка здесь работает нормально.
Для версии x.y.z скомпилируйте libfoo.dylib с
- имя_установки libfoo.x.dylib
- текущая версия x.y.z
- совместимая версия x.y
Скопируйте libfoo.dylib в /usr/local/lib/libfoo.x.dylib
Важно правильно установить версию совместимости, чтобы dyld на Mac не загрузил слишком старую второстепенную версию. Чтобы обновить библиотеку, перезапишите libfoo.x.dylib одной из более поздних внутренних дополнительных версий.
Пример кода
Пример переносимой сборки библиотеки и ее удобной установки для компоновщика и загрузчика см. в begriffs/libderp. Это моя первая общая библиотека, в которой я тестировал идеи для этой статьи.
Информационный бюллетень
Нравятся ли вам эти видео и сообщения в блоге? Подпишитесь на новостную рассылку Begriffs, чтобы получать уведомления о новых сообщениях, событиях и идеях. (Довольно мало, раз в несколько недель.)
Читайте также:
- Как отменить определенное обновление в Windows 10
- Реестр не открывается в Windows 10
- Как загрузить Windows
- Автозапуск Conky в Debian
- Как установить php на Windows 10