如何将特定于VM的代码编译为x86机器代码?

时间:2017-03-02 00:10:51

标签: compiler-construction virtual-machine executable jit machine-code

我有一个16位的基于寄存器的虚拟机,我想知道将它编译为实际的x86机器代码的步骤是什么?我不打算创建一个JIT编译器,除非必须能够将编译的代码与另一个可执行文件/ DLL链接。

虚拟机使得如果将VM添加到项目中,则可以添加特殊语言构造。 (例如,如果它嵌入到游戏引擎中,则可能会添加“实体”对象类型,并且可能会暴露引擎中的多个C函数。)这将导致代码完全依赖于某些公开的C函数或暴露的C ++类,在它嵌入的应用程序中。

如果将脚本代码从VM字节码编译为本机EXE,那么这种“链接”怎么可能?

它也像Lua的VM一样基于寄存器,因为所有基本变量都存储在“寄存器”中,这是一个巨大的C数组。当范围改变时,寄存器指针递增或递减,因此寄存器编号是相对的,类似于堆栈指针。 E.g:

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 个答案:

答案 0 :(得分:1)

如果我理解你的问题,那么是的,你必须在这里使用SP / BP等。这就是编译本机代码的意思:将程序的高级行为转换为遵循运行的操作系统约定的等效机器指令。

因此,如果您从汇编程序调用它们,那么您基本上必须执行相同的操作来调用主机提供的函数。这通常意味着将函数参数的值粘贴在适当的寄存器中/将它们推入堆栈,根据需要进行转换,然后生成CALL或JMP指令或CPU期望实际跳转到给定函数的内存地址的任何内容。

你需要有一个函数名表来实现主机为你提供的函数指针映射,并从那里查看地址。

一旦函数返回,你可以根据需要将函数返回的值转换回你的内部类型,然后继续你的快乐方式。 (这基本上就是所有那些“外部函数接口”库在内部执行的操作)。

根据您的语言及其用途,也可能在这里作弊。您可以使用自己的内部伪堆栈,只需添加一个特殊的“调用本机函数”指令。该指令将接收有关函数的信息作为参数(例如,它采取/返回的参数类型,如何查找函数指针),然后使用外部函数接口库进行实际的函数调用。

这意味着调用本机函数会产生轻微的开销,但这意味着您可以按原样保留VM,同时仍允许人们调用本机代码以与您的应用程序集成。

答案 1 :(得分:0)

  

虚拟机使得如果将VM添加到项目中,则可以添加特殊语言构造。 (例如,如果它嵌入到游戏引擎中,则可能会添加" Entity"对象类型,并且可能会暴露引擎中的几个C函数。)这将导致代码完全依赖于某些暴露的C函数或暴露的C ++类,在它嵌入的应用程序中。

有很多方法可以实现这种跨语言接口。除非您需要一个开销非常低的接口,否则您是否正在运行VM字节码或本机机器码并不重要。主要考虑因素是您的语言的性质 - 尤其是它是否具有静态或动态类型。

一般来说,这两种最常见的方法(您可能已经熟悉它们):

  • (a)' 外部功能界面'方法,您的语言/运行时提供自动包装来自C的函数和数据的工具。示例包括LuaJIT FFIjs-ctypesP/Invoke。大多数FFI可以在CDECL / STDCALL功能和POD结构上运行;有些人对C ++或COM类有不同程度的支持。

  • (b)' 运行时API '方法,您的运行时公开可用于手动构造/操作对象以供您的语言使用的C API。 Lua有一个广泛的API(example)和Python一样。

  

这种"如何连接"如果脚本代码是从VM字节码编译成本机EXE吗?

所以你可能正在考虑如何例如将外部函数地址烘焙到生成的机器代码中。好吧,如果你有适当的FFI基础设施,只要你知道共享库导入是如何工作的(导入地址表,重定位,修理等。)。

如果您对共享库了解不多,我想通过花一些时间研究该领域,您将开始更清楚地了解在编译器中实现FFI的方法。 / p>

但是,如果采用稍微更动态的方法可能更容易,例如:LoadLibrary()GetProcAddress(),则将函数指针包装为您的语言对象。

很遗憾,在不了解有关语言/ VM的情况下,很难提供更具体的建议。

  

[...]关于这部分的问题是,我是否必须使用堆栈指针进行此类操作?基指针?我可以用什么来取代这个概念?

我不完全确定这个注册阵列的目的是什么?'计划是。

在一个有词法范围的语言中,我的理解是,在编译函数时,您通常会枚举其体内声明的每个变量,并分配一个足够大的堆栈空间块来容纳所有变量(忽略复杂的主题) CPU寄存器分配)。代码可以使用堆栈指针或(更常见的)基指针来解决这些变量。

如果内部作用域中的变量影响外部作用域中的变量(如示例),则它们会在堆栈上分配单独的内存空间 - 因为就编译器而言,它们是不同的变量。

如果不了解虚拟机使用的任何方案背后的基本原理,我无法真正建议它应该如何转换为机器代码。也许有更多编程字节码编译经验的人可以给你你的答案。

然而,您的虚拟机方法实际上可能类似于我所描述的方法,在这种情况下,将其用于机器代码编译实际上应该非常简单 - 只需翻译您的虚拟本地 - 将可变内存空间放入堆栈空间。