如何直接从内存中编译和执行?

可以编译一个C ++(或类似的)程序,而不生成可执行文件,但写它并直接从内存执行它?

例如对于GCCclang ,具有类似效果的东西:

 c++ hello.cpp -o hello.x && ./hello.x $@ && rm -f hello.x 

在命令行中。

但是没有将可执行文件写入磁盘的负担立即加载/再次运行。

(如果可能,程序可能不使用磁盘空间。)

可能? 不是你想要的样子。 这个任务有两个部分:

1)如何将二进制文件读入内存

当我们在Linux中指定/dev/stdout作为输出文件时,我们可以通过pipe道传入我们的程序x0 ,从stdin读取一个可执行文件并执行它:

  gcc -pipe YourFiles1.cpp YourFile2.cpp -o/dev/stdout -Wall | ./x0 

x0我们可以直接从stdin读取,直到到达文件末尾:

 int main(int argc, const char ** argv) { const int stdin = 0; size_t ntotal = 0; char * buf = 0; while(true) { /* increasing buffer size dynamically since we do not know how many bytes to read */ buf = (char*)realloc(buf, ntotal+4096*sizeof(char)); int nread = read(stdin, buf+ntotal, 4096); if (nread<0) break; ntotal += nread; } memexec(buf, ntotal, argv); } 

x0也可以直接执行编译器并读取输出。 这个问题在这里得到了解答: 将exec输出redirect到缓冲区或文件

警告:我只是想出了一些奇怪的原因,当我使用pipe |时,这是行不通的 但是当我使用x0 < foo

注意:如果你愿意修改你的编译器,或者像LLVM,clang和其他框架一样,可以直接生成可执行代码。 但是对于本讨论的其余部分,我假设你想使用现有的编译器。

注意:通过临时文件执行

其他程序(如UPX)通过执行一个临时文件来实现类似的行为,这比下面概述的方法更容易和更便于携带。 在/tmp映射到RAM磁盘(例如典型服务器)的系统上,临时文件无论如何都是基于内存的。

 #include<cstring> // size_t #include <fcntl.h> #include <stdio.h> // perror #include <stdlib.h> // mkostemp #include <sys/stat.h> // O_WRONLY #include <unistd.h> // read int memexec(void * exe, size_t exe_size, const char * argv) { /* random temporary file name in /tmp */ char name[15] = "/tmp/fooXXXXXX"; /* creates temporary file, returns writeable file descriptor */ int fd_wr = mkostemp(name, O_WRONLY); /* makes file executable and readonly */ chmod(name, S_IRUSR | S_IXUSR); /* creates read-only file descriptor before deleting the file */ int fd_ro = open(name, O_RDONLY); /* removes file from file system, kernel buffers content in memory until all fd closed */ unlink(name); /* writes executable to file */ write(fd_wr, exe, exe_size); /* fexecve will not work as long as there in a open writeable file descriptor */ close(fd_wr); char *const newenviron[] = { NULL }; /* -fpermissive */ fexecve(fd_ro, argv, newenviron); perror("failed"); } 

警告:error handling是为了清晰的缘故。 包括为了简洁起见。

注意:通过将main()memexec()函数合并到一个函数中,并使用splice(2)直接在stdinfd_wr之间进行复制,程序可以得到显着优化。

2)直接从内存执行

一个不会简单地从内存中加载和执行一个ELF二进制文件。 一些准备工作,主要涉及dynamic链接,必须发生。 有很多材料解释ELF链接过程的各个步骤,研究它使我相信在理论上是可能的。 看到例如这个SO关系密切的问题,但似乎没有一个工作的解决scheme。

更新 UserModeExec似乎非常接近。

编写一个可行的实现将是非常耗时的,肯定会提出一些有趣的问题。 我喜欢相信这是通过devise:对于大多数应用程序,强烈不希望(偶然)执行其input数据,因为它允许代码注入 。

ELF执行时会发生什么? 通常情况下,内核收到一个文件名,然后创build一个进程,将可执行文件的不同部分加载并映射到内存中,执行很多完整性检查并将其标记为可执行文件,然后将控制权和文件名传递回运行时链接程序ld-linux.so (libc的一部分)。 负责重定位function,处理额外的库,设置全局对象并跳转到可执行文件入口点。 AIU这个繁重的工作是由dl_main()完成的(在libc / elf / rtld.c中实现)。

即使fexecve是使用/proc一个文件实现的,这就需要一个文件名来引导我们重新实现链接过程的一部分。

图书馆

  • UserModeExec
  • libelf – 读取,修改,创buildELF文件
  • eresi – 玩elfes
  • OSKit (虽然看起来像一个死了的项目)

相关的问题在SO

  • Linux用户空间ELF加载器
  • ELFdynamic加载器符号查找顺序
  • 加载时ELF重定位
  • 全局variables如何被elf加载器初始化

所以看来有可能,你决定是否也是可行的。

是的,虽然这样做需要考虑到编译器的重要部分。 LLVM家伙已经这样做了,首先是一个分开的JIT,然后是MC子项目。 我不认为有这样一个现成的工具。 但是原则上,这只是一个连接clang和llvm,把源码传给clang,把它创build的IR传给MCJIT的问题。 也许一个演示这样做(我隐约记得一个基本的C解释器,这样的工作,但我认为它是基于传统的JIT)。

编辑:find我回忆的演示 。 此外,还有一些事情,这似乎基本上是我所描述的,但更好。

Linux可以使用tempfs在RAM中创build虚拟文件系统。 例如,我有我的文件系统表中设置我的tmp目录,如下所示:

 tmpfs /tmp tmpfs nodev,nosuid 0 0 

使用这个,我放入/tmp任何文件都存储在我的RAM中。

Windows似乎没有任何“官方”的做法,但有许多第三方的select 。

如果没有这个“RAM磁盘”的概念,你可能不得不大量修改一个编译器和链接器来完全在内存中运行。

如果您不特别与C ++绑定,您也可以考虑其他基于JIT的解决scheme:

  • 在Common Lisp中, SBCL能够即时生成机器码
  • 你可以使用TinyCC及其libtcc.a ,它可以很快从内存中的C代码中发出很差的(即未优化的)机器代码。
  • 也考虑任何JITing库,例如libjit ,GNU Lightning , LLVM , GCCJIT , asmjit
  • 当然,在一些tmpfs上发射C ++代码并编译它…

但是如果你想要好的机器码,你需要优化它,而且速度不是很快(所以写入文件系统的时间可以忽略不计)。

如果你被绑定到C ++生成的代码,你需要一个好的C ++优化编译器(例如g++clang++ )。 他们需要花费大量的时间来编译C ++代码来优化二进制文件,所以你应该生成一些文件foo.cc (可能在一个RAM文件系统中,就像一些tmpfs文件一样,但是这样会带来很小的收益,因为大部分时间都花在g++clang++优化通过,而不是从磁盘读取),然后编译foo.ccfoo.so (使用也许make ,或至less分叉g++ -Wall -shared -O2 foo.cc -o foo.so ,也许与其他库)。 最后有你的主程序生成foo.so dlopen 。 FWIW, MELT正在那样做。

或者,生成一个自包含的源程序foobar.cc ,将其编译为可执行文件foobarbin例如用g++ -O2 foobar.cc -o foobarbin并用execve执行foobarbin可执行文件

在生成C ++代码时,您可能希望避免生成微小的C ++源文件(例如,只有十几行;如果可能,至less生成几百行的C ++文件)。 例如,如果可能的话,尝试将几个生成的C ++函数放在同一个生成的C ++文件中(但避免生成非常大的C ++函数,例如,单个函数中的10KLOC;它们需要花费大量时间由GCC编译)。 如果相关的话,你可以考虑在生成的C ++文件中只包含一个#include ,并且预编译通常包含的头文件。

人们可以很容易地修改编译器本身。 这听起来很难,但考虑到这一点,它显而易见。 因此,修改编译器源直接暴露一个库,并使其成为一个共享库不应该花费太多的负担(取决于实际的实施)。

只需用存储器映射文件的解决schemereplace每个文件访问。

这是我将要在后台透明地编译操作代码并执行Java内部的代码。

但是考虑一下你的原始问题,你就会想要加快编译速度和编辑和运行周期。 首先得到一个固态硬盘,你几乎得到了内存的速度(使用PCI版本),并可以说我们正在谈论的C。 C执行这个链接步骤会导致非常复杂的操作,这比读取和写入磁盘要花费更多的时间。 所以,把所有的东西放在SSD上,并与滞后。