Самое близкое к сборке - это создание моей собственной библиотеки классов Java, которая загружает файлы классов и позволяет создавать, компилировать и декомпилировать классы. Разрабатывая этот проект, я удивлялся, как виртуальная машина Java фактически генерировала машинный код во время выполнения во время оптимизации JIT.

Это заставило меня задуматься: как можно сгенерировать машинный код и выполнить его во время выполнения со сборкой, и в качестве бонуса, без библиотеки JIT-компилятора или «вручную»?

0
AMDG 6 Апр 2017 в 15:19

2 ответа

Лучший ответ

Ваш вопрос существенно изменился (в июле 2017 года). Первоначальный вариант ссылался на инструкцию EX (execute) мэйнфреймов IBM.

как можно сгенерировать машинный код и выполнить его во время выполнения со сборкой ...?

На практике вы будете использовать некоторую JIT-компиляцию библиотеку, и есть много их. Или вы можете использовать динамический загрузчик. На самом низком уровне все они пишут некоторые байтовые последовательности, представляющие действительный машинный код - последовательность из многих машинные инструкции - в сегменте памяти (вашего виртуального адресного пространства), который необходимо создать < a href = "https://en.wikipedia.org/wiki/Executable_space_protection" rel = "nofollow noreferrer"> исполняемый файл (читайте о бит NX), а затем часть вашего кода будет косвенно переходить на этот адрес или чаще вызывать его косвенно, то есть через указатель на функцию. В большинстве JVM используются методы компиляции JIT.

... и в качестве бонуса, без библиотеки JIT-компилятора или "вручную"?

Предположим, у вас есть некоторый допустимый машинный код для архитектуры процессора, на которой ваша программа в данный момент выполняется, например, вы можете получить сегмент памяти (например, mmap (2) в Linux), а затем сделать его исполняемым (например, mprotect (2)). Большинство других операционных систем предоставляют аналогичные системные вызовы.


Если вы используете библиотеку компиляции JIT, например, asmjit или libjit или libgccjit или LLVM или многие другие, вы сначала создаете в памяти представление (похожее на некоторые дерево абстрактного синтаксиса) генерируемого кода, а затем попросите библиотеку JIT выдать машинный код для него. Вы даже можете написать свой собственный код JIT-компиляции, но это много работы (вам нужно понять все детали вашего набор инструкций, например x86 для ПК). Кстати, генерировать быстро работающий машинный код действительно сложно, потому что вам нужно оптимизировать, например компиляторы делают (и заботятся о таких деталях, как планирование инструкций, распределение регистра и т. д. см. также этот), и именно поэтому используется существующая библиотека JIT-компиляции (например, libgccjit или LLVM) предпочтительнее (наоборот, более простые библиотеки JIT, такие как asmjit или href = "https://www.gnu.org/software/libjit/" rel = "nofollow noreferrer"> libjit или GNU lightning не сильно оптимизируют и генерируют плохой машинный код).

Если вы используете динамический загрузчик (например, dlopen (3) в POSIX) вы использовали бы некоторый внешний компилятор для создания разделяемой библиотеки (то есть плагин), а затем вы просите динамический компоновщик загрузить его в ваш процесс (и обрабатывать соответствующие перемещения) и получать по имени (используя dlsym (3)) некоторые адреса функций из него.

Некоторые языковые реализации (в частности, SBCL для Common Lisp) способны на лету генерировать хороший машинный код на каждом REPL. По сути, их среда выполнения запускает полный компилятор (содержащий часть компиляции JIT).

Хитрость, которую я часто использую в Linux, заключается в том, чтобы испускать некоторый код C (или C ++) во время выполнения в каком-то временном файле ( который компилирует некоторый предметно-ориентированный язык в C или в C ++), разветвляет его компиляцию в виде плагина и динамически загружает Это. С текущими (ноутбуки, настольные компьютеры, серверы) компьютерами это достаточно быстро, чтобы оставаться совместимым с интерактивным циклом.

Читайте также о eval (в частности, знаменитом книга SICP), метапрограммирование, многоступенчатое программирование, самоизменяющийся код, продолжения, компиляторы (книга драконов) , Прагматика языка программирования и Блог Дж. Питрата.

7
Peter Cordes 21 Авг 2018 в 11:52

Чтобы выполнить часть машины x86, используйте инструкцию jmp, чтобы перейти к ее началу. Обратите внимание, что процессор не знает, где заканчивается код, поэтому вы должны выполнить ручную аранжировку. Лучший способ - использовать call для вызова этого машинного кода, а затем вернуться с инструкцией ret где-нибудь в коде.

Не существует прямого способа выполнить только одну инструкцию, поскольку это обычно довольно бессмысленно. Я не уверен, чего вы пытаетесь достичь.

4
fuz 6 Апр 2017 в 12:41