Learn · concepts
什么是 JVM 字节码?Java 类文件详解
JVM 字节码是 Java 虚拟机运行的紧凑指令集。 了解 .class 文件的运作方式,以及 mod 为什么会重写字节码,以及 ASM 在其中的作用.
什么是 JVM 字节码?
JVM 字节码是 Java 虚拟机执行的指令集。当您编译 Java 源代码时,编译器不会为您的 CPU 生成机器码。它会生成字节码,这是一种紧凑、可移植的格式,存储在 .class 文件中。JVM 在运行时读取这些指令,并将其转换为本机代码。
这种分层方法——源代码到字节码到机器码——是 Java 具有可移植性的原因。相同的 .class 文件可以在 Windows、macOS、Linux 或任何具有兼容 JVM 的系统上运行,因为 JVM 是知道如何在本地机器上执行指令的层。编写一次,随处运行。
源代码如何转换为字节码
.java 文件是人编写的文本。.class 文件是 JVM 运行的编译输出。编译器读取您的源代码,对其进行检查,并为每个类写入一个 .class 文件,每个文件包含字节码指令以及常量池、方法表和其他元数据。
字节码不绑定到任何一个处理器。相同的 .class 文件可以在 Windows、macOS、Linux 或任何具有 Java 运行时环境的系统上运行,因为 JVM 是知道如何在本地机器上执行指令的层。这种可移植性是该格式的整个重点。
指令是什么样子的
字节码是一种基于栈的指令集。大多数操作将值压入操作数栈,然后弹出它们,并压入一个结果。无需考虑寄存器分配;该模型的设计是故意简单的,以便 JVM 可以快速验证和执行它。
以下是一些示例操作码,以供参考:
| 操作码 | 功能 |
|---|---|
iload | 将一个 int 局部变量压入栈 |
iadd | 弹出两个 int,压入它们的和 |
invokevirtual | 调用实例方法 |
getfield | 读取实例字段 |
return | 从方法返回 |
您很少手动读取或写入这些操作码。工具会生成和使用它们,而像 ASM 这样的库就应运而生。
为什么字节码对模组很重要
字节码很重要,因为它允许您在没有原始源代码的情况下更改行为。一个 Minecraft 模组几乎从不拥有游戏的源代码。它拥有的是磁盘上的编译游戏。为了添加或修改功能,模组会读取现有的字节码,重写它,并将结果反馈给 JVM,作为类加载的一部分。
这就是修补框架如何将钩子插入到它们不拥有的方法中的方式。它们在编译方法的正确位置找到一个点,并插入对模组代码的调用。在字节码级别工作意味着模组可以附加到游戏,即使它从未见过任何原始 Java 代码.
实践中,Terminus和其他基于Fabric的客户端正是这样修改Minecraft的。它们没有Minecraft的源代码,而是处理编译后的.class文件,并重写字节码以添加模块、设置屏幕和自定义渲染等功能。
ASM 的作用
ASM 是一个用于读取、写入和转换字节码的 Java 库。它可以将 .class 文件解析成事件或树状结构,允许代码修改指令,然后输出一个有效的 .class 文件。它体积小、速度快,并且直接操作原始格式,因此更高级的模组工具会在此基础上构建,而不是自行解析类文件。
大多数模组制作者从不直接调用 ASM。他们使用更高级的层,该层隐藏了操作码,并提供了一种更友好的方式来表达“在方法开始时运行我的代码”。底层,该层通常通过 ASM 编辑字节码.
反编译字节码回源代码
像 Fernflower 和 CFR 这样的反编译器,会从编译后的 .class 文件中重建可读的 Java 代码。这个过程并不完美:一些名称和注释会丢失,某些结构比其他结构更难恢复,而混淆的代码会变得不可读。但对于普通的、未混淆的代码,反编译器通常会产生足够接近原始代码的输出,以便理解和使用。
这对于学习游戏的工作原理很有用,但不能替代拥有实际源代码。反编译后的代码仍然是缺乏注释的编译逻辑,并且重建的变量名可能会产生误导。它是一个阅读工具,而不是一个编写工具。
常见问题解答
机器码是针对特定 CPU 的原生指令。字节码是 JVM 自己的指令集,JVM 在运行时将其翻译成机器码。这个额外的层级使得 .class 文件可以在任何平台上运行.
部分地。反汇编器能够从字节码中重建可读的 Java 代码,并且对于普通的代码,它们能做到非常接近。结果并不总是与原始代码完全相同,注释和一些局部名称会丢失,但通常足以追踪.
由于模组通常没有源代码,它们是基于已编译的游戏进行发布的。重写字节码可以让模组修改行为,而无需触及或拥有原始的 Java 文件.
通常不是。模组框架和脚本层会为你处理字节码工作。了解其工作原理在调试棘手的补丁或构建工具时会很有帮助.