【问题标题】:I'm writing my own JIT-interpreter. How do I execute generated instructions?我正在编写自己的 JIT 解释器。如何执行生成的指令?
【发布时间】:2012-01-26 08:36:50
【问题描述】:

我打算编写自己的 JIT 解释器作为 VM 课程的一部分。我对高级语言、编译器和解释器有很多知识,但对 x86 汇编(或 C 语言)知之甚少。

实际上我不知道 JIT 是如何工作的,但这是我的看法:用某种中间语言读入程序。将其编译为 x86 指令。确保最后一条指令返回到 VM 代码中正常的地方。将指令存储在内存中的某个位置。无条件跳转到第一条指令。瞧!

因此,考虑到这一点,我有以下小型 C 程序:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int *m = malloc(sizeof(int));
    *m = 0x90; // NOP instruction code

    asm("jmp *%0"
               : /* outputs:  */ /* none */
               : /* inputs:   */ "d" (m)
               : /* clobbers: */ "eax");

    return 42;

}

好的,所以我的目的是让这个程序将 NOP 指令存储在内存中的某个位置,然后跳转到那个位置,然后可能会崩溃(因为我没有设置任何方法让程序返回到 main)。

问题:我走对了吗?

问题:你能告诉我一个修改后的程序,它设法找到回到 main 内部某个地方的路吗?

问题:我应该注意的其他问题?

PS:我的目标是获得理解,并不一定要以正确的方式做每一件事。


感谢所有反馈。以下代码似乎是在我的 Linux 机器上开始和工作的地方:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

unsigned char *m;

int main() {
        unsigned int pagesize = getpagesize();
        printf("pagesize: %u\n", pagesize);

        m = malloc(1023+pagesize+1);
        if(m==NULL) return(1);

        printf("%p\n", m);
        m = (unsigned char *)(((long)m + pagesize-1) & ~(pagesize-1));
        printf("%p\n", m);

        if(mprotect(m, 1024, PROT_READ|PROT_EXEC|PROT_WRITE)) {
                printf("mprotect fail...\n");
                return 0;
        }

        m[0] = 0xc9; //leave
        m[1] = 0xc3; //ret
        m[2] = 0x90; //nop

        printf("%p\n", m);


asm("jmp *%0"
                   : /* outputs:  */ /* none */
                   : /* inputs:   */ "d" (m)
                   : /* clobbers: */ "ebx");

        return 21;
}

【问题讨论】:

  • 另一种选择是只解释指令或中间代码,而无需直接执行任何操作。
  • @Alex:这是实现语言的另一种选择,但根据定义,它不是 JIT。

标签: c assembly x86


【解决方案1】:

问题:我是否走在正确的道路上?

我会说是的。

问题:你能告诉我一个修改后的程序,它设法回到 main 内部的某个地方吗?

我没有任何代码给你,但是获取生成的代码并返回的更好方法是使用一对call/ret 指令,因为它们会自动管理返回地址。

问题:我应该注意的其他问题?

是的 - 作为一种安全措施,许多操作系统会阻止您在未进行特殊安排的情况下在堆上执行代码。这些特殊安排通常意味着您必须将相关内存页面标记为可执行。

在 Linux 上,这是使用 mprotect() 和 PROT_EXEC 完成的。

【讨论】:

  • 另外,指令缓存一般不会监控底层内存,因此在执行跳转前可能需要显式刷新缓存。
  • @Simon:同意,一般来说这是“在你完成将指令写入内存之后,但在执行之前”。因此,根据我的经验,您编写代码是在您完成编写之后执行它,而不是在您执行之前。在这个示例代码中,它们是同一个地方,但实际上您执行的次数可能比您编写的次数多。重要的是,正如 Simon 所说,刷新指令缓存而不是数据缓存。
  • x86 上不需要刷新 I 缓存,SMC 仅在非常接近 eip 时不起作用。适度关闭 SMC 将处以巨额罚款。
  • 有一个关于自我修改代码的问题,我记得你必须在对齐的边界上映射才能访问该内存。任何人都知道那张票在哪里,那应该会告诉你如何在内存中生成指令然后执行它们。
【解决方案2】:

如果您生成的代码遵循正确的calling convention,那么您可以声明一个指向函数的类型并以这种方式调用该函数:

typedef void (*generated_function)(void);

void *func = malloc(1024);
unsigned char *o = (unsigned char *)func;
generated_function *func_exec = (generated_function *)func;

*o++ = 0x90;     // NOP
*o++ = 0xcb;     // RET

func_exec();

【讨论】:

    猜你喜欢
    • 2012-06-05
    • 1970-01-01
    • 2012-02-06
    • 1970-01-01
    • 1970-01-01
    • 2012-05-23
    • 1970-01-01
    • 2012-08-21
    • 1970-01-01
    相关资源
    最近更新 更多