60

実行可能ファイルを生成せずに、それを書き込んでメモリから直接実行する C++ (または同様の) プログラムをコンパイルすることは可能ですか?

たとえば、 と を使用するGCCclang、次のような効果があります。

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

コマンドラインで。

ただし、実行可能ファイルをディスクに書き込んですぐにロード/再実行するという負担はありません。

(可能であれば、この手順ではディスク領域を使用しないか、少なくとも読み取り専用である可能性がある現在のディレクトリ内の領域を使用しない場合があります)。

4

7 に答える 7

51

Possible? Not the way you seem to wish. The task has two parts:

1) How to get the binary into memory

When we specify /dev/stdout as output file in Linux we can then pipe into our program x0 that reads an executable from stdin and executes it:

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

In x0 we can just read from stdin until reaching the end of the file:

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); 
}

It would also be possible for x0 directly execute the compiler and read the output. This question has been answered here: Redirecting exec output to a buffer or file

Caveat: I just figured out that for some strange reason this does not work when I use pipe | but works when I use the x0 < foo.

Note: If you are willing to modify your compiler or you do JIT like LLVM, clang and other frameworks you could directly generate executable code. However for the rest of this discussion I assume you want to use an existing compiler.

Note: Execution via temporary file

Other programs such as UPX achieve a similar behavior by executing a temporary file, this is easier and more portable than the approach outlined below. On systems where /tmp is mapped to a RAM disk for example typical servers, the temporary file will be memory based anyway.

#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");
}

Caveat: Error handling is left out for clarities sake. Includes for sake of brevity.

Note: By combining step main() and memexec() into a single function and using splice(2) for copying directly between stdin and fd_wr the program could be significantly optimized.

2) Execution directly from memory

One does not simply load and execute an ELF binary from memory. Some preparation, mostly related to dynamic linking, has to happen. There is a lot of material explaining the various steps of the ELF linking process and studying it makes me believe that theoretically possible. See for example this closely related question on SO however there seems not to exist a working solution.

Update UserModeExec seems to come very close.

Writing a working implementation would be very time consuming, and surely raise some interesting questions in its own right. I like to believe this is by design: for most applications it is strongly undesirable to (accidentially) execute its input data because it allows code injection.

What happens exactly when an ELF is executed? Normally the kernel receives a file name and then creates a process, loads and maps the different sections of the executable into memory, performs a lot of sanity checks and marks it as executable before passing control and a file name back to the run-time linker ld-linux.so (part of libc). The takes care of relocating functions, handling additional libraries, setting up global objects and jumping to the executables entry point. AIU this heavy lifting is done by dl_main() (implemented in libc/elf/rtld.c).

Even fexecve is implemented using a file in /proc and it is this need for a file name that leads us to reimplement parts of this linking process.

Libraries

Reading

Related Questions at SO

So it seems possible, you decide whether is also practical.

于 2012-12-04T11:40:44.197 に答える
24

はい。ただし、適切に行うには、これを念頭に置いてコンパイラの重要な部分を設計する必要があります。LLVM 関係者はこれを、最初は別の JIT で行い、その後MCサブプロジェクトで行いました。それを行う既製のツールはないと思います。ただ、原則的にはclangとllvmにリンクして、ソースをclangに渡し、作成したIRをMCJITに渡すだけです。おそらく、デモではこれが行われます (レガシー JIT に基づいていたと思いますが、このように機能する基本的な C インタープリターを漠然と思い出します)。

編集:思い出したデモを見つけました。また、基本的に私が説明したことを行うように見えるclingがありますが、より優れています。

于 2012-12-03T19:48:52.747 に答える
21

Linux は、 tempfsを使用して RAM に仮想ファイル システムを作成できます。たとえば、次のtmpようにファイル システム テーブルにディレクトリを設定しています。

tmpfs       /tmp    tmpfs   nodev,nosuid    0   0

これを使用すると、入力したファイルはすべて/tmpRAM に保存されます。

Windows にはこれを行うための「公式な」方法はないようですが、多くのサードパーティ オプションがあります

この「RAM ディスク」の概念がなければ、メモリ内で完全に動作するようにコンパイラとリンカを大幅に変更する必要があります。

于 2012-12-03T19:44:36.780 に答える
8

特にC++に縛られていない場合は、他のJITベースのソリューションも検討してください。

  • CommonLispではSBCLはその場でマシンコードを生成することができます
  • TinyCCとlibtcc.a、メモリ内のCコードからすぐに貧弱な(つまり最適化されていない)マシンコードを出力するTinyCCを使用できます。
  • libjit、GNU LightningLLVMGCCJITasmjitなどのJITingライブラリも検討してください。
  • もちろん、いくつかのtmpfsでC ++コードを発行し、それをコンパイルします...

ただし、優れたマシンコードが必要な場合は、最適化する必要があります。これは高速ではありません(したがって、ファイルシステムに書き込む時間はごくわずかです)。

C ++で生成されたコードに縛られている場合は、優れたC ++最適化コンパイラ(g++またはclang++)が必要です。C ++コードを最適化されたバイナリにコンパイルするにはかなりの時間がかかるため、いくつかのファイルを生成する必要がありますfoo.cc(おそらく、いくつかのようなRAMファイルシステムでは、ほとんどの時間が内部または最適化パスにtmpfs費やされるため、わずかな利益が得られます。ディスクから読み取る)、それをコンパイルします(おそらく、または少なくともフォークを使用して、おそらく追加のライブラリを使用して)。最後に、を生成したメインプログラムを用意します。FWIW、MELTはまさにそれを実行しており、Linuxワークステーションではmanydl.cプログラムは、プロセスがその時点で生成できることを示していますg++clang++foo.ccfoo.somakeg++ -Wall -shared -O2 foo.cc -o foo.sodlopenfoo.sodlopen(3)数十万の一時プラグイン。それぞれが一時Cファイルを生成してコンパイルすることで取得されます。C ++の場合は、 C ++ dlopenminiHOWTOをお読みください。

または、自己完結型のソースプログラムを生成し、foobar.ccそれを実行可能ファイルにコンパイルします。foobarbinたとえば、その実行可能バイナリg++ -O2 foobar.cc -o foobarbinを使用して実行します。execvefoobarbin

C ++コードを生成するときは、小さなC ++ソースファイルの生成を避けたい場合があります(たとえば、数十行のみ。可能であれば、少なくとも数百行のC++ファイルを生成します。template既存のC++コンテナーを広範囲に使用して多くの拡張が行われない限り、それらを組み合わせた小さなC++関数を生成することは理にかなっています)。たとえば、可能であれば、生成された複数のC++関数を同じ生成されたC++ファイルに配置してみてください(ただし、単一の関数に10KLOCなどの非常に大きな生成されたC ++関数を含めることは避けてください。GCCによるコンパイルには多くの時間がかかります)。必要に応じて、生成されたC ++ファイルにシングルを1つだけ#include含め、一般的に含まれているヘッダーをプリコンパイルすることを検討できます。

JacquesPitratの著書ArtificialBeings、意識のあるマシンの良心(ISBN 9781848211018)は、実行時にコードを生成することが有用である理由を詳細に説明しています(彼のCAIAシステムのようなシンボリック人工知能システムで)。RefPerSysプロジェクトは、そのアイデアに従い、実行時にいくつかのC ++コード(そして、できれば、ますます多くのコード)を生成しようとしています。部分評価は関連する概念です。

ソフトウェアは、コンパイルにGCCよりもC++コードの生成に多くのCPU時間を費やす可能性があります。

于 2012-12-04T06:10:50.940 に答える
1

コンパイラ自体を簡単に変更できます。最初は難しそうに聞こえますが、考えてみれば当たり前のことです。そのため、コンパイラ ソースを直接変更してライブラリを公開し、それを共有ライブラリにするのにそれほど余裕はありません (実際の実装によって異なります)。

すべてのファイル アクセスをメモリ マップ ファイルのソリューションに置き換えるだけです。

これは、バックグラウンドで透過的に何かを op コードにコンパイルし、Java 内からそれらを実行しようとしていることです。

-

しかし、元の質問について考えると、コンパイルと編集と実行のサイクルをスピードアップしたいと思われます。まず、ほとんどのメモリ速度を得る SSD ディスクを入手し (PCI バージョンを使用)、その C について話しているとしましょう。C はこのリンク手順を実行するため、非常に複雑な操作になり、ディスクの読み書きよりも時間がかかる可能性があります。したがって、すべてをSSDに置き、ラグに耐えてください。

于 2015-04-07T01:23:30.030 に答える