Анатомия динамических библиотек Linux
Создание библиотек и взаимодействие с ними
Библиотеки были придуманы для объединения схожей функциональности в отдельные модули, которые могли использоваться совместно несколькими разработчиками. Такой подход соответствует модульному программированию, при котором программы строятся на основе модулей. В Linux доступно два вида библиотек, и каждый из них имеет свои преимущества и недостатки. При использовании статических библиотек их функциональность внедряется в программный код на этапе компиляции. Напротив, динамические библиотеки загружаются после запуска приложения, а связывание происходит на этапе выполнения. На рисунке 1 иерархически показаны виды библиотек в Linux.
Рисунок 1. Иерархия библиотек в Linux
Другие статьи Тима серии на developerWorks
Существует два способа использования совместно используемых библиотек: динамическая компоновка в момент загрузки и динамическая загрузка с подключением программным путем. В статье будут описаны оба подхода.
В простых программах с минимальной функциональностью статические библиотеки могут быть предпочтительнее. В программах же, использующих несколько библиотек, применение совместно используемых библиотек позволяет снизить потребление оперативной и дисковой памяти во время работы приложения. Это достигается за счет того, что одна совместно используемая библиотека может использоваться одновременно несколькими приложениями, при этом она присутствует в памяти в единственном экземпляре. В случае со статическими библиотеками каждая программа загружает свою собственную копию библиотечных функций.
В GNU/Linux доступно два метода работы с совместно используемыми библиотеками (оба метода берут свое начало в Sun Solaris). Первый способ – это динамическая компоновка вашего приложения с совместно используемой библиотекой. При этом загрузку библиотеки при запуске программы возьмет на себя Linux (если, конечно, она не была загружена в память раньше). Второй способ подразумевает явный вызов функций библиотеки в процессе т. н. динамической загрузки. В этом случае программа явно загружает нужную библиотеку, а затем вызывает определенную библиотечную функцию. На этом методе обычно основан механизм загрузки подключаемых программных модулей – плагинов. Оба рассматриваемых способа показаны на рисунке 2.
Рисунок 2. Сравнение статической и динамической компоновки
Динамическая компоновка в Linux
Рассмотрим подробнее процесс использования динамически компонуемых совместно используемых библиотек Linux. Приложение, которое запускает пользователь, представляет собой ELF-образ (Executable and Linking Format, формат исполняемых и компонуемых файлов). После запуска ядро вначале загружает образ программы в виртуальное адресное пространство создаваемого процесса; при этом анализируется ELF-секция под названием .interp , которая указывает, какой динамический загрузчик будет использоваться (как правило, это /lib/ld-linux.so). Содержимое этой секции представлено в листинге 1. Все это очень похоже на то, как в shell-скриптах первой строкой задается интерпретатор, который будет исполнять скрипт: #!/bin/sh.
Листинг 1. Использование утилиты readelf для вывода заголовков исполняемого файла
Кстати, ld-linux.so тоже является совместно используемой ELF-библиотекой, хотя собрана она статически и не имеет других зависимостей. В случае использования динамической компоновки ядро передает управление на динамический компоновщик (другое название – ELF-интерпретатор), который после собственной инициализации загружает указанные совместно используемые библиотеки (если они уже не в памяти). Далее динамический компоновщик производит необходимые перемещения (relocations), включая совместно используемые объекты, на которые ссылаются требуемые совместно используемые библиотеки. Путь, по которому система будет искать совместно используемые объекты, задается переменной среды LD_LIBRARY_PATH . Закончив с библиотеками, компоновщик отдает управление исходной программе, которая начинает выполнение.
В основе процесса перемещения (relocation) лежит косвенная адресация, которую обеспечивают две таблицы – глобальная таблица смещений (Global Offset Table, GOT) и таблица связывания процедур (Procedure Linkage Table, PLT). В этих таблицах содержатся адреса внешних функций и данных, которые ld-linux.so должен загрузить в процессе перемещения. Получается, что код, содержащий обращение к внешним функциям и, таким образом, ссылающийся на данные этих таблиц, остается неизменным – модифицировать требуется только таблицы. Перемещение может проходить либо сразу во время загрузки программы, либо когда понадобится нужная функция. (Эта альтернатива будет рассмотрена подробно в разделе динамическая загрузка в Linux.
По завершении перемещения динамический компоновщик исполняет стартовый код каждой совместно используемой библиотеки (если этот код имеется), содержащий инициализацию и подготовку внутренних данных. Стартовый код определяется в секции .init ELF-файла. Во время выгрузки библиотеки может выполняться также и завершающий код, определяемый в секции .fini . Вызвав функции инициализации, динамический компоновщик отдает управление исходному исполняемому образу.
Динамическая загрузка в Linux
Наряду с автоматической загрузкой и компоновкой программы с ее библиотеками есть возможность переложить эту задачу на «плечи» самой программы – это и называется динамической загрузкой. В этом случае приложение само «решает», какие библиотеки загрузить, после чего вызывает библиотечные функции, как если бы они были частью исходной программы. Однако как вы уже поняли, библиотека, отвечающая за динамическую загрузку, – это обычная совместно используемая библиотека в формате ELF. Фактически в этом процессе опять же участвует динамический компоновщик ld-linux , являющийся загрузчиком и интерпретатором ELF-файлов.
Для реализации динамической загрузки существует интерфейс динамической загрузки (Dynamic Loading API), дающий приложению пользователя возможность использовать совместно используемые библиотеки. Этот интерфейс невелик, однако он реализует все необходимое, беря всю «черную» работу на себя. Все функции интерфейса приведены в таблице 1.
Таблица 1. Полный интерфейс динамической загрузки
Функция | Описание |
---|---|
dlopen | Дает программе доступ к ELF-библиотеке |
dlsym | Возвращает адрес функции из библиотеки, загруженной при помощи dlopen |
dlerror | Возвращает текстовое описание последней возникшей ошибки |
dlclose | Закрывает доступ к библиотеке |
Вначале приложение вызывает dlopen , передавая в параметрах имя файла и режим. Функция возвращает дескриптор, который будет использоваться в дальнейшем. Режим указывает компоновщику, когда производить перемещение. Возможные варианты – RTLD_NOW (сделать все необходимые перемещения в момент вызова dlopen ) и RTLD_LAZY (перемещения по требованию). В последнем случае работают внутренние механизмы, при которых каждое первое обращение к библиотечной функции перенаправляется динамическому компоновщику и происходит перемещение. Последующее обращение к той же функции уже не требует повторного перемещения.
Есть еще две опции режима, которые можно совместить с предыдущими путем логического ИЛИ . RTLD_LOCAL означает, что символы данной совместно используемой библиотеки не будут доступны из других ELF-файлов, относящихся к нашему приложению. Если же такой доступ нужен (например, чтобы иметь доступ к символам главной программы из совместно используемой библиотеки), используйте флаг RTLD_GLOBAL .
При вызове dlopen происходит автоматическое разрешение зависимостей между библиотеками. Это значит, что если некая библиотека использует другую библиотеку, функция загрузит и ее. dlopen возвращает дескриптор, используемый для дальнейшей работы с библиотекой. Прототип функции выглядит так:
По дескриптору с помощью функции dlsym находятся адреса символов библиотеки. Функция принимает в качестве параметра дескриптор и строковое имя символа и возвращает искомый адрес:
Если при работе этих функций возникла ошибка, ее текстовую формулировку можно получить при помощи dlerror . Эта функция не имеет входных аргументов и возвращает строку, если ошибка была, и NULL, если ошибки не было:
Если работа с библиотекой закончена и приложению больше не нужны ни дескриптор, ни ее функции, программист может вызвать dlclose . Система ведет счетчик ссылок на библиотеку, поэтому загрузка/выгрузка библиотеки разными приложениям не приводит к конфликту – библиотека будет в памяти до тех пор, пока хотя бы один пользователь работает с ней. Все адреса, полученные ранее при помощи dlsym , становятся недействительными.
Пример, демонстрирующий динамическую загрузку
Изучив API динамической загрузки, предлагаю рассмотреть пример его использования. В примере мы реализуем оболочку, позволяющую оператору задавать исходное имя библиотеки, имя функции и аргумент. Другими словами, пользователь сможет вызывать любую функцию из произвольной библиотеки, не скомпонованной предварительно с приложением. Адрес функции находится посредством рассматриваемого API, после чего она вызывается с заданным аргументом и возвращает результат. Полный исходный текст примера представлен в листинге 2.
Листинг 2. Утилита, использующая API динамической загрузки
Ниже приведена команда GCC, с помощью которой я рекомендую собрать наш пример. Опция -rdynamic указывает компоновщику включить в динамическую таблицу символов результирующего файла все символы, что позволит видеть стек вызовов при работе с dlopen . Опция -ldl означает компоновку с библиотекой libdl .
Вернемся к листингу 2 и функции main . Она фактически реализует интерпретатор, который воспринимает три аргумента в строке ввода – имя библиотеки, имя функции и параметр (число с плавающей точкой). Получив end , программа завершается, а во всех остальных случаях три аргумента передаются в функцию invoke_method , использующую API динамической загрузки.
Вначале вызывается dlopen для получения доступа к объектному файлу библиотеки. Если функция вернула NULL, то файл не удалось найти, и программа завершается. В случае успеха мы получаем дескриптор библиотеки, который и будем дальше использовать. Затем мы пытаемся получить адрес указанной библиотечной функции с помощью dlsym , которая вернет либо пригодный для вызова адрес, либо NULL в случае ошибки.
После того как мы получили искомую функцию, определенную в ELF-объекте, следующим шагом является просто вызов этой функции. Сравните код этого примера и подход, соответствующий динамической компоновке из предыдущего раздела. Здесь мы приводим адрес из таблицы символов объектного файла к указателю на функцию, который затем и вызываем. При динамической компоновке мы получаем библиотечный символ (в частности, функцию), уже настроенный на правильный адрес. Хотя всю «грязную работу» может сделать за вас динамический компоновщик, именно представленный подход позволяет обеспечить максимальную гибкость при написании программ, расширяемых на стадии выполнения.
Наконец, вызвав требуемую функцию из ELF-объекта, мы разрываем с ним связь при помощи dlclose .
Пример работы с нашей программой приведен в листинге 3. Сначала мы компилируем и запускаем приложение. Затем мы вызываем несколько функций из математической библиотеки libm.so. Тем самым продемонстрирована возможность вызова программой произвольной функции, принадлежащей совместно используемой библиотеке, через механизм динамической загрузки – мощное средство для расширения функциональности приложений.
Листинг 3. Пример простой программы, вызывающей библиотечные функции
Утилиты
В Linux доступны разнообразные утилиты для вывода содержимого и анализа ELF-файлов (в том числе совместно используемых библиотек). Одна из самых полезных утилит – ldd , которая выдает список библиотек, от которых зависит данный ELF-объект. Например, для последнего примера вывод команды ldd будет выглядеть примерно так:
ldd сообщила нам, что ELF-файл dl зависит от библиотек linux-gate.so (специальный псевдофайл, который отвечает за обработку системных вызовов Linux и не принадлежит файловой системе), libdl.so (API динамической загрузки), libc.so (библиотека GNU C ) и ld-linux.so.2 (динамический загрузчик Linux – он присутствует всегда, когда есть зависимости от совместно используемых библиотек).
Понять и вывести содержимое ELF-файла поможет мощная команда readelf . Одна из интересных возможностей этой команды – вывод списка перемещаемых элементов файла. Например, покажем, какие символы в нашей тестовой программе из листинга 2 требуют перемещения:
Из этого списка видно, что перемещения требуют различные вызовы библиотек C (libc.so), в том числе вызовы к libdl.so. Обратите внимание на __libc_start_main : это функция библиотеки C , которая вызывается перед main и производит всю необходимую инициализацию.
Среди других утилит упомянем objdump , которая печатает сведения об объектных файлах, и nm , служащую для вывода списка символов объектного файла (включая отладочную информацию). Кстати, можно запускать динамический компоновщик Linux прямо из командной строки, указав в параметре ELF-программу, которую требуется выполнить:
В добавок, ld-linux.so может выдать список зависимостей для ELF-файла – точно так же, как делает команда ldd . Для этого существует опция —list . Не забывайте, что ld-linux.so является просто исполняемым приложением, которое в нужный момент запускается ядром.
Что дальше
В статье даны лишь основы работы с динамическими библиотеками. Более подробную информацию о формате ELF и о перемещении процессов и символов вы найдете в разделе Ресурсы. И, как обычно в Linux, ничто не мешает вам загрузить исходный код динамического компоновщика и понять его внутреннее устройство (ссылки вы найдете в разделе Ресурсы).
Ресурсы для скачивания
Похожие темы
- Оригинал статьи (EN)
- Прочитав статью Питера Сибаха «Совместно используемые библиотеки под пристальным взором» (EN) (developerWorks, январь 2005 г.), вы научитесь создавать и использовать совместно используемые библиотеки, а также овладеете разнообразными инструментами для анализа их содержимого.
- Загрузите исходный код динамического компоновщика Linux – самый полный источник информации о динамической компоновке и динамической загрузке.
- Прочитайте о формате ELF (EN, PDF) на сайте SkyFree.org – статья затрагивает такие темы, объектные файлы, механизм загрузки программ и библиотека C . На сайте Википедии доступно краткое описание ELF (EN) с множеством ссылок на дополнительные ресурсы по ELF-формату, включая спецификации и интерфейсы применительно к различным процессорным архитектурам.
- В блоге Криса Рольфа EM_386 (EN) приведено подробнейшее описание процесса перемещения символов для ELF. Рассказывается о GOT- и PLT-таблицах и о том, как динамический компоновщик с ними работает.
- Хорошие статьи о библиотеках (EN), статических библиотеках (EN), компоновщиках (EN) и загрузчиках (EN) вы можете прочитать на страницах Википедии.
- Знакомство с ELF можно начать с замечательной статьи «Стандарты и спецификации: Невоспетый герой: Постигаем ELF» (EN) (developerWorks, декабрь 2005 г.). ELF, будучи стандартным форматом двоичных файлов для Linux, весьма гибок и применяется для исполняемых и объектных файлов, совместно используемых библиотек и даже дампов ядра. Более подробные сведения можно найти в описании формата (EN) или в его спецификации (EN).
- В статье Linux Journal «Компоновщики и загрузчики» (EN) (ноябрь 2002) содержится превосходное объяснение компоновщиков и загрузчиков ELF-файлов, а также описаны процессы разрешения имен и перемещения.
- В разделе Linux сайта developerWorks можно найти полезные материалы для Linux-разработчиков, а также список самых популярных статей и учебников по Linux.
- Взгляните на советы и руководства по Linux на developerWorks.
- Разработайте ваш следующий Linux-проект с помощью пробного ПО от IBM (EN), которое можно загрузить прямо с developerWorks.
Комментарии
Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.