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

alf*_*lfC 44 c++ linux

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

例如,使用GCCclang,具有类似效果的东西:

c++ hello.cpp -o hello.x && ./hello.x $@ && rm -f hello.x
Run Code Online (Sandbox Code Playgroud)

在命令行中.

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

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

Lot*_*har 43

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

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

当我们/dev/stdout在Linux中指定为输出文件时,我们可以管道进入我们的程序x0,该程序从stdin读取可执行文件并执行它:

  gcc -pipe YourFiles1.cpp YourFile2.cpp -o/dev/stdout -Wall | ./x0
Run Code Online (Sandbox Code Playgroud)

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); 
}
Run Code Online (Sandbox Code Playgroud)

也可以x0直接执行编译器并读取输出.此问题已在此处得到解答:将exec输出重定向到缓冲区或文件

警告:我只是想通了一些奇怪的原因,当我使用管道时这不起作用|但是在我使用时工作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");
}
Run Code Online (Sandbox Code Playgroud)

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

注意:通过结合步骤main()memexec()成一个单一的功能,并使用splice(2)对之间直接复制stdinfd_wr程序可以显著优化.

2)直接从内存中执行

一个不是简单地从内存加载和执行ELF二进制文件.必须进行一些主要与动态链接相关的准备工作.有很多材料解释了ELF连接过程的各个步骤,研究它让我相信理论上可行.例如,参见关于SO的这个密切相关的问题,但似乎没有一个可行的解决方案.

更新 UserModeExec似乎非常接近.

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

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

甚至fexecve是使用文件实现的,/proc这需要一个文件名,这导致我们重新实现这个链接过程的一部分.

图书馆

SO的相关问题

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


小智 22

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

编辑:找到我回忆的演示.此外,还有坚持,这似乎与我所描述的基本相同,但更好.


Mat*_*ine 16

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

tmpfs       /tmp    tmpfs   nodev,nosuid    0   0
Run Code Online (Sandbox Code Playgroud)

使用它,我输入的任何文件/tmp都存储在我的RAM中.

Windows似乎没有任何"官方"方式,但有许多第三方选项.

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


Bas*_*tch 6

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

  • 在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,或至少分叉g++ -Wall -shared -O2 foo.cc -o foo.so,可能还有其他库).最后dlopen生成你的主程序foo.so.FWIW,MELT正是这样做的.

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

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