mda*_*dag 7 c function-pointers shared-libraries dlopen
lib.so
如果在运行时满足特定条件,则需要动态打开共享库。该库包含约700个函数,我需要加载所有符号。
一个简单的解决方案是定义指向其中包含的所有符号的函数指针lib.so
,使用加载库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 <stdio.h>
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 <dlfcn.h>
#include <stdio.h>
#include <lib.h>
// 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
Run Code Online (Sandbox Code Playgroud)
请注意,其中的应用程序代码main.c
仅使用局部函数(包装库函数),而完全不必弄乱函数指针(除了在启动时初始化重定向表外,无论如何应自动生成代码)。
编辑:我已经创建了一个独立的工具Implib.so来自动创建存根库,例如上面的示例。事实证明,这或多或少等同于众所周知的Windows DLL导入库。
如果在运行时满足特定条件,我需要动态打开共享库 lib.so。该库包含约 700 个函数,我需要加载它们的所有符号。
dlopen
和的作用dlsym
当您dlopen
使用库时,该库定义的所有函数在您的虚拟地址空间中都可用(因为该库的所有代码段都通过多次dlopen
调用mmap(2)添加到您的虚拟地址空间中)。所以dlsym
不要添加(或加载)任何额外的代码,它已经存在了。如果你的程序运行在pid 1234的进程中,请cat /proc/1234/maps
在成功后尝试dlopen
。
所dlsym
提供的是使用该ELF插件中的一些动态符号表,从其名称获取该共享库中某些内容的地址的能力。如果不需要,则无需调用.dlsym
也许您可以简单地在共享库中拥有一个包含所有相关函数的大型数组(可作为共享库中的全局变量使用)。然后您只需要调用dlsym
一次该全局变量的名称。
顺便说一句,插件的构造函数( constructor
是一个函数属性)函数可以代替“注册”该插件的某些函数(到主程序的某些全局数据结构中;这就是Ocaml 动态链接的工作原理);所以从不调用dlsym
并且仍然能够使用插件的功能甚至是有意义的。
对于插件,它的构造函数在dlopen
时间(在dlopen
返回之前!)被调用,它的析构函数在dlclose
时间(在dlclose
返回之前)被调用。
dlsym
dlsym
多次使用是常见的做法。您的主程序将声明几个变量(或其他数据,例如 some 中的字段struct
、数组组件等...)并用dlsym
. 打电话dlsym
几百次真的很快。例如,您可以声明一些全局变量
void*p_func_a;
void*p_func_b;
Run Code Online (Sandbox Code Playgroud)
(您经常将这些声明为指向适当的,也许是不同类型的函数的指针;也许用于typedef
声明签名)
你会加载你的插件
void*plh = dlopen("/usr/lib/myapp/myplugin.so", RTLD_NOW);
if (!plh) { fprintf(stderr, "dlopen failure %s\n", dlerror());
exit(EXIT_FAILURE); };
Run Code Online (Sandbox Code Playgroud)
然后你将获取函数指针
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); };
Run Code Online (Sandbox Code Playgroud)
(当然,您可以使用预处理器宏来缩短此类重复代码;X 宏技巧很方便。)
不要害羞地拨打dlsym
数百次。要然而重要的定义和文档适当的约定关于你的插件(如解释说,每个插件都应该定义func_a
和func_b
和(使用时,他们通过你的主程序调用p_func_a
等等......有)。如果你的约定需要数百不同的名字,它是难闻的气味。
因此,假设您的库定义了func_a
, func_b
, func_c1
, ...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 */
};
Run Code Online (Sandbox Code Playgroud)
然后你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...
};
Run Code Online (Sandbox Code Playgroud)
并且使用这样的构造函数func_a
等...可能是static
或具有受限的可见性。然后我们不需要dlsym
从主程序(它应该提供register_plugin_function
函数)加载插件的任何调用。
更仔细地阅读动态加载和插件以及链接器维基页面。阅读 Levine 的Linkers and Loaders一书。阅读elf(5)、proc(5)、ld-linux(8)、dlopen(3)、dlsym(3)、dladdr(3)。玩objdump的(1) ,纳米(1) ,readelf(1) 。
当然,请阅读 Drepper 的“如何编写共享库”论文。
顺便说一句,你可以打电话dlopen
,然后dlsym
一个大很多倍。我的manydl.c程序正在生成“随机”C 代码,将其编译为插件,然后dlopen
-ing 和dlsym
-ing 它,并重复。它表明(耐心地)您可以dlopen
在同一个过程中拥有数百万个插件,并且您可以调用dlsym
很多次。