在C中使用dlopen时,是否有一种优雅的方法来避免dlsym?

如果在运行时满足特定条件,我需要动态打开共享库lib.so 该库包含~700个函数,我需要加载它们的所有符号。

一个简单的解决方案是定义lib.so包含的所有符号的函数指针,使用dlopen加载库,最后使用dlsym获取所有符号的地址。 但是,考虑到函数的数量,实现此解决方案的代码非常麻烦。

我想知道是否存在更优雅和简洁的解决方案,可能适当使用宏来定义函数指针。 谢谢!

如果在运行时满足特定条件,我需要动态打开共享库lib.so. 该库包含~700个函数,我需要加载它们的所有符号。

dlopendlsym作用

当您dlopen库时,该库定义的所有函数都可以在您的虚拟地址空间中使用 (因为该库的所有代码段都通过dlopen多次调用mmap(2)添加到您的虚拟地址空间中)。 所以dlsym不添加(或加载)任何额外的代码,它已经存在。 如果您的程序在pid 1234的过程中运行,请在成功执行dlopen后尝试cat /proc/1234/maps

dlsym提供的是使用该ELF插件中的一些动态符号表从该名称获取该共享库中某些内容的地址的能力。 如果你不需要,你不需要调用dlsym

也许你可以在共​​享库中简单地拥有一大堆所有相关函数(在共享库中作为全局变量提供)。 然后你只需要调用一次dlsym ,作为该全局变量的名称。

BTW,你的插件的构造函数constructor是一个函数属性 )函数可以改为“注册”该插件的一些函数(进入主程序的某些全局数据结构;这就是Ocaml动态链接的工作原理); 所以从来没有调用dlsym甚至能够使用你的插件的function甚至是有道理的。

对于插件,其构造函数在dlopen时调用(在dlopen返回之前!),并且在dlclose时调用其析构函数(在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); }; 

(当然你可以使用预处理器宏来缩短这些重复的代码; X-macro技巧很方便。)

在调用dlsym数百次时不要害羞。 然而, 定义记录关于插件的适当约定很重要(例如,解释每个插件应该定义func_afunc_b以及它们何时被主程序调用(使用p_func_a等…那里)。如果您的约定需要数百个不同的名字,这是一个难闻的气味。

将插件函数聚合到数据结构中

所以假设你的库定义了func_afunc_bfunc_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 */ }; 

然后你只需要一个符号: globalarray ; 我不知道你是否需要或想要那个。 当然,您可以使用更多精美的数据结构(例如模仿vtable或操作表)。


在插件中使用构造函数

使用构造函数方法,并假设您的主程序提供了一些执行适当操作的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或可见性受限。 然后我们不需要从主程序(应该提供register_plugin_function函数)加载插件来调用dlsym


引用

更仔细地阅读动态加载和插件以及链接器 wikipages。 阅读Levine的“ 连接器和装载器”一书。 阅读elf(5) , proc(5) , ld-linux(8) , dlopen(3) , dlsym(3) , dladdr(3) 。 使用objdump(1) , nm(1) , readelf(1)进行游戏 。

当然,请阅读Drepper的“ 如何编写共享库”一文。

顺便说一下,你可以多次调用dlopen然后调用dlsym 。 我的manydl.c程序生成“随机”C代码,将其编译为插件,然后dlopen -ing和dlsym -ing,并重复。 它表明(有耐心)你可以在同一个进程中拥有数百万个插件,你可以多次调用dlsym

您可以在dlopen -ed库中为所有符号自动生成trampoline函数。 Trampolines将被视为应用程序中的正常function,但会在内部重定向到库中的实际代码。 这是一个简单的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中的应用程序代码仅使用本地函数(包装库函数),并且根本不必弄乱函数指针(除了在启动时初始化重定向表,无论如何应该是自动生成的代码)。

编辑:我已经创建了一个独立的工具Implib.so来自动创建存根库,如上例所示。 事实certificate这或多或少等同于众所周知的Windows DLL导入库。