Динамические библиотеки

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

Источник http://www.firststeps.ru/

Данная статья описана на примере использования языка Си, но все нижеизложенное справедливо и для Си++ когда мне будет не лень - я перепишу примеры к статье.

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

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

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

Для начала стоит сказать, что объектный файл создаваемый обычным способом не подходит для динамических библиотек. Связано это с тем, что все объектные файлы создаваемые обычным образом не имеют представления о том в какие адреса памяти будет загружена использующая их программа. Несколько различных программ могут использовать одну библиотеку, и каждая из них располагается в различном адресном пространстве. Поэтому требуется, чтобы переходы в функциях библиотеки (операции goto на ассемблере) использовали не абсолютную адресацию, а относительную. То есть генерируемый компилятором код должен быть независимым от адресов, такая технология получила название PIC - Position Independent Code. В компиляторе gcc данная возможность включается ключом -fPIC

Теперь компилирование наших файлов будет иметь вид:


$ gcc -fPIC -c f1.c
$ gcc -fPIC -c f2.c

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


$ gcc -shared -o libfsdyn.so f1.o f2.o

В результате получим динамическую библиотеку libfsdyn.so, которая по моей задумке будет динамической версией библиотеки libfs.a, что видно из названия :) Теперь, чтобы компилировать результирующий файл с использованием динамической библиотеки нам надо собрать файл командой:


$ gcc -с main.с
$ gcc main.o -L. -lfsdyn -o rezultdyn

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

На этом фокусы не кончаются, если Вы сейчас попробуете запустить файл rezultdyn, то получите ошибку:


$ ./rezultdyn
./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open shared object file: No such file or directory

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

А сейчас стоит поговорить еще об одном моменте использования библиотек. Я специально создал динамическую библиотеку с названием fsdyn, чтобы она отличалась от названия статической библиотеки fs. Дело в том, что если у Вас две библиотеки статическая и динамическая с одинаковыми названиями, то есть libfs.a и libfs.so, то компилятор всегда будет использовать динамическую библиотеку.

Связано это с тем, что в ключе -l задается часть имени библиотеки, а префикс lib и окончание .a или .so приставляет сам компилятор. Так вот алгоритм работы компилятора таков, что если есть динамическая библиотека, то она используется по умолчанию. Статическая же библиотека используется когда компилятор не может обнаружить файл .so этой библиотеки. Во всей имеющейся у меня документации пишется, что если использовать ключ -static, то можно насильно заставить компилятор использовать статическую библиотеку. Отлично, попробуем...


$ ./rezultdyn
gcc -staticmain.o -L. -lfs  -o rez1

Как бы я не пробовал играть с позицией ключа -static, результирующий файл rez1 получается размером в 900 Кб. После применения программы strip размер ее уменьшается до 200 Кб, но это же не сравнить с тем, что наша первая статическая компиляция давала программу размером 10 Кб. А связано это с тем, что любая программа написанная на C/C++ в Linux использует стандартную библиотеку "C" library, которая содержит в себе определения таких функций, как printf(), write() и всех остальных. Эта библиотека линкуется к файлу как динамическая, чтобы все программы написанные на C++ могли использовать единожды загруженные функции. Ну, а при указании ключа -static компилятор делает линковку libc статической, поэтому размер кода увеличивается на все 200 Кб.

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

Использование динамических библиотек

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


./rezultdyn
./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open 
shared object file: No such file or directory

То это сообщение выдает загрузчик динамических библиотек(динамический линковщик - dynamic linker), который в нашем случае не может обнаружить библиотеку libfsdyn.so. Для настройки динамического линковщика существует ряд программ.

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


$ ldd rezultdyn
        libfsdyn.so => not found
        libc.so.6 => /lib/libc.so.6 (0x40016000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Как видите все правильно. Программа использует три библиотеки:

  • libc.so.6 - стандартную библиотеку функций языка C++.
  • ld-linux.so.2 - библиотеку динамической линковки программ ELF формата.
  • libfsdyn.so - нашу динамическую библиотеку функций.

Нашу библиотеку она найти не может. И правильно! Динамический линковщик ищет библиотеки только в известных ему каталогах, а каталог нашей программы ему явно не известен.

Для того, чтобы добавить нашу директорию с библиотекой в список известных директорий надо подредактировать файл /etc/ld.so.conf.

Лучше закинуть конфиг-файл .conf в директорию: /etc/ld.so.conf.d/

Например, у меня этот файл состоит из таких строк:


$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
/usr/X11R6/lib
/usr/i386-slackware-linux/lib
/usr/i386-slackware-linux-gnulibc1/lib
/usr/i386-slackware-linux-gnuaout/lib

Во всех этих директории хранятся всеми используемые библиотеки. В этом списке нет лишь одной директории - /lib, которая сама по себе не нуждается в описании, так как она является главной. Получается, что наша библиотека станет "заметной", если поместить ее в один их этих каталогов, либо отдельно описать в отдельном каталоге. Давайте для теста опишем, добавим строку в конец файла ld.so.conf

После этого динамический линковщик будет знать где можно найти наш файл, но после изменения конфигурационного файла ld.so.conf необходимо, чтобы система перечитала настройки заново. Это делает программа ldconfig. Пробуем запустить нашу программу:


$ ldconfig
$ ./rezultdyn
f1() = 25
f2() = 10

Заработало :) Если теперь удалить добавленную нами строку и снова запустить ldconfig, то данные о расположении нашей библиотеки исчезнут и будет появляться таже самая ошибка.

Но описанный метод влияет на всю систему в целом и требует доступа администратора системы, т.е. root. А если Вы простой пользователь без сверх возможностей ?!

Для такого случая есть другое безболезненное решение. Это использование специальной переменной среды LD_LIBRARY_PATH, в которой перечисляются все каталоги содержащие пользовательские динамические библиотеки. Для того, чтобы установить эту переменную в командной среде bash надо набрать всего несколько команд. Для начала посмотрим есть ли у нас такая переменная среды:


$ echo $LD_LIBRARY_PATH

У меня в ответ выводится пустая строка, означающая, что такой переменной среды нет. Устанавливается она следующим образом:


$ LD_LIBRARY_PATH=/root
$ export LD_LIBRARY_PATH

После этого программа rezultdyn будет прекрасно работать. В случае, если у Вас в системе эта переменная среды уже уставновлена, то, чтобы не испортить ее значение, надо новый каталог прибавить к старому значению. Делается это другой командой:


$ LD_LIBRARY_PATH=/root:${LD_LIBRARY_PATH}
$ export LD_LIBRARY_PATH

Если Вы обнулите эту переменную, то снова библиотека перестанет работать:


$ LD_LIBRARY_PATH=""
$ export LD_LIBRARY_PATH
$ ./rezultdyn
./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open 
shared object file: No such file or directory

Вы также параллельно можете зайти в систему под другим пользователем или даже тем же самым, но если Вы захотите просмотреть значение LD_LIBRARY_PATH, то увидите ее прежнее значение. Это означает, что два разных пользователя Linux не могут влиять на работу друг друга, а это и есть самое главное хорошее отличие систем Unix от большинства других систем.

Функции работы с динамическими библиотеками

Использовать динамические библиотеки можно не только в начале загрузки, но и в процессе самой работы программы. Программа сама может вызывать любые функции из библиотеки, когда ей захочется. Для этого всего-лишь надо использовать библиотеку dl, которая позволяет линковать библиотеки "на лету". Она управляет загрузкой динамических библиотек, вызовом функций из них и выгрузкой после конца работы.

Для использования функций программной работы с динамическими библиотеками необходимо подключить заголовочный файл:


#include <dlfcn.c>

Чтобы вызывать какие-то функции из динамической библиотеки сначала надо открыть эту библиотеку (можно сказать "загрузить"). Открывается она функцией:


void *dlopen (const char *filename, int flag);

Параметр filename содержит путь до требуемой библиотеки, а параметр flag задает некоторые специфические флаги для работы. Функция возвращает указатель на загруженную библиотеку. В случае любой ошибки возвращается указатель NULL. В таком случае тест ошибки понятный человеку можно получить с помощью функции dlerror(). Пока мы не будем задумываться над этим, и я приведу стандартный код для открытия библиотеки:


void *library_handler;

//......

//загрузка библиотеки

library_handler = dlopen("/path/to/the/library.so",RTLD_LAZY);
if (!library_handler){
    //если ошибка, то вывести ее на экран
    fprintf(stderr,"dlopen() error: %s\n", dlerror());
    exit(1); // в случае ошибки можно, например, закончить работу программы
};

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


void *dlsym(void *handle, char *symbol);

Для этой функции требуется адрес загруженной библиотеки handle, полученный при открытии функцией dlopen(). Требуемая функция или переменная задается своим именем в переменной symbol.

Закрывается библиотека функцией:


dlclose(void *handle);

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

Для примера создадим программу, которая в качестве параметра получает название функции, которую она будет использовать в работе. Например, это будут математические функции возведения в степень. Создадим сначала динамическую библиотеку. Пишем ее код:


double power2(double x){
    return x*x;
};
double power3(double x){
    return x*x*x;
};
double power4(double x){
    return power2(x)*power2(x);
};

Сохраняем его в файл lib.c и создаем динамическую библиотеку libpowers.so следующими командами:


$ gcc -fPIC -c lib.c
$ gcc -shared lib.o -o libpowers.so

Теперь создаем основную программу в файле main.c:


#include <stdio.h>

/* заголовочный файл для работы с динамическими библиотеками */
#include <dlfcn.h>

int main(int argc, char* argv[]){

    void *ext_library;  // хандлер внешней библиотеки
    double value=0;     // значение для теста
    double (*powerfunc)(double x);  // переменная для хранения адреса функции

    //загрузка библиотеки
    ext_library = dlopen("/root/libpowers.so",RTLD_LAZY);
    if (!ext_library){
        //если ошибка, то вывести ее на экран
        fprintf(stderr,"dlopen() error: %s\n", dlerror());
        return 1;
    };

    //загружаем из библиотеки требуемую процедуру
    powerfunc = dlsym(ext_library, argv[1]);    
    value=3.0;

    //выводим результат работы процедуры
    printf("%s(%f) = %f\n",argv[1],value,(*powerfunc)(value));

    //закрываем библиотеку
    dlclose(ext_library);
};

Код главной программы готов. Требуется его откомпилировать с использованием библиотеки dl:


$ gcc main.c -o main -ldl

Получим программный файл main, который можно тестировать. Наша программа должна возводить значение 3.0 в требуемую нами степень, которая задается названием функции. Давайте попробуем:


$ ./main power2
power2(3.000000) = 9.000000
$ ./main power3
power3(3.000000) = 27.000000
$ ./main power4
power4(3.000000) = 81.000000

На этом все.

Тут про статические библиотеки

По теме: