Анатомия динамических библиотек Linux

Создание библиотек и взаимодействие с ними

Динамически компонуемые совместно используемые библиотеки чрезвычайно важны в GNU/Linux®. При помощи них приложения получают доступ к внешним функциям. Библиотеки подключаются на стадии выполнения, а общее потребление памяти сокращается, так как используется только необходимая на данный момент функциональность. В данной статье рассказывается, как создавать и использовать динамические библиотеки, описываются различные утилиты для работы с ними, а также объясняются внутренние принципы работы динамических библиотек.

М. Тим Джонс, инженер-консультант, Emulex

M. Тим Джонс (M. Tim Jones) является архитектором встраиваимого программного обеспечения и автором работ: Программирование Приложений под GNU/Linux, Программирование AI-приложений и Использование BSD-сокетов в различных языках программирования. Он имеет опыт разработки процессоров для геостационарных космических летательных аппаратов, а также разработки архитектуры встраиваемых систем и сетевых протоколов. Сейчас Тим работает инженером-консультантом в корпорации Эмулекс (Emulex Corp.) в г.Лонгмонт, Колорадо.



10.03.2009

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

Рисунок 1. Иерархия библиотек в Linux
Иерархия библиотек в 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, ничто не мешает вам загрузить исходный код динамического компоновщика и понять его внутреннее устройство (ссылки вы найдете в разделе Ресурсы).

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


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

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=375092
ArticleTitle=Анатомия динамических библиотек Linux
publish-date=03102009