Как я могу скомпилировать специфичный для VM код в машинный код x86?
У меня есть 16-разрядная виртуальная машина на основе регистров, я хочу знать, каковы этапы ее компиляции с реальным машинным кодом x86? Я не собираюсь делать JIT-компилятор, если нет необходимости связывать скомпилированный код с другим исполняемым файлом / DLL.
Виртуальная машина сделана так, что, если виртуальная машина добавлена в проект, могут быть добавлены специальные языковые конструкции. (например, если он встроен в игровой движок, может быть добавлен тип объекта "Entity" и может быть открыто несколько функций C из движка.) Это приведет к тому, что код будет полностью зависеть от определенных предоставляемых функций C или открытые классы C++, в приложении, в которое он встроен.
Как будет возможно "связывание" такого рода, если код сценария скомпилирован из байт-кода виртуальной машины в собственный EXE-файл?
Он также основан на регистрах, как и виртуальная машина Lua, поскольку все основные переменные хранятся в "регистрах", которые представляют собой огромный массив Си. Указатель регистра увеличивается или уменьшается при изменении области видимости, поэтому номера регистров являются относительными, аналогично указателю стека. Например:
int a = 5;
{
int a = 1;
}
может быть, в виртуальной машине псевдо-сборки:
mov_int (%r0, $5)
; new scope, the "register pointer" is then incremented by the number
; of bytes that are used to store local variables in this new scope. E.g. int = 4 bytes
; say $rp is the "register pointer"
add (%rp, $4) ; since size of int is usually 4 bytes
; this is if registers are 1 bytes in size, if they were
; 4 bytes in size it would just be adding $1
mov_int (%r0, $1) ; now each register "index" is offset by 4,
; this is now technically setting %r4
; different instructions are used to get values above current scope
sub (%rp, $4) ; end of scope so reset %rp
У меня вопрос об этой части: должен ли я использовать указатель стека для такого рода вещей? Базовый указатель? Что я мог бы использовать, чтобы заменить эту концепцию?
2 ответа
Виртуальная машина сделана так, что, если виртуальная машина добавлена в проект, могут быть добавлены специальные языковые конструкции. (например, если он встроен в игровой движок, может быть добавлен тип объекта "Entity" и может быть открыто несколько функций C из движка.) Это приведет к тому, что код будет полностью зависеть от определенных предоставляемых функций C или открытые классы C++, в приложении, в которое он встроен.
Существует много способов реализации такого рода межязыкового взаимодействия. Используете ли вы байт-код виртуальной машины или собственный машинный код, здесь не будет иметь большого значения, если вам не нужен интерфейс с очень низкими издержками. Основное внимание уделяется характеру вашего языка, особенно если он имеет статическую или динамическую типизацию.
Вообще говоря, это два наиболее распространенных подхода (вы, возможно, уже знакомы с ними):
(a) Подход "внешний интерфейс-функция", когда ваш язык / среда выполнения предлагает средства для автоматической упаковки функций и данных из C. Примеры включают LuaJIT FFI, js-ctypes и P / Invoke. Большинство FFI могут работать с функциями CDECL/STDCALL и структурами POD; некоторые имеют различные уровни поддержки классов C++ или COM.
(b) Подход "runtime-API", в котором ваша среда выполнения предоставляет API C, который вы можете использовать для ручного создания / манипулирования объектами для использования на вашем языке. Lua имеет обширный API для этого ( пример), как и Python.
Как будет возможно "связывание" такого рода, если код сценария скомпилирован из байт-кода виртуальной машины в собственный EXE-файл?
Таким образом, вы, вероятно, думаете о том, как, например, внедрить адреса сторонних функций в ваш сгенерированный машинный код. Что ж, если у вас есть соответствующая инфраструктура FFI, нет никаких причин, по которым вы не можете этого сделать, если вы знаете, как работает импорт разделяемой библиотеки (импорт таблиц адресов, перемещение, исправления и т. Д.).
Если вы не очень много знаете о разделяемых библиотеках, я думаю, что потратив некоторое время на изучение этой области, вы начнете получать гораздо более четкое представление о способах реализации FFI в своем компиляторе.
Однако, если было бы, вероятно, легче использовать немного более динамичный подход, например: LoadLibrary()
, GetProcAddress()
, затем оберните указатель функции как объект вашего языка.
К сожалению, очень трудно давать более конкретные предложения, не зная ничего о рассматриваемом языке / ВМ.
[…] Мой вопрос об этой части: должен ли я использовать указатель стека для такого рода вещей? Базовый указатель? Что я мог бы использовать, чтобы заменить эту концепцию?
Я не совсем уверен, какова цель этой схемы 'массива регистров'.
На языке с лексической областью видимости я понимаю, что при компиляции функции вы обычно перечисляете каждую переменную, объявленную в ее теле, и выделяете блок стека, достаточно большой, чтобы вместить все переменные (игнорируя сложную тему выделения регистров ЦП). Код может обращаться к этим переменным, используя указатель стека или (чаще) базовый указатель.
Если переменная во внутренней области видимости скрывает переменную во внешней области, как в вашем примере, им назначается отдельное пространство памяти в стеке, поскольку для компилятора это разные переменные.
Без понимания причин, лежащих в основе схемы, используемой виртуальной машиной, я не могу точно сказать, как она должна переводиться в машинный код. Может быть, кто-то с большим опытом программирования байт-кодов может дать вам ответ, который вам нужен.
Однако может случиться так, что подход вашей виртуальной машины на самом деле аналогичен тому, что я описал, и в этом случае адаптация его для компиляции машинного кода должна быть очень простой - просто вопрос преобразования вашего виртуального пространства памяти локальной переменной в пространство стека.
Если я правильно понимаю ваш вопрос, то да, вам придется использовать SP/BP и т. Д. Здесь. Вот что означает компиляция с собственным машинным кодом: преобразуйте поведение вашей программы на более высоком уровне в эквивалентные машинные инструкции, которые следуют соглашениям операционной системы, в которой она работает.
Поэтому вам, по сути, придется делать то же самое, что и для вызова функций, предоставляемых хостом, если вы вызываете их из ассемблера. Обычно это означает сохранение значений аргументов функции в соответствующих регистрах / их помещение в стек, преобразование их по мере необходимости, затем генерацию инструкции CALL или JMP или чего-либо еще, что процессор ожидает, чтобы фактически перейти к адресу памяти данной функции.
Вам нужно иметь таблицу имен функций для сопоставления указателей на функции, которую предоставляет вам хост, и искать адрес оттуда.
После того, как функция вернется, вы должны преобразовать значения, возвращаемые функцией, в ваши внутренние типы, если это необходимо, и продолжить свой веселый путь. (Это в основном то, что все эти библиотеки "интерфейса внешних функций" делают внутри).
В зависимости от вашего языка и того, для чего он используется, здесь также можно обмануть. Вы можете использовать свой собственный внутренний псевдостек и просто добавить специальную инструкцию "call a native function". Эта инструкция будет получать информацию о функции в качестве параметра (например, какие типы параметров она принимает / возвращает, как искать указатель на функцию) и затем использовать библиотеку интерфейса сторонней функции для фактического вызова функции.
Это будет означать, что вызов нативной функции будет иметь незначительные накладные расходы, но будет означать, что вы можете оставить свою виртуальную машину как есть, но при этом позволить людям вызывать нативный код для интеграции с вашим приложением.