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

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

Что такое динамическая библиотека и чем она отличается от статической?

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

У динамического связывания есть ряд других преимуществ помимо уменьшения размера программы:

  1. Требуемая библиотека может быть обновлена ​​без необходимости перекомпилировать исполняемый файл в зависимости от этого.
  2. Несколько программ, которым требуется эта библиотека, могут совместно использовать одну копию в системе, уменьшая общий объем памяти, занимаемый всеми этими запущенными процессами.
  3. Часто можно включать только отдельные функции из динамической библиотеки без необходимости загружать всю библиотеку в память.

Хотя динамические библиотеки обладают рядом преимуществ, с их использованием связано несколько недостатков:

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

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

Как сделать динамическую библиотеку

Допустим, вы разработали свою собственную структуру машинного обучения и хотите создать динамическую библиотеку, которую можно будет использовать в своей работе или распространять среди других. Основной способ сделать это в системах на основе UNIX, таких как Linux, - использовать gcc и скомпилировать коллекцию кода с флагом -c, чтобы остановить компиляцию перед этапом компоновки. Затем, снова используя gcc, скомпилируйте группу файлов .o как общий объект с флагом -shared. При первоначальной компиляции в объектные файлы необходимо использовать еще один важный флаг -fPIC. PIC означает независимый от позиции код. Это означает, что инструкции машинного кода для каждой скомпилированной функции не ссылаются на абсолютные адреса памяти внутри исполняемого файла, а задаются только относительно самих себя с помощью смещения, найденного в глобальной таблице смещений или GOT. Основная причина этого заключается в том, что, поскольку разделяемая библиотека может использоваться несколькими программами одновременно, она не использует одну и ту же виртуальную память, как любой из процессов, которые ее используют. Следовательно, любой абсолютный адрес в его коде может конфликтовать с адресом процесса, который его вызвал.

Ниже показан простой bash скрипт для компиляции папки из .c файлов в динамическую библиотеку:

Как описано, опция -c останавливает компиляцию перед компоновкой, а-fPIC создает код, который можно запускать в любом месте программы, ссылаясь на GOT. Часто рекомендуется включать -g во флаги компиляции для ваших самодельных динамических библиотек, поскольку это дает отладочную информацию, которая может значительно упростить отслеживание проблем в вашем коде, особенно когда вы обновляете только динамическую библиотеку, но не приложения в зависимости от этого.

Если у вас есть общая библиотека (созданная вами или сторонней организацией), вы можете узнать, какие функции она реализует. В Linux самый простой способ сделать это - использовать утилиту nm для вывода списка символов, присутствующих в объектных файлах. Если ваша общая библиотека называется libc.so, вы можете использовать nm -D path/to/libc.so для вывода списка содержащихся в ней символов. Есть много вариантов форматирования вывода этой команды, но буква, напечатанная рядом с именем функции, указывает, происходит ли она из раздела Text (code), раздела Read only и т. Д. Unknown означает, что символ не определен в объектных файлах но часто указывает на то, что он определен в другой разделяемой библиотеке, вызываемой запрашиваемой вами.

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

Динамические библиотеки по-разному работают в разных операционных системах

До сих пор я обращался только к динамическим библиотекам в Linux. В этом контексте они обычно называются файлами общих объектов и имеют расширение .so. Это отличается от статических библиотек, которые получают расширение .a для архива. В других ОС используются другие расширения, например .dylib (macOS) или .DLL (Windows).

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

В Linux есть несколько стандартных мест для хранения и поиска разделяемых библиотек. Во-первых, /lib содержит библиотеки, используемые загрузчиком для всей системы. Во-вторых, /usr/lib содержит зрелые разделяемые библиотеки, обычно используемые системой. В-третьих, /usr/local/lib содержит общие библиотеки, установленные пользователем и самодельные. В этих местах будет выполняться поиск по умолчанию при загрузке общей библиотеки в память. Другие местоположения могут быть добавлены с помощью утилиты ldconfig, как описано ниже.

Как использовать общую библиотеку в программе

После того, как вы скомпилировали и настроили свою общую библиотеку в своей системе, процесс ее использования аналогичен статической библиотеке. С помощью gcc вы должны указать, где в файловой системе находится файл библиотеки и как он называется. Флаг -L сообщает компилятору, где искать, а флаг -l сразу предшествует базовому имени библиотеки, добавляя к написанному вами префикс lib и автоматически давая .so, например. если вы создаете библиотеку с именем libdyn.so, и она находится в том же каталоге, что и исходный код, вы должны написать gcc -L. -ldyn myfile.c.

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

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

Еще один способ - записать путь к вашей динамической библиотеке в сам исполняемый файл, присвоив компоновщику флаг -rpath=<path/to/lib>. Это сохранит путь к библиотеке в самом коде, который загрузчик распознает во время выполнения. Однако это не оптимально, потому что перемещение библиотеки нарушит работу программы.

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