从内存中的dlopen?

38

我正在寻找一种直接从内存中加载生成的目标代码的方法。

我知道如果将其写入文件,可以调用dlopen动态加载其符号并链接它们。 但是,考虑到它最初在内存中,被写入到磁盘上,然后再由dlopen重新加载到内存中,这似乎有点绕。 我想知道是否有一种动态链接存在于内存中的目标代码的方法。从我所了解的情况来看,可能有几种不同的方法可以做到这一点:

  1. 欺骗dlopen,使其认为您的内存位置是一个文件,尽管它从未离开内存。

  2. 搜索其他可执行我正在寻找的功能的系统调用(我认为这不可能存在)。

  3. 查找一些能够直接在内存中链接代码的动态链接库。显然,这很难通过谷歌搜索获得,因为“动态链接库”会返回关于如何动态链接库的信息,而不是执行动态链接任务的库。

  4. 从链接器中抽象出一些API并创建一个新的库。 (对我来说,这显然是最不理想的选择)。

那么这其中哪些是可能的? 可行的? 你能指出我猜想存在的任何东西吗?还有其他我甚至没有想到的方法吗?

9个回答

14

我需要解决这个问题,因为我有一个可脚本化的系统,它没有文件系统(使用来自数据库的blob),并且需要加载二进制插件来支持一些脚本。 这是我想出的解决方案,在FreeBSD上可以工作,但可能不具有可移植性。

void *dlblob(const void *blob, size_t len) {
    /* Create shared-memory file descriptor */
    int fd = shm_open(SHM_ANON, O_RDWR, 0);
    ftruncate(fd, len);
    /* MemMap file descriptor, and load data */
    void *mem = mmap(NULL, len, PROT_WRITE, MAP_SHARED, fd, 0);
    memcpy(mem, blob, len);
    munmap(mem, len);
    /* Open Dynamic Library from SHM file descriptor */
    void *so = fdlopen(fd,RTLD_LAZY);
    close(fd);
    return so;
}

显然,该代码缺乏任何形式的错误检查等,但这是核心功能。

预计完成时间:最初我认为fdlopen是POSIX标准,但这似乎是FreeBSD特有的。


2
@yugr,您的建议恰好是提问者已经排除的平凡情况。 - Parakleta
不完全正确,使用/run/shm时,文件永远不会被写入磁盘。 - yugr
2
@yugr /run/shm 不是 POSIX,它是 Linux 特有的。如果没有它,该函数将退回到仅写入 /tmp。无论文件是否成功写入磁盘(在某些系统上,/tmp 可能是一个 ramdisk),您仍然必须与文件系统交互,具备创建它的权限,控制其他人是否可以访问它,在完成时确保正确取消链接它(或崩溃)。为什么不发布一个答案,让人们评论和投票呢? - Parakleta
我认为这个小改动并不值得单独回答。虽然是Linux的特性,但OP没有明确提到他需要符合POSIX标准的解决方案。至于文件系统——同样是个好观点,但我认为OP更关心实际的磁盘访问(“写入磁盘,然后通过dlopen重新加载到内存中”)。 - yugr
@yugr 我提到它不是POSIX和Linux-ism的原因是,您已经评论了一个使用BSD-ism解决方案的答案,并提出了一个使用Linux-ism的建议,因此除了/tmp回退之外,这些解决方案之间没有任何交集。这两个解决方案在各自的平台上都有优点,但我不明白它们之间的关系。基本上,在BSD上使用fdlopen而无需访问文件系统,然后在Linux上使用/run/shm或在POSIX上使用/tmp,都需要访问文件系统。 - Parakleta
显示剩余7条评论

12

我不知道为什么你要考虑使用 dlopen,因为这会需要大量非便携式的代码来生成适合加载的正确对象格式(例如 ELF)在磁盘上。如果你已经知道如何为你的架构生成机器代码,那么只需用 mmap 将内存映射为可读、可写和可执行的内存,然后将其地址分配给一个函数指针并调用它。非常简单。


4
如果有多于几个人在开发,这种方式似乎不是很好。此外,您注入的代码是否需要解析自己的函数指针并且是PIC等呢?编译.so文件,然后能够使用dlopen会更好。 - mrduclaw
我猜这取决于你正在生成什么类型的代码。我在考虑为虚拟机/JIT代码和模拟器的动态编译,其中不会有任意调用和访问调用程序中的数据。 - R.. GitHub STOP HELPING ICE
1
这确实是一种处理相对简单的自包含代码的好方法(另外:说到底,你真的有多少次想要动态生成的代码能够进行任意调用呢?) - Stephen Canon
R.. 我当然考虑过这个问题,但这也需要一个链接器,因为我正在使用的编译器的输出是目标代码,而不是机器代码。这就是为什么我在上面提出了建议3和4的原因:如果我这样做,我需要找到一种跨平台的库来动态链接内存。但如果这不存在,那么这根本不是一个解决方案。 - Jeremy Salwen
@Stephen Canon,实际上在某些业务领域,这是一个非常常见的要求,在Windows上也经常发生。然而,这是一种你只需要编写一次并不断重复使用的东西。 - mrduclaw
@JeremySalwen 格拉斯哥 Haskell 编译器内置了一个链接器,用于您所描述的确切目的。我不确定它有多可重用,但我知道它是用 C 而不是 Haskell 编写的,并且可以在多个平台上工作。 - Demi

9

除了将文件写出并使用dlopen()再次加载之外,没有标准的方法。

您可能会在当前特定的平台上找到一些替代方法。这将取决于您是否认为它比使用“标准和(相对)可移植”的方法更好。

由于首先生成对象代码是非常特定于平台的,因此其他特定于平台的技术可能对您无关紧要。但这是一个判断调用 - 在任何情况下都取决于是否存在相对不太可能的非标准技术。


1
管道也算作文件描述符吗?那你为什么不把它输入到dlopen()中呢? - imacake
1
@imacake - 这是一个文件描述符,但它不是可以寻址或mmap的描述符。 - Flexo
除了将文件写出并重新加载之外,没有标准的方法来完成它。可以参考R..的回答,将其更正为“您可以将文件写出并加载”。 - Simon
2
@Simon:如果要加载的代码不需要调用任何其他函数(完全自包含),则可以直接使用mmap(),这样做可能会起作用。 如果要加载的代码调用其他函数,则必须通过某种方法解析这些符号的地址。 这通常是由dlopen()为您完成的。 如果短路了dlopen(),那么责任就在您身上,作为代码创建者,必须确保已考虑了ASLR等因素,并且在代码中将正确的函数地址放置在正确的位置。 - Jonathan Leffler
谢谢提供ASLR信息,我之前不知道。但是如果你自己加载库,你应该仍然能够链接它们吧? - Simon
2
需要注意的一个小问题是:在Linux上,我发现如果我想让一个程序写出一个.so文件,然后dlopen它,并从中dlsym,然后再写出另一个.so文件,再dlopen它并从中dlsym,那么这两个.so文件名必须不同。 - Mike Spear

7
我们在Google实现了一种方法来做到这一点。不幸的是,上游glibc未能理解这个需求,因此它从未被接受。带有补丁的功能请求已经停滞不前。它被称为dlopen_from_offset
在glibc google/grte* 分支中,dlopen_with_offset glibc代码可用。但是没有人应该享受修改自己的glibc。

1

你不需要将生成的代码加载到内存中,因为它已经在内存中了!

但是,你可以以非便携的方式在内存中生成机器码(前提是它在使用mmapPROT_EXEC标志映射的内存段中)。

(在这种情况下,不需要“链接”或重定位步骤,因为你生成的机器码具有明确的绝对或相对地址,特别是用于调用外部函数)

有一些库可以做到这一点: 在 GNU/Linux 上的 x86 或 x86-64 下,我知道有 GNU Lightning (它可以快速生成运行缓慢的机器代码), DotGNU LibJIT (可以生成中等质量的代码), 以及 LLVM & GCCJIT (能够在内存中生成相当优化的代码,但需要时间来发出)。 LuaJit 也有类似的功能。自2015年起,GCC 5就有一个 gccjit 库。

当然,你仍然可以在文件中生成C代码,fork一个编译器将其编译成共享对象,并dlopen该共享对象文件。我在GCC MELT中就是这样做的,它是一种用于扩展GCC的特定领域语言。实际上,它的效果非常好。
补充说明:
如果担心生成的C文件的写入性能(实际上不应该担心,因为编译C文件比写入要慢得多),考虑使用一些tmpfs文件系统(可能位于Linux上的/tmp/目录,通常是一个tmpfs文件系统)。

1
这个回答不值得任何投票。它完全误解了提问者的想法。 - Krypton

1

从内存加载solib具有固有的限制。 即,solib的DT_NEEDED依赖项不能引用 到内存缓冲区。这意味着,除其他外, 您无法轻松加载具有deps的solib 从内存缓冲区。恐怕,除非ELF规范 扩展以允许DT_NEEDED引用其他 对象而不是文件名,否则将没有标准 API可用于从内存缓冲区加载solib。

我认为你需要使用posix的shm_open(),然后 mmap共享内存,在那里生成您的solib, 然后通过/dev/shm挂载点使用纯dlopen()。 这也可以处理deps:它们可以 引用常规文件或/dev/shm对象 拥有您生成的solibs。


1
这是如何在Linux上完全使用内存(而不写入/tmp/xxx)并使用带有memfd_create的内存文件描述符进行操作的方法:
user@system $ ./main < example-library.so
add(1, 2) = 3

// example-library.c
int add(int a, int b) { return a + b; }

#include <cstdio>
#include <dlfcn.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <vector>

// Compile and then invoke as:
// $ ./main < my-shared-lib.so
int main() {
  // Read the shared library contents from stdin
  std::vector<char> library_contents;
  char buffer[1024];
  ssize_t bytes_read;
  while ((bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0) {
    library_contents.insert(library_contents.end(), buffer,
                            buffer + bytes_read);
  }

  // Create a memory file descriptor using memfd_create
  int fd = memfd_create("shared_library", 0);
  if (fd == -1) {
    perror("memfd_create failed");
    return 1;
  }

  // Write the shared library contents to the file descriptor
  if (write(fd, library_contents.data(), library_contents.size()) !=
      static_cast<ssize_t>(library_contents.size())) {
    perror("write failed");
    return 1;
  }

  // Create a path to the file descriptor using /proc/self/fd
  // https://sourceware.org/bugzilla/show_bug.cgi?id=30100#c33
  char path[100]; // > 35 == strlen("/proc/self/fd/") + log10(pow(2, 64)) + 1
  snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);

  // Use dlopen to dynamically load the shared library
  void *handle = dlopen(path, RTLD_NOW);
  if (handle == NULL) {
    fprintf(stderr, "dlopen failed: %s\n", dlerror());
    return 1;
  }

  // Use the shared library...
  // Get a pointer to the function "int add(int, int)"
  int (*add)(int, int) =
      reinterpret_cast<int (*)(int, int)>(dlsym(handle, "add"));

  if (add == NULL) {
    fprintf(stderr, "dlsym failed: %s\n", dlerror());
    return 1;
  }

  // Call the function "int add(int, int)"
  printf("add(1, 2) = %d\n", add(1, 2));

  // Cleanup
  dlclose(handle);
  close(fd);
  return 0;
}

0

需要注意的是,使用shm_open+dlopen从共享内存中加载动态库,如果/dev/shm没有noexec权限,则动态库将无法加载。


0

我找到了解决办法,使用memfd_create创建一个内存文件,然后从dlopen打开。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接