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

2024-11-13 08:36:00
admin
原创
22
摘要:问题描述:我想为一个我编写的玩具语言处理器(纯学术用途)编写一个非常小的概念验证 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 操作码可以将一些操作建模为优化的汇编。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用