如果在运行时满足特定条件,我需要动态打开一个共享库 lib.so。该库包含约 700 个函数,我需要加载它们的所有符号。
dlopen 和 dlsym 的角色
当您dlopen 一个库时,该库定义的所有函数都可以在您的virtual address space 中使用(因为该库的所有代码段都通过dlopen 调用mmap(2) 多次添加到您的虚拟地址空间中次)。所以dlsym 不要添加(或加载)任何额外的代码,它已经存在了。如果你的程序运行在pid 1234的进程中,在dlopen成功后尝试cat /proc/1234/maps。
dlsym 提供的功能是使用该ELF 插件中的一些动态symbol table 从其名称中获取该共享库中某物的地址。如果您不需要,则无需致电dlsym。
也许您可以在共享库中简单地拥有一个包含所有相关函数的大型数组(可作为共享库中的全局变量使用)。然后,您只需调用一次dlsym 即可获取该全局变量的名称。
顺便说一句,您的插件的 构造函数(constructor 是 function attribute)函数可以改为“注册”该插件的某些函数(到主程序的某些全局数据结构中;这Ocaml dynamic linking 是如何工作的);所以永远不要调用dlsym 并且仍然能够使用插件的功能甚至是有意义的。
对于一个插件,它的 constructor 函数在dlopen 调用(在dlopen 返回之前!),它的 destructor 函数在dlclose 调用时间(dlclose 返回之前)。
重复调用dlsym
经常使用dlsym 是很常见的做法。您的主程序将声明几个变量(或其他数据,例如某些struct、数组组件等中的字段)并用dlsym 填充这些变量。拨打dlsym 几百次真的很快。例如你可以声明一些全局变量
void*p_func_a;
void*p_func_b;
(您经常将它们声明为指向适当的、可能是不同类型的函数的指针;也许是use typedef to declare signatures)
然后你会加载你的插件
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); };
(当然你可以使用预处理宏来缩短重复代码;X-macro 技巧很方便。)
拨打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 */
};
然后你只需要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 或受限的visibility。然后,我们不需要从加载插件的主程序(应该提供register_plugin_function 函数)调用dlsym。
参考
请仔细阅读 dynamic loading 和 plug-ins 和 linker 维基页面。阅读莱文的Linkers and Loaders 书。阅读elf(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-ing 它,并重复。它表明(耐心地)你可以在同一个进程中拥有数百万个插件dlopen-ed,并且你可以多次调用dlsym。