【问题标题】:Is there an elegant way to avoid dlsym when using dlopen in C?在 C 中使用 dlopen 时,是否有一种避免 dlsym 的优雅方法?
【发布时间】:2018-02-05 15:41:09
【问题描述】:

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

一个简单的解决方案是定义指向lib.so中包含的所有符号的函数指针,使用dlopen加载库,最后使用dlsym获取所有符号的地址。但是,鉴于函数的数量,实现该方案的代码非常繁琐。

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

【问题讨论】:

  • 如果您需要从共享库中调用 700 个函数,为什么不在共享库中为您调用这些函数呢?这样的列表在库内部比在外部宏中更容易维护。
  • 实际上,应用程序是一个分析器,它拦截另一个库lib_ext.so 的调用,并将其替换为lib.so 中定义的分析版本。 lib_ext.so 的符号使用 LD_PRELOAD 变量进行重载。由于我事先不知道lib_ext.so 的哪些函数会在运行时被调用,所以我需要所有符号的地址。
  • @n.m.你是对的dlsym。我更改了问题的文字。
  • 目前还不清楚为什么需要所有地址。
  • 编辑您的问题以改进它并解释您为什么要问以及如何使用dlopen 以及您的程序及其插件在做什么。添加一些关于动机和背景的句子。

标签: c shared-libraries function-pointers dlopen


【解决方案1】:

您可以为dlopen-ed 库中的所有符号自动生成蹦床函数。 Trampolines 将被视为应用程序中的正常功能,但会在内部重定向到库中的真实代码。这是一个简单的 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

请注意,main.c 中的应用程序代码仅使用本地函数(包装库函数)并且根本不必弄乱函数指针(除了在启动时初始化重定向表,无论如何都应该是自动生成的代码)。

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

【讨论】:

  • 我不明白您的解决方案如何与使用 dlopen 打开的库一起使用,或者您确定您的解决方案?
  • @Stargateur 我最初编写的 PoC 并没有使用 shlibs 来说明这个想法,但你的评论促使我重写它,希望我的意图现在更清楚了。请注意,我希望尽快进行小型项目以获得完整的解决方案,我将在此处发布链接。
  • @Stargateur 我已经添加了我承诺的通用工具的链接。
【解决方案2】:

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

dlopendlsym 的角色

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

dlsym 提供的功能是使用该ELF 插件中的一些动态symbol table 从其名称中获取该共享库中某物的地址。如果您不需要,则无需致电dlsym

也许您可以在共享库中简单地拥有一个包含所有相关函数的大型数组(可作为共享库中的全局变量使用)。然后,您只需调用一次dlsym 即可获取该全局变量的名称。

顺便说一句,您的插件的 构造函数constructorfunction 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_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 */
};

然后你只需要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 loadingplug-inslinker 维基页面。阅读莱文的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

【讨论】:

  • 在 POSIX 上它明确不是 UB,dlsym 是 POSIX 特定的。
  • “如果你不需要,你就不需要调用 dlsym”——但是他将如何访问函数呢?请注意,在代码中显式引用函数名是不可能的,因为它会导致链接器错误(符号仅在运行时可用)。
  • 正如我所说,插件可以在一些构造函数中注册一些函数指针
  • “正如我所说,插件可以在某些构造函数中注册一些函数指针” - OP 不是要求不使用函数指针的解决方案吗?
  • 是的。但这仍然是一个有趣的问题。
猜你喜欢
  • 2023-03-15
  • 1970-01-01
  • 1970-01-01
  • 2010-11-07
  • 2011-02-22
  • 1970-01-01
  • 2011-11-11
  • 2010-11-01
  • 1970-01-01
相关资源
最近更新 更多