我如何在运行时生成和执行机器码?

How could I generate and execute machine code at runtime?

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

这让我开始思考:如果没有 JIT 编译器库或 "manually",如何生成机器代码并在运行时使用汇编执行它,作为奖励,又如何呢?

要执行一段x86机器,使用jmp指令跳转到它的开头。请注意,CPU 不知道代码在哪里结束,因此您必须进行手动安排。更好的方法是使用 call 调用该机器代码,然后在代码中的某处使用 ret 指令调用 return。

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

在我给你的评论中 a link 一个彻底解释事情的文件。

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

实现与 jump 几乎相同,只是 call 将下一个命令的地址存储在堆栈中,而 ret 弹出它 - 这就是它非常重要的原因在子程序中保持堆栈平衡。由于您不想弄乱可能包含重要 stuff/are 限制的寄存器,这是您保存所有局部变量的地方,因此平衡是一个问题。 您当然可以使用 jump 和一些推动和弹出来自己完成此操作。

就"arguments"而言,一个简单的方法是使用寄存器。如果您需要传递比寄存器更多的参数,这就是一个问题。一种更健壮的方法是在调用之前推送参数。 这就是许多真正的 32 位调用约定所做的。来自 link 的示例我提供了一个添加 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 , %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 
pushl 
pushl 
# 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. 如果这是你自己做的一件小事,你可以跳过所有这些堆栈播放,只为参数和 return 值传输留出寄存器,也许在你进入更高级的东西之前练习一下.

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

how could one generate machine code and execute it at runtime with assembly...?

在实践中,您会使用一些 JIT compilation library, and there are many of them. Or you would use some dynamic loader. At the lowest level, they all write some byte sequences representing valid machine code—a sequence of many machine instructions—in a memory segment (of your virtual address space) which has to be made executable (read about the NX bit), and then some of your code would jump indirectly to that address or more often call it indirectly—that is call through a function pointer. Most JVM 实现使用 JIT 编译技术。

...and as a bonus, without a JIT compiler library, or "manually"?

例如,假设您的程序当前正在执行的处理器架构有一些有效的机器代码,您可以获得一个内存段(例如 mmap(2) on Linux), and then make it executable (e.g. mprotect(2)). Most other operating systems provide similar system calls.


如果您使用像 asmjit or libjit or libgccjit or LLVM or many others, you first construct in memory a representation (similar to some abstract syntax tree) of the code to be generated, then ask the JIT library to emit machine code for it. You could even write your own JIT compilation code, but it is a lot of work (you need to understand all the details of your instruction set, e.g. x86 for PCs). By the way, generating fast-running machine code is really difficult, because you need to optimize like compilers do (and to care about details like instruction scheduling, register allocation, etc... see also this), and that is why using an existing JIT compilation library (like libgccjit or LLVM) is preferable (a contrario, simpler JIT libraries like asmjit or libjit or GNU lightning 这样的 JIT 编译库,请不要优化太多并生成糟糕的机器代码。

如果你使用 dynamic loader (e.g. dlopen(3) on POSIX) you would use some external compiler to produce a shared library (that is a plugin) and then you ask the dynamic linker to load it in your process (and handle appropriate relocations) and get by name (using dlsym(3)) 一些函数地址。

一些语言实现(特别是 SBCL for Common Lisp) are able to emit on the fly some good machine code at every REPL 交互。本质上,它们的运行时启动了一个完整的编译器(包含 JIT 编译部分)。

一个技巧我经常use on Linux is to emit some C (or C++) code at runtime in some temporary file (that is compiling some domain specific language to C or to C++), fork a compilation把它当成插件,动态加载它。对于当前的(笔记本电脑、台式机、服务器)计算机,它的速度足以与交互式循环保持兼容。

另请参阅 eval (in particular the famous SICP book), metaprogramming, multistage programming, self-modifying code, continuations, compilers (the Dragon Book), Scott's Programming Language Pragmatics, and J.Pitrat's blog