如何構建你本身的 JVM (2) HelloWorld

凡是過往 皆爲序章html

0x00 解釋器複習0x01 指令從哪裏來?class 文件指令解釋解析 class 文件, 獲取 main 方法字節碼指令class 文件格式上代碼0x02 指令有了, 而後呢0x03 方法調用怎麼玩invokevirtual 的簡單實現.0x04 Hello World 成就達成圖解執行流程.0x05 更復雜一點的例子源碼及使用0x06 小結0x07 預告0x08 FAQ0x09 相關連接0x10 尾記java

以前兩篇算是開端, 對解釋器有個基本印象, 可是如何與 Java 世界關聯起來, 彷佛又有些模糊, 此篇正式進入 Java 世界.
按照慣例, 天然是要寫個 HelloWorld , 對於構建一個簡單的 JVM 來講, HelloWorld 會是個樣子呢? git

HelloWorld.javagithub

public class HelloWorld {

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

}
複製代碼

案例如上圖, 在控制檯輸出數字 1 . web

[[ 爲何輸出 1, 按照慣例不是應該輸出 Hello World ? => 涉及到字符串的話, 程序就會複雜許多, 精簡起見, 輸出 1 已然足夠 ]]bash

0x00 解釋器複習

mini-jvm-1
mini-jvm-1

0x01 指令從哪裏來?

如果寫 JVM , 那指令天然指的是 字節碼指令, 天然是從 class 文件中解析而來. oracle

class 文件

如何生成 class 文件? 針對上面的案例, 可以使用 javac 編譯獲得.
針對案例. app

javac HelloWorld.java 
複製代碼

當前目錄會生成 HelloWorld.class 文件. jvm

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(1010);
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
複製代碼

圖解執行流程.

int-hw
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 相關連接

0x10 尾記

系列告一段落, 暫時不會更新了,我的雜記,不免會有疏漏錯誤, 若有興趣, 有問題, 請反饋於我(評論,issue,郵件均歡迎). 再次感謝閱讀.

相關文章
相關標籤/搜索