转载

如何构建你自己的 JVM (2) HelloWorld

凡是过往 皆为序章

之前两篇算是开端, 对解释器有个基本印象, 但是如何与 Java 世界关联起来, 似乎又有些模糊, 此篇正式进入 Java 世界.

按照惯例, 自然是要写个 HelloWorld , 对于构建一个简单的 JVM 来说, HelloWorld 会是个样子呢?

HelloWorld.java

public class HelloWorld {

  public static void main(String[] args) {
    int val = 1;
    System.out.println(val);
  }

}
复制代码

案例如上图, 在控制台输出数字 1 .

[[ 为什么输出 1, 按照惯例不是应该输出 Hello World ? => 涉及到字符串的话, 程序就会复杂许多, 精简起见, 输出 1 已然足够 ]]

0x00 解释器复习

如何构建你自己的 JVM (2) HelloWorld
mini-jvm-1

0x01 指令从哪里来?

若是写 JVM , 那指令自然指的是 字节码指令, 自然是从 class 文件中解析而来.

class 文件

如何生成 class 文件? 针对上面的案例, 可使用 javac 编译得到.

针对案例.

javac HelloWorld.java 
复制代码

当前目录会生成 HelloWorld.class 文件.

class 文件本质上是一个更为紧凑的源码, 以便于机器解析.

如何查看 class 文件内容, 可以使用 JDK 自带工具 javap, javap 有很多选项, 暂时只关注 -c (对代码进行反汇编).

javap -c HelloWorld
复制代码

输出如下, 行号是额外添加的.

1  Compiled from "HelloWorld.java"
2  public class HelloWorld {
3   public HelloWorld();
4     Code:
5        0: aload_0
6        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
7        4: return
8
9   public static void main(java.lang.String[]);
10    Code:
11       0: iconst_1
12       1: istore_1
13       2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
14       5: iload_1
15       6: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
16       9: return
17 }
复制代码

连蒙带猜, 想必也能想到上方 11-16 行对应着源代码的 main 方法体内容. 其他内容可暂时忽略.

指令解释

具体指令可参阅 官方说明

格式说明, [指令位置]: [指令] [指令参数]

e.g

0: iconst_1 , 指令位置 0, 指令为 iconst_1 , 无参数.

案例涉及到的指令说明

  • iconst_1: 将整数 1 push 到操作数栈顶.
  • istore_1: 将操作数栈顶整数推出, 并存放到本地变量表 index 为 1 的位置.
  • getstatic: 获取静态变量, 并 push 到操作数栈顶.
  • iload_1: 取出本地变量表 index 为 1 的整数, 并 push 到操作数栈顶
  • invokevirtual: 调用方法.
  • return: 退出当前方法调用.

解析 class 文件, 获取 main 方法字节码指令

本质上是提供一个方法, 参数为 class 文件名, 结果为 解析后的指令集合.

public static List<Inst> parseInst(String classfilePath) {
  // 实现
} 
复制代码

class 文件格式

官方 The ClassFile Structure 就案例而言, 了解即可.

上代码

List<Inst> insts = new ArrayList<>();
Inst inst = null;
while (len > 0) {
    int code = is.readUnsignedByte();
    switch (code) {
        case 0x03:
            inst = new IConst0();
            break;
        case 0x04:
            inst = new IConst1();
            break;
        case 0x3c:
            inst = new IStore1();
            break;
        case 0x3d:
            inst = new IStore2();
            break;
        case 0x10:
            inst = new Bipush(is.readByte());
            break;
        case 0xa3:
            inst = new IfIcmpGt(is.readShort());
            break;
        case 0x60:
            inst = new Iadd();
            break;
        case 0x84:
            inst = new Iinc(is.readUnsignedByte(), is.readByte());
            break;
        case 0xa7:
            inst = new Goto(is.readShort());
            break;
        case 0x1b:
            inst = new ILoad1();
            break;
        case 0x1c:
            inst = new ILoad2();
            break;
        case 0xb1:
            inst = new Return();
            break;
        case 0xb2:
            is.readUnsignedShort();
            inst = new Getstatic();
            break;
        case 0xb6:
            is.readUnsignedShort();
            inst = new Invokevirtual();
            break;
        default:
            throw new UnsupportedOperationException();
    }
    len -= inst.offset();
    insts.add(inst);
}
复制代码

核心代码如上, 主要是根据不同的情况解析出不同的指令. 并不复杂, 体力活. 对照着官方文档解析即可得到.

0x02 指令有了, 然后呢

与解释器联动起来, 解释上一步解析得到的指令, 由于解释器上篇已实现, 此处就不过多解释, 核心代码如下.

List<Inst> insts = parseInst(path + ".class");
// 由于 jvm 指令有步长的概念, 此处需要转为map.
Map<Integer, Inst> instructions = genInstructions(insts);

// 10 是临时写死, 实际应从 class 文件中解析得到.
Frame frame = new Frame(10, 10);
while (true) {
  int pc = frame.pc;
  Inst inst = instructions.get(pc);
  if (inst == null) {
    break;
  }
  inst.execute(frame);
  if (pc == frame.pc) {
    frame.pc += inst.offset();
  }
}
复制代码

与上篇提到的三个解释器大体相仿.

0x03 方法调用怎么玩

简单来讲, 就是个交换的问题, 用入参(即当前操作数栈的对象), 换一个返回值(放到当前栈顶)或者副作用(比如输入信息).

就案例来讲, 就是消耗掉栈的两个对象, 产生副作用(输出到控制台).

更复杂方法调用, 核心依然是上方的交换, 暂不讨论.

invokevirtual 的简单实现.

public void execute(Frame frame) {
    Object val = frame.operandStack.pop();  // 操作数栈顶, 即为要输出的值
    Object thisObj = frame.operandStack.pop(); // 其次是 System.out 这个静态变量, 暂时忽略实现. 
    System.out.println(val);  // 利用宿主 JVM 输出. 
}
复制代码

0x04 Hello World 成就达成

$ java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo HelloWorld
=> 1
复制代码

图解执行流程.

如何构建你自己的 JVM (2) HelloWorld
int-hw

0x05 更复杂一点的例子

求 1,2,3..100 的和, 并输出.

Sum100.java

public class Sum100 {
  public static void main(String[] args) {
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
      sum += i;
    }
    System.out.println(sum);
  }
}
复制代码

编译并解释

$ javac Sum100.java

$ java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo Sum100
=> 5050
复制代码

源码及使用

源码托管于 github, commit 传送门

git clone https://github.com/guxingke/demo.git
cd demo/interpreter-demo 
mvn package

# HelloWorld
java HelloWorld.java
java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo Sum100

javac Sum100.java
java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo Sum100
复制代码

0x06 小结

承接上篇, 使用单文件(300行代码)实现了一个简单的 JVM, 把 Java 世界 class 文件内的字节码指令解析出来, 并解释. 对于有兴趣入坑的同学来讲, 应该是个不错的案例.

0x07 预告

如果想了解更多, 可以关注 mini-jvm 项目, 以上文提到的解释器为核心, Java 的一些语言特性基本实现.

0x08 FAQ

系列还会有下一篇? 暂时不会有了, 解释器的核心已经就位, 一些语言特性就是逐步迭代了.

0x09 相关链接

  • 如何构建你自己的 JVM (0) 概述
  • 如何构建你自己的 JVM (1) 解释器
  • 如何构建你自己的 JVM (2) Hello World
  • Demo 源码地址
  • 使用 JDK8 实现 JVM

0x10 尾记

系列告一段落, 暂时不会更新了,个人杂记,难免会有疏漏错误, 如有兴趣, 有问题, 请反馈于我(评论,issue,邮件均欢迎). 再次感谢阅读.

原文  https://juejin.im/post/5e0c4b31f265da5d54294178
正文到此结束
Loading...