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

2024-11-13 08:36:00
admin
原创
171
摘要:问题描述:我想为一个我编写的玩具语言处理器(纯学术用途)编写一个非常小的概念验证 JIT 编译器,但我在设计的中期遇到了一些麻烦。从概念上讲,我熟悉 JIT 的工作原理 - 将字节码编译成(机器或汇编?)代码来运行。然而,在具体细节方面,我不太明白你实际上是如何做到这一点的。由于我根本不知道从哪里开始,所以我...

问题描述:

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

由于我根本不知道从哪里开始,所以我的(非常“新手”)下意识反应是尝试以下操作:

  1. mmap() 一块内存,设置对 PROT_EXEC 的访问

  2. 将本机代码写入块中

  3. 将当前寄存器(堆栈指针等)存储在某个舒适的地方

  4. 修改当前寄存器以指向映射区域中的本机代码块

  5. 本机代码现在将由机器执行

  6. 恢复先前的寄存器

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


解决方案 1:

不确定是否适用于 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
", arg1,arg2,arg1*arg2,res1 );

}

解决方案 2:

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

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

http://freshmeat.net/projects/libjit

解决方案 3:

如何 JIT - 介绍是一篇新文章(从今天开始!),它解决了其中一些问题并描述了更大的前景。

解决方案 4:

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

如果事情变得更加严重,那么看看 LLVM 也许也是一个不错的选择。

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

解决方案 5:

答案取决于您的编译器以及代码的放置位置。请参阅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 代码访问的数据需要声明为 volatile。

解决方案 6:

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

解决方案 7:

Linux x86mmap最小示例

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

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

主程序

#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

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

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

通过编译测试文件获取shell代码:

不是main.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/安全功能,因此我们可以(“不”安全地)忽略它:endbr64 指令实际上做什么?

有关的:

  • 在 C++ 中,是否可以在运行时动态创建一个函数?

  • 如何直接从内存编译并执行?

在 Ubuntu 23.10 amd64 上测试。

解决方案 8:

您可能对为什么幸运的僵尸使用Potion编程语言感兴趣。它是一种小型、不完整的语言,具有即时编译功能。Potion 的体积小,更容易理解。存储库包含该语言内部的描述(JIT 内容从标题“~jit ~ ”开始)。

由于它在Potion 的 VM上下文中运行,因此实现起来很复杂。不过,不要让这吓到你。你很快就能看出来他在做什么。基本上,使用一小组 VM 操作码可以将一些操作建模为优化的汇编。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用