DLOPEN(3)

ИМЯ
dladdr, dlclose, dlerror, dlopen, dlsym, dlvsym - программный интерфейс к динамически компонующему загрузчику

ОБЗОР
#include <dlfcn.h>

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

char *dlerror(void);

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

int dlclose(void *handle);

Компонуется при указании параметра -ldl.

ОПИСАНИЕ
Интерфейс динамически компонующего загрузчика реализован четырьмя функциями: dlopen(), dlsym(), dlclose(), dlerror().

  dlerror()
Функция dlerror() возвращает понятную для человека строку, описывающую последнюю произошедшую ошибку в dlopen(), dlsym() или dlclose() с последнего вызова dlerror(). Она возвращает NULL, если ошибок не было с начала инициализации или с момента её последнего вызова.

  dlopen()
Функция dlopen() загружает динамическую библиотеку, имя которой указано в строке filename (завершается нулём) и возвращает прямой указатель на динамическую библиотеку. Если filename равно NULL, то возвращается указатель на основную программу. Если filename содержит косую черту ("/"), то это воспринимается как имя с путём (относительным или абсолютным). Иначе динамический компоновщик ищет библиотеку в следующих местах (подробности см. в ld.so(8)):

o   (только в ELF) Если исполняемый файл вызывающей программы содержит метку DT_RPATH, т не содержит метки DT_RUNPATH, то производится поиск в каталогах, описанных в метке DT_RPATH.

o   Если при запуске программы была определена переменная окружения LD_LIBRARY_PATH, содержащая список каталогов через двоеточие, то производится поиск в этих каталогах. (По соображениям безопасности эта переменная игнорируется для программ с установленными битами set-user-ID и set-group-ID.)

o   (только в ELF) Если исполняемый файл вызывающей программы содержит метку DT_RUNPATH, то производится поиск по каталогам, перечисленным в этой метке.

o   Производится проверка в кэширующем файле /etc/ld.so.cache (обслуживается ldconfig(8)) на предмет наличия записи для filename.

o   Просматриваются каталоги /lib и /usr/lib (именно в таком порядке).

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

В flag должно быть одно из следующих значений:

RTLD_LAZY
    Выполнять позднее связывание. Выполняется поиск только тех символов, на которые есть ссылки из кода. Если на символ никогда не ссылаются, то он никогда не будет разрешён. (Позднее связывание (lazy binding) выполняется только при ссылке на функции; ссылки на переменные всегда привязываются сразу при загрузке библиотеки.)

RTLD_NOW
    Если указано данное значение или переменная окружения LD_BIND_NOW не пуста, то все неопределённые символы в библиотеке ищутся до возврата из dlopen(). Если этого сделать не удаётся, то возвращается ошибка.

Также в В flag может быть ноль или более значение, объединяемых по ИЛИ:

RTLD_GLOBAL
    Символы, определённые в библиотеке, будут доступны при поиске символов, для загружаемых далее библиотек.

RTLD_LOCAL
    Противоположность RTLD_GLOBAL, используется по умолчанию, если не задано ни одного флага. Символы, определённые в библиотеке, не будут доступны при разрешении ссылок для загружаемых далее библиотек.

RTLD_NODELETE (начиная с glibc 2.2)
    Не выгружать библиотеку при dlclose(). В результате статические переменные библиотеки не инициализируются повторно, если библиотека загружается снова по dlopen(). Этого флага нет в POSIX.1-2001.

RTLD_NOLOAD (начиная с glibc 2.2)
    Не загружать библиотеку. Это можно использовать для тестирования, загружена ли уже библиотека (dlopen() возвращает NULL, если нет, или указатель на библиотеку, если она загружена). Этот флаг можно также использовать для изменения флагов уже загруженной библиотеке. Например, если библиотека была загружена с флагом RTLD_LOCAL, то её можно повторно открыть с RTLD_NOLOAD | RTLD_GLOBAL. Этого флага нет в POSIX.1-2001.

RTLD_DEEPBIND (начиная с glibc 2.3.4)
    Задать библиотеку, в которой поиск символов будет осуществляться перед поиском в области глобальных символов. Это означает, что самодостаточная библиотека будет использовать свои собственные символы вместо глобальных символов с тем же именем, содержащихся в библиотеках, которые уже были загружены. Этого флага нет в POSIX.1-2001.

Если значение filename равно NULL, то возвращается указатель на главную программу. При передаче в dlsym(), этот указатель вызывает поиск символа в главной программе, затем во все общих библиотеках, загруженных при запуске программы, и затем во всех общих библиотеках, загруженных dlopen() с флагом RTLD_GLOBAL.

Внешние ссылки в библиотеке разрешаются с использованием библиотек по их списку библиотечных зависимостей и других библиотеках, ранее открытых с флагом RTLD_GLOBAL. Если исполняемый файл скомпонован с параметром «-rdynamic» (или тождественным ему «--export-dynamic»), то глобальные символы в исполняемом файле будут также использоваться при разрешении зависимостей в динамически загружаемой библиотеке.

Если данная библиотека загружается с помощью dlopen() снова, то возвращается тот же указатель на библиотеку. Библиотека dl ведёт счётчик ссылок для библиотечных указателей, поэтому динамическая библиотека не высвобождается dlclose() до тех пор, пока он не будет вызвана столько же раз сколько и dlopen(). Процедура _init(), если есть, вызывается только однажды. Но последующий вызов с флагом RTLD_NOW может привести к поиску символов для библиотеки ранее загруженной с флагом RTLD_LAZY.

Если по какой-то причине dlopen() завершается неудачно, то возвращается NULL.

  dlsym()
Функция dlsym() принимает "указатель" динамической библиотеки, возвращаемой dlopen(), и null-завершённое имя символа, и возвращает адрес, по которому этот символ загружен в память. Если символ не найден, в указанной библиотеке или во всех библиотеках, которые были автоматически загружены dlopen() при загрузке этой библиотеки, dlsym() возвращает NULL. (Поиск, выполняемый dlsym(), охватывает всё дерево зависимостей этих библиотек.) Так как значением символа может быть на самом деле NULL (возвращаемый NULL из dlsym() не нужно считать ошибкой), правильным способом проверки на ошибки — это вызвать dlerror() для очистки всех старых условий ошибок, затем вызвать dlsym(), а затем вызвать dlerror() ещё раз, сохранив возвращаемое значение в переменной, и проверить, не равно ли сохранённое значение NULL.

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

  dlclose()
Функция dlclose() уменьшает счётчик ссылок на указатель динамической библиотеки handle. Если счётчик ссылок достигает нуля и нет других загруженных библиотек использующих её символы, то динамическая библиотека выгружается.

Функция dlclose() возвращает 0 в случае успешной работы, и не ноль при ошибке.

  Устаревшие символы _init() и _fini()
Компоновщик учитывает специальные символы _init и _fini. Если динамическая библиотека экспортирует процедуру с именем _init(), то её код исполняется после загрузки, до возврата из dlopen(). Если динамическая библиотека экспортирует процедуру с именем _fini(), то её код вызывается перед тем как библиотека будет выгружена. Если вам нужно избежать компоновки с системными файлами запуска, то можно воспользоваться параметром командной строки gcc(1) -nostartfiles.

Использование данных процедур или параметров gcc -nostartfiles или -nostdlib не рекомендуется. Их использование может привести к нежелаемому поведению, так как не будут выполнены процедуры конструктора/деструктора (если не будут приняты специальные меры).

Вместо этого библиотеки должны экспортировать процедуры с помощью атрибутов функций __attribute__((constructor)) и __attribute__((destructor)). Подробней см. страницы info по gcc. Процедуры конструктора выполняются до возврата из dlopen(), а процедуры деструктора выполняются перед возвратом из dlclose().

  Расширения glibc: dladdr() и dlvsym()
В glibc добавлены две функции, не описанные в POSIX, с прототипами

#define _GNU_SOURCE         /* см. feature_test_macros(7) */
#include <dlfcn.h>

int dladdr(void *addr, Dl_info *info);

void *dlvsym(void *handle, char *symbol, char *version);

Функция dladdr() вызывается с указателем на функцию и пытается найти имя и файл, где она расположена. Информация сохраняется в структуре Dl_info:

typedef struct {
    const char *dli_fname;  /* Путь к общему объекту
                               с адресом */
    void       *dli_fbase;  /* Адрес, по которому загружен общий
                               объект */
    const char *dli_sname;  /* Имя символа, чьё определение
                               перекрывается с addr */
    void       *dli_saddr;  /* Точный адрес символа с
                               именем в dli_sname */
} Dl_info;

Если символ, соответствующий адресу addr, не найден, то dli_sname и dli_saddr устанавливаются в NULL.

В случае ошибки dladdr() возвращает 0, и ненулевое значение в случае успеха.

Функция dlvsym(), предоставляемая glibc начиная с версии 2.1, работает также как и dlsym(), но ожидает строку с версией в качестве дополнительного аргумента.

СООТВЕТСТВИЕ СТАНДАРТАМ
В POSIX.1-2001 описаны dlclose(), dlerror(), dlopen() и dlsym().

ЗАМЕЧАНИЯ
Символы RTLD_DEFAULT и RTLD_NEXT определены в <dlfcn.h> только когда определён символ _GNU_SOURCE до включения. Начиная с glibc 2.2.3, atexit(3) может использоваться для регистрации обработчика завершения работы, который автоматически вызывается при выгрузке библиотеки.

  История
Стандарт интерфейса dlopen впервые появился в SunOS. В этой системе также была функция dladdr(), но не было dlvsym().

ДЕФЕКТЫ
Иногда указатели на функции, передаваемые в dladdr(), могут вас удивить. На некоторых архитектурах (в частности, i386 и x86_64), dli_fname и dli_fbase могут указывать на объект, из которого вызывалась функция dladdr(), даже если функция, использовавшаяся как аргумент, должна быть из динамически скомпонованной библиотеки.

Проблема в том, что указатель на функцию по прежнему ищется во время компиляции, но всего лишь указывает на раздел plt (таблицу компоновки процедур) первоначального объекта (которая размещает вызов после запроса динамического компоновщика на поиск символа). Чтобы обойти это, вы можете попробовать скомпилировать независимый от размещения код: в этом случае компилятор больше не сможет подготовить указатель во время компиляции и gcc(1) создаст код, который просто загрузит конечный адрес символа из got (глобальной таблицы смещений) при запуске до передачи его в dladdr().

ПРИМЕР
Загружает математическую библиотеку и печатает косинус 2.0:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int
main(int argc, char **argv)
{
    void *handle;
    double (*cosine)(double);
    char *error;
    handle = dlopen("libm.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    dlerror();    /* Очистка всех результатов ошибок */
    cosine = (double (*)(double)) dlsym(handle, "cos");
    /* Согласно стандарту ISO C, преобразование между указателями на
       функции и «void *», использовавшемуся выше, приводит к
       неопределённым результатам. В POSIX.1-2003 и POSIX.1-2008
       принимается такое поведение и предлагается следующий
       обходной вариант:
           *(void **) (&cosine) = dlsym(handle, "cos");
       Такое (топорное) преобразование удовлетворяет стандарту ISO C и
       предупреждений компилятора не будет.
       Список опечаток 2013 к POSIX.1-2008 (т.н. POSIX.1-2013) улучшает
       состояние, требуя от реализаций поддержки преобразования «void *»
       в указатель на функцию. Тем не менее, некоторые компиляторы
       (например, gcc с параметром «-pedantic») могут выдавать
       предупреждение о преобразовании в этой программе. */
    error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "%s\n", error);
        exit(EXIT_FAILURE);
    }
    printf("%f\n", (*cosine)(2.0));
    dlclose(handle);
    exit(EXIT_SUCCESS);
}

Если эта программа записана в файл с именем «foo.c», то для сборки программы введите команду:

    gcc -rdynamic -o foo foo.c -ldl

Библиотеки, экспортирующие _init() и _fini(), нужно компилировать так (на примере bar.c):

    gcc -shared -nostartfiles -o bar bar.c

СМОТРИТЕ ТАКЖЕ
ld(1), ldd(1), pldd(1), dl_iterate_phdr(3), rtld-audit(7), ld.so(8), ldconfig(8) Страницы в формате Info для ld.so, gcc и ld