MyTetra Share
Делитесь знаниями!
Контейнеры для мусора купить контейнеры для мусора tverklining.ru.
Динамическая загрузка библиотек в Linux
11.03.2009
00:24
Автор: M. Тим Джонс (M. Tim Jones)
Раздел: Компьютер - Программирование - Язык C

Наряду с автоматической загрузкой и компоновкой программы и её библиотек, есть возможность переложить эту задачу на "плечи" самой программы – это и называется динамической загрузкой библиотек. В этом случае приложение само "решает", какие библиотеки загрузить, после чего вызывает библиотечные функции, как если бы они были частью исходной программы. Однако как вы уже поняли, библиотека, отвечающая за динамическую загрузку, – это обычная совместно используемая библиотека в формате 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$

Так же в этом разделе:
 
MyTetra Share v.0.43
Яндекс индекс цитирования