如何将特定于 VM 的代码编译为 x86 机器代码?
How can I compile VM-specific code into x86 machine code?
我有一个基于寄存器的 16 位虚拟机,我想知道将它编译成实际的 x86 机器码的步骤是什么?我不打算制作 JIT 编译器 除非 有必要能够 link 使用另一个可执行文件/DLL 编译的代码。
制作 VM 后,如果将 VM 添加到项目中,则可以添加特殊的语言结构。 (例如,如果嵌入到游戏引擎中,可能会添加一个 "Entity" 对象类型,并且可能会暴露引擎中的几个 C 函数。)这将导致代码完全依赖于某些暴露的 C函数或公开的 C++ 类,在它嵌入的应用程序中。
如果脚本代码是从 VM 字节码编译成本机 EXE,这种 "linking" 怎么可能?
它也像Lua的VM一样是基于寄存器的,因为所有基本变量都存储在"registers"中,这是一个巨大的C数组。当范围发生变化时,寄存器指针会递增或递减,因此寄存器编号是相对的,类似于堆栈指针。例如:
int a = 5;
{
int a = 1;
}
可能是,在虚拟机伪汇编中:
mov_int (%r0, )
; 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, ) ; 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
mov_int (%r0, ) ; 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, ) ; end of scope so reset %rp
我对这部分的问题是,我是否必须为这类事情使用堆栈指针?基指针?我可以用什么来代替这个概念?
The VM is made such that if the VM is added to a project, special language constructs can be added. (for example, if it is embedded into a game engine, an "Entity" object type may be added, and several C functions from the engine might be exposed.) This will cause the code to be completely dependent on certain exposed C functions or exposed C++ classes, in the application it's embedded into.
有很多方法可以实现这种跨语言接口。无论您是 运行 VM 字节码还是本机机器码在这里都无关紧要,除非您需要一个开销非常低的接口。主要考虑因素是您的语言的性质——尤其是它是静态类型还是动态类型。
一般来说,这是两种最常见的方法(您可能已经熟悉它们):
(a) 'foreign-function-interface' 方法,你的 language/runtime提供自动包装来自 C 的函数和数据的工具。示例包括 LuaJIT FFI, js-ctypes and P/Invoke。大多数 FFI 可以对 CDECL/STDCALL 函数和 POD 结构进行操作;有些对 C++ 或 COM 的支持程度不同 类.
(b) 'runtime-API' 方法,您的运行时公开一个C API 您可以手动使用 construct/manipulate 对象以在您的语言中使用。 Lua 对此有广泛的 API (example) as does Python.
How would this sort of "linking" be possible if the script code is compiled from VM bytecode into a native EXE?
所以您可能正在考虑如何将外部函数地址烘焙到生成的机器代码中。好吧,如果您拥有适当的 FFI 基础设施,那么您没有理由不能这样做,只要您了解共享库导入的工作方式(导入地址表、重定位、修正等)。
如果您不太了解共享库,我认为通过花一些时间研究该领域,您将开始更清楚地了解在编译器中实现 FFI 的方式。
然而,如果采用稍微更动态的方法可能更容易,例如:LoadLibrary()
、GetProcAddress()
,那么将函数指针包装为您的语言的对象。
不幸的是,如果不了解所讨论的 language/VM,很难给出更具体的建议。
[…] My question about this part is, would I have to use the stack pointer for this sort of thing? The base pointer? What could I use to replace this concept?
我不完全确定这个 'register array' 方案的目的是什么。
在具有词法作用域的语言中,据我了解,在编译函数时,您通常会枚举在其主体中声明的每个变量,并分配一个足够大的堆栈块 space 来容纳所有变量(忽略CPU 寄存器分配的复杂主题)。代码可以使用堆栈指针或(更常见的)基指针来寻址这些变量。
如果内部作用域中的变量像您的示例一样隐藏外部作用域中的变量,则它们会在堆栈上分配单独的内存 space——因为就编译器而言,它们是不同的变量。
在不了解 VM 使用的任何方案背后的基本原理的情况下,我无法真正建议应如何将其转换为机器代码。也许对字节码编译器编程有更多经验的人可以给你想要的答案。
然而,您的 VM 的方法实际上可能与我所描述的类似,在这种情况下,将其改编为机器代码编译实际上应该非常简单 - 只需翻译您的虚拟局部变量内存 space 入栈 space.
如果我正确理解你的问题,那么是的,你必须在这里使用 SP/BP 等。这就是编译为本机机器代码的含义:将程序的高级行为转换为等效的机器指令,这些指令遵循它所在的操作系统的约定 运行。
因此,如果您从汇编程序中调用主机提供的函数,您基本上必须执行与调用主机提供的函数相同的操作。这通常意味着将函数参数的值粘贴到适当的寄存器中/将它们压入堆栈,根据需要转换它们,然后生成 CALL 或 JMP 指令或任何 CPU 期望实际跳转到的内存地址给定的函数。
您需要有一个 table 函数名称到宿主提供给您的函数指针映射,然后从那里查找地址。
一旦函数 returns,如果需要,您可以将函数返回的值转换回您的内部类型,然后继续您的快乐之路。 (这基本上就是所有这些 "foreign function interface" 库在内部所做的事情)。
根据您的语言及其用途,也可以在此处作弊。您可以使用自己的内部伪堆栈,只需添加一个特殊的 "call a native function" 指令即可。该指令将接收有关函数的信息作为参数(例如,它的参数类型 takes/returns、如何查找函数指针),然后将使用外部函数接口库进行实际的函数调用。
这意味着调用本机函数会有轻微的开销,但意味着您可以保持 VM 原样,同时仍允许人们调用本机代码以与您的应用程序集成。
我有一个基于寄存器的 16 位虚拟机,我想知道将它编译成实际的 x86 机器码的步骤是什么?我不打算制作 JIT 编译器 除非 有必要能够 link 使用另一个可执行文件/DLL 编译的代码。
制作 VM 后,如果将 VM 添加到项目中,则可以添加特殊的语言结构。 (例如,如果嵌入到游戏引擎中,可能会添加一个 "Entity" 对象类型,并且可能会暴露引擎中的几个 C 函数。)这将导致代码完全依赖于某些暴露的 C函数或公开的 C++ 类,在它嵌入的应用程序中。
如果脚本代码是从 VM 字节码编译成本机 EXE,这种 "linking" 怎么可能?
它也像Lua的VM一样是基于寄存器的,因为所有基本变量都存储在"registers"中,这是一个巨大的C数组。当范围发生变化时,寄存器指针会递增或递减,因此寄存器编号是相对的,类似于堆栈指针。例如:
int a = 5;
{
int a = 1;
}
可能是,在虚拟机伪汇编中:
mov_int (%r0, )
; 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, ) ; 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
mov_int (%r0, ) ; 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, ) ; end of scope so reset %rp
我对这部分的问题是,我是否必须为这类事情使用堆栈指针?基指针?我可以用什么来代替这个概念?
The VM is made such that if the VM is added to a project, special language constructs can be added. (for example, if it is embedded into a game engine, an "Entity" object type may be added, and several C functions from the engine might be exposed.) This will cause the code to be completely dependent on certain exposed C functions or exposed C++ classes, in the application it's embedded into.
有很多方法可以实现这种跨语言接口。无论您是 运行 VM 字节码还是本机机器码在这里都无关紧要,除非您需要一个开销非常低的接口。主要考虑因素是您的语言的性质——尤其是它是静态类型还是动态类型。
一般来说,这是两种最常见的方法(您可能已经熟悉它们):
(a) 'foreign-function-interface' 方法,你的 language/runtime提供自动包装来自 C 的函数和数据的工具。示例包括 LuaJIT FFI, js-ctypes and P/Invoke。大多数 FFI 可以对 CDECL/STDCALL 函数和 POD 结构进行操作;有些对 C++ 或 COM 的支持程度不同 类.
(b) 'runtime-API' 方法,您的运行时公开一个C API 您可以手动使用 construct/manipulate 对象以在您的语言中使用。 Lua 对此有广泛的 API (example) as does Python.
How would this sort of "linking" be possible if the script code is compiled from VM bytecode into a native EXE?
所以您可能正在考虑如何将外部函数地址烘焙到生成的机器代码中。好吧,如果您拥有适当的 FFI 基础设施,那么您没有理由不能这样做,只要您了解共享库导入的工作方式(导入地址表、重定位、修正等)。
如果您不太了解共享库,我认为通过花一些时间研究该领域,您将开始更清楚地了解在编译器中实现 FFI 的方式。
然而,如果采用稍微更动态的方法可能更容易,例如:LoadLibrary()
、GetProcAddress()
,那么将函数指针包装为您的语言的对象。
不幸的是,如果不了解所讨论的 language/VM,很难给出更具体的建议。
[…] My question about this part is, would I have to use the stack pointer for this sort of thing? The base pointer? What could I use to replace this concept?
我不完全确定这个 'register array' 方案的目的是什么。
在具有词法作用域的语言中,据我了解,在编译函数时,您通常会枚举在其主体中声明的每个变量,并分配一个足够大的堆栈块 space 来容纳所有变量(忽略CPU 寄存器分配的复杂主题)。代码可以使用堆栈指针或(更常见的)基指针来寻址这些变量。
如果内部作用域中的变量像您的示例一样隐藏外部作用域中的变量,则它们会在堆栈上分配单独的内存 space——因为就编译器而言,它们是不同的变量。
在不了解 VM 使用的任何方案背后的基本原理的情况下,我无法真正建议应如何将其转换为机器代码。也许对字节码编译器编程有更多经验的人可以给你想要的答案。
然而,您的 VM 的方法实际上可能与我所描述的类似,在这种情况下,将其改编为机器代码编译实际上应该非常简单 - 只需翻译您的虚拟局部变量内存 space 入栈 space.
如果我正确理解你的问题,那么是的,你必须在这里使用 SP/BP 等。这就是编译为本机机器代码的含义:将程序的高级行为转换为等效的机器指令,这些指令遵循它所在的操作系统的约定 运行。
因此,如果您从汇编程序中调用主机提供的函数,您基本上必须执行与调用主机提供的函数相同的操作。这通常意味着将函数参数的值粘贴到适当的寄存器中/将它们压入堆栈,根据需要转换它们,然后生成 CALL 或 JMP 指令或任何 CPU 期望实际跳转到的内存地址给定的函数。
您需要有一个 table 函数名称到宿主提供给您的函数指针映射,然后从那里查找地址。
一旦函数 returns,如果需要,您可以将函数返回的值转换回您的内部类型,然后继续您的快乐之路。 (这基本上就是所有这些 "foreign function interface" 库在内部所做的事情)。
根据您的语言及其用途,也可以在此处作弊。您可以使用自己的内部伪堆栈,只需添加一个特殊的 "call a native function" 指令即可。该指令将接收有关函数的信息作为参数(例如,它的参数类型 takes/returns、如何查找函数指针),然后将使用外部函数接口库进行实际的函数调用。
这意味着调用本机函数会有轻微的开销,但意味着您可以保持 VM 原样,同时仍允许人们调用本机代码以与您的应用程序集成。