我怎样才能在运行时生成并执行机器代码?

时间:2017-04-06 12:19:48

标签: assembly code-generation execution instructions

我最接近汇编的是构建我自己的Java类库,它加载类文件并允许您创建,编译和反编译类。在努力完成这个项目时,我想知道Java虚拟机在JIT优化期间如何在运行时实际生成本机机器代码。

它让我思考:如何生成机器代码并在运行时使用汇编执行它,作为奖励,没有JIT编译器库,或“手动”?

3 个答案:

答案 0 :(得分:7)

您的问题发生了重大变化(2017年7月)。最初的变体引用了IBM大型机的EX (execute) instruction

  

如何生成机器代码并在运行时使用程序集执行...?

在实践中,您将使用一些JIT compilation库,其中有很多。或者你会使用一些dynamic loader。在最低级别,它们都写入一些字节序列,表示有效machine code - 一系列机器指令 - 在一个内存段(您的virtual address space)中,必须进行executable(阅读NX bit),然后你的一些代码将间接跳转到该地址,或者更经常地间接调用它 - 即通过function pointer调用。大多数JVM实现使用JIT编译技术。

  

...作为奖励,没有JIT编译器库,或者"手动"?

假设您有一些有效的机器代码用于您的程序当前正在执行的处理器体系结构,例如,您可以获得一个内存段(例如Linux上的mmap(2)),然后使其可执行(例如{ {3}})。大多数其他mprotect(2)提供类似的operating systems

如果您使用JIT编译库,如system callsasmjitlibjitlibgccjit或许多其他人,您首先在内存中构建一个表示形式(类似于某些{ {3}})要生成的代码,然后请求JIT库为其发出机器代码。您甚至可以编写自己的JIT编译代码,但这是很多的工作(您需要了解LLVM的所有详细信息,例如PC的abstract syntax tree。顺便说一句,生成快速运行的机器代码确实很困难,因为您需要像instruction set那样x86(并关注optimizecompilers等详细信息...另见instruction scheduling),这就是为什么使用现有的JIT编译库(如register allocationthis)更为可取(相反,更简单的JIT库,如{{3} }或libgccjit或GNU LLVM不会优化太多并生成糟糕的机器代码。)

如果您使用asmjit(例如POSIX上的libjit),您将使用一些外部编译器来生成共享库(即lightning),然后您询问动态链接器在你的进程中加载​​它(并处理适当的dynamic loader)并从名称(使用dlopen(3))获取一些函数地址。

某些语言实现(特别是Common Lisp的plugin)能够在每次relocations交互时动态发送一些好的机器代码。本质上,它们的运行时启动了一个完整的编译器(包含一个JIT编译部分)。

Linux上经常dlsym(3)的一个技巧是在运行时在一些临时文件中发出一些C(或C ++)代码(即将一些特定于域的语言编译为C或C ++),分叉{{3}它作为一个插件,并动态加载它。使用当前(笔记本电脑,台式机,服务器)计算机,它足够快,可以与交互式循环保持兼容。

另请阅读SBCL(特别是着名的REPL书籍),usecompilationevalSICP,编译器( metaprogramming),Scott multistage programmingself-modifying code

答案 1 :(得分:4)

要执行一台x86计算机,请使用jmp指令跳转到其开头。请注意,CPU不知道代码的结束位置,因此您必须手动安排。更好的方法是使用call来调用该机器代码,然后在代码中的某处返回ret指令。

没有直接的方法只执行一条指令,因为这通常是毫无意义的。我不确定你想要达到的目的。

答案 2 :(得分:4)

在评论中,我向你a link提供了一个彻底解释事情的文件。

大多数汇编语言都有一个子程序(就谷歌搜索所涉及的汇编词而言)实现为两个命令callret - 可能类似。

实现与jump几乎相同,除了call在堆栈中存储下一个命令的地址,ret弹出它 - 这就是为什么维护它非常重要的原因子程序中的平衡堆栈。既然你不想弄乱可能包含重要内容/有限的寄存器,那么这就是你保留所有局部变量的地方,因此平衡是一个问题。 当然,您可以使用jump和一些推送和弹出来自行完成此操作。

就“参数”而言,一种简单的方法是使用寄存器。如果您需要传递比寄存器更多的参数,这是一个问题。一个更健壮的方法是在调用之前推送参数。  这就是许多真正的32位调用约定。我为子程序添加3个数字提供的链接示例:

# Save old EBP
pushl %ebp
# Change EBP
movl %esp, %ebp
# Save caller-save registers if necessary
pushl %ebx
pushl %esi
pushl %edi
# Allocate space for local variable
subl $4, %esp
# Perform the addition
movl 8(%ebp), %eax
addl 12(%ebp), %eax
addl 16(%ebp), %eax
movl %eax, -16(%ebp)
# Copy the return value to EAX
movl -16(%ebp), %eax
# Restore callee-save registers if necessary
movl -12(%ebp), %edi
movl -8(%ebp), %esi
movl -4(%ebp), %ebx
# Restore ESP
movl %ebp, %esp
# Restore EBP
popl %ebp
# Return to calling
ret

调用子程序:

# Save caller-save registers if necessary
pushl %eax
pushl %ecx
pushl %edx
# Push parameters
pushl $5
pushl $4
pushl $3
# Call add3
call add3
# Pop parameters
addl %12, %esp
# Save return value
movl %eax, wherever
# Restore caller-save registers if necessary
popl %edx
popl %ecx
popl %eax
# Proceed!

正如您所看到的,您需要更多的工作,然后是高级语言。 pdf包含详细说明,包括堆栈的工作原理,但请注意:

  1. 您需要定义如何处理寄存器使用情况。在这个例子中,调用者和子例程都保存寄存器,以防万一 - 你当然可以简化。
  2. 参数和局部变量相对于堆栈指针进行寻址,本地化为正,参数为负。
  3. 如果这是你为自己制作的一件小事,你可以跳过所有这些堆栈播放,只需留出参数和返回值转移的寄存器,也许在你去更高级的东西之前练习。