如何动态生成并运行本机代码?

发布于 2024-10-16 08:28:29 字数 541 浏览 4 评论 0原文

我想为我编写的玩具语言处理器编写一个非常小的概念验证 JIT 编译器(纯粹学术性的),但我在设计的中间高度遇到了一些麻烦。从概念上讲,我熟悉 JIT 的工作原理 - 将字节码编译成(机器或汇编?)代码来运行。然而,在具体细节上,我不太清楚如何你实际上这样做

我的(非常“新手”)下意识的反应,因为我不知道从哪里开始,所以会尝试如下所示的操作:

  1. mmap() 一块内存,设置对 PROT_EXEC 的访问,
  2. 将本机代码写入该块
  3. 将当前寄存器(堆栈指针等)存储在某个地方,方便地
  4. 修改当前寄存器以指向映射区域中的本机代码块
  5. 本机代码现在将由机器执行
  6. 恢复以前的寄存器

是否甚至接近到正确的算法吗?我尝试过仔细研究我知道有 JIT 编译器可供研究的不同项目(例如 V8)但这些代码库由于其大小而难以使用,而且我不知道从哪里开始寻找。

I'd like to write a very small proof-of-concept JIT compiler for a toy language processor I've written (purely academic), but I'm having some trouble in the middle-altitudes of design. Conceptually, I'm familiar with how JIT works - you compile bytecode into (machine or assembly?) code to run. At the nuts-and-bolts level however, I'm not quite gripping how you actually go about doing that.

My (very "newb") knee-jerk reaction, since I haven't the first clue where to start, would be to try something like the following:

  1. mmap() a block of memory, setting access to PROT_EXEC
  2. write the native code into the block
  3. store the current registers (stack pointer, et al.) someplace cozy
  4. modify the current registers to point into the native code block in the mapped region
  5. the native code would now get executed by the machine
  6. restore the previous registers

Is that even close to a/the correct algorithm? I've tried perusing different projects that I know have JIT compilers to study (such as V8) but these codebases turn out to be difficult to consume because of their size, and I've little idea where to start looking.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

清旖 2024-10-23 08:28:29

不确定 Linux 是否如此,但这适用于 x86/windows。
更新:http://codepad.org/sQoF6kR8

#include <stdio.h>
#include <windows.h>

typedef unsigned char byte;

int arg1;
int arg2;
int res1;

typedef void (*pfunc)(void);

union funcptr {
  pfunc x;
  byte* y;
};

int main( void ) {

  byte* buf = (byte*)VirtualAllocEx( GetCurrentProcess(), 0, 1<<16, MEM_COMMIT, PAGE_EXECUTE_READWRITE );

  if( buf==0 ) return 0;

  byte* p = buf;

  *p++ = 0x50; // push eax
  *p++ = 0x52; // push edx

  *p++ = 0xA1; // mov eax, [arg2]
  (int*&)p[0] = &arg2; p+=sizeof(int*);

  *p++ = 0x92; // xchg edx,eax

  *p++ = 0xA1; // mov eax, [arg1]
  (int*&)p[0] = &arg1; p+=sizeof(int*);

  *p++ = 0xF7; *p++ = 0xEA; // imul edx

  *p++ = 0xA3; // mov [res1],eax
  (int*&)p[0] = &res1; p+=sizeof(int*);

  *p++ = 0x5A; // pop edx
  *p++ = 0x58; // pop eax
  *p++ = 0xC3; // ret

  funcptr func;
  func.y = buf;

  arg1 = 123; arg2 = 321; res1 = 0;

  func.x(); // call generated code

  printf( "arg1=%i arg2=%i arg1*arg2=%i func(arg1,arg2)=%i\n", arg1,arg2,arg1*arg2,res1 );

}

Not sure about linux, but this works on x86/windows.
Update: http://codepad.org/sQoF6kR8

#include <stdio.h>
#include <windows.h>

typedef unsigned char byte;

int arg1;
int arg2;
int res1;

typedef void (*pfunc)(void);

union funcptr {
  pfunc x;
  byte* y;
};

int main( void ) {

  byte* buf = (byte*)VirtualAllocEx( GetCurrentProcess(), 0, 1<<16, MEM_COMMIT, PAGE_EXECUTE_READWRITE );

  if( buf==0 ) return 0;

  byte* p = buf;

  *p++ = 0x50; // push eax
  *p++ = 0x52; // push edx

  *p++ = 0xA1; // mov eax, [arg2]
  (int*&)p[0] = &arg2; p+=sizeof(int*);

  *p++ = 0x92; // xchg edx,eax

  *p++ = 0xA1; // mov eax, [arg1]
  (int*&)p[0] = &arg1; p+=sizeof(int*);

  *p++ = 0xF7; *p++ = 0xEA; // imul edx

  *p++ = 0xA3; // mov [res1],eax
  (int*&)p[0] = &res1; p+=sizeof(int*);

  *p++ = 0x5A; // pop edx
  *p++ = 0x58; // pop eax
  *p++ = 0xC3; // ret

  funcptr func;
  func.y = buf;

  arg1 = 123; arg2 = 321; res1 = 0;

  func.x(); // call generated code

  printf( "arg1=%i arg2=%i arg1*arg2=%i func(arg1,arg2)=%i\n", arg1,arg2,arg1*arg2,res1 );

}
悍妇囚夫 2024-10-23 08:28:29

您可能想看看libjit,它提供了您正在寻找的基础设施:

libjit 库实现
即时编译
功能。与其他 JIT 不同的是,这
一个被设计为独立于
任何特定的虚拟机
字节码格式或语言。

http://freshmeat.net/projects/libjit

Youmay want to have a look at libjit which provides exactly the infrastructure you're looking for:

The libjit library implements
just-in-time compilation
functionality. Unlike other JITs, this
one is designed to be independent of
any particular virtual machine
bytecode format or language.

http://freshmeat.net/projects/libjit

一瞬间的火花 2024-10-23 08:28:29

如何 JIT - 简介 是新文章(从今天开始!)解决了其中一些问题并描述了更大的情况。

How to JIT - an introduction is a new article (from today!) that addresses some of these issues and describes the bigger picture as well.

羞稚 2024-10-23 08:28:29

Android Dalvik JIT 编译器也可能值得关注。它应该相当小且精简(不确定这是否有助于理解它或使事情变得更加复杂)。它也针对 Linux。

如果事情变得更严重,看看 LLVM 可能也是一个不错的选择。

Jeremiah 建议的函数指针方法听起来不错。无论如何,您可能想使用调用者的堆栈,并且可能只剩下一些寄存器(在 x86 上),您需要保留或不触摸它们。在这种情况下,如果您编译的代码(或条目存根)在继续之前将它们保存在堆栈上,这可能是最简单的。最后,这一切都归结为编写一个汇编函数并从 C 接口到它。

The Android Dalvik JIT compiler might also be worth looking at. It is supposed to be fairly small and lean (not sure if this helps understanding it or makes things more complicated). It targets Linux as well.

If things are getting more serious, looking at LLVM might be a good choice as well.

The function pointer approach suggested by Jeremiah sounds good. You may want to use the caller's stack anyway and there will probably only be a few registers left (on x86) which you need to preserve or not touch. In this case, it is probably easiest if your compiled code (or the entry stub) saves them on the stack before proceeding. In the end, it all boils down to writing an assembler function and interfacing to it from C.

三五鸿雁 2024-10-23 08:28:29

答案取决于您的编译器以及放置代码的位置。请参阅http: //encode.ru/threads/1273-Just-In-Time-Compilation-Improvement-For-ZPAQ?p=24902&posted=1#post24902

在 32 位 Vista 中测试,Visual C++ 给出了 DEP(数据无论代码是放在堆栈、堆还是静态内存中,都会出现执行预防)错误。 g++、Borland 和 Mars 有时可以工作。 JIT 代码访问的数据需要声明为易失性的。

The answer depends on your compiler and where you put the code. See http://encode.ru/threads/1273-Just-In-Time-Compilation-Improvement-For-ZPAQ?p=24902&posted=1#post24902

Testing in 32 bit Vista, Visual C++ gives a DEP (data execution prevention) error whether the code is put on the stack, heap, or static memory. g++, Borland, and Mars can be made to work sometimes. Data accessed by the JIT code needs to be declared volatile.

因为看清所以看轻 2024-10-23 08:28:29

除了到目前为止建议的技术之外,研究线程创建函数可能也是值得的。如果您创建一个新线程,并将起始地址设置为生成的代码,您可以确定没有需要保存或恢复的旧寄存器,并且操作系统会为您处理相关寄存器的设置。也就是说,您消除了列表中的步骤 3、4 和 6。

In addition to the techniques suggested so far, it might be worthwhile to look into the thread creation functions. If you create a new thread, with the starting address set to your generated code, you know for sure that there are no old registers that need saving or restoring, and the OS handles the setup of the relevant registers for you. I.e you eliminate steps 3, 4 and 6 of your list.

半世蒼涼 2024-10-23 08:28:29

Linux x86 mmap 最小示例

只是为了提供一个带有 mmap 的 Linux。在运行时,我向内存中注入一个相当于以下内容的函数:

int ing(int i) {
    return i + 1;
}

main.c

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <stddef.h> /* NULL */
#include <sys/mman.h> /* mmap, munmap */

union funcptr {
    int (*f)(int);
    unsigned char *bytes;
};

int main(void) {
    unsigned char *buf = (unsigned char *)mmap(NULL, 4, PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    assert(buf != MAP_FAILED);
    unsigned char *p = buf;

    // return i + 1;
    // lea 0x1(%rdi),%eax
    *p++ = 0x8d;
    *p++ = 0x47;
    *p++ = 0x01;

    // ret
    *p++ = 0xC3;

    assert(((union funcptr){ .bytes = buf }).f(1) == 2);

    // Just to check if we can modify the code witout any explicit icache flushing.
    // return i + 2;
    // lea 0x1(%rdi),%eax
    buf[2] = 0x02;

    assert(((union funcptr){ .bytes = buf }).f(1) == 3);

    int ret = munmap(buf, 4);
    assert(!ret);
}

编译并运行:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main main.c
./main

我使用 unionhttps://stackoverflow.com/a/4912662/895245 因为 C 标准显然禁止将非函数指针转换为函数指针:ISO C Void * 和函数指针 这有点家长式,如果你忽略警告,GCC 也可以直接使用:

assert(((int (*)(int))(buf))(1) == 2);

The shell code was returned by compiling一个测试文件:

notmain.c

int inc(int i) {
    return i + 1;
}

-O3 并反汇编它:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -c -o notmain.o notmain.c
objdump -d notmain.o

输出包含:

0000000000000000 <inc>:
   0:   f3 0f 1e fa             endbr64
   4:   8d 47 01                lea    0x1(%rdi),%eax
   7:   c3                      ret

endbr64 是一个 NOP/安全功能,所以我们可以 ("un"safely )忽略它: endbr64 指令实际上是做什么的?

相关:

在Ubuntu 23.10 amd64上测试。

Linux x86 mmap minimal example

Just to provide a Linux one with mmap. At runtime I inject into memory a function equivalent to:

int ing(int i) {
    return i + 1;
}

main.c

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <stddef.h> /* NULL */
#include <sys/mman.h> /* mmap, munmap */

union funcptr {
    int (*f)(int);
    unsigned char *bytes;
};

int main(void) {
    unsigned char *buf = (unsigned char *)mmap(NULL, 4, PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    assert(buf != MAP_FAILED);
    unsigned char *p = buf;

    // return i + 1;
    // lea 0x1(%rdi),%eax
    *p++ = 0x8d;
    *p++ = 0x47;
    *p++ = 0x01;

    // ret
    *p++ = 0xC3;

    assert(((union funcptr){ .bytes = buf }).f(1) == 2);

    // Just to check if we can modify the code witout any explicit icache flushing.
    // return i + 2;
    // lea 0x1(%rdi),%eax
    buf[2] = 0x02;

    assert(((union funcptr){ .bytes = buf }).f(1) == 3);

    int ret = munmap(buf, 4);
    assert(!ret);
}

Compile and run:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main main.c
./main

I use an union as in https://stackoverflow.com/a/4912662/895245 because the C standard apparently forbids casting non function pointers to function pointers: ISO C Void * and Function Pointers This is a bit paternalistic, and if you ignore the warning GCC can also just cast directly with:

assert(((int (*)(int))(buf))(1) == 2);

The shell code was obtained by compiling a test file:

notmain.c

int inc(int i) {
    return i + 1;
}

with -O3 and disassembling it:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -c -o notmain.o notmain.c
objdump -d notmain.o

and the output contains:

0000000000000000 <inc>:
   0:   f3 0f 1e fa             endbr64
   4:   8d 47 01                lea    0x1(%rdi),%eax
   7:   c3                      ret

The endbr64 is a NOP/security feature, so we can ("un"safely) ignore it: What does the endbr64 instruction actually do?

Related:

Tested on Ubuntu 23.10 amd64.

む无字情书 2024-10-23 08:28:29

您可能对为什么幸运僵硬Potion 编程语言。它是一种小型、不完整的语言,具有即时编译的特点。 Potion 的尺寸较小,更容易理解。该存储库包含语言内部结构的描述(JIT 内容从标题开始“~这个家伙~”)。

由于它在 Potion 的虚拟机。不过,不要让这吓跑你。不用多久就能看出他在做什么。基本上,使用一小组 VM 操作码允许将某些操作建模为 优化装配

You may be interested in why the lucky stiff's Potion programming language. It's a small, incomplete language that features just-in-time compilation. Potion's small size makes it easier to understand. The repository includes a description of the language's internals (JIT content starts at heading "~ the jit ~").

The implementation is complicated by the fact it runs in the context of Potion's VM. Don't let this scare you off, though. It doesn't take long to see what he's up to. Basically, using a small set of VM opcodes allows some actions to be modeled as optimized assembly.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文