JIT编译如何在运行时实际执行机器代码?

时间:2014-12-23 20:03:08

标签: compiler-construction compilation llvm jit machine-code

我理解JIT编译的工作原理(在阅读this SO question等资源之后)。但是,我仍然想知道它是如何在运行时执行机器代码的?

我没有深入的操作系统或编译器优化背景,也没有直接使用机器代码做任何事情,但我开始探索它。我已经开始在汇编中玩了,看看像NASM这样的东西如何能够获取汇编代码并将其编译为机器代码(可执行文件),然后你可以从./my-executable命令行“调用”它。 / p>

但JIT编译器在运行时实际上是如何做到的?是将流机器代码转换为stdin还是其他东西,或者它是如何工作的?如果你可以提供一个例子或一些伪代码来说明某些程序集(或那些沿着这些行的东西,而不是像C那样的高级程序)可能看起来可以展示基本流程,那也是很棒的。

3 个答案:

答案 0 :(得分:5)

你提到你玩过装配,所以你知道它是如何工作的,好的。想象一下,你编写了一个分配缓冲区的代码(例如:地址0x75612d39)。然后你的代码将程序集操作保存到该缓冲区以从堆栈中弹出一个数字,程序集调用打印函数来打印该数字,然后程序集返回"返回"。然后将数字3推入堆栈,并调用/跳转到地址0x75612d39。处理器将遵循打印您的号码的说明,然后再次返回您的代码,然后继续。在汇编级别,它实际上非常简单。

我不知道任何"真实"汇编语言,但这里是"样本"从我知道的字节码拼凑而成。该机器有2个字节的指针,字符串%s位于地址6a,函数printf位于地址1388

void myfunc(int a) {
    printf("%s", a);
}

此功能的程序集如下所示:

OP Params OpName     Description
13 82 6a  PushString 82 means string, 6a is the address of "%s"
                     So this function pushes a pointer to "%s" on the stack.
13 83 00  PushInt    83 means integer, 00 means the one on the top of the stack.
                     So this function gets the integer at the top of the stack,
                     And pushes it on the stack again
17 13 88 Call        1388 is printf, so this calls the printf function
03 02    Pop         This pops the two things we pushed back off the stack
02       Return      This returns to the calling code.

因此,当您的JITTER读入void myfunc(int a) {printf("%s", a);}时,它会为此函数分配内存(例如:地址0x75612d39),并将这些字节存储在该内存中:13 82 6a 13 83 00 17 13 88 03 02 02。然后,要调用该函数,它只需在地址0x75612d39处跳转/调用该函数。

答案 1 :(得分:2)

执行代码时,一切都归结为加载到已知内存部分的代码,程序计数器通过直接寄存器设置或jmp指令设置为代码的开头,或者类似。那么JIT编译器将要做的是在已知的内存部分中构建机器代码,然后从那里执行。

答案 2 :(得分:0)

我将尝试在@MooingDuck答案中进一步阐述。 让我们举一个Hello World代码的C#示例。

Path("path/to/current/file.foo").rename("path/to/new/destination/for/file.foo")

等效的汇编代码如下:

namespace Hello
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, world!");
        }
    }
}

(此代码摘自here)。

每条指令,以及数据本身,都可以用数字表示。现在,我可以将这些数字放入缓冲区中,告诉CPU到达缓冲区在内存中的位置,然后开始执行代码。对吧?

不是那么快。

正如您在this SO question中所看到的那样,它只有在将内存映射为可执行文件后才起作用。现在您可以强制转换为函数,并“调用”此内存。它将运行。

据我所知,总的来说,这或多或少是JITTER的工作方式:

  1. 读取IL
  2. 对其进行编译(即,确定执行该操作的操作码)
  3. 为它们分配内存并将其映射为可执行代码
  4. 调用此内存作为函数(通过强制转换或其他方式)