Подтвердить что ты не робот

Как скомпилировать и выполнить из памяти напрямую?

Можно ли скомпилировать программу С++ (или подобное) без генерации исполняемого файла, но написать его и выполнить его непосредственно из памяти?

Например, с GCC и clang, что имеет аналогичный эффект:

c++ hello.cpp -o hello.x && ./hello.x [email protected] && rm -f hello.x

в командной строке.

Но без бремени написания исполняемого файла на диск, чтобы сразу загрузить/запустить его.

(Если возможно, процедура может не использовать дисковое пространство.)

4b9b3361

Ответ 1

Возможные? Не так, как вам кажется. Задача состоит из двух частей:

1) Как получить двоичный код в памяти

Когда мы укажем /dev/stdout в качестве выходного файла в Linux, мы можем подключиться к нашей программе 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 в буфер или файл

Предостережение: я просто понял, что по какой-то странной причине это не работает, когда я использую pipe |, но работает, когда я использую x0 < foo.

Примечание. Если вы хотите изменить свой компилятор, или вы делаете JIT, как 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");
}

Предостережение: обработка ошибок не учитывается. Включает для краткости.

Примечание: объединив шаги main() и memexec() в одну функцию и используя splice(2) для копирования непосредственно между stdin и fd_wr, программа может быть значительно оптимизирована.

2) Выполнение непосредственно из памяти

Один из них не просто загружает и выполняет двоичный файл ELF из памяти. Некоторая подготовка, в основном связанная с динамической связью, должна произойти. Существует много материала, объясняющего различные этапы процесса связывания ELF и изучения его, заставляя меня считать, что это теоретически возможно. См. Например, этот близкий вопрос на SO, однако, похоже, не существует рабочего решения.

Обновить UserModeExec, похоже, очень близко.

Написание рабочей реализации потребует очень много времени и, несомненно, вызовет некоторые интересные вопросы. Мне нравится думать, что это по дизайну: для большинства приложений крайне нежелательно (случайно) выполнять свои входные данные, потому что позволяет впрыск кода.

Что происходит, когда выполняется ELF? Обычно ядро ​​получает имя файла, а затем создает процесс, загружает и сопоставляет различные разделы исполняемого файла в памяти, выполняет множество проверок здравомыслия и маркирует его как исполняемый файл перед передачей управления, а имя файла возвращается к компоновщику времени выполнения ld-linux.so (часть libc). Он заботится о перемещении функций, обработке дополнительных библиотек, настройке глобальных объектов и переходе на точку входа исполняемых файлов. AIU этот тяжелый подъем выполняется с помощью dl_main() (реализуется в libc/elf/rtld.c).

Даже fexecve реализуется с использованием файла в /proc, и именно эта потребность в имени файла приводит нас к переопределению частей этого процесса связывания.

Библиотека

  • UserModeExec
  • libelf - чтение, изменение, создание файлов ELF
  • eresi - играть с эльфами
  • OSKit (похоже, как мертвый проект)

Чтение

Вопросы, относящиеся к SO

Итак, кажется возможным, вы сами решаете, насколько это практично.

Ответ 2

Да, хотя для этого необходимо правильно спроектировать значительную часть компилятора с учетом этого. Ребята из LLVM сделали это, сначала с помощью отдельного JIT, а затем с MC подпроектом. Я не думаю, что там готовый инструмент делает это. Но в принципе, это просто вопрос связывания с clang и llvm, передача источника для clang и передача IR, который он создает для MCJIT. Может быть, демонстрация делает это (я смутно вспоминаю базового интерпретатора языка C, который работал так, хотя я думаю, что он был основан на наследии JIT).

Изменить: найдена демонстрация Я вспомнил. Кроме того, там cling, который, кажется, делает в основном то, что я описал, но лучше.

Ответ 3

Linux может создавать виртуальные файловые системы в ОЗУ с помощью tempfs. Например, у меня есть каталог tmp, настроенный в моей таблице файловой системы следующим образом:

tmpfs       /tmp    tmpfs   nodev,nosuid    0   0

Используя это, любые файлы, помещенные в /tmp, хранятся в моей ОЗУ.

Windows, похоже, не имеет никакого "официального" способа сделать это, но имеет множество сторонних опций.

Без этой концепции "RAM-диска" вам, вероятно, придется сильно модифицировать компилятор и компоновщик для полной работы в памяти.

Ответ 4

Если вы не привязаны к С++, вы можете также рассмотреть другие решения на основе JIT:

  • в Common Lisp SBCL способен генерировать машинный код на лету
  • вы можете использовать TinyCC и его libtcc.a, который испускает быстро бедный (то есть неоптимизированный) машинный код из кода C в памяти.
  • рассмотрим также любую библиотеку JITing, например. libjit, GNU Lightning, LLVM, GCCJIT, asmjit
  • конечно, испускающий код С++ на некоторых tmpfs и компиляцию его...

Но если вам нужен хороший машинный код, вам нужно будет его оптимизировать, и это не так быстро (поэтому время для записи в файловую систему ничтожно).

Если вы привязаны к сгенерированному С++ коду, вам нужен хороший компилятор С++ для оптимизации (например, g++ или clang++); они занимают значительное время, чтобы скомпилировать код на С++ для оптимизированного двоичного кода, поэтому вы должны сгенерировать в некоторый файл foo.cc (возможно, в файловой системе RAM, такой как некоторые tmpfs), но это даст небольшой выигрыш, так как большую часть времени тратится внутри g++ или clang++ выполняется оптимизация, а не чтение с диска), затем скомпилируйте foo.cc to foo.so (используя, возможно, make или, по крайней мере, forking 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 исполняемый двоичный

При генерации кода на С++ вы можете избежать генерации крошечных исходных файлов на С++ (например, только дюжины строк, если возможно, сгенерировать файлы С++ с несколькими сотнями строк). Например, попробуйте, если возможно, поместить несколько сгенерированных функций С++ в один и тот же сгенерированный файл С++ (но избегайте наличия очень больших сгенерированных функций С++, например 10KLOC в одной функции, и они собирают много времени для компиляции GCC). Вы могли бы рассмотреть, если уместно, только один единственный #include в том, что сгенерированный файл С++, и предварительно скомпилировать заголовок, который обычно включается.

Ответ 5

Можно легко изменить сам компилятор. Сначала это звучит тяжело, но, думая об этом, оно кажется очевидным. Так что модификация источников компилятора напрямую выставляет библиотеку и делает ее разделяемой библиотекой, она не должна принимать такую ​​значительную часть (в зависимости от реальной реализации).

Просто замените каждый доступ к файлу решением файла с отображением памяти.

Я собираюсь сделать что-то прозрачно в фоновом режиме для op-кодов и выполнить их из Java.

-

Но, думая о вашем первоначальном вопросе, он швы, вы хотите ускорить компиляцию и ваш цикл редактирования и запуска. Прежде всего, получить SSD-диск, вы получите почти скорость памяти (используйте версию PCI), и, скажем, о C, о котором мы говорим. C делает этот шаг связывания, приводящий к очень сложным операциям, которые могут занять больше времени, чем чтение и запись с/на диск. Так что просто положите все на SSD и живете с задержкой.