我将首先假设您主要(如果不是唯一地)对字节码解释器或类似的东西感兴趣(因为您的问题似乎是这样假设的)。直接从源代码(原始或标记化形式)工作的解释器是相当不同的。
对于典型的字节码解释器,您基本上设计了一些理想化的机器。为此目的,基于堆栈(或至少面向堆栈)的设计非常普遍,所以让我们假设一下。
因此,首先让我们考虑为操作码选择 4 位。这里很大程度上取决于我们想要支持多少数据格式,以及我们是否将其包含在操作码的 4 位中。只是为了争论,我们假设虚拟机本身支持的基本数据类型是 8 位和 64 位整数(也可以用于寻址),以及 32 位和 64 位浮点数。
对于整数,我们至少需要支持:加法、减法、乘法、除法、和、或、异或、非、否定、比较、测试、左/右移位/旋转(逻辑和算术类型的右移位),加载和存储。浮点将支持相同的算术运算,但删除了逻辑/按位运算。我们还需要一些分支/跳转操作(无条件跳转、零跳转、非零跳转等)对于堆栈机器,我们可能还需要至少一些面向堆栈的指令(push、pop、dupe,可能旋转等)
这为我们提供了数据类型的两位字段,以及操作码字段的至少 5(很可能 6)位。我们可能希望只有一条跳转指令和几个位来指定可应用于任何指令的条件执行,而不是条件跳转是特殊指令。我们还非常需要指定至少一些寻址模式:
- 可选:小立即数(指令本身包含 N 位数据)
- 大立即数(指令后 64 位字中的数据)
- 隐含的(堆栈顶部的操作数)
- 绝对(在指令后的 64 位中指定的地址)
- 相对的(在或遵循指令中指定的偏移量)
我已尽我最大的努力将这里的所有内容保持在合理范围内——您可能需要更多来提高效率。
无论如何,在这样的模型中,对象的值只是内存中的一些位置。同样,字符串只是内存中的一些 8 位整数序列。几乎所有对象/字符串的操作都是通过堆栈完成的。例如,假设您有一些类 A 和 B 定义如下:
class A {
int x;
int y;
};
class B {
int a;
int b;
};
...还有一些代码,例如:
A a {1, 2};
B b {3, 4};
a.x += b.a;
初始化将意味着可执行文件中的值加载到分配给 a 和 b 的内存位置。然后添加可以产生类似这样的代码:
push immediate a.x // put &a.x on top of stack
dupe // copy address to next lower stack position
load // load value from a.x
push immediate b.a // put &b.a on top of stack
load // load value from b.a
add // add two values
store // store back to a.x using address placed on stack with `dupe`
假设每条指令都有一个字节,那么整个序列大约有 23 个字节,其中 16 个字节是地址。如果我们使用 32 位寻址而不是 64 位,我们可以减少 8 个字节(即总共 15 个字节)。
要记住的最明显的事情是,由典型的字节码解释器(或类似的)实现的虚拟机与硬件实现的“真实”机器并没有太大的不同。您可能会添加一些对您尝试实现的模型很重要的指令(例如,JVM 包含直接支持其安全模型的指令),或者如果您只想支持不支持的语言,您可能会省略一些指令包括它们(例如,xor
如果你真的想的话,我想你可以省略一些)。您还需要决定要支持哪种类型的虚拟机。我上面描述的是面向堆栈的,但如果你愿意,你当然可以做一个面向寄存器的机器。
无论哪种方式,大多数对象访问、字符串存储等都归结为它们是内存中的位置。机器将从这些位置检索数据到堆栈/寄存器中,进行适当的操作,并存储回目标对象的位置。