Анатомия динамических библиотек Linux
Создание библиотек и взаимодействие с ними
Библиотеки были придуманы для объединения схожей функциональности в отдельные модули, которые могли использоваться совместно несколькими разработчиками. Такой подход соответствует модульному программированию, при котором программы строятся на основе модулей. В Linux доступно два вида библиотек, и каждый из них имеет свои преимущества и недостатки. При использовании статических библиотек их функциональность внедряется в программный код на этапе компиляции. Напротив, динамические библиотеки загружаются после запуска приложения, а связывание происходит на этапе выполнения. На рисунке 1 иерархически показаны виды библиотек в Linux.
Рисунок 1. Иерархия библиотек в Linux

Кликните, чтобы увидеть увеличенное изображение
Существует два способа использования совместно используемых библиотек: динамическая компоновка в момент загрузки и динамическая загрузка с подключением программным путем. В статье будут описаны оба подхода.
В простых программах с минимальной функциональностью статические библиотеки могут быть предпочтительнее. В программах же, использующих несколько библиотек, применение совместно используемых библиотек позволяет снизить потребление оперативной и дисковой памяти во время работы приложения. Это достигается за счет того, что одна совместно используемая библиотека может использоваться одновременно несколькими приложениями, при этом она присутствует в памяти в единственном экземпляре. В случае со статическими библиотеками каждая программа загружает свою собственную копию библиотечных функций.
В 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 для вывода заголовков исполняемого файла
mtj@camus:~/dl$ readelf -l dl Elf file type is EXEC (Executable file) Entry point 0x8048618 There are 7 program headers, starting at offset 52 Тип Elf файла – EXEC (исполняемый файл) Точка входа 0x8048618 Имеется 7 заголовков, начиная со смещения 52 Program Headers (Заголовки программы): Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align (Тип) (Смещ.) (Вирт.адр.)(Физ.адр.) (Разм.файла)(Разм.пам.)(Флаги)(Выравн.) PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] [Запрашиваемый интерпретатор программы: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00958 0x00958 R E 0x1000 LOAD 0x000958 0x08049958 0x08049958 0x00120 0x00128 RW 0x1000 DYNAMIC 0x00096c 0x0804996c 0x0804996c 0x000d0 0x000d0 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 ... mtj@camus:~dl$
Кстати, 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
возвращает дескриптор, используемый для дальнейшей работы с библиотекой. Прототип функции выглядит так:
#include <dlfcn.h> void *dlopen( const char *file, int mode );
По дескриптору с помощью функции dlsym
находятся адреса символов библиотеки. Функция принимает в качестве параметра дескриптор и строковое имя символа и возвращает искомый адрес:
void *dlsym( void *restrict handle, const char *restrict name );
Если при работе этих функций возникла ошибка, ее текстовую формулировку можно получить при помощи dlerror
. Эта функция не имеет входных аргументов и возвращает строку, если ошибка была, и NULL, если ошибки не было:
char *dlerror();
Если работа с библиотекой закончена и приложению больше не нужны ни дескриптор, ни ее функции, программист может вызвать dlclose
. Система ведет счетчик ссылок на библиотеку, поэтому загрузка/выгрузка библиотеки разными приложениям не приводит к конфликту – библиотека будет в памяти до тех пор, пока хотя бы один пользователь работает с ней. Все адреса, полученные ранее при помощи dlsym
, становятся недействительными.
char *dlclose( void *handle );
Пример, демонстрирующий динамическую загрузку
Изучив API динамической загрузки, предлагаю рассмотреть пример его использования. В примере мы реализуем оболочку, позволяющую оператору задавать исходное имя библиотеки, имя функции и аргумент. Другими словами, пользователь сможет вызывать любую функцию из произвольной библиотеки, не скомпонованной предварительно с приложением. Адрес функции находится посредством рассматриваемого API, после чего она вызывается с заданным аргументом и возвращает результат. Полный исходный текст примера представлен в листинге 2.
Листинг 2. Утилита, использующая API динамической загрузки
#include <stdio.h> #include <dlfcn.h> #include <string.h> #define MAX_STRING 80 void invoke_method( char *lib, char *method, float argument ) { void *dl_handle; float (*func)(float); char *error; /* Открываем совместно используемую библиотеку */ dl_handle = dlopen( lib, RTLD_LAZY ); if (!dl_handle) { printf( "!!! %s\n", dlerror() ); return; } /* Находим адрес функции в библиотеке */ func = dlsym( dl_handle, method ); error = dlerror(); if (error != NULL) { printf( "!!! %s\n", error ); return; } /* Вызываем функцию по найденному адресу и печатаем результат */ printf(" %f\n", (*func)(argument) ); /* Закрываем объект */ dlclose( dl_handle ); return; } int main( int argc, char *argv[] ) { char line[MAX_STRING+1]; char lib[MAX_STRING+1]; char method[MAX_STRING+1]; float argument; while (1) { printf("> "); line[0]=0; fgets( line, MAX_STRING, stdin); if (!strncmp(line, "end", 3)) break; sscanf( line, "%s %s %f", lib, method, &argument); invoke_method( lib, method, argument ); } }
Ниже приведена команда GCC, с помощью которой я рекомендую собрать наш пример. Опция -rdynamic
указывает компоновщику включить в динамическую таблицу символов результирующего файла все символы, что позволит видеть стек вызовов при работе с dlopen
. Опция -ldl
означает компоновку с библиотекой libdl
.
gcc -rdynamic -o dl dl.c -ldl
Вернемся к листингу 2 и функции main
. Она фактически реализует интерпретатор, который воспринимает три аргумента в строке ввода – имя библиотеки, имя функции и параметр (число с плавающей точкой). Получив end
, программа завершается, а во всех остальных случаях три аргумента передаются в функцию invoke_method
, использующую API динамической загрузки.
Вначале вызывается dlopen
для получения доступа к объектному файлу библиотеки. Если функция вернула NULL, то файл не удалось найти, и программа завершается. В случае успеха мы получаем дескриптор библиотеки, который и будем дальше использовать. Затем мы пытаемся получить адрес указанной библиотечной функции с помощью dlsym
, которая вернет либо пригодный для вызова адрес, либо NULL в случае ошибки.
После того как мы получили искомую функцию, определенную в ELF-объекте, следующим шагом является просто вызов этой функции. Сравните код этого примера и подход, соответствующий динамической компоновке из предыдущего раздела. Здесь мы приводим адрес из таблицы символов объектного файла к указателю на функцию, который затем и вызываем. При динамической компоновке мы получаем библиотечный символ (в частности, функцию), уже настроенный на правильный адрес. Хотя всю "грязную работу" может сделать за вас динамический компоновщик, именно представленный подход позволяет обеспечить максимальную гибкость при написании программ, расширяемых на стадии выполнения.
Наконец, вызвав требуемую функцию из ELF-объекта, мы разрываем с ним связь при помощи dlclose
.
Пример работы с нашей программой приведен в листинге 3. Сначала мы компилируем и запускаем приложение. Затем мы вызываем несколько функций из математической библиотеки libm.so. Тем самым продемонстрирована возможность вызова программой произвольной функции, принадлежащей совместно используемой библиотеке, через механизм динамической загрузки – мощное средство для расширения функциональности приложений.
Листинг 3. Пример простой программы, вызывающей библиотечные функции
mtj@camus:~/dl$ gcc -rdynamic -o dl dl.c -ldl mtj@camus:~/dl$ ./dl > libm.so cosf 0.0 1.000000 > libm.so sinf 0.0 0.000000 > libm.so tanf 1.0 1.557408 > конец mtj@camus:~/dl$
Утилиты
В Linux доступны разнообразные утилиты для вывода содержимого и анализа ELF-файлов (в том числе совместно используемых библиотек). Одна из самых полезных утилит – ldd
, которая выдает список библиотек, от которых зависит данный ELF-объект. Например, для последнего примера вывод команды ldd
будет выглядеть примерно так:
mtj@camus:~/dl$ ldd dl linux-gate.so.1 => (0xffffe000) libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7fdb000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7eac000) /lib/ld-linux.so.2 (0xb7fe7000) mtj@camus:~/dl$
ldd
сообщила нам, что ELF-файл dl
зависит от библиотек linux-gate.so (специальный псевдофайл, который отвечает за обработку системных вызовов Linux и не принадлежит файловой системе), libdl.so (API динамической загрузки), libc.so (библиотека GNU C
) и ld-linux.so.2 (динамический загрузчик Linux – он присутствует всегда, когда есть зависимости от совместно используемых библиотек).
Понять и вывести содержимое ELF-файла поможет мощная команда readelf
. Одна из интересных возможностей этой команды – вывод списка перемещаемых элементов файла. Например, покажем, какие символы в нашей тестовой программе из листинга 2 требуют перемещения:
mtj@camus:~/dl$ readelf -r dl Relocation section '.rel.dyn' at offset 0x520 contains 2 entries: (Раздел перемещения '.rel.dyn' со смещением 0x520 содержит 2 пункта:) Offset Info Type Sym.Value Sym. Name (Смещение) (Инфо) (Тип) (Знач.симв.)(Имя симв.) 08049a3c 00001806 R_386_GLOB_DAT 00000000 __gmon_start__ 08049a78 00001405 R_386_COPY 08049a78 stdin Relocation section '.rel.plt' at offset 0x530 contains 8 entries: Offset Info Type Sym.Value Sym. Name 08049a4c 00000207 R_386_JUMP_SLOT 00000000 dlsym 08049a50 00000607 R_386_JUMP_SLOT 00000000 fgets 08049a54 00000b07 R_386_JUMP_SLOT 00000000 dlerror 08049a58 00000c07 R_386_JUMP_SLOT 00000000 __libc_start_main 08049a5c 00000e07 R_386_JUMP_SLOT 00000000 printf 08049a60 00001007 R_386_JUMP_SLOT 00000000 dlclose 08049a64 00001107 R_386_JUMP_SLOT 00000000 sscanf 08049a68 00001907 R_386_JUMP_SLOT 00000000 dlopen mtj@camus:~/dl$
Из этого списка видно, что перемещения требуют различные вызовы библиотек C
(libc.so), в том числе вызовы к libdl.so. Обратите внимание на __libc_start_main
: это функция библиотеки C
, которая вызывается перед main
и производит всю необходимую инициализацию.
Среди других утилит упомянем objdump
, которая печатает сведения об объектных файлах, и nm
, служащую для вывода списка символов объектного файла (включая отладочную информацию). Кстати, можно запускать динамический компоновщик Linux прямо из командной строки, указав в параметре ELF-программу, которую требуется выполнить:
mtj@camus:~/dl$ /lib/ld-linux.so.2 ./dl > libm.so expf 0.0 1.000000 >
В добавок, 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.