Как динамически генерировать и запускать собственный код? - PullRequest
32 голосов
/ 06 февраля 2011

Я хотел бы написать очень маленький JIT-компилятор для проверки концепции для игрушечного языкового процессора, который я написал (чисто академический), но у меня есть некоторые проблемы на средних высотах дизайна. Концептуально, я знаком с тем, как работает JIT - вы компилируете байт-код в код (машина или сборка?) Для запуска. Однако, на уровне гайки и болты, я не совсем понимаю, как вы на самом деле делаете делая это.

Моя (очень "новенькая") реакция коленного рефлекса, поскольку я не имею ни малейшего понятия, с чего начать, - это попробовать что-то вроде следующего:

  1. mmap () блок памяти, настройка доступа к PROT_EXEC
  2. написать собственный код в блок
  3. хранить текущие регистры (указатель стека и др.) В удобном месте
  4. изменить текущие регистры так, чтобы они указывали на блок собственного кода в отображаемой области
  5. теперь машинный код будет выполняться на машине
  6. восстановить предыдущие регистры

Это даже близко к / правильному алгоритму? Я пробовал просматривать различные проекты, которые, как мне известно, имеют JIT-компиляторы для изучения (например, V8 ), но эти кодовые базы оказываются сложными для использования из-за их размера, и я не знаю, с чего начать смотреть.

Ответы [ 7 ]

28 голосов
/ 06 февраля 2011

Не уверен насчет linux, но это работает на x86 / windows.
Обновление: http://codepad.org/sQoF6kR8

#include <stdio.h>
#include <windows.h>

typedef unsigned char byte;

int arg1;
int arg2;
int res1;

typedef void (*pfunc)(void);

union funcptr {
  pfunc x;
  byte* y;
};

int main( void ) {

  byte* buf = (byte*)VirtualAllocEx( GetCurrentProcess(), 0, 1<<16, MEM_COMMIT, PAGE_EXECUTE_READWRITE );

  if( buf==0 ) return 0;

  byte* p = buf;

  *p++ = 0x50; // push eax
  *p++ = 0x52; // push edx

  *p++ = 0xA1; // mov eax, [arg2]
  (int*&)p[0] = &arg2; p+=sizeof(int*);

  *p++ = 0x92; // xchg edx,eax

  *p++ = 0xA1; // mov eax, [arg1]
  (int*&)p[0] = &arg1; p+=sizeof(int*);

  *p++ = 0xF7; *p++ = 0xEA; // imul edx

  *p++ = 0xA3; // mov [res1],eax
  (int*&)p[0] = &res1; p+=sizeof(int*);

  *p++ = 0x5A; // pop edx
  *p++ = 0x58; // pop eax
  *p++ = 0xC3; // ret

  funcptr func;
  func.y = buf;

  arg1 = 123; arg2 = 321; res1 = 0;

  func.x(); // call generated code

  printf( "arg1=%i arg2=%i arg1*arg2=%i func(arg1,arg2)=%i\n", arg1,arg2,arg1*arg2,res1 );

}
4 голосов
/ 06 февраля 2011

Вы можете взглянуть на libjit , который предоставляет именно ту инфраструктуру, которую вы ищете:

Библиотека libjit реализует функции компиляции точно в срок.В отличие от других JIT, этот разработан, чтобы быть независимым от любого формата или языка байт-кода конкретной виртуальной машины.

http://freshmeat.net/projects/libjit

3 голосов
/ 06 ноября 2013

Как выполнить JIT - введение - новая статья (с сегодняшнего дня!), В которой рассматриваются некоторые из этих проблем, а также описывается общая картина.

3 голосов
/ 06 февраля 2011

Также стоит посмотреть на JIT-компилятор Android Dalvik.Предполагается, что он довольно маленький и худой (не уверен, поможет ли это понять или усложнит ситуацию).Он также нацелен на Linux.

Если ситуация становится более серьезной, то, возможно, хорошим выбором будет и LLVM.

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

2 голосов
/ 14 апреля 2011

Ответ зависит от вашего компилятора и от того, куда вы положили код.См. http://encode.ru/threads/1273-Just-In-Time-Compilation-Improvement-For-ZPAQ?p=24902&posted=1#post24902

При тестировании в 32-битной Vista Visual C ++ выдает ошибку DEP (предотвращение выполнения данных), независимо от того, помещен ли код в стек, кучу или статическую память.G ++, Borland и Mars могут иногда работать.Данные, к которым обращается код JIT, должны быть объявлены как изменчивые.

0 голосов
/ 08 февраля 2011

Вас может заинтересовать почему язык программирования Potion удачливыйЭто небольшой, неполный язык, который включает компиляцию точно в срок.Маленький размер зелья облегчает понимание.Хранилище содержит описание внутренних компонентов языка (содержимое JIT начинается с заголовка " ~ the jit ~ ").

Реализация усложняется тем, что она выполняетсяв контексте Зелье VM .Но пусть это вас не пугает.Это не займет много времени, чтобы увидеть, что он задумал.По сути, использование небольшого набора кодов операций VM позволяет моделировать некоторые действия как оптимизированная сборка .

0 голосов
/ 07 февраля 2011

В дополнение к методам, предложенным до сих пор, возможно, стоит изучить функции создания потоков. Если вы создаете новый поток с начальным адресом, установленным для вашего сгенерированного кода, вы точно знаете, что не существует старых регистров, которые нужно сохранять или восстанавливать, и ОС выполняет настройку соответствующих регистров для вас. Т.е. вы исключаете шаги 3, 4 и 6 из вашего списка.

...