В последнее время я очень заинтересовался разработкой языка, у меня было несколько рабочих интерфейсов, и у меня были разные системы для выполнения кода. Я решил, что хотел бы попытаться разработать систему типа виртуальных машин. (Вроде как JVM, но, конечно, намного проще) Итак, мне удалось создать базовый набор рабочих инструкций со стеком и регистрами, но мне просто любопытно, как некоторые вещи должны быть реализованы.
Например, в Java после того, как вы написали программу, вы компилируете ее с помощью компилятора java, и она создает двоичный файл (.class) для выполнения JVM. Я не понимаю, как это сделать, как JVM интерпретирует этот двоичный файл, каков переход от читаемых человеком инструкций к этому двоичному файлу, как я могу создать нечто подобное?
Спасибо за любую помощь / предложения!
Хорошо, я укушу этот общий вопрос.
Реализация комбо-компилятора / ассемблера / виртуальной машины — сложная задача, особенно если вы делаете это самостоятельно.
Это сказанное: если вы сохраняете свою языковую спецификацию достаточно простой, это вполне выполнимо; также самостоятельно.
По сути, для создания двоичного файла выполняется следующее (это тад немного упрощено *:
1) Источник входных данных читается, лексируется и маркируется
2) Логика программы анализируется на семантическую корректность.
Например. в то время как следующий C ++ будет разбирать & токенизировать, не получится семантического анализа
float int* double = const (_identifier >><<) operator& *
3) Создайте абстрактное синтаксическое дерево для представления операторов
4) Создание таблиц символов и определение идентификаторов
5) Необязательный: Оптимизация кода
6) Генерация кода в выходном формате по вашему выбору; например, двоичные коды операций / операнды, строковые таблицы. Какой бы формат ни подходил вашим потребностям. В качестве альтернативы вы можете создать байт-код для существующей виртуальной машины или для собственного процессора.
РЕДАКТИРОВАТЬ
Если вы хотите разработать свой собственный формат байт-кода, вы можете написать, например:
1) File Header
DWORD filesize
DWORD checksum
BYTE endianness;
DWORD entrypoint <-- Entry point for first instruction in main() or whatever
2) String table
DWORD numstrings
<strings>
DWORD stringlen
<string bytes/words>
3) Instructions
DWORD numinstructions
<instructions>
DWORD opcode
DWORD numops <--- or deduce from opcode
DWORD op1_type <--- stack index, integer literal, index to string table, etc
DWORD operand1
DWORD op1_type
DWORD operand2
...
КОНЕЦ
В целом, шаги управляемы, но, как всегда, дьявол кроется в деталях.
Некоторые хорошие ссылки:
Книга Дракона — Это тяжело в теории, так что это сухое чтение, но стоит
Мастерство сценариев игры — Руководит вами при разработке всех трех компонентов в более практичном вопросе. Однако пример кода изобилует проблемами безопасности, утечками памяти и общим паршивым стилем кодирования (imho). Тем не менее, вы можете взять много понятий из этой книги, и это стоит прочитать.
Искусство дизайна компилятора — Я не читал это лично, но слышал положительные отзывы об этом.
Если вы решите пойти по этому пути, убедитесь, что вы знаете, во что вы ввязываетесь. Это не что-то слабонервное или новичок в программировании. Это требует много концептуального мышления и предварительного планирования. Это, однако, довольно полезно и весело
@APott —
1) Виртуальные машины не создают двоичные файлы. Компилятор Java создает двоичные файлы .class; работающая JVM загружает и выполняет файлы классов.
2) В Java JVM нет ничего особенно «нового» или уникального. Концептуально он не отличается от UCSD Pascal или IBM MV / 370. Вот хорошая краткая история ВМ:
3) Если вам интересно, полная спецификация JVM онлайн, и есть много книг / ссылок, которые обсуждают это подробно:
Все, что делает компилятор — это преобразовывает строку в строку, независимо от того, является ли цель реальной машиной или виртуальной машиной. Поскольку вы создаете свою собственную целевую виртуальную машину, вы можете использовать другой способ кодирования, чем существующие наборы инструкций виртуальных или физических машин, но это на самом деле не меняется. Весь набор команд физической машины можно эмулировать в программном обеспечении, а весь набор инструкций виртуальной машины можно запускать аппаратно (хотя на практике это может быть немного сложнее, поскольку набор инструкций, предназначенный для виртуальной машины, может быть гораздо более сложным, чем позволяет аппаратный бюджет). Процессор, в конце концов, просто интерпретатор набора инструкций.
Любые книги по компиляторам должны расширяться на этом, но процесс компиляции одинаков для физической или виртуальной машины. В общем, нужно начать с разбор ваш исходный язык в абстрактное синтаксическое дерево исходного кода (AST), то вам нужно перевод которые преобразуют этот исходный AST в целевой AST (хотя целевой язык, как правило, намного более плоский, чем исходный, поэтому вам может не понадобиться дерево, но обычно достаточно массива), тогда вам нужно генерация кода преобразовать целевой AST в байт-код (обычно это просто перевод одного из одного целевого узла AST в байт-код). Для языков со сложным синтаксисом вам может потребоваться промежуточный этап синтаксического анализа для формирования конкретных синтаксических деревьев a.k.a. дерево синтаксического анализа, прежде чем вы сможете сформировать исходный AST; и некоторые компиляторы могут использовать несколько этапов перевода и могут включать оптимизирующий переводчик между ними; это небольшие различия.