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

时间:2012-12-03 19:41:53

标签: c++ linux

是否可以编译C ++(或类似)程序而不生成可执行文件但是将其写入并直接从内存执行?

例如GCCclang,效果类似:

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

在命令行中。

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

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

5 个答案:

答案 0 :(得分:41)

可能?不是你想要的方式。该任务分为两部分:

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

当我们在Linux中指定/dev/stdout作为输出文件时,我们可以输入我们的程序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也可以直接执行编译器并读取输出。这个问题已在这里得到解答:Redirecting exec output to a buffer or file

警告:我只是想通了一些奇怪的原因,当我使用管道|时这不起作用,但是当我使用x0 < foo时它会起作用。

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

注意:通过临时文件执行

其他程序(如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");
}

警告:为了清楚起见,遗漏了错误处理。包括为了简洁起见。

注意:将步骤main()memexec()合并到一个函数中,并使用splice(2)直接在stdin和{{1}之间进行复制程序可以大大优化。

2)直接从内存执行

不只是从内存中加载并执行ELF二进制文件。必须进行一些主要与动态链接相关的准备工作。有很多材料解释了ELF链接过程的各个步骤,研究它让我相信理论上可能。例如,请参阅这个密切相关的question on SO但是似乎没有一个可行的解决方案。

更新 UserModeExec似乎非常接近。

编写一个有效的实现将非常耗时,并且肯定会提出一些有趣的问题。我喜欢相信这是设计的:对于大多数应用程序来说,非常不希望(意外地)执行其输入数据,因为它允许code injection

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

即使dl_main()是使用fexecve中的文件实现的,也需要一个文件名来引导我们重新实现此链接过程的部分内容。

阅读

SO的相关问题

所以看起来可能,你决定是否也是实用的。

答案 1 :(得分:22)

是的,虽然正确地做到这一点需要设计编译器的重要部分,但要记住这一点。 LLVM的人已经做到了这一点,首先是一个单独的JIT,后来是MC子项目。我不认为有这样的现成工具。但原则上,它只是链接到clang和llvm,将源传递给clang,并将它创建的IR传递给MCJIT。也许一个演示就是这样做的(我模糊地回忆起一个像这样工作的基本C语言解释器,尽管我认为它基于传统的JIT)。

编辑:找到我记得的demo。此外,还有cling,这似乎基本上与我所描述的相同,但更好。

答案 2 :(得分:16)

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

tmpfs       /tmp    tmpfs   nodev,nosuid    0   0

使用此功能,我放入/tmp的所有文件都存储在我的RAM中。

Windows似乎没有任何“官方”方式,但有很多third-party options

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

答案 3 :(得分:6)

如果您没有专门与C ++绑定,您也可以考虑其他基于JIT的解决方案:

    Common Lisp SBCL中的
  • 能够动态生成机器代码
  • 您可以使用TinyCC及其libtcc.a从内存中的C代码中快速发出差(即未优化)的机器代码。
  • 也考虑任何JITing库,例如libjit,GNU LightningLLVMGCCJITasmjit
  • 当然在某些tmpfs上发布C ++代码并编译它......

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

如果您与C ++生成的代码绑定,则需要一个好的C ++优化编译器(例如g++clang++);他们需要花费大量时间将C ++代码编译为优化的二进制文件,因此您应该生成一些文件foo.cc(可能在某个tmpfs的RAM文件系统中,但这会产生微小的收益,因为大多数时间花在g++clang++优化过程中,而不是从磁盘读取,然后将foo.cc编译为foo.so(可能使用make,或者最少分叉g++ -Wall -shared -O2 foo.cc -o foo.so,也许还有额外的库)。最后让您的主程序dlopen生成foo.so。 FWIW,MELT正是这样做的。

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

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

答案 4 :(得分:1)

可以轻松修改编译器本身。这听起来很难,但考虑到它,接缝很明显。因此,修改编译器源直接公开库并使其成为共享库不应该花费太多(取决于实际的实现)。

使用内存映射文件的解决方案替换每个文件访问。

我正在做的事情是在后台透明地编译某些代码并在Java中执行这些代码。

-

但考虑到你的原始问题,你需要加速编译和编辑和运行周期。首先得到一个SSD-Disk,你几乎可以获得内存速度(使用PCI版本),让我们说一下我们正在讨论的C语言。 C执行此链接步骤导致非常复杂的操作,这可能比从磁盘读取和写入花费更多时间。因此,只需将所有内容放在SSD上,就可以忍受延迟。