Есть ли элегантный способ избежать dlsym при использовании dlopen в C?

Мне нужно динамически открывать общую библиотеку lib.so если во время выполнения выполняется определенное условие. Библиотека содержит ~ 700 функций, и мне нужно загрузить все их символы.

Простое решение – определить указатели на все символы, содержащиеся в lib.so , загрузить библиотеку с помощью dlopen и, наконец, получить адреса всех символов с помощью dlsym . Однако, учитывая количество функций, код, реализующий это решение, очень громоздкий.

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

Мне нужно динамически открывать общую библиотеку lib.so, если во время выполнения выполняется определенное условие. Библиотека содержит ~ 700 функций, и мне нужно загрузить все их символы.

роль dlopen и dlsym

Когда вы dlopen библиотеку, все функции, определенные этой библиотекой, становятся доступными в вашем виртуальном адресном пространстве (потому что весь сегмент кода этой библиотеки добавляется в ваше виртуальное адресное пространство посредством dlopen вызывающего mmap (2) несколько раз). Так что dlsym не добавляет (или загружает) какой-либо дополнительный код, он уже есть. Если ваша программа запущена в процессе pid 1234, попробуйте cat /proc/1234/maps после успешного dlopen .

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

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

BTW, конструктор ( constructor – это атрибут функции ), функция вашего плагина могла вместо этого «зарегистрировать» некоторые функции этого плагина (в какую-то глобальную структуру данных вашей основной программы, так работает динамическая компоновка Ocaml ); поэтому даже имеет смысл никогда не звонить dlsym и все еще иметь возможность использовать функции вашего плагина.

Для плагина его функции- конструкторы вызываются в dlclose time (до возвращения dlclose !), А его функции деструктора вызываются в dlclose time (до возвращения dlclose ).

повторение звонков в dlsym

Общепринятой практикой является использование dlsym много раз. Ваша основная программа объявит несколько переменных (или другие данные, например, поля в некоторых struct , компонентах массива и т. Д.) И заполнит их dlsym . Вызов dlsym несколько сотен раз очень быстрый. Например, вы можете объявить некоторые глобальные переменные

 void*p_func_a; void*p_func_b; 

(вы часто объявляете их как указатели на функции соответствующих и, возможно, разных типов, возможно, используйте typedef для объявления подписи s)

и вы загрузите свой плагин

 void*plh = dlopen("/usr/lib/myapp/myplugin.so", RTLD_NOW); if (!plh) { fprintf(stderr, "dlopen failure %s\n", dlerror()); exit(EXIT_FAILURE); }; 

то вы получите функции указателей с

 p_func_a = dlsym(plh, "func_a"); if (!p_func_a) { fprintf(stderr, "dlsym func_a failure %s\n", dlerror()); exit(EXIT_FAILURE); }; p_func_b = dlsym(plh, "func_b"); if (!p_func_b) { fprintf(stderr, "dlsym func_b failure %s\n", dlerror()); exit(EXIT_FAILURE); }; 

(конечно, вы могли бы использовать macros препроцессора, чтобы сократить такой повторяющийся код, X-macro трюки удобны.)

Не стесняйтесь звонить dlsym сотни раз. Однако важно определить и документировать соответствующие соглашения относительно вашего плагина (например, объясните, что каждый плагин должен определять func_a и func_b и когда они func_b вашей основной программой (используя p_func_a т. Д.) Там. Если в ваших соглашениях требуются сотни разных имена, это плохой запах.

функции агломерационного плагина в структуру данных

Итак, предположим, что ваша библиотека определяет func_a , func_b , func_c1 , … func_c99 т. Д., func_c99 т. Д. У вас может быть глобальный массив (POSIX позволяет выполнять функции литья в void* но стандарт C11 не позволяет этого):

 const void* globalarray[] = { (void*)func_a, (void*)func_b, (void*)func_c1, /// etc (void*)func_c99, /// etc NULL /* final sentinel value */ }; 

и тогда вам понадобится dlsym только один символ: globalarray ; Я не знаю, нужно ли тебе это или нужно. Конечно, вы можете использовать более привлекательные структуры данных (например, имитировать vtables или рабочие таблицы).


использование функции- конструктора в вашем плагине

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

 static void my_plugin_starter(void) __attribute__((constructor)); void my_plugin_starter(void) { register_plugin_function ("func", 0, (void*)func_a); register_plugin_function ("func", 1, (void*)func_b); /// etc... register_plugin_function ("func", -1, (void*)func_c1); /// etc... }; 

и с таким конструктором func_a т. д. может быть static или с ограниченной видимостью . Тогда нам не нужен вызов dlsym из основной программы (которая должна предоставить функцию register_plugin_function ), загружая плагин.


Рекомендации

Внимательно прочитайте динамическую загрузку и подключаемые модули и графические википапы. Прочитайте книгу Линексеров и погрузчиков Левина. Читайте эльф (5) , proc (5) , ld-linux (8) , dlopen (3) , dlsym (3) , dladdr (3) . Играйте с objdump (1) , nm (1) , readelf (1) .

Конечно, прочитайте статью Drepper How To Write Shared Libraries .

Кстати, вы можете назвать dlopen тогда dlsym много раз. Моя программа manydl.c генерирует «случайный» код C, компилирует его как плагин, затем dlopen -ing и dlsym it и повторяет. Это демонстрирует, что (с терпением) у вас могут быть миллионы плагинов, которые были бы dlopen в одном и том же процессе, и вы можете называть dlsym много раз.

Вы можете автоматически генерировать функции батута для всех символов в библиотеке dlopen -ed. Батуты будут рассматриваться как обычные функции в приложении, но будут внутренне перенаправляться на реальный код в библиотеке. Вот простой 5-минутный PoC:

 $ cat lib.h // Dynamic library header #ifndef LIB_H #define LIB_H extern void foo(int); extern void bar(int); extern void baz(int); #endif $ cat lib.c // Dynamic library implementation #include  void foo(int x) { printf("Called library foo: %d\n", x); } void bar(int x) { printf("Called library baz: %d\n", x); } void baz(int x) { printf("Called library baz: %d\n", x); } $ cat main.c // Main application #include  #include  #include  // Should be autogenerated void *fptrs[100]; void init_trampoline_table(void *h) { fptrs[0] = dlsym(h, "foo"); fptrs[1] = dlsym(h, "bar"); fptrs[2] = dlsym(h, "baz"); } int main() { void *h = dlopen("./lib.so", RTLD_LAZY); init_trampoline_table(h); printf("Calling wrappers\n"); foo(123); bar(456); baz(789); printf("Returned from wrappers\n"); return 0; } $ cat trampolines.S // Trampoline code. // Should be autogenerated. Each wrapper gets its own index in table. // TODO: abort if table wasn't initialized. .text .globl foo foo: jmp *fptrs .globl bar bar: jmp *fptrs+8 .globl baz baz: jmp *fptrs+16 $ gcc -fPIC -shared -O2 lib.c -o lib.so $ gcc -I. -O2 main.c trampolines.S -ldl $ ./a.out Calling wrappers Called library foo: 123 Called library baz: 456 Called library baz: 789 Returned from wrappers 

Обратите внимание, что код приложения в main.c использует только локальные функции (которые обертывают библиотечные функции) и не должен вообще возиться с указателями функций (кроме инициализации таблицы перенаправления при запуске, которая в любом случае должна быть автогенерированным кодом).

EDIT: Я создал автономный инструмент Implib.so для автоматизации создания библиотек-заглушек, как в приведенном выше примере. Это оказалось более или менее эквивалентным известным библиотекам импорта DLL Windows.