是否可以编译C ++(或类似)程序而不生成可执行文件但是将其写入并直接从内存执行?
例如GCC
和clang
,效果类似:
c++ hello.cpp -o hello.x && ./hello.x $@ && rm -f hello.x
在命令行中。
但没有将可执行文件写入磁盘以立即加载/再次运行它的负担。
(如果可能,该过程可能不使用磁盘空间。)
答案 0 :(得分:41)
可能?不是你想要的方式。该任务分为两部分:
当我们在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}之间进行复制程序可以大大优化。
不只是从内存中加载并执行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)。
答案 2 :(得分:16)
Linux可以使用tempfs在RAM中创建虚拟文件系统。例如,我在我的文件系统表中设置了tmp
目录,如下所示:
tmpfs /tmp tmpfs nodev,nosuid 0 0
使用此功能,我放入/tmp
的所有文件都存储在我的RAM中。
Windows似乎没有任何“官方”方式,但有很多third-party options。
如果没有这个“RAM磁盘”概念,您可能不得不大量修改编译器和链接器以在内存中完全运行。
答案 3 :(得分:6)
如果您没有专门与C ++绑定,您也可以考虑其他基于JIT的解决方案:
libtcc.a
从内存中的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上,就可以忍受延迟。