A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
ProgrammingManual / 2 / Лекция-2.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cannot retrieve contributors at this time
143 lines (88 sloc) 24.4 KB
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents Copy raw contents
Copy raw contents
Уточнение понятия алгоритма
В школьном курсе информатики обычно говорится, что алгоритм — это некоторая последовательность действий, дающаяя решение задачи.
Однако в этом определении остается неясным, о каких именно действиях идет речь. Подразумевается, что это должно быть известно в контексте каждой конкретной задачи.
#1. Этапы трансляции программы в машинный код. Стандарты | Язык C для начинающих
В целом это так и есть, но все же здесь следует сделать следующее уточнение.
Прежде вего, отметим, что всякий алгоритм подразумевает некоторого исполнителя, которому алгоритм адресуется. Необходимо только сначала четко сформулировать, что же следует понимать под «исполнителем».
Под исполнителем понимается некоторое устройство (автомат), которое обладает следующими двумя атрибутами:
- множеством внутренних состояний (не более чем счетным);
- конечным множеством команд, которые исполнитель «умеет» исполнять. При этом в каждый момент времени исполнитель находится в каком-то одном и только одном из своих состояний. Самопроизвольно перейти из одного состояния в другое исполнитель не может. Такие переходы возможны лишь при выполнении исполнителем некоторых из своих команд. Любая команда выполняются исполнителем тогда и только тогда, когда он получает на это предписание (сигнал) из вне.
Множество всех команд исполнителя называется его командным интерфейсом.
Если внимательно приглядеться, то становится ясно, что программист всегда имеет дело только с каким-нибудь исполнителем. Будь то простые переменные в программе, программный модуль, объекты какого-либо типа (класса), любое внешнее устройство, управляемое компьютером, даже такие компоненты компьютера, как память или арифметико-логическое устройство процессора, аппаратным образом реализующее арифметические и логические операции, и т.п. — это все примеры исполнителей.
При этом если приходится иметь дело сразу с несколькими исполнителями, то, с формальной точки зрения, в совокупности их можно рассматривать как одного объединенного исполнителя. А именно, множество состояний этого объединенного исполнителя есть просто декартово произведение множеств состояний всех частных исполнителей, а его командный интерфейс будет объединением командных интерфейсов всех частных исполнителей.
Теперь мы уже можем дать более точное определение понятия алгоритма.
Чем машинный код отличается от ассемблера
Алгоритмом называется каким-либо образом определенная конечная последовательность команд исполнителя, перводящая его из заданного начального состояния в требумое конечное состояние.
Здесь существенно, что не имеет значения, каким именно образом определена эта последовательность команд: в виде обычного текста, в виде формального теста, графическими диаграммами или математическими формулами. Сам алгоритм от этого не меняется.
Далее, поледовательность команд должна быть конечной, иначе алгоритм никогда бы не заканчивался, т.е. никогда бы не выдавал решения задачи (имеется в виду, что любая команда требует каких-то временных затрат).
«Заданное начальное состояние» и «требуемое конечное состояние» определяются некоторыми высказываниями, которые мы будем именовать — «ДАНО» и «РЕЗУЛЬТАТ», соответственно.
Такого рода высказывания всегда зависят от неявно присутствующей (подразумеваемой) переменной, значения которой принадлежат множеству всех состояний исполнителя. Иными словами, при некоторых состояниях эти высказывания имеют значение true, а при других, возможно, — false.
Такие высказывания, значения которых зависят от какой-либо переменной, в логике называют предикатами — по сути это условия.
Пример предиката: x>0 — при одних значениях x это выскзывание верно, а при других — нет. Или другой пример: «Робот — у восточной границы поля» — при некоторых состояниях исполнителя «Робот на клетчатом поле», значение будет true, а при других — false.
Вот пример «ДАНО» и «РУЗУЛЬТАТ» для Робота, определяющие задание на разработку алгоритма: ДАНО: Робот — у восточной границы РЕЗУЛЬТА: Робот — у западной границы
Что такое «правильная программа»
Пусть имеется некоторый алгоритм, определяемый предикатами «ДАНО» и «РЕЗУЛЬТАТ».
Пусть имеется какая-либо запись этого алгоритма.
Любую конкретную запись алгоритма будем называть программой.
Программа называется правильной, если при любом начальном состоянии, удовлетворяющем условию «ДАНО», выполненяются пункты:
- программа завершается за конечное число шагов (на каждом шаге выполняется одна из команд исполнителя);
- достигнутое конечное состояние исполнителя удовлетворяет условию «РЕЗУЛЬТАТ».
Тут, конечно, мы упростили ситуацию, исключив из рассмотрения случаи, когда требуемое число шагов будет хотя и конечным, но непремлемо большим с практической точки зрения. А также, когда выполнение программы будет требовать слишком большой, превышающий технические возможности, объем памяти.
Таким образом, чтобы вполне удостоверится в правильности программы экспериментально, потребуется ее выполнить для каждого допустимого начального состояния. Но число допустимых состояний исполнителя может быть настолько большим, что это окажется практически не реализуемым (теоретически число состояний исполнителя может быть даже неограниченным; применительно к Роботу, например, — это случай неограниченного поля). Поэтому в таких случаях приходится продумывать систему тестов, которая будет охватывает лишь относительно небольшую часть всех возможных начальных состояний, но при этом проверять наиболее «критические» из них.
Другой подход к проверке правильности программы базируется на математических методах доказательства — это так называемое доказательное программирование.
Языки программирования и трансляция программы в машиный код
Алгоритмы записываются на формальных языках, называемых языками программирования.
Языки программирования, близкие к языку машинных команд, называтся языками низкого уровня — это ассемблеры (для каждого типа процессора существует свой ассеммблер). Такие языки позволяют в максимальной степени учитывать особенности архитектуры процессора, и поэтому позволяют создавать наиболее эффективный исполняемый (машинный — его еще называют) код. Однако на таких языках очень тяжело выражать сложные человеческие мысли.
Для удобства выражения человеческих мыслей используются языки высокого уровня, такие как Julia , Python , C/C++ , и очень многие другие. Но при этом возникает проблема перевода с языков высокого уровня на язык машинных команд. Перевод с асеммблера на язык машинных команд — это задача тривиальная.
Перевод с языка программирования на язык машинных команд (язык процессора) — называется трансляцией.
Трансляция осуществляется с помощью специальных программ, называемых трансляторами.
ТРАНСЛЯТРЫ: — ИНТЕРПРЕТАТОРЫ — КОМПИЛЯТОРЫ: — СТАТИЧЕСКИЕ КОМПИЛЯТОРЫ — ДИНАМИЧЕСКИЕ КОМПИЛЯТОРЫ (JIT-компиляторы)
Интерпретаторы, грубо говоря, транслируют каждую отдельную строку исходного кода. А компиляторы, преобразуют исходный код целиком в машинные инструкции, при этом осуществляют оптимизацию кода. В результате скомпилированный код будет работать в десятки и сотни раз быстрее интерпретируемого.
При этом интерпретаторы, в отличие от компиляторов, обладают возможностями интерактивн работы с исходным кодом, что значительно упрощает и ускоряет процесс разработки программы, и легко контролировать все этапы ее выполнения.
Обычно выбор между интерпретируемым языком и копилируемым сводится к решению дилемы, что важнее: скорость и удобство проектирования, или скорость выполнения программы.
Кроме того, граница между интепретаторами и компиляторами на самом деле не такая уж четкая. Дело в том что современные интепретаторы на самом деле тоже компилируют исходный код в инструкции какой-либо виртуальной машины (т.е. машины, реализованной программной, являющейся некоторой программной надстройкой над реальным процессором). Например, есть виртуальная машина Java (JVM), у Python тоже есть своя витруальная машина. А инструкции соответствующей виртуальной машины уже интерпретируются, т.е. последовательно преобразуются в настоящие машинные инструкции.
Тут все дело в том, насколько «далека» виртуальная машина от реального процессора. Если эта дистанция не велика, т.е. если вся оптимизация кода может быть выполнена на этапе компиляции в инструкции виртуальной машины, а эффективная трансляция этих инструкций в настояшие машинные коды — уже не представляет никакой проблемы, то такая трансляция на основе такой виртуальной машины будет порождать высокоэффективный код. Например, существует так называемая низкоуровневая виртуалная машина — LLVM (Low Level Virtual Machine). Многие современные компиляторы базируются на LLVM, в том числе и некоторые компиляторы C++.
При этом компиляция бывает двух видов — статическая и динамическая (JIT-компиляция — компиляция «на лету»). Статическая компиляция предполагает трансляцию до начала выполнения программы, а динамическая — трансляцию во время ее выполнения. Т.е. при динамической компиляции каждая отдельная функция, составляющая программу, или тело отдельного цикла транслируются при первом выполнении, а затем результат этой трансляции уже используется многократно.
Такие динамические компиляторы, если они базируются на LLVM, способны порождать очень эффективный машинный код, и могут почти не уступать в этом статическим компиляторам. При этом динамическая компиляция позволяет иметь все интерактивные возможности, свойственные интерпретаторам. Таков, например, язык Julia.
Устройство и алгоритм работы компьютера
Полезную информацию по этой теме и в четком изложении можно найти также здесь
Основные элементы компьтера — это
- ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР
- ПАМЯТЬ (опреативная память, ОЗУ)
- УСТРОЙСТВА ВВОДА-ВЫВОДА (например: монитор, клавиатура, внешние носители информации), называемые также переферийными устройствами
- ШИНА — это многожильный провод, по которому по специальному протоколу все перечисленные элементы обмениваются информацией
Память — на логическом уровне представляет собой последовательнсть байтов, в которой каждый байт имеет свой номер, называемый адресом. Есть 0-ой байт, 1-ый, . (2^N — 1)-ый, где N — разряность процессора. В этом смысле говорят, что адресное пространство — линейное. В современных компьютерах обычно N=64, но встречаются еще и 32-х разрядные машины.
Память служит для того, чтобы в ней, в едином адресном пространстве, размещались и программные коды и данные (это так называемая фоннеймановская архитектура, но бывает еще и гарвардская, в которой адресные пространства для машинных кодов и для данных разные).
Программные коды размещаются обычно в непрерывном участке памяти в естественной последовательности, команда за командой. При этом разные машинные инструкции могут иметь разную длину, т.е. некотрые из них занимают в памяти 1 байт, некоторые 2, некоторые — больше. Это происходит потому, что каждая машинная инструкция имеет свой код длиной в 1 байт, но число параметров у различных инструкций может быть разное.
В процессорах, кроме всего прочего, имеются специалные ячейки памяти, называемые регистрами. Физически регистры характеризуются тем, что позволяют с очень высокой скоростью записывать и считывать данные, много быстее ячеек обычной памяти. Регистры бывают общего назначения, их может быть довольно много, а есть специальные регистры.
В частности в любом процессоре есть регистр, называемый командным регистром, или, по другому, — счетчиком команд. Часто его обозначают как PC (Program Counter). Он предназначен для того, чтобы в каждый момент времени в нем был записан алрес команды, которая будет выполняться на следующем шаге.
Алгоритм работы компьютера может быть представлен следующим нескончаемым циклом
ПОВТОРЯТЬ БЕЗ КОНЦА: 1) прочитать команду, находящуюся в памяти по адресу, содержащемуся в PC 2) авеличить аддрес в PC на длину прочитанной команды 3) выполнить прочитанную команду
Поскольку прочитанная команда может содержать инструкцию, приводящую к измению содержимого PC, записанный алгоритм позволяет реализовавать не только линейную последовательность инструкций, но также — и циклы и ветвения.
Рассмотренная схема работы компьютера, конечно, предельно упрощена.
Во-первых, надо отметить, что программы, выполняемые на компьютере, обычно подразделяются на операционную систему (ОС), и прикладные программы, которые выполняются под управлением ОС. Операционная система может находится в фоновом режиме, передавая управление компьютером той или иной прикладной программе, или разделяя по определенному правилу машинное время между, как бы одновременно выполняемыми, прикладными прогарммами. Этот механизм мы здесь не рассматривали.
Во-вторых, приведенная схема работы компьютера не учитывает также возможность наличия многопроцессорной архитектуры.
ЗАМЕЧАНИЕ. В состав процессора входит так называемое арифметико-логическое усторйство, которое аппаратно реализует арифметические и логические опрерации. Ясно, что это устройство, в совокупности с некоторыми регистрами, является исполнителем, в том смысле, как это понятие было определено, со своим командным интерфейсом — набором процессорных инструкций. Однако компьютер в целом уже не является просто исполнителем, это автоматически работающее устройство, логика (алгоритм) работы которого была здесь рассмотрена.
Источник: github.com
Машинный код и компиляция в него — это как?
Хочу поработать с этим, потому что всегда мечтал попробовать собрать .exe самостоятельно, без помощи готовых компиляторов.
Отслеживать
6,641 6 6 золотых знаков 29 29 серебряных знаков 52 52 бронзовых знака
задан 21 апр 2020 в 8:02
Krutos VIP Krutos VIP
69 2 2 серебряных знака 9 9 бронзовых знаков
Если идти от сложного: изучить мануалы intel по платформе x86 — чтобы знать как писать машинный код, перед этим еще изучить ассемблер, хотя бы на минимальном уровне; изучить формат исполняемых файлов для вашей системы (portable executable для Windows или ELF для Linux); изучить как вообще создаются компиляторы — тут «Книга Дракона» в помощь.
21 апр 2020 в 8:22
Если от более простого — отказаться от идеи самому создавать бинарник, а генерировать например промежуточный Си код, который уже будет компилироваться в бинарный (или воспользоваться инструментарием LLVM). Но опять же в этом случае все равно нужно будет изучить «Книгу Дракона».
21 апр 2020 в 8:25
Связанный вопрос: Генерация exe файла
21 апр 2020 в 8:25
Не совсем по теме, но, возможно, вдохновит — история одного байта, fermi paradox
– user249284
21 апр 2020 в 9:54
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Baremetal
Каждый конкретный процессор (например, Intel Core i3-4160 или ARM Cortex-A9) имеет свою микроархитектуру и реализует архитектуру уровня набора команд (англ. instruction set architecture).
- Микроархитектура определяет структуру процессора на уровне электронных компонентов и логических вентилей.
- Архитектура уровня набора команд (ISA), грубо говоря, определяет то, какие команды может выполнять процессор. Эта архитектура абстрагированна от микроархитектуры. Процессоры разных комнаний могут реализовывать одну и ту же архитектуру (например, многие процессоры Intel и AMD реализует одно и то же семейство архитектур x86).
Если два процессора реализуют одну и ту же ISA, то они могут исполнять одни и те же программы. ISA определяет, какие команды доступны программисту, какие регистры он может использовать, как он может использовать страничную адресацию, виртуальную память и т. д. Кроме того, она определяет формат команд, которые понимает процессор.
Каждая программа процессора — это просто набор подряд идущих команд. При своем запуске процессор выбирает команду из память по адресу, называемому вектором сброса (англ. reset vector) и начинает исполнять эту программу, пока питание не будет отключено.
Написать программу в машинных кодах достаточно просто — нужно лишь взять справочник по ISA (например, Intel 64 and IA-32 Architectures Software Developer Manuals), которую реализует ваш процессор и написать нужные команды байт за байтом.
Конечно, в наше время никто в машинных кодах не пишет, потому что человеку тяжело работать с большим объемом чисел и сложными форматами команд (особенно в x86). Из-за таких сложностей были придуманы языки ассемблера, которые вводят простые мнемоники для инструкций процессора.
Например, одна инструкция ассемблера x86 MOV может кодировать около 20 различных инструкций процессора MOV 1 . Ассемблер читает вашу программу на языке ассемблера и переводит ее в бинарный файл 2 , который, опять же, является просто последовательность байт, кодирующих подряд идущие инструкции процессора.
Вот так может выглядет отрывок программы на языке ассемблера:
cli lgdt (gdtr) mov %cr0, %eax or $0x1, %eax mov %eax, %cr0
Вот так выглядит программа на машинном языке:
0000000 05ea 007c 3100 8ec0 8ed8 bcd0 7c00 1688 0000010 7cdb c031 c08e 00bb 8a80 db16 b67c b100 0000020 b502 b000 e830 0053 59e8 8400 75c0 fa30 0000030 010f f416 0f7c c020 8366 01c8 220f eac0 0000040 7c44 0008 b866 0010 d88e c08e e08e e88e 0000050 d08e 00bc 07c0 e800 03a4 0000 ebf4 befd 0000060 7cbc 03e8 f400 fdeb 5350 30fc b4ff ac0e 0000070 c084 0474 10cd f7eb 585b b4c3 cd02 7213 0000080 3102 c3c0 1e9c 0657 fa56 c031 d88e 10bf 0000090 f705 8ed0 bec0 0500 058a 2650 048a 2650 00000a0 04c6 c600 be05 8026 be3c 2658 0488 8858 00000b0 3105 74c0 4001 075e 1f5f c39d 3241 2030 00000c0 7369 6420 7369 6261 656c 2e64 4820 6c61 00000d0 2074 6874 2065 5043 2e55 0000 0000 0000 00000e0 0000 0000 ffff 0000 9a00 00cf ffff 0000 00000f0 9200 00cf 0017 7cdc 0000 0000 0000 0000
Очевидно, что асссемблерный код и читать, и писать проще.
Теперь у вас достаточно знаний, чтобы открыть справочник, как по словарю, написать программу в машинных кодах и исполнить ее на процессоре. Но, это не сработает в случае, если вы хотите написать программу, которая будет работать в какой-либо операционной системе.
Операционная система
Операционная система — это еще один уровень абстрации, который полностью лишает нас возможности неограниченно пользоваться нашим процессором, заставляя его исполнять любые наши команды 3 . ОС делает очень много различных вещей, но остановимся только на одной — запуск исполняемых файлов.
Как я уже сказал, каждая программа процессора — это просто последовательность команд, однако каждая программа операционной системы — это особая последовательность байт, имеющая специальную структуру, в которую входят не только команды процессора.
Если брать в пример ОС Windows 10, она работает с исполняемыми файлами .exe , которые имеют специальный формат, называемый Portable Executable. Он имеет довольно сложную структуру. Помимо собственно набора машинных команд он содержит в себе информацию необходимую для определения адреса и размера секций, таблиц импорта и экспорта, специальную сигнатуру и т. д.
Поэтому чтобы вручную написать программу в машинных кодах, которая будет запускаться в Windows 10, например, нам, по-мимо написания самой программы, потребуется привести ее к формату Portable Executable.
Но и этого будет не достаточно. Нам придется ознакомится с соглашениями, которые называются ABI и написать программу в машинных кодах, используя именно эти соглашения, а не какие-то другие.
Здесь необходимо, чтобы все части паззла подходили друг к другу по форме: программа должна быть валидной для процессора, формат бинарного файла должен быть понятен операционной системе, программа должна уметь корректно общаться с ОС и т. д. Это все очень сложно обеспечить, если писать программу в шестнадцатеричном редакторе.
Можете начать с написания программ на языке ассемблера (да, вам придется еще выучить синтаксис конкретного языка ассемблера и диалект Intel или AT —————————————————————————- ; helloworld.asm ; ; This is a Win32 console program that writes «Hello, World» on one line and ; then exits. It needs to be linked with a C library. ; —————————————————————————- global _main extern _printf section .text _main: push message call _printf add esp, 4 ret message: db ‘Hello, World’, 10, 0
А нужно ли вам это?
В наше время компьютеры стали очень сложными, с десятками слоями абстраций. Даже инструкции ISA современных процессоров — не атомарные сущности, и процессоры выполняет каждую такую инструкцию как набор еще более мелких инструкций — микрооперации (из таких мокроопераций складывается микрокод).
На самом деле, умение писать на языке ассемблера (а тем более, на машинном языке) довольно бесполезно. Умение просто читать и понимать ассемблерный листинг гораздо более практично и действительно может вам пригодится.
А непрактично это в первую очередь потому, что ничего сложнее «Hello, World!» в машинных кодах вы не напишете. На ассемблере — да, напишете, но потратите на это колоссальное количество времени, которое можно было бы потратить на более полезные вещи.
1. Что интересно, инструкция MOV в x86 является Тьюринг-полной, т. е. любая программа может быть написана с использованием одной только этой инструкции. Есть даже специальный компилятор, который использует только одну эту инструкцию.
2. Некоторые ассемблеры могут сразу формировать исполняемые файлы в нужном формате. В том числе и Portable Executable.
3. Я говорю о современных ОС типа Windows или Linux.
Источник: ru.stackoverflow.com
Машинный код и байт код: на каком языке говорит ваша программа?
У тех, кто только начинает знакомиться с Java, довольно часто возникает путаница в понятиях машинный и байт код. Что они собой представляют? В чём различия? В короткой заметке мы постараемся максимально просто и понятно расписать их особенности, чтоб раз и навсегда закрыть этот вопрос.
Машинный код
Процессор — это, по сути, очень сложный и продвинутый калькулятор. У него есть множество ячеек памяти (называемых регистрами) с которыми и между которыми проводятся различные математические и байтовые операции. Машинный код как раз и представляет собой описание последовательности выполнения операций и набора участвующих данных. По сути, это единственный язык, который понимает процессор вашего компьютера.
Врожденная несовместимость
При этом далеко не все процессоры «говорят» на одном языке. Различия есть не только между архитектурами CISC и RISC, но и внутри этих «лагерей».
CISC (англ. Complex Instruction Set Computing) — концепция проектирования процессоров, которая характеризуется следующим набором свойств:
- много команд, разных по длине;
- много режимов адресации;
- сложная кодировка инструкции.
В новых поколениях процессоров внедряют дополнительные наборы инструкций, которые моделям старшего поколения попросту неизвестны. Из-за этого программы, скомпилированные для одной архитектуры (или одного поколения процессоров) не могут работать на другом аппаратном обеспечении. Все это вынуждает заниматься перекомпиляцией программ для обеспечения их работы на других компьютерах. Впрочем, заново компилировать приходится не только из-за процессоров, но и еще из-за различий во взаимодействии программ и операционной системы. Именно из-за них невозможно запустить «виндовую» программу под Linux, а «линуксовую» под Windows.
Байт-код
Байт-код во многом похож на машинный код, только он использует набор инструкций не реального процессора, а виртуального. При этом он может включать в себя участки, ориентированные на использование JIT-компилятора, оптимизирующего выполнение команд под реальный процессор, на котором запущена программа.
JIT-компиляция (англ. Just-in-time compilation, компиляция «на лету») или динамическая компиляция (англ. dynamic translation) — это технология увеличения производительности программных систем, использующих байт-код, путём компиляции байт-кода в машинный код или в другой формат непосредственно во время работы программы. «Официально» в Java до 9-й версии был только JIT-компилятор. В Java 9 появился ещё один компилятор, причём компилирует он с опережением (AoT). Эта возможность позволяет компилировать классы Java в нативный код перед запуском на виртуальной машине. Данная функция предназначена для улучшения времени запуска и малых, и больших приложений, с ограниченным влиянием на максимальную производительность. |
Для CISC процессоров некоторые инструкции могут объединяться в более сложные конструкции, поддерживаемые процессором, а для RISC – наоборот разбиваться на более простые последовательности команд.
Еще и виртуальная ОС
Впрочем, байт код содержит не только процессорные инструкции. В нем также содержится логика взаимодействия с виртуальной операционной системой, которая делает поведение приложения независящим от используемой на компьютере операционной системы. Это отлично видно в JVM, где работа с системными вызовами и GUI зачастую не зависят от ОС, на которой запущена программа. По большому счету JVM эмулирует запуск процесса программы, в отличие от решений вроде Virtual Box, которые создают только виртуальную систему/железо.
JVM одна такая?
Определенно нет. Тот же DotNet CLI это тоже виртуальная машина, которую чаще всего используют на компьютерах, работающих под Windows с x86 совместимыми процессорами.
Впрочем существует ее реализация и под другие системы: приложения под него должны работать в Windows RT запущенной на ARM (RISC) совместимых процессорах, или можно запустить их на Linux/OSX в среде Mono, являющей сторонней (и потому не полностью совместимой) реализацией DotNet для этих платформ. Так что эта платформа, как и JVM, работает на разных процессорах и разных ОС.
Существует еще множество похожих решений (как старых, так и новых): LLVM, Flash SWF, и другие. У некоторых языков программирования есть собственные виртуальные машины. К примеру, CPython компилирует исходники из PY в файлы PYC – скомпилированный (compiled) байт код который подготовлен к запуску в PVM.
Или есть намного более древний пример — Lisp можно компилировать в файлы FASL (Fast Load). Фактически они содержат AST дерево, построенное генератором из исходного кода. Эти файлы могут быть прочитаны и запущены интерпретатором Lisp на разных платформах, или использованы для создания машинного кода для используемой на данный момент аппаратной архитектуры.
Источник: javarush.com