如何直接从内存中编译和执行?
可以编译一个C ++(或类似的)程序,而不生成可执行文件,但写它并直接从内存执行它?
例如对于GCC
和clang
,具有类似效果的东西:
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)
直接在stdin
和fd_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 (虽然看起来像一个死了的项目)
读
- http://www.linuxjournal.com/article/1060?page=0,0 – 介绍
- http://wiki.osdev.org/ELF – 很好的概述
- http://s.eresi-project.org/inc/articles/elf-rtld.txt – 更详细的Linux特定解释
- http://www.codeproject.com/Articles/33340/Code-Injection-into-Running-Linux-Application – 如何进入hello world
- http://www.acsu.buffalo.edu/~charngda/elf.html – ELF结构的很好的参考
- 装载机和连接器通过John Levine – 解释链接
相关的问题在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.cc
到foo.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上,并与滞后。