在程序集中编写JIT编译器

时间:2011-02-17 23:23:49

标签: c assembly code-generation jit self-modifying

我在C中编写了一个虚拟机,它对非JIT VM有很好的性能,但我想学习一些新的东西,并提高性能。我当前的实现只是使用一个开关从VM字节码转换为指令,并将其编译为跳转表。就像我说的那样,它的性能不错,但是我遇到了一个只能用JIT编译器克服的障碍。

我不久前已经问过一个类似的问题,关于自修改代码,但我开始意识到我没有问正确的问题。

所以我的目标是为这个C虚拟机编写一个JIT编译器,我想在x86汇编中完成它。 (我使用NASM作为我的汇编程序)我不太确定如何去做这个。我对汇编感到满意,并且我已经查看了一些自我修改的代码示例,但我还没有弄清楚如何进行代码生成。

到目前为止,我的主要块是将指令复制到可执行的内存块,带有我的参数。我知道我可以在NASM中标记某一行,并使用静态参数从该地址复制整行,但这不是非常动态的,并且不适用于JIT编译器。我需要能够解释字节码中的指令,将其复制到可执行内存,解释第一个参数,将其复制到内存,然后解释第二个参数,并将其复制到内存中。

我已经了解了几个可以简化此任务的库,例如GNU闪电,甚至是LLVM。但是,在使用外部资源之前,我想首先手动编写,以了解它是如何工作的。

此社区可以提供任何资源或示例来帮助我开始执行此任务吗?一个简单的例子显示了两个或三个指令,例如“add”和“mov”用于生成可执行代码,带有参数,动态地,在内存中,会产生奇迹。

2 个答案:

答案 0 :(得分:18)

我不建议在汇编中编写JIT。在汇编中编写解释器中最常执行的位有很好的理由。有关这种情况的示例,请参阅LuaJIT的作者comment from Mike Pall

至于JIT,有许多不同层次,复杂程度各不相同:

  1. 只需复制解释器的代码即可编译基本块(一系列非分支指令)。例如,一些(基于寄存器的)字节码指令的实现可能如下所示:

    ; ebp points to virtual register 0 on the stack
    instr_ADD:
        <decode instruction>
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        <dispatch next instruction>
    instr_SUB:
        ... ; similar
    

    因此,给定指令序列ADD R3, R1, R2SUB R3, R3, R4,一个简单的JIT可以将解释器实现的相关部分复制到一个新的机器代码块中:

        mov ecx, 1
        mov edx, 2
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        mov ecx, 3
        mov edx, 4
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        sub eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
    

    这只是复制相关代码,因此我们需要初始化相应使用的寄存器。更好的解决方案是将其直接转换为机器指令mov eax, [ebp + 4],但现在您必须手动编码所请求的指令。

    这种技术消除了解释的开销,但不会提高效率。如果代码只执行了一两次,那么首先将其转换为机器代码可能是不值得的(这需要至少刷新部分I-cache)。

  2. 虽然有些JIT使用上述技术而不是解释器,但它们对频繁执行的代码采用了更复杂的优化机制。这涉及将执行的字节码转换为中间表示(IR),在该中间表示上执行附加的优化。

    根据源语言和JIT的类型,这可能非常复杂(这就是许多JIT将此任务委派给LLVM的原因)。基于方法的JIT需要处理连接控制流图,因此他们使用SSA表单并对其进行各种分析(例如,Hotspot)。

    跟踪JIT(如LuaJIT 2)只编译直线代码,这使得许多事情更容易实现,但是你必须非常小心地选择跟踪以及如何有效地将多条跟踪链接在一起。 Gal和Franz在this paper (PDF)中描述了一种方法。有关另一种方法,请参阅LuaJIT源代码。两个JIT都是用C语言编写的(或者也许是C ++)。

答案 1 :(得分:7)

我建议您查看项目http://code.google.com/p/asmjit/。通过使用它提供的框架,您可以节省大量精力。如果你想手工编写所有东西,只需阅读源代码并自己重写,我认为这不是很难。