Learn · concepts

什么是 JVM 字节码?Java 类文件详解

JVM 字节码是 Java 虚拟机运行的紧凑指令集。 了解 .class 文件的运作方式,以及 mod 为什么会重写字节码,以及 ASM 在其中的作用.

TRtrolPublished 1 min read

什么是 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 代码。这个过程并不完美:一些名称和注释会丢失,某些结构比其他结构更难恢复,而混淆的代码会变得不可读。但对于普通的、未混淆的代码,反编译器通常会产生足够接近原始代码的输出,以便理解和使用。

这对于学习游戏的工作原理很有用,但不能替代拥有实际源代码。反编译后的代码仍然是缺乏注释的编译逻辑,并且重建的变量名可能会产生误导。它是一个阅读工具,而不是一个编写工具。

常见问题解答